paytrace 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paytrace-2.0.0/.github/ISSUE_TEMPLATE/new_collector.md +18 -0
- paytrace-2.0.0/.gitignore +150 -0
- paytrace-2.0.0/AUDIT_RESPONSE.md +167 -0
- paytrace-2.0.0/AUDIT_RESPONSE_2.md +130 -0
- paytrace-2.0.0/AUDIT_RESPONSE_3.md +138 -0
- paytrace-2.0.0/AUDIT_RESPONSE_4.md +177 -0
- paytrace-2.0.0/AUDIT_RESPONSE_5.md +101 -0
- paytrace-2.0.0/AUDIT_RESPONSE_6.md +85 -0
- paytrace-2.0.0/AUDIT_RESPONSE_7.md +114 -0
- paytrace-2.0.0/CHANGELOG.md +355 -0
- paytrace-2.0.0/CITATION.cff +28 -0
- paytrace-2.0.0/CLOSEOUT.md +131 -0
- paytrace-2.0.0/CODE_OF_CONDUCT.md +17 -0
- paytrace-2.0.0/CONTRIBUTING.md +43 -0
- paytrace-2.0.0/DEPLOYMENT.md +281 -0
- paytrace-2.0.0/DESIGN.md +146 -0
- paytrace-2.0.0/LICENSE +146 -0
- paytrace-2.0.0/METHODOLOGY_AUDIT.md +319 -0
- paytrace-2.0.0/NOTICE +14 -0
- paytrace-2.0.0/PKG-INFO +780 -0
- paytrace-2.0.0/PUBLISHING.md +478 -0
- paytrace-2.0.0/README.md +743 -0
- paytrace-2.0.0/SECURITY.md +79 -0
- paytrace-2.0.0/SELF_AUDIT.md +113 -0
- paytrace-2.0.0/SELF_AUDIT_2.md +84 -0
- paytrace-2.0.0/VERIFYING.md +40 -0
- paytrace-2.0.0/adversarial/README.md +179 -0
- paytrace-2.0.0/adversarial/TRACE_clean.md +51 -0
- paytrace-2.0.0/adversarial/TRACE_defended.md +52 -0
- paytrace-2.0.0/adversarial/TRACE_diff.md +83 -0
- paytrace-2.0.0/adversarial/TRACE_naive.md +44 -0
- paytrace-2.0.0/demo/run_demo.py +243 -0
- paytrace-2.0.0/examples/end_to_end_domain.py +362 -0
- paytrace-2.0.0/examples/reference_collector.py +228 -0
- paytrace-2.0.0/pyproject.toml +75 -0
- paytrace-2.0.0/src/paytrace/__init__.py +98 -0
- paytrace-2.0.0/src/paytrace/adstxt.py +468 -0
- paytrace-2.0.0/src/paytrace/adversarial.py +334 -0
- paytrace-2.0.0/src/paytrace/agent/__init__.py +20 -0
- paytrace-2.0.0/src/paytrace/agent/agent.py +432 -0
- paytrace-2.0.0/src/paytrace/agent/fixtures.py +210 -0
- paytrace-2.0.0/src/paytrace/agent/guards.py +269 -0
- paytrace-2.0.0/src/paytrace/agent/tools.py +435 -0
- paytrace-2.0.0/src/paytrace/catalog.py +174 -0
- paytrace-2.0.0/src/paytrace/cli.py +284 -0
- paytrace-2.0.0/src/paytrace/collectors/__init__.py +26 -0
- paytrace-2.0.0/src/paytrace/collectors/analytics.py +288 -0
- paytrace-2.0.0/src/paytrace/collectors/artifacts.py +439 -0
- paytrace-2.0.0/src/paytrace/collectors/base.py +60 -0
- paytrace-2.0.0/src/paytrace/collectors/business.py +558 -0
- paytrace-2.0.0/src/paytrace/collectors/disclosure.py +347 -0
- paytrace-2.0.0/src/paytrace/collectors/infra.py +246 -0
- paytrace-2.0.0/src/paytrace/collectors/lookups.py +454 -0
- paytrace-2.0.0/src/paytrace/collectors/persona.py +411 -0
- paytrace-2.0.0/src/paytrace/collectors/records.py +200 -0
- paytrace-2.0.0/src/paytrace/collectors/registries.py +405 -0
- paytrace-2.0.0/src/paytrace/collectors/surface.py +385 -0
- paytrace-2.0.0/src/paytrace/data/registries.yaml +484 -0
- paytrace-2.0.0/src/paytrace/egress.py +491 -0
- paytrace-2.0.0/src/paytrace/enrich.py +400 -0
- paytrace-2.0.0/src/paytrace/extract.py +374 -0
- paytrace-2.0.0/src/paytrace/fingerprint.py +333 -0
- paytrace-2.0.0/src/paytrace/index.py +460 -0
- paytrace-2.0.0/src/paytrace/ingest.py +341 -0
- paytrace-2.0.0/src/paytrace/net.py +580 -0
- paytrace-2.0.0/src/paytrace/netsec.py +241 -0
- paytrace-2.0.0/src/paytrace/pinned.py +135 -0
- paytrace-2.0.0/src/paytrace/pivot.py +231 -0
- paytrace-2.0.0/src/paytrace/portfolio.py +331 -0
- paytrace-2.0.0/src/paytrace/robin_ingest.py +337 -0
- paytrace-2.0.0/src/paytrace/sellersjson.py +259 -0
- paytrace-2.0.0/tests/test_adstxt.py +200 -0
- paytrace-2.0.0/tests/test_adversarial.py +77 -0
- paytrace-2.0.0/tests/test_adversarial_example.py +200 -0
- paytrace-2.0.0/tests/test_agent.py +199 -0
- paytrace-2.0.0/tests/test_artifacts.py +202 -0
- paytrace-2.0.0/tests/test_catalog.py +118 -0
- paytrace-2.0.0/tests/test_collectors.py +52 -0
- paytrace-2.0.0/tests/test_disclosure.py +213 -0
- paytrace-2.0.0/tests/test_egress.py +232 -0
- paytrace-2.0.0/tests/test_enrich.py +232 -0
- paytrace-2.0.0/tests/test_extract.py +245 -0
- paytrace-2.0.0/tests/test_fingerprint.py +189 -0
- paytrace-2.0.0/tests/test_index.py +127 -0
- paytrace-2.0.0/tests/test_ingest.py +125 -0
- paytrace-2.0.0/tests/test_lookups.py +194 -0
- paytrace-2.0.0/tests/test_obfuscation_collectors.py +171 -0
- paytrace-2.0.0/tests/test_persona_gate.py +84 -0
- paytrace-2.0.0/tests/test_records.py +81 -0
- paytrace-2.0.0/tests/test_robin_ingest.py +115 -0
- paytrace-2.0.0/tests/test_security.py +170 -0
- paytrace-2.0.0/tests/test_sellersjson.py +167 -0
- paytrace-2.0.0/tests/test_surface.py +219 -0
- paytrace-2.0.0/tests/test_transport.py +524 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: New collector proposal
|
|
3
|
+
about: Propose a new data source
|
|
4
|
+
labels: collector
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Source** (URL and what it publishes)
|
|
8
|
+
|
|
9
|
+
**Legal basis** — statutory registry / IAB spec / RFC protocol / mandated disclosure
|
|
10
|
+
|
|
11
|
+
**Auth** — keyless, free key, or paid (paid sources need a strong argument)
|
|
12
|
+
|
|
13
|
+
**Rate limit and robots.txt status**
|
|
14
|
+
|
|
15
|
+
**Claims emitted** — predicate, reliability, and what constitutes one
|
|
16
|
+
`correlation_group`
|
|
17
|
+
|
|
18
|
+
**Selectivity** — roughly how many entities share a typical value?
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# INVESTIGATION ARTIFACTS — never commit
|
|
3
|
+
#
|
|
4
|
+
# These are the real leak risk in this project. Case files carry the
|
|
5
|
+
# authorization reference (often an internal ticket number). Evidence
|
|
6
|
+
# packages carry bytes captured from a live target. Audit logs and graphs
|
|
7
|
+
# carry the identifiers of whoever was investigated. A corpus index carries
|
|
8
|
+
# a crawl of real domains.
|
|
9
|
+
#
|
|
10
|
+
# None of it is method, all of it is operational, and once it is in git
|
|
11
|
+
# history it is in every clone and fork forever.
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
# Case files (authorization refs, seeds, contact emails)
|
|
15
|
+
case.yaml
|
|
16
|
+
case-*.yaml
|
|
17
|
+
case_*.yaml
|
|
18
|
+
*.case.yaml
|
|
19
|
+
!case.example.yaml
|
|
20
|
+
!case.demo.yaml
|
|
21
|
+
|
|
22
|
+
# Run output
|
|
23
|
+
out/
|
|
24
|
+
out-*/
|
|
25
|
+
output/
|
|
26
|
+
results/
|
|
27
|
+
runs/
|
|
28
|
+
|
|
29
|
+
# Evidence packages: preserved response bodies, manifests, declarations
|
|
30
|
+
evidence/
|
|
31
|
+
captures/
|
|
32
|
+
evidence_manifest.json
|
|
33
|
+
DECLARATION_DRAFT.md
|
|
34
|
+
TIMESTAMP.md
|
|
35
|
+
verify.py
|
|
36
|
+
!src/**/verify.py
|
|
37
|
+
*.tsq
|
|
38
|
+
*.tsr
|
|
39
|
+
|
|
40
|
+
# Graphs and reports
|
|
41
|
+
investigation_graph.json
|
|
42
|
+
investigation_graph.minimized.json
|
|
43
|
+
entities.ftm.json
|
|
44
|
+
graph.cypher
|
|
45
|
+
attribution_report.md
|
|
46
|
+
attribution_report.html
|
|
47
|
+
verification_trail.md
|
|
48
|
+
verification_trail.json
|
|
49
|
+
portfolio.json
|
|
50
|
+
handle_graph.json
|
|
51
|
+
assessments.json
|
|
52
|
+
|
|
53
|
+
# Audit logs
|
|
54
|
+
audit.jsonl
|
|
55
|
+
audit-*.jsonl
|
|
56
|
+
*.audit.jsonl
|
|
57
|
+
|
|
58
|
+
# Corpus indexes and crawl data
|
|
59
|
+
*.sqlite
|
|
60
|
+
*.sqlite3
|
|
61
|
+
*.db
|
|
62
|
+
!tests/**/*.sqlite
|
|
63
|
+
paytrace.sqlite
|
|
64
|
+
domains.txt
|
|
65
|
+
tranco*.csv
|
|
66
|
+
tranco*.txt
|
|
67
|
+
|
|
68
|
+
# Operational blocklists — data, not method
|
|
69
|
+
infra_blacklist*.txt
|
|
70
|
+
*blacklist*.txt
|
|
71
|
+
!tests/**/*blacklist*.txt
|
|
72
|
+
|
|
73
|
+
# Imported third-party investigation exports
|
|
74
|
+
investigations/
|
|
75
|
+
spiderfoot*.db
|
|
76
|
+
spiderfoot*.csv
|
|
77
|
+
*.stix.json
|
|
78
|
+
opencti*.json
|
|
79
|
+
robin*.json
|
|
80
|
+
|
|
81
|
+
# Handle observations (real handles from real cases)
|
|
82
|
+
handles*.csv
|
|
83
|
+
observations*.csv
|
|
84
|
+
!examples/demo.csv
|
|
85
|
+
|
|
86
|
+
# Calibration corpora (labelled pairs from closed cases)
|
|
87
|
+
labelled_pairs.json
|
|
88
|
+
pairs.json
|
|
89
|
+
train.json
|
|
90
|
+
test.json
|
|
91
|
+
calibration_report.md
|
|
92
|
+
corpus/
|
|
93
|
+
|
|
94
|
+
# HTTP response cache
|
|
95
|
+
.eae-cache/
|
|
96
|
+
.cache/
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
99
|
+
# Secrets
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
101
|
+
.env
|
|
102
|
+
.env.*
|
|
103
|
+
!.env.example
|
|
104
|
+
*.pem
|
|
105
|
+
*.key
|
|
106
|
+
!**/tests/**/*.key
|
|
107
|
+
.pypirc
|
|
108
|
+
credentials.json
|
|
109
|
+
secrets.yaml
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# Python
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
114
|
+
__pycache__/
|
|
115
|
+
*.py[cod]
|
|
116
|
+
*$py.class
|
|
117
|
+
*.so
|
|
118
|
+
.Python
|
|
119
|
+
build/
|
|
120
|
+
dist/
|
|
121
|
+
sdist/
|
|
122
|
+
wheels/
|
|
123
|
+
*.egg-info/
|
|
124
|
+
*.egg
|
|
125
|
+
MANIFEST
|
|
126
|
+
.venv/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
.tox/
|
|
130
|
+
.nox/
|
|
131
|
+
.pytest_cache/
|
|
132
|
+
.ruff_cache/
|
|
133
|
+
.mypy_cache/
|
|
134
|
+
.coverage
|
|
135
|
+
.coverage.*
|
|
136
|
+
htmlcov/
|
|
137
|
+
coverage.xml
|
|
138
|
+
.hypothesis/
|
|
139
|
+
|
|
140
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
141
|
+
# Editors and OS
|
|
142
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
143
|
+
.idea/
|
|
144
|
+
.vscode/
|
|
145
|
+
*.swp
|
|
146
|
+
*.swo
|
|
147
|
+
*~
|
|
148
|
+
.DS_Store
|
|
149
|
+
Thumbs.db
|
|
150
|
+
.verify-venv/
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Response to external audit
|
|
2
|
+
|
|
3
|
+
Twenty-five findings. I agree with twenty-three, partially disagree with one,
|
|
4
|
+
and disagree with one. This records what changed, what did not, and why.
|
|
5
|
+
|
|
6
|
+
**The audit's central point is correct and I have acted on it:** the system is
|
|
7
|
+
an evidence-ranking engine and the API described it as a probabilistic
|
|
8
|
+
attribution engine. My own methodology audit said "an ordering, not a measured
|
|
9
|
+
frequency" and then shipped a field called `probability` and a band called
|
|
10
|
+
`ATTRIBUTED`. That gap between documentation and surface is exactly the failure
|
|
11
|
+
the auditor identified.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Fixed — the four that were dangerous
|
|
16
|
+
|
|
17
|
+
**1. CI failed open.** `|| true` on integration tests, examples and `pip-audit`.
|
|
18
|
+
Documentation claimed the integration suite gated releases; GitHub did not.
|
|
19
|
+
All three now fail the build. `pip-audit` is renamed "Dependency CVEs (fails the
|
|
20
|
+
build)" because a scan that cannot fail is theatre.
|
|
21
|
+
|
|
22
|
+
**2. `max_bytes` did not prevent OOM.** `r.content[:max_bytes]` materialises the
|
|
23
|
+
whole body first, so a 5 GB response exhausted memory before the slice applied.
|
|
24
|
+
The docstring claimed a protection the code did not provide, which is the worst
|
|
25
|
+
class of bug. Now streams with `aiter_bytes()` and stops reading at the cap,
|
|
26
|
+
after transfer decoding so decompression bombs are bounded by the same limit.
|
|
27
|
+
|
|
28
|
+
**3. Egress did not control transport.** `EgressPool` sat beside a single
|
|
29
|
+
`httpx.AsyncClient` that never used it; `self._clients` was dead. The manifest
|
|
30
|
+
would have recorded a vantage point the request never used — fabricated
|
|
31
|
+
provenance, worse than none. `Fetcher.client_for(label)` now builds one proxied
|
|
32
|
+
client per egress, and a misconfigured egress raises rather than silently
|
|
33
|
+
falling back to direct.
|
|
34
|
+
|
|
35
|
+
**4. Cache key omitted the egress.** A US capture would have served a request
|
|
36
|
+
that asked for a DE vantage point. Key now includes egress label and country.
|
|
37
|
+
|
|
38
|
+
## Fixed — semantics
|
|
39
|
+
|
|
40
|
+
**5. Stopped calling the output a probability.** `log(1/selectivity)` informs
|
|
41
|
+
the *denominator* of a likelihood ratio; `P(evidence | same entity)` is not
|
|
42
|
+
modelled and varies enormously between a shared GA4 ID and a shared company
|
|
43
|
+
address. Multiplying by a subjective reliability coefficient does not restore
|
|
44
|
+
the missing term. Output now carries `calibration_status: "unvalidated"` and a
|
|
45
|
+
`probability_note` stating plainly what the number is.
|
|
46
|
+
|
|
47
|
+
**23. Renamed the bands.** `ATTRIBUTED` / `PROBABLE` / `POSSIBLE` became
|
|
48
|
+
`STRONG_EVIDENCE` / `MODERATE_EVIDENCE` / `LIMITED_EVIDENCE`, and ICD 203
|
|
49
|
+
estimative language ("almost certainly") became evidence-strength phrasing
|
|
50
|
+
("strongly supported by the evidence"). The auditor's argument is the right one:
|
|
51
|
+
downstream users quote the strongest surface, and a disclaimer in a methodology
|
|
52
|
+
file does not travel with a word pasted into a report.
|
|
53
|
+
|
|
54
|
+
**7 & 21. Dependence is typed metadata, and the default is conservative.**
|
|
55
|
+
`Claim.dependence_class` is declared by the collector and always wins; regex
|
|
56
|
+
matching survives only as a fallback for older claims. Unrecognised evidence is
|
|
57
|
+
now `UNKNOWN` and discounted at 0.20, not assumed independent. My original
|
|
58
|
+
argument — that a wrong discount is harder to notice than a missing one — was
|
|
59
|
+
worse than the auditor's: for a model whose dominant failure mode is
|
|
60
|
+
overconfidence, unrecognised evidence should fail conservative.
|
|
61
|
+
|
|
62
|
+
**9. Agent no longer reimplements domain logic.** The `sellers.json` tool
|
|
63
|
+
hard-coded `ORG_NAME` while `classify_seller_name()` existed, so the agent path
|
|
64
|
+
typed individual publishers as organisations and routed them to corporate
|
|
65
|
+
registries that hold no record. It now delegates.
|
|
66
|
+
|
|
67
|
+
**12. Version reads source truth.** `importlib.metadata.version()` returned a
|
|
68
|
+
fallback for an uninstalled source tree, so a valid run reported a divergence
|
|
69
|
+
that did not exist.
|
|
70
|
+
|
|
71
|
+
**11. Test taxonomy.** Markers for `network`, `browser`, `integration`,
|
|
72
|
+
`adversarial`, `slow`. Default `addopts` excludes network and browser, so
|
|
73
|
+
`pytest` is green on a machine without DNS or Chromium — which is an environment
|
|
74
|
+
fact, not a defect.
|
|
75
|
+
|
|
76
|
+
**25. Exception taxonomy.** `EXPECTED_FAILURES` covers the world being
|
|
77
|
+
uncooperative; anything else is our defect and raises under `strict=True`. A
|
|
78
|
+
`TypeError` reporting as "tool failed" made production bugs indistinguishable
|
|
79
|
+
from a source being down.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Where I partially disagree — the monorepo
|
|
84
|
+
|
|
85
|
+
The auditor is right about the operational cost: four CI files, four LICENSEs,
|
|
86
|
+
four CHANGELOGs, synchronised versioning, and integration tests that hope
|
|
87
|
+
siblings exist at `../`. That is a distributed monorepo, and it is fragile.
|
|
88
|
+
|
|
89
|
+
But I do not think the answer is one repository *and* one package. The
|
|
90
|
+
`attribution-graph` boundary is real and load-bearing: **it performs no network
|
|
91
|
+
I/O**, which is precisely what makes its scoring auditable, and that guarantee
|
|
92
|
+
is enforced by the package boundary rather than by convention. Someone who wants
|
|
93
|
+
the inference model should not have to install an HTTP client and 36 collectors.
|
|
94
|
+
|
|
95
|
+
So: **one repository, four published packages.** A workspace, not a merge. That
|
|
96
|
+
takes the auditor's operational fix — one CI pipeline where integration failures
|
|
97
|
+
are fatal, one release process, no duplicated workflow config — while keeping
|
|
98
|
+
the boundary that carries a technical guarantee.
|
|
99
|
+
|
|
100
|
+
This is staged rather than done. The repository layout change is mechanical but
|
|
101
|
+
touches every path in `PUSH_INSTRUCTIONS.md`, and doing it half-way would be
|
|
102
|
+
worse than either end state.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Where I disagree
|
|
107
|
+
|
|
108
|
+
**14, on cache versus evidence.** The auditor reads these as conflated. They are
|
|
109
|
+
already separate objects: `EvidenceLog` writes content-addressed, hash-chained
|
|
110
|
+
captures under `evidence/`, while `.eae-cache/` is a replaceable performance
|
|
111
|
+
cache. The `net.py` comment saying "the cached body is the artifact" is wrong and
|
|
112
|
+
has been corrected — but the architecture was right; only the comment was not.
|
|
113
|
+
|
|
114
|
+
The auditor's underlying point about **capture completeness** is correct and is
|
|
115
|
+
now staged: the evidence record should carry the redirect chain, request and
|
|
116
|
+
response headers, TLS peer info, wire-body versus decoded-body hashes, and
|
|
117
|
+
truncation state. That is a real gap.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Staged, with reasons
|
|
122
|
+
|
|
123
|
+
**3. DNS rebinding / TOCTOU.** Correct, and not yet fixed. Validation resolves
|
|
124
|
+
the host; `httpx` resolves again when connecting. Closing it properly means a
|
|
125
|
+
custom transport that pins the validated address while preserving Host header,
|
|
126
|
+
SNI and certificate verification — worth doing carefully rather than quickly.
|
|
127
|
+
**Documented as a known limitation in `SECURITY.md` and the audit** rather than
|
|
128
|
+
left implied.
|
|
129
|
+
|
|
130
|
+
**6. Candidate-generation context.** The auditor is right that a global prior
|
|
131
|
+
applied to deliberately-selected candidates is selection bias, and that no amount
|
|
132
|
+
of tuning fixes it. The fix is an `AssessmentContext` recording how a candidate
|
|
133
|
+
entered the system, so calibration can be conditional on the selection rule.
|
|
134
|
+
That is a modelling change that should land *with* the calibration study, not
|
|
135
|
+
before it.
|
|
136
|
+
|
|
137
|
+
**8. `Reliability` conflates three concepts** — observation fidelity, source
|
|
138
|
+
independence, binding strength. This is the deepest finding after #5, and the
|
|
139
|
+
auditor is right that `AUTHORITATIVE` on a subject-controlled page is
|
|
140
|
+
misleading. Splitting it is a breaking change across every collector, and it
|
|
141
|
+
should be done once, deliberately.
|
|
142
|
+
|
|
143
|
+
**13. Capture completeness.** Above.
|
|
144
|
+
|
|
145
|
+
**24. Predicate policy table.** Behaviour currently spread across `model.py`,
|
|
146
|
+
`scoring.py`, `dependence.py` and collectors. Consolidating is correct and
|
|
147
|
+
mechanical.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## What the audit changed about the project's claim
|
|
152
|
+
|
|
153
|
+
From:
|
|
154
|
+
|
|
155
|
+
> determine which legal entity or person operates a domain, with a confidence
|
|
156
|
+
> figure
|
|
157
|
+
|
|
158
|
+
To:
|
|
159
|
+
|
|
160
|
+
> build an auditable evidence graph for entity-attribution investigations, rank
|
|
161
|
+
> candidate relationships by evidence strength, and preserve the provenance
|
|
162
|
+
> necessary for independent review
|
|
163
|
+
|
|
164
|
+
The auditor's closing observation is the one worth keeping: narrowing the claim
|
|
165
|
+
makes the project **more** credible, not less. The narrow claim is supported by
|
|
166
|
+
what exists. The broad one is not, and would not have survived first contact
|
|
167
|
+
with a reviewer who checked.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Response to re-audit (v1.6.0 → v1.7.0)
|
|
2
|
+
|
|
3
|
+
**Every finding I checked was reproducible.** I verified the five criticals
|
|
4
|
+
against the code before changing anything, and each behaved exactly as the
|
|
5
|
+
report described. No pushback on any P0 item.
|
|
6
|
+
|
|
7
|
+
The report's framing is the right one and I am adopting it: the problem is
|
|
8
|
+
**assurance mismatch**. Good local implementations existed while the
|
|
9
|
+
user-facing execution path did not compose their guarantees end-to-end. Fixing
|
|
10
|
+
that was this pass; no features were added.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Verified before fixing
|
|
15
|
+
|
|
16
|
+
| Finding | Reproduced |
|
|
17
|
+
|---|---|
|
|
18
|
+
| EA-01 entry hash omits headers/collector/note/body_path | yes — all five unhashed |
|
|
19
|
+
| EA-02 suffix deletion verifies | yes — deleted a capture, verifier said PASSED |
|
|
20
|
+
| EA-03 runner never wires EvidenceLog | yes — `Fetcher(...)` built with no log |
|
|
21
|
+
| EA-04 definitional self-grant | yes — `subject_html` claim merged evil.example into Victim Corp |
|
|
22
|
+
| EA-05 unique emails corroborate | yes — 6 different emails → 7 points, HIGH, durable=True |
|
|
23
|
+
| EA-09 budget bypassed by redirects | yes — `max_requests` not re-checked per hop |
|
|
24
|
+
| EA-10 declared dependence ignored | yes — never gathered in `assess()`, never serialised |
|
|
25
|
+
| EA-16 VERIFY.sh absent from repos | yes — existed only at workspace root |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## P0 — fixed
|
|
30
|
+
|
|
31
|
+
**EA-01.** The entry hash now covers a versioned canonical envelope: request
|
|
32
|
+
headers, response headers, collector, note, body_path, egress, outcome. Bumping
|
|
33
|
+
`ENTRY_SCHEMA` is required to change the field set, so a future field cannot
|
|
34
|
+
fall outside it by omission. Four tamper cases are permanent tests.
|
|
35
|
+
|
|
36
|
+
**EA-02.** The generated verifier recomputes the chain head and compares it to
|
|
37
|
+
the declared `manifest_hash`, and reconciles `capture_count`. Deleting the final
|
|
38
|
+
capture now fails with an explicit message. The verifier also states what it
|
|
39
|
+
**cannot** prove — internal consistency is not provenance, and only an external
|
|
40
|
+
anchor establishes that the package was not regenerated wholesale.
|
|
41
|
+
|
|
42
|
+
**EA-03 / EA-06.** Capture recording and policy evaluation are now transport
|
|
43
|
+
invariants: the layer performing the request records it, including refusals and
|
|
44
|
+
errors. `run_case()` passes the log and the policy engine into `Fetcher`, and
|
|
45
|
+
reconciles fetch count against capture count — a non-empty run producing an
|
|
46
|
+
empty package now fails verification rather than reporting `evidence_verified=True`.
|
|
47
|
+
|
|
48
|
+
**EA-04.** `is_definitional()` requires an engine-recognised registry collector
|
|
49
|
+
or source class, matched literally. Reliability is a scalar the claim author
|
|
50
|
+
supplies, so keying the model's strongest privilege on it made the carve-out
|
|
51
|
+
self-service. The hostile claim now scores UNSUPPORTED and does not merge;
|
|
52
|
+
`gleif` retains the exemption.
|
|
53
|
+
|
|
54
|
+
**EA-05.** A correlation point must connect observations. Identifiers are keyed
|
|
55
|
+
by value and counted only when the same normalised value appears on two or more
|
|
56
|
+
distinct profiles; singletons are reported in the caveat rather than silently
|
|
57
|
+
dropped. The reproduced case is now a permanent test: six profiles, six
|
|
58
|
+
different emails → **1 point, INSUFFICIENT**, down from 7/HIGH.
|
|
59
|
+
|
|
60
|
+
One existing test had encoded the bug — five profiles with five *different*
|
|
61
|
+
names and domains, asserting MODERATE. Rewritten to share the identifiers so it
|
|
62
|
+
tests the cap it was written for.
|
|
63
|
+
|
|
64
|
+
**EA-07.** `Fetcher` is constructed from `EgressPool.from_case(scope.egress)`.
|
|
65
|
+
|
|
66
|
+
**EA-09.** Budget is checked before every physical request including redirect
|
|
67
|
+
hops. `max_requests=1` with a 302→200 chain now raises rather than completing at
|
|
68
|
+
count=2.
|
|
69
|
+
|
|
70
|
+
**EA-10.** `assess()` gathers declared classes per canonical group, rejects
|
|
71
|
+
conflicting declarations within a group, and passes them through. Serialised in
|
|
72
|
+
`Claim.to_dict()`.
|
|
73
|
+
|
|
74
|
+
**EA-11.** Merge policy moved out of probability space:
|
|
75
|
+
`MERGE_LLR_THRESHOLD` in nats, applied to `log_odds`. Renaming the display while
|
|
76
|
+
keeping a 0.90 cut on the same unvalidated number would have been cosmetic.
|
|
77
|
+
|
|
78
|
+
**EA-15.** CI clones sibling repositories before the integration step, so the
|
|
79
|
+
gate can actually pass rather than being structurally red.
|
|
80
|
+
|
|
81
|
+
**EA-16.** `VERIFY.sh` ships in all four repositories. It existed only at the
|
|
82
|
+
workspace root while the docs told users to run it.
|
|
83
|
+
|
|
84
|
+
**EA-17 / EA-18.** Screenshot tests use the declared `browser` marker rather
|
|
85
|
+
than a `skipif` alias, so default `addopts` actually excludes them.
|
|
86
|
+
`available_renderer()` resolves and stats the Chromium executable instead of
|
|
87
|
+
treating package importability as availability. `Fetcher` accepts an injected
|
|
88
|
+
resolver, making the transport tests deterministic — mocking HTTP was not
|
|
89
|
+
enough, because validation performed real DNS first.
|
|
90
|
+
|
|
91
|
+
**EA-22.** Stale `net.py` comment corrected. Test counts reconciled: **723
|
|
92
|
+
collected, 714 in the deterministic default.**
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Accepted, staged, with the reason
|
|
97
|
+
|
|
98
|
+
**EA-12 — screenshot is a second network stack outside the SSRF boundary.**
|
|
99
|
+
Correct, and it also breaks the "no network I/O in the core" argument I used to
|
|
100
|
+
justify the package split. `screenshot.py` should move to a capture package
|
|
101
|
+
behind the same policy boundary, and browser subresource requests need route
|
|
102
|
+
interception. That is a package boundary change plus a sandboxing design, and
|
|
103
|
+
doing it badly would be worse than the current documented state.
|
|
104
|
+
|
|
105
|
+
**EA-13 — `parent_body_sha256` is caller-declared.** Right: Playwright
|
|
106
|
+
navigates the live URL independently, so a page changing between fetch and
|
|
107
|
+
screenshot yields screenshot B labelled as rendering body A. The fix is to
|
|
108
|
+
render the preserved body or hash the browser's actual main-document response.
|
|
109
|
+
|
|
110
|
+
**EA-14 — screenshot CLI flags are no-ops.** Confirmed. Rather than leave a
|
|
111
|
+
flag that does nothing, the flags are staged with the capture path.
|
|
112
|
+
|
|
113
|
+
**EA-19 / EA-20 / EA-21.** Exception taxonomy narrowing in `Engine`, audit-log
|
|
114
|
+
minimisation at the serializer boundary, and operator/hostname metadata as an
|
|
115
|
+
explicit choice. All correct, none load-bearing for the P0 assurance story.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## On the release framing
|
|
120
|
+
|
|
121
|
+
I accept the recommendation. This is **not** "ready to publish". `DESIGN.md`
|
|
122
|
+
and `PUBLICATION_PLAN.md` now describe it as a **pre-release experimental
|
|
123
|
+
research codebase**, and the evidence package is no longer described as
|
|
124
|
+
tamper-evident without the qualifier that deletion resistance requires an
|
|
125
|
+
external anchor.
|
|
126
|
+
|
|
127
|
+
The two things that would change that assessment, in order: replace the
|
|
128
|
+
LICENSE/OWNER placeholders, and run the calibration study. Everything else on
|
|
129
|
+
the P1/P2 list is quality work on a system whose assurances now at least
|
|
130
|
+
compose.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Response to release-readiness audit (v1.7.0 → v1.7.1)
|
|
2
|
+
|
|
3
|
+
**Decision accepted: HOLD was correct.** Four of the five mandatory blockers are
|
|
4
|
+
fixed; the fifth is licensing, which needs your decision rather than a code
|
|
5
|
+
change.
|
|
6
|
+
|
|
7
|
+
One finding I have to report before the substance.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## A discrepancy in the artifacts I shipped
|
|
12
|
+
|
|
13
|
+
The auditor tested the v1.7.0 tarballs. Reproducing their findings, I discovered
|
|
14
|
+
the working tree contained fixes for RB-01, RA-02 and RA-03 that were **not in
|
|
15
|
+
the tarballs I shipped**. I did not make those edits during the session that
|
|
16
|
+
produced v1.7.0.
|
|
17
|
+
|
|
18
|
+
I cannot explain the divergence, and for a tool whose entire purpose is evidence
|
|
19
|
+
integrity that is worth stating plainly rather than quietly reconciling. What I
|
|
20
|
+
have done:
|
|
21
|
+
|
|
22
|
+
- treated the **tarballs** as authoritative, since that is what was audited;
|
|
23
|
+
- re-run every one of the auditor's reproductions against the current tree
|
|
24
|
+
rather than assuming any fix was real;
|
|
25
|
+
- verified each remaining fix myself before claiming it.
|
|
26
|
+
|
|
27
|
+
Concretely: RB-01, RA-02 and RA-03 were already correct in the tree and I
|
|
28
|
+
confirmed them. RB-02, RB-03, RB-04, RA-01 and RA-04 were **not** fixed and I
|
|
29
|
+
fixed them in this pass.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## RB-02 — my own regression, and worse than reported
|
|
34
|
+
|
|
35
|
+
The auditor found `MERGE_LLR_THRESHOLD = 13.71` nats compared against
|
|
36
|
+
`a.probability`, which cannot exceed 1.0. Correct, and it was my fix from the
|
|
37
|
+
previous round: I renamed the constant's units without changing what it was
|
|
38
|
+
compared to, converting a tuning question into a correctness bug that failed
|
|
39
|
+
silently in the quiet direction.
|
|
40
|
+
|
|
41
|
+
The constant was also wrong on its own terms. `Assessment.log_odds` already
|
|
42
|
+
includes the prior, so adding `|log(PRIOR_ODDS)|` double-counted it. The
|
|
43
|
+
posterior threshold preserving the old 0.90 boundary is `log(0.9/0.1) = 2.197`.
|
|
44
|
+
|
|
45
|
+
**But fixing the threshold did not restore merging**, and this is the more
|
|
46
|
+
consequential finding. The resolver only scored directly-claimed
|
|
47
|
+
subject→object edges. Two domains sharing an identifier were **never assessed
|
|
48
|
+
as a pair at all** — the toolkit's canonical case. Each leg had one correlation
|
|
49
|
+
group, the corroboration rule correctly refused to merge on one group, and the
|
|
50
|
+
two-group evidence that actually existed was never pooled.
|
|
51
|
+
|
|
52
|
+
So no threshold could have helped: inferential merging had never worked.
|
|
53
|
+
|
|
54
|
+
`resolve()` now generates co-reference candidates — subjects sharing an object —
|
|
55
|
+
and pools both legs, which is the standard record-linkage construction.
|
|
56
|
+
Measured: two domains sharing two identifiers now merge at log_odds 8.89 with
|
|
57
|
+
two independent groups; sharing one identifier still does not merge.
|
|
58
|
+
|
|
59
|
+
## RB-03 — the constraint passed for the wrong reason
|
|
60
|
+
|
|
61
|
+
The transitive must-not-link check appeared to pass in my first run. It passed
|
|
62
|
+
because nothing merged at all. With merging restored, the real behaviour is
|
|
63
|
+
correct: A~B merges, and B~C is **rejected** because the union would place A and
|
|
64
|
+
C in one cluster against an explicit contradiction.
|
|
65
|
+
|
|
66
|
+
That is a lesson worth recording: a constraint test that passes while the
|
|
67
|
+
mechanism it constrains is disabled proves nothing.
|
|
68
|
+
|
|
69
|
+
## RB-04 — verifier no longer executes package code
|
|
70
|
+
|
|
71
|
+
`attribution verify` ran the `verify.py` inside the evidence directory. An
|
|
72
|
+
evidence package is untrusted input by definition. New
|
|
73
|
+
`attribution_graph.verify` reads the manifest as data, refuses `body_path`
|
|
74
|
+
values escaping the package, and the CLI uses it. Tested with a planted script
|
|
75
|
+
that writes a marker file: exit 0, marker absent.
|
|
76
|
+
|
|
77
|
+
The generated standalone `verify.py` still ships — a dependency-free checker a
|
|
78
|
+
reader can inspect in full has real value — and the CLI now prints a note saying
|
|
79
|
+
it was *not* executed and why.
|
|
80
|
+
|
|
81
|
+
## RB-05 — licensing
|
|
82
|
+
|
|
83
|
+
Unresolved, and not something I should decide. All four LICENSE files remain
|
|
84
|
+
placeholders while `pyproject.toml` asserts Apache-2.0, so the built metadata
|
|
85
|
+
makes a claim the shipped file does not support. `VERIFY.sh` fails on this by
|
|
86
|
+
design. ~20 files still carry `github.com/tusharkarumudi/`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## RA items
|
|
91
|
+
|
|
92
|
+
**RA-01 — evidence completeness.** 404s and redirect hops were already recorded
|
|
93
|
+
in the tree; I verified both. The cache case was not: a second run consumed
|
|
94
|
+
retrieved content and produced a package with zero captures that verified
|
|
95
|
+
clean. Cache hits now emit a capture with `outcome="cache"` carrying the body,
|
|
96
|
+
so the digest still commits to what was used and a reviewer can see the bytes
|
|
97
|
+
were not fetched during that run.
|
|
98
|
+
|
|
99
|
+
**RA-04 — provider/network mismatch.** `provider: oxylabs` with
|
|
100
|
+
`network: datacenter` was accepted and routed to `pr.oxylabs.io`, the profile's
|
|
101
|
+
own documented *residential* endpoint, while recording
|
|
102
|
+
`consent_sensitive: false`. Provenance and safeguard wrong at once. Profiles now
|
|
103
|
+
declare which networks they serve and a contradicting label is rejected.
|
|
104
|
+
|
|
105
|
+
**RA-06 / 8.3 / 8.4 — screenshots.** Taken off the release surface.
|
|
106
|
+
`--screenshots` now exits 2 with an explanation rather than being silently
|
|
107
|
+
accepted and doing nothing. Kept as an explicit error rather than removed so a
|
|
108
|
+
user with it in a script learns why.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Phase D gate
|
|
113
|
+
|
|
114
|
+
All six tests the auditor specified now exist and pass:
|
|
115
|
+
|
|
116
|
+
1. real `PolicyEngine` + real `Fetcher` across respect / record / ignore
|
|
117
|
+
2. resolver positive merge — two independent groups, non-definitional
|
|
118
|
+
3. resolver cluster constraint — A~B, B~C, A!~C
|
|
119
|
+
4. verifier safety — planted script not executed
|
|
120
|
+
5. evidence completeness — 404, redirect, cache
|
|
121
|
+
6. case egress — survives `load()`, reaches transport, mismatch rejected
|
|
122
|
+
|
|
123
|
+
Writing (1) surfaced a further defect in my own test: a shared cache directory
|
|
124
|
+
served mode 2 from mode 1's fetch, so the assertion passed for the wrong reason.
|
|
125
|
+
Each mode now gets its own cache.
|
|
126
|
+
|
|
127
|
+
**739 tests collected, 739 passing in the deterministic default.**
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Still open, deliberately
|
|
132
|
+
|
|
133
|
+
DNS rebinding through remote proxies; moving `screenshot.py` out of the
|
|
134
|
+
inference package; collector-by-collector `dependence_class` adoption; the
|
|
135
|
+
calibration study. All are on the auditor's own defer list, and the claims
|
|
136
|
+
around them are restrained accordingly.
|
|
137
|
+
|
|
138
|
+
The remaining release gate is licensing, which is yours.
|