Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Exit 0: completed run; 1: potentially affected with `--fail-on-finding`; 2: argu

CSV `record_type=result` rows are asset observations. A zero-result report has a `record_type=summary` row carrying completeness, schema, rule date, CVE and `not_tested`; it is not an asset finding.

All formats preserve the same run identity: `tool`, `tool_version`, and ISO 8601 UTC `generated_at`. CSV repeats these fields on every result and empty summary; HTML shows the same time and checker identity. The CLI captures generation time once before assessment and retains it when interrupted. `rules_as_of` remains the advisory verification date; generation time is not evidence acquisition time or deployed-patch attestation.

CSV rejects NUL characters before writing a report. An interruption after a complete report is published preserves that report and exits 130.

If an interrupt leaves file publication indeterminate, the checker exits 130 without retrying. Inspect the output before retrying; the diagnostic does not establish whether publication succeeded.
2 changes: 1 addition & 1 deletion SHA256SUMS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
52988c0a8ac271882ce5d4b39bd4ffa666f803022c76e9f9bf5f0cc41bf54580 tinypool_options_rce_check.py
e67672e9af952f19cc6b0c9f57c6cb9d9a6c93cdd5444dec1b5d16d0cbbae00a tinypool_options_rce_check.py
2 changes: 2 additions & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Implemented offline package-version evidence checker, with independent source/se
Local verification: 13 tests, CLI help, documented synthetic inputs, checksum validation and whitespace checks. Hosted CI must pass for the exact final PR head before merge; a local pass is not a hosted result. The advisory metadata now describes the implemented rule and preserves its original disabled research state under research_origin.

No live traffic, application execution, exploit verification or deployed-target assessment is claimed. Reported fixed versions do not attest installed patches; unknown evidence remains unknown.

Report provenance regression coverage includes complete, empty, interrupted and copied-standalone exports. Generation time is captured once per CLI run and shared by JSON, CSV and HTML; rules_as_of remains separate.
2 changes: 2 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ Exit 0: completed run; 1: potentially affected with `--fail-on-finding`; 2: argu
[Synthetic report](docs/example-report.html), [reference](docs/REFERENCE.md), [implementation status](docs/IMPLEMENTATION.md), [AGENTS](AGENTS.md), [contribution terms](CONTRIBUTING.md), [security policy](SECURITY.md), [license](LICENSE), [dual licensing](LICENSING.md). Bounded input and reporting helpers are bundled from the reviewed Node.js checker at `74aa57ba5df05c34d17ae67912cd26ec2335201b`, with package-specific range comparison and the mixed-whitespace CSV fix. No companion repository is needed at runtime.

CSV `record_type=result` rows are asset observations. A zero-result report has a `record_type=summary` row carrying completeness, schema, rule date, CVE and `not_tested`; it is not an asset finding.

