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
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
"""spanforge.testing_mocks — Mock service clients for testing (DX-003).
|
|
2
|
+
|
|
3
|
+
Provides drop-in mock replacements for every SpanForge SDK service client.
|
|
4
|
+
All mocks operate purely in-memory with no network calls, making them ideal
|
|
5
|
+
for unit tests.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from spanforge.testing_mocks import mock_all_services
|
|
10
|
+
|
|
11
|
+
with mock_all_services():
|
|
12
|
+
from spanforge.sdk import sf_pii, sf_audit
|
|
13
|
+
result = sf_pii.scan({"msg": "Call 555-867-5309"})
|
|
14
|
+
assert result.clean # mock returns clean by default
|
|
15
|
+
|
|
16
|
+
Each mock class records all calls for assertion. Use ``.calls`` to inspect
|
|
17
|
+
what was called, or ``.configure_response()`` to override default returns.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import threading
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
27
|
+
|
|
28
|
+
from spanforge.sdk._types import (
|
|
29
|
+
AirGapConfig,
|
|
30
|
+
AlertRecord,
|
|
31
|
+
AlertStatusInfo,
|
|
32
|
+
Annotation,
|
|
33
|
+
APIKeyBundle,
|
|
34
|
+
AuditAppendResult,
|
|
35
|
+
AuditStatusInfo,
|
|
36
|
+
BundleResult,
|
|
37
|
+
BundleVerificationResult,
|
|
38
|
+
CECStatusInfo,
|
|
39
|
+
DPADocument,
|
|
40
|
+
DSARExport,
|
|
41
|
+
EncryptionConfig,
|
|
42
|
+
EnterpriseStatusInfo,
|
|
43
|
+
ErasureReceipt,
|
|
44
|
+
ExportResult,
|
|
45
|
+
GateArtifact,
|
|
46
|
+
GateEvaluationResult,
|
|
47
|
+
GateStatusInfo,
|
|
48
|
+
HealthEndpointResult,
|
|
49
|
+
IsolationScope,
|
|
50
|
+
JWTClaims,
|
|
51
|
+
MagicLinkResult,
|
|
52
|
+
ObserveStatusInfo,
|
|
53
|
+
PIIAnonymisedResult,
|
|
54
|
+
PIIHeatMapEntry,
|
|
55
|
+
PIIPipelineResult,
|
|
56
|
+
PIIStatusInfo,
|
|
57
|
+
PIITextScanResult,
|
|
58
|
+
PRRIResult,
|
|
59
|
+
PublishResult,
|
|
60
|
+
RateLimitInfo,
|
|
61
|
+
SafeHarborResult,
|
|
62
|
+
SecretStr,
|
|
63
|
+
SecurityAuditResult,
|
|
64
|
+
SecurityScanResult,
|
|
65
|
+
SFPIIAnonymizeResult,
|
|
66
|
+
SFPIIRedactResult,
|
|
67
|
+
SFPIIScanResult,
|
|
68
|
+
SignedRecord,
|
|
69
|
+
TenantConfig,
|
|
70
|
+
ThreatModelEntry,
|
|
71
|
+
TokenIntrospectionResult,
|
|
72
|
+
TOTPEnrollResult,
|
|
73
|
+
TrustBadgeResult,
|
|
74
|
+
TrustDimension,
|
|
75
|
+
TrustDimensionWeights,
|
|
76
|
+
TrustGateResult,
|
|
77
|
+
TrustHistoryEntry,
|
|
78
|
+
TrustScorecardResponse,
|
|
79
|
+
TrustStatusInfo,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if TYPE_CHECKING:
|
|
83
|
+
from collections.abc import Generator
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
"MockSFAlert",
|
|
87
|
+
"MockSFAudit",
|
|
88
|
+
"MockSFCEC",
|
|
89
|
+
"MockSFEnterprise",
|
|
90
|
+
"MockSFGate",
|
|
91
|
+
"MockSFIdentity",
|
|
92
|
+
"MockSFObserve",
|
|
93
|
+
"MockSFPII",
|
|
94
|
+
"MockSFSecrets",
|
|
95
|
+
"MockSFSecurity",
|
|
96
|
+
"MockSFTrust",
|
|
97
|
+
"mock_all_services",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
_T = TypeVar("_T")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Call recorder mixin
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class _MockCall:
|
|
110
|
+
"""Record of a single method invocation."""
|
|
111
|
+
|
|
112
|
+
method: str
|
|
113
|
+
args: tuple[Any, ...]
|
|
114
|
+
kwargs: dict[str, Any]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _MockBase:
|
|
118
|
+
"""Base class for all mock service clients.
|
|
119
|
+
|
|
120
|
+
Provides call recording and response overriding.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self) -> None:
|
|
124
|
+
self.calls: list[_MockCall] = []
|
|
125
|
+
self._responses: dict[str, Any] = {}
|
|
126
|
+
self._lock = threading.Lock()
|
|
127
|
+
|
|
128
|
+
def _record(self, method: str, *args: Any, **kwargs: Any) -> None:
|
|
129
|
+
with self._lock:
|
|
130
|
+
self.calls.append(_MockCall(method=method, args=args, kwargs=kwargs))
|
|
131
|
+
|
|
132
|
+
def configure_response(self, method: str, response: Any) -> None:
|
|
133
|
+
"""Set a custom return value for *method*."""
|
|
134
|
+
self._responses[method] = response
|
|
135
|
+
|
|
136
|
+
def _get_response(self, method: str, default: _T) -> _T:
|
|
137
|
+
response = self._responses.get(method, default)
|
|
138
|
+
return response if isinstance(response, type(default)) else default
|
|
139
|
+
|
|
140
|
+
def reset(self) -> None:
|
|
141
|
+
"""Clear all recorded calls and configured responses."""
|
|
142
|
+
with self._lock:
|
|
143
|
+
self.calls.clear()
|
|
144
|
+
self._responses.clear()
|
|
145
|
+
|
|
146
|
+
def assert_called(self, method: str) -> None:
|
|
147
|
+
"""Assert that *method* was called at least once."""
|
|
148
|
+
if not any(c.method == method for c in self.calls):
|
|
149
|
+
raise AssertionError(f"{type(self).__name__}.{method}() was never called")
|
|
150
|
+
|
|
151
|
+
def assert_not_called(self, method: str) -> None:
|
|
152
|
+
"""Assert that *method* was never called."""
|
|
153
|
+
if any(c.method == method for c in self.calls):
|
|
154
|
+
raise AssertionError(
|
|
155
|
+
f"{type(self).__name__}.{method}() was called "
|
|
156
|
+
f"{sum(1 for c in self.calls if c.method == method)} time(s)"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def call_count(self, method: str) -> int:
|
|
160
|
+
"""Return the number of times *method* was called."""
|
|
161
|
+
return sum(1 for c in self.calls if c.method == method)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _now_iso() -> str:
|
|
165
|
+
return datetime.now(timezone.utc).isoformat()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# MockSFIdentity
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MockSFIdentity(_MockBase):
|
|
174
|
+
"""Mock replacement for :class:`~spanforge.sdk.identity.SFIdentityClient`."""
|
|
175
|
+
|
|
176
|
+
def issue_api_key(self, **kwargs: Any) -> APIKeyBundle:
|
|
177
|
+
self._record("issue_api_key", **kwargs)
|
|
178
|
+
return self._get_response(
|
|
179
|
+
"issue_api_key",
|
|
180
|
+
APIKeyBundle(
|
|
181
|
+
api_key=SecretStr("sf_test_mock_key_000000000000"),
|
|
182
|
+
key_id="mock-key-id",
|
|
183
|
+
jwt="mock.jwt.token",
|
|
184
|
+
expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc),
|
|
185
|
+
scopes=kwargs.get("scopes", []),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def refresh_token(self) -> str:
|
|
190
|
+
self._record("refresh_token")
|
|
191
|
+
return self._get_response("refresh_token", "mock.refreshed.jwt")
|
|
192
|
+
|
|
193
|
+
def create_session(self, api_key: str) -> str:
|
|
194
|
+
self._record("create_session", api_key)
|
|
195
|
+
return self._get_response("create_session", "mock.session.jwt")
|
|
196
|
+
|
|
197
|
+
def verify_token(self, jwt: str) -> JWTClaims:
|
|
198
|
+
self._record("verify_token", jwt)
|
|
199
|
+
return self._get_response(
|
|
200
|
+
"verify_token",
|
|
201
|
+
JWTClaims(
|
|
202
|
+
subject="mock-subject",
|
|
203
|
+
scopes=["*"],
|
|
204
|
+
project_id="mock-project",
|
|
205
|
+
expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc),
|
|
206
|
+
issued_at=datetime.now(timezone.utc),
|
|
207
|
+
jti="mock-jti",
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def introspect(self, token: str) -> TokenIntrospectionResult:
|
|
212
|
+
self._record("introspect", token)
|
|
213
|
+
return self._get_response(
|
|
214
|
+
"introspect", TokenIntrospectionResult(active=True, scope="*", sub="mock-subject")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def rotate_key(self, key_id: str) -> APIKeyBundle:
|
|
218
|
+
self._record("rotate_key", key_id)
|
|
219
|
+
return self.issue_api_key()
|
|
220
|
+
|
|
221
|
+
def revoke_key(self, key_id: str) -> None:
|
|
222
|
+
self._record("revoke_key", key_id)
|
|
223
|
+
|
|
224
|
+
def issue_magic_link(self, email: str) -> MagicLinkResult:
|
|
225
|
+
self._record("issue_magic_link", email)
|
|
226
|
+
return self._get_response(
|
|
227
|
+
"issue_magic_link",
|
|
228
|
+
MagicLinkResult(
|
|
229
|
+
link_id="mock-magic-link", expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc)
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def enroll_totp(self, key_id: str) -> TOTPEnrollResult:
|
|
234
|
+
self._record("enroll_totp", key_id)
|
|
235
|
+
return self._get_response(
|
|
236
|
+
"enroll_totp",
|
|
237
|
+
TOTPEnrollResult(
|
|
238
|
+
secret_base32=SecretStr("MOCKSECRET"),
|
|
239
|
+
qr_uri="otpauth://totp/mock",
|
|
240
|
+
backup_codes=["000000"],
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def verify_backup_code(self, key_id: str, code: str) -> bool:
|
|
245
|
+
self._record("verify_backup_code", key_id, code)
|
|
246
|
+
return self._get_response("verify_backup_code", True)
|
|
247
|
+
|
|
248
|
+
def check_rate_limit(self, key_id: str) -> RateLimitInfo:
|
|
249
|
+
self._record("check_rate_limit", key_id)
|
|
250
|
+
return self._get_response(
|
|
251
|
+
"check_rate_limit",
|
|
252
|
+
RateLimitInfo(limit=600, remaining=599, reset_at=datetime.now(timezone.utc)),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def record_request(self, key_id: str) -> bool:
|
|
256
|
+
self._record("record_request", key_id)
|
|
257
|
+
return self._get_response("record_request", True)
|
|
258
|
+
|
|
259
|
+
def require_scope(self, claims: Any, scope: str) -> None:
|
|
260
|
+
self._record("require_scope", claims, scope)
|
|
261
|
+
|
|
262
|
+
def get_jwks(self) -> dict[str, Any]:
|
|
263
|
+
self._record("get_jwks")
|
|
264
|
+
return self._get_response("get_jwks", {"keys": []})
|
|
265
|
+
|
|
266
|
+
def check_ip_allowlist(self, key_id: str, ip: str) -> None:
|
|
267
|
+
self._record("check_ip_allowlist", key_id, ip)
|
|
268
|
+
|
|
269
|
+
def saml_metadata(self) -> str:
|
|
270
|
+
self._record("saml_metadata")
|
|
271
|
+
return self._get_response("saml_metadata", "<mock-saml/>")
|
|
272
|
+
|
|
273
|
+
def saml_acs(self, saml_response: str) -> dict[str, Any]: # F-03
|
|
274
|
+
self._record("saml_acs", saml_response)
|
|
275
|
+
return self._get_response(
|
|
276
|
+
"saml_acs", {"subject": "mock-subject", "session": "mock-session"}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def validate_api_key(self, jwt: str) -> Any: # F-02 alias
|
|
280
|
+
return self.verify_token(jwt)
|
|
281
|
+
|
|
282
|
+
def get_status(self) -> dict[str, Any]: # F-02
|
|
283
|
+
self._record("get_status")
|
|
284
|
+
return self._get_response(
|
|
285
|
+
"get_status", {"status": "ok", "mode": "local", "keys_issued": 0, "active_sessions": 0}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def set_mfa_policy(self, project_id: str, mfa_required: bool) -> None:
|
|
289
|
+
self._record("set_mfa_policy", project_id, mfa_required)
|
|
290
|
+
|
|
291
|
+
def get_mfa_policy(self, project_id: str) -> bool:
|
|
292
|
+
self._record("get_mfa_policy", project_id)
|
|
293
|
+
return self._get_response("get_mfa_policy", False)
|
|
294
|
+
|
|
295
|
+
def set_key_tier(self, key_id: str, tier: str) -> None:
|
|
296
|
+
self._record("set_key_tier", key_id, tier)
|
|
297
|
+
|
|
298
|
+
def consume_quota(self, key_id: str) -> bool:
|
|
299
|
+
self._record("consume_quota", key_id)
|
|
300
|
+
return self._get_response("consume_quota", True)
|
|
301
|
+
|
|
302
|
+
def get_quota_usage(self, key_id: str) -> dict[str, Any]:
|
|
303
|
+
self._record("get_quota_usage", key_id)
|
|
304
|
+
return self._get_response("get_quota_usage", {"used": 0, "limit": 1000})
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# MockSFPII
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class MockSFPII(_MockBase):
|
|
313
|
+
"""Mock replacement for :class:`~spanforge.sdk.pii.SFPIIClient`."""
|
|
314
|
+
|
|
315
|
+
def scan(self, payload: dict[str, Any], **kwargs: Any) -> SFPIIScanResult:
|
|
316
|
+
self._record("scan", payload, **kwargs)
|
|
317
|
+
return self._get_response("scan", SFPIIScanResult(hits=[], scanned=1))
|
|
318
|
+
|
|
319
|
+
def redact(self, event: Any, **kwargs: Any) -> SFPIIRedactResult:
|
|
320
|
+
self._record("redact", event, **kwargs)
|
|
321
|
+
return self._get_response(
|
|
322
|
+
"redact",
|
|
323
|
+
SFPIIRedactResult(
|
|
324
|
+
event=event, redaction_count=0, redacted_at=_now_iso(), redacted_by="mock"
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def contains_pii(self, event: Any, **kwargs: Any) -> bool:
|
|
329
|
+
self._record("contains_pii", event, **kwargs)
|
|
330
|
+
return self._get_response("contains_pii", False)
|
|
331
|
+
|
|
332
|
+
def assert_redacted(self, event: Any, **kwargs: Any) -> None:
|
|
333
|
+
self._record("assert_redacted", event, **kwargs)
|
|
334
|
+
|
|
335
|
+
def anonymize(self, text: str, **kwargs: Any) -> SFPIIAnonymizeResult:
|
|
336
|
+
self._record("anonymize", text, **kwargs)
|
|
337
|
+
return self._get_response(
|
|
338
|
+
"anonymize", SFPIIAnonymizeResult(text=text, replacements=0, pii_types_found=[])
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def scan_text(self, text: str, **kwargs: Any) -> PIITextScanResult:
|
|
342
|
+
self._record("scan_text", text, **kwargs)
|
|
343
|
+
return self._get_response(
|
|
344
|
+
"scan_text", PIITextScanResult(entities=[], redacted_text=text, detected=False)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def anonymise(self, payload: dict[str, Any], **kwargs: Any) -> PIIAnonymisedResult:
|
|
348
|
+
self._record("anonymise", payload, **kwargs)
|
|
349
|
+
return self._get_response(
|
|
350
|
+
"anonymise", PIIAnonymisedResult(clean_payload=payload, redaction_manifest=[])
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def scan_batch(self, texts: list[str], **kwargs: Any) -> list[PIITextScanResult]:
|
|
354
|
+
self._record("scan_batch", texts, **kwargs)
|
|
355
|
+
return self._get_response(
|
|
356
|
+
"scan_batch",
|
|
357
|
+
[PIITextScanResult(entities=[], redacted_text=t, detected=False) for t in texts],
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def apply_pipeline_action(self, text: str, **kwargs: Any) -> PIIPipelineResult:
|
|
361
|
+
self._record("apply_pipeline_action", text, **kwargs)
|
|
362
|
+
return self._get_response(
|
|
363
|
+
"apply_pipeline_action",
|
|
364
|
+
PIIPipelineResult(
|
|
365
|
+
text=text,
|
|
366
|
+
action="flag",
|
|
367
|
+
detected=False,
|
|
368
|
+
entity_types=[],
|
|
369
|
+
low_confidence_hits=[],
|
|
370
|
+
redacted_text=text,
|
|
371
|
+
blocked=False,
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def get_status(self) -> PIIStatusInfo:
|
|
376
|
+
self._record("get_status")
|
|
377
|
+
return self._get_response(
|
|
378
|
+
"get_status",
|
|
379
|
+
PIIStatusInfo(
|
|
380
|
+
status="ok", presidio_available=False, entity_types_loaded=[], last_scan_at=None
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def erase_subject(self, subject_id: str, project_id: str) -> ErasureReceipt:
|
|
385
|
+
self._record("erase_subject", subject_id, project_id)
|
|
386
|
+
return self._get_response(
|
|
387
|
+
"erase_subject",
|
|
388
|
+
ErasureReceipt(
|
|
389
|
+
subject_id=subject_id,
|
|
390
|
+
project_id=project_id,
|
|
391
|
+
records_erased=0,
|
|
392
|
+
erasure_id="mock-erasure",
|
|
393
|
+
erased_at=_now_iso(),
|
|
394
|
+
exceptions=[],
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def export_subject_data(self, subject_id: str, project_id: str) -> DSARExport:
|
|
399
|
+
self._record("export_subject_data", subject_id, project_id)
|
|
400
|
+
return self._get_response(
|
|
401
|
+
"export_subject_data",
|
|
402
|
+
DSARExport(
|
|
403
|
+
subject_id=subject_id,
|
|
404
|
+
project_id=project_id,
|
|
405
|
+
event_count=0,
|
|
406
|
+
export_id="mock-export",
|
|
407
|
+
exported_at=_now_iso(),
|
|
408
|
+
events=[],
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def safe_harbor_deidentify(self, text: str) -> SafeHarborResult:
|
|
413
|
+
self._record("safe_harbor_deidentify", text)
|
|
414
|
+
return self._get_response(
|
|
415
|
+
"safe_harbor_deidentify",
|
|
416
|
+
SafeHarborResult(text=text, replacements=0, phi_types_found=[]),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def get_pii_stats(self, project_id: str, **kwargs: Any) -> list[PIIHeatMapEntry]:
|
|
420
|
+
self._record("get_pii_stats", project_id, **kwargs)
|
|
421
|
+
return self._get_response("get_pii_stats", [])
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# MockSFSecrets
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class MockSFSecrets(_MockBase):
|
|
430
|
+
"""Mock replacement for :class:`~spanforge.sdk.secrets.SFSecretsClient`."""
|
|
431
|
+
|
|
432
|
+
def scan(self, text: str, **kwargs: Any) -> Any:
|
|
433
|
+
self._record("scan", text, **kwargs)
|
|
434
|
+
return self._get_response("scan", None)
|
|
435
|
+
|
|
436
|
+
def scan_batch(self, texts: list[str], **kwargs: Any) -> list[Any]:
|
|
437
|
+
self._record("scan_batch", texts, **kwargs)
|
|
438
|
+
return self._get_response("scan_batch", [])
|
|
439
|
+
|
|
440
|
+
def get_status(self) -> dict[str, Any]:
|
|
441
|
+
self._record("get_status")
|
|
442
|
+
return self._get_response("get_status", {"status": "ok", "patterns_loaded": 0})
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
# MockSFAudit
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class MockSFAudit(_MockBase):
|
|
451
|
+
"""Mock replacement for :class:`~spanforge.sdk.audit.SFAuditClient`."""
|
|
452
|
+
|
|
453
|
+
def append(self, record: dict[str, Any], schema_key: str, **kwargs: Any) -> AuditAppendResult:
|
|
454
|
+
self._record("append", record, schema_key, **kwargs)
|
|
455
|
+
return self._get_response(
|
|
456
|
+
"append",
|
|
457
|
+
AuditAppendResult(
|
|
458
|
+
record_id="mock-record-id",
|
|
459
|
+
chain_position=0,
|
|
460
|
+
timestamp=_now_iso(),
|
|
461
|
+
hmac="mock-hmac",
|
|
462
|
+
schema_key=schema_key,
|
|
463
|
+
backend="mock",
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def sign(self, record: dict[str, Any]) -> SignedRecord:
|
|
468
|
+
self._record("sign", record)
|
|
469
|
+
return self._get_response(
|
|
470
|
+
"sign",
|
|
471
|
+
SignedRecord(
|
|
472
|
+
record=record,
|
|
473
|
+
record_id="mock-id",
|
|
474
|
+
checksum="mock-cksum",
|
|
475
|
+
signature="mock-sig",
|
|
476
|
+
timestamp=_now_iso(),
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def verify_chain(self, records: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]:
|
|
481
|
+
self._record("verify_chain", records, **kwargs)
|
|
482
|
+
return self._get_response("verify_chain", {"valid": True, "gaps": []})
|
|
483
|
+
|
|
484
|
+
def export(self, schema_key: str | None = None, **kwargs: Any) -> list[dict[str, Any]]:
|
|
485
|
+
self._record("export", schema_key, **kwargs)
|
|
486
|
+
return self._get_response("export", [])
|
|
487
|
+
|
|
488
|
+
def get_trust_scorecard(self, project_id: str | None = None, **kwargs: Any) -> Any:
|
|
489
|
+
self._record("get_trust_scorecard", project_id, **kwargs)
|
|
490
|
+
return self._get_response("get_trust_scorecard", None)
|
|
491
|
+
|
|
492
|
+
def get_status(self) -> AuditStatusInfo:
|
|
493
|
+
self._record("get_status")
|
|
494
|
+
return self._get_response(
|
|
495
|
+
"get_status",
|
|
496
|
+
AuditStatusInfo(
|
|
497
|
+
status="ok",
|
|
498
|
+
backend="mock",
|
|
499
|
+
byos_enabled=False,
|
|
500
|
+
record_count=0,
|
|
501
|
+
last_append_at=None,
|
|
502
|
+
schema_count=0,
|
|
503
|
+
index_healthy=True,
|
|
504
|
+
retention_years=7,
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def close(self) -> None:
|
|
509
|
+
self._record("close")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
# MockSFObserve
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class MockSFObserve(_MockBase):
|
|
518
|
+
"""Mock replacement for :class:`~spanforge.sdk.observe.SFObserveClient`."""
|
|
519
|
+
|
|
520
|
+
def export_spans(self, spans: list[dict[str, Any]], **kwargs: Any) -> ExportResult:
|
|
521
|
+
self._record("export_spans", spans, **kwargs)
|
|
522
|
+
return self._get_response(
|
|
523
|
+
"export_spans",
|
|
524
|
+
ExportResult(
|
|
525
|
+
exported_count=len(spans), failed_count=0, backend="mock", exported_at=_now_iso()
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def emit_span(self, name: str, attributes: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
|
|
530
|
+
self._record("emit_span", name, attributes, **kwargs)
|
|
531
|
+
return self._get_response("emit_span", {"span_id": "mock-span-id"})
|
|
532
|
+
|
|
533
|
+
def add_annotation(self, event_type: str, payload: dict[str, Any], **kwargs: Any) -> str:
|
|
534
|
+
self._record("add_annotation", event_type, payload, **kwargs)
|
|
535
|
+
return self._get_response("add_annotation", "mock-annotation-id")
|
|
536
|
+
|
|
537
|
+
def get_annotations(
|
|
538
|
+
self, event_type: str, from_dt: str, to_dt: str, **kwargs: Any
|
|
539
|
+
) -> list[Annotation]:
|
|
540
|
+
self._record("get_annotations", event_type, from_dt, to_dt, **kwargs)
|
|
541
|
+
return self._get_response("get_annotations", [])
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def healthy(self) -> bool:
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def last_export_at(self) -> str | None:
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def get_status(self) -> ObserveStatusInfo:
|
|
552
|
+
self._record("get_status")
|
|
553
|
+
return self._get_response(
|
|
554
|
+
"get_status",
|
|
555
|
+
ObserveStatusInfo(
|
|
556
|
+
status="ok",
|
|
557
|
+
backend="mock",
|
|
558
|
+
sampler_strategy="always_on",
|
|
559
|
+
span_count=0,
|
|
560
|
+
annotation_count=0,
|
|
561
|
+
export_count=0,
|
|
562
|
+
last_export_at=None,
|
|
563
|
+
healthy=True,
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# ---------------------------------------------------------------------------
|
|
569
|
+
# MockSFGate
|
|
570
|
+
# ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class MockSFGate(_MockBase):
|
|
574
|
+
"""Mock replacement for :class:`~spanforge.sdk.gate.SFGateClient`."""
|
|
575
|
+
|
|
576
|
+
def evaluate(
|
|
577
|
+
self, gate_id: str, payload: dict[str, Any], **kwargs: Any
|
|
578
|
+
) -> GateEvaluationResult:
|
|
579
|
+
self._record("evaluate", gate_id, payload, **kwargs)
|
|
580
|
+
return self._get_response(
|
|
581
|
+
"evaluate",
|
|
582
|
+
GateEvaluationResult(
|
|
583
|
+
gate_id=gate_id, verdict="PASS", metrics={}, artifact_url="", duration_ms=1
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def run_trust_gate(self, project_id: str, **kwargs: Any) -> TrustGateResult:
|
|
588
|
+
self._record("run_trust_gate", project_id, **kwargs)
|
|
589
|
+
return self._get_response(
|
|
590
|
+
"run_trust_gate",
|
|
591
|
+
TrustGateResult(
|
|
592
|
+
gate_id="mock-trust-gate",
|
|
593
|
+
verdict="PASS",
|
|
594
|
+
hri_critical_rate=0.0,
|
|
595
|
+
hri_critical_threshold=0.05,
|
|
596
|
+
pii_detected=False,
|
|
597
|
+
pii_detections_24h=0,
|
|
598
|
+
secrets_detected=False,
|
|
599
|
+
secrets_detections_24h=0,
|
|
600
|
+
failures=[],
|
|
601
|
+
timestamp=_now_iso(),
|
|
602
|
+
pipeline_id="",
|
|
603
|
+
project_id=project_id,
|
|
604
|
+
pass_=True,
|
|
605
|
+
),
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def evaluate_prri(self, project_id: str, **kwargs: Any) -> PRRIResult:
|
|
609
|
+
self._record("evaluate_prri", project_id, **kwargs)
|
|
610
|
+
return self._get_response(
|
|
611
|
+
"evaluate_prri",
|
|
612
|
+
PRRIResult(
|
|
613
|
+
gate_id="gate5_governance",
|
|
614
|
+
prri_score=95,
|
|
615
|
+
verdict="GREEN",
|
|
616
|
+
dimension_breakdown={},
|
|
617
|
+
framework="soc2",
|
|
618
|
+
policy_file="",
|
|
619
|
+
timestamp=_now_iso(),
|
|
620
|
+
allow=True,
|
|
621
|
+
project_id=project_id,
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def list_artifacts(self, gate_id: str | None = None, **kwargs: Any) -> list[GateArtifact]:
|
|
626
|
+
self._record("list_artifacts", gate_id, **kwargs)
|
|
627
|
+
return self._get_response("list_artifacts", [])
|
|
628
|
+
|
|
629
|
+
def get_status(self) -> GateStatusInfo:
|
|
630
|
+
self._record("get_status")
|
|
631
|
+
return self._get_response(
|
|
632
|
+
"get_status",
|
|
633
|
+
GateStatusInfo(
|
|
634
|
+
status="ok",
|
|
635
|
+
evaluate_count=0,
|
|
636
|
+
trust_gate_count=0,
|
|
637
|
+
last_evaluate_at=None,
|
|
638
|
+
artifact_count=0,
|
|
639
|
+
artifact_dir="",
|
|
640
|
+
open_circuit_breakers=[],
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ---------------------------------------------------------------------------
|
|
646
|
+
# MockSFCEC
|
|
647
|
+
# ---------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class MockSFCEC(_MockBase):
|
|
651
|
+
"""Mock replacement for :class:`~spanforge.sdk.cec.SFCECClient`."""
|
|
652
|
+
|
|
653
|
+
def build_bundle(
|
|
654
|
+
self, project_id: str, date_range: tuple[str, str], **kwargs: Any
|
|
655
|
+
) -> BundleResult:
|
|
656
|
+
self._record("build_bundle", project_id, date_range, **kwargs)
|
|
657
|
+
return self._get_response(
|
|
658
|
+
"build_bundle",
|
|
659
|
+
BundleResult(
|
|
660
|
+
bundle_id="mock-bundle",
|
|
661
|
+
download_url="",
|
|
662
|
+
expires_at=_now_iso(),
|
|
663
|
+
hmac_manifest="mock-hmac",
|
|
664
|
+
record_counts={},
|
|
665
|
+
zip_path="mock.zip",
|
|
666
|
+
frameworks=[],
|
|
667
|
+
project_id=project_id,
|
|
668
|
+
generated_at=_now_iso(),
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
def verify_bundle(self, zip_path: str) -> BundleVerificationResult:
|
|
673
|
+
self._record("verify_bundle", zip_path)
|
|
674
|
+
return self._get_response(
|
|
675
|
+
"verify_bundle",
|
|
676
|
+
BundleVerificationResult(
|
|
677
|
+
bundle_id="mock-bundle",
|
|
678
|
+
manifest_valid=True,
|
|
679
|
+
chain_valid=True,
|
|
680
|
+
timestamp_valid=True,
|
|
681
|
+
overall_valid=True,
|
|
682
|
+
errors=[],
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def generate_dpa(
|
|
687
|
+
self,
|
|
688
|
+
project_id: str,
|
|
689
|
+
controller_details: dict[str, str],
|
|
690
|
+
processor_details: dict[str, str],
|
|
691
|
+
**kwargs: Any,
|
|
692
|
+
) -> DPADocument:
|
|
693
|
+
self._record("generate_dpa", project_id, controller_details, processor_details, **kwargs)
|
|
694
|
+
return self._get_response(
|
|
695
|
+
"generate_dpa",
|
|
696
|
+
DPADocument(
|
|
697
|
+
project_id=project_id,
|
|
698
|
+
controller_name="Mock Controller",
|
|
699
|
+
controller_address="",
|
|
700
|
+
processor_name="SpanForge",
|
|
701
|
+
processor_address="",
|
|
702
|
+
processing_purposes=[],
|
|
703
|
+
data_categories=[],
|
|
704
|
+
data_subjects=[],
|
|
705
|
+
sub_processors=[],
|
|
706
|
+
transfer_mechanism="SCCs",
|
|
707
|
+
retention_period="7 years",
|
|
708
|
+
security_measures=[],
|
|
709
|
+
scc_clauses="Module 2",
|
|
710
|
+
document_id="mock-dpa",
|
|
711
|
+
generated_at=_now_iso(),
|
|
712
|
+
text="<mock-dpa/>",
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def get_status(self) -> CECStatusInfo:
|
|
717
|
+
self._record("get_status")
|
|
718
|
+
return self._get_response(
|
|
719
|
+
"get_status",
|
|
720
|
+
CECStatusInfo(
|
|
721
|
+
status="ok",
|
|
722
|
+
byos_enabled=False,
|
|
723
|
+
bundle_count=0,
|
|
724
|
+
last_bundle_at=None,
|
|
725
|
+
frameworks_supported=[],
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# ---------------------------------------------------------------------------
|
|
731
|
+
# MockSFAlert
|
|
732
|
+
# ---------------------------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class MockSFAlert(_MockBase):
|
|
736
|
+
"""Mock replacement for :class:`~spanforge.sdk.alert.SFAlertClient`."""
|
|
737
|
+
|
|
738
|
+
def register_topic(self, topic: str, description: str = "", **kwargs: Any) -> None:
|
|
739
|
+
self._record("register_topic", topic, description, **kwargs)
|
|
740
|
+
|
|
741
|
+
def publish(
|
|
742
|
+
self, topic: str, payload: dict[str, Any] | None = None, **kwargs: Any
|
|
743
|
+
) -> PublishResult:
|
|
744
|
+
self._record("publish", topic, payload, **kwargs)
|
|
745
|
+
return self._get_response(
|
|
746
|
+
"publish", PublishResult(alert_id="mock-alert-id", routed_to=[], suppressed=False)
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
def acknowledge(self, alert_id: str) -> bool:
|
|
750
|
+
self._record("acknowledge", alert_id)
|
|
751
|
+
return self._get_response("acknowledge", True)
|
|
752
|
+
|
|
753
|
+
def get_alert_history(self, **kwargs: Any) -> list[AlertRecord]:
|
|
754
|
+
self._record("get_alert_history", **kwargs)
|
|
755
|
+
return self._get_response("get_alert_history", [])
|
|
756
|
+
|
|
757
|
+
def get_status(self) -> AlertStatusInfo:
|
|
758
|
+
self._record("get_status")
|
|
759
|
+
return self._get_response(
|
|
760
|
+
"get_status",
|
|
761
|
+
AlertStatusInfo(
|
|
762
|
+
status="ok",
|
|
763
|
+
publish_count=0,
|
|
764
|
+
suppress_count=0,
|
|
765
|
+
queue_depth=0,
|
|
766
|
+
registered_topics=0,
|
|
767
|
+
active_maintenance_windows=0,
|
|
768
|
+
healthy=True,
|
|
769
|
+
),
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
def add_sink(self, alerter: Any, name: str | None = None) -> None:
|
|
773
|
+
self._record("add_sink", alerter, name)
|
|
774
|
+
|
|
775
|
+
def shutdown(self, timeout: float = 5.0) -> None:
|
|
776
|
+
self._record("shutdown", timeout)
|
|
777
|
+
|
|
778
|
+
@property
|
|
779
|
+
def healthy(self) -> bool:
|
|
780
|
+
return True
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
# ---------------------------------------------------------------------------
|
|
784
|
+
# MockSFTrust
|
|
785
|
+
# ---------------------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
class MockSFTrust(_MockBase):
|
|
789
|
+
"""Mock replacement for :class:`~spanforge.sdk.trust.SFTrustClient`."""
|
|
790
|
+
|
|
791
|
+
def get_scorecard(self, project_id: str | None = None, **kwargs: Any) -> TrustScorecardResponse:
|
|
792
|
+
self._record("get_scorecard", project_id, **kwargs)
|
|
793
|
+
dim = TrustDimension(score=1.0, trend="stable", last_updated=_now_iso())
|
|
794
|
+
return self._get_response(
|
|
795
|
+
"get_scorecard",
|
|
796
|
+
TrustScorecardResponse(
|
|
797
|
+
project_id=project_id or "mock",
|
|
798
|
+
overall_score=1.0,
|
|
799
|
+
colour_band="green",
|
|
800
|
+
transparency=dim,
|
|
801
|
+
reliability=dim,
|
|
802
|
+
user_trust=dim,
|
|
803
|
+
security=dim,
|
|
804
|
+
traceability=dim,
|
|
805
|
+
from_dt=_now_iso(),
|
|
806
|
+
to_dt=_now_iso(),
|
|
807
|
+
record_count=0,
|
|
808
|
+
weights=TrustDimensionWeights(),
|
|
809
|
+
),
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
def get_history(self, project_id: str | None = None, **kwargs: Any) -> list[TrustHistoryEntry]:
|
|
813
|
+
self._record("get_history", project_id, **kwargs)
|
|
814
|
+
return self._get_response("get_history", [])
|
|
815
|
+
|
|
816
|
+
def get_badge(self, project_id: str | None = None) -> TrustBadgeResult:
|
|
817
|
+
self._record("get_badge", project_id)
|
|
818
|
+
return self._get_response(
|
|
819
|
+
"get_badge",
|
|
820
|
+
TrustBadgeResult(svg="<svg/>", overall=1.0, colour_band="green", etag="mock-etag"),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
def get_status(self) -> TrustStatusInfo:
|
|
824
|
+
self._record("get_status")
|
|
825
|
+
return self._get_response(
|
|
826
|
+
"get_status",
|
|
827
|
+
TrustStatusInfo(
|
|
828
|
+
status="ok",
|
|
829
|
+
dimension_count=5,
|
|
830
|
+
total_trust_records=0,
|
|
831
|
+
pipelines_registered=0,
|
|
832
|
+
last_scorecard_computed=None,
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
# ---------------------------------------------------------------------------
|
|
838
|
+
# MockSFEnterprise
|
|
839
|
+
# ---------------------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
class MockSFEnterprise(_MockBase):
|
|
843
|
+
"""Mock replacement for :class:`~spanforge.sdk.enterprise.SFEnterpriseClient`."""
|
|
844
|
+
|
|
845
|
+
def register_tenant(self, project_id: str, org_id: str, **kwargs: Any) -> TenantConfig:
|
|
846
|
+
self._record("register_tenant", project_id, org_id, **kwargs)
|
|
847
|
+
return self._get_response(
|
|
848
|
+
"register_tenant",
|
|
849
|
+
TenantConfig(
|
|
850
|
+
project_id=project_id,
|
|
851
|
+
org_id=org_id,
|
|
852
|
+
data_residency="global",
|
|
853
|
+
org_secret="mock-secret", # nosec B106
|
|
854
|
+
),
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def get_tenant(self, project_id: str) -> TenantConfig | None:
|
|
858
|
+
self._record("get_tenant", project_id)
|
|
859
|
+
return self._get_response("get_tenant", None)
|
|
860
|
+
|
|
861
|
+
def list_tenants(self) -> list[TenantConfig]:
|
|
862
|
+
self._record("list_tenants")
|
|
863
|
+
return self._get_response("list_tenants", [])
|
|
864
|
+
|
|
865
|
+
def get_isolation_scope(self, project_id: str) -> IsolationScope:
|
|
866
|
+
self._record("get_isolation_scope", project_id)
|
|
867
|
+
return self._get_response(
|
|
868
|
+
"get_isolation_scope", IsolationScope(org_id="mock-org", project_id=project_id)
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
def check_cross_project_access(self, source: str, targets: list[str]) -> None:
|
|
872
|
+
self._record("check_cross_project_access", source, targets)
|
|
873
|
+
|
|
874
|
+
def get_endpoint_for_project(self, project_id: str) -> str:
|
|
875
|
+
self._record("get_endpoint_for_project", project_id)
|
|
876
|
+
return self._get_response("get_endpoint_for_project", "http://localhost:8080")
|
|
877
|
+
|
|
878
|
+
def enforce_data_residency(self, project_id: str, target_region: str) -> None:
|
|
879
|
+
self._record("enforce_data_residency", project_id, target_region)
|
|
880
|
+
|
|
881
|
+
def configure_encryption(self, **kwargs: Any) -> EncryptionConfig:
|
|
882
|
+
self._record("configure_encryption", **kwargs)
|
|
883
|
+
return self._get_response("configure_encryption", EncryptionConfig())
|
|
884
|
+
|
|
885
|
+
def get_encryption_config(self) -> EncryptionConfig:
|
|
886
|
+
self._record("get_encryption_config")
|
|
887
|
+
return self._get_response("get_encryption_config", EncryptionConfig())
|
|
888
|
+
|
|
889
|
+
def encrypt_payload(self, plaintext: bytes, key: bytes) -> dict[str, Any]:
|
|
890
|
+
self._record("encrypt_payload", plaintext, key)
|
|
891
|
+
return self._get_response("encrypt_payload", {"ciphertext": "", "nonce": "", "tag": ""})
|
|
892
|
+
|
|
893
|
+
def decrypt_payload(
|
|
894
|
+
self, ciphertext_hex: str, nonce_hex: str, tag_hex: str, key: bytes
|
|
895
|
+
) -> bytes:
|
|
896
|
+
self._record("decrypt_payload", ciphertext_hex, nonce_hex, tag_hex, key)
|
|
897
|
+
return self._get_response("decrypt_payload", b"")
|
|
898
|
+
|
|
899
|
+
def configure_airgap(self, **kwargs: Any) -> AirGapConfig:
|
|
900
|
+
self._record("configure_airgap", **kwargs)
|
|
901
|
+
return self._get_response("configure_airgap", AirGapConfig())
|
|
902
|
+
|
|
903
|
+
def get_airgap_config(self) -> AirGapConfig:
|
|
904
|
+
self._record("get_airgap_config")
|
|
905
|
+
return self._get_response("get_airgap_config", AirGapConfig())
|
|
906
|
+
|
|
907
|
+
def assert_network_allowed(self) -> None:
|
|
908
|
+
self._record("assert_network_allowed")
|
|
909
|
+
|
|
910
|
+
def check_health_endpoint(
|
|
911
|
+
self, service: str, endpoint: str = "/healthz"
|
|
912
|
+
) -> HealthEndpointResult:
|
|
913
|
+
self._record("check_health_endpoint", service, endpoint)
|
|
914
|
+
return self._get_response(
|
|
915
|
+
"check_health_endpoint",
|
|
916
|
+
HealthEndpointResult(
|
|
917
|
+
service=service,
|
|
918
|
+
endpoint=endpoint,
|
|
919
|
+
status=200,
|
|
920
|
+
ok=True,
|
|
921
|
+
latency_ms=1.0,
|
|
922
|
+
checked_at=_now_iso(),
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
def check_all_services_health(self) -> list[HealthEndpointResult]:
|
|
927
|
+
self._record("check_all_services_health")
|
|
928
|
+
return self._get_response("check_all_services_health", [])
|
|
929
|
+
|
|
930
|
+
def get_status(self) -> EnterpriseStatusInfo:
|
|
931
|
+
self._record("get_status")
|
|
932
|
+
return self._get_response("get_status", EnterpriseStatusInfo(status="ok"))
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ---------------------------------------------------------------------------
|
|
936
|
+
# MockSFSecurity
|
|
937
|
+
# ---------------------------------------------------------------------------
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class MockSFSecurity(_MockBase):
|
|
941
|
+
"""Mock replacement for :class:`~spanforge.sdk.security.SFSecurityClient`."""
|
|
942
|
+
|
|
943
|
+
def run_owasp_audit(self, **kwargs: Any) -> SecurityAuditResult:
|
|
944
|
+
self._record("run_owasp_audit", **kwargs)
|
|
945
|
+
return self._get_response(
|
|
946
|
+
"run_owasp_audit",
|
|
947
|
+
SecurityAuditResult(categories={}, pass_=True, audited_at=_now_iso(), threat_model=[]),
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def add_threat(
|
|
951
|
+
self, service: str, category: str, threat: str, mitigation: str, risk_level: str = "medium"
|
|
952
|
+
) -> ThreatModelEntry:
|
|
953
|
+
self._record("add_threat", service, category, threat, mitigation, risk_level)
|
|
954
|
+
return self._get_response(
|
|
955
|
+
"add_threat",
|
|
956
|
+
ThreatModelEntry(
|
|
957
|
+
service=service,
|
|
958
|
+
category=category,
|
|
959
|
+
threat=threat,
|
|
960
|
+
mitigation=mitigation,
|
|
961
|
+
risk_level=risk_level,
|
|
962
|
+
reviewed_at=_now_iso(),
|
|
963
|
+
),
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
def get_threat_model(self, service: str | None = None) -> list[ThreatModelEntry]:
|
|
967
|
+
self._record("get_threat_model", service)
|
|
968
|
+
return self._get_response("get_threat_model", [])
|
|
969
|
+
|
|
970
|
+
def generate_default_threat_model(self) -> list[ThreatModelEntry]:
|
|
971
|
+
self._record("generate_default_threat_model")
|
|
972
|
+
return self._get_response("generate_default_threat_model", [])
|
|
973
|
+
|
|
974
|
+
def scan_dependencies(self, **kwargs: Any) -> list[Any]:
|
|
975
|
+
self._record("scan_dependencies", **kwargs)
|
|
976
|
+
return self._get_response("scan_dependencies", [])
|
|
977
|
+
|
|
978
|
+
def run_static_analysis(self, **kwargs: Any) -> list[Any]:
|
|
979
|
+
self._record("run_static_analysis", **kwargs)
|
|
980
|
+
return self._get_response("run_static_analysis", [])
|
|
981
|
+
|
|
982
|
+
def audit_logs_for_secrets(self, log_lines: list[str]) -> int:
|
|
983
|
+
self._record("audit_logs_for_secrets", log_lines)
|
|
984
|
+
return self._get_response("audit_logs_for_secrets", 0)
|
|
985
|
+
|
|
986
|
+
def audit_logs_for_secrets_safe(self, log_lines: list[str]) -> int:
|
|
987
|
+
self._record("audit_logs_for_secrets_safe", log_lines)
|
|
988
|
+
return self._get_response("audit_logs_for_secrets_safe", 0)
|
|
989
|
+
|
|
990
|
+
def run_full_scan(self, **kwargs: Any) -> SecurityScanResult:
|
|
991
|
+
self._record("run_full_scan", **kwargs)
|
|
992
|
+
return self._get_response(
|
|
993
|
+
"run_full_scan",
|
|
994
|
+
SecurityScanResult(
|
|
995
|
+
vulnerabilities=[],
|
|
996
|
+
static_findings=[],
|
|
997
|
+
secrets_in_logs=0,
|
|
998
|
+
pass_=True,
|
|
999
|
+
scanned_at=_now_iso(),
|
|
1000
|
+
),
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
def get_last_scan(self) -> SecurityScanResult | None:
|
|
1004
|
+
self._record("get_last_scan")
|
|
1005
|
+
return self._get_response("get_last_scan", None)
|
|
1006
|
+
|
|
1007
|
+
def get_status(self) -> dict[str, Any]:
|
|
1008
|
+
self._record("get_status")
|
|
1009
|
+
return self._get_response("get_status", {"status": "ok"})
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
# ---------------------------------------------------------------------------
|
|
1013
|
+
# mock_all_services() context manager
|
|
1014
|
+
# ---------------------------------------------------------------------------
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
@contextlib.contextmanager
|
|
1018
|
+
def mock_all_services() -> Generator[dict[str, _MockBase], None, None]:
|
|
1019
|
+
"""Replace all SDK singletons with mock clients for the duration of the block.
|
|
1020
|
+
|
|
1021
|
+
Yields a dict mapping service names to their mock instances::
|
|
1022
|
+
|
|
1023
|
+
with mock_all_services() as mocks:
|
|
1024
|
+
mocks["sf_pii"].configure_response("scan", custom_result)
|
|
1025
|
+
# ... code under test ...
|
|
1026
|
+
mocks["sf_audit"].assert_called("append")
|
|
1027
|
+
"""
|
|
1028
|
+
from spanforge import sdk
|
|
1029
|
+
|
|
1030
|
+
mocks = {
|
|
1031
|
+
"sf_identity": MockSFIdentity(),
|
|
1032
|
+
"sf_pii": MockSFPII(),
|
|
1033
|
+
"sf_secrets": MockSFSecrets(),
|
|
1034
|
+
"sf_audit": MockSFAudit(),
|
|
1035
|
+
"sf_observe": MockSFObserve(),
|
|
1036
|
+
"sf_gate": MockSFGate(),
|
|
1037
|
+
"sf_cec": MockSFCEC(),
|
|
1038
|
+
"sf_alert": MockSFAlert(),
|
|
1039
|
+
"sf_trust": MockSFTrust(),
|
|
1040
|
+
"sf_enterprise": MockSFEnterprise(),
|
|
1041
|
+
"sf_security": MockSFSecurity(),
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
originals = {name: getattr(sdk, name) for name in mocks}
|
|
1045
|
+
|
|
1046
|
+
try:
|
|
1047
|
+
for name, mock in mocks.items():
|
|
1048
|
+
setattr(sdk, name, mock)
|
|
1049
|
+
yield mocks
|
|
1050
|
+
finally:
|
|
1051
|
+
for name, original in originals.items():
|
|
1052
|
+
setattr(sdk, name, original)
|