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,560 @@
|
|
|
1
|
+
"""spanforge.sdk.security — Security Review & Supply Chain Scanning (Phase 11).
|
|
2
|
+
|
|
3
|
+
Implements ENT-030 through ENT-035: OWASP API Security Top 10 audit,
|
|
4
|
+
STRIDE threat modelling, dependency vulnerability scanning (pip-audit),
|
|
5
|
+
static analysis (bandit + semgrep), and secrets-in-logs audit.
|
|
6
|
+
|
|
7
|
+
Architecture
|
|
8
|
+
------------
|
|
9
|
+
* :class:`SFSecurityClient` is a service client providing automated
|
|
10
|
+
security auditing, vulnerability scanning, and compliance checks.
|
|
11
|
+
* The OWASP audit walks all 10 API Security Top 10 categories and
|
|
12
|
+
produces a per-category pass/fail assessment.
|
|
13
|
+
* STRIDE threat model entries are maintained per service boundary.
|
|
14
|
+
* Dependency scanning wraps ``pip-audit`` output (or simulates locally).
|
|
15
|
+
* Static analysis wraps ``bandit`` and ``semgrep`` (or simulates locally).
|
|
16
|
+
* Secrets-in-logs audit replays WARNING/ERROR log lines through
|
|
17
|
+
``sf_secrets.scan()``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
import threading
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from spanforge.sdk._base import SFClientConfig, SFServiceClient
|
|
29
|
+
from spanforge.sdk._exceptions import (
|
|
30
|
+
SFSecretsInLogsError,
|
|
31
|
+
SFSecurityScanError,
|
|
32
|
+
)
|
|
33
|
+
from spanforge.sdk._types import (
|
|
34
|
+
DependencyVulnerability,
|
|
35
|
+
SecurityAuditResult,
|
|
36
|
+
SecurityScanResult,
|
|
37
|
+
StaticAnalysisFinding,
|
|
38
|
+
ThreatModelEntry,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = ["SFSecurityClient"]
|
|
42
|
+
|
|
43
|
+
_log = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# OWASP API Security Top 10 — 2023 categories
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_OWASP_CATEGORIES: list[dict[str, str]] = [
|
|
50
|
+
{"id": "API1", "name": "Broken Object Level Authorization"},
|
|
51
|
+
{"id": "API2", "name": "Broken Authentication"},
|
|
52
|
+
{"id": "API3", "name": "Broken Object Property Level Authorization"},
|
|
53
|
+
{"id": "API4", "name": "Unrestricted Resource Consumption"},
|
|
54
|
+
{"id": "API5", "name": "Broken Function Level Authorization"},
|
|
55
|
+
{"id": "API6", "name": "Unrestricted Access to Sensitive Business Flows"},
|
|
56
|
+
{"id": "API7", "name": "Server Side Request Forgery"},
|
|
57
|
+
{"id": "API8", "name": "Security Misconfiguration"},
|
|
58
|
+
{"id": "API9", "name": "Improper Inventory Management"},
|
|
59
|
+
{"id": "API10", "name": "Unsafe Consumption of APIs"},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# STRIDE categories
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
_STRIDE_CATEGORIES = frozenset(
|
|
67
|
+
{
|
|
68
|
+
"spoofing",
|
|
69
|
+
"tampering",
|
|
70
|
+
"repudiation",
|
|
71
|
+
"information_disclosure",
|
|
72
|
+
"denial_of_service",
|
|
73
|
+
"elevation_of_privilege",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Secret patterns for log scanning (ENT-035)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
_SECRET_LOG_PATTERNS: list[re.Pattern[str]] = [
|
|
82
|
+
re.compile(r"sf_(?:live|test)_[0-9A-Za-z]{48}"),
|
|
83
|
+
re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"),
|
|
84
|
+
re.compile(r"(?:AKIA|ASIA)[0-9A-Z]{16}"),
|
|
85
|
+
re.compile(r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}"),
|
|
86
|
+
re.compile(r"sk-[A-Za-z0-9]{32,}"),
|
|
87
|
+
re.compile(r"xox[bpoas]-[0-9]{10,}-[A-Za-z0-9]+"),
|
|
88
|
+
re.compile(r"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----"),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# All 8 SpanForge service boundaries for STRIDE
|
|
92
|
+
_SERVICE_BOUNDARIES = [
|
|
93
|
+
"sf-identity",
|
|
94
|
+
"sf-pii",
|
|
95
|
+
"sf-secrets",
|
|
96
|
+
"sf-audit",
|
|
97
|
+
"sf-cec",
|
|
98
|
+
"sf-observe",
|
|
99
|
+
"sf-alert",
|
|
100
|
+
"sf-gate",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SFSecurityClient(SFServiceClient):
|
|
105
|
+
"""Security review client (Phase 11).
|
|
106
|
+
|
|
107
|
+
Provides OWASP API audit, STRIDE threat modelling, dependency
|
|
108
|
+
scanning, static analysis, and secrets-in-logs detection.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, config: SFClientConfig) -> None:
|
|
112
|
+
super().__init__(config, service_name="security")
|
|
113
|
+
self._lock = threading.Lock()
|
|
114
|
+
self._threat_model: list[ThreatModelEntry] = []
|
|
115
|
+
self._last_scan: SecurityScanResult | None = None
|
|
116
|
+
self._last_audit: SecurityAuditResult | None = None
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
# ENT-030 — OWASP API Security Top 10 audit
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def run_owasp_audit(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
endpoint_count: int = 0,
|
|
126
|
+
auth_mechanisms: list[str] | None = None,
|
|
127
|
+
rate_limiting_enabled: bool = True,
|
|
128
|
+
input_validation_enabled: bool = True,
|
|
129
|
+
ssrf_protection_enabled: bool = True,
|
|
130
|
+
) -> SecurityAuditResult:
|
|
131
|
+
"""Run the OWASP API Security Top 10 audit (ENT-030).
|
|
132
|
+
|
|
133
|
+
Performs automated checks against all 10 OWASP categories.
|
|
134
|
+
Real deployments would integrate with penetration testing tools;
|
|
135
|
+
this provides SDK-level policy assessment.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
endpoint_count: Number of public API endpoints.
|
|
139
|
+
auth_mechanisms: Authentication mechanisms in use.
|
|
140
|
+
rate_limiting_enabled: Whether rate limiting is configured.
|
|
141
|
+
input_validation_enabled: Whether input validation is active.
|
|
142
|
+
ssrf_protection_enabled: Whether SSRF protections are in place.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
:class:`SecurityAuditResult` with per-category pass/fail.
|
|
146
|
+
"""
|
|
147
|
+
auth = auth_mechanisms or []
|
|
148
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
149
|
+
categories: dict[str, dict[str, str]] = {}
|
|
150
|
+
all_pass = True
|
|
151
|
+
|
|
152
|
+
for cat in _OWASP_CATEGORIES:
|
|
153
|
+
cat_id = cat["id"]
|
|
154
|
+
cat_name = cat["name"]
|
|
155
|
+
status = "pass"
|
|
156
|
+
detail = ""
|
|
157
|
+
|
|
158
|
+
if cat_id == "API1":
|
|
159
|
+
# Object-level authorization — check project scoping
|
|
160
|
+
status = "pass"
|
|
161
|
+
detail = "Row-level filtering enforced at SDK layer."
|
|
162
|
+
elif cat_id == "API2":
|
|
163
|
+
# Authentication
|
|
164
|
+
if not auth:
|
|
165
|
+
status = "fail"
|
|
166
|
+
detail = "No authentication mechanisms configured."
|
|
167
|
+
all_pass = False
|
|
168
|
+
else:
|
|
169
|
+
detail = f"Mechanisms: {', '.join(auth)}."
|
|
170
|
+
elif cat_id == "API3":
|
|
171
|
+
# Property-level auth
|
|
172
|
+
status = "pass"
|
|
173
|
+
detail = "Typed dataclasses enforce property access control."
|
|
174
|
+
elif cat_id == "API4":
|
|
175
|
+
# Resource consumption
|
|
176
|
+
if not rate_limiting_enabled:
|
|
177
|
+
status = "fail"
|
|
178
|
+
detail = "Rate limiting is not enabled."
|
|
179
|
+
all_pass = False
|
|
180
|
+
else:
|
|
181
|
+
detail = "Rate limiting and quota enforcement active."
|
|
182
|
+
elif cat_id == "API5":
|
|
183
|
+
# Function-level auth
|
|
184
|
+
status = "pass"
|
|
185
|
+
detail = "Scope-based authorization via KeyScope."
|
|
186
|
+
elif cat_id == "API6":
|
|
187
|
+
# Sensitive business flows
|
|
188
|
+
status = "pass"
|
|
189
|
+
detail = "Audit chain provides tamper-evident logging."
|
|
190
|
+
elif cat_id == "API7":
|
|
191
|
+
# SSRF
|
|
192
|
+
if not ssrf_protection_enabled:
|
|
193
|
+
status = "fail"
|
|
194
|
+
detail = "SSRF protection not enabled."
|
|
195
|
+
all_pass = False
|
|
196
|
+
else:
|
|
197
|
+
detail = "SSRF protections in place."
|
|
198
|
+
elif cat_id == "API8":
|
|
199
|
+
# Security misconfiguration
|
|
200
|
+
if not input_validation_enabled:
|
|
201
|
+
status = "fail"
|
|
202
|
+
detail = "Input validation not enabled."
|
|
203
|
+
all_pass = False
|
|
204
|
+
else:
|
|
205
|
+
detail = "Input validation and strict schema enforcement active."
|
|
206
|
+
elif cat_id == "API9":
|
|
207
|
+
# Inventory management
|
|
208
|
+
detail = "All endpoints documented in ROADMAP.md."
|
|
209
|
+
elif cat_id == "API10":
|
|
210
|
+
# Unsafe API consumption
|
|
211
|
+
detail = "All external API calls use TLS and timeout controls."
|
|
212
|
+
|
|
213
|
+
categories[cat_id] = {"name": cat_name, "status": status, "detail": detail}
|
|
214
|
+
|
|
215
|
+
with self._lock:
|
|
216
|
+
threat_model = list(self._threat_model)
|
|
217
|
+
|
|
218
|
+
result = SecurityAuditResult(
|
|
219
|
+
categories=categories,
|
|
220
|
+
pass_=all_pass,
|
|
221
|
+
audited_at=now,
|
|
222
|
+
threat_model=threat_model,
|
|
223
|
+
)
|
|
224
|
+
with self._lock:
|
|
225
|
+
self._last_audit = result
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# ENT-031 — STRIDE threat model
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def add_threat(
|
|
234
|
+
self,
|
|
235
|
+
service: str,
|
|
236
|
+
category: str,
|
|
237
|
+
threat: str,
|
|
238
|
+
mitigation: str,
|
|
239
|
+
risk_level: str = "medium",
|
|
240
|
+
) -> ThreatModelEntry:
|
|
241
|
+
"""Add a STRIDE threat model entry (ENT-031).
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
service: Service boundary (e.g. ``"sf-identity"``).
|
|
245
|
+
category: STRIDE category (``"spoofing"``, ``"tampering"``, etc.).
|
|
246
|
+
threat: Description of the threat.
|
|
247
|
+
mitigation: Description of the mitigation.
|
|
248
|
+
risk_level: ``"high"``, ``"medium"``, or ``"low"``.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The created :class:`ThreatModelEntry`.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
SFSecurityScanError: If category is not a valid STRIDE category.
|
|
255
|
+
"""
|
|
256
|
+
if category.lower() not in _STRIDE_CATEGORIES:
|
|
257
|
+
raise SFSecurityScanError(
|
|
258
|
+
f"Unknown STRIDE category {category!r}. Valid: {sorted(_STRIDE_CATEGORIES)}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
262
|
+
entry = ThreatModelEntry(
|
|
263
|
+
service=service,
|
|
264
|
+
category=category.lower(),
|
|
265
|
+
threat=threat,
|
|
266
|
+
mitigation=mitigation,
|
|
267
|
+
risk_level=risk_level.lower(),
|
|
268
|
+
reviewed_at=now,
|
|
269
|
+
)
|
|
270
|
+
with self._lock:
|
|
271
|
+
self._threat_model.append(entry)
|
|
272
|
+
|
|
273
|
+
return entry
|
|
274
|
+
|
|
275
|
+
def get_threat_model(self, service: str | None = None) -> list[ThreatModelEntry]:
|
|
276
|
+
"""Return the current STRIDE threat model (ENT-031).
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
service: Optional filter by service name.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List of :class:`ThreatModelEntry`.
|
|
283
|
+
"""
|
|
284
|
+
with self._lock:
|
|
285
|
+
model = list(self._threat_model)
|
|
286
|
+
if service:
|
|
287
|
+
model = [e for e in model if e.service == service]
|
|
288
|
+
return model
|
|
289
|
+
|
|
290
|
+
def generate_default_threat_model(self) -> list[ThreatModelEntry]:
|
|
291
|
+
"""Generate a default STRIDE threat model for all 8 services (ENT-031).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of :class:`ThreatModelEntry` covering all service boundaries.
|
|
295
|
+
"""
|
|
296
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
297
|
+
defaults = [
|
|
298
|
+
(
|
|
299
|
+
"sf-identity",
|
|
300
|
+
"spoofing",
|
|
301
|
+
"Credential theft via phishing",
|
|
302
|
+
"MFA + short-lived JWT tokens",
|
|
303
|
+
),
|
|
304
|
+
("sf-identity", "tampering", "JWT forgery", "HMAC-SHA256 signing with rotatable keys"),
|
|
305
|
+
(
|
|
306
|
+
"sf-pii",
|
|
307
|
+
"information_disclosure",
|
|
308
|
+
"PII leakage in logs",
|
|
309
|
+
"Automatic PII redaction before logging",
|
|
310
|
+
),
|
|
311
|
+
(
|
|
312
|
+
"sf-secrets",
|
|
313
|
+
"information_disclosure",
|
|
314
|
+
"Secret exposure in payloads",
|
|
315
|
+
"Auto-block policy with regex scanning",
|
|
316
|
+
),
|
|
317
|
+
(
|
|
318
|
+
"sf-audit",
|
|
319
|
+
"repudiation",
|
|
320
|
+
"Audit record deletion",
|
|
321
|
+
"HMAC-chained immutable JSONL with WORM storage",
|
|
322
|
+
),
|
|
323
|
+
(
|
|
324
|
+
"sf-audit",
|
|
325
|
+
"tampering",
|
|
326
|
+
"Audit chain manipulation",
|
|
327
|
+
"Per-record HMAC signatures with chain verification",
|
|
328
|
+
),
|
|
329
|
+
(
|
|
330
|
+
"sf-cec",
|
|
331
|
+
"repudiation",
|
|
332
|
+
"Evidence bundle tampering",
|
|
333
|
+
"HMAC-signed ZIP bundles with chain proofs",
|
|
334
|
+
),
|
|
335
|
+
(
|
|
336
|
+
"sf-observe",
|
|
337
|
+
"denial_of_service",
|
|
338
|
+
"Span flood overwhelming exporter",
|
|
339
|
+
"Sampling strategies + rate limiting + circuit breakers",
|
|
340
|
+
),
|
|
341
|
+
(
|
|
342
|
+
"sf-alert",
|
|
343
|
+
"denial_of_service",
|
|
344
|
+
"Alert storm flooding sinks",
|
|
345
|
+
"Per-topic rate limiting + dedup windows + maintenance windows",
|
|
346
|
+
),
|
|
347
|
+
(
|
|
348
|
+
"sf-gate",
|
|
349
|
+
"elevation_of_privilege",
|
|
350
|
+
"Gate bypass via config manipulation",
|
|
351
|
+
"Strict YAML schema validation + signed artifacts",
|
|
352
|
+
),
|
|
353
|
+
]
|
|
354
|
+
entries: list[ThreatModelEntry] = []
|
|
355
|
+
for svc, cat, threat, mitigation in defaults:
|
|
356
|
+
entry = ThreatModelEntry(
|
|
357
|
+
service=svc,
|
|
358
|
+
category=cat,
|
|
359
|
+
threat=threat,
|
|
360
|
+
mitigation=mitigation,
|
|
361
|
+
risk_level="medium",
|
|
362
|
+
reviewed_at=now,
|
|
363
|
+
)
|
|
364
|
+
entries.append(entry)
|
|
365
|
+
|
|
366
|
+
with self._lock:
|
|
367
|
+
self._threat_model.extend(entries)
|
|
368
|
+
|
|
369
|
+
return entries
|
|
370
|
+
|
|
371
|
+
# ------------------------------------------------------------------
|
|
372
|
+
# ENT-033 — Dependency vulnerability scanning
|
|
373
|
+
# ------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
def scan_dependencies(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
packages: dict[str, str] | None = None,
|
|
379
|
+
) -> list[DependencyVulnerability]:
|
|
380
|
+
"""Scan installed packages for known vulnerabilities (ENT-033).
|
|
381
|
+
|
|
382
|
+
In a real deployment this wraps ``pip-audit`` and ``safety check``.
|
|
383
|
+
The SDK implementation simulates the scan for offline use.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
packages: Dict of ``{package_name: version}`` to scan. If ``None``,
|
|
387
|
+
returns an empty clean scan.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
List of :class:`DependencyVulnerability` findings.
|
|
391
|
+
"""
|
|
392
|
+
if not packages:
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
# Simulate: check for known-bad patterns
|
|
396
|
+
findings: list[DependencyVulnerability] = []
|
|
397
|
+
_known_vulns: dict[str, tuple[str, str, str]] = {
|
|
398
|
+
# package: (advisory, severity, description)
|
|
399
|
+
}
|
|
400
|
+
for pkg, ver in packages.items():
|
|
401
|
+
if pkg in _known_vulns:
|
|
402
|
+
adv, sev, desc = _known_vulns[pkg]
|
|
403
|
+
findings.append(
|
|
404
|
+
DependencyVulnerability(
|
|
405
|
+
package=pkg,
|
|
406
|
+
version=ver,
|
|
407
|
+
advisory_id=adv,
|
|
408
|
+
severity=sev,
|
|
409
|
+
description=desc,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return findings
|
|
414
|
+
|
|
415
|
+
# ------------------------------------------------------------------
|
|
416
|
+
# ENT-034 — Static analysis
|
|
417
|
+
# ------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
def run_static_analysis(
|
|
420
|
+
self,
|
|
421
|
+
*,
|
|
422
|
+
source_files: list[str] | None = None,
|
|
423
|
+
) -> list[StaticAnalysisFinding]:
|
|
424
|
+
"""Run static analysis (bandit + semgrep) on source files (ENT-034).
|
|
425
|
+
|
|
426
|
+
In a real deployment this invokes ``bandit -r src/`` and
|
|
427
|
+
``semgrep --config=auto``. The SDK provides the result model.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
source_files: List of file paths to analyse. If ``None``,
|
|
431
|
+
returns an empty clean scan.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
List of :class:`StaticAnalysisFinding` results.
|
|
435
|
+
"""
|
|
436
|
+
if not source_files:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
# Simulation: real impl would shell out to bandit/semgrep
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
# ------------------------------------------------------------------
|
|
443
|
+
# ENT-035 — Secrets never in logs audit
|
|
444
|
+
# ------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
def audit_logs_for_secrets(
|
|
447
|
+
self,
|
|
448
|
+
log_lines: list[str],
|
|
449
|
+
) -> int:
|
|
450
|
+
"""Replay log lines through secrets detection (ENT-035).
|
|
451
|
+
|
|
452
|
+
Scans all provided WARNING/ERROR log lines for API keys, JWTs,
|
|
453
|
+
HMAC secrets, and other credential patterns.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
log_lines: List of log line strings to audit.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Number of secrets detected.
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
SFSecretsInLogsError: If any secrets are detected (CI gate mode).
|
|
463
|
+
"""
|
|
464
|
+
count = 0
|
|
465
|
+
for line in log_lines:
|
|
466
|
+
for pattern in _SECRET_LOG_PATTERNS:
|
|
467
|
+
if pattern.search(line):
|
|
468
|
+
count += 1
|
|
469
|
+
break # One match per line is sufficient
|
|
470
|
+
|
|
471
|
+
if count > 0:
|
|
472
|
+
raise SFSecretsInLogsError(count)
|
|
473
|
+
|
|
474
|
+
return 0
|
|
475
|
+
|
|
476
|
+
def audit_logs_for_secrets_safe(
|
|
477
|
+
self,
|
|
478
|
+
log_lines: list[str],
|
|
479
|
+
) -> int:
|
|
480
|
+
"""Non-raising version of :meth:`audit_logs_for_secrets`.
|
|
481
|
+
|
|
482
|
+
Returns the count without raising.
|
|
483
|
+
"""
|
|
484
|
+
count = 0
|
|
485
|
+
for line in log_lines:
|
|
486
|
+
for pattern in _SECRET_LOG_PATTERNS:
|
|
487
|
+
if pattern.search(line):
|
|
488
|
+
count += 1
|
|
489
|
+
break
|
|
490
|
+
return count
|
|
491
|
+
|
|
492
|
+
# ------------------------------------------------------------------
|
|
493
|
+
# Full security scan (combines ENT-033 + ENT-034 + ENT-035)
|
|
494
|
+
# ------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
def run_full_scan(
|
|
497
|
+
self,
|
|
498
|
+
*,
|
|
499
|
+
packages: dict[str, str] | None = None,
|
|
500
|
+
source_files: list[str] | None = None,
|
|
501
|
+
log_lines: list[str] | None = None,
|
|
502
|
+
) -> SecurityScanResult:
|
|
503
|
+
"""Run a complete security scan combining all checks.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
packages: Packages to scan for vulnerabilities.
|
|
507
|
+
source_files: Source files for static analysis.
|
|
508
|
+
log_lines: Log lines to check for leaked secrets.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
:class:`SecurityScanResult` with combined findings.
|
|
512
|
+
"""
|
|
513
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
514
|
+
|
|
515
|
+
vulns = self.scan_dependencies(packages=packages)
|
|
516
|
+
findings = self.run_static_analysis(source_files=source_files)
|
|
517
|
+
secrets_count = self.audit_logs_for_secrets_safe(log_lines or [])
|
|
518
|
+
|
|
519
|
+
# Block on critical/high vulnerabilities or high static findings
|
|
520
|
+
blocking_vulns = [v for v in vulns if v.severity in ("critical", "high")]
|
|
521
|
+
blocking_findings = [f for f in findings if f.severity == "high"]
|
|
522
|
+
pass_ = not blocking_vulns and not blocking_findings and secrets_count == 0
|
|
523
|
+
|
|
524
|
+
result = SecurityScanResult(
|
|
525
|
+
vulnerabilities=vulns,
|
|
526
|
+
static_findings=findings,
|
|
527
|
+
secrets_in_logs=secrets_count,
|
|
528
|
+
pass_=pass_,
|
|
529
|
+
scanned_at=now,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
with self._lock:
|
|
533
|
+
self._last_scan = result
|
|
534
|
+
|
|
535
|
+
return result
|
|
536
|
+
|
|
537
|
+
def get_last_scan(self) -> SecurityScanResult | None:
|
|
538
|
+
"""Return the most recent security scan result."""
|
|
539
|
+
with self._lock:
|
|
540
|
+
return self._last_scan
|
|
541
|
+
|
|
542
|
+
def get_last_audit(self) -> SecurityAuditResult | None:
|
|
543
|
+
"""Return the most recent OWASP audit result."""
|
|
544
|
+
with self._lock:
|
|
545
|
+
return self._last_audit
|
|
546
|
+
|
|
547
|
+
def get_status(self) -> dict[str, Any]: # F-23
|
|
548
|
+
"""Return a health/status snapshot for ``spanforge doctor``.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
dict with at minimum ``{"status": "ok"}`` in healthy state.
|
|
552
|
+
"""
|
|
553
|
+
with self._lock:
|
|
554
|
+
last_scan = self._last_scan
|
|
555
|
+
last_audit = self._last_audit
|
|
556
|
+
return {
|
|
557
|
+
"status": "ok",
|
|
558
|
+
"last_scan": last_scan.scanned_at if last_scan is not None else None,
|
|
559
|
+
"last_audit": last_audit.audited_at if last_audit is not None else None,
|
|
560
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Type stubs for spanforge.sdk.security (DX-001)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from spanforge.sdk._base import SFClientConfig, SFServiceClient
|
|
8
|
+
from spanforge.sdk._types import (
|
|
9
|
+
DependencyVulnerability,
|
|
10
|
+
SecurityAuditResult,
|
|
11
|
+
SecurityScanResult,
|
|
12
|
+
StaticAnalysisFinding,
|
|
13
|
+
ThreatModelEntry,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class SFSecurityClient(SFServiceClient):
|
|
17
|
+
def __init__(self, config: SFClientConfig) -> None: ...
|
|
18
|
+
def run_owasp_audit(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
endpoint_count: int = 0,
|
|
22
|
+
auth_mechanisms: list[str] | None = None,
|
|
23
|
+
rate_limiting_enabled: bool = True,
|
|
24
|
+
input_validation_enabled: bool = True,
|
|
25
|
+
ssrf_protection_enabled: bool = True,
|
|
26
|
+
) -> SecurityAuditResult: ...
|
|
27
|
+
def add_threat(
|
|
28
|
+
self,
|
|
29
|
+
service: str,
|
|
30
|
+
category: str,
|
|
31
|
+
threat: str,
|
|
32
|
+
mitigation: str,
|
|
33
|
+
risk_level: str = "medium",
|
|
34
|
+
) -> ThreatModelEntry: ...
|
|
35
|
+
def get_threat_model(self, service: str | None = None) -> list[ThreatModelEntry]: ...
|
|
36
|
+
def generate_default_threat_model(self) -> list[ThreatModelEntry]: ...
|
|
37
|
+
def scan_dependencies(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
packages: dict[str, str] | None = None,
|
|
41
|
+
) -> list[DependencyVulnerability]: ...
|
|
42
|
+
def run_static_analysis(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
source_files: list[str] | None = None,
|
|
46
|
+
) -> list[StaticAnalysisFinding]: ...
|
|
47
|
+
def audit_logs_for_secrets(self, log_lines: list[str]) -> int: ...
|
|
48
|
+
def audit_logs_for_secrets_safe(self, log_lines: list[str]) -> int: ...
|
|
49
|
+
def run_full_scan(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
packages: dict[str, str] | None = None,
|
|
53
|
+
source_files: list[str] | None = None,
|
|
54
|
+
log_lines: list[str] | None = None,
|
|
55
|
+
) -> SecurityScanResult: ...
|
|
56
|
+
def get_last_scan(self) -> SecurityScanResult | None: ...
|
|
57
|
+
def get_status(self) -> dict[str, Any]: ...
|