Ensure that diff also runs through validate()
All checks were successful
All checks were successful
This commit is contained in:
parent
1312b7eac2
commit
cec6023a40
2 changed files with 72 additions and 0 deletions
|
|
@ -31,6 +31,27 @@ from .pathfilter import PathFilter
|
||||||
from .sopsutil import decrypt_file_binary_to, require_sops_cmd
|
from .sopsutil import decrypt_file_binary_to, require_sops_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_diff_bundle(label: str, bundle_dir: Path) -> None:
|
||||||
|
"""Validate a resolved harvest bundle before diff reads artifacts.
|
||||||
|
|
||||||
|
`diff` intentionally compares older harvests, so keep schema validation out
|
||||||
|
of this internal safety pass. The important security property here is that
|
||||||
|
the bundle's artifact tree has the same path/symlink/hardlink/special-file
|
||||||
|
checks that `manifest` relies on before copying artifacts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Import lazily to avoid a module-level cycle: enroll.validate imports
|
||||||
|
# BundleRef/_bundle_from_input from this module.
|
||||||
|
from .validate import validate_harvest
|
||||||
|
|
||||||
|
validation = validate_harvest(str(bundle_dir), no_schema=True)
|
||||||
|
if not validation.ok:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{label} harvest failed validation; refusing to diff unsafe bundle.\n"
|
||||||
|
+ validation.to_text().strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _progress_enabled() -> bool:
|
def _progress_enabled() -> bool:
|
||||||
"""Return True if we should display interactive progress UI on the CLI.
|
"""Return True if we should display interactive progress UI on the CLI.
|
||||||
|
|
||||||
|
|
@ -371,6 +392,9 @@ def compare_harvests(
|
||||||
if new_b.tempdir:
|
if new_b.tempdir:
|
||||||
stack.callback(new_b.tempdir.cleanup)
|
stack.callback(new_b.tempdir.cleanup)
|
||||||
|
|
||||||
|
_validate_diff_bundle("old", old_b.dir)
|
||||||
|
_validate_diff_bundle("new", new_b.dir)
|
||||||
|
|
||||||
old_state = _load_state(old_b.dir)
|
old_state = _load_state(old_b.dir)
|
||||||
new_state = _load_state(new_b.dir)
|
new_state = _load_state(new_b.dir)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -701,6 +701,54 @@ def test_compare_harvests_with_exclude_paths(tmp_path: Path):
|
||||||
assert "/etc/passwd" not in [f["path"] for f in report["files"]["changed"]]
|
assert "/etc/passwd" not in [f["path"] for f in report["files"]["changed"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_compare_harvests_rejects_unsafe_artifact_symlink(tmp_path: Path):
|
||||||
|
secret = tmp_path / "secret.txt"
|
||||||
|
secret.write_text("do not hash me", encoding="utf-8")
|
||||||
|
|
||||||
|
old_bundle = tmp_path / "old"
|
||||||
|
old_artifacts = old_bundle / "artifacts" / "users"
|
||||||
|
old_artifacts.mkdir(parents=True)
|
||||||
|
(old_artifacts / "passwd").symlink_to(secret)
|
||||||
|
(old_bundle / "state.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"inventory": {"packages": {}},
|
||||||
|
"roles": {
|
||||||
|
"users": {
|
||||||
|
"managed_files": [{"path": "/etc/passwd", "src_rel": "passwd"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_bundle = tmp_path / "new"
|
||||||
|
new_artifacts = new_bundle / "artifacts" / "users"
|
||||||
|
new_artifacts.mkdir(parents=True)
|
||||||
|
(new_artifacts / "passwd").write_text("safe", encoding="utf-8")
|
||||||
|
(new_bundle / "state.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"inventory": {"packages": {}},
|
||||||
|
"roles": {
|
||||||
|
"users": {
|
||||||
|
"managed_files": [{"path": "/etc/passwd", "src_rel": "passwd"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as exc_info:
|
||||||
|
compare_harvests(str(old_bundle), str(new_bundle))
|
||||||
|
|
||||||
|
msg = str(exc_info.value)
|
||||||
|
assert "old harvest failed validation" in msg
|
||||||
|
assert "artifact is a symlink" in msg
|
||||||
|
|
||||||
|
|
||||||
def test_utc_now_iso():
|
def test_utc_now_iso():
|
||||||
result = _utc_now_iso()
|
result = _utc_now_iso()
|
||||||
assert "T" in result
|
assert "T" in result
|
||||||
|
|
|
||||||
Reference in a new issue