From 89db75a5f882372b6466de4f28ad9f12e8336d48 Mon Sep 17 00:00:00 2001 From: interplains Date: Tue, 20 Oct 2026 16:16:50 +0300 Subject: [PATCH] Prepare v0.1.8: correct the self-reported version and guard it v0.1.7 shipped a single file that reported "Zero Breach 0.1.6": the version was hardcoded in build/bundle.py and had drifted from scan/adint_scan/__init__.py, which was itself never bumped. Someone who updated to get the security fix saw 0.1.6 and could not tell the fix was present. - Bump the package version to 0.1.8. - bundle.py now derives the single-file version from the package instead of repeating it, so the two can never disagree again. - build.py gains --expect-version, which fails the build unless the package version matches the release being cut. - Add a regression test asserting the bundle reports the package version. - Update README and SIGNING.md to v0.1.8 and the new zero-breach.py hash. --- README.md | 12 ++++++------ SIGNING.md | 14 +++++++------- build/build.py | 19 +++++++++++++++++++ build/bundle.py | 16 +++++++++++++++- scan/adint_scan/__init__.py | 2 +- scan/tests/test_bundle_release.py | 11 +++++++++++ 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ecaf298..c1d7234 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Zero Breach -> Release v0.1.6 fixes COM file-path parsing when a folder name contains an -> executable extension, and limits historical coverage statements to evidence -> actually examined. Optional Candiru patterns still need the pinned YARA-X engine; +> Release v0.1.8 corrects the version the download reports (v0.1.7 shipped +> reporting itself as 0.1.6) and refuses network paths given to `--iocs`/`--json` +> in the single-file build. Optional Candiru patterns still need the pinned YARA-X engine; > see [pattern validation](DETECTION-UPGRADE.md). Default checks remain standard-library-only. [![tests](https://github.com/interplains/zero-breach/actions/workflows/tests.yml/badge.svg)](https://github.com/interplains/zero-breach/actions/workflows/tests.yml) @@ -145,7 +145,7 @@ set up (e.g. a bad indicator file). Use the official repository above and check the downloaded file before running it. -**1. Verify the publisher's signature.** Release v0.1.6 includes detached GPG +**1. Verify the publisher's signature.** Release v0.1.8 includes detached GPG signatures for the scanner and its checksum manifest, using ArchiveGenocide's established release key. Follow [SIGNING.md](SIGNING.md), and compare the full key fingerprint with [ArchiveGenocide's verification page](https://archivegenocide.com/verify.html). @@ -169,11 +169,11 @@ you do not have to trust the download at all; you can reproduce it: ``` git clone https://github.com/interplains/zero-breach.git cd zero-breach -git checkout v0.1.6 +git checkout v0.1.8 python build/build.py # writes dist/zero-breach.py and prints its SHA-256 ``` -These commands reproduce v0.1.6; substitute the matching tag for another release. +These commands reproduce v0.1.8; substitute the matching tag for another release. On macOS or Linux use `python3` for the build command. Compare the hash with that release's published hash. The development branch may contain later changes and is not expected to reproduce an earlier release. diff --git a/SIGNING.md b/SIGNING.md index 628a94b..867f3f0 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -1,17 +1,17 @@ # Verifying ArchiveGenocide release signatures -Zero Breach v0.1.7 supplies detached GPG signatures for the exact released +Zero Breach v0.1.8 supplies detached GPG signatures for the exact released scanner and its SHA256SUMS.txt manifest. They use ArchiveGenocide's existing Ed25519 release-signing key, also published on the archive's verification page. Official repository: https://github.com/interplains/zero-breach -Official release: https://github.com/interplains/zero-breach/releases/tag/v0.1.7 +Official release: https://github.com/interplains/zero-breach/releases/tag/v0.1.8 Independent publisher page: https://archivegenocide.com/scan/#verify-download Established key identity: https://archivegenocide.com/verify.html ## Check the publisher and file -Download these three files from the v0.1.7 release into one directory: +Download these three files from the v0.1.8 release into one directory: - zero-breach.py - zero-breach.py.asc @@ -36,9 +36,9 @@ Verification must succeed using the expected ArchiveGenocide key. A warning that the key is not certified by your own web of trust is different from a bad signature; do not ignore a fingerprint mismatch or failed verification. -The expected SHA-256 of zero-breach.py v0.1.7 is: +The expected SHA-256 of zero-breach.py v0.1.8 is: - 9431426a940a3a9bdea617017030e0fa7b155693c3b2d25d97960d1eb262ae4e + 74740af20f609f82dc6e50d66f234d0c39a20f516caa35731c2d19c11684ef44 A signature verifies which key signed these exact bytes. It does not establish that the program has no bugs. A checksum alone is not publisher authentication. @@ -58,14 +58,14 @@ The manifest lists the fifteen release-content files, including the optional requirements file and Candiru rule attribution and licence records. The manifest itself and the added public key/signatures are not self-listed. -Public verification artifacts are also retained in release-signatures/v0.1.7/. +Public verification artifacts are also retained in release-signatures/v0.1.8/. No private key is stored in this repository or in its project backups. ## Indicator updates are a separate trust path The manually run scan/update-iocs.py still checks HTTPS, a published SHA-256, and indicator-file validity. It does not automatically verify GPG signatures. -The signed release manifest covers the indicator file shipped with v0.1.7; +The signed release manifest covers the indicator file shipped with v0.1.8; it does not authenticate later files fetched from the development branch. ## Maintainer release procedure diff --git a/build/build.py b/build/build.py index 9ed5da0..9287fa0 100644 --- a/build/build.py +++ b/build/build.py @@ -49,8 +49,27 @@ def main(argv=None): p = argparse.ArgumentParser(description="Rebuild all generated artifacts.") p.add_argument("--check", action="store_true", help="exit non-zero if any artifact changed (for CI)") + p.add_argument("--expect-version", metavar="X.Y.Z", + help="fail unless the package __version__ equals this. Run " + "with the release's own number when cutting a release, " + "so the single file can never again ship reporting a " + "different version than the tag (as v0.1.7 did).") args = p.parse_args(argv) + if args.expect_version: + init = io.open(os.path.join(ROOT, "scan", "adint_scan", "__init__.py"), + encoding="utf-8").read() + import re as _re + found = _re.search(r'__version__\s*=\s*"([^"]+)"', init) + found = found.group(1) if found else "(unreadable)" + if found != args.expect_version: + print("\nERROR: package __version__ is {!r} but the release " + "being cut is {!r}. Update scan/adint_scan/__init__.py " + "before building.".format(found, args.expect_version), + file=sys.stderr) + return 1 + print("version check: package reports {} as expected.".format(found)) + if not os.path.isdir(DIST): os.makedirs(DIST) diff --git a/build/bundle.py b/build/bundle.py index 8c27ce6..9b6cf0b 100644 --- a/build/bundle.py +++ b/build/bundle.py @@ -26,6 +26,20 @@ PKG = os.path.join(SRC, "scan", "adint_scan") IOC = os.path.join(SRC, "iocs", "adfection_iocs.txt") +# Single source of truth for the version. Read it from the package rather than +# repeating it here: the two silently drifted once and the download shipped +# reporting the wrong version (it said 0.1.6 while it was v0.1.7). Derive it, so +# that can never happen again. +def _package_version(): + init = io.open(os.path.join(PKG, "__init__.py"), encoding="utf-8").read() + m = re.search(r'__version__\s*=\s*"([^"]+)"', init) + if not m: + raise SystemExit("bundle: could not read __version__ from the package") + return m.group(1) + + +VERSION = _package_version() + def read(path): return io.open(path, encoding="utf-8").read() @@ -161,7 +175,7 @@ def _asof_epoch(text): A('import sys') A('import time') A('') -A('__version__ = "0.1.6 (single-file build)"') +A('__version__ = "{} (single-file build)"'.format(VERSION)) A('') A('# --- embedded indicator data ------------------------------------------------') A('# The real file is iocs/adfection_iocs.txt in the project. Its provenance') diff --git a/scan/adint_scan/__init__.py b/scan/adint_scan/__init__.py index 19b16f7..8eb5811 100644 --- a/scan/adint_scan/__init__.py +++ b/scan/adint_scan/__init__.py @@ -4,4 +4,4 @@ that can be checked." It does NOT mean the device is clean. See SPEC-scanner.md. """ -__version__ = "0.1.6" +__version__ = "0.1.8" diff --git a/scan/tests/test_bundle_release.py b/scan/tests/test_bundle_release.py index be52174..5ce25e8 100644 --- a/scan/tests/test_bundle_release.py +++ b/scan/tests/test_bundle_release.py @@ -230,3 +230,14 @@ def test_emitter_writes_separate_deterministic_withdrawal_feed(self): if __name__ == "__main__": unittest.main() + + + def test_bundle_reports_the_package_version(self): + # v0.1.7 shipped a single file that reported "0.1.6" because the version + # was hardcoded in build/bundle.py and drifted from the package. The + # bundle now derives it, so the two can never disagree again. + from adint_scan import __version__ as pkg_version + self.assertTrue( + self.bundle.__version__.startswith(pkg_version), + "bundle reports {!r}, package is {!r}".format( + self.bundle.__version__, pkg_version))