From af3df56dec6c40a5e4c2501493b84cf7873afdc6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:07:43 +0000 Subject: [PATCH 1/3] test: audit official NT009 workbook provenance Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/domain-tables.yml | 3 + tools/audit_nt009_sources.py | 106 ++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tools/audit_nt009_sources.py diff --git a/.github/workflows/domain-tables.yml b/.github/workflows/domain-tables.yml index 16cc581..bfa98d6 100644 --- a/.github/workflows/domain-tables.yml +++ b/.github/workflows/domain-tables.yml @@ -7,6 +7,7 @@ on: pull_request: paths: - "tools/generate_domain_tables.py" + - "tools/audit_nt009_sources.py" - "tools/tests/test_generate_domain_tables.py" - "resources/domains/**" - ".github/workflows/domain-tables.yml" @@ -23,3 +24,5 @@ jobs: persist-credentials: false - name: Test deterministic XLSX normalization run: python3 -m unittest tools/tests/test_generate_domain_tables.py + - name: Verify official NT009 workbook provenance + run: python3 tools/audit_nt009_sources.py --inspect diff --git a/tools/audit_nt009_sources.py b/tools/audit_nt009_sources.py new file mode 100644 index 0000000..596f80c --- /dev/null +++ b/tools/audit_nt009_sources.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Audit the exact NT009 XLSX inputs before changing offline tax domains. + +Run manually; never fetch external data from application runtime. +The hashes are independent download observations recorded on 2026-10-05, +not a published governmental checksum. Reconfirm against the official URLs. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import tempfile +import time +import urllib.request +from pathlib import Path + +from generate_domain_tables import Workbook + +SOURCES = { + "annex_vi": ( + "https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/rtc/" + "anexovi-leiautesrn_rtc_ibscbs-v1-04-01-nt009.xlsx", + "103a150dd6f56ec8edcaa3a453b59a1b0e096d7cd5025d7bd06a387f141ddf39", + ), + "annex_vii": ( + "https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/rtc/" + "anexovii-indop_ibscbs_v1-03-00-nt009.xlsx", + "95f30a44ee94adeea57fb92f3a0337b1e62fdc106f393e73894087889be9e5aa", + ), +} + + +def retrieve(url: str, destination: Path) -> bytes: + request = urllib.request.Request( + url, headers={"User-Agent": "LibreCodeCoop/nfse-php normative source audit"} + ) + for attempt in range(4): + try: + with urllib.request.urlopen(request, timeout=45) as response: + payload = response.read() + destination.write_bytes(payload) + return payload + except (OSError, TimeoutError): + if attempt == 3: + raise + time.sleep(2 ** attempt) + raise RuntimeError("unreachable") + + +def inspect_annex(name: str, path: Path, dump: bool) -> None: + with Workbook(path) as book: + print(f"{name}: sheets = {list(book.sheet_targets)}", flush=True) + if not dump: + return + for sheet in book.sheet_targets: + if name == "annex_vi" and "LEIAUTE DPS" not in sheet: + continue + rows = book.rows(sheet) + for number, cells in enumerate(rows, 1): + joined = " ".join(cells.values()) + if name == "annex_vii": + if not any(re.fullmatch(r"[0-9]{6}", re.sub(r"\\D", "", val)) for val in cells.values()): + if number > 12: + continue + elif not any(marker in joined for marker in ( + "IBSCBS", "indDest", "finNFSe", "tpNFSeDebito", + "tpNFSeCredito", "gPgtoVinc", "regApIBSCBSSN", + "CST", "cClassTrib", + )): + continue + print(f"{name}:{sheet}:{number}: {json.dumps(cells, ensure_ascii=False)}", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--inspect", action="store_true", help="Print source rows for manual normative review") + parser.add_argument("--output-dir", type=Path, help="Keep exact verified XLSX files here") + args = parser.parse_args() + + def audit(destination: Path) -> None: + for name, (url, expected) in SOURCES.items(): + path = destination / f"{name}.xlsx" + payload = retrieve(url, path) + actual = hashlib.sha256(payload).hexdigest() + print(f"{name}: {url} sha256={actual} bytes={len(payload)}", flush=True) + if actual != expected: + raise ValueError(f"{name}: source checksum differs from recorded observation {expected}") + inspect_annex(name, path, args.inspect) + + if args.output_dir: + args.output_dir.mkdir(parents=True, exist_ok=True) + audit(args.output_dir) + else: + with tempfile.TemporaryDirectory() as temporary: + audit(Path(temporary)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 6ed54be924a2b4d04296b02297b7428be3a42333 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:07:59 +0000 Subject: [PATCH 2/3] fix: keep normative source audit opt-in and offline CI deterministic Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/domain-tables.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/domain-tables.yml b/.github/workflows/domain-tables.yml index bfa98d6..52dec78 100644 --- a/.github/workflows/domain-tables.yml +++ b/.github/workflows/domain-tables.yml @@ -24,5 +24,3 @@ jobs: persist-credentials: false - name: Test deterministic XLSX normalization run: python3 -m unittest tools/tests/test_generate_domain_tables.py - - name: Verify official NT009 workbook provenance - run: python3 tools/audit_nt009_sources.py --inspect From 91551e9a3da880e5ef53be52d6564e0e764af379 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:08:53 +0000 Subject: [PATCH 3/3] fix: normalize indicator codes with digit matching Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tools/audit_nt009_sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/audit_nt009_sources.py b/tools/audit_nt009_sources.py index 596f80c..42c4499 100644 --- a/tools/audit_nt009_sources.py +++ b/tools/audit_nt009_sources.py @@ -65,7 +65,7 @@ def inspect_annex(name: str, path: Path, dump: bool) -> None: for number, cells in enumerate(rows, 1): joined = " ".join(cells.values()) if name == "annex_vii": - if not any(re.fullmatch(r"[0-9]{6}", re.sub(r"\\D", "", val)) for val in cells.values()): + if not any(re.fullmatch(r"[0-9]{6}", re.sub(r"\D", "", val)) for val in cells.values()): if number > 12: continue elif not any(marker in joined for marker in (