shipwright-kit 0.8.0__tar.gz → 0.9.0__tar.gz

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 (38) hide show
  1. {shipwright_kit-0.8.0/shipwright_kit.egg-info → shipwright_kit-0.9.0}/PKG-INFO +1 -1
  2. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/pyproject.toml +1 -1
  3. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/__init__.py +1 -1
  4. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/security/__init__.py +10 -1
  5. shipwright_kit-0.9.0/shipwright_kit/security/ssrf.py +123 -0
  6. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0/shipwright_kit.egg-info}/PKG-INFO +1 -1
  7. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit.egg-info/SOURCES.txt +1 -0
  8. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/LICENSE +0 -0
  9. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/README.md +0 -0
  10. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/setup.cfg +0 -0
  11. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/cli.py +0 -0
  12. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/config.py +0 -0
  13. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/__init__.py +0 -0
  14. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/banner.py +0 -0
  15. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/console.py +0 -0
  16. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/glyphs.py +0 -0
  17. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/output.py +0 -0
  18. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/palette.py +0 -0
  19. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/design/tiers.py +0 -0
  20. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/eval/__init__.py +0 -0
  21. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/eval/corpus.py +0 -0
  22. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/eval/harness.py +0 -0
  23. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/eval/metrics.py +0 -0
  24. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/py.typed +0 -0
  25. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/security/eval.py +0 -0
  26. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/security/injection.py +0 -0
  27. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit/security/theme.py +0 -0
  28. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit.egg-info/dependency_links.txt +0 -0
  29. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit.egg-info/entry_points.txt +0 -0
  30. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit.egg-info/requires.txt +0 -0
  31. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/shipwright_kit.egg-info/top_level.txt +0 -0
  32. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_cli.py +0 -0
  33. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_config.py +0 -0
  34. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_packaging.py +0 -0
  35. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_packs_entrypoint.py +0 -0
  36. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_release_config.py +0 -0
  37. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_template_wiring.py +0 -0
  38. {shipwright_kit-0.8.0 → shipwright_kit-0.9.0}/tests/test_tooling.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipwright-kit
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Shipwright — AI-agent dev framework + import-light design/eval/security library
5
5
  Author: Christian Huhn
