- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Cross-platform hash comparison for determinism verification.
|
|
Sprint: SPRINT_20251226_007_BE_determinism_gaps
|
|
Task: DET-GAP-13 - Cross-platform hash comparison report generation
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def load_hashes(path: str) -> dict[str, str]:
|
|
"""Load hash file from path."""
|
|
with open(path) as f:
|
|
data = json.load(f)
|
|
return data.get("hashes", data)
|
|
|
|
|
|
def compare_hashes(
|
|
linux: dict[str, str],
|
|
windows: dict[str, str],
|
|
macos: dict[str, str]
|
|
) -> tuple[list[dict], list[str]]:
|
|
"""
|
|
Compare hashes across platforms.
|
|
Returns (divergences, matched_keys).
|
|
"""
|
|
all_keys = set(linux.keys()) | set(windows.keys()) | set(macos.keys())
|
|
divergences = []
|
|
matched = []
|
|
|
|
for key in sorted(all_keys):
|
|
linux_hash = linux.get(key, "MISSING")
|
|
windows_hash = windows.get(key, "MISSING")
|
|
macos_hash = macos.get(key, "MISSING")
|
|
|
|
if linux_hash == windows_hash == macos_hash:
|
|
matched.append(key)
|
|
else:
|
|
divergences.append({
|
|
"key": key,
|
|
"linux": linux_hash,
|
|
"windows": windows_hash,
|
|
"macos": macos_hash
|
|
})
|
|
|
|
return divergences, matched
|
|
|
|
|
|
def generate_markdown_report(
|
|
divergences: list[dict],
|
|
matched: list[str],
|
|
linux_path: str,
|
|
windows_path: str,
|
|
macos_path: str
|
|
) -> str:
|
|
"""Generate Markdown report."""
|
|
lines = [
|
|
f"**Generated:** {datetime.now(timezone.utc).isoformat()}",
|
|
"",
|
|
"### Summary",
|
|
"",
|
|
f"- ✅ **Matched:** {len(matched)} hashes",
|
|
f"- {'❌' if divergences else '✅'} **Divergences:** {len(divergences)} hashes",
|
|
"",
|
|
]
|
|
|
|
if divergences:
|
|
lines.extend([
|
|
"### Divergences",
|
|
"",
|
|
"| Key | Linux | Windows | macOS |",
|
|
"|-----|-------|---------|-------|",
|
|
])
|
|
for d in divergences:
|
|
linux_short = d["linux"][:16] + "..." if len(d["linux"]) > 16 else d["linux"]
|
|
windows_short = d["windows"][:16] + "..." if len(d["windows"]) > 16 else d["windows"]
|
|
macos_short = d["macos"][:16] + "..." if len(d["macos"]) > 16 else d["macos"]
|
|
lines.append(f"| `{d['key']}` | `{linux_short}` | `{windows_short}` | `{macos_short}` |")
|
|
lines.append("")
|
|
|
|
lines.extend([
|
|
"### Matched Hashes",
|
|
"",
|
|
f"<details><summary>Show {len(matched)} matched hashes</summary>",
|
|
"",
|
|
])
|
|
for key in matched[:50]: # Limit display
|
|
lines.append(f"- `{key}`")
|
|
if len(matched) > 50:
|
|
lines.append(f"- ... and {len(matched) - 50} more")
|
|
lines.extend(["", "</details>", ""])
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Compare determinism hashes across platforms")
|
|
parser.add_argument("--linux", required=True, help="Path to Linux hashes JSON")
|
|
parser.add_argument("--windows", required=True, help="Path to Windows hashes JSON")
|
|
parser.add_argument("--macos", required=True, help="Path to macOS hashes JSON")
|
|
parser.add_argument("--output", required=True, help="Output JSON report path")
|
|
parser.add_argument("--markdown", required=True, help="Output Markdown report path")
|
|
args = parser.parse_args()
|
|
|
|
# Load hashes
|
|
linux_hashes = load_hashes(args.linux)
|
|
windows_hashes = load_hashes(args.windows)
|
|
macos_hashes = load_hashes(args.macos)
|
|
|
|
# Compare
|
|
divergences, matched = compare_hashes(linux_hashes, windows_hashes, macos_hashes)
|
|
|
|
# Generate reports
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"sources": {
|
|
"linux": args.linux,
|
|
"windows": args.windows,
|
|
"macos": args.macos
|
|
},
|
|
"summary": {
|
|
"matched": len(matched),
|
|
"divergences": len(divergences),
|
|
"total": len(matched) + len(divergences)
|
|
},
|
|
"divergences": divergences,
|
|
"matched": matched
|
|
}
|
|
|
|
# Write JSON report
|
|
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
|
|
# Write Markdown report
|
|
markdown = generate_markdown_report(
|
|
divergences, matched,
|
|
args.linux, args.windows, args.macos
|
|
)
|
|
with open(args.markdown, "w") as f:
|
|
f.write(markdown)
|
|
|
|
# Print summary
|
|
print(f"Comparison complete:")
|
|
print(f" Matched: {len(matched)}")
|
|
print(f" Divergences: {len(divergences)}")
|
|
|
|
# Exit with error if divergences found
|
|
if divergences:
|
|
print("\nERROR: Hash divergences detected!")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|