fix(harness): redact secret-named meta values in the customization manifest (rcust)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Adversary heads-up (inbox 2026-06-10T19:06Z): meta values are repo-public by construction, but the manifest lands on the dashboard — a field literally named SECRET_KEY_BASE showing a value (plausible's committed CI dummy) is needless secret-scan noise. Mask values whose key NAME is secret-shaped (SECRET|PASSWORD|TOKEN|CREDENTIAL|word-segment KEY), top-level and nested dict keys; the key name stays visible. Unit test pins redacted vs passthrough (KEYCLOAK_URL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -16,14 +16,26 @@ from . import meta as meta_mod
|
|||||||
|
|
||||||
_PRE_OP_RE = re.compile(r"^def (pre_[a-z]+)\(", re.MULTILINE)
|
_PRE_OP_RE = re.compile(r"^def (pre_[a-z]+)\(", re.MULTILINE)
|
||||||
|
|
||||||
|
# Meta values are repo-public by construction (recipe_meta.py is committed; real secrets are
|
||||||
|
# class-B generated, never meta), but the manifest lands on the dashboard — mask values whose
|
||||||
|
# key NAME is secret-shaped so a field literally called SECRET_KEY_BASE never shows a value
|
||||||
|
# (defense in depth + keeps dashboard secret-scans quiet). `KEY` matches only as a word segment
|
||||||
|
# (API_KEY yes, KEYCLOAK_URL no).
|
||||||
|
_SENSITIVE_NAME_RE = re.compile(r"SECRET|PASSWORD|TOKEN|CREDENTIAL|(^|_)KEY(_|$)", re.IGNORECASE)
|
||||||
|
|
||||||
def _jsonable(v):
|
|
||||||
|
def _jsonable(v, name=""):
|
||||||
"""Manifest values must be JSON-serializable + deterministic: hooks render as '<hook>',
|
"""Manifest values must be JSON-serializable + deterministic: hooks render as '<hook>',
|
||||||
tuples become lists."""
|
tuples become lists, secret-named entries (by key name, incl. nested dict keys) as
|
||||||
|
'<redacted>'."""
|
||||||
if callable(v):
|
if callable(v):
|
||||||
return "<hook>"
|
return "<hook>"
|
||||||
|
if name and _SENSITIVE_NAME_RE.search(name):
|
||||||
|
return "<redacted>"
|
||||||
if isinstance(v, tuple):
|
if isinstance(v, tuple):
|
||||||
return list(v)
|
return list(v)
|
||||||
|
if isinstance(v, dict):
|
||||||
|
return {k: _jsonable(x, name=str(k)) for k, x in v.items()}
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +101,7 @@ def build(recipe: str, meta, repo_local: str | None) -> dict:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"meta_non_default": {
|
"meta_non_default": {
|
||||||
k: _jsonable(v) for k, v in sorted(meta_mod.non_default(meta).items())
|
k: _jsonable(v, name=k) for k, v in sorted(meta_mod.non_default(meta).items())
|
||||||
},
|
},
|
||||||
"hooks": hooks,
|
"hooks": hooks,
|
||||||
"overlays": overlays,
|
"overlays": overlays,
|
||||||
|
|||||||
@ -132,6 +132,36 @@ def test_manifest_env_overrides_and_ci_flag(tmp_path, monkeypatch):
|
|||||||
assert "!! dev-only override active in CI" in manifest.render(RECIPE, m)
|
assert "!! dev-only override active in CI" in manifest.render(RECIPE, m)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_redacts_sensitive_named_values(tmp_path, monkeypatch):
|
||||||
|
# Meta values are repo-public by construction, but the manifest lands on the dashboard:
|
||||||
|
# secret-NAMED entries (top-level or nested dict keys, e.g. plausible's
|
||||||
|
# EXTRA_ENV["SECRET_KEY_BASE"] dummy) render as '<redacted>' — name shown, value masked.
|
||||||
|
# Non-sensitive names (incl. KEYCLOAK_* — 'KEY' matches only as a word segment) pass through.
|
||||||
|
ccci_root = tmp_path / "cc-ci-tests"
|
||||||
|
d = ccci_root / RECIPE
|
||||||
|
d.mkdir(parents=True)
|
||||||
|
(d / "recipe_meta.py").write_text(
|
||||||
|
"EXTRA_ENV = {\n"
|
||||||
|
" 'SECRET_KEY_BASE': 'dummy-ci-constant',\n"
|
||||||
|
" 'API_KEY': 'also-dummy',\n"
|
||||||
|
" 'KEYCLOAK_URL': 'https://kc.example',\n"
|
||||||
|
"}\n"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r))
|
||||||
|
monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root))
|
||||||
|
monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(tmp_path / "missing.txt"))
|
||||||
|
meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root))
|
||||||
|
m = manifest.build(RECIPE, meta, None)
|
||||||
|
assert m["meta_non_default"]["EXTRA_ENV"] == {
|
||||||
|
"SECRET_KEY_BASE": "<redacted>",
|
||||||
|
"API_KEY": "<redacted>",
|
||||||
|
"KEYCLOAK_URL": "https://kc.example",
|
||||||
|
}
|
||||||
|
out = manifest.render(RECIPE, m)
|
||||||
|
assert "dummy-ci-constant" not in out and "also-dummy" not in out
|
||||||
|
assert "SECRET_KEY_BASE" in out # the key NAME stays visible
|
||||||
|
|
||||||
|
|
||||||
def test_render_lists_every_surface(tmp_path, monkeypatch):
|
def test_render_lists_every_surface(tmp_path, monkeypatch):
|
||||||
meta, rl = _mk_synthetic(tmp_path, monkeypatch)
|
meta, rl = _mk_synthetic(tmp_path, monkeypatch)
|
||||||
out = manifest.render(RECIPE, manifest.build(RECIPE, meta, rl))
|
out = manifest.render(RECIPE, manifest.build(RECIPE, meta, rl))
|
||||||
|
|||||||
Reference in New Issue
Block a user