1039 lines
33 KiB
Python
1039 lines
33 KiB
Python
"""Adversarial contracts for the Atlas-only draft pull-request client."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import copy
|
||
import importlib.util
|
||
import json
|
||
import sys
|
||
import urllib.request
|
||
from email.message import Message
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).parents[2]
|
||
CLIENT_PATH = ROOT / "services/hermes/scm-common/scripts/gitea_api.py"
|
||
HEAD_SHA = "465cf9146b05c174a2a8d310aff6c64be58277b6"
|
||
if str(CLIENT_PATH.parent) not in sys.path:
|
||
sys.path.insert(0, str(CLIENT_PATH.parent))
|
||
|
||
|
||
def _load():
|
||
spec = importlib.util.spec_from_file_location("safe_gitea_api", CLIENT_PATH)
|
||
assert spec and spec.loader
|
||
module = importlib.util.module_from_spec(spec)
|
||
sys.modules[spec.name] = module
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def _draft_payload(**updates):
|
||
payload = {
|
||
"base": "main",
|
||
"body": "Review evidence",
|
||
"head": "hermes/review-fix",
|
||
"title": "WIP: Repair review findings",
|
||
}
|
||
payload.update(updates)
|
||
return payload
|
||
|
||
|
||
def _draft_response(**updates):
|
||
response = {
|
||
"number": 3,
|
||
"state": "open",
|
||
"draft": True,
|
||
"merged": False,
|
||
"html_url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||
"url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||
"title": "WIP: Focused fix",
|
||
"body": "Review evidence",
|
||
"base": {"ref": "main", "repo": {"full_name": "atlas/cassandra"}},
|
||
"head": {
|
||
"ref": "hermes/fix",
|
||
"sha": HEAD_SHA,
|
||
"repo": {"full_name": "atlas/cassandra"},
|
||
},
|
||
}
|
||
response.update(updates)
|
||
return response
|
||
|
||
|
||
class Response:
|
||
def __init__(self, body: object, status: int | None = None):
|
||
self.body = body if isinstance(body, bytes) else json.dumps(body).encode()
|
||
self.status = status if status is not None else (201 if isinstance(body, dict) else 200)
|
||
self.headers = Message()
|
||
self.headers["Content-Type"] = "application/json"
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *_args):
|
||
return False
|
||
|
||
def read(self, limit=-1):
|
||
return self.body if limit < 0 else self.body[:limit]
|
||
|
||
|
||
def test_redirect_handler_rejects_cross_origin_with_sentinel_authorization():
|
||
client = _load()
|
||
source = urllib.request.Request(
|
||
"https://scm.bstein.dev/api/v1/repos/atlas/cassandra",
|
||
headers={"Authorization": "token redirect-sentinel"},
|
||
)
|
||
|
||
with pytest.raises(client.PolicyError, match="redirects are not allowed") as exc:
|
||
client.RejectRedirectHandler().redirect_request(
|
||
source,
|
||
None,
|
||
302,
|
||
"Found",
|
||
{},
|
||
"https://evil.example/collect",
|
||
)
|
||
|
||
assert "redirect-sentinel" not in str(exc.value)
|
||
assert any(
|
||
isinstance(handler, client.RejectRedirectHandler)
|
||
for handler in client._SAFE_OPENER.handlers
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("base_url", "path"),
|
||
[
|
||
("https://evil.example", "/api/v1/repos/atlas/cassandra"),
|
||
("http://scm.bstein.dev", "/api/v1/repos/atlas/cassandra"),
|
||
("https://scm.bstein.dev:443", "/api/v1/repos/atlas/cassandra"),
|
||
("https://scm.bstein.dev/", "/api/v1/repos/atlas/cassandra"),
|
||
("HTTPS://scm.bstein.dev", "/api/v1/repos/atlas/cassandra"),
|
||
("https://SCM.bstein.dev", "/api/v1/repos/atlas/cassandra"),
|
||
("https://user@scm.bstein.dev", "/api/v1/repos/atlas/cassandra"),
|
||
("https://scm.bstein.dev", "https://evil.example/api/v1/repos/atlas/cassandra"),
|
||
("https://scm.bstein.dev", "/api/v1/repos/evil/cassandra"),
|
||
("https://scm.bstein.dev", "/api/v1/repos/%61tlas/cassandra"),
|
||
("https://scm.bstein.dev", "/api/v1/repos/atlas/../admin"),
|
||
],
|
||
)
|
||
def test_host_owner_and_path_escape_attempts_are_rejected(base_url: str, path: str):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.build_request("GET", path, base_url=base_url, token="secret")
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"path",
|
||
[
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\nHost: evil.example",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\r\nX-Test: value",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\tignored",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\x00ignored",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\x1fignored",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1\x7fignored",
|
||
"/api/v1/repos/atlas/cassandra\\pulls\\1",
|
||
"/api/v1/repos/atlas/cassandra/pulls/%31",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1",
|
||
" https://scm.bstein.dev/api/v1/repos/atlas/cassandra",
|
||
"https://scm.bstein.dev/api/v1/repos/atlas/cassandra",
|
||
],
|
||
)
|
||
def test_raw_noncanonical_target_is_rejected_before_urlsplit_and_opener(
|
||
path: str, monkeypatch
|
||
):
|
||
client = _load()
|
||
split_called = False
|
||
opener_called = False
|
||
original_urlsplit = client.urllib.parse.urlsplit
|
||
|
||
def urlsplit(*args, **kwargs):
|
||
nonlocal split_called
|
||
split_called = True
|
||
return original_urlsplit(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
return Response(b"{}")
|
||
|
||
monkeypatch.setattr(client.urllib.parse, "urlsplit", urlsplit)
|
||
with pytest.raises(client.PolicyError):
|
||
client.read(path, token="runtime", opener=opener)
|
||
assert split_called is False
|
||
assert opener_called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"path",
|
||
[
|
||
"/api/v1/repos/atlas/cassandra/pulls/1?",
|
||
"//scm.bstein.dev/api/v1/repos/atlas/cassandra",
|
||
"/api/v1/repos/atlas/cassandra/./pulls/1",
|
||
"/api/v1/repos/atlas/cassandra/../admin",
|
||
"/api/v1/repos/atlas/cassandra//pulls/1",
|
||
"/api/v1/repos/atlas/cassandra/pulls/1?limit=01",
|
||
],
|
||
)
|
||
def test_noncanonical_round_trip_or_segments_never_reach_opener(path: str):
|
||
client = _load()
|
||
opener_called = False
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
return Response(b"{}")
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.read(path, token="runtime", opener=opener)
|
||
assert opener_called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"path",
|
||
[
|
||
"/api/v1/repos/atlas/cassandra",
|
||
"/api/v1/repos/atlas/cassandra/pulls?state=open&limit=20&page=1",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7/commits?limit=20",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7/files?page=1",
|
||
"/api/v1/repos/atlas/cassandra/branches",
|
||
"/api/v1/repos/atlas/cassandra/branches/main",
|
||
"/api/v1/repos/atlas/cassandra/commits?limit=10",
|
||
f"/api/v1/repos/atlas/cassandra/git/commits/{HEAD_SHA}",
|
||
f"/api/v1/repos/atlas/cassandra/commits/{HEAD_SHA}/status",
|
||
f"/api/v1/repos/atlas/cassandra/commits/{HEAD_SHA}/statuses?limit=10",
|
||
f"/api/v1/repos/atlas/cassandra/statuses/{HEAD_SHA}?page=1",
|
||
],
|
||
)
|
||
def test_explicit_read_allowlist_accepts_only_engineering_metadata(path: str):
|
||
client = _load()
|
||
|
||
request = client.build_request(
|
||
"GET", path, base_url=client.CANONICAL_BASE_URL, token="runtime"
|
||
)
|
||
assert request.method == "GET"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"path",
|
||
[
|
||
"/api/v1/repos/atlas/cassandra/hooks",
|
||
"/api/v1/repos/atlas/cassandra/actions/secrets",
|
||
"/api/v1/repos/atlas/cassandra/actions/variables",
|
||
"/api/v1/repos/atlas/cassandra/collaborators",
|
||
"/api/v1/repos/atlas/cassandra/branch_protections",
|
||
"/api/v1/repos/atlas/cassandra/keys",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7/reviews",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7/merge",
|
||
"/api/v1/repos/atlas/cassandra/pulls/7.diff",
|
||
"/api/v1/repos/atlas/cassandra/releases",
|
||
],
|
||
)
|
||
def test_privileged_or_content_routes_are_denied_even_for_get(path: str):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError, match="outside the metadata read allowlist"):
|
||
client.authorize_request("GET", path, None)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"path",
|
||
[
|
||
"/api/v1/repos/atlas/cassandra/pulls?limit=51",
|
||
"/api/v1/repos/atlas/cassandra/pulls?state=merged",
|
||
"/api/v1/repos/atlas/cassandra/pulls?private=true",
|
||
"/api/v1/repos/atlas/cassandra/pulls?limit=1&limit=2",
|
||
"/api/v1/repos/atlas/cassandra?p=1",
|
||
],
|
||
)
|
||
def test_read_query_is_bounded(path: str):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.authorize_request("GET", path, None)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"query",
|
||
[
|
||
"page=0",
|
||
"page=10001",
|
||
"page=01",
|
||
"limit=0",
|
||
"limit=51",
|
||
"limit=01",
|
||
"page=" + "9" * 4000,
|
||
"page=0",
|
||
"page=%EF%BC%90",
|
||
"state=%6fpen",
|
||
"p%61ge=1",
|
||
"page=1&page=2",
|
||
"page=1&" + "x" * 17 + "=1",
|
||
"state=" + "x" * 17,
|
||
"page=1&limit=2&state=open&extra=3",
|
||
],
|
||
)
|
||
def test_read_query_requires_canonical_bounded_ascii(query: str):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.authorize_request(
|
||
"GET", f"/api/v1/repos/atlas/cassandra/pulls?{query}", None
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"suffix",
|
||
[
|
||
"pulls/{number}",
|
||
"pulls/{number}.patch",
|
||
"pulls/{number}.diff",
|
||
"pulls/{number}/commits",
|
||
"pulls/{number}/files",
|
||
"commits/{number}/status",
|
||
"commits/{number}/statuses",
|
||
"statuses/{number}",
|
||
"branches/{number}",
|
||
],
|
||
)
|
||
def test_oversized_numeric_or_captured_path_never_reaches_opener(suffix: str):
|
||
client = _load()
|
||
called = False
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(b"{}")
|
||
|
||
path = "/api/v1/repos/atlas/cassandra/" + suffix.format(number="9" * 4000)
|
||
with pytest.raises(client.PolicyError):
|
||
client.read(path, token="runtime", opener=opener)
|
||
assert called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"number",
|
||
["0", "01", "2147483648", "12", "%31", "12345678901"],
|
||
)
|
||
@pytest.mark.parametrize("tail", ["", ".patch", ".diff", "/commits", "/files"])
|
||
def test_noncanonical_or_out_of_range_pr_number_never_reaches_opener(
|
||
number: str, tail: str
|
||
):
|
||
client = _load()
|
||
called = False
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(b"{}")
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.read(
|
||
f"/api/v1/repos/atlas/cassandra/pulls/{number}{tail}",
|
||
token="runtime",
|
||
opener=opener,
|
||
)
|
||
assert called is False
|
||
|
||
|
||
def test_maximum_bounded_pr_number_is_readable():
|
||
client = _load()
|
||
called = False
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(b"{}")
|
||
|
||
assert (
|
||
client.read(
|
||
"/api/v1/repos/atlas/cassandra/pulls/2147483647",
|
||
token="runtime",
|
||
opener=opener,
|
||
)
|
||
== b"{}"
|
||
)
|
||
assert called is True
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("method", "path", "data"),
|
||
[
|
||
("DELETE", "/api/v1/repos/atlas/cassandra/pulls/4", None),
|
||
("POST", "/api/v1/repos/atlas/cassandra/pulls/4/merge", {}),
|
||
(
|
||
"POST",
|
||
"/api/v1/repos/atlas/cassandra/pulls/4/reviews",
|
||
{"event": "APPROVED"},
|
||
),
|
||
("PATCH", "/api/v1/repos/atlas/cassandra/pulls/4", {"title": "WIP: x"}),
|
||
("PUT", "/api/v1/repos/atlas/cassandra/branches/main", {}),
|
||
],
|
||
)
|
||
def test_merge_approve_close_delete_update_and_other_mutations_are_rejected(
|
||
method: str, path: str, data: object
|
||
):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client.build_request(
|
||
method, path, base_url=client.CANONICAL_BASE_URL, token="secret", data=data
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"ref",
|
||
[
|
||
"foo/.bar",
|
||
"foo/bar.lock/baz",
|
||
"foo..bar",
|
||
"foo@{bar",
|
||
"foo//bar",
|
||
"-danger",
|
||
"danger.",
|
||
"danger~one",
|
||
"danger^one",
|
||
"danger:one",
|
||
"danger one",
|
||
],
|
||
)
|
||
def test_complete_git_ref_validation_rejects_invalid_names(ref: str):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError):
|
||
client._validate_ref(ref, "head")
|
||
|
||
|
||
def test_git_ref_validation_uses_fixed_trusted_binary():
|
||
client = _load()
|
||
|
||
assert client._validate_ref.__globals__["GIT_BIN"] == "/usr/bin/git"
|
||
assert client._validate_ref("hermes/valid-fix", "head") == "hermes/valid-fix"
|
||
|
||
|
||
@pytest.mark.parametrize("field", ["base", "head"])
|
||
@pytest.mark.parametrize("oversized", ["r" * 100_000, "🧪" * 128])
|
||
def test_oversized_ref_never_invokes_git_request_or_opener(
|
||
field: str, oversized: str, monkeypatch
|
||
):
|
||
client = _load()
|
||
git_called = False
|
||
request_built = False
|
||
opener_called = False
|
||
original_build_request = client.build_request
|
||
|
||
def git_run(*_args, **_kwargs):
|
||
nonlocal git_called
|
||
git_called = True
|
||
raise AssertionError("Git must not receive an oversized ref")
|
||
|
||
def build_request(*args, **kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
return original_build_request(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
return Response(_draft_response())
|
||
|
||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||
client.build_request = build_request
|
||
refs = {"base": "main", "head": "hermes/fix"}
|
||
refs[field] = oversized
|
||
with pytest.raises(client.PolicyError, match="branch-name limit"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base=refs["base"],
|
||
head=refs["head"],
|
||
head_sha=HEAD_SHA,
|
||
title="Focused fix",
|
||
body="Review evidence",
|
||
token="runtime",
|
||
opener=opener,
|
||
)
|
||
assert git_called is False
|
||
assert request_built is False
|
||
assert opener_called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("field", "oversized"),
|
||
[("repo", "r" * 101), ("title", "🧪" * 200), ("body", "🧪" * 9_000)],
|
||
)
|
||
def test_other_text_bounds_fail_before_git_request_or_opener(
|
||
field: str, oversized: str, monkeypatch
|
||
):
|
||
client = _load()
|
||
git_called = False
|
||
request_built = False
|
||
opener_called = False
|
||
original_build_request = client.build_request
|
||
|
||
def git_run(*_args, **_kwargs):
|
||
nonlocal git_called
|
||
git_called = True
|
||
raise AssertionError("Git must not run before cheap input bounds")
|
||
|
||
def build_request(*args, **kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
return original_build_request(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
return Response(_draft_response())
|
||
|
||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||
client.build_request = build_request
|
||
values = {
|
||
"repo": "cassandra",
|
||
"title": "Focused fix",
|
||
"body": "Review evidence",
|
||
}
|
||
values[field] = oversized
|
||
with pytest.raises(client.PolicyError):
|
||
client.create_draft(
|
||
values["repo"],
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title=values["title"],
|
||
body=values["body"],
|
||
token="runtime",
|
||
opener=opener,
|
||
)
|
||
assert git_called is False
|
||
assert request_built is False
|
||
assert opener_called is False
|
||
|
||
|
||
def test_create_forces_draft_title_and_same_repository_branch_names():
|
||
client = _load()
|
||
assert (
|
||
client.authorize_request(
|
||
"POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload()
|
||
)
|
||
== "create-draft"
|
||
)
|
||
with pytest.raises(client.PolicyError, match="draft-title prefix"):
|
||
client.authorize_request(
|
||
"POST",
|
||
"/api/v1/repos/atlas/cassandra/pulls",
|
||
_draft_payload(title="Not a draft"),
|
||
)
|
||
with pytest.raises(client.PolicyError):
|
||
client.authorize_request(
|
||
"POST",
|
||
"/api/v1/repos/atlas/cassandra/pulls",
|
||
_draft_payload(head="someone:branch"),
|
||
)
|
||
|
||
|
||
def test_runtime_token_is_only_an_authorization_header():
|
||
client = _load()
|
||
request = client.build_request(
|
||
"POST",
|
||
"/api/v1/repos/atlas/cassandra/pulls",
|
||
base_url=client.CANONICAL_BASE_URL,
|
||
token="do-not-leak",
|
||
data=_draft_payload(),
|
||
)
|
||
|
||
assert "do-not-leak" not in request.full_url
|
||
assert b"do-not-leak" not in request.data
|
||
assert request.get_header("Authorization") == "token do-not-leak"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"sensitive",
|
||
[
|
||
"client_" + "secret=not-a-real-value",
|
||
'{"client_' + 'secret":\n"synthetic-value"}',
|
||
'{"client_' + 'secret"\n:\n"synthetic-value"}',
|
||
'{"client\\u005f' + 'secret":"synthetic-value"}',
|
||
"client_" + "secret:\n synthetic-value",
|
||
'{"client\\q' + 'secret":"synthetic-value"}',
|
||
'{"client\n' + 'secret":"synthetic-value"}',
|
||
'"client_' + 'secret"\x0b:\n"synthetic-value"',
|
||
"ACCESS_" + "TOKEN = 'not-a-real-value'",
|
||
'{"refresh_' + 'token": "not-a-real-value"}',
|
||
"private_" + "key: not-a-real-value",
|
||
"AWS_SECRET_ACCESS_" + "KEY=not-a-real-value",
|
||
"aws_access_key_" + "id: not-a-real-value",
|
||
"AWS_SESSION_" + "TOKEN = 'not-a-real-value'",
|
||
'{"AccessKey' + 'Id":"not-a-real-value"}',
|
||
'{"SecretAccess' + 'Key":"not-a-real-value"}',
|
||
'{"Session' + 'Token":"not-a-real-value"}',
|
||
"aws-security-" + "token: not-a-real-value",
|
||
"Account" + "Key=not-a-real-value",
|
||
"SharedAccess" + "Signature: not-a-real-value",
|
||
"AZURE_STORAGE_CONNECTION_" + "STRING='not-a-real-value'",
|
||
"DefaultEndpointsProtocol=https;AccountName=fake;Account"
|
||
+ "Key=not-a-real-value;EndpointSuffix=example",
|
||
"?sv=2024-11-04&ss=b&srt=sco&sp=rwdlac&se=2099-01-01&sig=" + "not-a-real-value",
|
||
"DOCKER_AUTH_" + "CONFIG='not-a-real-value'",
|
||
'{"auths":{"registry.example":{"auth":"bm90LXJlYWw="}}}',
|
||
'{"identity' + 'token":"not-a-real-value"}',
|
||
'{"type":"service_' + 'account","client_email":"fake@example.test"}',
|
||
'{"private_key_' + 'id":"not-a-real-value"}',
|
||
'{"client_' + 'email":"fake@example.test"}',
|
||
"GOOGLE_CREDENTIALS" + "=not-a-real-value",
|
||
"personal_access_" + "token: not-a-real-value",
|
||
"GITEA_" + "TOKEN=not-a-real-value",
|
||
"gitlab-token" + ": not-a-real-value",
|
||
"pat" + "=not-a-real-value",
|
||
"Authorization: " + "Bearer not-a-real-credential-value",
|
||
"authorization = " + '"Basic not-a-real-credential-value"',
|
||
"Bearer" + "=not-a-real-credential-value",
|
||
"Basic" + ": not-a-real-credential-value",
|
||
"pass" + "word=not-a-real-credential",
|
||
"ghp_" + "notarealcredentialvalue123456",
|
||
"github_pat_" + "notarealcredentialvalue123456",
|
||
"glpat-" + "notarealcredentialvalue123456",
|
||
"xoxb-" + "not-a-real-credential-value-123456",
|
||
"sk-ant-" + "notarealcredentialvalue123456",
|
||
"sk-proj-" + "notarealcredentialvalue123456",
|
||
"sk_live_" + "notarealcredentialvalue123456",
|
||
"ya29." + "notarealcredentialvalue123456",
|
||
"gta_" + "notarealcredentialvalue123456",
|
||
"whsec_" + "notarealcredentialvalue123456",
|
||
"npm_" + "notarealcredentialvalue123456",
|
||
"pypi-" + "notarealcredentialvalue123456789012345",
|
||
"hf_" + "notarealcredentialvalue123456",
|
||
"SG." + "notarealvalue1234" + ".notarealcredentialvalue123456",
|
||
"SK" + "a" * 32,
|
||
"https://hooks.slack.com/services/" + "T000/B000/notarealvalue123456",
|
||
"https://discord.com/api/webhooks/123456789/" + "notarealcredentialvalue123456",
|
||
"https://fake.webhook.office.com/" + "notarealcredentialvalue123456",
|
||
"webhook_" + "url=https://example.test/not-real",
|
||
"FutureCloudSigning" + "Credential=not-a-real-value",
|
||
"future-client-signing-" + "key: not-a-real-value",
|
||
"future_client_signing_" + "key='not-a-real-value'",
|
||
'{"serviceAccountPrivate' + 'Key":"not-a-real-value"}',
|
||
"CONTAINER_REGISTRY_" + "CREDENTIAL=not-a-real-value",
|
||
"someWebhookSigning" + "Secret: not-a-real-value",
|
||
"client" + "Key=not-a-real-value",
|
||
"session" + "Key: not-a-real-value",
|
||
"access" + "Id=not-a-real-value",
|
||
"credentials" + ": {user: fake}",
|
||
"private" + "Key: |",
|
||
"nuget_api_" + "key=not-a-real-value",
|
||
"oy2" + "a" * 44,
|
||
"sk_test_" + "notarealcredentialvalue123456",
|
||
"A1b2C3d4E5f6G7h8I9j0K_l-M+n/O=pQ2rS3tU4vW5xY6zZ7aB8cC9d",
|
||
"AKIA" + "A" * 16,
|
||
"AIza" + "a" * 35,
|
||
"-----BEGIN OPENSSH " + "PRIVATE KEY-----",
|
||
"ssh-ed25519 " + "bm90YXJlYWxjcmVkZW50aWFsdmFsdWU=",
|
||
"eyJnotarealheader." + "notarealpayloadvalue." + "notarealsignature",
|
||
],
|
||
)
|
||
@pytest.mark.parametrize("field", ["title", "body"])
|
||
def test_create_rejects_expanded_credential_shapes_before_network(
|
||
sensitive: str, field: str
|
||
):
|
||
client = _load()
|
||
called = False
|
||
request_built = False
|
||
|
||
original_build_request = client.build_request
|
||
|
||
def build_request(*args, **kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
return original_build_request(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(_draft_response())
|
||
|
||
client.build_request = build_request
|
||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||
values[field] = sensitive
|
||
|
||
with pytest.raises(client.PolicyError, match="credential material"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title=values["title"],
|
||
body=values["body"],
|
||
token="runtime-sentinel",
|
||
opener=opener,
|
||
)
|
||
assert request_built is False
|
||
assert called is False
|
||
|
||
|
||
def test_very_long_compact_body_is_rejected_before_request_or_network():
|
||
client = _load()
|
||
request_built = False
|
||
opener_called = False
|
||
original_build_request = client.build_request
|
||
|
||
def build_request(*args, **kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
return original_build_request(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
return Response(_draft_response())
|
||
|
||
client.build_request = build_request
|
||
with pytest.raises(client.PolicyError, match="credential material"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title="Focused fix",
|
||
body="a" * 300,
|
||
token="runtime-sentinel",
|
||
opener=opener,
|
||
)
|
||
assert request_built is False
|
||
assert opener_called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"safe_text",
|
||
[
|
||
"AWS_SECRET_ACCESS_KEY is injected at runtime",
|
||
"Token: reject empty values",
|
||
"Authorization = preserve header behavior",
|
||
"Password: add regression",
|
||
"Review the Authorization header behavior",
|
||
"Bearer authentication is required for this route",
|
||
"Document Docker auths payload rejection",
|
||
"The client_email field belongs to service accounts",
|
||
"AccountKey assignments must be blocked",
|
||
"This patch changes token validation without including a value",
|
||
"FutureCloudSigningCredential handling needs a regression test",
|
||
"The clientKey name is documented without an assigned value",
|
||
"A SHA-256 digest 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef is evidence",
|
||
],
|
||
)
|
||
def test_credential_policy_keeps_normal_engineering_prose_usable(safe_text: str):
|
||
client = _load()
|
||
|
||
assert client._validate_body(safe_text) == safe_text
|
||
assert client._draft_title(safe_text) == f"WIP: {safe_text}"
|
||
|
||
|
||
@pytest.mark.parametrize("field", ["title", "body"])
|
||
def test_create_rejects_exact_runtime_token_before_network(field: str):
|
||
client = _load()
|
||
called = False
|
||
request_built = False
|
||
|
||
original_build_request = client.build_request
|
||
|
||
def build_request(*args, **kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
return original_build_request(*args, **kwargs)
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(_draft_response())
|
||
|
||
client.build_request = build_request
|
||
runtime_token = "exact-random-runtime-sentinel-7b73ac61"
|
||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||
values[field] = f"Accidental {runtime_token} value"
|
||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title=values["title"],
|
||
body=values["body"],
|
||
token=runtime_token,
|
||
opener=opener,
|
||
)
|
||
assert request_built is False
|
||
assert called is False
|
||
|
||
|
||
@pytest.mark.parametrize("field", ["repo", "base", "head", "title", "body"])
|
||
def test_every_public_field_rejects_exact_runtime_token_before_git_or_network(
|
||
field: str, monkeypatch
|
||
):
|
||
client = _load()
|
||
git_called = False
|
||
request_built = False
|
||
opener_called = False
|
||
runtime_token = "runtime-sentinel"
|
||
values = {
|
||
"repo": "cassandra",
|
||
"base": "main",
|
||
"head": "hermes/fix",
|
||
"title": "Focused fix",
|
||
"body": "Review evidence",
|
||
}
|
||
values[field] = runtime_token
|
||
|
||
def git_run(*_args, **_kwargs):
|
||
nonlocal git_called
|
||
git_called = True
|
||
raise AssertionError("Git must not run for a credential-bearing field")
|
||
|
||
def build_request(*_args, **_kwargs):
|
||
nonlocal request_built
|
||
request_built = True
|
||
raise AssertionError("a request must not be built")
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal opener_called
|
||
opener_called = True
|
||
raise AssertionError("the opener must not be called")
|
||
|
||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||
monkeypatch.setattr(client, "build_request", build_request)
|
||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||
client.create_draft(
|
||
values["repo"],
|
||
base=values["base"],
|
||
head=values["head"],
|
||
head_sha=HEAD_SHA,
|
||
title=values["title"],
|
||
body=values["body"],
|
||
token=runtime_token,
|
||
opener=opener,
|
||
)
|
||
assert git_called is False
|
||
assert request_built is False
|
||
assert opener_called is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"sensitive",
|
||
[
|
||
" ".join(["{}"] * 32) + ' {"client_secret":"synthetic-value"}',
|
||
"prefix_ghp_notarealcredentialvalue123456_suffix",
|
||
"client_secret: correct horse battery staple",
|
||
"'client_secret':\n synthetic-value",
|
||
],
|
||
)
|
||
@pytest.mark.parametrize("field", ["title", "body"])
|
||
def test_structured_scanner_closes_bounded_and_wrapped_token_bypasses(
|
||
sensitive: str, field: str
|
||
):
|
||
client = _load()
|
||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||
values[field] = sensitive
|
||
called = False
|
||
|
||
def opener(*_args, **_kwargs):
|
||
nonlocal called
|
||
called = True
|
||
return Response(_draft_response())
|
||
|
||
with pytest.raises(client.PolicyError, match="credential material"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title=values["title"],
|
||
body=values["body"],
|
||
token="runtime-sentinel",
|
||
opener=opener,
|
||
)
|
||
assert called is False
|
||
|
||
|
||
@pytest.mark.parametrize("status", [200, 202, 204, 206])
|
||
def test_create_accepts_only_http_201(status: int):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title="Focused fix",
|
||
body="Review evidence",
|
||
token="runtime",
|
||
opener=lambda *_a, **_k: Response(_draft_response(), status=status),
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("status", [201, 202, 204, 206])
|
||
def test_read_accepts_only_http_200(status: int):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||
client.read(
|
||
"/api/v1/repos/atlas/cassandra",
|
||
token="runtime",
|
||
opener=lambda *_a, **_k: Response(b"{}", status=status),
|
||
)
|
||
|
||
|
||
def test_successful_create_validates_each_ref_once(monkeypatch):
|
||
client = _load()
|
||
calls = []
|
||
|
||
def git_run(command, **_kwargs):
|
||
calls.append(command)
|
||
return type("Result", (), {"returncode": 0})()
|
||
|
||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||
client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title="Focused fix",
|
||
body="Review evidence",
|
||
token="runtime",
|
||
opener=lambda *_a, **_k: Response(_draft_response()),
|
||
)
|
||
|
||
assert [command[-1] for command in calls] == [
|
||
"refs/heads/main",
|
||
"refs/heads/hermes/fix",
|
||
]
|
||
|
||
|
||
def test_create_verifies_every_server_postcondition():
|
||
client = _load()
|
||
calls = []
|
||
|
||
def opener(request, timeout):
|
||
calls.append((request, timeout))
|
||
return Response(_draft_response())
|
||
|
||
result = client.create_draft(
|
||
"cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title="Focused fix",
|
||
body="Review evidence",
|
||
token="runtime",
|
||
opener=opener,
|
||
)
|
||
|
||
assert json.loads(result) == _draft_response()
|
||
payload = json.loads(calls[0][0].data)
|
||
assert payload == {
|
||
"base": "main",
|
||
"body": "Review evidence",
|
||
"head": "hermes/fix",
|
||
"title": "WIP: Focused fix",
|
||
}
|
||
assert calls[0][1] == 30
|
||
|
||
|
||
def test_create_postcondition_rejects_every_material_mismatch():
|
||
client = _load()
|
||
mutations = [
|
||
("number", 0),
|
||
("number", 2_147_483_648),
|
||
("state", "closed"),
|
||
("draft", False),
|
||
("merged", True),
|
||
("html_url", "https://evil.example/pulls/3"),
|
||
("url", "https://evil.example/api/pulls/3"),
|
||
("title", "Focused fix"),
|
||
("body", "different"),
|
||
]
|
||
documents = []
|
||
for key, value in mutations:
|
||
document = _draft_response()
|
||
document[key] = value
|
||
documents.append(document)
|
||
for path, value in [
|
||
(("base", "ref"), "master"),
|
||
(("base", "repo", "full_name"), "evil/cassandra"),
|
||
(("head", "ref"), "other"),
|
||
(("head", "sha"), "0" * 40),
|
||
(("head", "repo", "full_name"), "evil/cassandra"),
|
||
]:
|
||
document = copy.deepcopy(_draft_response())
|
||
target = document
|
||
for key in path[:-1]:
|
||
target = target[key]
|
||
target[path[-1]] = value
|
||
documents.append(document)
|
||
|
||
for document in documents:
|
||
with pytest.raises(client.PolicyError):
|
||
client._require_create_response(
|
||
json.dumps(document).encode(),
|
||
repo="cassandra",
|
||
base="main",
|
||
head="hermes/fix",
|
||
head_sha=HEAD_SHA,
|
||
title="WIP: Focused fix",
|
||
body="Review evidence",
|
||
)
|
||
|
||
|
||
def test_read_response_is_bounded():
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError, match="safe size limit"):
|
||
client.read(
|
||
"/api/v1/repos/atlas/cassandra",
|
||
token="runtime",
|
||
opener=lambda *_a, **_k: Response(b"x" * (client.MAX_RESPONSE_BYTES + 1)),
|
||
)
|
||
|
||
|
||
def test_direct_api_rejects_unexpected_success_content_type():
|
||
client = _load()
|
||
response = Response(b"{}")
|
||
response.headers.replace_header("Content-Type", "text/html")
|
||
|
||
with pytest.raises(client.PolicyError, match="unexpected response type"):
|
||
client.read(
|
||
"/api/v1/repos/atlas/cassandra",
|
||
token="runtime",
|
||
opener=lambda *_a, **_k: response,
|
||
)
|
||
|
||
|
||
def test_output_redaction_covers_exact_token_and_authorization_header():
|
||
client = _load()
|
||
raw = b'{"message":"do-not-leak","debug":"Authorization: token do-not-leak"}'
|
||
redacted = client.redact_bytes(raw, "do-not-leak")
|
||
|
||
assert b"do-not-leak" not in redacted
|
||
assert redacted.count(b"[REDACTED]") >= 1
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"reflected",
|
||
[
|
||
b"runtime-sentinel",
|
||
base64.b64encode(b"runtime-sentinel"),
|
||
base64.b64encode(b"hermes-automation:runtime-sentinel"),
|
||
],
|
||
)
|
||
def test_direct_api_rejects_credential_reflection(reflected: bytes):
|
||
client = _load()
|
||
|
||
with pytest.raises(client.PolicyError, match="credential material"):
|
||
client.read(
|
||
"/api/v1/repos/atlas/cassandra",
|
||
token="runtime-sentinel",
|
||
opener=lambda *_a, **_k: Response(
|
||
b'{"unexpected":"' + reflected + b'"}'
|
||
),
|
||
)
|