projection: admit retired-path classification
The screenshot names remain bounded in reachable history and absent from the current public tree. Source-Sha: c357423ec432febcfce49adb42b863ee1a309b99 Policy-Sha: c357423ec432febcfce49adb42b863ee1a309b99 Tree-Digest: 2c39e139c90830e419de6510b326b6de9be288d4f5121c94cfb921734f815d55
This commit is contained in:
parent
2938f3c61e
commit
5a7b122d4e
4 changed files with 60 additions and 16 deletions
|
|
@ -59,17 +59,6 @@ RAF/verification/03-key-rotation.md
|
||||||
RAF/verification/04-replay-protection.md
|
RAF/verification/04-replay-protection.md
|
||||||
README.md
|
README.md
|
||||||
SECURITY.md
|
SECURITY.md
|
||||||
Screenshots/7Zip-Updates-RedFlag-Dependency.png
|
|
||||||
Screenshots/RedFlag Agent List.png
|
|
||||||
Screenshots/RedFlag Default Dashboard.png
|
|
||||||
Screenshots/RedFlag Docker Dashboard.png
|
|
||||||
Screenshots/RedFlag Heartbeat System.png
|
|
||||||
Screenshots/RedFlag History Dashboard.png
|
|
||||||
Screenshots/RedFlag Linux Agent Details.png
|
|
||||||
Screenshots/RedFlag Live Operations - Failed Dashboard.png
|
|
||||||
Screenshots/RedFlag Updates Dashboard.png
|
|
||||||
Screenshots/RedFlag Windows Agent Details.png
|
|
||||||
Screenshots/Upstream-Version-Tracking.png
|
|
||||||
THIRD_PARTY_LICENSES.md
|
THIRD_PARTY_LICENSES.md
|
||||||
agent/NOTICE
|
agent/NOTICE
|
||||||
agent/cmd/agent/cli.go
|
agent/cmd/agent/cli.go
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,20 @@
|
||||||
"RAF/flows/03-agent-upgrade.md",
|
"RAF/flows/03-agent-upgrade.md",
|
||||||
"scripts/generate-keypair.go"
|
"scripts/generate-keypair.go"
|
||||||
],
|
],
|
||||||
"review_required": [
|
"history_only": [
|
||||||
"Screenshots/7Zip-Updates-RedFlag-Dependency.png",
|
"Screenshots/7Zip-Updates-RedFlag-Dependency.png",
|
||||||
|
"Screenshots/RedFlag Agent List.png",
|
||||||
"Screenshots/RedFlag Default Dashboard.png",
|
"Screenshots/RedFlag Default Dashboard.png",
|
||||||
"Screenshots/RedFlag Docker Dashboard.png",
|
"Screenshots/RedFlag Docker Dashboard.png",
|
||||||
"Screenshots/RedFlag Heartbeat System.png",
|
"Screenshots/RedFlag Heartbeat System.png",
|
||||||
"Screenshots/RedFlag History Dashboard.png",
|
"Screenshots/RedFlag History Dashboard.png",
|
||||||
"Screenshots/RedFlag Linux Agent Details.png",
|
"Screenshots/RedFlag Linux Agent Details.png",
|
||||||
"Screenshots/RedFlag Live Operations - Failed Dashboard.png",
|
"Screenshots/RedFlag Live Operations - Failed Dashboard.png",
|
||||||
"Screenshots/RedFlag Agent List.png",
|
|
||||||
"Screenshots/RedFlag Updates Dashboard.png",
|
"Screenshots/RedFlag Updates Dashboard.png",
|
||||||
"Screenshots/Upstream-Version-Tracking.png",
|
|
||||||
"Screenshots/RedFlag Windows Agent Details.png",
|
"Screenshots/RedFlag Windows Agent Details.png",
|
||||||
|
"Screenshots/Upstream-Version-Tracking.png"
|
||||||
|
],
|
||||||
|
"review_required": [
|
||||||
"RAF/README.md",
|
"RAF/README.md",
|
||||||
"RAF/components/04-helper.md",
|
"RAF/components/04-helper.md",
|
||||||
"RAF/components/05-desktop.md",
|
"RAF/components/05-desktop.md",
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,17 @@ class Manifest:
|
||||||
if len(paths) != len(set(paths)):
|
if len(paths) != len(set(paths)):
|
||||||
raise ValueError(f"{self.path_manifest}: duplicate path")
|
raise ValueError(f"{self.path_manifest}: duplicate path")
|
||||||
self.allowed_paths = frozenset(paths)
|
self.allowed_paths = frozenset(paths)
|
||||||
|
history_only = list(raw.get("history_only", []))
|
||||||
|
for candidate in history_only:
|
||||||
|
validate_policy_path(candidate, "history_only path")
|
||||||
|
if history_only != sorted(history_only):
|
||||||
|
raise ValueError(f"{path}: history_only paths must be bytewise sorted")
|
||||||
|
if len(history_only) != len(set(history_only)):
|
||||||
|
raise ValueError(f"{path}: duplicate history_only path")
|
||||||
|
self.history_only_paths = frozenset(history_only)
|
||||||
|
overlap = self.allowed_paths & self.history_only_paths
|
||||||
|
if overlap:
|
||||||
|
raise ValueError(f"{path}: current and history_only paths overlap: {sorted(overlap)[0]}")
|
||||||
self.exclude = list(raw.get("exclude", []))
|
self.exclude = list(raw.get("exclude", []))
|
||||||
self.review_required = list(raw.get("review_required", []))
|
self.review_required = list(raw.get("review_required", []))
|
||||||
self.forbidden = list(raw.get("forbidden", []))
|
self.forbidden = list(raw.get("forbidden", []))
|
||||||
|
|
@ -259,7 +270,9 @@ def gate_path_authority(repo, sha, manifest, findings):
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
"path-authority", DENY, "excluded-path", path,
|
"path-authority", DENY, "excluded-path", path,
|
||||||
f"{location} contains a path excluded by {excluded_by!r}"))
|
f"{location} contains a path excluded by {excluded_by!r}"))
|
||||||
if path not in manifest.allowed_paths:
|
admitted = manifest.allowed_paths if origin == sha else (
|
||||||
|
manifest.allowed_paths | manifest.history_only_paths)
|
||||||
|
if path not in admitted:
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
"path-authority", DENY, "unlisted-path", path,
|
"path-authority", DENY, "unlisted-path", path,
|
||||||
f"no exact manifest entry admits this path in {location}"))
|
f"no exact manifest entry admits this path in {location}"))
|
||||||
|
|
@ -274,6 +287,11 @@ def gate_path_authority(repo, sha, manifest, findings):
|
||||||
"path-authority", DENY, "missing-path", path,
|
"path-authority", DENY, "missing-path", path,
|
||||||
"exact manifest entry is absent from the candidate tree"))
|
"exact manifest entry is absent from the candidate tree"))
|
||||||
|
|
||||||
|
for path in sorted(manifest.history_only_paths & present):
|
||||||
|
findings.append(Finding(
|
||||||
|
"path-authority", DENY, "history-only-path", path,
|
||||||
|
"path is classified for reachable history but reappears in the candidate tree"))
|
||||||
|
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
"path-authority", NOTE, "exact-census", "-",
|
"path-authority", NOTE, "exact-census", "-",
|
||||||
f"{len(present)} candidate paths checked against "
|
f"{len(present)} candidate paths checked against "
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class SurfaceGateTests(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.work.cleanup()
|
self.work.cleanup()
|
||||||
|
|
||||||
def candidate(self, files, admitted=None, exclude=None):
|
def candidate(self, files, admitted=None, exclude=None, history_only=None):
|
||||||
for name, content in files.items():
|
for name, content in files.items():
|
||||||
path = self.repo / name
|
path = self.repo / name
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -43,6 +43,7 @@ class SurfaceGateTests(unittest.TestCase):
|
||||||
"repository": "Fimeg/RedFlag",
|
"repository": "Fimeg/RedFlag",
|
||||||
"path_manifest": ".publication/paths.txt",
|
"path_manifest": ".publication/paths.txt",
|
||||||
"exclude": exclude or [],
|
"exclude": exclude or [],
|
||||||
|
"history_only": history_only or [],
|
||||||
"review_required": [],
|
"review_required": [],
|
||||||
"forbidden": [],
|
"forbidden": [],
|
||||||
"forbidden_classes": [],
|
"forbidden_classes": [],
|
||||||
|
|
@ -113,6 +114,40 @@ class SurfaceGateTests(unittest.TestCase):
|
||||||
self.assertIn("missing-path", result.stdout)
|
self.assertIn("missing-path", result.stdout)
|
||||||
self.assertIn("product/missing.go", result.stdout)
|
self.assertIn("product/missing.go", result.stdout)
|
||||||
|
|
||||||
|
def test_history_only_path_may_leave_the_current_tree(self):
|
||||||
|
old = self.candidate({"legacy/screenshot.png": b"old"})
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-C", self.repo, "rm", "legacy/screenshot.png"], check=True
|
||||||
|
)
|
||||||
|
policy_path = self.repo / ".publication/paths.txt"
|
||||||
|
policy_path.write_text(
|
||||||
|
".publication/paths.txt\n.publication/surface.json\n"
|
||||||
|
)
|
||||||
|
surface_path = self.repo / ".publication/surface.json"
|
||||||
|
policy = json.loads(surface_path.read_text())
|
||||||
|
policy["history_only"] = ["legacy/screenshot.png"]
|
||||||
|
surface_path.write_text(json.dumps(policy, indent=2) + "\n")
|
||||||
|
subprocess.run(["git", "-C", self.repo, "add", "."], check=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-C", self.repo, "commit", "-q", "-m", "test: retire screenshot"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
current = subprocess.check_output(
|
||||||
|
["git", "-C", self.repo, "rev-parse", "HEAD"], text=True
|
||||||
|
).strip()
|
||||||
|
result = self.run_gate(current, "--previous", old)
|
||||||
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||||
|
|
||||||
|
def test_history_only_path_cannot_reappear(self):
|
||||||
|
sha = self.candidate(
|
||||||
|
{"legacy/screenshot.png": b"still here"},
|
||||||
|
admitted=set(),
|
||||||
|
history_only=["legacy/screenshot.png"],
|
||||||
|
)
|
||||||
|
result = self.run_gate(sha)
|
||||||
|
self.assertEqual(1, result.returncode)
|
||||||
|
self.assertIn("history-only-path", result.stdout)
|
||||||
|
|
||||||
def test_approved_candidate_inventory_is_deterministic(self):
|
def test_approved_candidate_inventory_is_deterministic(self):
|
||||||
sha = self.candidate({"a.txt": "a\n", "nested/b.txt": "b\n"})
|
sha = self.candidate({"a.txt": "a\n", "nested/b.txt": "b\n"})
|
||||||
first = self.repo / "first.manifest"
|
first = self.repo / "first.manifest"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue