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,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()
|