cisco-ai-skill-scanner 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.
Files changed (100) hide show
  1. cisco_ai_skill_scanner-1.0.0.dist-info/METADATA +253 -0
  2. cisco_ai_skill_scanner-1.0.0.dist-info/RECORD +100 -0
  3. cisco_ai_skill_scanner-1.0.0.dist-info/WHEEL +4 -0
  4. cisco_ai_skill_scanner-1.0.0.dist-info/entry_points.txt +4 -0
  5. cisco_ai_skill_scanner-1.0.0.dist-info/licenses/LICENSE +17 -0
  6. skillanalyzer/__init__.py +45 -0
  7. skillanalyzer/_version.py +34 -0
  8. skillanalyzer/api/__init__.py +25 -0
  9. skillanalyzer/api/api.py +34 -0
  10. skillanalyzer/api/api_cli.py +78 -0
  11. skillanalyzer/api/api_server.py +634 -0
  12. skillanalyzer/api/router.py +527 -0
  13. skillanalyzer/cli/__init__.py +25 -0
  14. skillanalyzer/cli/cli.py +816 -0
  15. skillanalyzer/config/__init__.py +26 -0
  16. skillanalyzer/config/config.py +149 -0
  17. skillanalyzer/config/config_parser.py +122 -0
  18. skillanalyzer/config/constants.py +85 -0
  19. skillanalyzer/core/__init__.py +24 -0
  20. skillanalyzer/core/analyzers/__init__.py +75 -0
  21. skillanalyzer/core/analyzers/aidefense_analyzer.py +872 -0
  22. skillanalyzer/core/analyzers/base.py +53 -0
  23. skillanalyzer/core/analyzers/behavioral/__init__.py +30 -0
  24. skillanalyzer/core/analyzers/behavioral/alignment/__init__.py +45 -0
  25. skillanalyzer/core/analyzers/behavioral/alignment/alignment_llm_client.py +240 -0
  26. skillanalyzer/core/analyzers/behavioral/alignment/alignment_orchestrator.py +216 -0
  27. skillanalyzer/core/analyzers/behavioral/alignment/alignment_prompt_builder.py +422 -0
  28. skillanalyzer/core/analyzers/behavioral/alignment/alignment_response_validator.py +136 -0
  29. skillanalyzer/core/analyzers/behavioral/alignment/threat_vulnerability_classifier.py +198 -0
  30. skillanalyzer/core/analyzers/behavioral_analyzer.py +453 -0
  31. skillanalyzer/core/analyzers/cross_skill_analyzer.py +490 -0
  32. skillanalyzer/core/analyzers/llm_analyzer.py +440 -0
  33. skillanalyzer/core/analyzers/llm_prompt_builder.py +270 -0
  34. skillanalyzer/core/analyzers/llm_provider_config.py +215 -0
  35. skillanalyzer/core/analyzers/llm_request_handler.py +284 -0
  36. skillanalyzer/core/analyzers/llm_response_parser.py +81 -0
  37. skillanalyzer/core/analyzers/meta_analyzer.py +845 -0
  38. skillanalyzer/core/analyzers/static.py +1105 -0
  39. skillanalyzer/core/analyzers/trigger_analyzer.py +341 -0
  40. skillanalyzer/core/analyzers/virustotal_analyzer.py +463 -0
  41. skillanalyzer/core/exceptions.py +77 -0
  42. skillanalyzer/core/loader.py +377 -0
  43. skillanalyzer/core/models.py +300 -0
  44. skillanalyzer/core/reporters/__init__.py +26 -0
  45. skillanalyzer/core/reporters/json_reporter.py +65 -0
  46. skillanalyzer/core/reporters/markdown_reporter.py +209 -0
  47. skillanalyzer/core/reporters/sarif_reporter.py +246 -0
  48. skillanalyzer/core/reporters/table_reporter.py +195 -0
  49. skillanalyzer/core/rules/__init__.py +19 -0
  50. skillanalyzer/core/rules/patterns.py +165 -0
  51. skillanalyzer/core/rules/yara_scanner.py +157 -0
  52. skillanalyzer/core/scanner.py +437 -0
  53. skillanalyzer/core/static_analysis/__init__.py +27 -0
  54. skillanalyzer/core/static_analysis/cfg/__init__.py +21 -0
  55. skillanalyzer/core/static_analysis/cfg/builder.py +439 -0
  56. skillanalyzer/core/static_analysis/context_extractor.py +742 -0
  57. skillanalyzer/core/static_analysis/dataflow/__init__.py +25 -0
  58. skillanalyzer/core/static_analysis/dataflow/forward_analysis.py +715 -0
  59. skillanalyzer/core/static_analysis/interprocedural/__init__.py +21 -0
  60. skillanalyzer/core/static_analysis/interprocedural/call_graph_analyzer.py +406 -0
  61. skillanalyzer/core/static_analysis/interprocedural/cross_file_analyzer.py +190 -0
  62. skillanalyzer/core/static_analysis/parser/__init__.py +21 -0
  63. skillanalyzer/core/static_analysis/parser/python_parser.py +380 -0
  64. skillanalyzer/core/static_analysis/semantic/__init__.py +28 -0
  65. skillanalyzer/core/static_analysis/semantic/name_resolver.py +206 -0
  66. skillanalyzer/core/static_analysis/semantic/type_analyzer.py +200 -0
  67. skillanalyzer/core/static_analysis/taint/__init__.py +21 -0
  68. skillanalyzer/core/static_analysis/taint/tracker.py +252 -0
  69. skillanalyzer/core/static_analysis/types/__init__.py +36 -0
  70. skillanalyzer/data/__init__.py +30 -0
  71. skillanalyzer/data/prompts/boilerplate_protection_rule_prompt.md +26 -0
  72. skillanalyzer/data/prompts/code_alignment_threat_analysis_prompt.md +901 -0
  73. skillanalyzer/data/prompts/llm_response_schema.json +71 -0
  74. skillanalyzer/data/prompts/skill_meta_analysis_prompt.md +303 -0
  75. skillanalyzer/data/prompts/skill_threat_analysis_prompt.md +263 -0
  76. skillanalyzer/data/prompts/unified_response_schema.md +97 -0
  77. skillanalyzer/data/rules/signatures.yaml +440 -0
  78. skillanalyzer/data/yara_rules/autonomy_abuse.yara +66 -0
  79. skillanalyzer/data/yara_rules/code_execution.yara +61 -0
  80. skillanalyzer/data/yara_rules/coercive_injection.yara +115 -0
  81. skillanalyzer/data/yara_rules/command_injection.yara +54 -0
  82. skillanalyzer/data/yara_rules/credential_harvesting.yara +115 -0
  83. skillanalyzer/data/yara_rules/prompt_injection.yara +71 -0
  84. skillanalyzer/data/yara_rules/script_injection.yara +83 -0
  85. skillanalyzer/data/yara_rules/skill_discovery_abuse.yara +57 -0
  86. skillanalyzer/data/yara_rules/sql_injection.yara +73 -0
  87. skillanalyzer/data/yara_rules/system_manipulation.yara +65 -0
  88. skillanalyzer/data/yara_rules/tool_chaining_abuse.yara +60 -0
  89. skillanalyzer/data/yara_rules/transitive_trust_abuse.yara +73 -0
  90. skillanalyzer/data/yara_rules/unicode_steganography.yara +65 -0
  91. skillanalyzer/hooks/__init__.py +21 -0
  92. skillanalyzer/hooks/pre_commit.py +450 -0
  93. skillanalyzer/threats/__init__.py +25 -0
  94. skillanalyzer/threats/threats.py +480 -0
  95. skillanalyzer/utils/__init__.py +28 -0
  96. skillanalyzer/utils/command_utils.py +129 -0
  97. skillanalyzer/utils/di_container.py +154 -0
  98. skillanalyzer/utils/file_utils.py +86 -0
  99. skillanalyzer/utils/logging_config.py +96 -0
  100. skillanalyzer/utils/logging_utils.py +71 -0
