weir-scan 0.1.0__py3-none-any.whl
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.
- weir/__init__.py +0 -0
- weir/adapters/__init__.py +5 -0
- weir/adapters/otel/__init__.py +17 -0
- weir/adapters/otel/_contract.py +297 -0
- weir/adapters/otel/_map.py +584 -0
- weir/adapters/otel/_wire.py +240 -0
- weir/catalog/__init__.py +16 -0
- weir/catalog/_types.py +50 -0
- weir/catalog/default.py +47 -0
- weir/catalog/digest.py +45 -0
- weir/catalog/eligibility.py +19 -0
- weir/catalog/structure_classes.py +43 -0
- weir/cli/__init__.py +0 -0
- weir/cli/main.py +230 -0
- weir/data/__init__.py +1 -0
- weir/data/sample-export.json +279 -0
- weir/evaluate/__init__.py +11 -0
- weir/evaluate/_types.py +25 -0
- weir/evaluate/evaluator.py +83 -0
- weir/evaluate/witness.py +46 -0
- weir/gauge/__init__.py +10 -0
- weir/gauge/_types.py +25 -0
- weir/gauge/gauge.py +103 -0
- weir/gauge/ladder.py +46 -0
- weir/graph/__init__.py +4 -0
- weir/graph/_types.py +27 -0
- weir/graph/builder.py +49 -0
- weir/label/__init__.py +4 -0
- weir/label/_types.py +26 -0
- weir/label/labeler.py +80 -0
- weir/py.typed +0 -0
- weir/report/__init__.py +11 -0
- weir/report/_template.py +86 -0
- weir/report/lexicon.py +20 -0
- weir/report/renderer.py +102 -0
- weir/report/text.py +43 -0
- weir/rules_commons/__init__.py +4 -0
- weir/rules_commons/_types.py +17 -0
- weir/rules_commons/bundled/injection-exfil-to-outbound-sink.json +9 -0
- weir/rules_commons/loader.py +28 -0
- weir/schema/__init__.py +0 -0
- weir/schema/_digest.py +75 -0
- weir/schema/baseline.py +208 -0
- weir/schema/delta.py +168 -0
- weir/schema/dialect.py +103 -0
- weir/schema/flowfact.py +179 -0
- weir/schema/trace.py +143 -0
- weir/taint/__init__.py +12 -0
- weir/taint/_types.py +24 -0
- weir/taint/canon.py +9 -0
- weir/taint/engine.py +99 -0
- weir/taint/reachability.py +33 -0
- weir_scan-0.1.0.dist-info/METADATA +131 -0
- weir_scan-0.1.0.dist-info/RECORD +56 -0
- weir_scan-0.1.0.dist-info/WHEEL +4 -0
- weir_scan-0.1.0.dist-info/entry_points.txt +2 -0
weir/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""OTel GenAI OTLP-JSON adapter (M4): bytes -> CanonicalTrace + ledger.
|
|
2
|
+
|
|
3
|
+
Reject-narrow: OtlpRejectError (exit-2 material) fires only for
|
|
4
|
+
not-JSON / not-OTLP-shaped input. Everything else degrades, named."""
|
|
5
|
+
|
|
6
|
+
from weir.adapters.otel._contract import REMEDIATION, Degradation, DegradationReason
|
|
7
|
+
from weir.adapters.otel._map import ADAPTER_NAME, ADAPTER_VERSION, AdapterResult, map_wire
|
|
8
|
+
from weir.adapters.otel._wire import OtlpRejectError, decode_input
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ADAPTER_NAME", "ADAPTER_VERSION", "AdapterResult", "Degradation",
|
|
12
|
+
"DegradationReason", "OtlpRejectError", "REMEDIATION", "adapt_otlp",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def adapt_otlp(data: bytes) -> AdapterResult:
|
|
17
|
+
return map_wire(decode_input(data))
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""The reject-vs-degrade contract in executable form (M4 design section 2).
|
|
2
|
+
|
|
3
|
+
Exit-2 rejects are exactly two conditions and live in _wire.OtlpRejectError.
|
|
4
|
+
EVERYTHING else is a named row here. The corrupt corpus derives its expected
|
|
5
|
+
verdicts from these rows; the capability ladder prints the remediation
|
|
6
|
+
strings. A malformation without a row is a bug in this contract, never a
|
|
7
|
+
judgment call in the mapper - add the row WITH its corpus item.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import enum
|
|
13
|
+
|
|
14
|
+
import msgspec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DegradationReason(enum.StrEnum):
|
|
18
|
+
INVALID_ENCODING = "invalid_encoding"
|
|
19
|
+
UNDECODABLE_BATCH = "undecodable_batch"
|
|
20
|
+
NON_TRACE_BATCHES_SKIPPED = "non_trace_batches_skipped"
|
|
21
|
+
UNDECODABLE_SPAN = "undecodable_span"
|
|
22
|
+
UNDECODABLE_SCOPE = "undecodable_scope"
|
|
23
|
+
ORPHANED_PARENT = "orphaned_parent"
|
|
24
|
+
DUPLICATE_SPAN_ID = "duplicate_span_id"
|
|
25
|
+
MISSING_SPAN_ID = "missing_span_id"
|
|
26
|
+
NONSTANDARD_ID_ENCODING = "nonstandard_id_encoding"
|
|
27
|
+
UNMAPPABLE_GENAI_SPAN = "unmappable_genai_span"
|
|
28
|
+
NON_GENAI_SPANS_FILTERED = "non_genai_spans_filtered"
|
|
29
|
+
MISSING_CONTENT = "missing_content"
|
|
30
|
+
UNPARSEABLE_CONTENT = "unparseable_content"
|
|
31
|
+
TRUNCATED_CONTENT = "truncated_content"
|
|
32
|
+
INVALID_TIMESTAMP = "invalid_timestamp"
|
|
33
|
+
AMBIGUOUS_JOIN = "ambiguous_join"
|
|
34
|
+
CONFLICTING_JOIN_EVIDENCE = "conflicting_join_evidence"
|
|
35
|
+
UNKNOWN_DIALECT = "unknown_dialect"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
REMEDIATION: dict[DegradationReason, str] = {
|
|
39
|
+
DegradationReason.INVALID_ENCODING: (
|
|
40
|
+
"the input is not valid UTF-8; invalid bytes were replaced with "
|
|
41
|
+
"U+FFFD, which can silently alter content-mined join evidence and "
|
|
42
|
+
"taint payloads - fix the exporter's encoding"
|
|
43
|
+
),
|
|
44
|
+
DegradationReason.NON_TRACE_BATCHES_SKIPPED: (
|
|
45
|
+
"lines that decoded but carry no resourceSpans list (logs/metrics "
|
|
46
|
+
"in a mixed export, or a malformed traces batch) were skipped; "
|
|
47
|
+
"export traces to their own well-formed file"
|
|
48
|
+
),
|
|
49
|
+
DegradationReason.UNDECODABLE_BATCH: (
|
|
50
|
+
"one or more JSONL lines could not be decoded; re-export the batch or "
|
|
51
|
+
"check the exporter's file rotation for mid-write truncation"
|
|
52
|
+
),
|
|
53
|
+
DegradationReason.UNDECODABLE_SPAN: (
|
|
54
|
+
"a span failed structural decode and was quarantined; check the "
|
|
55
|
+
"exporter for schema-violating span serialization"
|
|
56
|
+
),
|
|
57
|
+
DegradationReason.UNDECODABLE_SCOPE: (
|
|
58
|
+
"an instrumentation scope failed structural decode and was dropped; "
|
|
59
|
+
"framework identity for its spans is lost - check the exporter's "
|
|
60
|
+
"scope serialization"
|
|
61
|
+
),
|
|
62
|
+
DegradationReason.ORPHANED_PARENT: (
|
|
63
|
+
"a span references a parent absent from this export; include the full "
|
|
64
|
+
"trace in one export (or concatenate the batches into one JSONL file)"
|
|
65
|
+
),
|
|
66
|
+
DegradationReason.DUPLICATE_SPAN_ID: (
|
|
67
|
+
"two spans share a span id; check the instrumentation's id generator "
|
|
68
|
+
"or de-duplicate the export"
|
|
69
|
+
),
|
|
70
|
+
DegradationReason.MISSING_SPAN_ID: (
|
|
71
|
+
"a span carries no id; upgrade the exporter - joins to this span "
|
|
72
|
+
"cannot be trusted"
|
|
73
|
+
),
|
|
74
|
+
DegradationReason.NONSTANDARD_ID_ENCODING: (
|
|
75
|
+
"span ids are not OTLP/JSON lowercase hex (base64 protojson output is "
|
|
76
|
+
"the usual cause); linkage is unaffected, but spec-true hex ids are "
|
|
77
|
+
"recommended"
|
|
78
|
+
),
|
|
79
|
+
DegradationReason.UNMAPPABLE_GENAI_SPAN: (
|
|
80
|
+
"a GenAI span's operation could not be derived; set "
|
|
81
|
+
"gen_ai.operation.name on every GenAI span"
|
|
82
|
+
),
|
|
83
|
+
DegradationReason.NON_GENAI_SPANS_FILTERED: (
|
|
84
|
+
"non-GenAI spans (http/db/plumbing) were filtered from analysis; this "
|
|
85
|
+
"is normal and not a defect"
|
|
86
|
+
),
|
|
87
|
+
DegradationReason.MISSING_CONTENT: (
|
|
88
|
+
"content capture is off; for OTel GenAI instrumentations built on the "
|
|
89
|
+
"util-genai layer, set "
|
|
90
|
+
"OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental and "
|
|
91
|
+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY to capture "
|
|
92
|
+
"gen_ai.input.messages / gen_ai.output.messages / "
|
|
93
|
+
"gen_ai.tool.call.arguments and unlock cross-step analysis"
|
|
94
|
+
),
|
|
95
|
+
DegradationReason.UNPARSEABLE_CONTENT: (
|
|
96
|
+
"a content attribute was present but not parseable; check the "
|
|
97
|
+
"instrumentation's payload serialization"
|
|
98
|
+
),
|
|
99
|
+
DegradationReason.TRUNCATED_CONTENT: (
|
|
100
|
+
"a content attribute appears truncated (payload limit hit); raise the "
|
|
101
|
+
"exporter's attribute length limit"
|
|
102
|
+
),
|
|
103
|
+
DegradationReason.INVALID_TIMESTAMP: (
|
|
104
|
+
"a span timestamp was unparseable; ordering fell back to span id - "
|
|
105
|
+
"fix the exporter's clock fields"
|
|
106
|
+
),
|
|
107
|
+
DegradationReason.AMBIGUOUS_JOIN: (
|
|
108
|
+
"join evidence matched multiple candidate spans, so no join was "
|
|
109
|
+
"recorded; ensure tool call ids are unique per trace"
|
|
110
|
+
),
|
|
111
|
+
DegradationReason.CONFLICTING_JOIN_EVIDENCE: (
|
|
112
|
+
"two join sources disagreed and precedence resolved it; this is the "
|
|
113
|
+
"fingerprint of malformed instrumentation or attempted linkage "
|
|
114
|
+
"forgery - inspect the named spans"
|
|
115
|
+
),
|
|
116
|
+
DegradationReason.UNKNOWN_DIALECT: (
|
|
117
|
+
"the trace matched no registered dialect; it was mapped under the "
|
|
118
|
+
"default profile with every node degraded - tell us which "
|
|
119
|
+
"instrumentation produced it at "
|
|
120
|
+
"https://github.com/IdoGol24/weir/issues"
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
TRIGGER: dict[DegradationReason, str] = {
|
|
126
|
+
DegradationReason.INVALID_ENCODING: (
|
|
127
|
+
"input bytes are not valid UTF-8"
|
|
128
|
+
),
|
|
129
|
+
DegradationReason.UNDECODABLE_BATCH: (
|
|
130
|
+
"a JSONL line failed to parse as JSON"
|
|
131
|
+
),
|
|
132
|
+
DegradationReason.NON_TRACE_BATCHES_SKIPPED: (
|
|
133
|
+
"a batch decoded fine but carries no resourceSpans list"
|
|
134
|
+
),
|
|
135
|
+
DegradationReason.UNDECODABLE_SPAN: (
|
|
136
|
+
"a span object failed structural decode against the wire schema"
|
|
137
|
+
),
|
|
138
|
+
DegradationReason.UNDECODABLE_SCOPE: (
|
|
139
|
+
"an instrumentation scope object failed structural decode"
|
|
140
|
+
),
|
|
141
|
+
DegradationReason.ORPHANED_PARENT: (
|
|
142
|
+
"a span's parentSpanId names no span present in this export"
|
|
143
|
+
),
|
|
144
|
+
DegradationReason.DUPLICATE_SPAN_ID: (
|
|
145
|
+
"two or more spans carry the same span id"
|
|
146
|
+
),
|
|
147
|
+
DegradationReason.MISSING_SPAN_ID: (
|
|
148
|
+
"a span's spanId is empty"
|
|
149
|
+
),
|
|
150
|
+
DegradationReason.NONSTANDARD_ID_ENCODING: (
|
|
151
|
+
"a span id is not OTLP/JSON lowercase hex"
|
|
152
|
+
),
|
|
153
|
+
DegradationReason.UNMAPPABLE_GENAI_SPAN: (
|
|
154
|
+
"gen_ai.operation.name is absent and no gen_ai.tool.name marker is "
|
|
155
|
+
"present either"
|
|
156
|
+
),
|
|
157
|
+
DegradationReason.NON_GENAI_SPANS_FILTERED: (
|
|
158
|
+
"a span carries no gen_ai.* attribute"
|
|
159
|
+
),
|
|
160
|
+
DegradationReason.MISSING_CONTENT: (
|
|
161
|
+
"the content attribute for this node's kind is absent"
|
|
162
|
+
),
|
|
163
|
+
DegradationReason.UNPARSEABLE_CONTENT: (
|
|
164
|
+
"a content attribute is present but its JSON does not parse, and "
|
|
165
|
+
"the failure does not fingerprint as truncation"
|
|
166
|
+
),
|
|
167
|
+
DegradationReason.TRUNCATED_CONTENT: (
|
|
168
|
+
"a content attribute's JSON cuts off at end-of-input or inside an "
|
|
169
|
+
"unterminated string"
|
|
170
|
+
),
|
|
171
|
+
DegradationReason.INVALID_TIMESTAMP: (
|
|
172
|
+
"startTimeUnixNano is missing, non-numeric, or negative"
|
|
173
|
+
),
|
|
174
|
+
DegradationReason.AMBIGUOUS_JOIN: (
|
|
175
|
+
"join evidence (explicit id, nesting, or a mined id) matches more "
|
|
176
|
+
"than one candidate, or a duplicated id is shared"
|
|
177
|
+
),
|
|
178
|
+
DegradationReason.CONFLICTING_JOIN_EVIDENCE: (
|
|
179
|
+
"an envelope join (explicit or nested) already exists for a call, "
|
|
180
|
+
"and mined content evidence points to a different result"
|
|
181
|
+
),
|
|
182
|
+
DegradationReason.UNKNOWN_DIALECT: (
|
|
183
|
+
"no schemaUrl or attribute fingerprint matches a registered dialect"
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
DISPOSITION: dict[DegradationReason, str] = {
|
|
189
|
+
DegradationReason.INVALID_ENCODING: (
|
|
190
|
+
"decoding continues with U+FFFD in place of the invalid bytes; no "
|
|
191
|
+
"node is marked degraded, but content-derived joins and taint can "
|
|
192
|
+
"be silently wrong"
|
|
193
|
+
),
|
|
194
|
+
DegradationReason.UNDECODABLE_BATCH: (
|
|
195
|
+
"the line is skipped; decoding continues with the remaining lines"
|
|
196
|
+
),
|
|
197
|
+
DegradationReason.NON_TRACE_BATCHES_SKIPPED: (
|
|
198
|
+
"the batch is skipped entirely; its spans never enter the trace"
|
|
199
|
+
),
|
|
200
|
+
DegradationReason.UNDECODABLE_SPAN: (
|
|
201
|
+
"the span is discarded; it never becomes a node - absent from the "
|
|
202
|
+
"trace"
|
|
203
|
+
),
|
|
204
|
+
DegradationReason.UNDECODABLE_SCOPE: (
|
|
205
|
+
"the scope is replaced with an empty scope; its spans are still "
|
|
206
|
+
"mapped but lose framework identity"
|
|
207
|
+
),
|
|
208
|
+
DegradationReason.ORPHANED_PARENT: (
|
|
209
|
+
"the parent link is cleared; the node is kept but appears "
|
|
210
|
+
"unlinked, not marked degraded by this alone"
|
|
211
|
+
),
|
|
212
|
+
DegradationReason.DUPLICATE_SPAN_ID: (
|
|
213
|
+
"byte-identical duplicates collapse to one node; differing "
|
|
214
|
+
"duplicates keep a digest-suffixed id and any join touching them "
|
|
215
|
+
"is dropped"
|
|
216
|
+
),
|
|
217
|
+
DegradationReason.MISSING_SPAN_ID: (
|
|
218
|
+
"a digest-derived surrogate id is assigned so the node can still "
|
|
219
|
+
"be created and joined"
|
|
220
|
+
),
|
|
221
|
+
DegradationReason.NONSTANDARD_ID_ENCODING: (
|
|
222
|
+
"processing continues unaffected; linkage still matches on the "
|
|
223
|
+
"raw token"
|
|
224
|
+
),
|
|
225
|
+
DegradationReason.UNMAPPABLE_GENAI_SPAN: (
|
|
226
|
+
"the span is dropped; it never becomes a node - absent from the "
|
|
227
|
+
"trace"
|
|
228
|
+
),
|
|
229
|
+
DegradationReason.NON_GENAI_SPANS_FILTERED: (
|
|
230
|
+
"the span is filtered before mapping and never becomes a node - "
|
|
231
|
+
"expected, not a defect"
|
|
232
|
+
),
|
|
233
|
+
DegradationReason.MISSING_CONTENT: (
|
|
234
|
+
"the node is created with empty payload content and marked "
|
|
235
|
+
"degraded -> excluded from verdict-eligible coverage"
|
|
236
|
+
),
|
|
237
|
+
DegradationReason.UNPARSEABLE_CONTENT: (
|
|
238
|
+
"the node is created with empty payload content and marked "
|
|
239
|
+
"degraded -> excluded from verdict-eligible coverage"
|
|
240
|
+
),
|
|
241
|
+
DegradationReason.TRUNCATED_CONTENT: (
|
|
242
|
+
"the node is created with the truncated payload and marked "
|
|
243
|
+
"degraded -> excluded from verdict-eligible coverage"
|
|
244
|
+
),
|
|
245
|
+
DegradationReason.INVALID_TIMESTAMP: (
|
|
246
|
+
"the node gets a synthetic epoch timestamp, ordering falls back "
|
|
247
|
+
"to span id, and it is marked degraded -> excluded from "
|
|
248
|
+
"verdict-eligible coverage"
|
|
249
|
+
),
|
|
250
|
+
DegradationReason.AMBIGUOUS_JOIN: (
|
|
251
|
+
"no join is recorded for the affected call; without an eligible "
|
|
252
|
+
"join confidence it is excluded from verdict-eligible coverage"
|
|
253
|
+
),
|
|
254
|
+
DegradationReason.CONFLICTING_JOIN_EVIDENCE: (
|
|
255
|
+
"the envelope join is kept per precedence; the disagreement is "
|
|
256
|
+
"only recorded, coverage for that call is unaffected"
|
|
257
|
+
),
|
|
258
|
+
DegradationReason.UNKNOWN_DIALECT: (
|
|
259
|
+
"mapping proceeds under the default profile but every resulting "
|
|
260
|
+
"node is marked degraded -> excluded from verdict-eligible "
|
|
261
|
+
"coverage"
|
|
262
|
+
),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def render_contract_doc() -> str:
|
|
267
|
+
"""The reject-vs-degrade contract as markdown, generated from the enum
|
|
268
|
+
so the committed doc (docs/contract.md) cannot drift from the code."""
|
|
269
|
+
lines = [
|
|
270
|
+
"# The reject-vs-degrade contract",
|
|
271
|
+
"",
|
|
272
|
+
"Exit-2 rejects are exactly two conditions: the input is not JSON at "
|
|
273
|
+
"all (neither a JSON document nor JSON Lines), or the input is JSON "
|
|
274
|
+
"but not OTLP-shaped (no `resourceSpans` list found anywhere). "
|
|
275
|
+
"Everything else degrades, named by one of the rows below.",
|
|
276
|
+
"",
|
|
277
|
+
"| reason | trigger | what weir does | remediation |",
|
|
278
|
+
"| --- | --- | --- | --- |",
|
|
279
|
+
]
|
|
280
|
+
for reason in DegradationReason:
|
|
281
|
+
trigger = TRIGGER[reason].replace("|", "\\|")
|
|
282
|
+
disposition = DISPOSITION[reason].replace("|", "\\|")
|
|
283
|
+
remediation = REMEDIATION[reason].replace("|", "\\|")
|
|
284
|
+
lines.append(
|
|
285
|
+
f"| {reason.value} | {trigger} | {disposition} | {remediation} |"
|
|
286
|
+
)
|
|
287
|
+
return "\n".join(lines) + "\n"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class Degradation(msgspec.Struct, frozen=True):
|
|
291
|
+
"""One ledger entry. `subject` names what it applies to: a span id wire
|
|
292
|
+
token, `resource`, or `line:<n>` for JSONL batches. NOT part of
|
|
293
|
+
CanonicalTrace - the ledger is adapter commentary about one ingestion."""
|
|
294
|
+
|
|
295
|
+
reason: DegradationReason
|
|
296
|
+
subject: str
|
|
297
|
+
note: str = ""
|