diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..2f66d4c
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,33 @@
+name: Tests
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Python ${{ matrix.python }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ python: ['3.10', '3.14']
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
+ with:
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Run local-only tests
+ run: python -m unittest -v
+ - name: Check CLI help
+ run: python tinypool_options_rce_check.py --help
diff --git a/AGENTS.md b/AGENTS.md
index 1a10244..e6babf7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,19 +8,18 @@ before changing this project.
This repository covers only **CVE-2026-104849** (Tinypool worker-options prototype-pollution gadget). Its aliases
refer to the same exploit. Keep unrelated vulnerabilities in separate repositories.
-The repository is an initialized research foundation. No checker, tests, or CI
-workflow exists yet; never imply otherwise or document nonexistent commands.
+The repository contains the offline checker `tinypool_options_rce_check.py`, synthetic tests and CI. Preserve the exact affected interval, disputed boundaries and reviewed fixed line; no network mode or application execution is implemented.
## Project map
-- `advisory.json`: source-preserving research record; detection is disabled.
+- `advisory.json`: enabled offline rule metadata with preserved research origin.
- `README.md`: exact scope, advisory snapshot, primary sources, and current status.
- `docs/IMPLEMENTATION.md`: local implementation and verification checklist.
- `LICENSING.md`, `CONTRIBUTING.md`, `BRAND_ASSETS.md`: licensing and contribution terms.
## Implementation constraints
-Follow the established [node2shell-check](https://github.com/CompleteTech-LLC/node2shell-check) and [php2shell-check](https://github.com/CompleteTech-LLC/php2shell-check) conventions. Prefer a small, auditable,
+Follow the established [node2shell-check](https://github.com/completetech-security/node2shell-check) and [php2shell-check](https://github.com/completetech-security/php2shell-check) conventions. Prefer a small, auditable,
dependency-free checker when appropriate. Do not turn this project into a generic
product scanner or execute exploit payloads.
@@ -42,8 +41,8 @@ disabled until a real implementation is reviewed and tested.
## Validation and delivery
-Use focused synthetic cases and local-only fixtures for meaningful behavior tests
-once code exists. Do not invent passing tests, add prose-only tests, or publish
+Use focused synthetic cases and local-only fixtures for meaningful behavior tests.
+Do not invent passing tests, add prose-only tests, or publish
private assessments. Keep CLI, schemas, methodology, and README aligned. Distinguish
local results from hosted CI on the exact published commit. Preserve unrelated
work and follow the licensing and brand policies linked above.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 013b82f..8b0355b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,22 +1,6 @@
# Contributing
-This repository is initialized for **CVE-2026-104849**. Checker implementation is
-pending; start with the [implementation checklist](docs/IMPLEMENTATION.md).
-Submit focused pull requests for this exploit. Report flaws requiring private
-disclosure according to [SECURITY.md](SECURITY.md).
-
-## Development expectations
-
-Follow [AGENTS.md](AGENTS.md). Prefer a standalone Python 3.10+ checker with no
-third-party runtime/test dependencies when suitable. Validate primary sources
-before encoding affected or fixed ranges. Preserve evidence provenance and
-`unknown` for missing or conflicting facts. Keep exploitability `not_tested`.
-
-Use offline inventories, synthetic examples, mocks, and local fixtures. Do not
-commit credentials, real target lists, private reports, or exploit payloads.
-Network detection must be justified, optional, ordinary, bounded, and authorized.
-Document implemented behavior and actual commands; no checker commands or test
-suite are available in this foundation yet.
+Read AGENTS.md and README.md. Run `python -m unittest -v`, `python tinypool_options_rce_check.py --help` and `git diff --check`. Use synthetic evidence only, verify primary sources before changing ranges, keep source conflicts unknown, and never run application code or send exploit payloads. Preserve safe publication, bounded reads, output escaping and complete=false after interruption. Review and exact-head hosted CI precede merge.
## Licensing of contributions
diff --git a/README.md b/README.md
index e919fb3..cb96601 100644
--- a/README.md
+++ b/README.md
@@ -1,140 +1,57 @@
-
-
-
+# tinypool-options-rce-check
-
-
-
-
-
+Offline evidence checker for **CVE-2026-104849 — Tinypool worker-options prototype-pollution gadget**. Python 3.10+, standard library only. The executable can be copied alone. It never contacts websites, imports application code, runs a package manager, or executes exploit payloads.
-
- Tinypool worker-options prototype-pollution gadget
- CVE-2026-104849 · GHSA-85c8-ppgw-ccpr · research only, no checker yet
-
+```sh
+python tinypool_options_rce_check.py --version 2.1.2 --format json
+python tinypool_options_rce_check.py --inventory examples/inventory.example.json --format html --output report.html
+python tinypool_options_rce_check.py --csv-list inventory.csv --fail-on-finding
+python tinypool_options_rce_check.py --help
+python -m unittest -v
+```
----
+## Reviewed rules
-> **Repository initialized; checker implementation pending.**
-> This project currently contains research and an implementation checklist.
-> It does not assess systems or provide an executable detection rule.
+Primary advisory and reviewed database retrieved 2026-10-24. A public URL cannot provide installed package inventory. Obtain each deployed package instance's exact version and provenance; declarations and local workstation versions are not deployment evidence.
-`tinypool-options-rce-check` is the planned home of a checker for **CVE-2026-104849**, a Tinypool worker-options prototype-pollution gadget. Today it holds source-backed research and a work plan only. It sends no requests, runs no exploit payloads, and reports no results. When built, it is meant to report uncertainty honestly: missing, conflicting, or vendor-qualified evidence stays `unknown`, never "safe".
-
-By [CompleteTech LLC](https://complete.tech).
-
-## Status and what is planned
-
-There are no checker commands, installation steps, downloadable binaries, test results, or CI claims, so there is no Quick start yet. To follow or contribute to the work:
-
-1. Review [advisory.json](advisory.json) and the [primary sources](#primary-sources).
-2. Work through the exploit-specific [implementation checklist](docs/IMPLEMENTATION.md).
-3. Read [AGENTS.md](AGENTS.md), [CONTRIBUTING.md](CONTRIBUTING.md), and [SECURITY.md](SECURITY.md) before contributing.
-
-## Planned report layout
-
-
-
-
-
-
-
-
-
Illustrative only. A layout sketch with placeholder values on reserved example.* domains. No checker, report format, or result exists yet.
Tinypool worker-options prototype-pollution gadget · generated 2026-10-24 14:45 UTC · rules as of 2026-10-24
+
Version evidence, not a proof of exploitability. Every result has exploitability not_tested.
+Unknown results need investigation. Precondition observations are operator-reported and do not change the version status.
Verify this package instance is actually deployed and contains the upstream fix.
Evidence and limitations
Reported version meets the fix within this reviewed release line; this is not installed-patch attestation.
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.
+
Upstream fixed releases
Branch
Fixed in
2.1.x
2.1.2
+
Verify the deployed application
+
Check the actual tinypool binary in each serving process or container. Upgrade the runtime, redeploy, then collect the version again.
+Vendor and custom builds require vendor evidence because fixes may be backported.
+
+
diff --git a/examples/inventory.example.json b/examples/inventory.example.json
new file mode 100644
index 0000000..33044a2
--- /dev/null
+++ b/examples/inventory.example.json
@@ -0,0 +1,11 @@
+{
+ "schema_version": 1,
+ "package": "tinypool",
+ "package_version": "2.1.2",
+ "label": "example-package-instance",
+ "build_source": "unknown",
+ "caller_supplies_pool_run_options_object": null,
+ "options_lacks_an_own_filename_property": null,
+ "separate_prototype_pollution_primitive": null,
+ "attacker_controlled_worker_module_available": null
+}
diff --git a/test_assessment.py b/test_assessment.py
new file mode 100644
index 0000000..0ffdd9a
--- /dev/null
+++ b/test_assessment.py
@@ -0,0 +1,99 @@
+"""Independent advisory golden cases and hostile-input/publication behavior."""
+import contextlib
+import csv
+import io
+import json
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest import mock
+import tinypool_options_rce_check as c
+CASES = [('2.0.9', 'potentially_affected'), ('2.1.0', 'potentially_affected'), ('2.1.1', 'unknown'), ('2.1.2', 'fixed_version_reported'), ('2.1.3', 'fixed_version_reported'), ('2.2.0', 'unknown'), ('3.0.0', 'unknown')]
+FIXED = '2.1.2'
+
+class AssessmentTests(unittest.TestCase):
+ def run_cli(self,*args):
+ with contextlib.redirect_stdout(io.StringIO()) as out,contextlib.redirect_stderr(io.StringIO()) as err:
+ try: code=c.main(list(args))
+ except SystemExit as exc: code=exc.code
+ return code,out.getvalue(),err.getvalue()
+ def test_primary_source_boundary_cases(self):
+ for version,status in CASES:
+ with self.subTest(version=version): self.assertEqual(status,c.assess_version(version)['status'])
+ def test_version_conflicts_qualifiers_and_custom_provenance(self):
+ for version in ['', 'latest', '1', '1.2', '01.2.3', '1.2.3', FIXED+'-rc.1', FIXED+'+vendor', '\u00a0'+FIXED, True, None]:
+ self.assertEqual('unknown',c.assess_version(version)['status'])
+ self.assertEqual('unknown',c.assess_version(FIXED,version_candidates=['0.0.1'])['status'])
+ for version,_ in CASES:
+ self.assertEqual('unknown',c.assess_version(version,build_source='custom')['status'])
+ def test_observations_never_change_patch_or_exploitability(self):
+ for flag in [True,False,None]:
+ r=c.assess_entry('example','inventory',FIXED,observations={k:flag for k in c.PRECONDITION_KEYS})
+ self.assertEqual('fixed_version_reported',r['status']);self.assertEqual('not_tested',r['exploitability'])
+ def test_inventory_schema_types_and_duplicate_keys(self):
+ good={'schema_version':1,'package_version':FIXED,'package':'tinypool'}
+ with tempfile.TemporaryDirectory() as d:
+ p=Path(d)/'input.json'
+ for data in [{},[],{**good,'extra':1},{**good,'schema_version':True},{**good,c.PRECONDITION_KEYS[0]:'true'},{**good,'package':'different-package'},{**good,'package_version':4}]:
+ p.write_text(json.dumps(data));self.assertEqual(2,self.run_cli('--inventory',str(p))[0])
+ p.write_text('{"sensitive-key":1,"sensitive-key":2}')
+ code,out,err=self.run_cli('--inventory',str(p));self.assertEqual((2,''),(code,out));self.assertNotIn('sensitive-key',err)
+ p.write_text(json.dumps(good));code,out,_=self.run_cli('--inventory',str(p),'--format','json')
+ self.assertEqual(0,code);self.assertEqual('fixed_version_reported',json.loads(out)['results'][0]['status'])
+ def test_input_budgets_and_special_files(self):
+ self.assertEqual(2,self.run_cli('--version','v'*513)[0])
+ with tempfile.TemporaryDirectory() as d:
+ p=Path(d)/'large.json';p.write_bytes(b' '*(c.MAX_INPUT_BYTES+1));self.assertEqual(2,self.run_cli('--inventory',str(p))[0])
+ if hasattr(os,'mkfifo'):
+ q=Path(d)/'pipe';os.mkfifo(q)
+ with mock.patch.object(Path,'open',side_effect=AssertionError('must not open pipe')):
+ self.assertEqual(2,self.run_cli('--inventory',str(q))[0])
+ with mock.patch.object(c,'MAX_ENTRIES',2):
+ self.assertEqual(2,self.run_cli('--version',FIXED,'--version',FIXED,'--version',FIXED)[0])
+ def test_csv_roundtrips_controls_and_neutralizes_mixed_whitespace(self):
+ labels=['alpha\r=1+1',' \t=1+1',' \r=1+1','\v=1+1','\f@x','\u00a0=1+1','=1+1']
+ out=io.StringIO();c.write_report(out,[c.assess_entry(x,'inventory',FIXED) for x in labels],format='csv')
+ rows=list(csv.DictReader(io.StringIO(out.getvalue(),newline='')))
+ self.assertEqual(len(labels),len(rows));self.assertEqual(labels[0],rows[0]['input_label'])
+ for original,row in zip(labels[1:],rows[1:]): self.assertEqual("'"+original,row['input_label'])
+ def test_html_is_escaped_and_script_free(self):
+ text=c.render_html([c.assess_entry('','inventory',FIXED)])
+ self.assertNotIn('