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,527 @@
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
+ """API router for Skill Analyzer endpoints."""
18
+
19
+ import shutil
20
+ import tempfile
21
+ import uuid
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+
25
+ try:
26
+ from fastapi import APIRouter, BackgroundTasks, File, HTTPException, Query, UploadFile
27
+ from pydantic import BaseModel, Field
28
+
29
+ MULTIPART_AVAILABLE = True
30
+ except ImportError:
31
+ raise ImportError("API server requires FastAPI. Install with: pip install fastapi uvicorn python-multipart")
32
+
33
+ from ..core.analyzers.static import StaticAnalyzer
34
+ from ..core.models import ScanResult # noqa: F401
35
+ from ..core.scanner import SkillScanner
36
+
37
+ try:
38
+ from ..core.analyzers.llm_analyzer import LLMAnalyzer
39
+
40
+ LLM_AVAILABLE = True
41
+ except (ImportError, ModuleNotFoundError):
42
+ LLM_AVAILABLE = False
43
+ LLMAnalyzer = None
44
+
45
+ try:
46
+ from ..core.analyzers.behavioral_analyzer import BehavioralAnalyzer
47
+
48
+ BEHAVIORAL_AVAILABLE = True
49
+ except (ImportError, ModuleNotFoundError):
50
+ BEHAVIORAL_AVAILABLE = False
51
+ BehavioralAnalyzer = None
52
+
53
+ try:
54
+ from ..core.analyzers.aidefense_analyzer import AIDefenseAnalyzer
55
+
56
+ AIDEFENSE_AVAILABLE = True
57
+ except (ImportError, ModuleNotFoundError):
58
+ AIDEFENSE_AVAILABLE = False
59
+ AIDefenseAnalyzer = None
60
+
61
+ try:
62
+ from ..core.analyzers.meta_analyzer import MetaAnalyzer, apply_meta_analysis_to_results
63
+
64
+ META_AVAILABLE = True
65
+ except (ImportError, ModuleNotFoundError):
66
+ META_AVAILABLE = False
67
+ MetaAnalyzer = None
68
+ apply_meta_analysis_to_results = None
69
+
70
+ router = APIRouter()
71
+
72
+ # In-memory storage for async scans (in production, use Redis or database)
73
+ scan_results_cache = {}
74
+
75
+
76
+ # Pydantic models for API
77
+ class ScanRequest(BaseModel):
78
+ """Request model for scanning a skill."""
79
+
80
+ skill_directory: str = Field(..., description="Path to skill directory")
81
+ use_llm: bool = Field(False, description="Enable LLM analyzer")
82
+ llm_provider: str | None = Field("anthropic", description="LLM provider (anthropic or openai)")
83
+ use_behavioral: bool = Field(False, description="Enable behavioral analyzer")
84
+ use_aidefense: bool = Field(False, description="Enable AI Defense analyzer")
85
+ aidefense_api_key: str | None = Field(None, description="AI Defense API key")
86
+ enable_meta: bool = Field(False, description="Enable meta-analysis for false positive filtering")
87
+
88
+
89
+ class ScanResponse(BaseModel):
90
+ """Response model for scan results."""
91
+
92
+ scan_id: str
93
+ skill_name: str
94
+ is_safe: bool
95
+ max_severity: str
96
+ findings_count: int
97
+ scan_duration_seconds: float
98
+ timestamp: str
99
+ findings: list[dict]
100
+
101
+
102
+ class HealthResponse(BaseModel):
103
+ """Health check response."""
104
+
105
+ status: str
106
+ version: str
107
+ analyzers_available: list[str]
108
+
109
+
110
+ class BatchScanRequest(BaseModel):
111
+ """Request for batch scanning."""
112
+
113
+ skills_directory: str
114
+ recursive: bool = False
115
+ use_llm: bool = False
116
+ llm_provider: str | None = "anthropic"
117
+ use_behavioral: bool = False
118
+ use_aidefense: bool = False
119
+ aidefense_api_key: str | None = None
120
+ enable_meta: bool = Field(False, description="Enable meta-analysis")
121
+
122
+
123
+ @router.get("/", response_model=dict)
124
+ async def root():
125
+ """Root endpoint."""
126
+ return {"service": "Claude Skill Analyzer API", "version": "0.2.0", "docs": "/docs", "health": "/health"}
127
+
128
+
129
+ @router.get("/health", response_model=HealthResponse)
130
+ async def health_check():
131
+ """Health check endpoint."""
132
+ analyzers = ["static_analyzer"]
133
+ if BEHAVIORAL_AVAILABLE:
134
+ analyzers.append("behavioral_analyzer")
135
+ if LLM_AVAILABLE:
136
+ analyzers.append("llm_analyzer")
137
+ if AIDEFENSE_AVAILABLE:
138
+ analyzers.append("aidefense_analyzer")
139
+ if META_AVAILABLE:
140
+ analyzers.append("meta_analyzer")
141
+
142
+ return HealthResponse(status="healthy", version="0.2.0", analyzers_available=analyzers)
143
+
144
+
145
+ @router.post("/scan", response_model=ScanResponse)
146
+ async def scan_skill(request: ScanRequest):
147
+ """
148
+ Scan a single skill package.
149
+
150
+ Args:
151
+ request: Scan request with skill directory and options
152
+
153
+ Returns:
154
+ Scan results with findings
155
+ """
156
+ import asyncio
157
+ import concurrent.futures
158
+ import os
159
+
160
+ skill_dir = Path(request.skill_directory)
161
+
162
+ if not skill_dir.exists():
163
+ raise HTTPException(status_code=404, detail=f"Skill directory not found: {skill_dir}")
164
+
165
+ if not (skill_dir / "SKILL.md").exists():
166
+ raise HTTPException(status_code=400, detail="SKILL.md not found in directory")
167
+
168
+ def run_scan():
169
+ """Run the scan in a separate thread to avoid event loop conflicts."""
170
+ from ..core.analyzers.base import BaseAnalyzer
171
+
172
+ analyzers: list[BaseAnalyzer] = [StaticAnalyzer()]
173
+
174
+ if request.use_behavioral and BEHAVIORAL_AVAILABLE:
175
+ behavioral_analyzer = BehavioralAnalyzer(use_static_analysis=True)
176
+ analyzers.append(behavioral_analyzer)
177
+
178
+ if request.use_llm and LLM_AVAILABLE:
179
+ # Check for model override from environment
180
+ llm_model = os.getenv("SKILL_SCANNER_LLM_MODEL")
181
+ provider_str = request.llm_provider or "anthropic"
182
+ if llm_model:
183
+ # Use explicit model from environment
184
+ llm_analyzer = LLMAnalyzer(model=llm_model)
185
+ else:
186
+ # Use provider default model
187
+ llm_analyzer = LLMAnalyzer(provider=provider_str)
188
+ analyzers.append(llm_analyzer)
189
+
190
+ if request.use_aidefense and AIDEFENSE_AVAILABLE:
191
+ api_key = request.aidefense_api_key or os.getenv("AI_DEFENSE_API_KEY")
192
+ if not api_key:
193
+ raise ValueError("AI Defense API key required")
194
+ aidefense_analyzer = AIDefenseAnalyzer(api_key=api_key)
195
+ analyzers.append(aidefense_analyzer)
196
+
197
+ scanner = SkillScanner(analyzers=analyzers)
198
+ return scanner.scan_skill(skill_dir)
199
+
200
+ try:
201
+ # Run the scan in a thread pool to avoid nested event loop issues
202
+ loop = asyncio.get_running_loop()
203
+ with concurrent.futures.ThreadPoolExecutor() as executor:
204
+ result = await loop.run_in_executor(executor, run_scan)
205
+
206
+ # Run meta-analysis if enabled (in separate executor to avoid shutdown issues)
207
+ if request.enable_meta and META_AVAILABLE and len(result.findings) > 0:
208
+ try:
209
+ from ..core.loader import SkillLoader
210
+
211
+ meta_analyzer = MetaAnalyzer()
212
+ loader = SkillLoader()
213
+ skill = loader.load_skill(skill_dir)
214
+
215
+ import asyncio as async_lib
216
+
217
+ def run_meta():
218
+ return async_lib.run(
219
+ meta_analyzer.analyze_with_findings(
220
+ skill=skill,
221
+ findings=result.findings,
222
+ analyzers_used=result.analyzers_used,
223
+ )
224
+ )
225
+
226
+ with concurrent.futures.ThreadPoolExecutor() as meta_executor:
227
+ meta_result = await loop.run_in_executor(meta_executor, run_meta)
228
+
229
+ filtered_findings = apply_meta_analysis_to_results(
230
+ original_findings=result.findings,
231
+ meta_result=meta_result,
232
+ skill=skill,
233
+ )
234
+ result.findings = filtered_findings
235
+ result.analyzers_used.append("meta_analyzer")
236
+
237
+ except Exception as meta_error:
238
+ print(f"Warning: Meta-analysis failed: {meta_error}")
239
+
240
+ scan_id = str(uuid.uuid4())
241
+
242
+ return ScanResponse(
243
+ scan_id=scan_id,
244
+ skill_name=result.skill_name,
245
+ is_safe=result.is_safe,
246
+ max_severity=result.max_severity.value,
247
+ findings_count=len(result.findings),
248
+ scan_duration_seconds=result.scan_duration_seconds,
249
+ timestamp=result.timestamp.isoformat(),
250
+ findings=[f.to_dict() for f in result.findings],
251
+ )
252
+
253
+ except ValueError as e:
254
+ raise HTTPException(status_code=400, detail=str(e))
255
+ except Exception as e:
256
+ raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}")
257
+
258
+
259
+ @router.post("/scan-upload")
260
+ async def scan_uploaded_skill(
261
+ file: UploadFile = File(..., description="ZIP file containing skill package"),
262
+ use_llm: bool = Query(False, description="Enable LLM analyzer"),
263
+ llm_provider: str = Query("anthropic", description="LLM provider"),
264
+ use_behavioral: bool = Query(False, description="Enable behavioral analyzer"),
265
+ use_aidefense: bool = Query(False, description="Enable AI Defense analyzer"),
266
+ aidefense_api_key: str | None = Query(None, description="AI Defense API key"),
267
+ ):
268
+ """
269
+ Scan an uploaded skill package (ZIP file).
270
+
271
+ Args:
272
+ file: ZIP file containing skill package
273
+ use_llm: Enable LLM analyzer
274
+ llm_provider: LLM provider to use
275
+ use_behavioral: Enable behavioral analyzer
276
+ use_aidefense: Enable AI Defense analyzer
277
+ aidefense_api_key: AI Defense API key
278
+
279
+ Returns:
280
+ Scan results
281
+ """
282
+ if not file.filename or not file.filename.endswith(".zip"):
283
+ raise HTTPException(status_code=400, detail="File must be a ZIP archive")
284
+
285
+ temp_dir = Path(tempfile.mkdtemp(prefix="skill_analyzer_"))
286
+
287
+ try:
288
+ zip_path = temp_dir / file.filename
289
+ with open(zip_path, "wb") as f:
290
+ content = await file.read()
291
+ f.write(content)
292
+
293
+ import zipfile
294
+
295
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
296
+ zip_ref.extractall(temp_dir / "extracted")
297
+
298
+ extracted_dir = temp_dir / "extracted"
299
+ skill_dirs = list(extracted_dir.rglob("SKILL.md"))
300
+
301
+ if not skill_dirs:
302
+ raise HTTPException(status_code=400, detail="No SKILL.md found in uploaded archive")
303
+
304
+ skill_dir = skill_dirs[0].parent
305
+
306
+ request = ScanRequest(
307
+ skill_directory=str(skill_dir),
308
+ use_llm=use_llm,
309
+ llm_provider=llm_provider,
310
+ use_behavioral=use_behavioral,
311
+ use_aidefense=use_aidefense,
312
+ aidefense_api_key=aidefense_api_key,
313
+ )
314
+
315
+ result = await scan_skill(request)
316
+ return result
317
+
318
+ finally:
319
+ shutil.rmtree(temp_dir, ignore_errors=True)
320
+
321
+
322
+ @router.post("/scan-batch")
323
+ async def scan_batch(request: BatchScanRequest, background_tasks: BackgroundTasks):
324
+ """
325
+ Scan multiple skills in a directory (batch scan).
326
+
327
+ Returns a scan ID. Use /scan-batch/{scan_id} to get results.
328
+
329
+ Args:
330
+ request: Batch scan request
331
+ background_tasks: FastAPI background tasks
332
+
333
+ Returns:
334
+ Scan ID for tracking
335
+ """
336
+ skills_dir = Path(request.skills_directory)
337
+
338
+ if not skills_dir.exists():
339
+ raise HTTPException(status_code=404, detail=f"Skills directory not found: {skills_dir}")
340
+
341
+ scan_id = str(uuid.uuid4())
342
+
343
+ scan_results_cache[scan_id] = {"status": "processing", "started_at": datetime.now().isoformat(), "result": None}
344
+
345
+ background_tasks.add_task(
346
+ run_batch_scan,
347
+ scan_id,
348
+ skills_dir,
349
+ request.recursive,
350
+ request.use_llm,
351
+ request.llm_provider,
352
+ request.use_behavioral,
353
+ request.use_aidefense,
354
+ request.aidefense_api_key,
355
+ )
356
+
357
+ return {
358
+ "scan_id": scan_id,
359
+ "status": "processing",
360
+ "message": "Batch scan started. Use GET /scan-batch/{scan_id} to check status.",
361
+ }
362
+
363
+
364
+ @router.get("/scan-batch/{scan_id}")
365
+ async def get_batch_scan_result(scan_id: str):
366
+ """
367
+ Get results of a batch scan.
368
+
369
+ Args:
370
+ scan_id: Scan ID from /scan-batch
371
+
372
+ Returns:
373
+ Scan results or status
374
+ """
375
+ if scan_id not in scan_results_cache:
376
+ raise HTTPException(status_code=404, detail="Scan ID not found")
377
+
378
+ cached = scan_results_cache[scan_id]
379
+
380
+ if cached["status"] == "processing":
381
+ return {"scan_id": scan_id, "status": "processing", "started_at": cached["started_at"]}
382
+ elif cached["status"] == "completed":
383
+ return {
384
+ "scan_id": scan_id,
385
+ "status": "completed",
386
+ "started_at": cached["started_at"],
387
+ "completed_at": cached.get("completed_at"),
388
+ "result": cached["result"],
389
+ }
390
+ else:
391
+ return {"scan_id": scan_id, "status": "error", "error": cached.get("error", "Unknown error")}
392
+
393
+
394
+ def run_batch_scan(
395
+ scan_id: str,
396
+ skills_dir: Path,
397
+ recursive: bool,
398
+ use_llm: bool,
399
+ llm_provider: str | None,
400
+ use_behavioral: bool = False,
401
+ use_aidefense: bool = False,
402
+ aidefense_api_key: str | None = None,
403
+ ):
404
+ """
405
+ Background task to run batch scan.
406
+
407
+ Args:
408
+ scan_id: Scan ID
409
+ skills_dir: Directory containing skills
410
+ recursive: Search recursively
411
+ use_llm: Use LLM analyzer
412
+ llm_provider: LLM provider
413
+ use_behavioral: Use behavioral analyzer
414
+ use_aidefense: Use AI Defense analyzer
415
+ aidefense_api_key: AI Defense API key
416
+ """
417
+ try:
418
+ import os
419
+
420
+ from ..core.analyzers.base import BaseAnalyzer
421
+
422
+ analyzers: list[BaseAnalyzer] = [StaticAnalyzer()]
423
+
424
+ if use_behavioral and BEHAVIORAL_AVAILABLE:
425
+ try:
426
+ behavioral_analyzer = BehavioralAnalyzer(use_static_analysis=True)
427
+ analyzers.append(behavioral_analyzer)
428
+ except Exception:
429
+ pass # Continue without behavioral analyzer
430
+
431
+ if use_llm and LLM_AVAILABLE:
432
+ try:
433
+ # Check for model override from environment
434
+ llm_model = os.getenv("SKILL_SCANNER_LLM_MODEL")
435
+ provider_str = llm_provider or "anthropic"
436
+ if llm_model:
437
+ # Use explicit model from environment
438
+ llm_analyzer = LLMAnalyzer(model=llm_model)
439
+ else:
440
+ # Use provider default model
441
+ llm_analyzer = LLMAnalyzer(provider=provider_str)
442
+ analyzers.append(llm_analyzer)
443
+ except Exception:
444
+ pass # Continue without LLM analyzer
445
+
446
+ if use_aidefense and AIDEFENSE_AVAILABLE:
447
+ try:
448
+ api_key = aidefense_api_key or os.getenv("AI_DEFENSE_API_KEY")
449
+ if not api_key:
450
+ raise ValueError("AI Defense API key required (set AI_DEFENSE_API_KEY or pass aidefense_api_key)")
451
+ aidefense_analyzer = AIDefenseAnalyzer(api_key=api_key)
452
+ analyzers.append(aidefense_analyzer)
453
+ except ValueError:
454
+ raise # Re-raise ValueError to fail the batch scan
455
+ except Exception:
456
+ pass # Continue without AI Defense analyzer for other errors
457
+
458
+ scanner = SkillScanner(analyzers=analyzers)
459
+ report = scanner.scan_directory(skills_dir, recursive=recursive)
460
+
461
+ scan_results_cache[scan_id] = {
462
+ "status": "completed",
463
+ "started_at": scan_results_cache[scan_id]["started_at"],
464
+ "completed_at": datetime.now().isoformat(),
465
+ "result": report.to_dict(),
466
+ }
467
+
468
+ except Exception as e:
469
+ scan_results_cache[scan_id] = {
470
+ "status": "error",
471
+ "started_at": scan_results_cache[scan_id]["started_at"],
472
+ "error": str(e),
473
+ }
474
+
475
+
476
+ @router.get("/analyzers")
477
+ async def list_analyzers():
478
+ """List available analyzers."""
479
+ analyzers = [
480
+ {
481
+ "name": "static_analyzer",
482
+ "description": "Pattern-based detection using YAML and YARA rules",
483
+ "available": True,
484
+ "rules_count": "40+",
485
+ }
486
+ ]
487
+
488
+ if BEHAVIORAL_AVAILABLE:
489
+ analyzers.append(
490
+ {
491
+ "name": "behavioral_analyzer",
492
+ "description": "Static dataflow analysis for Python files",
493
+ "available": True,
494
+ }
495
+ )
496
+
497
+ if LLM_AVAILABLE:
498
+ analyzers.append(
499
+ {
500
+ "name": "llm_analyzer",
501
+ "description": "Semantic analysis using LLM as a judge",
502
+ "available": True,
503
+ "providers": ["anthropic", "openai", "azure", "bedrock", "gemini"],
504
+ }
505
+ )
506
+
507
+ if AIDEFENSE_AVAILABLE:
508
+ analyzers.append(
509
+ {
510
+ "name": "aidefense_analyzer",
511
+ "description": "Cisco AI Defense cloud-based threat detection",
512
+ "available": True,
513
+ "requires_api_key": True,
514
+ }
515
+ )
516
+
517
+ if META_AVAILABLE:
518
+ analyzers.append(
519
+ {
520
+ "name": "meta_analyzer",
521
+ "description": "Second-pass LLM analysis for false positive filtering",
522
+ "available": True,
523
+ "requires": "2+ analyzers, LLM API key",
524
+ }
525
+ )
526
+
527
+ return {"analyzers": analyzers}
@@ -0,0 +1,25 @@
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
+ """
18
+ Command-line interface for Claude Skill Analyzer.
19
+
20
+ Matches MCP Scanner's CLI structure.
21
+ """
22
+
23
+ from .cli import main
24
+
25
+ __all__ = ["main"]