@@ -0,0 +1,463 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+ """
17
+ VirusTotal analyzer for scanning binary files using hash-based lookups.
18
+
19
+ This analyzer checks binary files (images, PDFs, archives, etc.) against
20
+ VirusTotal's database using SHA256 hash lookups. It does NOT scan code files
21
+ like Python, JavaScript, or Markdown files.
22
+ """
23
+
24
+ import hashlib
25
+ import logging
26
+ from pathlib import Path
27
+
28
+ import httpx
29
+
30
+ from ..models import Finding, Severity, Skill, ThreatCategory
31
+ from .base import BaseAnalyzer
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class VirusTotalAnalyzer(BaseAnalyzer):
37
+ """
38
+ Analyzer that checks binary files against VirusTotal using hash lookups.
39
+
40
+ Only scans binary file types (images, PDFs, executables, archives).
41
+ Excludes text-based code files (.py, .js, .md, .txt, .json, .yaml, etc.).
42
+ """
43
+
44
+ # Binary file extensions to scan
45
+ BINARY_EXTENSIONS = {
46
+ # Images
47
+ ".png",
48
+ ".jpg",
49
+ ".jpeg",
50
+ ".gif",
51
+ ".bmp",
52
+ ".ico",
53
+ ".svg",
54
+ ".webp",
55
+ ".tiff",
56
+ # Documents
57
+ ".pdf",
58
+ ".doc",
59
+ ".docx",
60
+ ".xls",
61
+ ".xlsx",
62
+ ".ppt",
63
+ ".pptx",
64
+ # Archives
65
+ ".zip",
66
+ ".tar",
67
+ ".gz",
68
+ ".bz2",
69
+ ".7z",
70
+ ".rar",
71
+ ".tgz",
72
+ # Executables
73
+ ".exe",
74
+ ".dll",
75
+ ".so",
76
+ ".dylib",
77
+ ".bin",
78
+ ".com", # .com = MS-DOS executables
79
+ # Other binaries
80
+ ".wasm",
81
+ ".class",
82
+ ".jar",
83
+ ".war",
84
+ }
85
+
86
+ # Text/code extensions to EXCLUDE from scanning
87
+ EXCLUDED_EXTENSIONS = {
88
+ ".py",
89
+ ".js",
90
+ ".ts",
91
+ ".jsx",
92
+ ".tsx",
93
+ ".java",
94
+ ".c",
95
+ ".cpp",
96
+ ".h",
97
+ ".hpp",
98
+ ".go",
99
+ ".rs",
100
+ ".rb",
101
+ ".php",
102
+ ".swift",
103
+ ".kt",
104
+ ".cs",
105
+ ".vb",
106
+ ".md",
107
+ ".txt",
108
+ ".json",
109
+ ".yaml",
110
+ ".yml",
111
+ ".toml",
112
+ ".ini",
113
+ ".conf",
114
+ ".cfg",
115
+ ".xml",
116
+ ".html",
117
+ ".css",
118
+ ".scss",
119
+ ".sass",
120
+ ".less",
121
+ ".sh",
122
+ ".bash",
123
+ ".zsh",
124
+ ".fish",
125
+ ".ps1",
126
+ ".bat",
127
+ ".cmd",
128
+ ".sql",
129
+ ".graphql",
130
+ ".proto",
131
+ ".thrift",
132
+ ".rst",
133
+ ".org",
134
+ ".adoc",
135
+ ".tex",
136
+ }
137
+
138
+ def __init__(self, api_key: str | None = None, enabled: bool = True, upload_files: bool = False):
139
+ """
140
+ Initialize VirusTotal analyzer.
141
+
142
+ Args:
143
+ api_key: VirusTotal API key (optional, can be set via environment)
144
+ enabled: Whether the analyzer is enabled (default: True)
145
+ upload_files: If True, upload files to VT for scanning. If False (default),
146
+ only check existing hashes (more privacy-friendly)
147
+ """
148
+ super().__init__("virustotal_analyzer")
149
+ self.api_key = api_key
150
+ self.enabled = enabled and api_key is not None
151
+ self.upload_files = upload_files
152
+ self.validated_binary_files = [] # Track files validated as safe by VirusTotal
153
+ self.base_url = "https://www.virustotal.com/api/v3"
154
+ self.session = httpx.Client()
155
+
156
+ if not self.api_key:
157
+ logger.warning("VirusTotal API key is missing!")
158
+
159
+ if self.api_key:
160
+ self.session.headers.update({"x-apikey": self.api_key, "Accept": "application/json"})
161
+ logger.info("VirusTotal API key configured (length: %d)", len(self.api_key))
162
+ else:
163
+ logger.warning("VirusTotal analyzer initialized without API key")
164
+
165
+ def analyze(self, skill: Skill) -> list[Finding]:
166
+ """
167
+ Analyze binary files in the skill using VirusTotal hash lookups.
168
+
169
+ Args:
170
+ skill: The skill to analyze
171
+
172
+ Returns:
173
+ List of findings for malicious files. Also stores validated file paths
174
+ in skill metadata to allow suppression of binary file warnings.
175
+ """
176
+ if not self.enabled:
177
+ return []
178
+
179
+ findings = []
180
+ validated_files = [] # Track files validated as safe
181
+
182
+ # Only scan binary files
183
+ binary_files = [f for f in skill.files if self._is_binary_file(f.relative_path)]
184
+
185
+ for skill_file in binary_files:
186
+ try:
187
+ file_path = Path(skill.directory) / skill_file.relative_path
188
+ file_hash = self._calculate_sha256(file_path)
189
+
190
+ logger.info("Checking file: %s (SHA256: %s)", skill_file.relative_path, file_hash)
191
+
192
+ vt_result, hash_found = self._query_virustotal(file_hash)
193
+
194
+ if hash_found:
195
+ total = vt_result.get("total_engines", 0)
196
+ malicious = vt_result.get("malicious", 0)
197
+ suspicious = vt_result.get("suspicious", 0)
198
+
199
+ if malicious > 0 or suspicious > 0:
200
+ logger.warning(
201
+ "Found in VT database: %d malicious, %d suspicious out of %d vendors",
202
+ malicious,
203
+ suspicious,
204
+ total,
205
+ )
206
+ else:
207
+ logger.info("Found in VT database: %d/%d vendors flagged (file appears safe)", malicious, total)
208
+ validated_files.append(skill_file.relative_path)
209
+
210
+ if vt_result.get("permalink"):
211
+ logger.info("Report: %s", vt_result["permalink"])
212
+
213
+ if malicious > 0:
214
+ findings.append(
215
+ self._create_finding(skill_file=skill_file, file_hash=file_hash, vt_result=vt_result)
216
+ )
217
+ elif self.upload_files:
218
+ logger.warning("Hash not found in VT database - uploading for analysis")
219
+ vt_result = self._upload_and_scan(file_path, file_hash)
220
+
221
+ if vt_result:
222
+ if vt_result.get("malicious", 0) > 0:
223
+ findings.append(
224
+ self._create_finding(skill_file=skill_file, file_hash=file_hash, vt_result=vt_result)
225
+ )
226
+ else:
227
+ validated_files.append(skill_file.relative_path)
228
+ else:
229
+ logger.warning("Hash not found in VT database - upload disabled, cannot scan unknown file")
230
+
231
+ except Exception as e:
232
+ logger.warning("VirusTotal scan failed for %s: %s", skill_file.relative_path, e)
233
+ continue
234
+
235
+ # Store validated files in analyzer instance for post-processing
236
+ self.validated_binary_files = validated_files
237
+
238
+ return findings
239
+
240
+ def _is_binary_file(self, file_path: str) -> bool:
241
+ """
242
+ Check if a file should be scanned (is binary, not code).
243
+
244
+ Args:
245
+ file_path: Path to the file
246
+
247
+ Returns:
248
+ True if file should be scanned
249
+ """
250
+ path = Path(file_path)
251
+ ext = path.suffix.lower()
252
+
253
+ # Explicitly exclude text/code files
254
+ if ext in self.EXCLUDED_EXTENSIONS:
255
+ return False
256
+
257
+ # Include known binary extensions
258
+ if ext in self.BINARY_EXTENSIONS:
259
+ return True
260
+
261
+ # For unknown extensions, default to not scanning
262
+ # (conservative approach to avoid scanning code files)
263
+ return False
264
+
265
+ def _calculate_sha256(self, file_path: Path) -> str:
266
+ """
267
+ Calculate SHA256 hash of a file.
268
+
269
+ Args:
270
+ file_path: Path to the file
271
+
272
+ Returns:
273
+ SHA256 hash as hex string
274
+ """
275
+ sha256_hash = hashlib.sha256()
276
+ with open(file_path, "rb") as f:
277
+ # Read file in chunks for memory efficiency
278
+ for byte_block in iter(lambda: f.read(4096), b""):
279
+ sha256_hash.update(byte_block)
280
+ return sha256_hash.hexdigest()
281
+
282
+ def _query_virustotal(self, file_hash: str) -> tuple[dict | None, bool]:
283
+ """
284
+ Query VirusTotal API for file hash.
285
+
286
+ Args:
287
+ file_hash: SHA256 hash of the file
288
+
289
+ Returns:
290
+ Tuple of (detection stats dictionary or None, hash_found boolean)
291
+ - If hash found: (stats_dict, True)
292
+ - If hash not found (404): (None, False)
293
+ - If error: (None, False)
294
+ """
295
+ try:
296
+ response = self.session.get(f"{self.base_url}/files/{file_hash}", timeout=10)
297
+
298
+ if response.status_code == 404:
299
+ # File hash not in VirusTotal database (never scanned before)
300
+ return None, False
301
+
302
+ if response.status_code == 200:
303
+ data = response.json()
304
+
305
+ # Extract detection statistics
306
+ stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
307
+
308
+ # Construct GUI URL (not API endpoint)
309
+ gui_url = f"https://www.virustotal.com/gui/file/{file_hash}"
310
+
311
+ result = {
312
+ "malicious": stats.get("malicious", 0),
313
+ "suspicious": stats.get("suspicious", 0),
314
+ "undetected": stats.get("undetected", 0),
315
+ "harmless": stats.get("harmless", 0),
316
+ "total_engines": sum(stats.values()),
317
+ "scan_date": data.get("data", {}).get("attributes", {}).get("last_analysis_date"),
318
+ "permalink": gui_url,
319
+ }
320
+
321
+ return result, True
322
+
323
+ if response.status_code == 429:
324
+ logger.warning("VirusTotal rate limit exceeded. Please wait before retrying.")
325
+ else:
326
+ logger.warning("VirusTotal API returned status %d", response.status_code)
327
+ return None, False
328
+
329
+ except httpx.RequestError as e:
330
+ logger.warning("VirusTotal API request failed: %s", e)
331
+ return None, False
332
+
333
+ def _upload_and_scan(self, file_path: Path, file_hash: str) -> dict | None:
334
+ """
335
+ Upload file to VirusTotal for scanning.
336
+
337
+ Args:
338
+ file_path: Path to the file to upload
339
+ file_hash: SHA256 hash of the file
340
+
341
+ Returns:
342
+ Dictionary with detection stats or None if upload failed
343
+ """
344
+ try:
345
+ import time
346
+
347
+ file_size = file_path.stat().st_size
348
+ if file_size > 32 * 1024 * 1024:
349
+ logger.warning("File too large to upload to VT: %s (%d bytes)", file_path.name, file_size)
350
+ return None
351
+
352
+ with open(file_path, "rb") as f:
353
+ files = {"file": (file_path.name, f)}
354
+ response = self.session.post(f"{self.base_url}/files", files=files, timeout=60)
355
+
356
+ if response.status_code != 200:
357
+ logger.warning("File upload failed with status %d", response.status_code)
358
+ return None
359
+
360
+ upload_data = response.json()
361
+ analysis_id = upload_data.get("data", {}).get("id")
362
+
363
+ if not analysis_id:
364
+ logger.warning("No analysis ID returned from upload")
365
+ return None
366
+
367
+ logger.info("File uploaded successfully. Analysis ID: %s", analysis_id)
368
+
369
+ max_retries = 6
370
+ for attempt in range(max_retries):
371
+ time.sleep(10)
372
+
373
+ analysis_response = self.session.get(f"{self.base_url}/analyses/{analysis_id}", timeout=10)
374
+
375
+ if analysis_response.status_code == 200:
376
+ analysis_data = analysis_response.json()
377
+ status = analysis_data.get("data", {}).get("attributes", {}).get("status")
378
+ stats = analysis_data.get("data", {}).get("attributes", {}).get("stats", {})
379
+
380
+ if status == "completed":
381
+ result, _ = self._query_virustotal(file_hash)
382
+ if result and result.get("total_engines", 0) > 0:
383
+ logger.info(
384
+ "Analysis complete: %d/%d vendors scanned",
385
+ result.get("malicious", 0),
386
+ result.get("total_engines", 0),
387
+ )
388
+ return result
389
+ else:
390
+ total_scans = sum(stats.values()) if stats else 0
391
+ logger.info(
392
+ "Status: %s (%d engines scanned, attempt %d/%d)",
393
+ status,
394
+ total_scans,
395
+ attempt + 1,
396
+ max_retries,
397
+ )
398
+ else:
399
+ logger.warning("Analysis query failed with status %d", analysis_response.status_code)
400
+
401
+ logger.warning("Analysis still processing after %d seconds", max_retries * 10)
402
+ result, _ = self._query_virustotal(file_hash)
403
+ return result
404
+
405
+ except httpx.RequestError as e:
406
+ logger.warning("File upload to VirusTotal failed: %s", e)
407
+ return None
408
+ except Exception as e:
409
+ logger.warning("Unexpected error during file upload: %s", e)
410
+ return None
411
+
412
+ def _create_finding(self, skill_file, file_hash: str, vt_result: dict) -> Finding:
413
+ """
414
+ Create a finding for a malicious file.
415
+
416
+ Args:
417
+ skill_file: The SkillFile object
418
+ file_hash: SHA256 hash of the file
419
+ vt_result: VirusTotal scan results
420
+
421
+ Returns:
422
+ Finding object
423
+ """
424
+ malicious_count = vt_result.get("malicious", 0)
425
+ total_engines = vt_result.get("total_engines", 0)
426
+
427
+ # Determine severity based on detection ratio
428
+ if total_engines > 0:
429
+ detection_ratio = malicious_count / total_engines
430
+ if detection_ratio >= 0.3: # 30%+ detection rate
431
+ severity = Severity.CRITICAL
432
+ elif detection_ratio >= 0.1: # 10-30% detection rate
433
+ severity = Severity.HIGH
434
+ else:
435
+ severity = Severity.MEDIUM
436
+ else:
437
+ severity = Severity.MEDIUM
438
+
439
+ return Finding(
440
+ id=f"VT_{file_hash[:8]}",
441
+ rule_id="VIRUSTOTAL_MALICIOUS_FILE",
442
+ category=ThreatCategory.MALWARE,
443
+ severity=severity,
444
+ title=f"Malicious file detected: {skill_file.relative_path}",
445
+ description=(
446
+ f"VirusTotal detected this file as malicious. "
447
+ f"{malicious_count}/{total_engines} security vendors flagged this file. "
448
+ f"SHA256: {file_hash}"
449
+ ),
450
+ file_path=skill_file.relative_path,
451
+ line_number=None,
452
+ snippet=f"File hash: {file_hash}",
453
+ remediation=(
454
+ "Remove this file from the skill package. "
455
+ "Binary files flagged by multiple antivirus engines should not be included."
456
+ ),
457
+ analyzer="virustotal",
458
+ metadata={
459
+ "confidence": 0.95 if malicious_count >= 5 else 0.8,
460
+ "references": [f"https://www.virustotal.com/gui/file/{file_hash}"],
461
+ "file_hash": file_hash,
462
+ },
463
+ )
@@ -0,0 +1,77 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Skill Analyzer exceptions.
18
+
19
+ This module defines custom exceptions for Skill Analyzer operations.
20
+ All exceptions inherit from SkillAnalyzerError for easy catching.
21
+
22
+ Example:
23
+ >>> from skillanalyzer import Scanner
24
+ >>> from skillanalyzer.core.exceptions import SkillLoadError
25
+ >>>
26
+ >>> scanner = Scanner()
27
+ >>>
28
+ >>> try:
29
+ ... skill = scanner.load_skill("path/to/skill")
30
+ ... except SkillLoadError as e:
31
+ ... print(f"Failed to load skill: {e}")
32
+ ... except SkillAnalysisError as e:
33
+ ... print(f"Analysis failed: {e}")
34
+ """
35
+
36
+
37
+ class SkillAnalyzerError(Exception):
38
+ """Base exception for all Skill Analyzer errors."""
39
+
40
+ pass
41
+
42
+
43
+ class SkillLoadError(SkillAnalyzerError):
44
+ """Raised when unable to load a skill package.
45
+
46
+ This can indicate:
47
+ - Missing SKILL.md file
48
+ - Invalid YAML frontmatter
49
+ - Corrupted skill package
50
+ - File system errors
51
+ """
52
+
53
+ pass
54
+
55
+
56
+ class SkillAnalysisError(SkillAnalyzerError):
57
+ """Raised when skill analysis fails.
58
+
59
+ This typically indicates:
60
+ - Analyzer configuration errors
61
+ - Internal analysis errors
62
+ - Resource exhaustion during analysis
63
+ """
64
+
65
+ pass
66
+
67
+
68
+ class SkillValidationError(SkillAnalyzerError):
69
+ """Raised when skill validation fails.
70
+
71
+ This indicates:
72
+ - Invalid skill manifest
73
+ - Missing required fields
74
+ - Invalid skill structure
75
+ """
76
+
77
+ pass