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,475 @@
|
|
|
1
|
+
"""In-memory demo backend — serves the canned 7-skill store.
|
|
2
|
+
|
|
3
|
+
No Docker, no model, no database. Responses are schema-identical to the live
|
|
4
|
+
server so Dev 3 can switch by base URL only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import UTC, datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from uuid import UUID, uuid4
|
|
15
|
+
|
|
16
|
+
from skill_sentry_cli.core.contracts import (
|
|
17
|
+
CacheStatsResponse,
|
|
18
|
+
DeleteCacheResponse,
|
|
19
|
+
ExportFormat,
|
|
20
|
+
HealthResponse,
|
|
21
|
+
MetaResponse,
|
|
22
|
+
ScanListResponse,
|
|
23
|
+
ScanRequest,
|
|
24
|
+
ScanStartResponse,
|
|
25
|
+
ScanStatus,
|
|
26
|
+
ScanStatusResponse,
|
|
27
|
+
ScanSummary,
|
|
28
|
+
SkillDetail,
|
|
29
|
+
Stage,
|
|
30
|
+
VerdictsResponse,
|
|
31
|
+
WSEventType,
|
|
32
|
+
WSMessage,
|
|
33
|
+
)
|
|
34
|
+
from skill_sentry_cli.core.models.mock import MockGemma
|
|
35
|
+
from skill_sentry_cli.core.reports import RenderedReport, render_report
|
|
36
|
+
from skill_sentry_cli.core.verdict.stix_report import NoIOCsError
|
|
37
|
+
from skill_sentry_cli.core.version import CONTRACT_VERSION, GEMMA_BADGE, __version__, engine_version
|
|
38
|
+
from skill_sentry_cli.server.demo_data import DEMO_STARTED_AT, DemoSkill, build_demo_store
|
|
39
|
+
from skill_sentry_cli.server.errors import ApiError
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class _DemoScan:
|
|
44
|
+
scan_id: UUID
|
|
45
|
+
items: list[DemoSkill]
|
|
46
|
+
started_at: datetime
|
|
47
|
+
cached: bool
|
|
48
|
+
status: ScanStatus = ScanStatus.QUEUED
|
|
49
|
+
current_stage: Stage | None = None
|
|
50
|
+
elapsed_ms: int = 0
|
|
51
|
+
error: str | None = None
|
|
52
|
+
events: list[WSMessage] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DemoBackend:
|
|
56
|
+
demo_mode = True
|
|
57
|
+
|
|
58
|
+
def __init__(self, *, speed: float | None = None) -> None:
|
|
59
|
+
self._store = build_demo_store()
|
|
60
|
+
self._by_skill_id = {item.skill_id: item for item in self._store}
|
|
61
|
+
self._by_scan_id = {item.scan_id: item for item in self._store}
|
|
62
|
+
self._by_name = {item.name: item for item in self._store}
|
|
63
|
+
self._scans: dict[UUID, _DemoScan] = {}
|
|
64
|
+
self._completed: set[str] = set()
|
|
65
|
+
if speed is None:
|
|
66
|
+
speed = float(os.getenv("SKILLSENTRY_DEMO_SPEED", "1"))
|
|
67
|
+
self._speed = max(0.0, speed)
|
|
68
|
+
|
|
69
|
+
# -- lifecycle ----------------------------------------------------------
|
|
70
|
+
async def start_scan(self, request: ScanRequest) -> ScanStartResponse:
|
|
71
|
+
items = self._items_for(request.path)
|
|
72
|
+
no_cache = request.options.no_cache
|
|
73
|
+
cached = not no_cache and all(item.name in self._completed for item in items)
|
|
74
|
+
|
|
75
|
+
scan_id = uuid4()
|
|
76
|
+
started_at = datetime.now(UTC)
|
|
77
|
+
scan = _DemoScan(
|
|
78
|
+
scan_id=scan_id,
|
|
79
|
+
items=items,
|
|
80
|
+
started_at=started_at,
|
|
81
|
+
cached=cached,
|
|
82
|
+
)
|
|
83
|
+
scan.events = self._build_events(scan)
|
|
84
|
+
self._scans[scan_id] = scan
|
|
85
|
+
return ScanStartResponse(
|
|
86
|
+
scan_id=scan_id,
|
|
87
|
+
status=ScanStatus.QUEUED,
|
|
88
|
+
stream=f"/scan/{scan_id}/stream",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _items_for(self, path: str | None) -> list[DemoSkill]:
|
|
92
|
+
if not path:
|
|
93
|
+
return list(self._store)
|
|
94
|
+
name = Path(path).name.lower()
|
|
95
|
+
if name in self._by_name:
|
|
96
|
+
return [self._by_name[name]]
|
|
97
|
+
lowered = path.lower()
|
|
98
|
+
matches = [item for item in self._store if item.name in lowered]
|
|
99
|
+
if len(matches) == 1:
|
|
100
|
+
return matches
|
|
101
|
+
raise ApiError(
|
|
102
|
+
404,
|
|
103
|
+
"no_skill_manifest",
|
|
104
|
+
f"demo store has no skill matching path: {path}",
|
|
105
|
+
stage=Stage.SPEC,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def get_scan(self, scan_id: UUID) -> ScanStatusResponse:
|
|
109
|
+
scan = self._scans.get(scan_id)
|
|
110
|
+
if scan is not None:
|
|
111
|
+
elapsed = scan.elapsed_ms
|
|
112
|
+
if scan.status is ScanStatus.RUNNING:
|
|
113
|
+
elapsed = int((datetime.now(UTC) - scan.started_at).total_seconds() * 1000)
|
|
114
|
+
return ScanStatusResponse(
|
|
115
|
+
scan_id=scan.scan_id,
|
|
116
|
+
status=scan.status,
|
|
117
|
+
stage=scan.current_stage,
|
|
118
|
+
skill=scan.items[0].name if len(scan.items) == 1 else None,
|
|
119
|
+
started_at=scan.started_at,
|
|
120
|
+
elapsed_ms=elapsed,
|
|
121
|
+
error=scan.error,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
item = self._by_scan_id.get(scan_id)
|
|
125
|
+
if item is not None:
|
|
126
|
+
return ScanStatusResponse(
|
|
127
|
+
scan_id=item.scan_id,
|
|
128
|
+
status=ScanStatus.COMPLETED,
|
|
129
|
+
stage=Stage.VERDICT,
|
|
130
|
+
skill=item.name,
|
|
131
|
+
started_at=DEMO_STARTED_AT,
|
|
132
|
+
elapsed_ms=item.verdict.scan_duration_ms,
|
|
133
|
+
)
|
|
134
|
+
raise ApiError(404, "unknown_scan", f"unknown scan: {scan_id}")
|
|
135
|
+
|
|
136
|
+
async def get_verdicts(self, scan_id: UUID) -> VerdictsResponse:
|
|
137
|
+
scan = self._scans.get(scan_id)
|
|
138
|
+
if scan is not None:
|
|
139
|
+
return VerdictsResponse(
|
|
140
|
+
scan_id=scan_id,
|
|
141
|
+
verdicts=[
|
|
142
|
+
item.verdict_for_scan(scan_id, cached=scan.cached) for item in scan.items
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
item = self._by_scan_id.get(scan_id)
|
|
146
|
+
if item is not None:
|
|
147
|
+
return VerdictsResponse(scan_id=scan_id, verdicts=[item.verdict])
|
|
148
|
+
raise ApiError(404, "unknown_scan", f"unknown scan: {scan_id}")
|
|
149
|
+
|
|
150
|
+
async def list_scans(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
limit: int = 100,
|
|
154
|
+
offset: int = 0,
|
|
155
|
+
status: ScanStatus | None = None,
|
|
156
|
+
) -> ScanListResponse:
|
|
157
|
+
summaries: list[ScanSummary] = []
|
|
158
|
+
for index, item in enumerate(self._store):
|
|
159
|
+
summaries.append(
|
|
160
|
+
ScanSummary(
|
|
161
|
+
scan_id=item.scan_id,
|
|
162
|
+
skill_id=item.skill_id,
|
|
163
|
+
skill=item.name,
|
|
164
|
+
verdict=item.verdict.verdict,
|
|
165
|
+
risk_score=item.verdict.risk_score,
|
|
166
|
+
cached=False,
|
|
167
|
+
status=ScanStatus.COMPLETED,
|
|
168
|
+
started_at=DEMO_STARTED_AT + timedelta(seconds=index * 4),
|
|
169
|
+
duration_ms=item.verdict.scan_duration_ms,
|
|
170
|
+
key_finding=item.key_finding,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
# New demo runs are appended (a batch contributes one row per skill).
|
|
174
|
+
for scan in self._scans.values():
|
|
175
|
+
for item in scan.items:
|
|
176
|
+
summaries.append(
|
|
177
|
+
ScanSummary(
|
|
178
|
+
scan_id=scan.scan_id,
|
|
179
|
+
skill_id=item.skill_id,
|
|
180
|
+
skill=item.name,
|
|
181
|
+
verdict=item.verdict.verdict,
|
|
182
|
+
risk_score=item.verdict.risk_score,
|
|
183
|
+
cached=scan.cached,
|
|
184
|
+
status=scan.status,
|
|
185
|
+
started_at=scan.started_at,
|
|
186
|
+
duration_ms=item.verdict.scan_duration_ms,
|
|
187
|
+
key_finding=item.key_finding,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
if status is not None:
|
|
191
|
+
summaries = [row for row in summaries if row.status is status]
|
|
192
|
+
return ScanListResponse(scans=summaries[offset : offset + limit])
|
|
193
|
+
|
|
194
|
+
async def get_skill(self, skill_id: UUID) -> SkillDetail:
|
|
195
|
+
item = self._by_skill_id.get(skill_id) or self._by_scan_id.get(skill_id)
|
|
196
|
+
if item is None:
|
|
197
|
+
scan = self._scans.get(skill_id)
|
|
198
|
+
if scan is not None and len(scan.items) == 1:
|
|
199
|
+
item = scan.items[0]
|
|
200
|
+
if item is None:
|
|
201
|
+
raise ApiError(404, "unknown_scan", f"unknown skill: {skill_id}")
|
|
202
|
+
return item.detail()
|
|
203
|
+
|
|
204
|
+
# -- streaming ----------------------------------------------------------
|
|
205
|
+
async def stream(self, scan_id: UUID):
|
|
206
|
+
scan = self._scans.get(scan_id)
|
|
207
|
+
if scan is None:
|
|
208
|
+
raise ApiError(404, "unknown_scan", f"unknown scan: {scan_id}")
|
|
209
|
+
scan.status = ScanStatus.RUNNING
|
|
210
|
+
for message in scan.events:
|
|
211
|
+
heartbeat = bool(message.detail and message.detail.get("heartbeat"))
|
|
212
|
+
delay = (1.2 if heartbeat else 0.12) * self._speed
|
|
213
|
+
if delay:
|
|
214
|
+
await asyncio.sleep(delay)
|
|
215
|
+
scan.current_stage = message.stage
|
|
216
|
+
offset_ms = int((message.ts - scan.started_at).total_seconds() * 1000)
|
|
217
|
+
scan.elapsed_ms = max(scan.elapsed_ms, offset_ms)
|
|
218
|
+
yield message
|
|
219
|
+
scan.status = ScanStatus.CACHED_HIT if scan.cached else ScanStatus.COMPLETED
|
|
220
|
+
scan.current_stage = Stage.VERDICT
|
|
221
|
+
for item in scan.items:
|
|
222
|
+
self._completed.add(item.name)
|
|
223
|
+
|
|
224
|
+
async def cancel(self, scan_id: UUID) -> None:
|
|
225
|
+
scan = self._scans.get(scan_id)
|
|
226
|
+
if scan is None:
|
|
227
|
+
raise ApiError(404, "unknown_scan", f"unknown scan: {scan_id}")
|
|
228
|
+
scan.status = ScanStatus.FAILED
|
|
229
|
+
scan.error = "cancelled by client"
|
|
230
|
+
|
|
231
|
+
# -- meta / health ------------------------------------------------------
|
|
232
|
+
async def health(self) -> HealthResponse:
|
|
233
|
+
return HealthResponse(ollama="ok", docker="ok", db="ok", demo_mode=True)
|
|
234
|
+
|
|
235
|
+
async def meta(self) -> MetaResponse:
|
|
236
|
+
return MetaResponse(
|
|
237
|
+
version=__version__,
|
|
238
|
+
backend_active="demo",
|
|
239
|
+
gemma_badge=GEMMA_BADGE,
|
|
240
|
+
demo_mode=True,
|
|
241
|
+
contract_version=CONTRACT_VERSION,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def cache_stats(self) -> CacheStatsResponse:
|
|
245
|
+
entries = len(self._store)
|
|
246
|
+
hit_rate = len(self._completed) / entries if entries else 0.0
|
|
247
|
+
return CacheStatsResponse(
|
|
248
|
+
entries=entries,
|
|
249
|
+
hit_rate=round(hit_rate, 3),
|
|
250
|
+
db_size_bytes=245_760,
|
|
251
|
+
engine_version=engine_version(),
|
|
252
|
+
model_version=MockGemma.model_version,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
async def clear_cache(self, older_than: str | None = None) -> DeleteCacheResponse:
|
|
256
|
+
del older_than
|
|
257
|
+
deleted = len(self._completed)
|
|
258
|
+
self._completed.clear()
|
|
259
|
+
return DeleteCacheResponse(deleted=deleted, retained=0)
|
|
260
|
+
|
|
261
|
+
def export(self, skill_id: UUID, fmt: ExportFormat) -> RenderedReport:
|
|
262
|
+
item = self._by_skill_id.get(skill_id) or self._by_scan_id.get(skill_id)
|
|
263
|
+
if item is None:
|
|
264
|
+
raise ApiError(404, "unknown_scan", f"unknown skill: {skill_id}")
|
|
265
|
+
try:
|
|
266
|
+
return render_report(item.verdict, fmt)
|
|
267
|
+
except NoIOCsError as exc:
|
|
268
|
+
raise ApiError(
|
|
269
|
+
409,
|
|
270
|
+
"no_iocs",
|
|
271
|
+
"STIX export requires an active-defense scan with confirmed IOCs",
|
|
272
|
+
stage=Stage.POISONED_WELL,
|
|
273
|
+
) from exc
|
|
274
|
+
|
|
275
|
+
# -- event generation ---------------------------------------------------
|
|
276
|
+
def _build_events(self, scan: _DemoScan) -> list[WSMessage]:
|
|
277
|
+
events: list[WSMessage] = []
|
|
278
|
+
offset = 0
|
|
279
|
+
|
|
280
|
+
def add(
|
|
281
|
+
stage: Stage,
|
|
282
|
+
event: WSEventType,
|
|
283
|
+
detail: dict | None = None,
|
|
284
|
+
*,
|
|
285
|
+
delta: int = 120,
|
|
286
|
+
skill: str | None = None,
|
|
287
|
+
) -> None:
|
|
288
|
+
nonlocal offset
|
|
289
|
+
offset += delta
|
|
290
|
+
events.append(
|
|
291
|
+
WSMessage(
|
|
292
|
+
stage=stage,
|
|
293
|
+
skill=skill or scan.items[0].name,
|
|
294
|
+
event=event,
|
|
295
|
+
ts=scan.started_at + timedelta(milliseconds=offset),
|
|
296
|
+
detail=detail,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if scan.cached:
|
|
301
|
+
for item in scan.items:
|
|
302
|
+
add(
|
|
303
|
+
Stage.CACHE,
|
|
304
|
+
WSEventType.CACHED,
|
|
305
|
+
{
|
|
306
|
+
"message": "cached verdict",
|
|
307
|
+
"durationMs": 1180,
|
|
308
|
+
"hash": f"{item.skill_id.hex[:12]}…",
|
|
309
|
+
},
|
|
310
|
+
delta=60,
|
|
311
|
+
skill=item.name,
|
|
312
|
+
)
|
|
313
|
+
add(
|
|
314
|
+
Stage.VERDICT,
|
|
315
|
+
WSEventType.COMPLETED,
|
|
316
|
+
{
|
|
317
|
+
"verdict": item.verdict_for_scan(scan.scan_id, cached=True).model_dump(
|
|
318
|
+
by_alias=True, mode="json"
|
|
319
|
+
)
|
|
320
|
+
},
|
|
321
|
+
delta=80,
|
|
322
|
+
skill=item.name,
|
|
323
|
+
)
|
|
324
|
+
return events
|
|
325
|
+
|
|
326
|
+
for item in scan.items:
|
|
327
|
+
verdict = item.verdict_for_scan(scan.scan_id, cached=False)
|
|
328
|
+
active = verdict.active_defense
|
|
329
|
+
add(
|
|
330
|
+
Stage.CACHE,
|
|
331
|
+
WSEventType.PROGRESS,
|
|
332
|
+
{"message": "cache miss", "hash": f"{item.skill_id.hex[:12]}…"},
|
|
333
|
+
skill=item.name,
|
|
334
|
+
)
|
|
335
|
+
add(
|
|
336
|
+
Stage.SPEC,
|
|
337
|
+
WSEventType.PROGRESS,
|
|
338
|
+
{"message": "frontmatter valid · typosquat check: clean"},
|
|
339
|
+
skill=item.name,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
for finding in verdict.findings:
|
|
343
|
+
if finding.stage is Stage.DETERMINISTIC_ANCHOR:
|
|
344
|
+
add(
|
|
345
|
+
Stage.DETERMINISTIC_ANCHOR,
|
|
346
|
+
WSEventType.FINDING,
|
|
347
|
+
{"finding": finding.model_dump(by_alias=True, mode="json")},
|
|
348
|
+
delta=90,
|
|
349
|
+
skill=item.name,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
add(
|
|
353
|
+
Stage.SEMANTIC,
|
|
354
|
+
WSEventType.STARTED,
|
|
355
|
+
{"message": "Gemma analyzing…"},
|
|
356
|
+
skill=item.name,
|
|
357
|
+
)
|
|
358
|
+
add(
|
|
359
|
+
Stage.SEMANTIC,
|
|
360
|
+
WSEventType.PROGRESS,
|
|
361
|
+
{"heartbeat": True, "message": "waiting on model…"},
|
|
362
|
+
delta=1500,
|
|
363
|
+
skill=item.name,
|
|
364
|
+
)
|
|
365
|
+
for finding in verdict.findings:
|
|
366
|
+
if finding.stage is Stage.SEMANTIC:
|
|
367
|
+
add(
|
|
368
|
+
Stage.SEMANTIC,
|
|
369
|
+
WSEventType.FINDING,
|
|
370
|
+
{"finding": finding.model_dump(by_alias=True, mode="json")},
|
|
371
|
+
delta=120,
|
|
372
|
+
skill=item.name,
|
|
373
|
+
)
|
|
374
|
+
add(
|
|
375
|
+
Stage.SEMANTIC,
|
|
376
|
+
WSEventType.COMPLETED,
|
|
377
|
+
{"declaredIntent": (active.declared_intent if active else None) or "Not available"},
|
|
378
|
+
delta=140,
|
|
379
|
+
skill=item.name,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if item.vision_evidence:
|
|
383
|
+
vision = item.vision_evidence[0]
|
|
384
|
+
add(
|
|
385
|
+
Stage.VISION,
|
|
386
|
+
WSEventType.PROGRESS,
|
|
387
|
+
{"message": f"scanning {vision.image_name or 'image'}"},
|
|
388
|
+
skill=item.name,
|
|
389
|
+
)
|
|
390
|
+
for finding in verdict.findings:
|
|
391
|
+
if finding.stage is Stage.VISION:
|
|
392
|
+
add(
|
|
393
|
+
Stage.VISION,
|
|
394
|
+
WSEventType.FINDING,
|
|
395
|
+
{
|
|
396
|
+
"finding": finding.model_dump(by_alias=True, mode="json"),
|
|
397
|
+
"bbox": vision.bboxes[0] if vision.bboxes else None,
|
|
398
|
+
"ocr": vision.ocr,
|
|
399
|
+
},
|
|
400
|
+
delta=200,
|
|
401
|
+
skill=item.name,
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
add(
|
|
405
|
+
Stage.VISION,
|
|
406
|
+
WSEventType.PROGRESS,
|
|
407
|
+
{"message": "no bundled images"},
|
|
408
|
+
delta=60,
|
|
409
|
+
skill=item.name,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
add(
|
|
413
|
+
Stage.SANDBOX,
|
|
414
|
+
WSEventType.STARTED,
|
|
415
|
+
{"message": "container up · 512 MB · 1 CPU · 60 s"},
|
|
416
|
+
delta=180,
|
|
417
|
+
skill=item.name,
|
|
418
|
+
)
|
|
419
|
+
for finding in verdict.findings:
|
|
420
|
+
if finding.stage is Stage.SANDBOX:
|
|
421
|
+
add(
|
|
422
|
+
Stage.SANDBOX,
|
|
423
|
+
WSEventType.FINDING,
|
|
424
|
+
{"finding": finding.model_dump(by_alias=True, mode="json")},
|
|
425
|
+
delta=220,
|
|
426
|
+
skill=item.name,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if active and active.triggered:
|
|
430
|
+
add(
|
|
431
|
+
Stage.POISONED_WELL,
|
|
432
|
+
WSEventType.OVERRIDE,
|
|
433
|
+
{
|
|
434
|
+
"kind": "poisoned_well",
|
|
435
|
+
"message": "CANARY MARKER DECODED → CONFIRMED",
|
|
436
|
+
"marker": active.canary_marker,
|
|
437
|
+
"c2": active.c2_indicators,
|
|
438
|
+
"declaredIntent": active.declared_intent,
|
|
439
|
+
"contextAwareCanary": (
|
|
440
|
+
active.context_aware_canary.value
|
|
441
|
+
if active.context_aware_canary
|
|
442
|
+
else None
|
|
443
|
+
),
|
|
444
|
+
},
|
|
445
|
+
delta=260,
|
|
446
|
+
skill=item.name,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if active and active.sandbox_evasion_detected:
|
|
450
|
+
add(
|
|
451
|
+
Stage.EVASION,
|
|
452
|
+
WSEventType.OVERRIDE,
|
|
453
|
+
{
|
|
454
|
+
"kind": "evasion",
|
|
455
|
+
"message": "SANDBOX EVASION DETECTED",
|
|
456
|
+
"probes": ["/.dockerenv"],
|
|
457
|
+
},
|
|
458
|
+
delta=140,
|
|
459
|
+
skill=item.name,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
add(
|
|
463
|
+
Stage.VERDICT,
|
|
464
|
+
WSEventType.COMPLETED,
|
|
465
|
+
{
|
|
466
|
+
"verdict": verdict.model_dump(by_alias=True, mode="json"),
|
|
467
|
+
"durationMs": verdict.scan_duration_ms,
|
|
468
|
+
},
|
|
469
|
+
delta=160,
|
|
470
|
+
skill=item.name,
|
|
471
|
+
)
|
|
472
|
+
return events
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
__all__ = ["DemoBackend"]
|