feat: add tenant isolation pre-audit evidence
This commit is contained in:
@@ -133,6 +133,7 @@ run "phase-sec23-state-isolation" bash "$TESTS/phase-sec23-state-isolation-tests
|
||||
run "phase-sec23-rbac-tenant" bash "$TESTS/phase-sec23-rbac-tenant-tests.sh"
|
||||
run "phase-sec23-scope" bash "$TESTS/phase-sec23-scope-tests.sh"
|
||||
run "phase-sec23-registry-crypt" bash "$TESTS/phase-sec23-registry-crypt-tests.sh"
|
||||
run "phase-tenant-isolation-review" bash "$TESTS/phase-tenant-isolation-review-tests.sh"
|
||||
# Plan-16 SEC-24/25 supply-chain (offline slice)
|
||||
run "phase-sec24-supplychain" bash "$TESTS/phase-sec24-tests.sh"
|
||||
run "phase-sec25-attestation" bash "$TESTS/phase-sec25-tests.sh"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only tenant-isolation pre-audit for CASAN state partitions.
|
||||
|
||||
It validates filesystem boundaries and records concrete findings. It is not an
|
||||
independent audit; use its JSON output as input to an independent review.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TENANT = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
def mode(path: Path) -> int:
|
||||
return stat.S_IMODE(path.lstat().st_mode)
|
||||
|
||||
def insecure(mode_value: int) -> bool:
|
||||
return bool(mode_value & 0o077)
|
||||
|
||||
def review(root: Path) -> dict:
|
||||
findings: list[dict] = []
|
||||
tenants: list[str] = []
|
||||
keys: dict[str, str] = {}
|
||||
if not root.is_dir() or root.is_symlink():
|
||||
findings.append({"severity": "critical", "code": "tenant_root_invalid", "path": str(root)})
|
||||
return {"ok": False, "root": str(root), "tenants": tenants, "findings": findings}
|
||||
if insecure(mode(root)):
|
||||
findings.append({"severity": "high", "code": "tenant_root_permissions", "path": str(root), "mode": oct(mode(root))})
|
||||
for candidate in sorted(root.iterdir()):
|
||||
if not TENANT.fullmatch(candidate.name):
|
||||
findings.append({"severity": "medium", "code": "unexpected_root_entry", "path": str(candidate)})
|
||||
continue
|
||||
tenants.append(candidate.name)
|
||||
if candidate.is_symlink() or not candidate.is_dir():
|
||||
findings.append({"severity": "critical", "code": "tenant_partition_not_directory", "tenant": candidate.name})
|
||||
continue
|
||||
if insecure(mode(candidate)):
|
||||
findings.append({"severity": "high", "code": "tenant_permissions", "tenant": candidate.name, "mode": oct(mode(candidate))})
|
||||
for current, dirs, files in os.walk(candidate, followlinks=False):
|
||||
base = Path(current)
|
||||
for name in dirs + files:
|
||||
item = base / name
|
||||
if item.is_symlink():
|
||||
findings.append({"severity": "critical", "code": "tenant_symlink_forbidden", "tenant": candidate.name, "path": str(item)})
|
||||
key = candidate / "keys" / "at-rest.key"
|
||||
if key.exists():
|
||||
if key.is_symlink() or mode(key) != 0o600:
|
||||
findings.append({"severity": "high", "code": "tenant_key_permissions", "tenant": candidate.name, "path": str(key), "mode": oct(mode(key))})
|
||||
else:
|
||||
keys[candidate.name] = hashlib.sha256(key.read_bytes()).hexdigest()
|
||||
values = list(keys.values())
|
||||
if len(values) != len(set(values)):
|
||||
findings.append({"severity": "critical", "code": "tenant_key_material_reused"})
|
||||
return {"ok": not findings, "root": str(root), "tenants": tenants, "keyed_tenants": sorted(keys), "findings": findings}
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", required=True)
|
||||
parser.add_argument("--out")
|
||||
args = parser.parse_args()
|
||||
result = review(Path(args.root).resolve())
|
||||
payload = json.dumps(result, sort_keys=True, indent=2) + "\n"
|
||||
if args.out:
|
||||
Path(args.out).write_text(payload, encoding="utf8")
|
||||
print(payload, end="")
|
||||
return 0 if result["ok"] else 2
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
R="$SCRIPT_DIR/../scripts/bash/tenant-isolation-review.py"
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
ROOT="$WORK/tenants"; mkdir -p "$ROOT/alpha/keys" "$ROOT/beta/keys"; chmod 700 "$ROOT" "$ROOT/alpha" "$ROOT/beta"
|
||||
printf 'alpha-key\n' > "$ROOT/alpha/keys/at-rest.key"; printf 'beta-key\n' > "$ROOT/beta/keys/at-rest.key"; chmod 600 "$ROOT"/*/keys/at-rest.key
|
||||
PASS=0; FAIL=0
|
||||
pass(){ echo "PASS: $1"; PASS=$((PASS+1)); }; fail(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
if python3 "$R" --root "$ROOT" >/dev/null; then pass "isolated tenant partitions pass review"; else fail "valid partitions rejected"; fi
|
||||
chmod 755 "$ROOT/alpha"
|
||||
if python3 "$R" --root "$ROOT" >/dev/null 2>&1; then fail "world-readable tenant accepted"; else pass "world-readable tenant denied"; fi
|
||||
chmod 700 "$ROOT/alpha"; ln -s /tmp "$ROOT/beta/escape"
|
||||
if python3 "$R" --root "$ROOT" >/dev/null 2>&1; then fail "tenant symlink accepted"; else pass "tenant symlink denied"; fi
|
||||
echo "===== TENANT ISOLATION REVIEW SUMMARY: PASS=$PASS FAIL=$FAIL ====="; [[ "$FAIL" -eq 0 ]]
|
||||
Reference in New Issue
Block a user