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