skill-sentry-cli 2.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.
- skill_sentry_cli/__init__.py +5 -0
- skill_sentry_cli/__main__.py +4 -0
- skill_sentry_cli/cli.py +918 -0
- skill_sentry_cli/core/__init__.py +1 -0
- skill_sentry_cli/core/anchors/__init__.py +5 -0
- skill_sentry_cli/core/anchors/credential_flow.py +440 -0
- skill_sentry_cli/core/anchors/declared_hosts.py +78 -0
- skill_sentry_cli/core/cache/__init__.py +35 -0
- skill_sentry_cli/core/cache/hash_utils.py +206 -0
- skill_sentry_cli/core/cache/sqlite_store.py +925 -0
- skill_sentry_cli/core/config.py +299 -0
- skill_sentry_cli/core/contracts.py +551 -0
- skill_sentry_cli/core/decisions/__init__.py +44 -0
- skill_sentry_cli/core/decisions/base.py +174 -0
- skill_sentry_cli/core/decisions/calibration.py +325 -0
- skill_sentry_cli/core/decisions/laya.py +439 -0
- skill_sentry_cli/core/decisions/mapping.py +218 -0
- skill_sentry_cli/core/decisions/mock.py +118 -0
- skill_sentry_cli/core/decisions/questions.py +118 -0
- skill_sentry_cli/core/decisions/report.py +65 -0
- skill_sentry_cli/core/decisions/selector.py +151 -0
- skill_sentry_cli/core/engine.py +1412 -0
- skill_sentry_cli/core/evasion/__init__.py +27 -0
- skill_sentry_cli/core/evasion/detector.py +210 -0
- skill_sentry_cli/core/exit_codes.py +13 -0
- skill_sentry_cli/core/models/__init__.py +20 -0
- skill_sentry_cli/core/models/_json.py +147 -0
- skill_sentry_cli/core/models/base.py +50 -0
- skill_sentry_cli/core/models/gemma_cloud.py +240 -0
- skill_sentry_cli/core/models/gemma_local.py +273 -0
- skill_sentry_cli/core/models/image_utils.py +112 -0
- skill_sentry_cli/core/models/mock.py +152 -0
- skill_sentry_cli/core/models/selector.py +123 -0
- skill_sentry_cli/core/parser/__init__.py +29 -0
- skill_sentry_cli/core/parser/skill_parser.py +520 -0
- skill_sentry_cli/core/parser/typosquat.py +167 -0
- skill_sentry_cli/core/poisoned_well/__init__.py +52 -0
- skill_sentry_cli/core/poisoned_well/attacker_profile.py +230 -0
- skill_sentry_cli/core/poisoned_well/canary_gen.py +413 -0
- skill_sentry_cli/core/poisoned_well/exfil_tracker.py +470 -0
- skill_sentry_cli/core/reports.py +114 -0
- skill_sentry_cli/core/sandbox/__init__.py +74 -0
- skill_sentry_cli/core/sandbox/assets/Dockerfile.sandbox +39 -0
- skill_sentry_cli/core/sandbox/assets/masquerade.c +484 -0
- skill_sentry_cli/core/sandbox/assets/run_skill.sh +41 -0
- skill_sentry_cli/core/sandbox/assets/sinkhole_sidecar.py +171 -0
- skill_sentry_cli/core/sandbox/canary_fs.py +108 -0
- skill_sentry_cli/core/sandbox/context_honeypot.py +97 -0
- skill_sentry_cli/core/sandbox/dns_sinkhole.py +218 -0
- skill_sentry_cli/core/sandbox/fake_http.py +194 -0
- skill_sentry_cli/core/sandbox/instructions.py +170 -0
- skill_sentry_cli/core/sandbox/masquerade.py +265 -0
- skill_sentry_cli/core/sandbox/monitor.py +278 -0
- skill_sentry_cli/core/sandbox/runner.py +776 -0
- skill_sentry_cli/core/sandbox/sidecar.py +327 -0
- skill_sentry_cli/core/semantic/__init__.py +9 -0
- skill_sentry_cli/core/semantic/analyzer.py +443 -0
- skill_sentry_cli/core/semantic/ast_anchor.py +371 -0
- skill_sentry_cli/core/semantic/ast_python.py +156 -0
- skill_sentry_cli/core/semantic/candidates.py +78 -0
- skill_sentry_cli/core/semantic/laya_analyzer.py +241 -0
- skill_sentry_cli/core/semantic/prompts.py +61 -0
- skill_sentry_cli/core/verdict/__init__.py +24 -0
- skill_sentry_cli/core/verdict/fusion.py +392 -0
- skill_sentry_cli/core/verdict/markdown_report.py +211 -0
- skill_sentry_cli/core/verdict/sarif_report.py +139 -0
- skill_sentry_cli/core/verdict/status.py +35 -0
- skill_sentry_cli/core/verdict/stix_report.py +132 -0
- skill_sentry_cli/core/version.py +38 -0
- skill_sentry_cli/core/vision/__init__.py +8 -0
- skill_sentry_cli/core/vision/image_extractor.py +91 -0
- skill_sentry_cli/core/vision/vision_scanner.py +274 -0
- skill_sentry_cli/data/__init__.py +1 -0
- skill_sentry_cli/data/known_skills_v1.json +276 -0
- skill_sentry_cli/data/sandbox_image.json +4 -0
- skill_sentry_cli/rules/credential_rules.toml +94 -0
- skill_sentry_cli/server/__init__.py +5 -0
- skill_sentry_cli/server/app.py +129 -0
- skill_sentry_cli/server/backend.py +63 -0
- skill_sentry_cli/server/demo_assets.py +62 -0
- skill_sentry_cli/server/demo_backend.py +475 -0
- skill_sentry_cli/server/demo_data.py +814 -0
- skill_sentry_cli/server/demo_mode.py +40 -0
- skill_sentry_cli/server/errors.py +48 -0
- skill_sentry_cli/server/live_backend.py +576 -0
- skill_sentry_cli/server/routes.py +191 -0
- skill_sentry_cli/skill/SKILL.md +67 -0
- skill_sentry_cli/skill/scripts/audit.sh +48 -0
- skill_sentry_cli/ui/dist/assets/index-Bf0h24IX.css +1 -0
- skill_sentry_cli/ui/dist/assets/index-CrYBKSSB.js +8 -0
- skill_sentry_cli/ui/dist/index.html +14 -0
- skill_sentry_cli-2.0.0.dist-info/METADATA +76 -0
- skill_sentry_cli-2.0.0.dist-info/RECORD +96 -0
- skill_sentry_cli-2.0.0.dist-info/WHEEL +4 -0
- skill_sentry_cli-2.0.0.dist-info/entry_points.txt +2 -0
- skill_sentry_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Frozen SkillSentry contracts (I1) and pipeline interface (I3).
|
|
2
|
+
|
|
3
|
+
This module is the **single source of truth** for:
|
|
4
|
+
|
|
5
|
+
* pipeline stage identifiers and their exact JSON spellings;
|
|
6
|
+
* verdict tiers, severities, WS event names, exfil channels, canary contexts;
|
|
7
|
+
* the Verdict JSON Contract v1 (FROZEN at M0 — additive changes only);
|
|
8
|
+
* the request/response models used by the REST/WS API;
|
|
9
|
+
* the ``StageResult`` returned by every analysis stage;
|
|
10
|
+
* the tab payloads consumed by the Triage Dashboard.
|
|
11
|
+
|
|
12
|
+
Python field names are snake_case; API responses are serialised in camelCase
|
|
13
|
+
by ``APIModel``. ``POST /scan`` request bodies intentionally stay snake_case
|
|
14
|
+
because that is what docs/05-api-reference.md §5.2 specifies.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from datetime import UTC, datetime
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Annotated, Any, Literal
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
|
|
26
|
+
from pydantic import (
|
|
27
|
+
BaseModel,
|
|
28
|
+
ConfigDict,
|
|
29
|
+
Field,
|
|
30
|
+
PlainSerializer,
|
|
31
|
+
field_validator,
|
|
32
|
+
)
|
|
33
|
+
from pydantic.alias_generators import to_camel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Time helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
def utc_now() -> datetime:
|
|
40
|
+
"""Timezone-aware UTC now (never the deprecated ``utcnow``)."""
|
|
41
|
+
return datetime.now(UTC)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_utc(value: datetime) -> str:
|
|
45
|
+
"""ISO-8601 UTC with milliseconds and a literal ``Z`` suffix (§5.1)."""
|
|
46
|
+
if value.tzinfo is None:
|
|
47
|
+
value = value.replace(tzinfo=UTC)
|
|
48
|
+
value = value.astimezone(UTC)
|
|
49
|
+
return value.strftime("%Y-%m-%dT%H:%M:%S.") + f"{value.microsecond // 1000:03d}Z"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
UtcDatetime = Annotated[datetime, PlainSerializer(format_utc, return_type=str)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Enumerations
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
class VerdictTier(str, Enum):
|
|
59
|
+
SAFE = "SAFE"
|
|
60
|
+
SUSPICIOUS = "SUSPICIOUS"
|
|
61
|
+
MALICIOUS = "MALICIOUS"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Severity(str, Enum):
|
|
65
|
+
LOW = "low"
|
|
66
|
+
MEDIUM = "medium"
|
|
67
|
+
HIGH = "high"
|
|
68
|
+
CRITICAL = "critical"
|
|
69
|
+
NOTE = "note"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def rank(self) -> int:
|
|
73
|
+
return {
|
|
74
|
+
Severity.NOTE: 0,
|
|
75
|
+
Severity.LOW: 1,
|
|
76
|
+
Severity.MEDIUM: 2,
|
|
77
|
+
Severity.HIGH: 3,
|
|
78
|
+
Severity.CRITICAL: 4,
|
|
79
|
+
}[self]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Stage(str, Enum):
|
|
83
|
+
"""Canonical pipeline stage identifiers (docs/05 §5.4)."""
|
|
84
|
+
|
|
85
|
+
CACHE = "cache"
|
|
86
|
+
SPEC = "spec"
|
|
87
|
+
TYPOSQUAT = "typosquat"
|
|
88
|
+
DETERMINISTIC_ANCHOR = "deterministicAnchor"
|
|
89
|
+
SEMANTIC = "semantic"
|
|
90
|
+
VISION = "vision"
|
|
91
|
+
SANDBOX = "sandbox"
|
|
92
|
+
POISONED_WELL = "poisonedWell"
|
|
93
|
+
EVASION = "evasion"
|
|
94
|
+
VERDICT = "verdict"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class WSEventType(str, Enum):
|
|
98
|
+
STARTED = "started"
|
|
99
|
+
PROGRESS = "progress"
|
|
100
|
+
FINDING = "finding"
|
|
101
|
+
OVERRIDE = "override"
|
|
102
|
+
COMPLETED = "completed"
|
|
103
|
+
ERROR = "error"
|
|
104
|
+
CACHED = "cached"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ScanStatus(str, Enum):
|
|
108
|
+
QUEUED = "queued"
|
|
109
|
+
RUNNING = "running"
|
|
110
|
+
COMPLETED = "completed"
|
|
111
|
+
FAILED = "failed"
|
|
112
|
+
CACHED_HIT = "cached-hit"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ExfilChannel(str, Enum):
|
|
116
|
+
DNS_TUNNELING = "DNS_TUNNELING"
|
|
117
|
+
HTTP_POST = "HTTP_POST"
|
|
118
|
+
RAW_SOCKET = "RAW_SOCKET"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ContextCanary(str, Enum):
|
|
122
|
+
AWS = "AWS"
|
|
123
|
+
GITHUB = "GITHUB"
|
|
124
|
+
SLACK = "SLACK"
|
|
125
|
+
SSH = "SSH"
|
|
126
|
+
GENERIC = "GENERIC"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Backend(str, Enum):
|
|
130
|
+
AUTO = "auto"
|
|
131
|
+
LOCAL = "local"
|
|
132
|
+
CLOUD = "cloud"
|
|
133
|
+
DEMO = "demo"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ExportFormat(str, Enum):
|
|
137
|
+
SARIF = "sarif"
|
|
138
|
+
MARKDOWN = "markdown"
|
|
139
|
+
STIX = "stix"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
HealthStatus = Literal["ok", "down"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Base models
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
class APIModel(BaseModel):
|
|
149
|
+
"""Response model: camelCase on the wire, snake_case in Python."""
|
|
150
|
+
|
|
151
|
+
model_config = ConfigDict(
|
|
152
|
+
alias_generator=to_camel,
|
|
153
|
+
populate_by_name=True,
|
|
154
|
+
extra="forbid",
|
|
155
|
+
use_enum_values=False,
|
|
156
|
+
# Fields such as model_version describe the model; that is not a protected namespace here.
|
|
157
|
+
protected_namespaces=(),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RequestModel(BaseModel):
|
|
162
|
+
"""Request model: field names exactly as documented (snake_case)."""
|
|
163
|
+
|
|
164
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Findings + active defense (Verdict Contract v1)
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
class FindingEvidence(APIModel):
|
|
171
|
+
"""Free-form evidence. Common keys are typed; extras are preserved."""
|
|
172
|
+
|
|
173
|
+
model_config = ConfigDict(
|
|
174
|
+
alias_generator=to_camel,
|
|
175
|
+
populate_by_name=True,
|
|
176
|
+
extra="allow",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
file: str | None = None
|
|
180
|
+
line: int | None = Field(default=None, ge=1)
|
|
181
|
+
code: str | None = None
|
|
182
|
+
c2: str | None = None
|
|
183
|
+
marker: str | None = None
|
|
184
|
+
bbox: list[float] | None = None
|
|
185
|
+
ocr: str | None = None
|
|
186
|
+
process: str | None = None
|
|
187
|
+
path: str | None = None
|
|
188
|
+
channel: str | None = None
|
|
189
|
+
url: str | None = None
|
|
190
|
+
domain: str | None = None
|
|
191
|
+
ip: str | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Finding(APIModel):
|
|
195
|
+
stage: Stage
|
|
196
|
+
severity: Severity
|
|
197
|
+
message: str
|
|
198
|
+
evidence: FindingEvidence = Field(default_factory=FindingEvidence)
|
|
199
|
+
# Additive field required by SARIF ruleId = skillsentry/<stage>/<type> (§5.6).
|
|
200
|
+
finding_type: str | None = Field(default=None, alias="type")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ActiveDefense(APIModel):
|
|
204
|
+
triggered: bool = False
|
|
205
|
+
targeted_assets: list[str] = Field(default_factory=list)
|
|
206
|
+
exfil_channel: ExfilChannel | None = None
|
|
207
|
+
canary_marker: str | None = None
|
|
208
|
+
secondary_canary: str | None = None
|
|
209
|
+
c2_indicators: list[str] = Field(default_factory=list)
|
|
210
|
+
encoding: str | None = None
|
|
211
|
+
sandbox_evasion_detected: bool = False
|
|
212
|
+
declared_intent: str | None = None
|
|
213
|
+
context_aware_canary: ContextCanary | None = None
|
|
214
|
+
# Present in the SARIF extension (§5.6) — additive to the verdict block.
|
|
215
|
+
actual_behavior: str | None = None
|
|
216
|
+
ioc_export: str | None = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Verdict(APIModel):
|
|
220
|
+
"""Verdict JSON Contract v1 (FROZEN).
|
|
221
|
+
|
|
222
|
+
``skillId`` / ``scanId`` are additive fields agreed before the M0 freeze so
|
|
223
|
+
``GET /skills/{skill_id}`` is addressable for folder-of-skills scans.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
skill: str
|
|
227
|
+
skill_id: UUID
|
|
228
|
+
scan_id: UUID
|
|
229
|
+
verdict: VerdictTier
|
|
230
|
+
risk_score: int = Field(ge=0, le=100)
|
|
231
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
232
|
+
cached: bool
|
|
233
|
+
scan_duration_ms: int = Field(ge=0)
|
|
234
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
235
|
+
active_defense: ActiveDefense | None = None
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def key_finding(self) -> Finding | None:
|
|
239
|
+
if not self.findings:
|
|
240
|
+
return None
|
|
241
|
+
return max(self.findings, key=lambda f: f.severity.rank)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Pipeline interface I3
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
class OverrideSignal(APIModel):
|
|
248
|
+
"""Hard evidence that overrides weighted fusion."""
|
|
249
|
+
|
|
250
|
+
verdict: VerdictTier = VerdictTier.MALICIOUS
|
|
251
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
252
|
+
reason: str
|
|
253
|
+
source: Stage
|
|
254
|
+
evidence: dict[str, Any] = Field(default_factory=dict)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class StageResult(APIModel):
|
|
258
|
+
"""Result returned by every stage (interface I3).
|
|
259
|
+
|
|
260
|
+
``signals`` keys consumed by fusion: ``anchors``, ``semantic``, ``vision``,
|
|
261
|
+
``sandbox`` and ``typosquat``. Values are normalised 0-100.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
stage: Stage
|
|
265
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
266
|
+
signals: dict[str, float] = Field(default_factory=dict)
|
|
267
|
+
telemetry: dict[str, Any] | None = None
|
|
268
|
+
override: OverrideSignal | None = None
|
|
269
|
+
degraded: bool = False
|
|
270
|
+
error: str | None = None
|
|
271
|
+
|
|
272
|
+
@field_validator("signals")
|
|
273
|
+
@classmethod
|
|
274
|
+
def _normalise_signals(cls, value: dict[str, float]) -> dict[str, float]:
|
|
275
|
+
normalised: dict[str, float] = {}
|
|
276
|
+
for key, raw in value.items():
|
|
277
|
+
score = float(raw)
|
|
278
|
+
if score != score: # NaN
|
|
279
|
+
score = 0.0
|
|
280
|
+
normalised[key] = max(0.0, min(100.0, score))
|
|
281
|
+
return normalised
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# REST request / response models
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
class ScanOptions(RequestModel):
|
|
288
|
+
no_cache: bool = False
|
|
289
|
+
backend: Backend = Backend.AUTO
|
|
290
|
+
sandbox: bool = True
|
|
291
|
+
poisoned_well: bool = True
|
|
292
|
+
timeout_s: int = Field(default=60, ge=5, le=600)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ScanRequest(RequestModel):
|
|
296
|
+
# Optional in demo mode ("Scan All" on the canned store); required live.
|
|
297
|
+
path: str | None = None
|
|
298
|
+
options: ScanOptions = Field(default_factory=ScanOptions)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ScanStartResponse(APIModel):
|
|
302
|
+
scan_id: UUID
|
|
303
|
+
status: ScanStatus = ScanStatus.QUEUED
|
|
304
|
+
stream: str
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ScanStatusResponse(APIModel):
|
|
308
|
+
scan_id: UUID
|
|
309
|
+
status: ScanStatus
|
|
310
|
+
stage: Stage | None = None
|
|
311
|
+
skill: str | None = None
|
|
312
|
+
started_at: UtcDatetime
|
|
313
|
+
elapsed_ms: int | None = None
|
|
314
|
+
error: str | None = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class VerdictsResponse(APIModel):
|
|
318
|
+
scan_id: UUID
|
|
319
|
+
verdicts: list[Verdict] = Field(default_factory=list)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class ScanSummary(APIModel):
|
|
323
|
+
scan_id: UUID
|
|
324
|
+
skill_id: UUID
|
|
325
|
+
skill: str
|
|
326
|
+
verdict: VerdictTier | None = None
|
|
327
|
+
risk_score: int | None = None
|
|
328
|
+
cached: bool = False
|
|
329
|
+
status: ScanStatus
|
|
330
|
+
started_at: UtcDatetime
|
|
331
|
+
duration_ms: int | None = None
|
|
332
|
+
key_finding: str | None = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class ScanListResponse(APIModel):
|
|
336
|
+
scans: list[ScanSummary] = Field(default_factory=list)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class CacheStatsResponse(APIModel):
|
|
340
|
+
entries: int
|
|
341
|
+
hit_rate: float
|
|
342
|
+
db_size_bytes: int
|
|
343
|
+
engine_version: str
|
|
344
|
+
model_version: str
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class DeleteCacheResponse(APIModel):
|
|
348
|
+
deleted: int
|
|
349
|
+
retained: int = 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class HealthResponse(APIModel):
|
|
353
|
+
ollama: HealthStatus
|
|
354
|
+
docker: HealthStatus
|
|
355
|
+
db: HealthStatus
|
|
356
|
+
demo_mode: bool = False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class MetaResponse(APIModel):
|
|
360
|
+
version: str
|
|
361
|
+
backend_active: str
|
|
362
|
+
gemma_badge: str
|
|
363
|
+
demo_mode: bool
|
|
364
|
+
contract_version: str
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ErrorDetail(APIModel):
|
|
368
|
+
code: str
|
|
369
|
+
message: str
|
|
370
|
+
stage: Stage | None = None
|
|
371
|
+
degraded: bool = False
|
|
372
|
+
scan_id: UUID | None = None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class ErrorResponse(APIModel):
|
|
376
|
+
error: ErrorDetail
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# WebSocket message
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
class WSMessage(APIModel):
|
|
383
|
+
stage: Stage
|
|
384
|
+
skill: str
|
|
385
|
+
event: WSEventType
|
|
386
|
+
ts: UtcDatetime
|
|
387
|
+
detail: dict[str, Any] | None = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Skill-detail tab payloads (I5)
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
class StaticFinding(APIModel):
|
|
394
|
+
file: str
|
|
395
|
+
line: int = Field(ge=1)
|
|
396
|
+
code: str | None = None
|
|
397
|
+
stage: Stage
|
|
398
|
+
severity: Severity
|
|
399
|
+
message: str
|
|
400
|
+
anchor: bool = False
|
|
401
|
+
highlight: bool = True
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class VisionEvidence(APIModel):
|
|
405
|
+
image: str
|
|
406
|
+
image_name: str | None = None
|
|
407
|
+
ocr: str
|
|
408
|
+
bboxes: list[list[float]] = Field(default_factory=list)
|
|
409
|
+
view_modes: list[str] = Field(default_factory=lambda: ["normal", "contrast", "gemma-eye"])
|
|
410
|
+
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
411
|
+
position_note: str | None = None
|
|
412
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class SandboxEvent(APIModel):
|
|
416
|
+
ts: UtcDatetime
|
|
417
|
+
event_type: str
|
|
418
|
+
path_or_channel: str | None = None
|
|
419
|
+
marker: str | None = None
|
|
420
|
+
process: str | None = None
|
|
421
|
+
detail: dict[str, Any] | None = None
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class NetLog(APIModel):
|
|
425
|
+
ts: UtcDatetime
|
|
426
|
+
protocol: Literal["dns", "http"]
|
|
427
|
+
query_or_url: str
|
|
428
|
+
body_b64: str | None = None
|
|
429
|
+
responder_action: str | None = None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class SandboxTrace(APIModel):
|
|
433
|
+
events: list[SandboxEvent] = Field(default_factory=list)
|
|
434
|
+
net_logs: list[NetLog] = Field(default_factory=list)
|
|
435
|
+
resource_usage: dict[str, Any] | None = None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class TimelineRow(APIModel):
|
|
439
|
+
ts: UtcDatetime
|
|
440
|
+
label: str
|
|
441
|
+
kind: str
|
|
442
|
+
marker: str | None = None
|
|
443
|
+
detail: dict[str, Any] | None = None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class IOCGraphNode(APIModel):
|
|
447
|
+
id: str
|
|
448
|
+
label: str
|
|
449
|
+
kind: str
|
|
450
|
+
evidence: dict[str, Any] | None = None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class IOCGraphEdge(APIModel):
|
|
454
|
+
source: str
|
|
455
|
+
target: str
|
|
456
|
+
label: str
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class IOCGraph(APIModel):
|
|
460
|
+
nodes: list[IOCGraphNode] = Field(default_factory=list)
|
|
461
|
+
edges: list[IOCGraphEdge] = Field(default_factory=list)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class PoisonedWellView(APIModel):
|
|
465
|
+
active_defense: ActiveDefense | None = None
|
|
466
|
+
timeline: list[TimelineRow] = Field(default_factory=list)
|
|
467
|
+
graph: IOCGraph = Field(default_factory=IOCGraph)
|
|
468
|
+
confirmed: bool = False
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class ReportLinks(APIModel):
|
|
472
|
+
sarif: str | None = None
|
|
473
|
+
markdown: str | None = None
|
|
474
|
+
stix: str | None = None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class SkillDetail(APIModel):
|
|
478
|
+
verdict: Verdict
|
|
479
|
+
static_findings: list[StaticFinding] = Field(default_factory=list)
|
|
480
|
+
vision_evidence: list[VisionEvidence] = Field(default_factory=list)
|
|
481
|
+
sandbox_trace: SandboxTrace = Field(default_factory=SandboxTrace)
|
|
482
|
+
poisoned_well: PoisonedWellView = Field(default_factory=PoisonedWellView)
|
|
483
|
+
report_links: ReportLinks = Field(default_factory=ReportLinks)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# Model-client helper models (interface I2)
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
class ImageAnalysis(APIModel):
|
|
490
|
+
ocr: str = ""
|
|
491
|
+
bboxes: list[list[float]] = Field(default_factory=list)
|
|
492
|
+
findings: list[dict[str, Any]] = Field(default_factory=list)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class ModelInfo(APIModel):
|
|
496
|
+
backend: str
|
|
497
|
+
model_version: str
|
|
498
|
+
available: bool = True
|
|
499
|
+
detail: str | None = None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
# Schema export + JSON helpers
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
SCHEMA_MODELS: dict[str, type[BaseModel]] = {
|
|
506
|
+
"Verdict": Verdict,
|
|
507
|
+
"SkillDetail": SkillDetail,
|
|
508
|
+
"ScanRequest": ScanRequest,
|
|
509
|
+
"ScanStartResponse": ScanStartResponse,
|
|
510
|
+
"ScanStatusResponse": ScanStatusResponse,
|
|
511
|
+
"VerdictsResponse": VerdictsResponse,
|
|
512
|
+
"ScanSummary": ScanSummary,
|
|
513
|
+
"ScanListResponse": ScanListResponse,
|
|
514
|
+
"CacheStatsResponse": CacheStatsResponse,
|
|
515
|
+
"HealthResponse": HealthResponse,
|
|
516
|
+
"MetaResponse": MetaResponse,
|
|
517
|
+
"ErrorResponse": ErrorResponse,
|
|
518
|
+
"WSMessage": WSMessage,
|
|
519
|
+
"StageResult": StageResult,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def export_json_schemas(directory: str | Path) -> dict[str, Path]:
|
|
524
|
+
"""Write the frozen Verdict schema plus the API model bundle.
|
|
525
|
+
|
|
526
|
+
Dev 3 generates TypeScript types from these files; contract tests assert
|
|
527
|
+
they are reproducible from ``contracts.py``.
|
|
528
|
+
"""
|
|
529
|
+
target = Path(directory)
|
|
530
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
531
|
+
written: dict[str, Path] = {}
|
|
532
|
+
|
|
533
|
+
verdict_path = target / "verdict.v1.json"
|
|
534
|
+
verdict_path.write_text(
|
|
535
|
+
json.dumps(Verdict.model_json_schema(by_alias=True), indent=2, sort_keys=True) + "\n",
|
|
536
|
+
encoding="utf-8",
|
|
537
|
+
)
|
|
538
|
+
written["verdict"] = verdict_path
|
|
539
|
+
|
|
540
|
+
api_bundle = {
|
|
541
|
+
name: model.model_json_schema(by_alias=True) for name, model in SCHEMA_MODELS.items()
|
|
542
|
+
}
|
|
543
|
+
api_path = target / "api.v1.json"
|
|
544
|
+
api_path.write_text(json.dumps(api_bundle, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
545
|
+
written["api"] = api_path
|
|
546
|
+
return written
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def model_json(model: BaseModel) -> str:
|
|
550
|
+
"""Serialise a contract model with camelCase aliases."""
|
|
551
|
+
return model.model_dump_json(by_alias=True, indent=2)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Typed decision contracts for local semantic backends.
|
|
2
|
+
|
|
3
|
+
Laya and similar System One models answer typed questions instead of
|
|
4
|
+
generating free-form text. Keep this interface separate from
|
|
5
|
+
:class:`skill_sentry_cli.core.models.base.ModelClient`, which remains the
|
|
6
|
+
generation/vision interface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from skill_sentry_cli.core.decisions.base import (
|
|
10
|
+
DecisionAnswer,
|
|
11
|
+
DecisionClient,
|
|
12
|
+
DecisionError,
|
|
13
|
+
DecisionQuestion,
|
|
14
|
+
DecisionResult,
|
|
15
|
+
DecisionUnavailable,
|
|
16
|
+
EvidenceCandidate,
|
|
17
|
+
)
|
|
18
|
+
from skill_sentry_cli.core.decisions.laya import LayaDecisionClient
|
|
19
|
+
from skill_sentry_cli.core.decisions.mock import MockDecisionClient
|
|
20
|
+
from skill_sentry_cli.core.decisions.questions import (
|
|
21
|
+
CAPABILITY_QUESTIONS,
|
|
22
|
+
DECEPTION_CHOICES,
|
|
23
|
+
INTENT_CHOICES,
|
|
24
|
+
semantic_questions,
|
|
25
|
+
)
|
|
26
|
+
from skill_sentry_cli.core.decisions.selector import DecisionSelection, select_decision_model
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CAPABILITY_QUESTIONS",
|
|
30
|
+
"DECEPTION_CHOICES",
|
|
31
|
+
"INTENT_CHOICES",
|
|
32
|
+
"DecisionAnswer",
|
|
33
|
+
"DecisionClient",
|
|
34
|
+
"DecisionError",
|
|
35
|
+
"DecisionQuestion",
|
|
36
|
+
"DecisionResult",
|
|
37
|
+
"DecisionSelection",
|
|
38
|
+
"DecisionUnavailable",
|
|
39
|
+
"EvidenceCandidate",
|
|
40
|
+
"LayaDecisionClient",
|
|
41
|
+
"MockDecisionClient",
|
|
42
|
+
"select_decision_model",
|
|
43
|
+
"semantic_questions",
|
|
44
|
+
]
|