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,437 @@
|
|
|
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
|
+
Core scanner engine for orchestrating skill analysis.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from .analyzers.base import BaseAnalyzer
|
|
27
|
+
from .analyzers.static import StaticAnalyzer
|
|
28
|
+
from .analyzers.virustotal_analyzer import VirusTotalAnalyzer
|
|
29
|
+
from .loader import SkillLoader, SkillLoadError
|
|
30
|
+
from .models import Finding, Report, ScanResult, Severity, Skill, ThreatCategory
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Common stop words for Jaccard similarity - created once at module level
|
|
35
|
+
_STOP_WORDS = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"the",
|
|
38
|
+
"a",
|
|
39
|
+
"an",
|
|
40
|
+
"is",
|
|
41
|
+
"are",
|
|
42
|
+
"was",
|
|
43
|
+
"were",
|
|
44
|
+
"be",
|
|
45
|
+
"been",
|
|
46
|
+
"being",
|
|
47
|
+
"have",
|
|
48
|
+
"has",
|
|
49
|
+
"had",
|
|
50
|
+
"do",
|
|
51
|
+
"does",
|
|
52
|
+
"did",
|
|
53
|
+
"will",
|
|
54
|
+
"would",
|
|
55
|
+
"could",
|
|
56
|
+
"should",
|
|
57
|
+
"can",
|
|
58
|
+
"may",
|
|
59
|
+
"might",
|
|
60
|
+
"must",
|
|
61
|
+
"shall",
|
|
62
|
+
"to",
|
|
63
|
+
"of",
|
|
64
|
+
"in",
|
|
65
|
+
"for",
|
|
66
|
+
"on",
|
|
67
|
+
"with",
|
|
68
|
+
"at",
|
|
69
|
+
"by",
|
|
70
|
+
"from",
|
|
71
|
+
"as",
|
|
72
|
+
"into",
|
|
73
|
+
"through",
|
|
74
|
+
"and",
|
|
75
|
+
"or",
|
|
76
|
+
"but",
|
|
77
|
+
"if",
|
|
78
|
+
"then",
|
|
79
|
+
"else",
|
|
80
|
+
"when",
|
|
81
|
+
"up",
|
|
82
|
+
"down",
|
|
83
|
+
"out",
|
|
84
|
+
"that",
|
|
85
|
+
"this",
|
|
86
|
+
"these",
|
|
87
|
+
"those",
|
|
88
|
+
"it",
|
|
89
|
+
"its",
|
|
90
|
+
"they",
|
|
91
|
+
"them",
|
|
92
|
+
"their",
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SkillScanner:
|
|
98
|
+
"""Main scanner that orchestrates skill analysis."""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
analyzers: list[BaseAnalyzer] | None = None,
|
|
103
|
+
use_virustotal: bool = False,
|
|
104
|
+
virustotal_api_key: str | None = None,
|
|
105
|
+
virustotal_upload_files: bool = False,
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Initialize scanner with analyzers.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
analyzers: List of analyzers to use. If None, uses default (static).
|
|
112
|
+
use_virustotal: Whether to enable VirusTotal binary scanning
|
|
113
|
+
virustotal_api_key: VirusTotal API key (required if use_virustotal=True)
|
|
114
|
+
virustotal_upload_files: If True, upload unknown files to VT. If False (default),
|
|
115
|
+
only check existing hashes
|
|
116
|
+
"""
|
|
117
|
+
if analyzers is None:
|
|
118
|
+
self.analyzers: list[BaseAnalyzer] = [StaticAnalyzer()]
|
|
119
|
+
|
|
120
|
+
if use_virustotal and virustotal_api_key:
|
|
121
|
+
vt_analyzer = VirusTotalAnalyzer(
|
|
122
|
+
api_key=virustotal_api_key, enabled=True, upload_files=virustotal_upload_files
|
|
123
|
+
)
|
|
124
|
+
self.analyzers.append(vt_analyzer)
|
|
125
|
+
else:
|
|
126
|
+
self.analyzers = analyzers
|
|
127
|
+
|
|
128
|
+
self.loader = SkillLoader()
|
|
129
|
+
|
|
130
|
+
def scan_skill(self, skill_directory: Path) -> ScanResult:
|
|
131
|
+
"""
|
|
132
|
+
Scan a single skill package.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
skill_directory: Path to skill directory
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
ScanResult with findings
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
SkillLoadError: If skill cannot be loaded
|
|
142
|
+
"""
|
|
143
|
+
if not isinstance(skill_directory, Path):
|
|
144
|
+
skill_directory = Path(skill_directory)
|
|
145
|
+
|
|
146
|
+
start_time = time.time()
|
|
147
|
+
|
|
148
|
+
# Load the skill
|
|
149
|
+
skill = self.loader.load_skill(skill_directory)
|
|
150
|
+
|
|
151
|
+
# Run all analyzers
|
|
152
|
+
all_findings = []
|
|
153
|
+
analyzer_names = []
|
|
154
|
+
validated_binary_files = set()
|
|
155
|
+
|
|
156
|
+
for analyzer in self.analyzers:
|
|
157
|
+
findings = analyzer.analyze(skill)
|
|
158
|
+
all_findings.extend(findings)
|
|
159
|
+
analyzer_names.append(analyzer.get_name())
|
|
160
|
+
|
|
161
|
+
if hasattr(analyzer, "validated_binary_files"):
|
|
162
|
+
validated_binary_files.update(analyzer.validated_binary_files)
|
|
163
|
+
|
|
164
|
+
# Post-process findings: Suppress BINARY_FILE_DETECTED for VirusTotal-validated files
|
|
165
|
+
if validated_binary_files:
|
|
166
|
+
filtered_findings = []
|
|
167
|
+
for finding in all_findings:
|
|
168
|
+
if finding.rule_id == "BINARY_FILE_DETECTED" and finding.file_path in validated_binary_files:
|
|
169
|
+
continue
|
|
170
|
+
filtered_findings.append(finding)
|
|
171
|
+
all_findings = filtered_findings
|
|
172
|
+
|
|
173
|
+
scan_duration = time.time() - start_time
|
|
174
|
+
|
|
175
|
+
result = ScanResult(
|
|
176
|
+
skill_name=skill.name,
|
|
177
|
+
skill_directory=str(skill_directory.absolute()),
|
|
178
|
+
findings=all_findings,
|
|
179
|
+
scan_duration_seconds=scan_duration,
|
|
180
|
+
analyzers_used=analyzer_names,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
def scan_directory(self, skills_directory: Path, recursive: bool = False, check_overlap: bool = False) -> Report:
|
|
186
|
+
"""
|
|
187
|
+
Scan all skill packages in a directory.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
skills_directory: Directory containing skill packages
|
|
191
|
+
recursive: If True, search recursively for SKILL.md files
|
|
192
|
+
check_overlap: If True, check for description overlap between skills
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Report with results from all skills
|
|
196
|
+
"""
|
|
197
|
+
if not isinstance(skills_directory, Path):
|
|
198
|
+
skills_directory = Path(skills_directory)
|
|
199
|
+
|
|
200
|
+
if not skills_directory.exists():
|
|
201
|
+
raise FileNotFoundError(f"Directory does not exist: {skills_directory}")
|
|
202
|
+
|
|
203
|
+
skill_dirs = self._find_skill_directories(skills_directory, recursive)
|
|
204
|
+
report = Report()
|
|
205
|
+
|
|
206
|
+
# Keep track of loaded skills for cross-skill analysis
|
|
207
|
+
loaded_skills: list[Skill] = []
|
|
208
|
+
|
|
209
|
+
for skill_dir in skill_dirs:
|
|
210
|
+
try:
|
|
211
|
+
# Load skill once for both scanning and cross-skill analysis
|
|
212
|
+
skill = self.loader.load_skill(skill_dir)
|
|
213
|
+
|
|
214
|
+
# Run all analyzers on the already-loaded skill
|
|
215
|
+
start_time = time.time()
|
|
216
|
+
all_findings = []
|
|
217
|
+
analyzer_names = []
|
|
218
|
+
validated_binary_files = set()
|
|
219
|
+
|
|
220
|
+
for analyzer in self.analyzers:
|
|
221
|
+
findings = analyzer.analyze(skill)
|
|
222
|
+
all_findings.extend(findings)
|
|
223
|
+
analyzer_names.append(analyzer.get_name())
|
|
224
|
+
|
|
225
|
+
if hasattr(analyzer, "validated_binary_files"):
|
|
226
|
+
validated_binary_files.update(analyzer.validated_binary_files)
|
|
227
|
+
|
|
228
|
+
# Post-process findings
|
|
229
|
+
if validated_binary_files:
|
|
230
|
+
all_findings = [
|
|
231
|
+
f
|
|
232
|
+
for f in all_findings
|
|
233
|
+
if not (f.rule_id == "BINARY_FILE_DETECTED" and f.file_path in validated_binary_files)
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
scan_duration = time.time() - start_time
|
|
237
|
+
|
|
238
|
+
result = ScanResult(
|
|
239
|
+
skill_name=skill.name,
|
|
240
|
+
skill_directory=str(skill_dir.absolute()),
|
|
241
|
+
findings=all_findings,
|
|
242
|
+
scan_duration_seconds=scan_duration,
|
|
243
|
+
analyzers_used=analyzer_names,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
report.add_scan_result(result)
|
|
247
|
+
|
|
248
|
+
# Store skill for cross-skill analysis if needed
|
|
249
|
+
if check_overlap:
|
|
250
|
+
loaded_skills.append(skill)
|
|
251
|
+
|
|
252
|
+
except SkillLoadError as e:
|
|
253
|
+
logger.warning("Failed to scan %s: %s", skill_dir, e)
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
# Perform cross-skill analysis if requested
|
|
257
|
+
if check_overlap and len(loaded_skills) > 1:
|
|
258
|
+
overlap_findings = self._check_description_overlap(loaded_skills)
|
|
259
|
+
if overlap_findings and report.scan_results:
|
|
260
|
+
report.scan_results[0].findings.extend(overlap_findings)
|
|
261
|
+
|
|
262
|
+
# Full cross-skill attack pattern detection
|
|
263
|
+
try:
|
|
264
|
+
from .analyzers.cross_skill_analyzer import CrossSkillAnalyzer
|
|
265
|
+
|
|
266
|
+
cross_analyzer = CrossSkillAnalyzer()
|
|
267
|
+
cross_findings = cross_analyzer.analyze_skill_set(loaded_skills)
|
|
268
|
+
if cross_findings and report.scan_results:
|
|
269
|
+
report.scan_results[0].findings.extend(cross_findings)
|
|
270
|
+
except ImportError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
return report
|
|
274
|
+
|
|
275
|
+
def _check_description_overlap(self, skills: list[Skill]) -> list[Finding]:
|
|
276
|
+
"""
|
|
277
|
+
Check for description overlap between skills.
|
|
278
|
+
|
|
279
|
+
Similar descriptions could cause trigger hijacking where one skill
|
|
280
|
+
steals requests intended for another.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
skills: List of loaded skills to compare
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of findings for overlapping descriptions
|
|
287
|
+
"""
|
|
288
|
+
findings = []
|
|
289
|
+
|
|
290
|
+
for i, skill_a in enumerate(skills):
|
|
291
|
+
for skill_b in skills[i + 1 :]:
|
|
292
|
+
similarity = self._jaccard_similarity(skill_a.description, skill_b.description)
|
|
293
|
+
|
|
294
|
+
if similarity > 0.7:
|
|
295
|
+
findings.append(
|
|
296
|
+
Finding(
|
|
297
|
+
id=f"OVERLAP_{hash(skill_a.name + skill_b.name) & 0xFFFFFFFF:08x}",
|
|
298
|
+
rule_id="TRIGGER_OVERLAP_RISK",
|
|
299
|
+
category=ThreatCategory.SOCIAL_ENGINEERING,
|
|
300
|
+
severity=Severity.MEDIUM,
|
|
301
|
+
title="Skills have overlapping descriptions",
|
|
302
|
+
description=(
|
|
303
|
+
f"Skills '{skill_a.name}' and '{skill_b.name}' have {similarity:.0%} "
|
|
304
|
+
f"similar descriptions. This may cause confusion about which skill "
|
|
305
|
+
f"should handle a request, or enable trigger hijacking attacks."
|
|
306
|
+
),
|
|
307
|
+
file_path=f"{skill_a.name}/SKILL.md",
|
|
308
|
+
remediation=(
|
|
309
|
+
"Make skill descriptions more distinct by clearly specifying "
|
|
310
|
+
"the unique capabilities, file types, or use cases for each skill."
|
|
311
|
+
),
|
|
312
|
+
metadata={
|
|
313
|
+
"skill_a": skill_a.name,
|
|
314
|
+
"skill_b": skill_b.name,
|
|
315
|
+
"similarity": similarity,
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
elif similarity > 0.5:
|
|
320
|
+
findings.append(
|
|
321
|
+
Finding(
|
|
322
|
+
id=f"OVERLAP_WARN_{hash(skill_a.name + skill_b.name) & 0xFFFFFFFF:08x}",
|
|
323
|
+
rule_id="TRIGGER_OVERLAP_WARNING",
|
|
324
|
+
category=ThreatCategory.SOCIAL_ENGINEERING,
|
|
325
|
+
severity=Severity.LOW,
|
|
326
|
+
title="Skills have somewhat similar descriptions",
|
|
327
|
+
description=(
|
|
328
|
+
f"Skills '{skill_a.name}' and '{skill_b.name}' have {similarity:.0%} "
|
|
329
|
+
f"similar descriptions. Consider making descriptions more distinct."
|
|
330
|
+
),
|
|
331
|
+
file_path=f"{skill_a.name}/SKILL.md",
|
|
332
|
+
remediation="Consider making skill descriptions more distinct",
|
|
333
|
+
metadata={
|
|
334
|
+
"skill_a": skill_a.name,
|
|
335
|
+
"skill_b": skill_b.name,
|
|
336
|
+
"similarity": similarity,
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return findings
|
|
342
|
+
|
|
343
|
+
def _jaccard_similarity(self, text_a: str, text_b: str) -> float:
|
|
344
|
+
"""
|
|
345
|
+
Calculate Jaccard similarity between two text strings.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
text_a: First text
|
|
349
|
+
text_b: Second text
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Similarity score from 0.0 to 1.0
|
|
353
|
+
"""
|
|
354
|
+
tokens_a = set(re.findall(r"\b[a-zA-Z]+\b", text_a.lower()))
|
|
355
|
+
tokens_b = set(re.findall(r"\b[a-zA-Z]+\b", text_b.lower()))
|
|
356
|
+
|
|
357
|
+
# Remove common stop words (using module-level constant)
|
|
358
|
+
tokens_a = tokens_a - _STOP_WORDS
|
|
359
|
+
tokens_b = tokens_b - _STOP_WORDS
|
|
360
|
+
|
|
361
|
+
if not tokens_a or not tokens_b:
|
|
362
|
+
return 0.0
|
|
363
|
+
|
|
364
|
+
intersection = len(tokens_a & tokens_b)
|
|
365
|
+
union = len(tokens_a | tokens_b)
|
|
366
|
+
|
|
367
|
+
return intersection / union if union > 0 else 0.0
|
|
368
|
+
|
|
369
|
+
def _find_skill_directories(self, directory: Path, recursive: bool) -> list[Path]:
|
|
370
|
+
"""
|
|
371
|
+
Find all directories containing SKILL.md files.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
directory: Directory to search
|
|
375
|
+
recursive: Search recursively
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of skill directory paths
|
|
379
|
+
"""
|
|
380
|
+
skill_dirs = []
|
|
381
|
+
|
|
382
|
+
if recursive:
|
|
383
|
+
for skill_md in directory.rglob("SKILL.md"):
|
|
384
|
+
skill_dirs.append(skill_md.parent)
|
|
385
|
+
else:
|
|
386
|
+
for item in directory.iterdir():
|
|
387
|
+
if item.is_dir():
|
|
388
|
+
skill_md = item / "SKILL.md"
|
|
389
|
+
if skill_md.exists():
|
|
390
|
+
skill_dirs.append(item)
|
|
391
|
+
|
|
392
|
+
return skill_dirs
|
|
393
|
+
|
|
394
|
+
def add_analyzer(self, analyzer: BaseAnalyzer):
|
|
395
|
+
"""Add an analyzer to the scanner."""
|
|
396
|
+
self.analyzers.append(analyzer)
|
|
397
|
+
|
|
398
|
+
def list_analyzers(self) -> list[str]:
|
|
399
|
+
"""Get names of all configured analyzers."""
|
|
400
|
+
return [analyzer.get_name() for analyzer in self.analyzers]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def scan_skill(skill_directory: Path, analyzers: list[BaseAnalyzer] | None = None) -> ScanResult:
|
|
404
|
+
"""
|
|
405
|
+
Convenience function to scan a single skill.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
skill_directory: Path to skill directory
|
|
409
|
+
analyzers: Optional list of analyzers
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
ScanResult
|
|
413
|
+
"""
|
|
414
|
+
scanner = SkillScanner(analyzers=analyzers)
|
|
415
|
+
return scanner.scan_skill(skill_directory)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def scan_directory(
|
|
419
|
+
skills_directory: Path,
|
|
420
|
+
recursive: bool = False,
|
|
421
|
+
analyzers: list[BaseAnalyzer] | None = None,
|
|
422
|
+
check_overlap: bool = False,
|
|
423
|
+
) -> Report:
|
|
424
|
+
"""
|
|
425
|
+
Convenience function to scan multiple skills.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
skills_directory: Directory containing skills
|
|
429
|
+
recursive: Search recursively
|
|
430
|
+
analyzers: Optional list of analyzers
|
|
431
|
+
check_overlap: If True, check for description overlap between skills
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Report with all results
|
|
435
|
+
"""
|
|
436
|
+
scanner = SkillScanner(analyzers=analyzers)
|
|
437
|
+
return scanner.scan_directory(skills_directory, recursive=recursive, check_overlap=check_overlap)
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
"""Static analysis modules for behavioral analyzer."""
|
|
18
|
+
|
|
19
|
+
from .context_extractor import ContextExtractor, SkillFunctionContext, SkillScriptContext
|
|
20
|
+
from .parser.python_parser import PythonParser
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PythonParser",
|
|
24
|
+
"ContextExtractor",
|
|
25
|
+
"SkillScriptContext",
|
|
26
|
+
"SkillFunctionContext",
|
|
27
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""Control Flow Graph (CFG) building for dataflow analysis."""
|
|
18
|
+
|
|
19
|
+
from .builder import CFGNode, ControlFlowGraph, DataFlowAnalyzer
|
|
20
|
+
|
|
21
|
+
__all__ = ["CFGNode", "ControlFlowGraph", "DataFlowAnalyzer"]
|