spanforge 1.0.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.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
spanforge/compliance.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Public compliance facade for SpanForge.
|
|
2
|
+
|
|
3
|
+
This module exposes the stable ``spanforge.compliance`` API referenced by the
|
|
4
|
+
CLI and documentation. It provides lightweight compatibility and isolation
|
|
5
|
+
checks while re-exporting the richer compliance evidence engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from spanforge.core.compliance_mapping import (
|
|
15
|
+
ClauseStatus,
|
|
16
|
+
ComplianceAttestation,
|
|
17
|
+
ComplianceEvidencePackage,
|
|
18
|
+
ComplianceFramework,
|
|
19
|
+
ComplianceMappingEngine,
|
|
20
|
+
EvidenceRecord,
|
|
21
|
+
GapReport,
|
|
22
|
+
verify_attestation_signature,
|
|
23
|
+
verify_pdf_attestation,
|
|
24
|
+
)
|
|
25
|
+
from spanforge.event import _SOURCE_PATTERN
|
|
26
|
+
from spanforge.signing import ChainVerificationResult, verify_chain
|
|
27
|
+
from spanforge.types import is_registered, validate_custom
|
|
28
|
+
from spanforge.ulid import validate as validate_ulid
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
|
|
33
|
+
from spanforge.event import Event
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"ChainIntegrityResult",
|
|
37
|
+
"ChainIntegrityViolation",
|
|
38
|
+
"ClauseStatus",
|
|
39
|
+
"CompatibilityResult",
|
|
40
|
+
"CompatibilityViolation",
|
|
41
|
+
"ComplianceAttestation",
|
|
42
|
+
"ComplianceEvidencePackage",
|
|
43
|
+
"ComplianceFramework",
|
|
44
|
+
"ComplianceMappingEngine",
|
|
45
|
+
"EvidenceRecord",
|
|
46
|
+
"GapReport",
|
|
47
|
+
"IsolationResult",
|
|
48
|
+
"IsolationViolation",
|
|
49
|
+
"test_compatibility",
|
|
50
|
+
"verify_attestation_signature",
|
|
51
|
+
"verify_chain_integrity",
|
|
52
|
+
"verify_events_scoped",
|
|
53
|
+
"verify_pdf_attestation",
|
|
54
|
+
"verify_tenant_isolation",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class CompatibilityViolation:
|
|
60
|
+
"""A single compatibility non-conformance."""
|
|
61
|
+
|
|
62
|
+
check_id: str
|
|
63
|
+
rule: str
|
|
64
|
+
detail: str
|
|
65
|
+
event_id: str | None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class CompatibilityResult:
|
|
70
|
+
"""Result of running the compatibility checklist."""
|
|
71
|
+
|
|
72
|
+
passed: bool
|
|
73
|
+
events_checked: int
|
|
74
|
+
violations: list[CompatibilityViolation]
|
|
75
|
+
|
|
76
|
+
def __bool__(self) -> bool:
|
|
77
|
+
return self.passed
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ChainIntegrityViolation:
|
|
82
|
+
"""A single audit-chain integrity issue."""
|
|
83
|
+
|
|
84
|
+
kind: str
|
|
85
|
+
detail: str
|
|
86
|
+
event_id: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ChainIntegrityResult:
|
|
91
|
+
"""Result of audit-chain integrity verification."""
|
|
92
|
+
|
|
93
|
+
passed: bool
|
|
94
|
+
chain_result: ChainVerificationResult
|
|
95
|
+
violations: list[ChainIntegrityViolation]
|
|
96
|
+
events_verified: int
|
|
97
|
+
gaps_detected: int
|
|
98
|
+
|
|
99
|
+
def __bool__(self) -> bool:
|
|
100
|
+
return self.passed
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class IsolationViolation:
|
|
105
|
+
"""A single tenant-scoping violation."""
|
|
106
|
+
|
|
107
|
+
detail: str
|
|
108
|
+
event_id: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class IsolationResult:
|
|
113
|
+
"""Result of tenant/isolation checks."""
|
|
114
|
+
|
|
115
|
+
passed: bool
|
|
116
|
+
violations: list[IsolationViolation]
|
|
117
|
+
|
|
118
|
+
def __bool__(self) -> bool:
|
|
119
|
+
return self.passed
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_compatibility(events: Sequence[Event]) -> CompatibilityResult:
|
|
123
|
+
"""Apply the public compatibility checklist to a batch of events."""
|
|
124
|
+
violations: list[CompatibilityViolation] = []
|
|
125
|
+
|
|
126
|
+
for event in events:
|
|
127
|
+
if not getattr(event, "schema_version", ""):
|
|
128
|
+
violations.append(
|
|
129
|
+
CompatibilityViolation(
|
|
130
|
+
check_id="CHK-1",
|
|
131
|
+
rule="required fields present",
|
|
132
|
+
detail="schema_version must be present and non-empty",
|
|
133
|
+
event_id=getattr(event, "event_id", None),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
if not getattr(event, "source", ""):
|
|
137
|
+
violations.append(
|
|
138
|
+
CompatibilityViolation(
|
|
139
|
+
check_id="CHK-1",
|
|
140
|
+
rule="required fields present",
|
|
141
|
+
detail="source must be present and non-empty",
|
|
142
|
+
event_id=getattr(event, "event_id", None),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
payload = getattr(event, "payload", None)
|
|
146
|
+
if not isinstance(payload, (dict, MappingProxyType)) or not payload:
|
|
147
|
+
violations.append(
|
|
148
|
+
CompatibilityViolation(
|
|
149
|
+
check_id="CHK-1",
|
|
150
|
+
rule="required fields present",
|
|
151
|
+
detail="payload must be a non-empty dict",
|
|
152
|
+
event_id=getattr(event, "event_id", None),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
event_type = str(getattr(event, "event_type", ""))
|
|
157
|
+
if not event_type:
|
|
158
|
+
violations.append(
|
|
159
|
+
CompatibilityViolation(
|
|
160
|
+
check_id="CHK-2",
|
|
161
|
+
rule="event_type namespace validity",
|
|
162
|
+
detail="event_type must be present and non-empty",
|
|
163
|
+
event_id=getattr(event, "event_id", None),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
elif not is_registered(event_type):
|
|
167
|
+
try:
|
|
168
|
+
validate_custom(event_type)
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
violations.append(
|
|
171
|
+
CompatibilityViolation(
|
|
172
|
+
check_id="CHK-2",
|
|
173
|
+
rule="event_type namespace validity",
|
|
174
|
+
detail=str(exc),
|
|
175
|
+
event_id=getattr(event, "event_id", None),
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
source = str(getattr(event, "source", ""))
|
|
180
|
+
if source and not _SOURCE_PATTERN.match(source):
|
|
181
|
+
violations.append(
|
|
182
|
+
CompatibilityViolation(
|
|
183
|
+
check_id="CHK-3",
|
|
184
|
+
rule="source format",
|
|
185
|
+
detail="source must match <service>@<semver>",
|
|
186
|
+
event_id=getattr(event, "event_id", None),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
event_id = str(getattr(event, "event_id", ""))
|
|
191
|
+
if event_id and not validate_ulid(event_id):
|
|
192
|
+
violations.append(
|
|
193
|
+
CompatibilityViolation(
|
|
194
|
+
check_id="CHK-5",
|
|
195
|
+
rule="event_id is a valid ULID",
|
|
196
|
+
detail="event_id must be a valid 26-character ULID",
|
|
197
|
+
event_id=event_id,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return CompatibilityResult(
|
|
202
|
+
passed=not violations,
|
|
203
|
+
events_checked=len(events),
|
|
204
|
+
violations=violations,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
setattr(test_compatibility, "__test__", False) # noqa: B010
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def verify_chain_integrity(
|
|
212
|
+
events: Sequence[Event],
|
|
213
|
+
org_secret: str,
|
|
214
|
+
*,
|
|
215
|
+
check_monotonic_timestamps: bool = True,
|
|
216
|
+
) -> ChainIntegrityResult:
|
|
217
|
+
"""Verify an ordered event chain for gaps, tampering, and timestamp regressions."""
|
|
218
|
+
chain_result = verify_chain(events, org_secret)
|
|
219
|
+
violations: list[ChainIntegrityViolation] = []
|
|
220
|
+
|
|
221
|
+
if chain_result.first_tampered is not None:
|
|
222
|
+
violations.append(
|
|
223
|
+
ChainIntegrityViolation(
|
|
224
|
+
kind="tampered_signature",
|
|
225
|
+
detail="one or more event signatures failed verification",
|
|
226
|
+
event_id=chain_result.first_tampered,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
violations.extend(
|
|
231
|
+
ChainIntegrityViolation(
|
|
232
|
+
kind="broken_prev_id_link",
|
|
233
|
+
detail="prev_id chain linkage is broken",
|
|
234
|
+
event_id=event_id,
|
|
235
|
+
)
|
|
236
|
+
for event_id in chain_result.gaps
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if check_monotonic_timestamps:
|
|
240
|
+
previous: str | None = None
|
|
241
|
+
previous_event_id: str | None = None
|
|
242
|
+
for event in events:
|
|
243
|
+
current = getattr(event, "timestamp", None)
|
|
244
|
+
if previous is not None and current is not None and str(current) < str(previous):
|
|
245
|
+
violations.append(
|
|
246
|
+
ChainIntegrityViolation(
|
|
247
|
+
kind="non_monotonic_timestamp",
|
|
248
|
+
detail="timestamps must be monotonically non-decreasing",
|
|
249
|
+
event_id=getattr(event, "event_id", previous_event_id),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
break
|
|
253
|
+
previous = str(current) if current is not None else None
|
|
254
|
+
previous_event_id = getattr(event, "event_id", None)
|
|
255
|
+
|
|
256
|
+
return ChainIntegrityResult(
|
|
257
|
+
passed=not violations,
|
|
258
|
+
chain_result=chain_result,
|
|
259
|
+
violations=violations,
|
|
260
|
+
events_verified=len(events),
|
|
261
|
+
gaps_detected=len(chain_result.gaps),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def verify_tenant_isolation(
|
|
266
|
+
group_a: Sequence[Event],
|
|
267
|
+
group_b: Sequence[Event],
|
|
268
|
+
*,
|
|
269
|
+
strict: bool = False,
|
|
270
|
+
) -> IsolationResult:
|
|
271
|
+
"""Verify that two event batches are scoped to separate tenants."""
|
|
272
|
+
violations: list[IsolationViolation] = []
|
|
273
|
+
orgs_a = {event.org_id for event in group_a if event.org_id}
|
|
274
|
+
orgs_b = {event.org_id for event in group_b if event.org_id}
|
|
275
|
+
overlap = sorted(orgs_a & orgs_b)
|
|
276
|
+
|
|
277
|
+
if overlap:
|
|
278
|
+
violations.append(
|
|
279
|
+
IsolationViolation(
|
|
280
|
+
detail=f"tenant groups share org_id values: {', '.join(overlap)}",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if strict:
|
|
285
|
+
violations.extend(
|
|
286
|
+
IsolationViolation(
|
|
287
|
+
detail="strict tenant isolation requires org_id on every event",
|
|
288
|
+
event_id=event.event_id,
|
|
289
|
+
)
|
|
290
|
+
for event in list(group_a) + list(group_b)
|
|
291
|
+
if not event.org_id
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return IsolationResult(passed=not violations, violations=violations)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def verify_events_scoped(
|
|
298
|
+
events: Sequence[Event],
|
|
299
|
+
*,
|
|
300
|
+
expected_org_id: str | None = None,
|
|
301
|
+
expected_team_id: str | None = None,
|
|
302
|
+
) -> IsolationResult:
|
|
303
|
+
"""Verify that events carry the expected tenant scope values."""
|
|
304
|
+
violations: list[IsolationViolation] = []
|
|
305
|
+
for event in events:
|
|
306
|
+
if expected_org_id is not None and event.org_id != expected_org_id:
|
|
307
|
+
violations.append(
|
|
308
|
+
IsolationViolation(
|
|
309
|
+
detail=(
|
|
310
|
+
f"expected org_id={expected_org_id!r}, found {event.org_id!r}"
|
|
311
|
+
),
|
|
312
|
+
event_id=event.event_id,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
if expected_team_id is not None and event.team_id != expected_team_id:
|
|
316
|
+
violations.append(
|
|
317
|
+
IsolationViolation(
|
|
318
|
+
detail=(
|
|
319
|
+
f"expected team_id={expected_team_id!r}, found {event.team_id!r}"
|
|
320
|
+
),
|
|
321
|
+
event_id=event.event_id,
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return IsolationResult(passed=not violations, violations=violations)
|