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,299 @@
|
|
|
1
|
+
"""Configuration loading for SkillSentry.
|
|
2
|
+
|
|
3
|
+
Precedence (docs/07-cli-reference.md §7.3): CLI flags > environment > file >
|
|
4
|
+
defaults. The file is ``skillsentry.toml`` in the working directory, or the
|
|
5
|
+
path supplied via ``--config`` / ``SKILLSENTRY_CONFIG``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tomllib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
16
|
+
|
|
17
|
+
from skill_sentry_cli.core.version import DEFAULT_WEIGHTS, engine_version
|
|
18
|
+
|
|
19
|
+
APP_DIR_NAME = ".skillsentry"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigError(RuntimeError):
|
|
23
|
+
"""Raised when the configuration file is invalid."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _ConfigModel(BaseModel):
|
|
27
|
+
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelSettings(_ConfigModel):
|
|
31
|
+
backend: str = "auto" # auto | local | cloud | demo
|
|
32
|
+
ollama_url: str = "http://localhost:11434"
|
|
33
|
+
ollama_model: str = "gemma4:e4b-it-qat"
|
|
34
|
+
gemini_model: str = "gemma-4-e4b"
|
|
35
|
+
image_max_px: int = Field(default=768, ge=256, le=4096)
|
|
36
|
+
request_timeout_s: float = Field(default=180.0, gt=0)
|
|
37
|
+
ollama_num_ctx: int = Field(default=4096, ge=512, le=32768)
|
|
38
|
+
ollama_num_predict: int = Field(default=768, ge=1, le=4096)
|
|
39
|
+
ollama_keep_alive: str = "30m"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DecisionSettings(_ConfigModel):
|
|
43
|
+
"""Typed semantic decision backend settings.
|
|
44
|
+
|
|
45
|
+
``model`` remains the legacy generation/vision client selection. This
|
|
46
|
+
section controls the Laya-style decision layer independently.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Opt-in: Laya stays off until its calibration gates pass (docs/13 §13.20).
|
|
50
|
+
backend: str = "legacy" # auto | laya | legacy | none
|
|
51
|
+
checkpoint: str = "auto" # auto | english | multilingual | typed-decisions
|
|
52
|
+
device: str = "auto" # auto | cpu | cuda | mps
|
|
53
|
+
model_path: str = ""
|
|
54
|
+
timeout_s: float = Field(default=30.0, gt=0, le=600)
|
|
55
|
+
max_len: int = Field(default=1024, ge=64, le=8192)
|
|
56
|
+
head_max_len: int = Field(default=256, ge=16, le=4096)
|
|
57
|
+
calibration_path: str = ""
|
|
58
|
+
calibration_version: str = ""
|
|
59
|
+
required: bool = False
|
|
60
|
+
|
|
61
|
+
@field_validator("backend")
|
|
62
|
+
@classmethod
|
|
63
|
+
def _validate_backend(cls, value: str) -> str:
|
|
64
|
+
normalised = value.strip().lower()
|
|
65
|
+
allowed = {"auto", "laya", "legacy", "none"}
|
|
66
|
+
if normalised not in allowed:
|
|
67
|
+
raise ValueError(f"backend must be one of {sorted(allowed)}")
|
|
68
|
+
return normalised
|
|
69
|
+
|
|
70
|
+
@field_validator("checkpoint")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _validate_checkpoint(cls, value: str) -> str:
|
|
73
|
+
normalised = value.strip().lower()
|
|
74
|
+
allowed = {"auto", "english", "multilingual", "typed-decisions"}
|
|
75
|
+
if normalised not in allowed:
|
|
76
|
+
raise ValueError(f"checkpoint must be one of {sorted(allowed)}")
|
|
77
|
+
return normalised
|
|
78
|
+
|
|
79
|
+
@field_validator("device")
|
|
80
|
+
@classmethod
|
|
81
|
+
def _validate_device(cls, value: str) -> str:
|
|
82
|
+
normalised = value.strip().lower()
|
|
83
|
+
allowed = {"auto", "cpu", "cuda", "mps"}
|
|
84
|
+
if normalised not in allowed:
|
|
85
|
+
raise ValueError(f"device must be one of {sorted(allowed)}")
|
|
86
|
+
return normalised
|
|
87
|
+
|
|
88
|
+
@model_validator(mode="after")
|
|
89
|
+
def _validate_token_budgets(self) -> DecisionSettings:
|
|
90
|
+
if self.head_max_len > self.max_len:
|
|
91
|
+
raise ValueError("head_max_len must not exceed max_len")
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def cache_identity(self) -> str:
|
|
96
|
+
"""Stable identity for cache invalidation when the decision layer changes."""
|
|
97
|
+
|
|
98
|
+
calibration = self.calibration_version.strip() or "uncalibrated"
|
|
99
|
+
return f"{self.backend}:{self.checkpoint}:{calibration}:{self.max_len}:{self.head_max_len}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SandboxSettings(_ConfigModel):
|
|
103
|
+
enabled: bool = True
|
|
104
|
+
base_image: str = "skillsentry-sandbox:local"
|
|
105
|
+
timeout_s: int = Field(default=60, ge=5, le=600)
|
|
106
|
+
memory_mb: int = Field(default=512, ge=128)
|
|
107
|
+
cpus: float = Field(default=1.0, gt=0)
|
|
108
|
+
masquerade: bool = True
|
|
109
|
+
max_parallel: int = Field(default=1, ge=1, le=8)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PoisonedWellSettings(_ConfigModel):
|
|
113
|
+
enabled: bool = True
|
|
114
|
+
correlation_window_s: float = Field(default=10.0, gt=0)
|
|
115
|
+
secondary_canary: bool = True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TierSettings(_ConfigModel):
|
|
119
|
+
safe_max: int = Field(default=30, ge=0, le=100)
|
|
120
|
+
malicious_min: int = Field(default=70, ge=0, le=100)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ScoringSettings(_ConfigModel):
|
|
124
|
+
weights: dict[str, float] = Field(default_factory=lambda: dict(DEFAULT_WEIGHTS))
|
|
125
|
+
typosquat_bonus: int = Field(default=15, ge=0, le=100)
|
|
126
|
+
tiers: TierSettings = Field(default_factory=TierSettings)
|
|
127
|
+
|
|
128
|
+
def weight_fingerprint_input(self) -> dict[str, float]:
|
|
129
|
+
return dict(self.weights)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CacheSettings(_ConfigModel):
|
|
133
|
+
db_path: str = "~/.skillsentry/skillsentry.db"
|
|
134
|
+
backend_strict: bool = False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CISettings(_ConfigModel):
|
|
138
|
+
fail_on: str = "malicious"
|
|
139
|
+
upload_sarif: bool = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SkillsEntryConfig(_ConfigModel):
|
|
143
|
+
model: ModelSettings = Field(default_factory=ModelSettings)
|
|
144
|
+
decision: DecisionSettings = Field(default_factory=DecisionSettings)
|
|
145
|
+
sandbox: SandboxSettings = Field(default_factory=SandboxSettings)
|
|
146
|
+
poisoned_well: PoisonedWellSettings = Field(default_factory=PoisonedWellSettings)
|
|
147
|
+
scoring: ScoringSettings = Field(default_factory=ScoringSettings)
|
|
148
|
+
cache: CacheSettings = Field(default_factory=CacheSettings)
|
|
149
|
+
ci: CISettings = Field(default_factory=CISettings)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def db_path(self) -> Path:
|
|
153
|
+
return Path(os.path.expandvars(os.path.expanduser(self.cache.db_path))).resolve()
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def engine_version(self) -> str:
|
|
157
|
+
return engine_version(self.scoring.weights)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
DEFAULT_CONFIG_TOML = """\
|
|
161
|
+
# SkillSentry 2.0 configuration — generated by `skill-sentry-cli init`.
|
|
162
|
+
# Precedence: CLI flags > environment > this file > built-in defaults.
|
|
163
|
+
|
|
164
|
+
[model]
|
|
165
|
+
backend = "auto" # auto | local | cloud | demo
|
|
166
|
+
ollama_url = "http://localhost:11434"
|
|
167
|
+
ollama_model = "gemma4:e4b-it-qat"
|
|
168
|
+
gemini_model = "gemma-4-e4b" # used only when backend = cloud/auto-fallback
|
|
169
|
+
image_max_px = 1024
|
|
170
|
+
request_timeout_s = 180
|
|
171
|
+
|
|
172
|
+
[decision]
|
|
173
|
+
backend = "legacy" # auto | laya | legacy | none (laya is opt-in until calibrated)
|
|
174
|
+
checkpoint = "auto" # auto | english | multilingual | typed-decisions
|
|
175
|
+
device = "auto" # auto | cpu | cuda | mps
|
|
176
|
+
model_path = "" # optional offline checkpoint directory
|
|
177
|
+
timeout_s = 30
|
|
178
|
+
max_len = 1024
|
|
179
|
+
head_max_len = 256
|
|
180
|
+
calibration_path = ""
|
|
181
|
+
calibration_version = ""
|
|
182
|
+
required = false
|
|
183
|
+
|
|
184
|
+
[sandbox]
|
|
185
|
+
enabled = true
|
|
186
|
+
base_image = "skillsentry-sandbox:local" # built from assets/Dockerfile.sandbox
|
|
187
|
+
timeout_s = 60
|
|
188
|
+
memory_mb = 512
|
|
189
|
+
cpus = 1.0
|
|
190
|
+
masquerade = true
|
|
191
|
+
max_parallel = 1
|
|
192
|
+
|
|
193
|
+
[poisoned_well]
|
|
194
|
+
enabled = true
|
|
195
|
+
correlation_window_s = 10
|
|
196
|
+
secondary_canary = true
|
|
197
|
+
|
|
198
|
+
[scoring]
|
|
199
|
+
weights = { anchors = 0.15, semantic = 0.20, vision = 0.20, sandbox = 0.25 }
|
|
200
|
+
typosquat_bonus = 15
|
|
201
|
+
tiers = { safe_max = 30, malicious_min = 70 }
|
|
202
|
+
|
|
203
|
+
[cache]
|
|
204
|
+
db_path = "~/.skillsentry/skillsentry.db"
|
|
205
|
+
backend_strict = false
|
|
206
|
+
|
|
207
|
+
[ci]
|
|
208
|
+
fail_on = "malicious"
|
|
209
|
+
upload_sarif = false
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def settings_dir() -> Path:
|
|
214
|
+
return Path.home() / APP_DIR_NAME
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def reports_dir(scan_id: str | None = None) -> Path:
|
|
218
|
+
base = settings_dir() / "reports"
|
|
219
|
+
return base / scan_id if scan_id else base
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
merged = dict(base)
|
|
224
|
+
for key, value in override.items():
|
|
225
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
226
|
+
merged[key] = _deep_merge(merged[key], value)
|
|
227
|
+
else:
|
|
228
|
+
merged[key] = value
|
|
229
|
+
return merged
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _env_overrides() -> dict[str, Any]:
|
|
233
|
+
overrides: dict[str, Any] = {}
|
|
234
|
+
|
|
235
|
+
def set_value(section: str, key: str, value: Any) -> None:
|
|
236
|
+
overrides.setdefault(section, {})[key] = value
|
|
237
|
+
|
|
238
|
+
if backend := os.getenv("SKILLSENTRY_BACKEND"):
|
|
239
|
+
set_value("model", "backend", backend)
|
|
240
|
+
if decision_backend := os.getenv("SKILLSENTRY_DECISION_BACKEND"):
|
|
241
|
+
set_value("decision", "backend", decision_backend)
|
|
242
|
+
if checkpoint := os.getenv("SKILLSENTRY_LAYA_CHECKPOINT"):
|
|
243
|
+
set_value("decision", "checkpoint", checkpoint)
|
|
244
|
+
if device := os.getenv("SKILLSENTRY_LAYA_DEVICE"):
|
|
245
|
+
set_value("decision", "device", device)
|
|
246
|
+
if model_path := os.getenv("SKILLSENTRY_LAYA_MODEL_PATH"):
|
|
247
|
+
set_value("decision", "model_path", model_path)
|
|
248
|
+
if os.getenv("SKILLSENTRY_LAYA_REQUIRED") == "1":
|
|
249
|
+
set_value("decision", "required", True)
|
|
250
|
+
if os.getenv("SKILLSENTRY_NO_SANDBOX") == "1":
|
|
251
|
+
set_value("sandbox", "enabled", False)
|
|
252
|
+
if db_path := os.getenv("SKILLSENTRY_DB"):
|
|
253
|
+
set_value("cache", "db_path", db_path)
|
|
254
|
+
if ollama_host := os.getenv("OLLAMA_HOST"):
|
|
255
|
+
set_value("model", "ollama_url", ollama_host)
|
|
256
|
+
return overrides
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def default_config() -> SkillsEntryConfig:
|
|
260
|
+
return SkillsEntryConfig()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def load_config(path: str | Path | None = None) -> SkillsEntryConfig:
|
|
264
|
+
"""Load config from TOML, then apply environment overrides.
|
|
265
|
+
|
|
266
|
+
A missing file is not an error: defaults are used (except when the caller
|
|
267
|
+
explicitly passes a path, in which case a missing file raises).
|
|
268
|
+
"""
|
|
269
|
+
# A path named by the environment is as explicit as one passed in: a typo must not
|
|
270
|
+
# silently fall back to defaults.
|
|
271
|
+
env_path = os.getenv("SKILLSENTRY_CONFIG")
|
|
272
|
+
explicit = path is not None or bool(env_path)
|
|
273
|
+
if path is None:
|
|
274
|
+
path = env_path or Path.cwd() / "skillsentry.toml"
|
|
275
|
+
config_path = Path(path).expanduser()
|
|
276
|
+
|
|
277
|
+
data: dict[str, Any] = {}
|
|
278
|
+
if config_path.exists():
|
|
279
|
+
try:
|
|
280
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
281
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
282
|
+
raise ConfigError(f"could not read {config_path}: {exc}") from exc
|
|
283
|
+
elif explicit:
|
|
284
|
+
raise ConfigError(f"config file not found: {config_path}")
|
|
285
|
+
|
|
286
|
+
data = _deep_merge(data, _env_overrides())
|
|
287
|
+
try:
|
|
288
|
+
return SkillsEntryConfig.model_validate(data)
|
|
289
|
+
except Exception as exc: # pydantic ValidationError
|
|
290
|
+
raise ConfigError(f"invalid configuration in {config_path}: {exc}") from exc
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def write_default_config(path: str | Path, *, force: bool = False) -> Path:
|
|
294
|
+
target = Path(path).expanduser()
|
|
295
|
+
if target.exists() and not force:
|
|
296
|
+
raise ConfigError(f"{target} already exists (use --force to overwrite)")
|
|
297
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
target.write_text(DEFAULT_CONFIG_TOML, encoding="utf-8")
|
|
299
|
+
return target
|