traffic-twin 0.1.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.
- traffic_twin-0.1.0/LICENSE +202 -0
- traffic_twin-0.1.0/PKG-INFO +293 -0
- traffic_twin-0.1.0/README.md +263 -0
- traffic_twin-0.1.0/pyproject.toml +44 -0
- traffic_twin-0.1.0/setup.cfg +4 -0
- traffic_twin-0.1.0/src/traffic_twin/__init__.py +15 -0
- traffic_twin-0.1.0/src/traffic_twin/__main__.py +40 -0
- traffic_twin-0.1.0/src/traffic_twin/_log.py +38 -0
- traffic_twin-0.1.0/src/traffic_twin/_version.py +2 -0
- traffic_twin-0.1.0/src/traffic_twin/agent.py +819 -0
- traffic_twin-0.1.0/src/traffic_twin/auto.py +26 -0
- traffic_twin-0.1.0/src/traffic_twin/autocapture.py +445 -0
- traffic_twin-0.1.0/src/traffic_twin/bodies.py +660 -0
- traffic_twin-0.1.0/src/traffic_twin/bootstrap/sitecustomize.py +33 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/__init__.py +1 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/asgi.py +233 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/common.py +139 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/outbound.py +867 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/routes.py +80 -0
- traffic_twin-0.1.0/src/traffic_twin/capture/wsgi.py +376 -0
- traffic_twin-0.1.0/src/traffic_twin/conformance.py +79 -0
- traffic_twin-0.1.0/src/traffic_twin/context.py +128 -0
- traffic_twin-0.1.0/src/traffic_twin/ed25519.py +147 -0
- traffic_twin-0.1.0/src/traffic_twin/event.py +95 -0
- traffic_twin-0.1.0/src/traffic_twin/exporter.py +269 -0
- traffic_twin-0.1.0/src/traffic_twin/fleet.py +350 -0
- traffic_twin-0.1.0/src/traffic_twin/formats.py +138 -0
- traffic_twin-0.1.0/src/traffic_twin/gqlshape.py +395 -0
- traffic_twin-0.1.0/src/traffic_twin/hostpat.py +127 -0
- traffic_twin-0.1.0/src/traffic_twin/jsonlex.py +366 -0
- traffic_twin-0.1.0/src/traffic_twin/policy.py +937 -0
- traffic_twin-0.1.0/src/traffic_twin/py.typed +0 -0
- traffic_twin-0.1.0/src/traffic_twin/runtime.py +158 -0
- traffic_twin-0.1.0/src/traffic_twin/sanitizer.py +1402 -0
- traffic_twin-0.1.0/src/traffic_twin/store.py +191 -0
- traffic_twin-0.1.0/src/traffic_twin/textutil.py +60 -0
- traffic_twin-0.1.0/src/traffic_twin/vectors.py +131 -0
- traffic_twin-0.1.0/src/traffic_twin.egg-info/PKG-INFO +293 -0
- traffic_twin-0.1.0/src/traffic_twin.egg-info/SOURCES.txt +52 -0
- traffic_twin-0.1.0/src/traffic_twin.egg-info/dependency_links.txt +1 -0
- traffic_twin-0.1.0/src/traffic_twin.egg-info/top_level.txt +1 -0
- traffic_twin-0.1.0/tests/test_asgi.py +171 -0
- traffic_twin-0.1.0/tests/test_autocapture.py +302 -0
- traffic_twin-0.1.0/tests/test_bench.py +33 -0
- traffic_twin-0.1.0/tests/test_bodies.py +215 -0
- traffic_twin-0.1.0/tests/test_fleet.py +446 -0
- traffic_twin-0.1.0/tests/test_fork.py +56 -0
- traffic_twin-0.1.0/tests/test_hosts.py +209 -0
- traffic_twin-0.1.0/tests/test_install.py +164 -0
- traffic_twin-0.1.0/tests/test_otel.py +74 -0
- traffic_twin-0.1.0/tests/test_outbound.py +353 -0
- traffic_twin-0.1.0/tests/test_safety.py +79 -0
- traffic_twin-0.1.0/tests/test_units.py +206 -0
- traffic_twin-0.1.0/tests/test_wsgi.py +201 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: traffic-twin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: traffic-twin capture SDK for Python: sanitizes HTTP exchanges in process and exports them to tt-core over mTLS.
|
|
5
|
+
Author: traffic-twin maintainers
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ivseb/traffic-twin
|
|
8
|
+
Project-URL: Documentation, https://github.com/ivseb/traffic-twin/tree/main/sdk/python#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ivseb/traffic-twin
|
|
10
|
+
Project-URL: Issues, https://github.com/ivseb/traffic-twin/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/ivseb/traffic-twin/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: traffic-twin,http,capture,sanitizer,opentelemetry,mtls
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: System :: Monitoring
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# traffic-twin (Python)
|
|
32
|
+
|
|
33
|
+
SDK di cattura di traffic-twin per Python ≥ 3.9. Osserva le chiamate HTTP in ingresso
|
|
34
|
+
(Flask, FastAPI/Starlette, Django, qualunque app WSGI o ASGI) e in uscita
|
|
35
|
+
(`requests`, `httpx`, `urllib`, `http.client`), le **sanifica nel processo** secondo la
|
|
36
|
+
policy firmata (specifica `docs/spec/sanitizer.md`, fino alla v1.19) e invia a `tt-core` solo forme,
|
|
37
|
+
token HMAC e valori ammessi dalla policy, come log OTLP/HTTP JSON su mTLS.
|
|
38
|
+
|
|
39
|
+
- **Zero dipendenze a runtime**: solo la libreria standard (`pyproject.toml` ha
|
|
40
|
+
`dependencies = []`). Né `opentelemetry-*` né librerie crittografiche: la verifica delle
|
|
41
|
+
firme Ed25519 (RFC 8032) è scritta in Python puro (`traffic_twin/ed25519.py`, provata con i
|
|
42
|
+
vettori della RFC).
|
|
43
|
+
- **Non modifica il traffico dell'app**: nessun `traceparent` iniettato; l'unica eccezione,
|
|
44
|
+
opzionale e decisa dalla policy, è il membro di baggage `tt.s` verso gli host di
|
|
45
|
+
`sampling.propagate_to` (mai verso un host esterno della policy). Il membro è messo **in testa**
|
|
46
|
+
al baggage (chi legge prende il primo `tt.s` valido): un `tt.s` falsificato da un client e
|
|
47
|
+
inoltrato dall'app non può mai prevalere.
|
|
48
|
+
- **Non rallenta l'app**: sulla strada della richiesta ci sono solo la decisione di
|
|
49
|
+
campionamento e la copia dei byte del corpo; sanificazione, serializzazione e invio
|
|
50
|
+
avvengono in thread in background con code limitate. Quando le code sono piene si scartano
|
|
51
|
+
gli eventi più vecchi (e si contano), mai le richieste.
|
|
52
|
+
- **Fail-closed**: senza policy valida, chiave HMAC o primo heartbeat non cattura nulla;
|
|
53
|
+
l'app continua a funzionare normalmente. Un errore interno del sanitizer scarta lo
|
|
54
|
+
scambio e incrementa `sanitizer_errors`.
|
|
55
|
+
|
|
56
|
+
## Avvio senza codice
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install ./traffic_twin-x.y.z-py3-none-any.whl # dal kit; dal registro interno: pip install traffic-twin
|
|
60
|
+
export TT_ENROLL_URL=https://tt-core.internal:4318
|
|
61
|
+
export TT_ENROLL_TOKEN=tte_… # token di iscrizione (contiene l'impronta della CA)
|
|
62
|
+
export TT_HMAC_KEY_FILE=/run/secrets/tt-hmac-key
|
|
63
|
+
export TT_SERVICE_NAME=shop-api
|
|
64
|
+
python -m traffic_twin run -- uvicorn app:app # o gunicorn app:app, flask --app app run, python manage.py runserver, …
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Il comando cattura da solo **sia le chiamate in ingresso sia quelle in uscita**, senza toccare il codice
|
|
68
|
+
(`traffic_twin/autocapture.py`): la prima volta che un modulo noto viene importato, una piccola patch fa sì che
|
|
69
|
+
gli oggetti che crea catturino il traffico.
|
|
70
|
+
|
|
71
|
+
| Cosa | Come si avvolge |
|
|
72
|
+
|---|---|
|
|
73
|
+
| FastAPI / Starlette | `build_middleware_stack` restituisce lo stack avvolto nel middleware ASGI (una volta per app, mai se l'app ha già `install()` o il middleware) |
|
|
74
|
+
| Flask | `Flask.__init__` avvolge `wsgi_app` |
|
|
75
|
+
| Django | `get_wsgi_application()` / `get_asgi_application()` restituiscono l'oggetto avvolto |
|
|
76
|
+
| uvicorn (`uvicorn app:app`, `uvicorn.run(app)`) | l'app caricata, se è una semplice funzione/oggetto ASGI (o WSGI con `--interface wsgi`) che nessuna patch di framework copre; con `--factory` l'app che la factory crea è avvolta solo se è Starlette/FastAPI/Flask |
|
|
77
|
+
| gunicorn (WSGI o `-k uvicorn.workers.UvicornWorker`) | l'app che `gunicorn.util.import_app` restituisce, se è una semplice funzione/oggetto WSGI o ASGI |
|
|
78
|
+
| `requests`, `httpx`, `urllib` | in uscita, appena `requests`/`httpx` sono importati (`TT_OUTBOUND=auto`, il default di questo avvio); `TT_OUTBOUND=off` spegne la parte in uscita, una lista (`httpx`, `requests,urllib`, `http.client`) sceglie quali |
|
|
79
|
+
|
|
80
|
+
Un'app montata dentro un'altra già catturata (Starlette `Mount`, `DispatcherMiddleware`) non si cattura due volte.
|
|
81
|
+
|
|
82
|
+
**Quando non si riesce ad avvolgere nessuna app** (un server o un framework che non si conosce) l'SDK non resta in
|
|
83
|
+
silenzio: dopo 20 secondi scrive **una volta sola** su stderr `inbound not captured: ... add traffic_twin.install(app)`
|
|
84
|
+
e segnala a `tt-core` l'errore `inbound_not_captured` nell'heartbeat; la console mostra sulla sorgente il problema
|
|
85
|
+
**Ingresso non catturato** (e, senza ingresso, non c'è `identity.subject`). Rimedio: `app = traffic_twin.install(app)`
|
|
86
|
+
(sotto). Il controllo si ripete ogni 5 minuti finché nessuna app è avvolta.
|
|
87
|
+
|
|
88
|
+
`python -m traffic_twin run -- <comando>` mette `traffic_twin/bootstrap/sitecustomize.py` in
|
|
89
|
+
testa a `PYTHONPATH`: Python lo importa all'avvio, esso esegue prima l'eventuale
|
|
90
|
+
`sitecustomize` già presente nell'app e poi avvia l'agente. L'agente parte in background:
|
|
91
|
+
iscrizione, heartbeat e download della policy non ritardano l'avvio dell'app. Senza
|
|
92
|
+
`TT_ENROLL_URL` l'SDK resta inerte.
|
|
93
|
+
|
|
94
|
+
Alternative equivalenti: `import traffic_twin.auto` (una riga all'inizio dell'app) oppure
|
|
95
|
+
`traffic_twin.start()`.
|
|
96
|
+
|
|
97
|
+
| Variabile | Significato |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `TT_ENROLL_URL` | URL del listener Fleet/intake di `tt-core` (sempre `https`) |
|
|
100
|
+
| `TT_ENROLL_TOKEN` | Token di iscrizione `tte_<segreto>.<impronta CA>`, monouso o di gruppo |
|
|
101
|
+
| `TT_HMAC_KEY` / `TT_HMAC_KEY_FILE` | Chiave HMAC locale, una o più righe `<kid>:<base64url>` (§7.1). Non viaggia mai verso `tt-core` |
|
|
102
|
+
| `TT_SERVICE_NAME` | Nome di servizio richiesto all'iscrizione (altrimenti `OTEL_SERVICE_NAME` in minuscolo). Il nome usato negli eventi è quello **assegnato** da `tt-core` |
|
|
103
|
+
| `TT_STATE_DIR` | Cartella dello stato locale (default `~/.traffic-twin/<id>`): certificato, chiave privata, CA, policy firmata, interruttore. Cartella `0700`, file `0600`. Se non è scrivibile, o è di un altro utente, si usa una cartella temporanea privata (l'iscrizione non sopravvive al riavvio); una cartella propria più larga (anche `~/.traffic-twin`) viene riportata a `0700`. I file di stato si leggono solo se regolari, dell'utente e non scrivibili da gruppo/altri (`state_file_untrusted`). L'interruttore di emergenza scarta anche i batch in retry e gli scambi in sanificazione (generazione) |
|
|
104
|
+
| `TT_OUTBOUND` | Hook in uscita **globali** da attivare, separati da virgola: `requests`, `httpx`, `urllib`, `http.client`, oppure `auto` (requests/httpx se già importati, più urllib). Con `python -m traffic_twin run` e `import traffic_twin.auto` il default è `auto` applicato **all'import** di `requests`/`httpx` (`off` lo spegne); con `traffic_twin.start()` / `install()` il default è nessuno (vedi sotto) |
|
|
105
|
+
| `TT_ENROLL_CA_FILE` | PEM della CA radice. Obbligatorio solo su Python 3.9, che non sa leggere la catena presentata dal server: l'impronta del token viene verificata su questo file |
|
|
106
|
+
| `TT_DISABLED` | `1` disattiva completamente l'SDK (nessun thread, nessun hook) |
|
|
107
|
+
| `TT_DEBUG` | `1` scrive su stderr solo codici di stato ed errore (mai valori) |
|
|
108
|
+
| `TT_HEARTBEAT_INTERVAL_MS`, `TT_POLICY_POLL_INTERVAL_MS`, `TT_EXPORT_FLUSH_MS`, `TT_INBOUND_GRACE_MS` | Solo per test: sostituiscono gli intervalli ricevuti da `tt-core` e l'attesa (20 s) prima dell'avviso `inbound not captured` |
|
|
109
|
+
|
|
110
|
+
## Installazione nelle app
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import traffic_twin
|
|
114
|
+
|
|
115
|
+
# Flask (modifica app.wsgi_app sul posto)
|
|
116
|
+
app = traffic_twin.install(app)
|
|
117
|
+
|
|
118
|
+
# FastAPI / Starlette (add_middleware, il più esterno)
|
|
119
|
+
app = traffic_twin.install(app)
|
|
120
|
+
|
|
121
|
+
# Django: si installa sull'oggetto WSGI/ASGI e si riassegna
|
|
122
|
+
application = traffic_twin.install(get_wsgi_application()) # oppure get_asgi_application()
|
|
123
|
+
|
|
124
|
+
# qualunque app WSGI o ASGI (rilevata dalla firma; kind="wsgi"|"asgi" per forzare)
|
|
125
|
+
application = traffic_twin.install(application)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`install()` è idempotente: su un'app già avvolta (da un'altra `install()` o dal comando `run`) non fa nulla. Avvia l'agente dall'ambiente (o dai parametri: `install(app, enroll_url=…,
|
|
129
|
+
service_name=…, outbound="requests,httpx")`) e **non lancia mai eccezioni**: se non riesce a
|
|
130
|
+
installarsi emette un `RuntimeWarning` e lascia l'app com'è. Si possono usare anche i
|
|
131
|
+
middleware direttamente: `traffic_twin.capture.asgi.ASGIMiddleware`,
|
|
132
|
+
`traffic_twin.capture.wsgi.WSGIMiddleware`.
|
|
133
|
+
|
|
134
|
+
### `framework_route` (§3.3, §3.4)
|
|
135
|
+
|
|
136
|
+
Il template della route si risolve **dopo** la risposta, nel thread di sanificazione, chiedendo
|
|
137
|
+
al router del framework quale route corrisponde alla richiesta (senza eseguire codice
|
|
138
|
+
dell'app), sempre completo dei prefissi di montaggio:
|
|
139
|
+
|
|
140
|
+
| Framework | Fonte | Note |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Flask | `app.url_map.bind_to_environ(...).match(return_rule=True)` | Blueprint e `SCRIPT_NAME` inclusi; `<path:x>` non si normalizza |
|
|
143
|
+
| FastAPI / Starlette | `route.matches(scope)` sul router, anche dentro `Mount` | `{x:path}` non si normalizza |
|
|
144
|
+
| Django | `django.urls.resolve(...).route` | `re_path` non si normalizza: vale il template della policy |
|
|
145
|
+
|
|
146
|
+
Se la route non si può ricostruire non si passa `framework_route` e vale il punto 2 di §3.3.
|
|
147
|
+
|
|
148
|
+
## Chiamate in uscita
|
|
149
|
+
|
|
150
|
+
Prima i **punti di estensione**, senza patch globali:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import requests, httpx, urllib.request
|
|
154
|
+
from traffic_twin.capture.outbound import make_requests_adapter, make_httpx_transports, make_urllib_handler
|
|
155
|
+
|
|
156
|
+
s = requests.Session(); s.mount("https://", make_requests_adapter()())
|
|
157
|
+
T, AT = make_httpx_transports()
|
|
158
|
+
c = httpx.Client(transport=T(httpx.HTTPTransport())) # sincrono
|
|
159
|
+
a = httpx.AsyncClient(transport=AT(httpx.AsyncHTTPTransport())) # asincrono
|
|
160
|
+
opener = urllib.request.build_opener(make_urllib_handler()())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Per le app che costruiscono i client altrove, una patch è inevitabile: è **opt-in**
|
|
164
|
+
(`TT_OUTBOUND` o `outbound=`) e **reversibile** (ogni `install_*` di
|
|
165
|
+
`traffic_twin.capture.outbound` restituisce la funzione che ripristina gli originali):
|
|
166
|
+
|
|
167
|
+
| Valore | Cosa fa |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `requests` | le nuove `requests.Session` ricevono l'adattatore (usa il punto di estensione) |
|
|
170
|
+
| `httpx` | avvolge `handle_request` / `handle_async_request` dei trasporti predefiniti |
|
|
171
|
+
| `urllib` | installa un opener predefinito con l'handler (`install_opener`, API ufficiale) |
|
|
172
|
+
| `http.client` | patch di `HTTPConnection` (`putrequest`, `putheader`, `endheaders`, `send`, `getresponse`): non esiste un punto di estensione a questo livello. Copre anche `urllib3`/`requests` e tutto ciò che sta sopra `http.client` |
|
|
173
|
+
|
|
174
|
+
Se due livelli vedono la stessa chiamata (adattatore `requests` e patch `http.client`) la
|
|
175
|
+
registra solo il più esterno (`contextvars`). Le chiamate fatte dall'SDK stesso (Fleet,
|
|
176
|
+
intake) non si catturano mai.
|
|
177
|
+
|
|
178
|
+
I corpi si copiano mentre l'app li legge (tee con limite `max_body_bytes + 1`, il resto si
|
|
179
|
+
conta): il corpo di una risposta che l'app non legge fino alla fine esce come `trunc:size`.
|
|
180
|
+
La codifica `gzip`/`deflate` la decodifica il sanitizer (§9.0), partendo dai byte ancora
|
|
181
|
+
codificati.
|
|
182
|
+
|
|
183
|
+
### Host non dichiarati e scoperta automatica (spec v1.5)
|
|
184
|
+
|
|
185
|
+
Un host esterno che nessuna `base` della policy né `segments.hosts_allow` elenca non esce
|
|
186
|
+
più come `host:unlisted`: con `hosts.discovery: auto` (default, anche quando il campo
|
|
187
|
+
`hosts` manca) il `service` dell'evento è `host:` + **pattern** (sanitizer.md §3.5). Il
|
|
188
|
+
pattern è l'host in minuscolo, senza porta di default e senza un punto finale, in cui ogni
|
|
189
|
+
etichetta resta letterale solo se è `^[a-z][a-z0-9-]{0,62}$`, non ha sei cifre consecutive
|
|
190
|
+
né dodici esadecimali consecutivi, altrimenti diventa `{*}` (`t-3912345678.tenant.example` →
|
|
191
|
+
`host:{*}.tenant.example`). I letterali IP escono solo come `ip:private` / `ip:public`
|
|
192
|
+
(tabella chiusa di §3.6, parser IPv6 proprio in `traffic_twin/hostpat.py`: né `ipaddress` né
|
|
193
|
+
`is_private`), con la porta se non di default; un'etichetta vuota dà `host:invalid`. Gli
|
|
194
|
+
adattatori passano l'host così come lo vede il client (IPv6 tra parentesi, nessun IDN,
|
|
195
|
+
nessun punto tolto, nessun `str.lower()` Unicode).
|
|
196
|
+
|
|
197
|
+
`hosts.discovery: off` è la **modalità stretta**: gli host non dichiarati escono come
|
|
198
|
+
`host:unlisted`, byte per byte come prima della v1.5. Lo imposta la policy (`tt-core`:
|
|
199
|
+
`auto` → `off` chiude, `off` → `auto` apre, con le approvazioni normali); l'SDK non ha
|
|
200
|
+
opzioni locali. Il dominio registrabile e la cardinalità delle etichette sono controlli di
|
|
201
|
+
`tt-core` (zona B): l'SDK non li implementa.
|
|
202
|
+
|
|
203
|
+
### Corpi XML e SOAP, multipart, documenti e corpi grandi (spec v1.19)
|
|
204
|
+
|
|
205
|
+
Con l'oggetto `bodies` della policy (sanitizer.md §22; assente = tutto spento, uscita
|
|
206
|
+
identica alle versioni precedenti) il sanitizer analizza anche:
|
|
207
|
+
|
|
208
|
+
- **XML e SOAP** (`application/xml`, `text/xml`, `*+xml`): lettore XML scritto a mano
|
|
209
|
+
(`traffic_twin/bodies.py`), sottoinsieme rigoroso di XML 1.0 + Namespaces, **senza**
|
|
210
|
+
`xml.etree`/`expat`: nessuna DTD, nessuna entità oltre alle cinque predefinite e ai
|
|
211
|
+
riferimenti numerici, iterativo e con al più 256 elementi annidati (`trunc:invalid-xml`
|
|
212
|
+
altrimenti). Escono anche `fmt` (`xml`/`soap11`/`soap12`), `xmlns` e, per la richiesta,
|
|
213
|
+
`soap_action`; le regole usano i passi `.@nome` e `.#text`.
|
|
214
|
+
- **`multipart/form-data`**: i campi come un form, le parti file sempre come
|
|
215
|
+
`drop:file:<tipo>:<classe>` (`trunc:invalid-multipart` se il corpo non è conforme).
|
|
216
|
+
- **Documenti base64** (`bodies.docs`): `drop:doc:<tipo>:<classe>` al posto della classe di
|
|
217
|
+
lunghezza.
|
|
218
|
+
- **Corpi grandi** (`bodies.max_large_body_bytes`): la copia del corpo conserva
|
|
219
|
+
`max(limits.max_body_bytes, bodies.max_large_body_bytes) + 1` byte (WSGI, ASGI e client
|
|
220
|
+
in uscita) e gli array si tagliano a `large_array` / `large_nested_array`.
|
|
221
|
+
|
|
222
|
+
L'azione `code` accetta anche la forma `{"action":"code","shape":"alnum"}` (§5.6).
|
|
223
|
+
|
|
224
|
+
## Contesto, OpenTelemetry e campionamento
|
|
225
|
+
|
|
226
|
+
- Un `contextvars.ContextVar` lega ogni chiamata in uscita allo scambio in ingresso (stesso
|
|
227
|
+
`trace_id`, `parent_span_id` = `span_id` dell'evento in ingresso, decisione di campionamento
|
|
228
|
+
**ereditata**): vale anche per `asyncio.create_task` e per i thread dei pool che copiano il
|
|
229
|
+
contesto (Starlette/anyio). I thread avviati a mano dall'app non ereditano il contesto: lì
|
|
230
|
+
valgono le regole "job" di §12 (trace OpenTelemetry, poi caso).
|
|
231
|
+
- Si legge `traceparent`. Se `opentelemetry-api` è importabile se ne legge **solo** il contesto
|
|
232
|
+
dello span corrente (`trace.get_current_span().get_span_context()`): l'SDK **non** crea né
|
|
233
|
+
registra mai provider, propagatori o contesti, e non crea span. Il test
|
|
234
|
+
`tests/test_otel.py` esegue la stessa app con e senza l'SDK e confronta gli span esportati
|
|
235
|
+
(nome, tipo, attributi, struttura), il provider e il propagatore globali.
|
|
236
|
+
- Campionamento per soggetto (§11, §12): `HMAC(chiave, epoch ‖ "sample" ‖ soggetto)`;
|
|
237
|
+
baggage `tt.s` considerato solo con `trust_baggage` su servizi `internal`; l'override
|
|
238
|
+
dell'heartbeat può solo **abbassare** il tasso.
|
|
239
|
+
|
|
240
|
+
## Fleet e stato locale
|
|
241
|
+
|
|
242
|
+
- **Iscrizione**: connessione TLS senza fiducia, ricerca nella catena presentata da
|
|
243
|
+
`tt-core` del certificato con l'impronta SHA-256 contenuta nel token, richiesta di iscrizione
|
|
244
|
+
con quel certificato come unica CA. La libreria standard non sa costruire una CSR PKCS#10:
|
|
245
|
+
si usa il percorso **senza CSR** dell'API (`tt-core` genera la coppia di chiavi, restituisce
|
|
246
|
+
`client_key` una sola volta sul canale appena fissato e non la salva). La chiave si scrive
|
|
247
|
+
subito con modo `0600` in `TT_STATE_DIR` (le API `ssl` di Python vogliono dei file per
|
|
248
|
+
`load_cert_chain`) e si riusa ai riavvii. Più processi (gunicorn con più worker) condividono
|
|
249
|
+
lo stato e si serializzano con un lock su file: un token monouso si consuma una volta sola.
|
|
250
|
+
- **Heartbeat prima di qualsiasi cattura**; interruttore di emergenza, revoca e
|
|
251
|
+
`min_issued_seq` sono salvati e restano validi dopo un riavvio.
|
|
252
|
+
- **Policy**: firma Ed25519 su `"tt.policy.body/v1" ‖ 0x00 ‖ corpo` con chiavi fissate e non
|
|
253
|
+
revocate (verifica in Python puro, circa 30 ms, una volta per policy), schema completo del
|
|
254
|
+
corpo, coerenza busta/corpo, gruppo, `not_after`, `issued_seq` crescente e ≥
|
|
255
|
+
`min_issued_seq`. Una policy rifiutata non sostituisce mai la precedente valida. Nuove
|
|
256
|
+
chiavi di firma solo se approvate (firma di endorsement) da una chiave già fissata.
|
|
257
|
+
- Server con `fork` (gunicorn `--preload`, uWSGI): i thread non sopravvivono al `fork`; il
|
|
258
|
+
processo figlio ricrea l'agente con `os.register_at_fork`.
|
|
259
|
+
|
|
260
|
+
## Limiti noti
|
|
261
|
+
|
|
262
|
+
- **WSGI**: `PATH_INFO` e i nomi degli header arrivano già decodificati dal server. Il path
|
|
263
|
+
grezzo si prende da `RAW_URI`/`REQUEST_URI` (gunicorn, uWSGI, mod_wsgi); altrimenti il path
|
|
264
|
+
decodificato si ricodifica (un `%2F` originale non si distingue da `/`, con `wsgiref` e
|
|
265
|
+
Werkzeug). I nomi degli header si ricostruiscono da `HTTP_*` con `_` → `-`: un header che
|
|
266
|
+
contiene davvero un underscore compare con il trattino. Più header con lo stesso nome
|
|
267
|
+
arrivano già uniti dal server (con `, `, anche per `Cookie`).
|
|
268
|
+
- **Python 3.9**: manca l'accesso alla catena TLS presentata dal server; per l'iscrizione serve
|
|
269
|
+
`TT_ENROLL_CA_FILE`. Da 3.10 a 3.12 si usa l'API privata `_sslobj.get_unverified_chain()`,
|
|
270
|
+
da 3.13 quella pubblica.
|
|
271
|
+
- File serviti con `wsgi.file_wrapper` / `http.response.pathsend`: i byte non passano
|
|
272
|
+
dall'app, la risposta esce come `trunc:size`.
|
|
273
|
+
- Gli errori di rete di `urllib` senza la patch `http.client` si classificano `other`; con
|
|
274
|
+
l'adattatore `requests` o i trasporti `httpx` si distinguono `timeout`, `refused`, `dns`,
|
|
275
|
+
`tls`, `reset`.
|
|
276
|
+
- WebSocket e altri scope ASGI non `http` non si catturano.
|
|
277
|
+
- I `latency_ms` includono il tempo di streaming della risposta (fino alla chiusura).
|
|
278
|
+
|
|
279
|
+
## Sviluppo
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
cd sdk/python
|
|
283
|
+
make vectors # python -m traffic_twin.conformance : tutti i vettori di conformità (726 con la v1.19), byte per byte
|
|
284
|
+
make test # unittest: vettori, Ed25519 (RFC 8032), JSON, G1, WSGI, ASGI, uscita, Fleet, OTel, install
|
|
285
|
+
make bench # overhead (python bench/overhead.py)
|
|
286
|
+
pip install -r requirements-dev.txt # solo per i test opzionali (httpx, requests, Flask, …)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
I test usano solo `unittest`; quelli che richiedono una dipendenza di sviluppo
|
|
290
|
+
(`requirements-dev.txt`, versioni fissate) si saltano se manca. Un finto `tt-core` con
|
|
291
|
+
intake OTLP e API Fleet su mTLS (`tests/fakecore.py`) usa una PKI costruita in Python puro
|
|
292
|
+
(certificati Ed25519 in DER, `tests/pki.py`): non serve il binario `openssl`. Provato su
|
|
293
|
+
Python 3.9, 3.11, 3.12 e 3.13.
|