From 39680d825a5a23809fede3d807fec288bfd2a1a4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:42:57 +0700 Subject: [PATCH 01/11] fix(analytics): count current portable EXE downloads with historical ZIP scope --- landing/analytics.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/landing/analytics.js b/landing/analytics.js index 03cce0a59..bf4a696e1 100644 --- a/landing/analytics.js +++ b/landing/analytics.js @@ -52,7 +52,9 @@ const classifyDownload = href => { if (href.endsWith('/ARSAS-Windows-x64-Setup.exe')) return ['download_installer', 'ARSAS-Windows-x64-Setup.exe']; - if (href.endsWith('/ARSAS-Windows-x64-Portable.zip')) return ['download_portable', 'ARSAS-Windows-x64-Portable.zip']; + if (href.endsWith('/ARSAS-Windows-x64-Portable.exe')) return ['download_portable', 'ARSAS-Windows-x64-Portable.exe']; + // Historical tagged ZIP links remain classifiable but never masquerade as latest stable. + if (/\/releases\/download\/v\d+\.\d+\.\d+\/ARSAS-Windows-x64-Portable\.zip(?:[?#].*)?$/.test(href)) return ['download_portable_legacy', 'ARSAS-Windows-x64-Portable.zip']; if (href.endsWith('/ARSAS-Windows-x64-SHA256SUMS.txt')) return ['download_checksums', 'ARSAS-Windows-x64-SHA256SUMS.txt']; return null; }; From 9a413544f6fdb4ae65bcd370922580a317c71189 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:43:00 +0700 Subject: [PATCH 02/11] fix(site): enforce single EXE portable contract for stable releases --- scripts/validate-product-source.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/validate-product-source.py b/scripts/validate-product-source.py index ea196df6d..b1c1a7071 100644 --- a/scripts/validate-product-source.py +++ b/scripts/validate-product-source.py @@ -141,9 +141,21 @@ def validate_release(errors: list[str]) -> None: if not isinstance(installer, dict) or installer.get("name") != "ARSAS-Windows-x64-Setup.exe" or not re.fullmatch(r"[0-9a-fA-F]{64}", str(installer.get("sha256", ""))): errors.append("latest.json installer evidence is invalid") portable = evidence.get("portable") - portable_names = {"ARSAS-Windows-x64-Portable.zip", "ARSAS-Windows-x64-Portable.exe"} - if not isinstance(portable, dict) or portable.get("name") not in portable_names or not re.fullmatch(r"[0-9a-fA-F]{64}", str(portable.get("sha256", ""))): - errors.append("latest.json portable evidence is invalid") + # Modern stable distribution is a real single EXE. Legacy ZIPs are permitted + # only for older historical tags; do not silently regress the current channel. + stable_parts = tuple(int(part) for part in version.split(".")) + expected_portable = ( + "ARSAS-Windows-x64-Portable.exe" if stable_parts >= (1, 6, 42) + else "ARSAS-Windows-x64-Portable.zip" + ) + expected_portable_url = ( + "https://github.com/masarray/arsas/releases/latest/download/" + expected_portable + ) + if (not isinstance(portable, dict) + or portable.get("name") != expected_portable + or portable.get("url") != expected_portable_url + or not re.fullmatch(r"[0-9a-fA-F]{64}", str(portable.get("sha256", "")))): + errors.append(f"latest.json portable must match the {expected_portable} stable release contract") if not isinstance(evidence.get("checksums"), dict): errors.append("latest.json checksum evidence is missing") signing = evidence.get("codeSigning") note_signing = notes.get("codeSigning") From 6ad5344a8ece979f4556b9d6cc54232535b37972 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:43:02 +0700 Subject: [PATCH 03/11] docs(seo): record actual October updates for refreshed public pages --- landing/site.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/landing/site.json b/landing/site.json index fa4ba1b83..8d442af07 100644 --- a/landing/site.json +++ b/landing/site.json @@ -117,7 +117,7 @@ }, "changefreq": "monthly", "priority": "0.97", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "panduan-mulai-arsas.html", @@ -131,7 +131,7 @@ }, "changefreq": "monthly", "priority": "0.95", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "learning-center.html", @@ -229,7 +229,7 @@ }, "changefreq": "monthly", "priority": "0.92", - "lastmod": "2026-07-27" + "lastmod": "2026-10-25" }, { "path": "faq-arsas.html", @@ -243,7 +243,7 @@ }, "changefreq": "monthly", "priority": "0.90", - "lastmod": "2026-07-27" + "lastmod": "2026-10-25" }, { "path": "compatibility.html", @@ -257,7 +257,7 @@ }, "changefreq": "monthly", "priority": "0.91", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "bukti-kompatibilitas.html", @@ -271,7 +271,7 @@ }, "changefreq": "monthly", "priority": "0.89", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "demo.html", @@ -285,7 +285,7 @@ }, "changefreq": "monthly", "priority": "0.90", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "demo-arsas.html", @@ -299,7 +299,7 @@ }, "changefreq": "monthly", "priority": "0.88", - "lastmod": "2026-09-22" + "lastmod": "2026-10-25" }, { "path": "features.html", From d9918b2ddd685ebd8d4e17cd540e685c1dca6a92 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:44:27 +0700 Subject: [PATCH 04/11] fix(site): keep malformed manifest errors actionable --- scripts/validate-product-source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/validate-product-source.py b/scripts/validate-product-source.py index b1c1a7071..f344e1371 100644 --- a/scripts/validate-product-source.py +++ b/scripts/validate-product-source.py @@ -143,7 +143,8 @@ def validate_release(errors: list[str]) -> None: portable = evidence.get("portable") # Modern stable distribution is a real single EXE. Legacy ZIPs are permitted # only for older historical tags; do not silently regress the current channel. - stable_parts = tuple(int(part) for part in version.split(".")) + stable_parts = (tuple(int(part) for part in version.split(".")) + if re.fullmatch(r"\d+\.\d+\.\d+", version) else (0, 0, 0)) expected_portable = ( "ARSAS-Windows-x64-Portable.exe" if stable_parts >= (1, 6, 42) else "ARSAS-Windows-x64-Portable.zip" From b0df65c5a53caded1fae6f9ca393350429fcde5e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:44:30 +0700 Subject: [PATCH 05/11] refactor(site): direct retired validator command to canonical 62-page gate --- scripts/validate-built-landing.py | 220 ++---------------------------- 1 file changed, 9 insertions(+), 211 deletions(-) diff --git a/scripts/validate-built-landing.py b/scripts/validate-built-landing.py index 99fb30089..26a37d737 100644 --- a/scripts/validate-built-landing.py +++ b/scripts/validate-built-landing.py @@ -1,223 +1,21 @@ #!/usr/bin/env python3 -"""Validate the final public ARSAS website artifact.""" +"""Compatibility entry point for the canonical rendered product-site validator. + +The old eight-page validator and Portable.zip contract were superseded by the +62-page, single-EXE-aware scripts/validate-product-build.py. Keep this command +for documented downstream callers without maintaining two divergent policies. +""" from __future__ import annotations -import json -import re -import struct +import subprocess import sys -from html.parser import HTMLParser from pathlib import Path -EXPECTED_PAGES = ( - "index.html", - "download.html", - "smart-reporting.html", - "features.html", - "control.html", - "architecture.html", - "roadmap.html", - "404.html", -) - -EXPECTED_MEDIA = ( - "assets/app-icon.png", - "assets/social-card.png", - "assets/screenshots/arsas-first-launch.webp", - "assets/screenshots/arsas-multi-ied.webp", - "assets/screenshots/arsas-live-values.webp", - "assets/screenshots/arsas-event-log.webp", - "assets/screenshots/arsas-goose.webp", - "assets/screenshots/arsas-diagnostics.webp", - "assets/screenshots/arsas-overview-v1.6.19.webp", -) - -DIRECT_INSTALLER = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Setup.exe" -DIRECT_PORTABLE = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Portable.zip" -DIRECT_CHECKSUMS = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-SHA256SUMS.txt" -ALLOWED_GITHUB_URLS = {DIRECT_INSTALLER, DIRECT_PORTABLE, DIRECT_CHECKSUMS} -APP_ICON = "assets/app-icon.png" - - -class AssetParser(HTMLParser): - def __init__(self) -> None: - super().__init__(convert_charrefs=True) - self.refs: list[str] = [] - self.icons: list[dict[str, str | None]] = [] - - def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: - values = dict(attrs) - for key in ("href", "src", "content"): - value = values.get(key) - if value: - self.refs.append(value) - if tag == "link" and "icon" in (values.get("rel") or "").lower(): - self.icons.append(values) - - -def png_size(path: Path) -> tuple[int, int]: - data = path.read_bytes()[:24] - if len(data) < 24 or data[:8] != b"\x89PNG\r\n\x1a\n": - raise ValueError("not a PNG") - return struct.unpack(">II", data[16:24]) - - -def validate_updater_manifest(site: Path, errors: list[str]) -> None: - path = site / "latest.json" - if not path.exists(): - errors.append("missing deployable file: latest.json") - return - - try: - manifest = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - errors.append(f"latest.json: {exc}") - return - - if manifest.get("schemaVersion") != 1: - errors.append("latest.json: schemaVersion must be 1") - if manifest.get("product") != "ARSAS": - errors.append("latest.json: product must be ARSAS") - if manifest.get("channel") != "stable": - errors.append("latest.json: channel must be stable") - if not re.fullmatch(r"\d+\.\d+\.\d+", str(manifest.get("version", ""))): - errors.append("latest.json: version must use major.minor.patch") - - installer = manifest.get("installer") - if not isinstance(installer, dict): - errors.append("latest.json: installer object is missing") - return - if installer.get("name") != "ARSAS-Windows-x64-Setup.exe": - errors.append("latest.json: installer name is not the stable ARSAS EXE") - if installer.get("url") != DIRECT_INSTALLER: - errors.append("latest.json: installer URL is not the approved direct stable EXE") - if not re.fullmatch(r"[0-9a-fA-F]{64}", str(installer.get("sha256", ""))): - errors.append("latest.json: installer SHA-256 is invalid") - size = installer.get("sizeBytes") - if not isinstance(size, int) or not 1_000_000 <= size <= 500_000_000: - errors.append("latest.json: installer size is outside the accepted range") - - -def validate_web_manifest(site: Path, errors: list[str]) -> None: - path = site / "site.webmanifest" - try: - manifest = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - errors.append(f"site.webmanifest: {exc}") - return - - icons = manifest.get("icons") - if not isinstance(icons, list) or len(icons) != 1: - errors.append("site.webmanifest: expected one canonical ARSAS app icon") - return - - icon = icons[0] - if not isinstance(icon, dict): - errors.append("site.webmanifest: icon entry is invalid") - return - if icon.get("src") != APP_ICON: - errors.append("site.webmanifest: icon must use assets/app-icon.png") - if icon.get("sizes") != "256x256": - errors.append("site.webmanifest: app icon size must be 256x256") - if icon.get("type") != "image/png": - errors.append("site.webmanifest: app icon type must be image/png") - def main() -> int: - site = Path(sys.argv[1] if len(sys.argv) > 1 else "_site").resolve() - errors: list[str] = [] - - for relative in EXPECTED_PAGES + EXPECTED_MEDIA: - if not (site / relative).exists(): - errors.append(f"missing deployable file: {relative}") - - combined = "" - all_refs: list[tuple[str, str]] = [] - for page_name in EXPECTED_PAGES: - page = site / page_name - if not page.exists(): - continue - text = page.read_text(encoding="utf-8") - combined += text - parser = AssetParser() - parser.feed(text) - for ref in parser.refs: - all_refs.append((page_name, ref)) - if ref.startswith("assets/") and not (site / ref.split("?", 1)[0].split("#", 1)[0]).exists(): - errors.append(f"{page_name}: missing local asset {ref}") - - favicon = [icon for icon in parser.icons if (icon.get("rel") or "").lower() == "icon"] - if len(favicon) != 1: - errors.append(f"{page_name}: expected exactly one favicon link") - elif favicon[0].get("href") != APP_ICON or favicon[0].get("type") != "image/png": - errors.append(f"{page_name}: favicon must use the ARSAS PNG app icon") - - touch_icon = [icon for icon in parser.icons if (icon.get("rel") or "").lower() == "apple-touch-icon"] - if len(touch_icon) != 1 or touch_icon[0].get("href") != APP_ICON: - errors.append(f"{page_name}: apple-touch-icon must use the ARSAS app icon") - - forbidden_text = ( - "raw.githubusercontent.com/masarray/arsas/main/Assets/screenshot", - "https://masarray.github.io/arsas/assets/social-card.svg", - 'href="assets/favicon.svg"', - '"codeRepository"', - ">Repository", - ">Issues", - "Build from source", - "Build instructions", - ) - for value in forbidden_text: - if value in combined: - errors.append(f"deployable HTML still exposes forbidden public route or text: {value}") - - for page_name, ref in all_refs: - if ref.startswith("https://github.com/") and ref not in ALLOWED_GITHUB_URLS: - errors.append(f"{page_name}: public website exposes a GitHub page instead of a direct binary: {ref}") - - if "https://masarray.github.io/arsas/assets/social-card.png" not in combined: - errors.append("PNG social preview is not referenced") - if DIRECT_INSTALLER not in combined: - errors.append("latest stable installer direct-download URL is missing") - if 'href="download.html"' in combined: - errors.append("public CTA still opens an intermediate download page instead of the EXE") - - download = site / "download.html" - if download.exists(): - text = download.read_text(encoding="utf-8") - for required in (DIRECT_INSTALLER, DIRECT_PORTABLE, DIRECT_CHECKSUMS, "Latest stable channel"): - if required not in text: - errors.append(f"download page is missing {required}") - if "download.js" in text: - errors.append("download page still depends on repository-aware JavaScript") - - app_icon = site / APP_ICON - if app_icon.exists(): - try: - if png_size(app_icon) != (256, 256): - errors.append(f"app-icon.png must be 256x256, found {png_size(app_icon)}") - except ValueError as exc: - errors.append(f"app-icon.png: {exc}") - - social = site / "assets/social-card.png" - if social.exists(): - try: - if png_size(social) != (1200, 630): - errors.append(f"social-card.png must be 1200x630, found {png_size(social)}") - except ValueError as exc: - errors.append(f"social-card.png: {exc}") - - validate_web_manifest(site, errors) - validate_updater_manifest(site, errors) - - if errors: - print("Public landing validation failed:", file=sys.stderr) - for error in errors: - print(f"- {error}", file=sys.stderr) - return 1 - - print("Public landing validation passed: ARSAS app-icon branding, landing-only navigation, direct downloads, and the verified updater manifest are enforced.") - return 0 + canonical = Path(__file__).resolve().with_name("validate-product-build.py") + return subprocess.call([sys.executable, str(canonical), *sys.argv[1:]]) if __name__ == "__main__": From 2759124b8b7d9aad2f7857c94c60e31716301795 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:44:33 +0700 Subject: [PATCH 06/11] ci(release): fail closed on legacy ZIP publishers for modern EXE tags --- .github/workflows/release-supply-chain.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release-supply-chain.yml b/.github/workflows/release-supply-chain.yml index a1a152850..541eb31d7 100644 --- a/.github/workflows/release-supply-chain.yml +++ b/.github/workflows/release-supply-chain.yml @@ -35,6 +35,16 @@ jobs: exit 1 fi version="${tag#v}" + # This additive backfill extracts a historical ZIP to produce a file-level SBOM. + # Modern public releases (v1.6.42+) ship a portable single EXE. They require + # the canonical release-windows.yml provenance/SBOM flow; never claim that + # the EXE is an extractable archive or silently substitute historical bytes. + python - "$version" <<'PY' + import sys + version = tuple(map(int, sys.argv[1].split(".")[:3])) + if version >= (1, 6, 42): + raise SystemExit("Single-EXE stable release: use the canonical release SBOM/provenance workflow; legacy ZIP backfill is not applicable.") + PY echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" From 4ea35e99fa1cb146d02147fd9e825c3330a4e708 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:44:36 +0700 Subject: [PATCH 07/11] ci(release): fail closed on legacy ZIP publishers for modern EXE tags --- .github/workflows/publish-verified-release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/publish-verified-release.yml b/.github/workflows/publish-verified-release.yml index 0dde2da9f..3713b2617 100644 --- a/.github/workflows/publish-verified-release.yml +++ b/.github/workflows/publish-verified-release.yml @@ -45,6 +45,11 @@ jobs: raise SystemExit('Missing publication request fields: ' + ', '.join(missing)) if not re.fullmatch(r'\d+\.\d+\.\d+', str(request['version'])): raise SystemExit('Publication request version is invalid') + # This historical artifact publisher expects a portable ZIP. Modern stable + # releases require the exact tested single EXE with SBOM/provenance from + # release-windows.yml. Prevent any newer release from silently reverting. + if tuple(map(int, request['version'].split('.'))) >= (1, 6, 42): + raise SystemExit('Modern single-EXE release must use release-windows.yml; legacy ZIP publisher refuses this version') if notes.get('version') != request['version']: raise SystemExit('Release notes version does not match publication request') signing = notes.get('codeSigning', {}) From bfdc4862442b3d3e1bf20652849524bc3fcd7ade Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:46:37 +0700 Subject: [PATCH 08/11] test(media): verify v1.6.42 screenshot PNG/WebP hidden metadata without modifying originals --- scripts/audit-public-media.py | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 scripts/audit-public-media.py diff --git a/scripts/audit-public-media.py b/scripts/audit-public-media.py new file mode 100644 index 000000000..7ca068d0d --- /dev/null +++ b/scripts/audit-public-media.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Audit non-pixel metadata in the public v1.6.42 PNG/WebP captures. + +This is an automated hidden-metadata boundary, NOT proof that on-screen IP +addresses, device identities or customer information have been sanitized. +""" + +from __future__ import annotations + +import argparse +import json +import struct +import sys +from pathlib import Path + +CAPTURES = ( + "ied-browser", "event-logs", "signal-testing", "fat-report-preview", + "smv", "goose", "annunciator", "diagnostics", +) +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" +PRIVATE_PNG_CHUNKS = {b"tEXt", b"zTXt", b"iTXt", b"eXIf"} +PRIVATE_WEBP_CHUNKS = {b"EXIF", b"XMP "} + + +def png_metadata(data: bytes) -> list[str]: + if not data.startswith(PNG_SIGNATURE): + raise ValueError("not PNG") + cursor = len(PNG_SIGNATURE) + found: list[str] = [] + terminated = False + while cursor + 12 <= len(data): + length = int.from_bytes(data[cursor:cursor + 4], "big") + kind = data[cursor + 4:cursor + 8] + cursor += 8 + if length > len(data) - cursor - 4: + raise ValueError("truncated PNG chunk") + cursor += length + 4 # data + CRC + if kind in PRIVATE_PNG_CHUNKS: + found.append(kind.decode("ascii")) + if kind == b"IEND": + terminated = True + break + if not terminated or cursor != len(data): + raise ValueError("missing/invalid PNG end") + return found + + +def webp_metadata(data: bytes) -> list[str]: + if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WEBP": + raise ValueError("not WebP") + length = struct.unpack_from(" len(data) - cursor: + raise ValueError("truncated WebP chunk") + if kind in PRIVATE_WEBP_CHUNKS: + found.append(kind.decode("ascii").strip()) + cursor += size + (size & 1) + if cursor != len(data): + raise ValueError("invalid WebP chunk alignment") + return found + + +def audit(root: Path) -> list[dict[str, object]]: + screens = root / "landing" / "assets" / "screenshots" + results: list[dict[str, object]] = [] + for name in CAPTURES: + for ext, scanner in (("png", png_metadata), ("webp", webp_metadata)): + path = screens / f"arsas-{name}-v1.6.42.{ext}" + try: + data = path.read_bytes() + metadata = scanner(data) + status = "PASS" if not metadata else "FAIL" + finding = ",".join(metadata) + except (OSError, ValueError) as exc: + status = "FAIL" + finding = type(exc).__name__ # never copy potentially private metadata + results.append({ + "file": path.name, "status": status, "metadata_chunks": finding + }) + return results + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--report", type=Path) + args = parser.parse_args() + rows = audit(args.root) + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps({"scope": "metadata-only", "screens": rows}, + indent=2) + "\n", encoding="utf-8") + failures = [row for row in rows if row["status"] != "PASS"] + print(f"Public media embedded-metadata audit: {len(rows)-len(failures)}/{len(rows)} clean source assets.") + for row in failures: + print(f"- {row['file']}: {row['metadata_chunks']}", file=sys.stderr) + if failures: + print("Inspect the original image and seek owner approval before replacing public evidence.", file=sys.stderr) + print("Pixel-level private-network/IED identity review remains a separate human gate.") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 5a1fda353876b8d50955177725bbf5a47cca6324 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:47:31 +0700 Subject: [PATCH 09/11] test(media): positive and negative PNG/WebP metadata fixtures --- scripts/test-audit-public-media.py | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts/test-audit-public-media.py diff --git a/scripts/test-audit-public-media.py b/scripts/test-audit-public-media.py new file mode 100644 index 000000000..71f2c7665 --- /dev/null +++ b/scripts/test-audit-public-media.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Deterministic positive/negative fixtures for the public screenshot metadata gate.""" + +from __future__ import annotations + +import importlib.util +import struct +import unittest +import zlib +from pathlib import Path + +module_path = Path(__file__).with_name("audit-public-media.py") +spec = importlib.util.spec_from_file_location("audit_public_media", module_path) +if spec is None or spec.loader is None: + raise RuntimeError("Unable to import public media audit") +media = importlib.util.module_from_spec(spec) +spec.loader.exec_module(media) + + +def png_chunk(kind: bytes, payload: bytes) -> bytes: + return (struct.pack(">I", len(payload)) + kind + payload + + struct.pack(">I", zlib.crc32(kind + payload))) + + +def webp_chunk(kind: bytes, payload: bytes) -> bytes: + return kind + struct.pack(" None: + png = media.PNG_SIGNATURE + png_chunk(b"IHDR", b"\x00" * 13) + png_chunk(b"IEND", b"") + self.assertEqual(media.png_metadata(png), []) + + def test_png_text_metadata_fails(self) -> None: + png = (media.PNG_SIGNATURE + png_chunk(b"IHDR", b"\x00" * 13) + + png_chunk(b"iTXt", b"private") + png_chunk(b"IEND", b"")) + self.assertEqual(media.png_metadata(png), ["iTXt"]) + + def test_truncated_png_fails(self) -> None: + with self.assertRaises(ValueError): + media.png_metadata(media.PNG_SIGNATURE + b"\x00" * 10) + + def test_clean_webp_passes(self) -> None: + payload = webp_chunk(b"VP8L", b"\x00\x00") + webp = b"RIFF" + struct.pack(" None: + payload = webp_chunk(b"VP8L", b"\x00\x00") + webp_chunk(b"EXIF", b"private") + webp = b"RIFF" + struct.pack(" None: + with self.assertRaises(ValueError): + media.webp_metadata(b"RIFF" + struct.pack(" Date: Sun, 25 Oct 2026 15:47:34 +0700 Subject: [PATCH 10/11] ci(site): gate hidden image metadata and preserve first failing validation result --- .github/workflows/pages.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 076015ea4..62c3e9f81 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -6,6 +6,8 @@ on: paths: - "landing/**" - "scripts/validate-product-source.py" + - "scripts/audit-public-media.py" + - "scripts/test-audit-public-media.py" - "scripts/build-product-site.py" - "scripts/validate-product-build.py" - "scripts/apply-search-authority.py" @@ -30,6 +32,8 @@ on: paths: - "landing/**" - "scripts/validate-product-source.py" + - "scripts/audit-public-media.py" + - "scripts/test-audit-public-media.py" - "scripts/build-product-site.py" - "scripts/validate-product-build.py" - "scripts/apply-search-authority.py" @@ -122,9 +126,12 @@ jobs: continue-on-error: true shell: bash run: | + set -euo pipefail mkdir -p _validation python scripts/validate-product-source.py > _validation/source.log 2>&1 python scripts/validate-adoption-proof.py >> _validation/source.log 2>&1 + python scripts/test-audit-public-media.py >> _validation/source.log 2>&1 + python scripts/audit-public-media.py --report _validation/media-audit.json >> _validation/source.log 2>&1 - name: Upload source validation report if: always() @@ -133,6 +140,7 @@ jobs: name: product-source-validation path: | _validation/source.log + _validation/media-audit.json _validation/site-source.json if-no-files-found: error From 3b9f834adb985f8a312a7e4f63fea352bcbfc655 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 15:48:22 +0700 Subject: [PATCH 11/11] docs(trust): record release-asset authority and pending human screenshot disclosure review --- ..._P2_RELEASE_AND_MEDIA_REVIEW_2026-10-25.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/PUBLIC_PRODUCT_P2_RELEASE_AND_MEDIA_REVIEW_2026-10-25.md diff --git a/docs/PUBLIC_PRODUCT_P2_RELEASE_AND_MEDIA_REVIEW_2026-10-25.md b/docs/PUBLIC_PRODUCT_P2_RELEASE_AND_MEDIA_REVIEW_2026-10-25.md new file mode 100644 index 000000000..10d9a86db --- /dev/null +++ b/docs/PUBLIC_PRODUCT_P2_RELEASE_AND_MEDIA_REVIEW_2026-10-25.md @@ -0,0 +1,45 @@ +# ARSAS Public Release and Screenshot Review — 2026-10-25 + +## Scope and immutable boundaries + +This checkpoint follows the accepted public **v1.6.42** release and the merged P0/P1 landing work (#742/#743). It does **not** repackage the application, repoint `v1.6.42`, repin ARIEC61850, rewrite the Windows executable, certify IEC 61850 conformance, or approve physical R7/R10 promotion (#729). + +**Source of truth for a downloadable release:** the published GitHub stable tag and immutable release assets, mirrored to `landing/latest.json` and `.release/published.json`. A different version on main/VERSION is intentionally possible pending physical authority. Do not change the stable ledgers to match an unqualified application tree. + +## P2A — release contract hardened + +- The public stable Portable artifact is `ARSAS-Windows-x64-Portable.exe` for **v1.6.42 and later**. Older *tag-specific* release ZIP links may be recognized as historical but must never be presented as the latest stable asset. +- Public analytics must report current portable EXE clicks; a historical ZIP may use a separate legacy event. +- The canonical 62-page rendered site validator is `scripts/validate-product-build.py`. The obsolete `scripts/validate-built-landing.py` command is only a compatibility forwarding entry point; do not resurrect its stale eight-page, Portable ZIP policy. +- `publish-verified-release.yml` remains a **legacy ZIP-format artifact publisher** with a version gate. It must reject a publication request for v1.6.42+ rather than quietly publish a ZIP under a modern tag. **The canonical modern publish lane remains `release-windows.yml`.** +- `release-supply-chain.yml` backfills *historical extractable ZIP* assets. It must fail closed for opaque single-EXE releases; don't invent an extracted EXE file inventory or replace an existing SBOM. Current v1.6.42 already has an SPDX SBOM and provenance from its canonical release. +- `scripts/validate-product-source.py` verifies exact modern asset name and download URL (not a permissive ZIP-or-EXE choice), while keeping legacy version handling explicit. + +## P2B — public screenshot data safety (separate human review) + +Eight user-supplied authentic v1.6.42 screenshots and their WebP counterparts are public: IED Browser, Event Logs, Signal Testing, FAT PDF Preview, SMV, GOOSE, Annunciator, Diagnostics. + +**Known provenance:** #743 records operator approval to publish the supplied screenshot captures. The existing manifest `docs/asset-provenance-manifest.json` records image paths, Git blob IDs and byte sizes. This is **not** independent confirmation that every visible IP address, station/device label, log identifier, person or customer is harmless. + +The Pages source gate `scripts/audit-public-media.py` checks the 16 current PNG/WebP assets for image-container EXIF/XMP/text chunks and malformed headers. Results are uploaded as `media-audit.json`. It reads but never alters source bytes. **This does not inspect rasterized pixels** and does not replace the required visual review. + +### Human review acceptance checklist + +- [ ] View all eight original **PNG** captures at full resolution and all eight **WebP** derivatives; check cropped/offscreen panes, popovers, titles, tabs and status bar. +- [ ] Assess every visible IP/MAC, VLAN, station/feeder/device label, engineer/customer name, serial/asset ID, filename, SCL/path, event/packet log, credentials/certificates, and topology clue for authorized public disclosure. +- [ ] Record reviewer, review date, each capture's exact SHA/blob reference and disposition in a separate review issue or signed off review document. +- [ ] If any pixel-level disclosure is not authorized, remove or redact **both** public derivatives through a separately reviewed change; update provenance and all references. Do not silently alter the original operator capture under the same SHA or claim it is unchanged. +- [ ] Confirm that screenshots demonstrate product UI only; no certification or universal compatibility claim is inferred from simulated/limited field test evidence. + +**Review status:** publication approved by the operator; automated hidden-metadata audit added; **independent pixel/content sanitization NOT YET ASSERTED**. Keep this review open until a person with the authority to disclose the shown engineering data signs off. Do not mark it complete solely because CI is green. + +## P4 — sitemap correction + +The actual October 25 P1 content refresh modified EN/ID Quick Start, Demo, FAQ, and Compatibility pages. Their sitemap registry entries are stamped with **2026-10-25**. This fixes demonstrably stale `lastmod` dates without falsifying unrelated older pages. Subsequent SEO/mobile/accessibility measurement remains a separate review. + +## Next phase gates + +1. Complete and document the human screenshot disclosure review. +2. Audit all 62 rendered site pages for keyboard access, links, contrast, mobile breakpoints, and Core Web Vitals using measured evidence. +3. Simplify the beginner-first README and contribution paths without removing historical changelog, trust boundaries or Git provenance. +4. Keep PR #729 source qualification as a **separate** physical R7/R10 workstream. Mainline source promotion does not follow from website CI success.