diff --git a/scripts/audit_c5_benchmark_evidence.py b/scripts/audit_c5_benchmark_evidence.py index 7bd30423a..a1842a678 100644 --- a/scripts/audit_c5_benchmark_evidence.py +++ b/scripts/audit_c5_benchmark_evidence.py @@ -1531,13 +1531,19 @@ def main(argv: list[str] | None = None) -> int: extra_input_paths=extra_inputs, ) if preflight_errors: - for err in preflight_errors: - print(f"preflight error: {err}", file=sys.stderr) + # Input paths may contain credentials. Do not echo validation details to stderr. + print("preflight error: unsafe or conflicting output destination", file=sys.stderr) return 2 if not result.get("manifest_valid") or result.get("configuration_valid") is False or result.get("errors"): - for err in result.get("errors", []): - print(f"audit error: {err}", file=sys.stderr) + # The structured audit result retains diagnostics for trusted callers. + # Never log its untrusted error strings, paths, or configuration values. + if not result.get("manifest_valid"): + print("audit error: manifest is unreadable or invalid", file=sys.stderr) + elif result.get("configuration_valid") is False: + print("audit error: invalid trusted expected-sets configuration", file=sys.stderr) + else: + print("audit error: invalid audit input", file=sys.stderr) return 2 output_json = json.dumps(result, indent=2, sort_keys=False) + "\n" @@ -1554,8 +1560,8 @@ def main(argv: list[str] | None = None) -> int: if args.output: try: _publish_file_securely(args.output, output_json, protected_paths, protected_inodes) - except OSError as exc: - print(f"publication error writing output: {exc}", file=sys.stderr) + except OSError: + print("publication error: unable to write output", file=sys.stderr) return 2 else: print(output_json, end="") @@ -1563,8 +1569,8 @@ def main(argv: list[str] | None = None) -> int: if args.summary: try: _publish_file_securely(args.summary, summary_md, protected_paths, protected_inodes) - except OSError as exc: - print(f"publication error writing summary: {exc}", file=sys.stderr) + except OSError: + print("publication error: unable to write summary", file=sys.stderr) return 2 # Determine exit code diff --git a/tests/test_audit_c5_benchmark_evidence.py b/tests/test_audit_c5_benchmark_evidence.py index 905624e48..0f970ce21 100644 --- a/tests/test_audit_c5_benchmark_evidence.py +++ b/tests/test_audit_c5_benchmark_evidence.py @@ -2533,5 +2533,90 @@ class TestMalformedFilenameRejection(unittest.TestCase): self.assertEqual(res["artifacts"][0]["status"], auditor.INVALID) +# --------------------------------------------------------------------------- +# Security: command-line diagnostics must not disclose untrusted input values +# --------------------------------------------------------------------------- + +class TestCliDiagnosticConfidentiality(unittest.TestCase): + """Keep detailed diagnostics in the structured API, not in terminal logs.""" + + def test_invalid_manifest_category_does_not_leak_into_stderr(self): + import subprocess + marker = "SENSITIVE_MARKER_CATEGORY_0183" + with tempfile.TemporaryDirectory() as td: + root = Path(td) + manifest = root / "manifest.json" + manifest.write_text(json.dumps(_manifest([], expected_sets={marker: {"expected_count": 1}}))) + result = auditor.audit(str(manifest), str(root)) + self.assertTrue(any(marker in err for err in result["errors"])) + proc = subprocess.run( + [sys.executable, str(_ROOT / "scripts" / "audit_c5_benchmark_evidence.py"), str(manifest), str(root)], + capture_output=True, text=True, + ) + self.assertEqual(proc.returncode, 2) + self.assertIn("audit error: manifest is unreadable or invalid", proc.stderr) + self.assertNotIn(marker, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + + def test_unreadable_trusted_config_path_does_not_leak_into_stderr(self): + import subprocess + marker = "SENSITIVE_MARKER_TRUSTED_PATH_0294" + with tempfile.TemporaryDirectory() as td: + root = Path(td) + manifest = root / "manifest.json" + manifest.write_text(json.dumps(_manifest([]))) + trusted_path = root / (marker + ".json") + proc = subprocess.run( + [sys.executable, str(_ROOT / "scripts" / "audit_c5_benchmark_evidence.py"), str(manifest), str(root), + "--trusted-expected-sets", str(trusted_path)], + capture_output=True, text=True, + ) + self.assertEqual(proc.returncode, 2) + self.assertIn("audit error: invalid trusted expected-sets configuration", proc.stderr) + self.assertNotIn(marker, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + + def test_preflight_conflict_does_not_leak_path_or_modify_manifest(self): + import subprocess + marker = "SENSITIVE_MARKER_DESTINATION_0312" + with tempfile.TemporaryDirectory() as td: + root = Path(td) + manifest = root / (marker + ".json") + manifest.write_text(json.dumps(_manifest([]))) + original = manifest.read_bytes() + proc = subprocess.run( + [sys.executable, str(_ROOT / "scripts" / "audit_c5_benchmark_evidence.py"), str(manifest), str(root), + "--output", str(manifest)], + capture_output=True, text=True, + ) + self.assertEqual(proc.returncode, 2) + self.assertIn("preflight error: unsafe or conflicting output destination", proc.stderr) + self.assertNotIn(marker, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(manifest.read_bytes(), original) + + def test_publication_error_does_not_leak_exception_text(self): + from contextlib import redirect_stderr + from io import StringIO + from unittest.mock import patch + marker = "SENSITIVE_MARKER_PUBLISH_0489" + with tempfile.TemporaryDirectory() as td: + root = Path(td) + manifest = root / "manifest.json" + manifest.write_text(json.dumps(_manifest([]))) + for option, expected in ( + ("--output", "publication error: unable to write output"), + ("--summary", "publication error: unable to write summary"), + ): + stderr = StringIO() + with patch.object(auditor, "_publish_file_securely", side_effect=OSError(marker)): + with redirect_stderr(stderr): + code = auditor.main([str(manifest), str(root), option, str(root / "output.txt")]) + self.assertEqual(code, 2) + self.assertIn(expected, stderr.getvalue()) + self.assertNotIn(marker, stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + + if __name__ == "__main__": unittest.main()