Files
bot-bottle/tests/unit/test_pipelock_allowlist.py
T
didericis 27b2d78b11
test / unit (pull_request) Successful in 15s
test / integration (pull_request) Successful in 29s
fix(cred_proxy): close git-push bypass + route through pipelock (PRD 0010)
Three coupled fixes that close a documented bypass of git-gate's
gitleaks pre-receive hook:

1. cred-proxy refuses git smart-HTTP push at runtime. Any path
   ending in /git-receive-pack or /info/refs?service=git-receive-pack
   returns 403 with a pointer at the bottle.git SSH path. Fetch
   (upload-pack) is still allowed — the bypass we're closing is
   push, where gitleaks is the load-bearing scanner. Hard guarantee.

2. The provisioner suppresses the cred-proxy `~/.gitconfig` insteadOf
   rewrite for any host already declared in bottle.git. git-gate is
   the canonical git path there; we don't write a competing rule
   that would let `git clone https://<host>/...` succeed in ways
   that confuse on push. Defense in depth — (1) is the hard guarantee.

3. cred-proxy routes its outbound HTTPS through pipelock. The
   sidecar's environ now sets HTTPS_PROXY=<pipelock-url>, and the
   image's entrypoint runs `update-ca-certificates` over the
   per-bottle pipelock CA (docker cp'd into
   /usr/local/share/ca-certificates/pipelock.crt before start) so
   the proxy's HTTPS client trusts pipelock's bumped certs.

   Consequence: pipelock's allowlist + body scanner now sit in the
   cred-proxy egress path the same way they sit in front of direct
   agent traffic. The cred-proxy upstream hosts (api.github.com,
   github.com, gitea hosts, registry.npmjs.org) come OFF
   pipelock's passthrough_domains. Only api.anthropic.com remains
   on passthrough (LLM body content legitimately trips DLP).

PRD 0010 updated to reflect all three. Tests adjusted: the
"cred-proxy hosts go on passthrough" assertion in
test_pipelock_allowlist flips to "they don't", a new
TestIsGitPushRequest exercises the smart-HTTP refusal predicate,
and the gitconfig renderer tests cover the per-host suppression
matrix.
2026-05-13 21:09:33 -04:00

115 lines
4.2 KiB
Python

"""Unit: pipelock_effective_allowlist — the union of baked-in defaults,
bottle.egress.allowlist, and cred-proxy upstream hosts derived from
bottle.tokens (PRD 0010). Git upstreams declared in bottle.git do not
contribute here; they flow through the per-agent git-gate (PRD 0008)."""
import unittest
from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import (
pipelock_effective_allowlist,
pipelock_effective_tls_passthrough,
pipelock_token_hosts,
)
def _bottle(spec):
return Manifest.from_json_obj({
"bottles": {"dev": spec},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
class TestEffectiveAllowlist(unittest.TestCase):
def test_union_and_dedup(self):
eff = pipelock_effective_allowlist(_bottle({
"egress": {
"allowlist": [
"registry.npmjs.org",
# Duplicate of a baked default; the union must dedupe.
"api.anthropic.com",
],
},
}))
self.assertIn("api.anthropic.com", eff, "baked default present")
self.assertIn("registry.npmjs.org", eff, "egress.allowlist present")
self.assertEqual(len(eff), len(set(eff)), "deduplicated")
self.assertEqual(eff, sorted(eff), "sorted")
class TestTokenHosts(unittest.TestCase):
def test_github_yields_both_hosts(self):
hosts = pipelock_token_hosts(_bottle({
"tokens": [{"Kind": "github", "TokenRef": "GH"}],
}))
self.assertEqual(["api.github.com", "github.com"], hosts)
def test_gitea_yields_configured_host(self):
hosts = pipelock_token_hosts(_bottle({
"tokens": [{"Kind": "gitea", "TokenRef": "T",
"Url": "https://gitea.dideric.is"}],
}))
self.assertEqual(["gitea.dideric.is"], hosts)
def test_npm_yields_registry(self):
hosts = pipelock_token_hosts(_bottle({
"tokens": [{"Kind": "npm", "TokenRef": "N"}],
}))
self.assertEqual(["registry.npmjs.org"], hosts)
def test_anthropic_yields_api_host(self):
hosts = pipelock_token_hosts(_bottle({
"tokens": [{"Kind": "anthropic", "TokenRef": "A"}],
}))
self.assertEqual(["api.anthropic.com"], hosts)
def test_no_tokens_empty(self):
self.assertEqual([], pipelock_token_hosts(_bottle({})))
class TestAllowlistWithTokens(unittest.TestCase):
def test_token_hosts_added_to_allowlist(self):
eff = pipelock_effective_allowlist(_bottle({
"tokens": [
{"Kind": "npm", "TokenRef": "N"},
{"Kind": "github", "TokenRef": "G"},
],
}))
self.assertIn("registry.npmjs.org", eff)
self.assertIn("api.github.com", eff)
self.assertIn("github.com", eff)
def test_gitea_host_added(self):
eff = pipelock_effective_allowlist(_bottle({
"tokens": [{"Kind": "gitea", "TokenRef": "T",
"Url": "https://gitea.dideric.is"}],
}))
self.assertIn("gitea.dideric.is", eff)
class TestTlsPassthrough(unittest.TestCase):
def test_default_includes_api_anthropic(self):
passthrough = pipelock_effective_tls_passthrough(_bottle({}))
self.assertEqual(["api.anthropic.com"], passthrough)
def test_token_hosts_NOT_added_to_passthrough(self):
# cred-proxy now trusts pipelock's per-bottle CA (loaded into
# its container's trust store via docker cp + update-ca-
# certificates at start time), so pipelock can MITM the
# cred-proxy -> upstream leg and body-scan it. Auto-adding
# cred-proxy hosts to passthrough would silently disable that
# second scanner for github / gitea / npm.
passthrough = pipelock_effective_tls_passthrough(_bottle({
"tokens": [
{"Kind": "github", "TokenRef": "G"},
{"Kind": "npm", "TokenRef": "N"},
{"Kind": "gitea", "TokenRef": "T",
"Url": "https://gitea.dideric.is"},
],
}))
self.assertEqual(["api.anthropic.com"], passthrough)
if __name__ == "__main__":
unittest.main()