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.
- cisco_ai_skill_scanner-1.0.0.dist-info/METADATA +253 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/RECORD +100 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/WHEEL +4 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/entry_points.txt +4 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/licenses/LICENSE +17 -0
- skillanalyzer/__init__.py +45 -0
- skillanalyzer/_version.py +34 -0
- skillanalyzer/api/__init__.py +25 -0
- skillanalyzer/api/api.py +34 -0
- skillanalyzer/api/api_cli.py +78 -0
- skillanalyzer/api/api_server.py +634 -0
- skillanalyzer/api/router.py +527 -0
- skillanalyzer/cli/__init__.py +25 -0
- skillanalyzer/cli/cli.py +816 -0
- skillanalyzer/config/__init__.py +26 -0
- skillanalyzer/config/config.py +149 -0
- skillanalyzer/config/config_parser.py +122 -0
- skillanalyzer/config/constants.py +85 -0
- skillanalyzer/core/__init__.py +24 -0
- skillanalyzer/core/analyzers/__init__.py +75 -0
- skillanalyzer/core/analyzers/aidefense_analyzer.py +872 -0
- skillanalyzer/core/analyzers/base.py +53 -0
- skillanalyzer/core/analyzers/behavioral/__init__.py +30 -0
- skillanalyzer/core/analyzers/behavioral/alignment/__init__.py +45 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_llm_client.py +240 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_orchestrator.py +216 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_prompt_builder.py +422 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_response_validator.py +136 -0
- skillanalyzer/core/analyzers/behavioral/alignment/threat_vulnerability_classifier.py +198 -0
- skillanalyzer/core/analyzers/behavioral_analyzer.py +453 -0
- skillanalyzer/core/analyzers/cross_skill_analyzer.py +490 -0
- skillanalyzer/core/analyzers/llm_analyzer.py +440 -0
- skillanalyzer/core/analyzers/llm_prompt_builder.py +270 -0
- skillanalyzer/core/analyzers/llm_provider_config.py +215 -0
- skillanalyzer/core/analyzers/llm_request_handler.py +284 -0
- skillanalyzer/core/analyzers/llm_response_parser.py +81 -0
- skillanalyzer/core/analyzers/meta_analyzer.py +845 -0
- skillanalyzer/core/analyzers/static.py +1105 -0
- skillanalyzer/core/analyzers/trigger_analyzer.py +341 -0
- skillanalyzer/core/analyzers/virustotal_analyzer.py +463 -0
- skillanalyzer/core/exceptions.py +77 -0
- skillanalyzer/core/loader.py +377 -0
- skillanalyzer/core/models.py +300 -0
- skillanalyzer/core/reporters/__init__.py +26 -0
- skillanalyzer/core/reporters/json_reporter.py +65 -0
- skillanalyzer/core/reporters/markdown_reporter.py +209 -0
- skillanalyzer/core/reporters/sarif_reporter.py +246 -0
- skillanalyzer/core/reporters/table_reporter.py +195 -0
- skillanalyzer/core/rules/__init__.py +19 -0
- skillanalyzer/core/rules/patterns.py +165 -0
- skillanalyzer/core/rules/yara_scanner.py +157 -0
- skillanalyzer/core/scanner.py +437 -0
- skillanalyzer/core/static_analysis/__init__.py +27 -0
- skillanalyzer/core/static_analysis/cfg/__init__.py +21 -0
- skillanalyzer/core/static_analysis/cfg/builder.py +439 -0
- skillanalyzer/core/static_analysis/context_extractor.py +742 -0
- skillanalyzer/core/static_analysis/dataflow/__init__.py +25 -0
- skillanalyzer/core/static_analysis/dataflow/forward_analysis.py +715 -0
- skillanalyzer/core/static_analysis/interprocedural/__init__.py +21 -0
- skillanalyzer/core/static_analysis/interprocedural/call_graph_analyzer.py +406 -0
- skillanalyzer/core/static_analysis/interprocedural/cross_file_analyzer.py +190 -0
- skillanalyzer/core/static_analysis/parser/__init__.py +21 -0
- skillanalyzer/core/static_analysis/parser/python_parser.py +380 -0
- skillanalyzer/core/static_analysis/semantic/__init__.py +28 -0
- skillanalyzer/core/static_analysis/semantic/name_resolver.py +206 -0
- skillanalyzer/core/static_analysis/semantic/type_analyzer.py +200 -0
- skillanalyzer/core/static_analysis/taint/__init__.py +21 -0
- skillanalyzer/core/static_analysis/taint/tracker.py +252 -0
- skillanalyzer/core/static_analysis/types/__init__.py +36 -0
- skillanalyzer/data/__init__.py +30 -0
- skillanalyzer/data/prompts/boilerplate_protection_rule_prompt.md +26 -0
- skillanalyzer/data/prompts/code_alignment_threat_analysis_prompt.md +901 -0
- skillanalyzer/data/prompts/llm_response_schema.json +71 -0
- skillanalyzer/data/prompts/skill_meta_analysis_prompt.md +303 -0
- skillanalyzer/data/prompts/skill_threat_analysis_prompt.md +263 -0
- skillanalyzer/data/prompts/unified_response_schema.md +97 -0
- skillanalyzer/data/rules/signatures.yaml +440 -0
- skillanalyzer/data/yara_rules/autonomy_abuse.yara +66 -0
- skillanalyzer/data/yara_rules/code_execution.yara +61 -0
- skillanalyzer/data/yara_rules/coercive_injection.yara +115 -0
- skillanalyzer/data/yara_rules/command_injection.yara +54 -0
- skillanalyzer/data/yara_rules/credential_harvesting.yara +115 -0
- skillanalyzer/data/yara_rules/prompt_injection.yara +71 -0
- skillanalyzer/data/yara_rules/script_injection.yara +83 -0
- skillanalyzer/data/yara_rules/skill_discovery_abuse.yara +57 -0
- skillanalyzer/data/yara_rules/sql_injection.yara +73 -0
- skillanalyzer/data/yara_rules/system_manipulation.yara +65 -0
- skillanalyzer/data/yara_rules/tool_chaining_abuse.yara +60 -0
- skillanalyzer/data/yara_rules/transitive_trust_abuse.yara +73 -0
- skillanalyzer/data/yara_rules/unicode_steganography.yara +65 -0
- skillanalyzer/hooks/__init__.py +21 -0
- skillanalyzer/hooks/pre_commit.py +450 -0
- skillanalyzer/threats/__init__.py +25 -0
- skillanalyzer/threats/threats.py +480 -0
- skillanalyzer/utils/__init__.py +28 -0
- skillanalyzer/utils/command_utils.py +129 -0
- skillanalyzer/utils/di_container.py +154 -0
- skillanalyzer/utils/file_utils.py +86 -0
- skillanalyzer/utils/logging_config.py +96 -0
- 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"]
|