-
Notifications
You must be signed in to change notification settings - Fork 0
fix(sdp): unblock CI — fuzz OOM + crash-log visibility + base SAST remediation #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
b4254a9
fix(fuzz): surface Atheris crash reproducers in the run log
claude 25fe95c
fix(fuzz): bound in-memory evidence growth to stop execute_query OOM
claude 82911bd
fix(security): remediate base-branch Semgrep SAST findings blocking PRs
claude cc6dbeb
test(authz): cover JWKS URL scheme guard (unblock coverage-evidence)
claude 47386e7
test(authz): reach 100% coverage of the OIDC/JWKS module
claude 38ab65f
test(authz): drop constant-condition ternary flagged by code-quality
claude d1691c1
test(policy): pin governance deny-paths (policy.py 88% -> 98%)
claude 23909c7
test(browse): cover PII-masking + policy-deny/validation paths (brows…
claude aec9570
test(observability): cover log-sink export + header extraction (80% -…
claude cbaf7c8
test(config): cover the DB-backed KV config loader (config.py 77% -> …
claude a504523
test(orchestrator): pin SQL-safety guards + query-draft branches (89%…
claude 529aa03
test(connectors): pin source-connector contract guards (84% -> 100%)
claude a72033a
test(sdp): cover embeddings, connector-secret status, semantic valida…
claude 6263c0c
test(sdp): bring catalog + demo_smoke to 100% line coverage
claude 03f0225
chore: refresh head for last-push approval
opencode-agent[bot] 02a63c6
docs(fuzz): document all reproducer artifact types
seonghobae a71f4b4
fix(fuzz): emit shell-safe absolute replay command
seonghobae 492ea6e
test(authz): prove rejected JWKS schemes never fetch
seonghobae c73b872
test(credentials): remove vacuous secret exposure assertion
seonghobae 88e4f39
fix(catalog): preserve schema fields in patch requests
seonghobae c257ae4
test(catalog): exercise schema patch through public contract
seonghobae 03fe04c
test(browse): require masking integration fixture
seonghobae bd8b307
chore(ci): retrigger required review pipeline at same tree
claude 2ba30b9
chore(ci): re-fire synchronize after Actions event-processing outage
claude 8130be1
chore: refresh head for last-push approval
opencode-agent[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| """Tests for OIDC JWKS loading, focused on the URL-scheme hardening.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
|
|
||
| import pytest | ||
|
|
||
| from sdp import authz | ||
|
|
||
|
|
||
| def test_load_jwks_from_url_rejects_non_http_schemes(monkeypatch): | ||
| """A misconfigured non-http(s) JWKS URL must be rejected before any fetch, | ||
| so urllib's ``file://`` support cannot be turned into local file disclosure.""" | ||
| unexpected_calls = [] | ||
|
|
||
| def _unexpected_urlopen(*args, **kwargs): | ||
| unexpected_calls.append((args, kwargs)) | ||
| raise AssertionError("urlopen must not be called for a rejected URL scheme") | ||
|
|
||
| monkeypatch.setattr(authz, "urlopen", _unexpected_urlopen) | ||
| for bad_url in ("file:///etc/passwd", "ftp://host/keys.json", "gopher://x", ""): | ||
| with pytest.raises(ValueError): | ||
| authz._load_jwks_from_url(bad_url) | ||
|
|
||
| assert unexpected_calls == [] | ||
|
|
||
|
|
||
| def test_load_jwks_from_url_fetches_over_https(monkeypatch): | ||
| """An https JWKS URL passes the scheme allow-list and its JSON body is | ||
| parsed and returned.""" | ||
| payload = {"keys": [{"kid": "abc", "kty": "RSA"}]} | ||
|
|
||
| class _FakeResponse: | ||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, *exc): | ||
| return False | ||
|
|
||
| def read(self): | ||
| return json.dumps(payload).encode("utf-8") | ||
|
|
||
| captured = {} | ||
|
|
||
| def _fake_urlopen(url, timeout=None): | ||
| captured["url"] = url | ||
| captured["timeout"] = timeout | ||
| return _FakeResponse() | ||
|
|
||
| monkeypatch.delenv("SDP_OIDC_JWKS_TIMEOUT_SECONDS", raising=False) | ||
| monkeypatch.setattr(authz, "urlopen", _fake_urlopen) | ||
| result = authz._load_jwks_from_url("https://idp.example/.well-known/jwks.json") | ||
|
|
||
| assert result == payload | ||
| assert captured["url"] == "https://idp.example/.well-known/jwks.json" | ||
| assert captured["timeout"] == pytest.approx(2.0) | ||
|
seonghobae marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def test_load_jwks_from_url_honours_timeout_override(monkeypatch): | ||
| """The JWKS fetch timeout is configurable via SDP_OIDC_JWKS_TIMEOUT_SECONDS.""" | ||
| monkeypatch.setenv("SDP_OIDC_JWKS_TIMEOUT_SECONDS", "5") | ||
|
|
||
| class _FakeResponse: | ||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, *exc): | ||
| return False | ||
|
|
||
| def read(self): | ||
| return b"{}" | ||
|
|
||
| seen = {} | ||
|
|
||
| def _fake_urlopen(url, timeout=None): | ||
| seen["timeout"] = timeout | ||
| return _FakeResponse() | ||
|
|
||
| monkeypatch.setattr(authz, "urlopen", _fake_urlopen) | ||
| assert authz._load_jwks_from_url("http://localhost:8080/jwks") == {} | ||
| assert seen["timeout"] == pytest.approx(5.0) | ||
|
|
||
|
|
||
| # --- OIDC claim/role/JWK guard branches (security-critical error paths) --- | ||
|
|
||
| import time | ||
|
|
||
| import jwt as _jwt | ||
|
|
||
|
|
||
| def test_claim_values_handles_str_list_none_and_scalar(): | ||
| assert authz._claim_values(None) == [] | ||
| assert authz._claim_values("one") == ["one"] | ||
| assert authz._claim_values(["a", 2]) == ["a", "2"] | ||
| assert authz._claim_values(7) == ["7"] # non-str, non-list scalar | ||
|
|
||
|
|
||
| def test_load_oidc_role_map_default_and_override(monkeypatch): | ||
| monkeypatch.delenv("SDP_OIDC_GROUP_ROLE_MAP", raising=False) | ||
| assert authz.load_oidc_role_map() == authz._DEFAULT_OIDC_GROUP_ROLE_MAP | ||
|
|
||
| monkeypatch.setenv("SDP_OIDC_GROUP_ROLE_MAP", '{"grp": ["data-analyst"]}') | ||
| assert authz.load_oidc_role_map() == {"grp": ["data-analyst"]} | ||
|
|
||
| monkeypatch.setenv("SDP_OIDC_GROUP_ROLE_MAP", "[]") | ||
| with pytest.raises(ValueError): | ||
| authz.load_oidc_role_map() | ||
|
|
||
|
|
||
| def _valid_claims(**overrides): | ||
| claims = { | ||
| "preferred_username": "alice", | ||
| "tenant_id": "demo", | ||
| "exp": int(time.time()) + 3600, | ||
| } | ||
| claims.update(overrides) | ||
| return claims | ||
|
|
||
|
|
||
| def test_validate_oidc_claim_shape_guard_branches(): | ||
| authz.validate_oidc_claim_shape(_valid_claims()) # happy path | ||
| with pytest.raises(ValueError): # missing subject | ||
| authz.validate_oidc_claim_shape({"tenant_id": "d", "exp": int(time.time()) + 60}) | ||
| with pytest.raises(ValueError): # missing tenant | ||
| authz.validate_oidc_claim_shape({"sub": "s", "exp": int(time.time()) + 60}) | ||
| with pytest.raises(ValueError): # missing exp | ||
| authz.validate_oidc_claim_shape({"sub": "s", "tenant_id": "d"}) | ||
| with pytest.raises(ValueError): # invalid exp type | ||
| authz.validate_oidc_claim_shape({"sub": "s", "tenant_id": "d", "exp": "soon"}) | ||
| with pytest.raises(ValueError): # expired | ||
| authz.validate_oidc_claim_shape({"sub": "s", "tenant_id": "d", "exp": 1}) | ||
|
|
||
|
|
||
| def test_select_jwk_guard_branches(): | ||
| jwks = {"keys": [{"kid": "k1", "kty": "RSA"}]} | ||
| assert authz._select_jwk(jwks, "k1")["kid"] == "k1" | ||
| with pytest.raises(ValueError): # missing kid | ||
| authz._select_jwk(jwks, None) | ||
| with pytest.raises(ValueError): # keys not a list | ||
| authz._select_jwk({"keys": "nope"}, "k1") | ||
| with pytest.raises(ValueError): # no matching kid | ||
| authz._select_jwk(jwks, "absent") | ||
|
|
||
|
|
||
| def test_verify_oidc_jwks_token_config_and_alg_guards(monkeypatch): | ||
| monkeypatch.delenv("SDP_OIDC_ISSUER", raising=False) | ||
| monkeypatch.delenv("SDP_OIDC_AUDIENCE", raising=False) | ||
| monkeypatch.delenv("SDP_OIDC_JWKS_URL", raising=False) | ||
| with pytest.raises(ValueError): # missing issuer | ||
| authz.verify_oidc_jwks_token("t", jwks={"keys": []}) | ||
| with pytest.raises(ValueError): # missing audience | ||
| authz.verify_oidc_jwks_token("t", issuer="iss", jwks={"keys": []}) | ||
| with pytest.raises(ValueError): # jwks is None and no JWKS URL configured | ||
| authz.verify_oidc_jwks_token("t", issuer="iss", audience="aud") | ||
|
|
||
| # Unsupported algorithm is rejected before signature verification. | ||
| hs_token = _jwt.encode({"sub": "s"}, "secret", algorithm="HS256") | ||
| with pytest.raises(ValueError): | ||
| authz.verify_oidc_jwks_token(hs_token, issuer="iss", audience="aud", jwks={"keys": []}) | ||
|
|
||
|
|
||
| def test_verify_oidc_jwks_token_loads_jwks_from_env_url(monkeypatch): | ||
| monkeypatch.setenv("SDP_OIDC_JWKS_URL", "https://idp.example/jwks") | ||
|
|
||
| def _fake_load(url): | ||
| assert url == "https://idp.example/jwks" | ||
| return {"keys": []} | ||
|
|
||
| monkeypatch.setattr(authz, "_load_jwks_from_url", _fake_load) | ||
| # Unsupported alg is rejected after the env JWKS is loaded -> wrapped ValueError, | ||
| # which exercises the `jwks = _load_jwks_from_url(jwks_url)` branch. | ||
| hs_token = _jwt.encode({"sub": "s"}, "secret", algorithm="HS256") | ||
| with pytest.raises(ValueError): | ||
| authz.verify_oidc_jwks_token(hs_token, issuer="iss", audience="aud") | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.