From 1029a9aefaeab8501acb1ad94d3876fcbc0ead9d Mon Sep 17 00:00:00 2001 From: interplains Date: Mon, 19 Oct 2026 22:53:51 +0300 Subject: [PATCH 1/2] Search retained DNS history when current logging is disabled --- README.md | 13 +++++---- SIGNING.md | 14 ++++----- TESTING.md | 5 ++-- build/bundle.py | 2 +- scan/README.md | 7 +++-- scan/adint_scan/__init__.py | 2 +- scan/adint_scan/modules/host.py | 28 ++++++++++++------ scan/tests/test_evidence.py | 52 ++++++++++++++++++++++++++++++++- tools/release_check.py | 3 +- tools/release_fixture.py | 19 ++++++++++-- 10 files changed, 112 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 091ff86..46ebdc7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Zero Breach -> Release v0.1.4 prioritizes local COM-registered files in the optional Candiru pattern check and expands Windows DNS-history coverage. See -> [the v0.1.4 scope](COVERAGE-UPGRADE.md) and [pattern validation](DETECTION-UPGRADE.md). It needs a pinned YARA-X -> dependency; default checks remain standard-library-only. +> Release v0.1.5 searches saved Windows DNS events even when logging is currently +> off. It keeps a coverage warning for unrecorded periods and does not change +> logging settings. 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) @@ -144,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.4 includes detached GPG +**1. Verify the publisher's signature.** Release v0.1.5 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). @@ -168,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.4 +git checkout v0.1.5 python build/build.py # writes dist/zero-breach.py and prints its SHA-256 ``` -These commands reproduce v0.1.4; substitute the matching tag for another release. +These commands reproduce v0.1.5; 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 5924710..f565d61 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -1,17 +1,17 @@ # Verifying ArchiveGenocide release signatures -Zero Breach v0.1.4 supplies detached GPG signatures for the exact released +Zero Breach v0.1.5 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.4 +Official release: https://github.com/interplains/zero-breach/releases/tag/v0.1.5 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.4 release into one directory: +Download these three files from the v0.1.5 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.4 is: +The expected SHA-256 of zero-breach.py v0.1.5 is: - 5ab89ef3dda3579dfc0b4930b0d66290fdc755611afd91e28c373e30fe9996cc + 59811565be507c7d956e77f5f396b81545259c551fa9208d85654a01dec1048e 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.4/. +Public verification artifacts are also retained in release-signatures/v0.1.5/. 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.4; +The signed release manifest covers the indicator file shipped with v0.1.5; it does not authenticate later files fetched from the development branch. ## Maintainer release procedure diff --git a/TESTING.md b/TESTING.md index a2fb7bb..5b60f6c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -35,11 +35,12 @@ untested optional positives as a skip. Use `--require-patterns` before release. - The existing unit/regression suite. - Two byte-identical builds of the downloadable script, plus its help/version. -- Eleven fixture scenarios, each in fresh source-CLI and single-file processes: +- Fifteen fixture scenarios, each in fresh source-CLI and single-file processes: clean data, contacted domain, harmless research reference, exact hash hit, corrupt database, a hit alongside a corrupt database, `--quick` coverage gap, a DNS hit past the old 20,000-event limit, unread older DNS events without a - hit, invalid indicator input, and COM-referenced optional file patterns. + hit, four disabled-DNS-log cases (retained match, retained clean data, + unreadable retained events and no events), invalid indicator input, and COM-referenced optional file patterns. - Real browser-shaped SQLite files and harmless rule-matching bytes pass through the real scanner modules, evidence grading, exit codes and text/JSON reports. Source and download must agree. Cookie values must stay out of reports; diff --git a/build/bundle.py b/build/bundle.py index 1798274..dbf3d8f 100644 --- a/build/bundle.py +++ b/build/bundle.py @@ -160,7 +160,7 @@ def _asof_epoch(text): A('import sys') A('import time') A('') -A('__version__ = "0.1.4 (single-file build)"') +A('__version__ = "0.1.5 (single-file build)"') A('') A('# --- embedded indicator data ------------------------------------------------') A('# The real file is iocs/adfection_iocs.txt in the project. Its provenance') diff --git a/scan/README.md b/scan/README.md index d90d121..bc67c92 100644 --- a/scan/README.md +++ b/scan/README.md @@ -1,8 +1,9 @@ # adint-scan -> Release v0.1.4 improves coverage for the experimental, opt-in `--patterns` check. See -> [COVERAGE-UPGRADE.md](../COVERAGE-UPGRADE.md) and [DETECTION-UPGRADE.md](../DETECTION-UPGRADE.md) for dependencies, limits and -> validation evidence. Default checks need no added packages. +> Release v0.1.5 reads retained Windows DNS events even if logging is currently +> disabled, and keeps gaps for unrecorded activity. It never enables logging. +> Optional Candiru pattern dependencies and limits are documented in +> [DETECTION-UPGRADE.md](../DETECTION-UPGRADE.md). A local, read-only check for the published AdFection / Candiru indicators, with explicit reporting of missing evidence and checks that could not run. diff --git a/scan/adint_scan/__init__.py b/scan/adint_scan/__init__.py index f5d6c89..c62af21 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.4" +__version__ = "0.1.5" diff --git a/scan/adint_scan/modules/host.py b/scan/adint_scan/modules/host.py index 744b335..4e3616d 100644 --- a/scan/adint_scan/modules/host.py +++ b/scan/adint_scan/modules/host.py @@ -690,7 +690,7 @@ def _check_dns_log(r: ModuleResult, ctx) -> None: r.submetrics["dns_log_enabled"] = 1 if enabled else 0 r.submetrics["dns_log_records"] = records - if not enabled or records <= 0: + if records <= 0: # The important branch, and the common one. This must never read clean. r.could_not_run("dns-log", _DNS_LOG_GAP) r.note("dns-log: {} enabled={} records={} — forward-looking checks are " @@ -702,14 +702,23 @@ def _check_dns_log(r: ModuleResult, ctx) -> None: "days": 0, "basis": "DNS logging is {} and holds {} records".format( "enabled" if enabled else "disabled", records), - "note": "Nothing was ever recorded, so there is no window at all — " - "not a short one. This is the Windows default, on every PC.", + "note": "No saved DNS events were available for this scan. " + "This does not establish whether older events existed or were cleared.", "how_to_enable": _HOW_TO_ENABLE, } return - ctx.emit("[host] DNS-Client/Operational is enabled ({} records); " - "scanning messages".format(records)) + # IsEnabled controls current collection, not whether retained events can + # be read. Disabling logging must not hide existing historical evidence. + if not enabled: + r.could_not_run("dns-log-recording", + "DNS logging is currently off; queries made while " + "logging was off may be missing") + r.note("dns-log: logging is off; the log reports {} saved event(s). " + "Checking them does not enable logging." + .format(records)) + ctx.emit("[host] DNS-Client/Operational logging is {} ({} retained records); " + "scanning messages".format("on" if enabled else "off", records)) # -ErrorAction Stop, not SilentlyContinue: with SilentlyContinue a failure to # read the channel (ACL, corrupt .evtx, Event Log service degraded) exits 0 # with no output, so `events` is [] and `ev_err` is "" - and the log, which is @@ -725,18 +734,16 @@ def _check_dns_log(r: ModuleResult, ctx) -> None: " | ConvertTo-Json -Depth 3 -Compress".format( _DNS_LOG, _DNS_EVENT_CAP), timeout=300) if (ev_err and not events) or (records > 0 and not events): - reason = ("log is enabled with {} records but none could be read{} - this " + reason = ("log has {} retained records but none could be read{} - this " "machine's DNS history was NOT searched, so nothing here is " "cleared".format(records, ": " + ev_err if ev_err else "")) r.could_not_run("dns-log", reason) - r.note(_ARM_HINT) return hits = 0 times = [t for t in (_str(ev, "Time") for ev in events) if t] if times: - # The log IS on, so there is a real window. Report its true extent rather - # than implying the whole history of the machine. + # Retained records have dates even when logging is currently disabled. r.reach["dns-log"] = { "earliest": min(times), "days": None, @@ -745,6 +752,9 @@ def _check_dns_log(r: ModuleResult, ctx) -> None: "note": "Queries before this are either rotated out of the log or " "beyond the bounded event read.", } + if not enabled: + r.reach["dns-log"]["note"] += " Logging is currently off; queries during unrecorded periods may be missing." + r.reach["dns-log"]["how_to_enable"] = _HOW_TO_ENABLE for ev in events: when = _str(ev, "Time") hits += _match( diff --git a/scan/tests/test_evidence.py b/scan/tests/test_evidence.py index 17a04a5..a452b3f 100644 --- a/scan/tests/test_evidence.py +++ b/scan/tests/test_evidence.py @@ -30,7 +30,7 @@ from adint_scan import report # noqa: E402 from adint_scan.context import ScanContext # noqa: E402 from adint_scan.modules import browser # noqa: E402 -from adint_scan.result import Hit, ModuleResult, PARTIAL # noqa: E402 +from adint_scan.result import Hit, ModuleResult, PARTIAL, UNKNOWN # noqa: E402 TEST_DOMAIN = "synthetic-test-indicator.invalid" @@ -305,6 +305,56 @@ def test_real_address_is_a_dns_record_without_proof_of_contact(self): class TestDnsLogIsNotProofOfContact(unittest.TestCase): + def disabled_log(self, records, events, error=""): + from adint_scan.modules import host + with tempfile.TemporaryDirectory(prefix="dnslog_off_") as tmp: + result = ModuleResult("host") + replies = [([{"IsEnabled": False, "RecordCount": records}], "")] + if records: + replies.append((events, error)) + with patch.object(host, "_ps_json", side_effect=replies) as query: + host._check_dns_log(result, make_ctx(tmp)) + self.assertEqual(query.call_count, 2 if records else 1) + for call in query.call_args_list: + self.assertIn("Get-WinEvent", call.args[0]) + self.assertNotIn("wevtutil", call.args[0]) + return result + + def test_disabled_log_still_searches_retained_matching_event(self): + result = self.disabled_log(1, [{"Time": "2025-03-04T12:00:00Z", "Message": TEST_DOMAIN}]) + self.assertEqual(result.status, "HITS") + self.assertEqual([(h.indicator, h.evidence) for h in result.hits], [(TEST_DOMAIN, "dns-record")]) + self.assertEqual(result.hits[0].observed, "2025-03-04T12:00:00Z") + self.assertEqual(result.submetrics["dns_log_events_scanned"], 1) + self.assertEqual(result.submetrics["dns_log_enabled"], 0) + self.assertIn("how_to_enable", result.reach["dns-log"]) + + def test_disabled_log_clean_retained_event_keeps_recording_gap(self): + result = self.disabled_log(1, [{"Time": "2025-03-04T12:00:00Z", "Message": "ordinary.invalid"}]) + self.assertEqual(result.status, PARTIAL) + self.assertEqual(result.hits, []) + self.assertTrue(any(c.name == "dns-log" and c.ran for c in result.checks)) + self.assertTrue(any(c.name == "dns-log-recording" and not c.ran for c in result.checks)) + + def test_disabled_empty_log_is_not_clean(self): + result = self.disabled_log(0, []) + self.assertEqual(result.status, UNKNOWN) + self.assertNotIn("dns_log_events_scanned", result.submetrics) + + def test_disabled_log_read_failure_is_not_clean(self): + result = self.disabled_log(1, [], "fixture permission denied") + self.assertEqual(result.status, UNKNOWN) + self.assertEqual(result.hits, []) + self.assertTrue(any(c.name == "dns-log" and not c.ran for c in result.checks)) + self.assertNotIn("dns_log_events_scanned", result.submetrics) + + def test_disabled_log_preserves_hit_and_older_retained_gap_together(self): + result = self.disabled_log(50001, [{"Time": "2025-03-04T12:00:00Z", "Message": TEST_DOMAIN}]) + self.assertEqual(result.status, "HITS") + self.assertEqual(len(result.hits), 1) + self.assertTrue(any(c.name == "dns-log-older-events" and not c.ran for c in result.checks)) + self.assertTrue(any(c.name == "dns-log-recording" and not c.ran for c in result.checks)) + def test_dns_match_after_old_20000_event_limit_is_read(self): from adint_scan.modules import host with tempfile.TemporaryDirectory(prefix="dnslog_extended_") as tmp: diff --git a/tools/release_check.py b/tools/release_check.py index 25343a2..d82996a 100644 --- a/tools/release_check.py +++ b/tools/release_check.py @@ -15,7 +15,8 @@ ROOT = Path(__file__).resolve().parents[1] CASES = ("clean", "contact", "reference", "hash", "corrupt", "hit-and-gap", "quick", - "dns-cap-hit", "dns-cap-gap", "bad-feed", "patterns") + "dns-cap-hit", "dns-cap-gap", "dns-off-retained-hit", "dns-off-retained-clean", + "dns-off-unreadable", "dns-off-empty", "bad-feed", "patterns") class DownloadPanel(HTMLParser): diff --git a/tools/release_fixture.py b/tools/release_fixture.py index 95c6821..f64c235 100644 --- a/tools/release_fixture.py +++ b/tools/release_fixture.py @@ -126,6 +126,8 @@ def audit(event, args): raise RuntimeError("fixture attempted file access outside test/code/runtime roots") events = [{"Time": "2025-06-01T00:00:00", "Message": "ordinary.invalid"}] + if case == "dns-off-retained-hit": + events[0]["Message"] = DOMAIN if case.startswith("dns-cap-"): events = events * 50000 if case == "dns-cap-hit": @@ -133,9 +135,12 @@ def audit(event, args): def ps_json(command, timeout=60): if "-ListLog " in command: - return [{"IsEnabled": True, "RecordCount": len(events) + int(case.startswith("dns-cap-"))}], "" + return [{"IsEnabled": not case.startswith("dns-off-"), + "RecordCount": 0 if case == "dns-off-empty" else len(events) + int(case.startswith("dns-cap-"))}], "" if "Get-WinEvent -LogName " in command: require("-MaxEvents 50000" in command, "unexpected DNS read cap") + if case == "dns-off-unreadable": + return [], "fixture read denied" return events, "" if "Get-DnsClientCache" in command: return [{"Entry": DOMAIN, "Data": "0.0.0.0", "Type": 1}], "" @@ -196,6 +201,7 @@ def ps_json(command, timeout=60): "reference": ("browser", DOMAIN, "referenced"), "hash": ("hash", DIGEST, "on-disk"), "dns-cap-hit": ("host", DOMAIN, "dns-record")} + expected["dns-off-retained-hit"] = ("host", DOMAIN, "dns-record") if case == "patterns" and engine: expected[case] = ("patterns", "yara:MAL_DevilsTongue_HijackDll", "file-pattern") require(hits == ([expected[case]] if case in expected else []), @@ -204,11 +210,20 @@ def ps_json(command, timeout=60): require(verdict["hits_total"] == len(hits), "incorrect total finding count") expected_verdict = ("INDICATORS_FOUND" if hits else "NO_KNOWN_INDICATORS_WITH_GAPS" if case in - ("corrupt", "quick", "dns-cap-gap", "patterns") else "NO_KNOWN_INDICATORS") + ("corrupt", "quick", "dns-cap-gap", "patterns", "dns-off-retained-clean", + "dns-off-unreadable", "dns-off-empty") else "NO_KNOWN_INDICATORS") require(verdict["status"] == expected_verdict, "incorrect overall verdict: " + str(verdict)) if case.startswith("dns-cap-"): require(any("dns-log-older-events" in g for g in verdict["gaps"]), "missing DNS cap gap") require(modules["host"]["submetrics"]["dns_log_events_scanned"] == 50000, "wrong DNS count") + if case.startswith("dns-off-"): + require(modules["host"]["submetrics"]["dns_log_enabled"] == 0, "logging state misreported") + require(bool(verdict["gaps"]), "disabled logging coverage gap missing") + if case.startswith("dns-off-retained-"): + require(modules["host"]["submetrics"]["dns_log_events_scanned"] == 1, "retained event not searched") + require("HOW TO TURN ON DNS HISTORY" in text, "future-logging instructions missing") + if case in ("dns-off-empty", "dns-off-unreadable"): + require(not modules["host"]["submetrics"].get("dns_log_events_scanned"), "unread log falsely searched") if case == "quick": require(modules["hash"]["status"] == "UNKNOWN", "quick scan falsely cleared file hashes") if case in ("corrupt", "hit-and-gap"): From dbbeee3ebbbeb97faf8a975f0983c2c7af000ffb Mon Sep 17 00:00:00 2001 From: interplains Date: Mon, 19 Oct 2026 22:55:21 +0300 Subject: [PATCH 2/2] Publish verification files for v0.1.5 --- release-signatures/v0.1.5/SHA256SUMS.txt | 15 +++++++++++++++ release-signatures/v0.1.5/SHA256SUMS.txt.asc | 7 +++++++ .../v0.1.5/archivegenocide-signing-key.asc | 10 ++++++++++ release-signatures/v0.1.5/zero-breach.py.asc | 7 +++++++ 4 files changed, 39 insertions(+) create mode 100644 release-signatures/v0.1.5/SHA256SUMS.txt create mode 100644 release-signatures/v0.1.5/SHA256SUMS.txt.asc create mode 100644 release-signatures/v0.1.5/archivegenocide-signing-key.asc create mode 100644 release-signatures/v0.1.5/zero-breach.py.asc diff --git a/release-signatures/v0.1.5/SHA256SUMS.txt b/release-signatures/v0.1.5/SHA256SUMS.txt new file mode 100644 index 0000000..1e15017 --- /dev/null +++ b/release-signatures/v0.1.5/SHA256SUMS.txt @@ -0,0 +1,15 @@ +b013f21a44956c6d717f211990b450aeaa4d7266da4614ab8c386b479565296e LICENSE-CC-BY-SA-4.0.txt +8a959b2413d0c83ca1b3ce9153076c649abf2e4bbfebbb493fb24c2074a1eb8a LICENSE-DRL-1.1.txt +b84b756654d259f98841cb8ecc9ce7560d1b407276f2a1e640d462559fefc0c4 NOTICE +437a044d6ab5157e5e7af5a17ea3e311e409ff7093a6173d53376f3a1d42962c adfection.adblock.txt +712e2ec22cd6020c7e72c995c1e2c925546c24616a27bfe6b57692a53f2f093d adfection.dnsmasq.conf +d47609b69b58b918305b1c2e7fa6a8d1386cded512fe167613aec1375d254b71 adfection.domains.txt +8276d3ec8c4210ea4c3c7fbc2065a6d02fdc782003bd168a11cc39e127c85e49 adfection.hosts.txt +5ee47a8d062082f7a98ab85284a46d379141dc6172ba1e8e0d3fb22960a5ebe7 adfection.misp.json +b346ad6fa1be58172847a5efca8567abb92428bd5359e9150922c37d1567c745 adfection.stix.json +b86d4526066599e80b12971739e70a701a217e4c046ec29d06b8716ef2bf0572 adfection.withdrawals.stix.json +c9801e3ba37376535a676d9938d65550bcaa5478578cb80a6dd09eb2ac402d38 adfection_iocs.txt +9e8a80c2f80cf2fe7ff74be6308b4d9fc3a7fe14d6b90c9c9567e3479131b6c8 adfection_iocs.txt.sha256 +d1c2ca7de2def9915071835ec3755ab4c856b7672fae184a792051c7952f19cb candiru-rule-PROVENANCE.json +649f9eb135b22de90ed1d18d227fb80506ff403f70905330806f8a98346d0e97 requirements-patterns.txt +59811565be507c7d956e77f5f396b81545259c551fa9208d85654a01dec1048e zero-breach.py diff --git a/release-signatures/v0.1.5/SHA256SUMS.txt.asc b/release-signatures/v0.1.5/SHA256SUMS.txt.asc new file mode 100644 index 0000000..6a6a96d --- /dev/null +++ b/release-signatures/v0.1.5/SHA256SUMS.txt.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTCTskrEtZnCiUWBl+bTVdUma+lPAUCatZ1qQAKCRCbTVdUma+l +PAQyAQD1/O34Yzh4/UVtZSbMboGlTw9vC3QmMyNri1hCLnPHpgEA6go9S8A8JGNB +JACPBVNIu+fDBaCvcvFOJPdia7ensgM= +=15hH +-----END PGP SIGNATURE----- diff --git a/release-signatures/v0.1.5/archivegenocide-signing-key.asc b/release-signatures/v0.1.5/archivegenocide-signing-key.asc new file mode 100644 index 0000000..e36686c --- /dev/null +++ b/release-signatures/v0.1.5/archivegenocide-signing-key.asc @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEakkTBRYJKwYBBAHaRw8BAQdA+pOHPwElYpMviQuBZYxZeFXDSxJeG+Ib+RfE +IBDND8+0LkFyY2hpdmUgR2Vub2NpZGUgPGFyY2hpdmVAYXJjaGl2ZWdlbm9jaWRl +LmNvbT6IkwQTFgoAOxYhBMJOySsS1mcKJRYGX5tNV1SZr6U8BQJqSRMFAhsDBQsJ +CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEJtNV1SZr6U8dS4BANz2SqF8JJEe +npU+A9bm+aYLKrTYsG1SUKmuctWU2ZnYAQCQS5WMaZM6tIUBL9tQDnW7gVCjFBQr +/lMpsWDm8BG2Aw== +=L8Ay +-----END PGP PUBLIC KEY BLOCK----- diff --git a/release-signatures/v0.1.5/zero-breach.py.asc b/release-signatures/v0.1.5/zero-breach.py.asc new file mode 100644 index 0000000..4f37090 --- /dev/null +++ b/release-signatures/v0.1.5/zero-breach.py.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTCTskrEtZnCiUWBl+bTVdUma+lPAUCatZ1qQAKCRCbTVdUma+l +PIbMAP9sPoqzbgq0F7iq5NaiTOH568jl+cQvxsBeNSCLLqg61gEA3U6VctZGc4HO +HDWOAPdGMjncfZ8VtQ9+Jq+xCDp3dwI= +=amYZ +-----END PGP SIGNATURE-----