6
6
  License-Expression: MIT
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name. The bare `shipwright` is taken on PyPI (unrelated 6si
8
8
  # tool), so the dist is `shipwright-kit`; the IMPORT name is `shipwright_kit`.
9
9
  name = "shipwright-kit"
10
- version = "0.8.0"
10
+ version = "0.9.0"
11
11
  description = "Shipwright — AI-agent dev framework + import-light design/eval/security library"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Shipwright — design-token + tooling library."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.9.0"
@@ -1,4 +1,5 @@
1
- """Security pack: threat-verdict theme + labels + shared injection defense."""
1
+ """Security pack: threat-verdict theme + labels + shared injection defense
2
+ + SSRF host-allowlist guard."""
2
3
 
3
4
  from shipwright_kit.security.injection import (
4
5
  INJECTION_PATTERNS_VERSION,
@@ -7,11 +8,19 @@ from shipwright_kit.security.injection import (
7
8
  SeverityLevel,
8
9
  scan,
9
10
  )
11
+ from shipwright_kit.security.ssrf import (
12
+ UnsafeURLError,
13
+ assert_safe_url,
14
+ is_safe_url,
15
+ )
10
16
 
11
17
  __all__ = [
12
18
  "INJECTION_PATTERNS_VERSION",
13
19
  "InjectionFinding",
14
20
  "PromptInjectionDetector",
15
21
  "SeverityLevel",
22
+ "UnsafeURLError",
23
+ "assert_safe_url",
24
+ "is_safe_url",
16
25
  "scan",
17
26
  ]
@@ -0,0 +1,123 @@
1
+ """SSRF host-allowlist guard for outbound HTTP requests.
2
+
3
+ Pre-committed guardrail for W3 (the LLM-provider layer making ollama's
4
+ ``base_url`` configurable). This module is the primitive ONLY — it is not
5
+ wired into any tool or into ``ollama.py`` yet (``base_url`` is hardcoded
6
+ today, so wiring now would be dead code in the shipped path). W3 does the
7
+ wiring.
8
+
9
+ Import-light: stdlib only (``ipaddress``, ``urllib.parse``, ``socket``) — no
10
+ third-party deps, no import-time side effects. Mirrors the import-light
11
+ invariant of ``shipwright_kit.security.injection``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import ipaddress
17
+ import socket
18
+ from urllib.parse import urlsplit
19
+
20
+ __all__ = [
21
+ "UnsafeURLError",
22
+ "assert_safe_url",
23
+ "is_safe_url",
24
+ ]
25
+
26
+ _ALLOWED_SCHEMES = {"http", "https"}
27
+
28
+ _IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
29
+
30
+
31
+ class UnsafeURLError(ValueError):
32
+ """Raised by :func:`assert_safe_url` when a URL is unsafe to fetch."""
33
+
34
+
35
+ def _is_blocked_ip(ip: _IPAddress, *, allow_loopback: bool) -> bool:
36
+ """True if `ip` should be blocked as an SSRF target.
37
+
38
+ Loopback (127.0.0.0/8, ::1) is carved out first so ``allow_loopback``
39
+ can permit it — every other non-globally-routable range stays blocked
40
+ unconditionally: link-local (169.254.0.0/16, incl. the 169.254.169.254
41
+ cloud-metadata address, and IPv6 fe80::/10), RFC1918 private ranges and
42
+ IPv6 unique-local (fc00::/7) via ``is_private``, plus multicast/
43
+ reserved/unspecified as defense in depth.
44
+ """
45
+ if ip.is_loopback:
46
+ return not allow_loopback
47
+ if ip.is_link_local:
48
+ return True
49
+ if ip.is_multicast or ip.is_reserved or ip.is_unspecified:
50
+ return True
51
+ if ip.is_private:
52
+ return True
53
+ return False
54
+
55
+
56
+ def assert_safe_url(url: str, *, allow_loopback: bool = False, resolve: bool = False) -> None:
57
+ """Raise :class:`UnsafeURLError` if `url` is unsafe to fetch outbound; else return None.
58
+
59
+ Blocks:
60
+ - non-http(s) schemes (``file://``, ``ftp://``, etc.) and malformed URLs.
61
+ - IP-literal hosts in RFC1918 private ranges (10/8, 172.16/12, 192.168/16),
62
+ link-local ranges including the 169.254.169.254 cloud-metadata address
63
+ (169.254.0.0/16), IPv6 unique-local (fc00::/7), IPv6 link-local
64
+ (fe80::/10), and loopback (127.0.0.0/8, ::1) — unless
65
+ ``allow_loopback=True``.
66
+
67
+ When ``resolve=True``, a non-IP-literal hostname is resolved via
68
+ ``socket.getaddrinfo`` and rejected if ANY resolved address is blocked.
69
+
70
+ Caveat: this is a best-effort, TOCTOU-prone check. DNS is not pinned —
71
+ the hostname can re-resolve to a different (unsafe) address between this
72
+ check and the actual outbound request (a classic DNS-rebinding SSRF
73
+ bypass). Callers with a hard safety requirement should connect to the
74
+ resolved IP directly (not re-resolve the hostname) or maintain an
75
+ explicit host allowlist; this function is a guardrail, not a substitute
76
+ for that.
77
+ """
78
+ try:
79
+ parsed = urlsplit(url)
80
+ except ValueError as exc:
81
+ raise UnsafeURLError(f"malformed URL: {url!r}") from exc
82
+
83
+ scheme = parsed.scheme.lower()
84
+ if scheme not in _ALLOWED_SCHEMES:
85
+ raise UnsafeURLError(f"unsupported scheme: {scheme or '(none)'!r}")
86
+
87
+ # Not wrapped in try/except: urlsplit() above already rejects malformed
88
+ # IPv6-bracket netlocs before .hostname would ever raise.
89
+ host = parsed.hostname
90
+ if not host:
91
+ raise UnsafeURLError(f"URL has no host: {url!r}")
92
+
93
+ try:
94
+ literal_ip: _IPAddress | None = ipaddress.ip_address(host)
95
+ except ValueError:
96
+ literal_ip = None
97
+
98
+ if literal_ip is not None:
99
+ if _is_blocked_ip(literal_ip, allow_loopback=allow_loopback):
100
+ raise UnsafeURLError(f"blocked address: {host}")
101
+ return None
102
+
103
+ if resolve:
104
+ try:
105
+ infos = socket.getaddrinfo(host, None)
106
+ except OSError as exc:
107
+ raise UnsafeURLError(f"could not resolve host: {host!r} ({exc})") from exc
108
+ for info in infos:
109
+ sockaddr = info[4]
110
+ resolved_ip = ipaddress.ip_address(sockaddr[0])
111
+ if _is_blocked_ip(resolved_ip, allow_loopback=allow_loopback):
112
+ raise UnsafeURLError(f"host {host!r} resolves to blocked address: {sockaddr[0]}")
113
+
114
+ return None
115
+
116
+
117
+ def is_safe_url(url: str, **kwargs: bool) -> bool:
118
+ """Convenience wrapper: True if `url` passes :func:`assert_safe_url`, else False."""
119
+ try:
120
+ assert_safe_url(url, **kwargs)
121
+ except UnsafeURLError:
122
+ return False
123
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipwright-kit
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Shipwright — AI-agent dev framework + import-light design/eval/security library
5
5
  Author: Christian Huhn
6
6
  License-Expression: MIT
@@ -25,6 +25,7 @@ shipwright_kit/eval/metrics.py
25
25
  shipwright_kit/security/__init__.py
26
26
  shipwright_kit/security/eval.py
27
27
  shipwright_kit/security/injection.py
28
+ shipwright_kit/security/ssrf.py
28
29
  shipwright_kit/security/theme.py
29
30
  tests/test_cli.py
30
31
  tests/test_config.py
File without changes
File without changes
File without changes