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.
- umbermark/__init__.py +3 -0
- umbermark/checks/__init__.py +88 -0
- umbermark/checks/auto_updates_configured.py +149 -0
- umbermark/checks/dns_resolver_sanity.py +47 -0
- umbermark/checks/fail2ban_enabled.py +67 -0
- umbermark/checks/failed_login_anomaly.py +48 -0
- umbermark/checks/failed_systemd_units.py +41 -0
- umbermark/checks/firewall_protective.py +247 -0
- umbermark/checks/journald_persistence.py +55 -0
- umbermark/checks/kernel_age.py +74 -0
- umbermark/checks/listen_vs_firewall_delta.py +136 -0
- umbermark/checks/mac_framework_enforcing.py +97 -0
- umbermark/checks/pending_security_updates.py +188 -0
- umbermark/checks/public_listen_anomalies.py +200 -0
- umbermark/checks/reboot_pending.py +71 -0
- umbermark/checks/repo_gpg_trust.py +97 -0
- umbermark/checks/root_disk_free.py +46 -0
- umbermark/checks/ssh_non_default_port.py +62 -0
- umbermark/checks/ssh_password_auth.py +71 -0
- umbermark/checks/ssh_permit_root.py +59 -0
- umbermark/checks/ssh_weak_ciphers.py +85 -0
- umbermark/checks/sudo_requires_authentication.py +41 -0
- umbermark/checks/sudoers_nopasswd.py +111 -0
- umbermark/checks/swap_pressure.py +51 -0
- umbermark/checks/time_sync.py +55 -0
- umbermark/checks/tmp_hardening.py +45 -0
- umbermark/checks/user_password_aging.py +95 -0
- umbermark/checks/world_writable_suid_audit.py +171 -0
- umbermark/cli/__init__.py +4 -0
- umbermark/cli/app.py +23 -0
- umbermark/cli/auth.py +44 -0
- umbermark/cli/commands/__init__.py +0 -0
- umbermark/cli/commands/doctor.py +16 -0
- umbermark/cli/commands/login.py +80 -0
- umbermark/cli/commands/push.py +54 -0
- umbermark/cli/commands/report.py +26 -0
- umbermark/cli/commands/scan.py +77 -0
- umbermark/cli/commands/scan_local.py +72 -0
- umbermark/cli/commands/scan_wsl.py +35 -0
- umbermark/cli/commands/version.py +11 -0
- umbermark/cli/config.py +13 -0
- umbermark/core/__init__.py +0 -0
- umbermark/core/accepted.py +61 -0
- umbermark/core/access.py +896 -0
- umbermark/core/capture.py +228 -0
- umbermark/core/check.py +82 -0
- umbermark/core/checkin.py +89 -0
- umbermark/core/crypto.py +15 -0
- umbermark/core/duration.py +45 -0
- umbermark/core/engine.py +74 -0
- umbermark/core/external/__init__.py +6 -0
- umbermark/core/external/check.py +54 -0
- umbermark/core/external/checks/__init__.py +31 -0
- umbermark/core/external/checks/exposed_datastores.py +55 -0
- umbermark/core/external/checks/http_security_headers.py +249 -0
- umbermark/core/external/checks/legacy_services.py +52 -0
- umbermark/core/external/checks/open_management_ports.py +65 -0
- umbermark/core/external/checks/ssh_alt_ports.py +77 -0
- umbermark/core/external/checks/ssh_weak_algorithms.py +93 -0
- umbermark/core/external/checks/tls_hygiene.py +107 -0
- umbermark/core/external/engine.py +89 -0
- umbermark/core/external/ownership.py +607 -0
- umbermark/core/external/probe.py +103 -0
- umbermark/core/external/socket_probe.py +400 -0
- umbermark/core/firewall.py +713 -0
- umbermark/core/fleet.py +92 -0
- umbermark/core/history.py +235 -0
- umbermark/core/html_report.py +306 -0
- umbermark/core/keyenc.py +42 -0
- umbermark/core/labels.py +29 -0
- umbermark/core/lifecycle.py +100 -0
- umbermark/core/logredact.py +44 -0
- umbermark/core/partial.py +57 -0
- umbermark/core/pdf/__init__.py +0 -0
- umbermark/core/pdf/renderer.py +26 -0
- umbermark/core/pdf/weasyprint_renderer.py +18 -0
- umbermark/core/platform.py +73 -0
- umbermark/core/redact.py +75 -0
- umbermark/core/report.py +25 -0
- umbermark/core/scoring.py +105 -0
- umbermark/core/settings.py +238 -0
- umbermark/core/short_id.py +27 -0
- umbermark/core/signing.py +24 -0
- umbermark/core/ssh.py +156 -0
- umbermark/core/ssh_fingerprint.py +16 -0
- umbermark/core/tiers.py +128 -0
- umbermark/core/webhooks.py +177 -0
- umbermark/core/wording.py +371 -0
- umbermark_cli-1.0.0rc1.dist-info/METADATA +11 -0
- umbermark_cli-1.0.0rc1.dist-info/RECORD +93 -0
- umbermark_cli-1.0.0rc1.dist-info/WHEEL +5 -0
- umbermark_cli-1.0.0rc1.dist-info/entry_points.txt +2 -0
- umbermark_cli-1.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Outbound alert webhooks: URL validation, SSRF-safe delivery, message bodies (UMBR-66 alert channels).
|
|
2
|
+
|
|
3
|
+
Three kinds of destination, all customer-configured:
|
|
4
|
+
webhook any https URL; gets JSON, signed with HMAC-SHA256 (X-Umbermark-Signature: sha256=<hex>)
|
|
5
|
+
slack https://hooks.slack.com/services/... ({"text": ...})
|
|
6
|
+
discord https://discord.com/api/webhooks/... ({"content": ...}; discordapp.com accepted)
|
|
7
|
+
|
|
8
|
+
SSRF rules (every send, not only on save):
|
|
9
|
+
* https only, port 443 only for generic webhooks, no credentials in the URL, no IP-literal hosts;
|
|
10
|
+
* the host must resolve and EVERY address must be public (core.external.ownership.resolve_public_ips refuses
|
|
11
|
+
loopback, RFC1918, CGNAT/tailnet, link-local/metadata, multicast and the rest);
|
|
12
|
+
* the TCP connection goes to the address that was checked (pinned), with SNI and certificate checks against
|
|
13
|
+
the hostname, so a DNS answer cannot change between the check and the connect;
|
|
14
|
+
* redirects are never followed (a 3xx counts as a failed delivery);
|
|
15
|
+
* short timeouts and a capped response read.
|
|
16
|
+
The host's egress firewall (deploy/umk-egress.nft) blocks the same ranges for the app's uid as a second layer.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
22
|
+
import http.client
|
|
23
|
+
import json
|
|
24
|
+
import socket
|
|
25
|
+
import ssl
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from urllib.parse import urlsplit
|
|
28
|
+
|
|
29
|
+
KINDS: tuple[str, ...] = ("webhook", "slack", "discord")
|
|
30
|
+
KIND_LABELS = {"webhook": "Webhook (JSON)", "slack": "Slack", "discord": "Discord"}
|
|
31
|
+
MAX_URL_LEN = 500
|
|
32
|
+
TIMEOUT_SECONDS = 8
|
|
33
|
+
_SLACK_HOSTS = {"hooks.slack.com"}
|
|
34
|
+
_DISCORD_HOSTS = {"discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WebhookError(ValueError):
|
|
38
|
+
"""The URL is not acceptable, or delivery was refused before any bytes were sent."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class Target:
|
|
43
|
+
host: str
|
|
44
|
+
port: int
|
|
45
|
+
path: str # path + query, as sent
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate(kind: str, url: str) -> Target:
|
|
49
|
+
"""Shape checks only (no DNS). Raises WebhookError with a message fit for the form."""
|
|
50
|
+
if kind not in KINDS:
|
|
51
|
+
raise WebhookError("Choose webhook, Slack or Discord")
|
|
52
|
+
url = (url or "").strip()
|
|
53
|
+
if not url or len(url) > MAX_URL_LEN:
|
|
54
|
+
raise WebhookError(f"Paste the full https URL (at most {MAX_URL_LEN} characters)")
|
|
55
|
+
try:
|
|
56
|
+
u = urlsplit(url)
|
|
57
|
+
port = u.port or 443
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise WebhookError("That is not a valid URL") from exc
|
|
60
|
+
host = (u.hostname or "").lower().rstrip(".")
|
|
61
|
+
if u.scheme != "https" or not host:
|
|
62
|
+
raise WebhookError("The URL must start with https://")
|
|
63
|
+
if u.username or u.password:
|
|
64
|
+
raise WebhookError("The URL must not contain a user name or password")
|
|
65
|
+
if port != 443:
|
|
66
|
+
raise WebhookError("Only the standard https port (443) is allowed")
|
|
67
|
+
if _is_ip_literal(host):
|
|
68
|
+
raise WebhookError("Use a host name, not an IP address")
|
|
69
|
+
path = (u.path or "/") + (f"?{u.query}" if u.query else "")
|
|
70
|
+
if kind == "slack" and (host not in _SLACK_HOSTS or not u.path.startswith("/services/")):
|
|
71
|
+
raise WebhookError("A Slack incoming-webhook URL starts with https://hooks.slack.com/services/")
|
|
72
|
+
if kind == "discord" and (host not in _DISCORD_HOSTS or not u.path.startswith("/api/webhooks/")):
|
|
73
|
+
raise WebhookError("A Discord webhook URL starts with https://discord.com/api/webhooks/")
|
|
74
|
+
return Target(host, port, path)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def masked(url: str) -> str:
|
|
78
|
+
"""What the page shows: scheme, host and the last 4 characters (the rest of a Slack/Discord URL is a secret)."""
|
|
79
|
+
try:
|
|
80
|
+
u = urlsplit(url)
|
|
81
|
+
tail = url[-4:] if len(url) > 12 else ""
|
|
82
|
+
return f"https://{u.hostname}/…{tail}"
|
|
83
|
+
except ValueError:
|
|
84
|
+
return "(invalid URL)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_ip_literal(host: str) -> bool:
|
|
88
|
+
import ipaddress
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
ipaddress.ip_address(host.strip("[]"))
|
|
92
|
+
return True
|
|
93
|
+
except ValueError:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve(host: str) -> str:
|
|
98
|
+
"""The first public address of `host`; WebhookError unless every address is public."""
|
|
99
|
+
from umbermark.core.external.ownership import BlockedTarget, resolve_public_ips
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return resolve_public_ips(host)[0]
|
|
103
|
+
except BlockedTarget as exc:
|
|
104
|
+
raise WebhookError("That host does not resolve to a public address") from exc
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _PinnedHTTPS(http.client.HTTPSConnection):
|
|
108
|
+
"""HTTPS to a fixed IP while SNI and the certificate check use the hostname."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, host: str, ip: str, port: int, timeout: float):
|
|
111
|
+
super().__init__(host, port, timeout=timeout, context=ssl.create_default_context())
|
|
112
|
+
self._ip = ip
|
|
113
|
+
|
|
114
|
+
def connect(self): # noqa: D401 - http.client hook
|
|
115
|
+
sock = socket.create_connection((self._ip, self.port), self.timeout)
|
|
116
|
+
self.sock = self._context.wrap_socket(sock, server_hostname=self.host)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def body_for(kind: str, event: dict) -> bytes:
|
|
120
|
+
"""The request body for one alert event (see `drop_event`)."""
|
|
121
|
+
text = event["text"]
|
|
122
|
+
if kind == "slack":
|
|
123
|
+
return json.dumps({"text": text}).encode()
|
|
124
|
+
if kind == "discord":
|
|
125
|
+
return json.dumps({"content": text[:1900], "allowed_mentions": {"parse": []}}).encode()
|
|
126
|
+
return json.dumps({k: v for k, v in event.items() if k != "text"} | {"text": text}, sort_keys=True).encode()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def signature(secret: str, body: bytes) -> str:
|
|
130
|
+
return "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def send(kind: str, url: str, event: dict, *, secret: str | None = None, resolve=None,
|
|
134
|
+
connection_factory=None) -> int:
|
|
135
|
+
"""Deliver one event. Returns the HTTP status (2xx = delivered). Raises WebhookError before sending when the
|
|
136
|
+
URL or its addresses are not acceptable; OSError/ssl errors propagate for the caller to record."""
|
|
137
|
+
t = validate(kind, url)
|
|
138
|
+
ip = (resolve or _resolve)(t.host)
|
|
139
|
+
connection_factory = connection_factory or _PinnedHTTPS
|
|
140
|
+
body = body_for(kind, event)
|
|
141
|
+
headers = {"Content-Type": "application/json", "User-Agent": "Umbermark-Alerts/1"}
|
|
142
|
+
if kind == "webhook" and secret:
|
|
143
|
+
headers["X-Umbermark-Signature"] = signature(secret, body)
|
|
144
|
+
conn = connection_factory(t.host, ip, t.port, TIMEOUT_SECONDS)
|
|
145
|
+
try:
|
|
146
|
+
conn.request("POST", t.path, body=body, headers=headers)
|
|
147
|
+
resp = conn.getresponse()
|
|
148
|
+
resp.read(4096) # capped; never kept
|
|
149
|
+
return resp.status
|
|
150
|
+
finally:
|
|
151
|
+
conn.close()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def drop_event(*, label: str, server_id: int, previous: int, current: int, threshold: int, report_url: str) -> dict:
|
|
155
|
+
"""The one alert Umbermark sends today: a score drop at or past the server's threshold."""
|
|
156
|
+
delta = previous - current
|
|
157
|
+
return {
|
|
158
|
+
"event": "score_drop", "server": {"id": server_id, "label": label},
|
|
159
|
+
"score": {"previous": previous, "current": current, "drop": delta, "threshold": threshold},
|
|
160
|
+
"report_url": report_url,
|
|
161
|
+
"text": f"Umbermark: {label} score dropped {delta} points ({previous} -> {current}). {report_url}",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def missed_event(*, label: str, server_id: int, last_checkin_at: str | None, server_url: str) -> dict:
|
|
166
|
+
"""A check-in server did not check in within its interval plus grace (UMBR-66)."""
|
|
167
|
+
last = (last_checkin_at or "never")[:16].replace("T", " ")
|
|
168
|
+
return {
|
|
169
|
+
"event": "missed_checkin", "server": {"id": server_id, "label": label}, "score": None,
|
|
170
|
+
"last_checkin_at": last_checkin_at, "report_url": server_url,
|
|
171
|
+
"text": f"Umbermark: {label} missed its scheduled check-in (last check-in: {last} UTC). {server_url}",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sample_event(label: str = "test") -> dict:
|
|
176
|
+
return {"event": "test", "server": None, "score": None, "report_url": None,
|
|
177
|
+
"text": f"Umbermark: test alert for channel '{label}'. If you can read this, alerts reach you."}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Report wording: how a check's result reads to a human.
|
|
2
|
+
|
|
3
|
+
A check has three strings, kept apart from the check classes so existing
|
|
4
|
+
stored reports (which only carry ``check_id`` + the old ``title``) pick up new
|
|
5
|
+
wording at render time with no migration:
|
|
6
|
+
|
|
7
|
+
* ``control`` neutral name of the thing being checked ("SSH cipher strength").
|
|
8
|
+
Used on SEO pages, the checks index and anywhere no verdict is
|
|
9
|
+
implied.
|
|
10
|
+
* ``pass_text`` what is right on the server (shown on PASS rows).
|
|
11
|
+
* ``fail_text`` what is wrong on the server (shown on FAIL / WARN rows and in
|
|
12
|
+
"What cost you points"). Present tense, no "should".
|
|
13
|
+
|
|
14
|
+
Rule of thumb: ``fail_text`` is a finding, never a control name. A reader who
|
|
15
|
+
only sees the fail text must never think the *good* thing is the problem.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import datetime as dt
|
|
20
|
+
import html
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from markupsafe import Markup
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Wording:
|
|
29
|
+
control: str
|
|
30
|
+
pass_text: str
|
|
31
|
+
fail_text: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
WORDING: dict[str, Wording] = {
|
|
35
|
+
# --- identity / auth ---------------------------------------------------
|
|
36
|
+
"ssh_permit_root_login": Wording(
|
|
37
|
+
"SSH root login",
|
|
38
|
+
"SSH does not allow direct root login",
|
|
39
|
+
"SSH allows direct root login",
|
|
40
|
+
),
|
|
41
|
+
"ssh_password_authentication": Wording(
|
|
42
|
+
"SSH password login",
|
|
43
|
+
"SSH only accepts keys; password login is off",
|
|
44
|
+
"SSH still accepts password logins",
|
|
45
|
+
),
|
|
46
|
+
"ssh_weak_ciphers": Wording(
|
|
47
|
+
"SSH cipher strength",
|
|
48
|
+
"SSH only accepts strong ciphers, MACs and key exchange",
|
|
49
|
+
"SSH still accepts weak ciphers, MACs or key exchange",
|
|
50
|
+
),
|
|
51
|
+
"ssh_non_default_port": Wording(
|
|
52
|
+
"SSH port",
|
|
53
|
+
"SSH listens on a non-default port",
|
|
54
|
+
"SSH listens on the default port 22",
|
|
55
|
+
),
|
|
56
|
+
"sudoers_nopasswd": Wording(
|
|
57
|
+
"Passwordless sudo rules",
|
|
58
|
+
"No risky passwordless sudo rules",
|
|
59
|
+
"Passwordless sudo (NOPASSWD) rules are in place",
|
|
60
|
+
),
|
|
61
|
+
"sudo_requires_authentication": Wording(
|
|
62
|
+
"sudo authentication",
|
|
63
|
+
"sudo asks users to authenticate",
|
|
64
|
+
"sudo lets users in without authenticating",
|
|
65
|
+
),
|
|
66
|
+
"user_password_aging": Wording(
|
|
67
|
+
"Password aging policy",
|
|
68
|
+
"Account passwords have an expiry policy",
|
|
69
|
+
"Some accounts have passwords that never expire",
|
|
70
|
+
),
|
|
71
|
+
"failed_login_anomaly": Wording(
|
|
72
|
+
"Failed SSH logins (7 days)",
|
|
73
|
+
"Few failed SSH logins in the last 7 days",
|
|
74
|
+
"Many failed SSH login attempts in the last 7 days",
|
|
75
|
+
),
|
|
76
|
+
"fail2ban_enabled": Wording(
|
|
77
|
+
"fail2ban brute-force protection",
|
|
78
|
+
"fail2ban is running with active jails",
|
|
79
|
+
"fail2ban is missing, stopped or has no active jails",
|
|
80
|
+
),
|
|
81
|
+
# --- patching ----------------------------------------------------------
|
|
82
|
+
"kernel_age": Wording(
|
|
83
|
+
"Kernel age",
|
|
84
|
+
"The running kernel is recent",
|
|
85
|
+
"The running kernel is more than 6 months old",
|
|
86
|
+
),
|
|
87
|
+
"pending_security_updates": Wording(
|
|
88
|
+
"Security updates",
|
|
89
|
+
"No security updates are waiting",
|
|
90
|
+
"Security updates are waiting to be installed",
|
|
91
|
+
),
|
|
92
|
+
"reboot_pending": Wording(
|
|
93
|
+
"Pending reboot",
|
|
94
|
+
"No reboot is pending",
|
|
95
|
+
"A reboot is pending, so updates are not active yet",
|
|
96
|
+
),
|
|
97
|
+
"auto_updates_configured": Wording(
|
|
98
|
+
"Automatic security updates",
|
|
99
|
+
"Automatic security updates are turned on",
|
|
100
|
+
"Automatic security updates are not set up",
|
|
101
|
+
),
|
|
102
|
+
"repo_gpg_trust": Wording(
|
|
103
|
+
"Package signing keys",
|
|
104
|
+
"Package repositories have signing keys configured",
|
|
105
|
+
"Package repositories have no signing keys configured",
|
|
106
|
+
),
|
|
107
|
+
# --- network -----------------------------------------------------------
|
|
108
|
+
"firewall_protective": Wording(
|
|
109
|
+
"Host firewall",
|
|
110
|
+
"The host firewall blocks unsolicited inbound traffic",
|
|
111
|
+
"The host firewall is off or lets inbound traffic through",
|
|
112
|
+
),
|
|
113
|
+
"time_sync": Wording(
|
|
114
|
+
"Clock synchronisation",
|
|
115
|
+
"The clock is synced and an NTP service is running",
|
|
116
|
+
"The clock is not synced or no NTP service is running",
|
|
117
|
+
),
|
|
118
|
+
"dns_resolver_sanity": Wording(
|
|
119
|
+
"DNS resolver",
|
|
120
|
+
"DNS is configured and answering",
|
|
121
|
+
"DNS is misconfigured or not answering",
|
|
122
|
+
),
|
|
123
|
+
# --- filesystem / services / logging / hardening -----------------------
|
|
124
|
+
"root_disk_free": Wording(
|
|
125
|
+
"Root disk space",
|
|
126
|
+
"The root disk has plenty of free space",
|
|
127
|
+
"The root disk is nearly full",
|
|
128
|
+
),
|
|
129
|
+
"tmp_hardening": Wording(
|
|
130
|
+
"/tmp mount options",
|
|
131
|
+
"/tmp is mounted noexec and nosuid",
|
|
132
|
+
"/tmp allows running programs or setuid files",
|
|
133
|
+
),
|
|
134
|
+
"failed_systemd_units": Wording(
|
|
135
|
+
"Failed systemd units",
|
|
136
|
+
"No systemd units have failed",
|
|
137
|
+
"One or more systemd units have failed",
|
|
138
|
+
),
|
|
139
|
+
"swap_pressure": Wording(
|
|
140
|
+
"Swap pressure",
|
|
141
|
+
"The server is not swapping memory to disk",
|
|
142
|
+
"The server is swapping memory to disk",
|
|
143
|
+
),
|
|
144
|
+
"journald_persistence": Wording(
|
|
145
|
+
"Persistent system logs",
|
|
146
|
+
"System logs survive a reboot",
|
|
147
|
+
"System logs are lost on reboot",
|
|
148
|
+
),
|
|
149
|
+
"mac_framework_enforcing": Wording(
|
|
150
|
+
"AppArmor / SELinux",
|
|
151
|
+
"AppArmor or SELinux is enforcing",
|
|
152
|
+
"AppArmor or SELinux is off or not enforcing",
|
|
153
|
+
),
|
|
154
|
+
"public_listen_anomalies": Wording(
|
|
155
|
+
"Public listening ports",
|
|
156
|
+
"Only expected ports listen on public addresses",
|
|
157
|
+
"Unexpected ports listen on public addresses",
|
|
158
|
+
),
|
|
159
|
+
"listen_vs_firewall_delta": Wording(
|
|
160
|
+
"Firewall cover for listening ports",
|
|
161
|
+
"Public listening ports are covered by a deny-by-default firewall",
|
|
162
|
+
"Public ports are reachable only because the firewall allows everything by default",
|
|
163
|
+
),
|
|
164
|
+
"world_writable_suid_audit": Wording(
|
|
165
|
+
"World-writable files and SUID binaries",
|
|
166
|
+
"No unexpected world-writable files or SUID binaries",
|
|
167
|
+
"Unexpected world-writable files or SUID binaries exist",
|
|
168
|
+
),
|
|
169
|
+
# --- external (outside-in) --------------------------------------------
|
|
170
|
+
"external_exposed_datastores": Wording(
|
|
171
|
+
"Exposed data stores",
|
|
172
|
+
"No database or cache is reachable from the internet",
|
|
173
|
+
"A database or cache is reachable from the internet",
|
|
174
|
+
),
|
|
175
|
+
"external_legacy_plaintext_services": Wording(
|
|
176
|
+
"Legacy plaintext services",
|
|
177
|
+
"No telnet, FTP or r-services are reachable from the internet",
|
|
178
|
+
"A plaintext service (telnet, FTP or r-service) is reachable from the internet",
|
|
179
|
+
),
|
|
180
|
+
"external_open_management_ports": Wording(
|
|
181
|
+
"Remote-management ports",
|
|
182
|
+
"No remote-management ports are reachable from the internet",
|
|
183
|
+
"Remote-management ports (RDP, VNC, Docker, etcd...) are reachable from the internet",
|
|
184
|
+
),
|
|
185
|
+
"external_ssh_weak_algorithms": Wording(
|
|
186
|
+
"SSH algorithms (outside view)",
|
|
187
|
+
"SSH offers only modern algorithms to the internet",
|
|
188
|
+
"SSH offers weak or deprecated algorithms to the internet",
|
|
189
|
+
),
|
|
190
|
+
"external_ssh_alt_ports": Wording(
|
|
191
|
+
"SSH algorithms on alternate ports (outside view)",
|
|
192
|
+
"SSH on alternate ports offers only modern algorithms to the internet",
|
|
193
|
+
"SSH on an alternate port offers weak or deprecated algorithms to the internet",
|
|
194
|
+
),
|
|
195
|
+
"external_tls_hygiene": Wording(
|
|
196
|
+
"TLS and certificate",
|
|
197
|
+
"TLS is modern and the certificate is valid",
|
|
198
|
+
"TLS uses old protocols or the certificate has a problem",
|
|
199
|
+
),
|
|
200
|
+
"external_http_security_headers": Wording(
|
|
201
|
+
"HTTP security headers",
|
|
202
|
+
"HTTPS responses set the core security headers",
|
|
203
|
+
"HTTPS responses are missing or weakly set core security headers",
|
|
204
|
+
),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def wording_for(check_id: str, fallback_title: str = "") -> Wording:
|
|
209
|
+
"""Wording for a check; unknown ids (old/foreign payloads) fall back to the
|
|
210
|
+
stored title for all three strings."""
|
|
211
|
+
w = WORDING.get(check_id)
|
|
212
|
+
if w is not None:
|
|
213
|
+
return w
|
|
214
|
+
t = fallback_title or check_id
|
|
215
|
+
return Wording(t, t, t)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def control_name(check_id: str, fallback_title: str = "") -> str:
|
|
219
|
+
return wording_for(check_id, fallback_title).control
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- skip reasons -----------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def skip_note(reason: str) -> str:
|
|
225
|
+
"""Public-safe, human reason a check was skipped (no raw command output)."""
|
|
226
|
+
r = (reason or "").lower()
|
|
227
|
+
if not r:
|
|
228
|
+
return "Skipped: no data was available for this check."
|
|
229
|
+
if "ownership" in r or "re-verify" in r or "not probed (unverified)" in r:
|
|
230
|
+
return "Skipped: this check only runs after you verify you own the server."
|
|
231
|
+
if "not applicable" in r or "no protective firewall" in r or "non-systemd" in r:
|
|
232
|
+
return "Skipped: not applicable to this server."
|
|
233
|
+
if "check raised" in r:
|
|
234
|
+
return "Skipped: the check hit an internal error."
|
|
235
|
+
if "requires root" in r or "needs root" in r:
|
|
236
|
+
return "Skipped: needs root access, which this scan did not have."
|
|
237
|
+
if "no tls service" in r or "no https service" in r or "kexinit" in r:
|
|
238
|
+
return "Skipped: the service did not answer on the expected port."
|
|
239
|
+
return "Skipped: the data was not available on this server."
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# --- presentation -------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def points_lost(verdict: str, weight: int) -> int:
|
|
245
|
+
if verdict == "fail":
|
|
246
|
+
return int(weight)
|
|
247
|
+
if verdict == "warn":
|
|
248
|
+
return int(weight) // 2
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _val(x):
|
|
253
|
+
return x.value if hasattr(x, "value") else x
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def annotate_finding(f: dict) -> dict:
|
|
257
|
+
"""Add display fields to a finding dict (in place) and return it."""
|
|
258
|
+
verdict = str(_val(f.get("verdict", "")))
|
|
259
|
+
w = wording_for(f.get("check_id", ""), f.get("title", ""))
|
|
260
|
+
f["control"] = w.control
|
|
261
|
+
if verdict == "pass":
|
|
262
|
+
f["text"] = w.pass_text
|
|
263
|
+
elif verdict in ("fail", "warn"):
|
|
264
|
+
f["text"] = w.fail_text
|
|
265
|
+
else:
|
|
266
|
+
f["text"] = w.control
|
|
267
|
+
lost = points_lost(verdict, f.get("weight", 0))
|
|
268
|
+
f["points_lost"] = lost
|
|
269
|
+
f["informational"] = verdict == "warn" and lost == 0
|
|
270
|
+
if verdict == "skip":
|
|
271
|
+
f["skip_note"] = f.get("skip_note") or skip_note(f.get("skip_reason", ""))
|
|
272
|
+
f["remediation_html"] = remediation_html(f.get("remediation", ""))
|
|
273
|
+
return f
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def present_payload(payload: dict) -> dict:
|
|
277
|
+
"""Return the payload with display fields added to findings and the
|
|
278
|
+
deduction list split into points-costing vs informational (0-point) rows."""
|
|
279
|
+
findings = [annotate_finding(dict(f)) for f in payload.get("findings", [])]
|
|
280
|
+
out = dict(payload)
|
|
281
|
+
out["findings"] = findings
|
|
282
|
+
by_id = {f.get("check_id"): f for f in findings}
|
|
283
|
+
cost, info = [], []
|
|
284
|
+
for d in payload.get("result", {}).get("deductions", []):
|
|
285
|
+
d = dict(d)
|
|
286
|
+
f = by_id.get(d.get("check_id"))
|
|
287
|
+
d["text"] = f["text"] if f else wording_for(d.get("check_id", ""), d.get("title", "")).fail_text
|
|
288
|
+
(cost if d.get("points_lost", 0) > 0 else info).append(d)
|
|
289
|
+
# Zero-point findings that never produced a Deduction row (e.g. weight 0).
|
|
290
|
+
seen = {d.get("check_id") for d in cost + info}
|
|
291
|
+
for f in findings:
|
|
292
|
+
if f["informational"] and f.get("check_id") not in seen:
|
|
293
|
+
info.append({"check_id": f.get("check_id"), "severity": _val(f.get("severity", "")),
|
|
294
|
+
"verdict": "warn", "points_lost": 0, "text": f["text"]})
|
|
295
|
+
out["cost_deductions"] = cost
|
|
296
|
+
out["info_deductions"] = info
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --- remediation text -> safe HTML -------------------------------------------
|
|
301
|
+
|
|
302
|
+
_BULLET = re.compile(r"^\s{1,3}(?:-|\d+\.)\s+(.*)$")
|
|
303
|
+
_BACKTICK = re.compile(r"`([^`]+)`")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _inline(s: str) -> str:
|
|
307
|
+
return _BACKTICK.sub(lambda m: "<code>" + m.group(1) + "</code>", html.escape(s, quote=False))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def remediation_html(text: str) -> Markup:
|
|
311
|
+
"""Render a remediation string: indented (4+ spaces) lines become <pre><code>
|
|
312
|
+
blocks, " - " / " 1. " lines become list items, hard-wrapped prose lines
|
|
313
|
+
are joined into paragraphs, `x` becomes <code>x</code>. Everything is
|
|
314
|
+
HTML-escaped first."""
|
|
315
|
+
if not text:
|
|
316
|
+
return Markup("")
|
|
317
|
+
out: list[str] = []
|
|
318
|
+
para: list[str] = []
|
|
319
|
+
code: list[str] = []
|
|
320
|
+
items: list[str] = []
|
|
321
|
+
|
|
322
|
+
def flush() -> None:
|
|
323
|
+
if para:
|
|
324
|
+
out.append("<p>" + _inline(" ".join(para)) + "</p>")
|
|
325
|
+
para.clear()
|
|
326
|
+
if code:
|
|
327
|
+
out.append("<pre><code>" + html.escape("\n".join(code), quote=False) + "</code></pre>")
|
|
328
|
+
code.clear()
|
|
329
|
+
if items:
|
|
330
|
+
out.append("<ul>" + "".join("<li>" + _inline(i) + "</li>" for i in items) + "</ul>")
|
|
331
|
+
items.clear()
|
|
332
|
+
|
|
333
|
+
for raw in text.split("\n"):
|
|
334
|
+
line = raw.rstrip()
|
|
335
|
+
if not line.strip():
|
|
336
|
+
flush()
|
|
337
|
+
continue
|
|
338
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
339
|
+
b = _BULLET.match(line) if indent < 4 else None
|
|
340
|
+
if b:
|
|
341
|
+
if para or code:
|
|
342
|
+
flush()
|
|
343
|
+
items.append(b.group(1))
|
|
344
|
+
continue
|
|
345
|
+
if items and indent >= 2: # wrapped continuation of a bullet
|
|
346
|
+
items[-1] += " " + line.strip()
|
|
347
|
+
continue
|
|
348
|
+
if indent >= 4:
|
|
349
|
+
if para or items:
|
|
350
|
+
flush()
|
|
351
|
+
code.append(line[4:])
|
|
352
|
+
continue
|
|
353
|
+
if code or items:
|
|
354
|
+
flush()
|
|
355
|
+
para.append(line.strip())
|
|
356
|
+
flush()
|
|
357
|
+
return Markup("".join(out))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# --- dates ----------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def human_date(iso: str | None) -> str:
|
|
363
|
+
"""'2026-11-21T10:00:00.123456+00:00' -> '21 Nov 2026'. Unparseable input is
|
|
364
|
+
returned unchanged so we never hide a value."""
|
|
365
|
+
if not iso:
|
|
366
|
+
return ""
|
|
367
|
+
try:
|
|
368
|
+
d = dt.datetime.fromisoformat(iso)
|
|
369
|
+
except ValueError:
|
|
370
|
+
return iso
|
|
371
|
+
return f"{d.day} {d.strftime('%b %Y')}"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: umbermark-cli
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Umbermark — Linux server hardening scanner (CLI only).
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: paramiko>=3.4
|
|
7
|
+
Requires-Dist: typer>=0.12
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pynacl>=1.5
|
|
11
|
+
Requires-Dist: itsdangerous>=2.2
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
umbermark/__init__.py,sha256=f15smPp8ezk4d4DQ77dhLtpHz1Vzyz1i8AE-bmxsvvQ,75
|
|
2
|
+
umbermark/checks/__init__.py,sha256=meATU7ey2y7yN1Z34X6nnyB1V4IEQkHD8BlqMsXv_kI,3536
|
|
3
|
+
umbermark/checks/auto_updates_configured.py,sha256=SGUeKPIPzZmSOVkUmXPoe-6dnQXQEAiXFNM75zub_OY,7226
|
|
4
|
+
umbermark/checks/dns_resolver_sanity.py,sha256=pkvlq7AOU1c52YoJRjrJG-tHMMKKvAcrNTlnu6DUHV4,2043
|
|
5
|
+
umbermark/checks/fail2ban_enabled.py,sha256=bXTY1sVCaeLTNodT1ef5lpnts5lgBl3f8Ezpyf1HUu0,3131
|
|
6
|
+
umbermark/checks/failed_login_anomaly.py,sha256=wyL6xFVSKQnDhQ2GMnR80ocCMoIDjDzJOiql7whp7_Q,1978
|
|
7
|
+
umbermark/checks/failed_systemd_units.py,sha256=xQhn00oF1HqpN0Is0n0GIZURG-H7D4kc2xucmiJrmQE,1524
|
|
8
|
+
umbermark/checks/firewall_protective.py,sha256=JjT8SlTyo-suTa3DyicCLm3MkpiOhcH_3JXopq9GH0k,11349
|
|
9
|
+
umbermark/checks/journald_persistence.py,sha256=QXDdz6sqgoUFtxLEbgiImqV1oaGh2pODbw4K3Ua6Hbw,2255
|
|
10
|
+
umbermark/checks/kernel_age.py,sha256=RaPDg4OTdE92qF-3Xf8RzzgilYPHHO1-X1QSPqrM2TA,2753
|
|
11
|
+
umbermark/checks/listen_vs_firewall_delta.py,sha256=mXCrJNGHxoYyZ5TOjsPkwaL2ysbXMvLehLgmQY8XTWU,6045
|
|
12
|
+
umbermark/checks/mac_framework_enforcing.py,sha256=uGFam024Le7Q2uYssaVfaq6CIWs8TW_J95PpnsvDlDI,4385
|
|
13
|
+
umbermark/checks/pending_security_updates.py,sha256=vriHpGaHn-T46LkPm8AVEJjrhlKORqPYzKabbibdMO0,9023
|
|
14
|
+
umbermark/checks/public_listen_anomalies.py,sha256=fLLhR0201bYS2061BOz1mpuMsaPE6ubGDJ1881Zc7cA,8953
|
|
15
|
+
umbermark/checks/reboot_pending.py,sha256=7sWI-Bu6LdFuaRiMn9xJIg2hemtqZHzxrv2jPyv4ZPc,3372
|
|
16
|
+
umbermark/checks/repo_gpg_trust.py,sha256=tV1F7JfCPdZQ6f3P3Hs1W1_CMF-OsBVFzYEHAe5U0G8,4405
|
|
17
|
+
umbermark/checks/root_disk_free.py,sha256=Wl-yAgsK3KwQKRf7L60iz2YE0-t5oTlge51zdimbTF8,1967
|
|
18
|
+
umbermark/checks/ssh_non_default_port.py,sha256=0poTmvUcsZX3gygetFV0O-dDhttw-PIf2Ni10xQ3IQk,2382
|
|
19
|
+
umbermark/checks/ssh_password_auth.py,sha256=NpH1Z6jxWUMkISRwS0dDWMHnkJtT0UJtOeeXDWA_zjE,3516
|
|
20
|
+
umbermark/checks/ssh_permit_root.py,sha256=995A5PvpVkBGbkOF-g9GEKqnNLDrK2rEe7AwryBj6qo,2304
|
|
21
|
+
umbermark/checks/ssh_weak_ciphers.py,sha256=9zrI3baYNeVb30gNoluovSF41gXxlRGt1FMh1hpRuX4,3419
|
|
22
|
+
umbermark/checks/sudo_requires_authentication.py,sha256=h4ssUIVQMRfqHgykuxHb2zcANj0HA-jOfxVUV8VFyZI,2252
|
|
23
|
+
umbermark/checks/sudoers_nopasswd.py,sha256=EGlsZIY_wrXJhWKieCKXhfhoRAyq8MbHWSdWfchxrn0,4807
|
|
24
|
+
umbermark/checks/swap_pressure.py,sha256=aW2ulos4RL1KEJiW5vwnnZLpiD_waSeElrBhjoBdzsI,2046
|
|
25
|
+
umbermark/checks/time_sync.py,sha256=QvXQTl1Va7sGfs0uXXc-vhf_qYIqzWGHO1-BLuY3BwQ,2138
|
|
26
|
+
umbermark/checks/tmp_hardening.py,sha256=OXPGKUJvyL0MtgFpDngKf4QDdx6ab0opJ1bvjQxTaQY,1693
|
|
27
|
+
umbermark/checks/user_password_aging.py,sha256=PV-6yzxGn59S-eTGlQaW1f8Kdz7SZ0-Grc2HG0hcVWI,4139
|
|
28
|
+
umbermark/checks/world_writable_suid_audit.py,sha256=_6jP5MvdCKZqUNLWi6DfTJWT2U5vuq0BP_vz7Blep9A,7969
|
|
29
|
+
umbermark/cli/__init__.py,sha256=OafcDkXjKNGtcm9Jq94td3ziRxmSomi4eyuJ-kDWS0w,82
|
|
30
|
+
umbermark/cli/app.py,sha256=zcrMgD8WRs3xq1KTR4AwOkvrHGKXx1kNO7aeOilcuqE,982
|
|
31
|
+
umbermark/cli/auth.py,sha256=tVHU6A0BWN7ZS12hrcmbQGv5m6vBHLl5NQXsOZc4lBE,1322
|
|
32
|
+
umbermark/cli/config.py,sha256=b-8KF6-LtiXVfEi-WPFuMs2Tb2sCAt3Q8xqpm6TeKD8,386
|
|
33
|
+
umbermark/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
umbermark/cli/commands/doctor.py,sha256=Z4RLfwHawKAkgetjyjYX0ndL5zs9LO5P7KAQ9YHGVSM,527
|
|
35
|
+
umbermark/cli/commands/login.py,sha256=jKFnVjj-2r7JImPXhpCF_unS3WXgivbJeYHnJykYr04,2623
|
|
36
|
+
umbermark/cli/commands/push.py,sha256=ykyiWgNaG7abwrTpkFRr53otPvODJMUS3n74lp_okik,1591
|
|
37
|
+
umbermark/cli/commands/report.py,sha256=OqaVZj7Msux0oGg0B8vrjUsZGTecL-9nO7FzzSS2LUI,825
|
|
38
|
+
umbermark/cli/commands/scan.py,sha256=zVrqksoDMqE-7b5873oPlyPqpyV0VSQE9hP_nD3RXKU,2395
|
|
39
|
+
umbermark/cli/commands/scan_local.py,sha256=fnypb-9iceBR-Ly6_3MywIZIBSYKfkCT1YO3qlTt59k,2212
|
|
40
|
+
umbermark/cli/commands/scan_wsl.py,sha256=P-0pi3YwvtKEHf2kzuNYs9yxV7r64-p-BQpa7QwXfSA,1021
|
|
41
|
+
umbermark/cli/commands/version.py,sha256=H3LVWwojlQp6Sc8vxquSxf89yCCExcTfirmqnWloNf8,249
|
|
42
|
+
umbermark/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
umbermark/core/accepted.py,sha256=YTfmNw3zVQpderKyje-Fbm8fyAKTGAnrz8pktSU4Z8U,2783
|
|
44
|
+
umbermark/core/access.py,sha256=8FogW5EfcLaec3sD5ZkQjjG2H7WUW0fOQ0VJyOwWi9A,39801
|
|
45
|
+
umbermark/core/capture.py,sha256=MnD3cLC3eIaMxT6pt-JSm9V136i7jp38WdL0R_w9j0M,9208
|
|
46
|
+
umbermark/core/check.py,sha256=xgpGu6wBwycBk4KrgKOHzza7JwLLn4rEUNPbjLPPw20,2289
|
|
47
|
+
umbermark/core/checkin.py,sha256=YZKHU6uxrB2cPZy2NFWkUFIBXm0RmSETwg2Ow8rKByw,4073
|
|
48
|
+
umbermark/core/crypto.py,sha256=FioGtwhLM61hZvA__L9mNN7vVfPtqGUTGnO4PFCpdGI,552
|
|
49
|
+
umbermark/core/duration.py,sha256=9ArbjuIGhrYxI9B6jkr4_HBC6Rw9YANe_D1GDumoMG8,1704
|
|
50
|
+
umbermark/core/engine.py,sha256=NvpQlfjFKa_S76_g6NZ4bUbL5TS8QWDJ3ubr7JpEzvA,2434
|
|
51
|
+
umbermark/core/firewall.py,sha256=phz079QyUBglp_-4BsCQvS7xfJLiX5jocAGDSPjVjrc,28651
|
|
52
|
+
umbermark/core/fleet.py,sha256=CJoqoUmoBBwxkE8ppIxH2fWOtArKjq9r8DcUZ_fQkQs,3747
|
|
53
|
+
umbermark/core/history.py,sha256=qCNbRdijXXBNwoCG-IwOqcrOoKXALMUevxIQ5t5aeRY,9449
|
|
54
|
+
umbermark/core/html_report.py,sha256=KZXirarag99yuZJROJWY_i7WYV70J5J9uHvXimwYD-o,11826
|
|
55
|
+
umbermark/core/keyenc.py,sha256=8xocjqYFR8zKhX1g455ZW2LtNYxrjQgHFFvGHhnb6uk,1464
|
|
56
|
+
umbermark/core/labels.py,sha256=Ir_JH2dSq3uGKLP3tTl6xr-QNHWhNexmHHpUMYtn-pc,844
|
|
57
|
+
umbermark/core/lifecycle.py,sha256=oBilfueGuqL3YiLjW4rlxEEplRABCY000KBJK7rRmYk,3975
|
|
58
|
+
umbermark/core/logredact.py,sha256=fj9FJ-E5c_9DR3AaVi7HbgfaFZe3O9SZpsAApQJ62mA,1523
|
|
59
|
+
umbermark/core/partial.py,sha256=OkfQuMjJYUJBdigNXwf-j_XcazhRpoKBTfVlkXrDWNM,2232
|
|
60
|
+
umbermark/core/platform.py,sha256=QRY5Yzz1t4YlNUSgqd8n-C_fDvrOBNe0VNP-y1R8Nck,2355
|
|
61
|
+
umbermark/core/redact.py,sha256=LesaIucpdiNKl5JAxsIhwuF-ayyuDWn2-OjUK-9J0Nc,3159
|
|
62
|
+
umbermark/core/report.py,sha256=UM9VDYHivLTuNmYFGFreosgtjAIh5d3QitecxyAET60,746
|
|
63
|
+
umbermark/core/scoring.py,sha256=vDTeaHp8mqZKBofs2xIJ40P7v_FdSXZv-kSQogl7EZM,2833
|
|
64
|
+
umbermark/core/settings.py,sha256=BIp83Cfp5dwmHjxCzHwkvPg0CVGsypp65Y0vy8JPooQ,12542
|
|
65
|
+
umbermark/core/short_id.py,sha256=LFRLtScYRgilCa1EEnnjdVJNImBaA7-1QxOnMH4I4OQ,817
|
|
66
|
+
umbermark/core/signing.py,sha256=07Cc7f16PKhTbatznzexGTGOeNZhbJGap5QvcTllNTs,909
|
|
67
|
+
umbermark/core/ssh.py,sha256=K40zfyd26K-lZT8mi0E6hWSt-wQYwiLKZlO8z6TWYAE,4392
|
|
68
|
+
umbermark/core/ssh_fingerprint.py,sha256=s2eGduJBxbmpjD2tAkVWd1XQE9cgMfpT1zY36aE6d5c,632
|
|
69
|
+
umbermark/core/tiers.py,sha256=ZDpPrbLDC68K5CgO1SKZ5Ey7mqDCjsNbQJBeNX3OB1A,5345
|
|
70
|
+
umbermark/core/webhooks.py,sha256=AdkqeaCRMilJufZ5oxQCoaXKBMa0Di3s4S5TMOn8MGc,7792
|
|
71
|
+
umbermark/core/wording.py,sha256=Yw0eB2zsMWJBm81X11A35KxRnlpS7GgLiGB-5_sKhYc,13924
|
|
72
|
+
umbermark/core/external/__init__.py,sha256=NMyipAiYW4PpAfkonW6tGmesTBMRxnE8vmDNFv301Vw,288
|
|
73
|
+
umbermark/core/external/check.py,sha256=1Ql1FkgD3u4K8ALlPOboJexC5CIFLfsCrUBf1K-jC9U,1827
|
|
74
|
+
umbermark/core/external/engine.py,sha256=35AJVhM9VWnvlL9cWtjlpjJX8HiJVLAXZBlI3CPS86o,3226
|
|
75
|
+
umbermark/core/external/ownership.py,sha256=zpvz9QkSEUegzNJWA8LnSiNXUxdQicdz8fDZLbCAspE,20794
|
|
76
|
+
umbermark/core/external/probe.py,sha256=wkqGZg-AU3P8jJLyWNHgsbaKDde18A2OmdUap0YaQrY,3861
|
|
77
|
+
umbermark/core/external/socket_probe.py,sha256=pCt9bKguSsYDah6HjYq71NLIAhrgeMfD8TO63V1TeQ8,16157
|
|
78
|
+
umbermark/core/external/checks/__init__.py,sha256=sibKeKHcgUm8DeweGF0ROnap8zcxkzRwZucPkRWwzB0,1169
|
|
79
|
+
umbermark/core/external/checks/exposed_datastores.py,sha256=ssFUxGSJoCkTm9VwmtErPUq4DEhsnH4bbXSTrfCJ24U,2140
|
|
80
|
+
umbermark/core/external/checks/http_security_headers.py,sha256=vqLEQuNYr10WhdTzcCkmqjmJG96QSLHIEECL0mKILCk,10602
|
|
81
|
+
umbermark/core/external/checks/legacy_services.py,sha256=iAAxHEdDkY7NCrv3UViH83d1EOtKK65qV1ofLROxVxA,1898
|
|
82
|
+
umbermark/core/external/checks/open_management_ports.py,sha256=wpuJUGpytXiZxp-wsD2bORxgmp2SH3QduTsDZmaMXKQ,2681
|
|
83
|
+
umbermark/core/external/checks/ssh_alt_ports.py,sha256=PAdEOTrvhGFjUQwHUIf0yAU5Cef94KVORULhYxTXa5c,3600
|
|
84
|
+
umbermark/core/external/checks/ssh_weak_algorithms.py,sha256=9PKozJwfOvE-xrYcyajh6iyouEkN10EQOD6RKeefvUw,3603
|
|
85
|
+
umbermark/core/external/checks/tls_hygiene.py,sha256=sCkM0RH22fiWH6LLRYLe6Og-KatMcYQ3nVI_Et7vYLA,4989
|
|
86
|
+
umbermark/core/pdf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
|
+
umbermark/core/pdf/renderer.py,sha256=05XW10Yqh3Ndwyv7zfdrUBea-haNu7WY4p-zLlAXwXM,815
|
|
88
|
+
umbermark/core/pdf/weasyprint_renderer.py,sha256=tQbrSNOM-1Y-YNB6HCchA4dYkF4jTWXpCY-Sn6w2j88,616
|
|
89
|
+
umbermark_cli-1.0.0rc1.dist-info/METADATA,sha256=auk3QsD9expQZE4LE3FI0zm_5RbrwCXQewhO-IDgF64,331
|
|
90
|
+
umbermark_cli-1.0.0rc1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
91
|
+
umbermark_cli-1.0.0rc1.dist-info/entry_points.txt,sha256=QN7Tr9MH2fInhCNRYDOz9H5YFJwuGuo7WgpgEHMWZcY,48
|
|
92
|
+
umbermark_cli-1.0.0rc1.dist-info/top_level.txt,sha256=d3SP0jhXKY0P_B_52xzKfnpLWnbUhedUltMqcA8BFEA,10
|
|
93
|
+
umbermark_cli-1.0.0rc1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
umbermark
|