From 2fe1fc85d0e72494a139c5774bb05c58634539f7 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg
Date: Sun, 25 Oct 2026 21:08:17 -0400
Subject: [PATCH] Preserve executable provenance and completeness in every
report format
---
README.md | 3 ++
docs/REFERENCE.md | 24 +++++++++++--
node2shell_check.py | 36 ++++++++++++++-----
test_report_provenance.py | 76 +++++++++++++++++++++++++++++++++++++++
4 files changed, 127 insertions(+), 12 deletions(-)
create mode 100644 test_report_provenance.py
diff --git a/README.md b/README.md
index a49a065..bcda0fe 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,9 @@ Dual licensed: [AGPL-3.0](LICENSE), including commercial use that meets its term
An interruption after a complete report is published preserves that report and exits 130.
CSV rejects NUL characters before writing a report.
+Report schema `1.1` records the executable SHA-256 and explicit
+run completeness in JSON, CSV and HTML. Empty CSV reports contain a summary row;
+existing observation columns and inventory schema `1` remain compatible.
Optional release tooling: [offline TUF bundle verification](tools/signed_rules/README.md)
uses separate optional dependencies and an explicitly provisioned public trust root.
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 09d3af0..6d26fa8 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -133,6 +133,19 @@ fields. Array values are represented as readable text. Strings that could become
spreadsheet formulas are prefixed with an apostrophe. Progress and diagnostic
messages go to stderr, leaving stdout available for data.
+Report schema `1.1` preserves every existing result-column position
+and appends `tool`, `tool_version`, `tool_sha256`, `report_schema_version`,
+`generated_at`, `complete`, and `record_type`. `complete` is lowercase `true` or
+`false`; `record_type=result` identifies an observation. Empty runs have one
+`record_type=summary` row carrying build identity, rule date, completeness and
+`not_tested`, with no fabricated target. Consumers must distinguish summaries
+from observations. An interrupted zero-result CSV is therefore self-describing.
+The classifier's `tool_version` remains `1.0.0` for existing named-adapter
+compatibility; the additive report schema and exact executable digest identify
+this changed export. Existing bridge profile `node2shell-v1` still accepts its
+JSON; retain the original report for the new top-level provenance fields, which
+that older converter does not project. Classification thresholds are unchanged.
+
### JSON
JSON is an object with these top-level fields:
@@ -141,6 +154,8 @@ JSON is an object with these top-level fields:
| --- | --- |
| `tool` | Checker name. |
| `tool_version` | Checker version used for the run. |
+| `tool_sha256` | SHA-256 of this standalone executable's exact bytes, captured at startup. Not a publisher signature. |
+| `report_schema_version` | Additive output contract, currently `1.1`; inventory schema remains `1`. |
| `rules_as_of` | Date of the embedded assessment rules. |
| `generated_at` | Report generation timestamp. |
| `complete` | Whether all requested observations completed. Inspect individual operational outcomes as well. |
@@ -157,7 +172,10 @@ does not update its rules.
HTML is a single offline report with an embedded local logo, summary counts,
assessment details, remediation guidance, fixed-version thresholds, source links,
-and run-completeness information. It supports dark/light themes and printing.
+and run-completeness information. Its footer identifies the tool version, report
+schema and executable SHA-256 even for empty reports. Observation `checked_at`,
+report `generated_at`, and rules verification date retain distinct meanings.
+It supports dark/light themes and printing.
Dynamic text is escaped; the document contains no scripts or remotely loaded
assets. The checked website URLs are evidence, not resources fetched by the
report.
@@ -176,8 +194,8 @@ files cannot be used as the output destination, including with `--force`.
The checker writes the report after the input loop or a handled keyboard
interruption. Abrupt termination, process crashes, or power loss before that
point can prevent report creation. It does not implement incremental checkpoints
-or automatic resume. CSV does not have a run-completeness envelope; retain the
-exit status/stderr or use JSON/HTML when that distinction matters.
+or automatic resume. CSV now carries run completeness on every result or empty
+summary row; retain the exit status/stderr as separate operational evidence.
## Requests made in website mode
diff --git a/node2shell_check.py b/node2shell_check.py
index 5f25678..e7b25bd 100644
--- a/node2shell_check.py
+++ b/node2shell_check.py
@@ -8,6 +8,7 @@
import argparse
import base64
import csv
+import hashlib
import io
import html
import json
@@ -28,6 +29,10 @@
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
TOOL_VERSION = "1.0.0"
+REPORT_SCHEMA_VERSION = "1.1"
+# Capture the distributed executable's byte identity at module load, before a
+# later report could accidentally identify a changed on-disk file instead.
+TOOL_SHA256 = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
BOT = "Node2ShellChecker"
REDIRECTS = {301, 302, 303, 307, 308}
MAX_BODY = 2 * 1024 * 1024
@@ -747,11 +752,19 @@ def logo_data_uri(path) -> str:
return f"data:{mime};base64,{base64.b64encode(data).decode('ascii')}"
-def render_html(rows, *, logo=DEFAULT_LOGO, brand="CompleteTech LLC", generated_at=None, complete=True) -> str:
+def report_metadata(complete, generated_at=None):
+ return {"tool": "node2shell-check", "tool_version": TOOL_VERSION,
+ "tool_sha256": TOOL_SHA256, "report_schema_version": REPORT_SCHEMA_VERSION,
+ "generated_at": generated_at or datetime.now(timezone.utc).isoformat(),
+ "complete": complete}
+
+
+def render_html(rows, *, logo=DEFAULT_LOGO, brand="CompleteTech LLC", generated_at=None, complete=True, metadata=None) -> str:
def esc(value):
return html.escape("" if value is None else str(value), quote=True)
- generated_at = generated_at or datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
+ metadata = metadata or report_metadata(complete, generated_at)
+ generated_at = metadata["generated_at"]
rows = sorted(rows, key=lambda row: STATUS_ORDER.get(row.get("node2shell_status"), 2))
counts = {key: sum(row.get("node2shell_status") == key for row in rows) for key in STATUS_ORDER}
tiles = [("", len(rows), "Inputs assessed"), ("bad", counts["potentially_affected"], "Potentially affected"),
@@ -771,7 +784,7 @@ def esc(value):
f"
{esc(row.get('source'))} {esc(row.get('status'))}
"
f"
{esc(row.get('http_status') if row.get('http_status') is not None else row.get('fetch_status', '—'))}
"
f"
{esc(row.get('node2shell_recommendation'))}
"
- f"Evidence and limitations
{detail_html}
"
+ f"Evidence and limitations
Observed: {esc(row.get('checked_at'))}
{detail_html}
"
)
fixes = "".join(f"
{major}.x
{'.'.join(map(str, version))}
" for major, version in NODE2SHELL_FIXES.items())
logo_uri = logo_data_uri(logo)
@@ -800,24 +813,29 @@ def esc(value):
then collect metadata again. An updated package lock or a patched workstation does not update a running server.
Review incoming Brotli request parsers, responses fetched from untrusted services, and direct Brotli decompression calls.
Vendor and custom builds require vendor evidence. The Brotli version string alone cannot establish patch status because fixes may be backported.