All formats preserve the same run identity: `tool`, `tool_version`, and ISO 8601 UTC `generated_at`. CSV repeats these fields on every result and empty summary; HTML shows the same time and checker identity. The CLI captures generation time once before assessment and retains it when interrupted. `rules_as_of` remains the advisory verification date; generation time is not evidence acquisition time or deployed-patch attestation.
6 changes: 3 additions & 3 deletions docs/example-report.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
@media print{body{background:#fff}tr,.tile{break-inside:avoid}details>summary{display:none}}
</style></head><body><main>
<header><div class="eyebrow">CompleteTech LLC</div><h1>CVE-2026-104849 Version Report</h1>
<p class="sub">Tinypool worker-options prototype-pollution gadget · generated 2026-10-24 14:45 UTC · rules as of 2026-10-24</p></header>
<p class="sub">Tinypool worker-options prototype-pollution gadget · generated 2026-10-25T12:34:56+00:00 · rules as of 2026-10-24</p></header>
<p class="callout"><b>Version evidence, not a proof of exploitability.</b> Every result has exploitability <code>not_tested</code>.
Unknown results need investigation. Precondition observations are operator-reported and do not change the version status.</p>
<div class="tiles"><div class='tile '><b>1</b><span>Inputs assessed</span></div><div class='tile bad'><b>0</b><span>Potentially affected</span></div><div class='tile '><b>0</b><span>Unknown</span></div><div class='tile ok'><b>1</b><span>Fixed version reported</span></div></div>
<div class="tiles"><div class='tile '><b>3</b><span>Inputs assessed</span></div><div class='tile bad'><b>1</b><span>Potentially affected</span></div><div class='tile '><b>1</b><span>Unknown</span></div><div class='tile ok'><b>1</b><span>Fixed version reported</span></div></div>
<h2>Results</h2><div class="wrap"><table><thead><tr><th>Input</th><th>Assessment</th><th>Reported version</th>
<th>Upstream fix</th><th>Precondition observations</th><th>Next step and evidence</th></tr></thead>
<tbody><tr><td class='site'>example-package-instance<br><small>inventory</small></td><td><span class='pill fixed_version_reported'>Fixed version reported</span><br><small><code>at_or_above_fixed_version</code></small></td><td><code>2.1.2</code><br><small>build: unknown</small></td><td><code>2.1.2</code></td><td><ul class='obs'><li title='caller supplies pool.run options object'><code>caller_supplies_pool_run_options_object</code>: not_reported</li><li title='options lacks an own filename property'><code>options_lacks_an_own_filename_property</code>: not_reported</li><li title='separate prototype-pollution primitive'><code>separate_prototype_pollution_primitive</code>: not_reported</li><li title='attacker-controlled worker module available'><code>attacker_controlled_worker_module_available</code>: not_reported</li></ul></td><td class='notes'><p>Verify this package instance is actually deployed and contains the upstream fix.</p><details><summary>Evidence and limitations</summary><ul><li>Reported version meets the fix within this reviewed release line; this is not installed-patch attestation.</li><li>pool.run(task) alone is not the documented trigger. Surface 2.1.1 source conflict and do not report clean. Maintainer severity High differs from database Critical; retain provenance.</li></ul></details></td></tr></tbody></table></div>
<tbody><tr><td class='site'>Synthetic Tinypool 2.1.0<br><small>version</small></td><td><span class='pill potentially_affected'>Potentially affected</span><br><small><code>in_documented_affected_range</code></small></td><td><code>2.1.0</code><br><small>build: unknown</small></td><td><code>2.1.2</code></td><td><ul class='obs'><li title='caller supplies pool.run options object'><code>caller_supplies_pool_run_options_object</code>: not_reported</li><li title='options lacks an own filename property'><code>options_lacks_an_own_filename_property</code>: not_reported</li><li title='separate prototype-pollution primitive'><code>separate_prototype_pollution_primitive</code>: not_reported</li><li title='attacker-controlled worker module available'><code>attacker_controlled_worker_module_available</code>: not_reported</li></ul></td><td class='notes'><p>Verify the deployed tinypool instance and upgrade to 2.1.2 or a reviewed fixed release; review advisory conditions without executing untrusted code.</p><details><summary>Evidence and limitations</summary><ul><li>Reported version matches the documented affected range; application preconditions and exploitability were not established.</li><li>pool.run(task) alone is not the documented trigger. Surface 2.1.1 source conflict and do not report clean. Maintainer severity High differs from database Critical; retain provenance.</li></ul></details></td></tr><tr><td class='site'>Synthetic Tinypool 2.1.1<br><small>version</small></td><td><span class='pill unknown'>Unknown</span><br><small><code>source_discrepancy</code></small></td><td><code>2.1.1</code><br><small>build: unknown</small></td><td><code>—</code></td><td><ul class='obs'><li title='caller supplies pool.run options object'><code>caller_supplies_pool_run_options_object</code>: not_reported</li><li title='options lacks an own filename property'><code>options_lacks_an_own_filename_property</code>: not_reported</li><li title='separate prototype-pollution primitive'><code>separate_prototype_pollution_primitive</code>: not_reported</li><li title='attacker-controlled worker module available'><code>attacker_controlled_worker_module_available</code>: not_reported</li></ul></td><td class='notes'><p>Collect or verify the exact upstream tinypool version and build provenance of the running process, then run this checker again.</p><details><summary>Evidence and limitations</summary><ul><li>Primary advisory and reviewed database disagree at this boundary; obtain maintainer evidence.</li><li>pool.run(task) alone is not the documented trigger. Surface 2.1.1 source conflict and do not report clean. Maintainer severity High differs from database Critical; retain provenance.</li></ul></details></td></tr><tr><td class='site'>Synthetic Tinypool 2.1.2<br><small>version</small></td><td><span class='pill fixed_version_reported'>Fixed version reported</span><br><small><code>at_or_above_fixed_version</code></small></td><td><code>2.1.2</code><br><small>build: unknown</small></td><td><code>2.1.2</code></td><td><ul class='obs'><li title='caller supplies pool.run options object'><code>caller_supplies_pool_run_options_object</code>: not_reported</li><li title='options lacks an own filename property'><code>options_lacks_an_own_filename_property</code>: not_reported</li><li title='separate prototype-pollution primitive'><code>separate_prototype_pollution_primitive</code>: not_reported</li><li title='attacker-controlled worker module available'><code>attacker_controlled_worker_module_available</code>: not_reported</li></ul></td><td class='notes'><p>Verify this package instance is actually deployed and contains the upstream fix.</p><details><summary>Evidence and limitations</summary><ul><li>Reported version meets the fix within this reviewed release line; this is not installed-patch attestation.</li><li>pool.run(task) alone is not the documented trigger. Surface 2.1.1 source conflict and do not report clean. Maintainer severity High differs from database Critical; retain provenance.</li></ul></details></td></tr></tbody></table></div>
<h2>Upstream fixed releases</h2><div class="wrap"><table class="narrow"><thead><tr><th>Branch</th><th>Fixed in</th></tr></thead><tbody><tr><td>2.1.x</td><td><code>2.1.2</code></td></tr></tbody></table></div>
<h2>Verify the deployed application</h2>
<p>Check the actual tinypool binary in each serving process or container. Upgrade the runtime, redeploy, then collect the version again.
Expand Down
96 changes: 96 additions & 0 deletions test_report_provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Offline regressions for detached exports and interrupted run identity."""
import contextlib
import csv
from datetime import datetime, timezone
import io
import json
import socket
import unittest
from unittest import mock

import tinypool_options_rce_check as checker


STAMP = datetime(2026, 10, 25, 12, 34, 56, tzinfo=timezone.utc)


class ProvenanceTests(unittest.TestCase):
def verify_export(self, text, fmt, *, count, complete):
if fmt == 'csv':
rows = list(csv.DictReader(io.StringIO(text, newline='')))
self.assertEqual(max(1, count), len(rows))
for row in rows:
self.assertEqual(checker.TOOL_NAME, row['tool'])
self.assertEqual(checker.TOOL_VERSION, row['tool_version'])
self.assertEqual(STAMP.isoformat(), row['generated_at'])
self.assertEqual(checker.RULES_DATE, row['rules_as_of'])
self.assertEqual(str(complete).lower(), row['report_complete'])
self.assertEqual('not_tested', row['exploitability'])
self.assertEqual('result' if count else 'summary', row['record_type'])
return [r['status'] for r in rows] if count else []
if fmt == 'json':
report = json.loads(text)
self.assertEqual(checker.TOOL_NAME, report['tool'])
self.assertEqual(checker.TOOL_VERSION, report['tool_version'])
self.assertEqual(STAMP.isoformat(), report['generated_at'])
self.assertEqual(checker.RULES_DATE, report['rules_as_of'])
self.assertEqual(complete, report['complete'])
self.assertEqual(count, len(report['results']))
return [r['status'] for r in report['results']]
self.assertIn(STAMP.isoformat(), text)
self.assertIn(checker.TOOL_NAME + ' ' + checker.TOOL_VERSION, text)
self.assertIn('rules as of ' + checker.RULES_DATE, text)
if not complete:
self.assertIn('Interrupted run.', text)

def test_complete_and_empty_exports_share_captured_metadata(self):
results = [checker.assess_entry('synthetic', 'version', v) for v in ('2.1.0', '2.1.1', '2.1.2')]
for rows in (results, []):
for fmt in ('csv', 'json', 'html'):
with self.subTest(count=len(rows), fmt=fmt), mock.patch.object(checker, 'datetime') as clock:
clock.now.return_value = STAMP
out = io.StringIO()
checker.write_report(out, rows, format=fmt)
clock.now.assert_called_once_with(timezone.utc)
statuses = self.verify_export(out.getvalue(), fmt, count=len(rows), complete=True)
if statuses:
self.assertEqual(['potentially_affected', 'unknown', 'fixed_version_reported'], statuses)

def test_interrupted_cli_preserves_start_time_and_completed_rows(self):
real = checker.assess_entry
for completed in (0, 1):
for fmt in ('csv', 'json', 'html'):
calls = 0
def assess(*args, **kwargs):
nonlocal calls
calls += 1
if calls > completed:
raise KeyboardInterrupt
return real(*args, **kwargs)
out = io.StringIO()
with self.subTest(completed=completed, fmt=fmt), mock.patch.object(checker, 'datetime') as clock, mock.patch.object(checker, 'assess_entry', side_effect=assess), mock.patch.object(socket, 'socket', side_effect=AssertionError('offline only')), contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
clock.now.return_value = STAMP
self.assertEqual(130, checker.main(['--version', '2.1.1', '--version', '2.1.2', '--format', fmt]))
clock.now.assert_called_once_with(timezone.utc)
statuses = self.verify_export(out.getvalue(), fmt, count=completed, complete=False)
if statuses:
self.assertEqual(['unknown'], statuses)

def test_injected_timestamp_is_shared_without_reading_clock(self):
for fmt in ('csv', 'json', 'html'):
with mock.patch.object(checker, 'datetime') as clock:
out = io.StringIO()
checker.write_report(out, [], format=fmt, generated_at=STAMP.isoformat(), complete=False)
clock.now.assert_not_called()
self.verify_export(out.getvalue(), fmt, count=0, complete=False)

def test_csv_row_cannot_override_run_identity(self):
row = checker.assess_entry('synthetic', 'version', '2.1.1')
row.update(tool='other', tool_version='other', generated_at='other')
out = io.StringIO()
checker.write_report(out, [row], generated_at=STAMP.isoformat())
self.verify_export(out.getvalue(), 'csv', count=1, complete=True)


if __name__ == '__main__':
unittest.main()
Loading
Loading