diff --git a/claude_bottle/pipelock.py b/claude_bottle/pipelock.py index 408f4d7..b19bd1a 100644 --- a/claude_bottle/pipelock.py +++ b/claude_bottle/pipelock.py @@ -13,13 +13,13 @@ Image pin: ghcr.io/luckypipewrench/pipelock@sha256: for tag 2.3.0. from __future__ import annotations import os -import re import subprocess from dataclasses import dataclass from pathlib import Path from .log import die, info, warn from .manifest import Manifest +from .util import is_ipv4_literal # Pipelock image, pinned by digest. The digest is the multi-arch image # index for ghcr.io/luckypipewrench/pipelock:2.3.0. @@ -67,18 +67,6 @@ def pipelock_bottle_ssh_hostnames(manifest: Manifest, bottle_name: str) -> list[ return [e.Hostname for e in manifest.bottles[bottle_name].ssh if e.Hostname] -_IPV4_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$") - - -def is_ipv4_literal(s: str) -> bool: - """Pipelock's SSRF check fires on resolved IP, so an IP-literal - Hostname goes to ssrf.ip_allowlist while a hostname goes to - trusted_domains.""" - if not s: - return False - return bool(_IPV4_RE.match(s)) - - def pipelock_bottle_ssh_trusted_domains(manifest: Manifest, bottle_name: str) -> list[str]: return [h for h in pipelock_bottle_ssh_hostnames(manifest, bottle_name) if not is_ipv4_literal(h)] diff --git a/claude_bottle/util.py b/claude_bottle/util.py index b936c02..1fb877d 100644 --- a/claude_bottle/util.py +++ b/claude_bottle/util.py @@ -6,6 +6,7 @@ claude_bottle/backend/docker/util.py.""" from __future__ import annotations import os +import re def expand_tilde(path: str) -> str: @@ -16,3 +17,15 @@ def expand_tilde(path: str) -> str: home = os.environ.get("HOME", "") return home + path[1:] return path + + +_IPV4_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$") + + +def is_ipv4_literal(s: str) -> bool: + """True iff `s` looks like a dotted-quad IPv4 literal. Does not + validate octet ranges; consumers that care about that should run + a stricter check. Empty input returns False.""" + if not s: + return False + return bool(_IPV4_RE.match(s)) diff --git a/tests/test_pipelock_classify.py b/tests/test_pipelock_classify.py index 749a732..3c08ddf 100644 --- a/tests/test_pipelock_classify.py +++ b/tests/test_pipelock_classify.py @@ -1,10 +1,10 @@ """Unit: is_ipv4_literal — the classifier that decides whether -bottle.ssh[].Hostname goes into ssrf.ip_allowlist (IPv4 literal) or -trusted_domains (hostname).""" +bottle.ssh[].Hostname goes into pipelock's ssrf.ip_allowlist (IPv4 +literal) or trusted_domains (hostname).""" import unittest -from claude_bottle.pipelock import is_ipv4_literal +from claude_bottle.util import is_ipv4_literal class TestIPv4Classify(unittest.TestCase):