sre-agent-cli 0.2.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.
- sre_agent/__init__.py +3 -0
- sre_agent/adapters.py +833 -0
- sre_agent/approvals.py +243 -0
- sre_agent/audit.py +225 -0
- sre_agent/backend_scores.py +184 -0
- sre_agent/backend_url.py +146 -0
- sre_agent/cli.py +732 -0
- sre_agent/config.py +22 -0
- sre_agent/evaluation.py +391 -0
- sre_agent/evaluation_page.py +209 -0
- sre_agent/hosted_demo.py +153 -0
- sre_agent/ingestion.py +423 -0
- sre_agent/injection.py +580 -0
- sre_agent/logging_config.py +83 -0
- sre_agent/memory.py +411 -0
- sre_agent/model_backend.py +551 -0
- sre_agent/model_proposals.py +287 -0
- sre_agent/models.py +278 -0
- sre_agent/policy.py +208 -0
- sre_agent/postmortems.py +296 -0
- sre_agent/reasoning.py +547 -0
- sre_agent/regression.py +287 -0
- sre_agent/runbooks.py +290 -0
- sre_agent/security_review.py +826 -0
- sre_agent/server.py +684 -0
- sre_agent/storage.py +266 -0
- sre_agent/tier3.py +440 -0
- sre_agent/tools.py +712 -0
- sre_agent_cli-0.2.0.dist-info/METADATA +364 -0
- sre_agent_cli-0.2.0.dist-info/RECORD +34 -0
- sre_agent_cli-0.2.0.dist-info/WHEEL +5 -0
- sre_agent_cli-0.2.0.dist-info/entry_points.txt +2 -0
- sre_agent_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- sre_agent_cli-0.2.0.dist-info/top_level.txt +1 -0
sre_agent/backend_url.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Backend address rules: where the agent may send a request.
|
|
2
|
+
|
|
3
|
+
In plain words, every live backend in this project — Prometheus,
|
|
4
|
+
Kubernetes, Datadog, Azure Monitor — and the command-line tool's
|
|
5
|
+
own server address take a base address from whoever sets the
|
|
6
|
+
agent up, and some of them send a credential to that address as
|
|
7
|
+
a request header. Finding F-05 in the security review named the
|
|
8
|
+
risk: a mistyped or hostile address would receive a live token,
|
|
9
|
+
and a plain ``http://`` address would carry it across a network
|
|
10
|
+
unencrypted.
|
|
11
|
+
|
|
12
|
+
This module is the one place that risk is answered, so every
|
|
13
|
+
backend applies the same rules:
|
|
14
|
+
|
|
15
|
+
- **https, or loopback http, and nothing else.** An ``https://``
|
|
16
|
+
address is accepted. A plain ``http://`` address is accepted
|
|
17
|
+
only when its host is on this same machine — ``localhost``,
|
|
18
|
+
``127.0.0.0/8`` or ``::1`` — which is where the demo backends
|
|
19
|
+
and the local server live. Plain http to any other host is
|
|
20
|
+
refused, and so is every other scheme (``ftp:``, ``file:``,
|
|
21
|
+
a bare host with no scheme, and the rest).
|
|
22
|
+
- **No credentials inside the address.** An address carrying a
|
|
23
|
+
user or password (``https://user:secret@host``) is refused:
|
|
24
|
+
credentials belong in the header a backend adds on purpose,
|
|
25
|
+
never pasted into an address that gets logged and copied.
|
|
26
|
+
- **An optional allow-list of hosts.** A caller may pass
|
|
27
|
+
``allowed_hosts`` — host names, optionally with a port, such
|
|
28
|
+
as ``api.datadoghq.com`` or ``prometheus.internal:9090``. When
|
|
29
|
+
a list is given, the address's host (and its port, when the
|
|
30
|
+
entry names one) must be on it. The list can only narrow where
|
|
31
|
+
the agent may call; it can never widen the scheme rule — an
|
|
32
|
+
allow-list entry does not make plain http to a remote host
|
|
33
|
+
acceptable.
|
|
34
|
+
|
|
35
|
+
The check runs when a backend is built, before any request is
|
|
36
|
+
made, and raises ``ValueError`` with a plain reason. Addresses
|
|
37
|
+
still come only from operator code or configuration — never
|
|
38
|
+
from telemetry, an alert or a model output — and that boundary,
|
|
39
|
+
stated in the review, is unchanged; these rules are the second
|
|
40
|
+
layer, for the day an operator's address is wrong.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import ipaddress
|
|
46
|
+
import urllib.parse
|
|
47
|
+
from collections.abc import Iterable
|
|
48
|
+
|
|
49
|
+
_LOOPBACK_NAMES = {"localhost"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_loopback(host: str) -> bool:
|
|
53
|
+
"""True when *host* names this same machine only."""
|
|
54
|
+
if host.lower() in _LOOPBACK_NAMES:
|
|
55
|
+
return True
|
|
56
|
+
try:
|
|
57
|
+
return ipaddress.ip_address(host).is_loopback
|
|
58
|
+
except ValueError:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _host_matches(host: str, port: int | None, entry: str) -> bool:
|
|
63
|
+
"""True when one allow-list *entry* names this host (and port).
|
|
64
|
+
|
|
65
|
+
An entry is a host name, optionally with ``:port``. A bare
|
|
66
|
+
host entry allows that host on any port; an entry with a
|
|
67
|
+
port allows only that port. Matching is case-insensitive,
|
|
68
|
+
as host names are.
|
|
69
|
+
"""
|
|
70
|
+
entry = entry.strip().lower()
|
|
71
|
+
if not entry:
|
|
72
|
+
return False
|
|
73
|
+
if entry.startswith("["): # a bracketed IPv6 entry, [::1] or [::1]:8443
|
|
74
|
+
end = entry.find("]")
|
|
75
|
+
if end == -1:
|
|
76
|
+
return False
|
|
77
|
+
entry_host = entry[1:end]
|
|
78
|
+
rest = entry[end + 1 :]
|
|
79
|
+
entry_port: int | None = None
|
|
80
|
+
if rest.startswith(":"):
|
|
81
|
+
try:
|
|
82
|
+
entry_port = int(rest[1:])
|
|
83
|
+
except ValueError:
|
|
84
|
+
return False
|
|
85
|
+
elif rest:
|
|
86
|
+
return False
|
|
87
|
+
elif entry.count(":") == 1:
|
|
88
|
+
entry_host, _, port_text = entry.partition(":")
|
|
89
|
+
try:
|
|
90
|
+
entry_port = int(port_text)
|
|
91
|
+
except ValueError:
|
|
92
|
+
return False
|
|
93
|
+
else:
|
|
94
|
+
entry_host, entry_port = entry, None
|
|
95
|
+
if host.lower() != entry_host:
|
|
96
|
+
return False
|
|
97
|
+
return entry_port is None or entry_port == port
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_backend_url(
|
|
101
|
+
url: str,
|
|
102
|
+
*,
|
|
103
|
+
allowed_hosts: Iterable[str] | None = None,
|
|
104
|
+
what: str = "backend address",
|
|
105
|
+
) -> str:
|
|
106
|
+
"""Check one base address against the rules; return it cleaned.
|
|
107
|
+
|
|
108
|
+
The returned address is the caller's, stripped of surrounding
|
|
109
|
+
whitespace and any trailing slash, so a backend can append
|
|
110
|
+
its paths to it directly. Anything outside the rules raises
|
|
111
|
+
``ValueError`` with a plain reason naming *what* was checked.
|
|
112
|
+
"""
|
|
113
|
+
cleaned = url.strip().rstrip("/") if isinstance(url, str) else ""
|
|
114
|
+
if not cleaned:
|
|
115
|
+
raise ValueError(f"{what} must not be empty")
|
|
116
|
+
if any(ch.isspace() for ch in cleaned):
|
|
117
|
+
raise ValueError(f"{what} must not contain spaces: {url!r}")
|
|
118
|
+
parsed = urllib.parse.urlparse(cleaned)
|
|
119
|
+
host = parsed.hostname
|
|
120
|
+
if parsed.scheme not in {"http", "https"} or not host:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"{what} must be an http:// or https:// address with a host: {url!r}"
|
|
123
|
+
)
|
|
124
|
+
if parsed.username or parsed.password:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"{what} must not carry a user or password in the address: {url!r}"
|
|
127
|
+
)
|
|
128
|
+
if parsed.scheme == "http" and not _is_loopback(host):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"{what} uses plain http to a host that is not on this "
|
|
131
|
+
f"machine ({host}); use https:// for any address beyond "
|
|
132
|
+
"localhost, so a credential cannot cross a network "
|
|
133
|
+
"unencrypted"
|
|
134
|
+
)
|
|
135
|
+
if allowed_hosts is not None:
|
|
136
|
+
entries = list(allowed_hosts)
|
|
137
|
+
try:
|
|
138
|
+
port = parsed.port
|
|
139
|
+
except ValueError:
|
|
140
|
+
port = -1 # an unparseable port matches no entry
|
|
141
|
+
if not any(_host_matches(host, port, entry) for entry in entries):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"{what} host {host} is not on the allow-list "
|
|
144
|
+
f"for this backend: {sorted(entries)}"
|
|
145
|
+
)
|
|
146
|
+
return cleaned
|