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,380 @@
|
|
|
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
|
+
Python AST parser for Claude Skills scripts.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FunctionInfo:
|
|
27
|
+
"""Information about a function in a skill script."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
parameters: list[str]
|
|
31
|
+
docstring: str | None
|
|
32
|
+
line_number: int
|
|
33
|
+
source_code: str
|
|
34
|
+
ast_node: ast.FunctionDef
|
|
35
|
+
|
|
36
|
+
# Security indicators
|
|
37
|
+
has_network_calls: bool = False
|
|
38
|
+
has_file_operations: bool = False
|
|
39
|
+
has_subprocess: bool = False
|
|
40
|
+
has_eval_exec: bool = False
|
|
41
|
+
|
|
42
|
+
# Collected elements
|
|
43
|
+
imports: list[str] = field(default_factory=list)
|
|
44
|
+
function_calls: list[str] = field(default_factory=list)
|
|
45
|
+
string_literals: list[str] = field(default_factory=list)
|
|
46
|
+
assignments: list[str] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PythonParser:
|
|
50
|
+
"""Parse Python source code and extract security-relevant information."""
|
|
51
|
+
|
|
52
|
+
# Security-relevant patterns
|
|
53
|
+
NETWORK_MODULES = ["requests", "urllib", "http", "socket", "aiohttp"]
|
|
54
|
+
FILE_OPERATIONS = ["open", "read", "write", "Path", "os.remove", "shutil"]
|
|
55
|
+
SUBPROCESS_PATTERNS = ["subprocess", "os.system", "os.popen"]
|
|
56
|
+
DANGEROUS_FUNCTIONS = ["eval", "exec", "compile", "__import__"]
|
|
57
|
+
|
|
58
|
+
# Claude Code tool indicators - map code patterns to Claude Code tools
|
|
59
|
+
TOOL_INDICATORS = {
|
|
60
|
+
"Read": {
|
|
61
|
+
"open",
|
|
62
|
+
"read",
|
|
63
|
+
"readline",
|
|
64
|
+
"readlines",
|
|
65
|
+
"Path.read_text",
|
|
66
|
+
"Path.read_bytes",
|
|
67
|
+
"json.load",
|
|
68
|
+
"yaml.safe_load",
|
|
69
|
+
"configparser",
|
|
70
|
+
},
|
|
71
|
+
"Write": {
|
|
72
|
+
"write",
|
|
73
|
+
"writelines",
|
|
74
|
+
"Path.write_text",
|
|
75
|
+
"Path.write_bytes",
|
|
76
|
+
"json.dump",
|
|
77
|
+
"yaml.dump",
|
|
78
|
+
},
|
|
79
|
+
"Bash": {
|
|
80
|
+
"subprocess.run",
|
|
81
|
+
"subprocess.call",
|
|
82
|
+
"subprocess.Popen",
|
|
83
|
+
"subprocess.check_output",
|
|
84
|
+
"subprocess.check_call",
|
|
85
|
+
"os.system",
|
|
86
|
+
"os.popen",
|
|
87
|
+
"os.spawn",
|
|
88
|
+
"commands.getoutput",
|
|
89
|
+
"commands.getstatusoutput",
|
|
90
|
+
},
|
|
91
|
+
"Grep": {
|
|
92
|
+
"re.search",
|
|
93
|
+
"re.match",
|
|
94
|
+
"re.findall",
|
|
95
|
+
"re.finditer",
|
|
96
|
+
"re.sub",
|
|
97
|
+
"re.split",
|
|
98
|
+
},
|
|
99
|
+
"Glob": {
|
|
100
|
+
"glob.glob",
|
|
101
|
+
"glob.iglob",
|
|
102
|
+
"Path.glob",
|
|
103
|
+
"Path.rglob",
|
|
104
|
+
"fnmatch.fnmatch",
|
|
105
|
+
"fnmatch.filter",
|
|
106
|
+
},
|
|
107
|
+
"Network": {
|
|
108
|
+
"requests.get",
|
|
109
|
+
"requests.post",
|
|
110
|
+
"requests.put",
|
|
111
|
+
"requests.delete",
|
|
112
|
+
"urllib.request.urlopen",
|
|
113
|
+
"urllib.urlopen",
|
|
114
|
+
"http.client.HTTPConnection",
|
|
115
|
+
"http.client.HTTPSConnection",
|
|
116
|
+
"socket.connect",
|
|
117
|
+
"socket.create_connection",
|
|
118
|
+
"aiohttp.ClientSession",
|
|
119
|
+
"httpx.get",
|
|
120
|
+
"httpx.post",
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def __init__(self, source_code: str):
|
|
125
|
+
"""
|
|
126
|
+
Initialize parser with source code.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
source_code: Python source code to parse
|
|
130
|
+
"""
|
|
131
|
+
self.source_code = source_code
|
|
132
|
+
self.tree: ast.Module | None = None
|
|
133
|
+
self.functions: list[FunctionInfo] = []
|
|
134
|
+
self.imports: list[str] = []
|
|
135
|
+
self.global_calls: list[str] = []
|
|
136
|
+
self.module_strings: list[str] = [] # All strings at module/class level
|
|
137
|
+
self.class_attributes: list[dict[str, str]] = [] # Class-level attributes
|
|
138
|
+
|
|
139
|
+
def parse(self) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Parse the source code.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if parsing succeeded, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
self.tree = ast.parse(self.source_code)
|
|
148
|
+
self._extract_imports()
|
|
149
|
+
self._extract_module_level_strings()
|
|
150
|
+
self._extract_functions()
|
|
151
|
+
self._extract_global_code()
|
|
152
|
+
return True
|
|
153
|
+
except SyntaxError as e:
|
|
154
|
+
print(f"Syntax error in source: {e}")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def _extract_module_level_strings(self) -> None:
|
|
158
|
+
"""Extract strings from module and class level (not in functions)."""
|
|
159
|
+
if self.tree is None:
|
|
160
|
+
return
|
|
161
|
+
for node in self.tree.body:
|
|
162
|
+
# Module-level assignments
|
|
163
|
+
if isinstance(node, ast.Assign):
|
|
164
|
+
for value_node in ast.walk(node.value):
|
|
165
|
+
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str):
|
|
166
|
+
self.module_strings.append(value_node.value)
|
|
167
|
+
|
|
168
|
+
# Class definitions
|
|
169
|
+
elif isinstance(node, ast.ClassDef):
|
|
170
|
+
for class_node in node.body:
|
|
171
|
+
if isinstance(class_node, ast.Assign):
|
|
172
|
+
# Class attribute
|
|
173
|
+
for target in class_node.targets:
|
|
174
|
+
if isinstance(target, ast.Name):
|
|
175
|
+
# Extract value if it's a string constant
|
|
176
|
+
if isinstance(class_node.value, ast.Constant):
|
|
177
|
+
if isinstance(class_node.value.value, str):
|
|
178
|
+
self.module_strings.append(class_node.value.value)
|
|
179
|
+
self.class_attributes.append(
|
|
180
|
+
{"name": target.id, "value": class_node.value.value}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _extract_imports(self) -> None:
|
|
184
|
+
"""Extract all import statements."""
|
|
185
|
+
if self.tree is None:
|
|
186
|
+
return
|
|
187
|
+
for node in ast.walk(self.tree):
|
|
188
|
+
if isinstance(node, ast.Import):
|
|
189
|
+
for alias in node.names:
|
|
190
|
+
self.imports.append(alias.name)
|
|
191
|
+
elif isinstance(node, ast.ImportFrom):
|
|
192
|
+
if node.module:
|
|
193
|
+
self.imports.append(node.module)
|
|
194
|
+
|
|
195
|
+
def _extract_functions(self) -> None:
|
|
196
|
+
"""Extract all function definitions."""
|
|
197
|
+
if self.tree is None:
|
|
198
|
+
return
|
|
199
|
+
for node in ast.walk(self.tree):
|
|
200
|
+
if isinstance(node, ast.FunctionDef):
|
|
201
|
+
func_info = self._analyze_function(node)
|
|
202
|
+
self.functions.append(func_info)
|
|
203
|
+
|
|
204
|
+
def _analyze_function(self, node: ast.FunctionDef) -> FunctionInfo:
|
|
205
|
+
"""
|
|
206
|
+
Analyze a function and extract security information.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
node: AST FunctionDef node
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
FunctionInfo with extracted data
|
|
213
|
+
"""
|
|
214
|
+
# Extract parameters
|
|
215
|
+
parameters = [arg.arg for arg in node.args.args]
|
|
216
|
+
|
|
217
|
+
# Extract docstring
|
|
218
|
+
docstring = ast.get_docstring(node)
|
|
219
|
+
|
|
220
|
+
# Get source code snippet
|
|
221
|
+
source_lines = self.source_code.split("\n")
|
|
222
|
+
func_source = "\n".join(source_lines[node.lineno - 1 : node.end_lineno])
|
|
223
|
+
|
|
224
|
+
# Create function info
|
|
225
|
+
func_info = FunctionInfo(
|
|
226
|
+
name=node.name,
|
|
227
|
+
parameters=parameters,
|
|
228
|
+
docstring=docstring,
|
|
229
|
+
line_number=node.lineno,
|
|
230
|
+
source_code=func_source,
|
|
231
|
+
ast_node=node,
|
|
232
|
+
imports=self.imports.copy(),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Analyze function body for security indicators
|
|
236
|
+
self._analyze_function_body(node, func_info)
|
|
237
|
+
|
|
238
|
+
return func_info
|
|
239
|
+
|
|
240
|
+
def _analyze_function_body(self, node: ast.FunctionDef, func_info: FunctionInfo):
|
|
241
|
+
"""Analyze function body for security patterns."""
|
|
242
|
+
|
|
243
|
+
for child in ast.walk(node):
|
|
244
|
+
# Check for function calls
|
|
245
|
+
if isinstance(child, ast.Call):
|
|
246
|
+
call_name = self._get_call_name(child)
|
|
247
|
+
if call_name:
|
|
248
|
+
func_info.function_calls.append(call_name)
|
|
249
|
+
|
|
250
|
+
# Check for security-relevant calls
|
|
251
|
+
if any(net in call_name for net in self.NETWORK_MODULES):
|
|
252
|
+
func_info.has_network_calls = True
|
|
253
|
+
if any(file_op in call_name for file_op in self.FILE_OPERATIONS):
|
|
254
|
+
func_info.has_file_operations = True
|
|
255
|
+
if any(sub in call_name for sub in self.SUBPROCESS_PATTERNS):
|
|
256
|
+
func_info.has_subprocess = True
|
|
257
|
+
if any(danger in call_name for danger in self.DANGEROUS_FUNCTIONS):
|
|
258
|
+
func_info.has_eval_exec = True
|
|
259
|
+
|
|
260
|
+
# Extract string literals
|
|
261
|
+
elif isinstance(child, ast.Constant) and isinstance(child.value, str):
|
|
262
|
+
if len(child.value) > 5: # Skip very short strings
|
|
263
|
+
func_info.string_literals.append(child.value)
|
|
264
|
+
|
|
265
|
+
# Track assignments
|
|
266
|
+
elif isinstance(child, ast.Assign):
|
|
267
|
+
for target in child.targets:
|
|
268
|
+
if isinstance(target, ast.Name):
|
|
269
|
+
func_info.assignments.append(target.id)
|
|
270
|
+
|
|
271
|
+
def _get_call_name(self, node: ast.Call) -> str | None:
|
|
272
|
+
"""Extract function call name from AST node."""
|
|
273
|
+
if isinstance(node.func, ast.Name):
|
|
274
|
+
return node.func.id
|
|
275
|
+
elif isinstance(node.func, ast.Attribute):
|
|
276
|
+
# Handle module.function() calls
|
|
277
|
+
if isinstance(node.func.value, ast.Name):
|
|
278
|
+
return f"{node.func.value.id}.{node.func.attr}"
|
|
279
|
+
return node.func.attr
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _extract_global_code(self) -> None:
|
|
283
|
+
"""Extract code executed at module level (not in functions)."""
|
|
284
|
+
if self.tree is None:
|
|
285
|
+
return
|
|
286
|
+
for node in self.tree.body:
|
|
287
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
288
|
+
call_name = self._get_call_name(node.value)
|
|
289
|
+
if call_name:
|
|
290
|
+
self.global_calls.append(call_name)
|
|
291
|
+
|
|
292
|
+
def get_functions(self) -> list[FunctionInfo]:
|
|
293
|
+
"""Get all analyzed functions."""
|
|
294
|
+
return self.functions
|
|
295
|
+
|
|
296
|
+
def has_security_indicators(self) -> dict[str, bool]:
|
|
297
|
+
"""Check if code has any security indicators."""
|
|
298
|
+
return {
|
|
299
|
+
"has_network": any(f.has_network_calls for f in self.functions),
|
|
300
|
+
"has_file_ops": any(f.has_file_operations for f in self.functions),
|
|
301
|
+
"has_subprocess": any(f.has_subprocess for f in self.functions),
|
|
302
|
+
"has_eval_exec": any(f.has_eval_exec for f in self.functions),
|
|
303
|
+
"has_dangerous_imports": any(
|
|
304
|
+
mod in self.imports for mod in self.NETWORK_MODULES + self.SUBPROCESS_PATTERNS
|
|
305
|
+
),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def get_inferred_tools(self) -> dict[str, bool]:
|
|
309
|
+
"""
|
|
310
|
+
Determine which Claude Code tools are implied by the code patterns.
|
|
311
|
+
|
|
312
|
+
Analyzes function calls, imports, and patterns in the code to infer
|
|
313
|
+
which Claude Code tools would be needed to execute similar operations.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Dictionary mapping tool names to whether they are detected
|
|
317
|
+
"""
|
|
318
|
+
inferred = {tool: False for tool in self.TOOL_INDICATORS.keys()}
|
|
319
|
+
|
|
320
|
+
# Collect all function calls from all functions
|
|
321
|
+
all_calls = set()
|
|
322
|
+
for func in self.functions:
|
|
323
|
+
all_calls.update(func.function_calls)
|
|
324
|
+
all_calls.update(self.global_calls)
|
|
325
|
+
|
|
326
|
+
# Also check imports for module-level indicators
|
|
327
|
+
import_based_tools = {
|
|
328
|
+
"requests": "Network",
|
|
329
|
+
"urllib": "Network",
|
|
330
|
+
"aiohttp": "Network",
|
|
331
|
+
"httpx": "Network",
|
|
332
|
+
"socket": "Network",
|
|
333
|
+
"subprocess": "Bash",
|
|
334
|
+
"glob": "Glob",
|
|
335
|
+
"fnmatch": "Glob",
|
|
336
|
+
"re": "Grep",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for module in self.imports:
|
|
340
|
+
base_module = module.split(".")[0]
|
|
341
|
+
if base_module in import_based_tools:
|
|
342
|
+
tool = import_based_tools[base_module]
|
|
343
|
+
inferred[tool] = True
|
|
344
|
+
|
|
345
|
+
# Check function calls against tool indicators
|
|
346
|
+
for tool, patterns in self.TOOL_INDICATORS.items():
|
|
347
|
+
for call in all_calls:
|
|
348
|
+
# Check both exact match and partial match for method calls
|
|
349
|
+
for pattern in patterns:
|
|
350
|
+
if pattern in call or call.endswith(pattern.split(".")[-1] if "." in pattern else pattern):
|
|
351
|
+
inferred[tool] = True
|
|
352
|
+
break
|
|
353
|
+
if inferred[tool]:
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
# Check for file operations that indicate Read/Write
|
|
357
|
+
for func in self.functions:
|
|
358
|
+
if func.has_file_operations:
|
|
359
|
+
# Need to check if it's read or write
|
|
360
|
+
for call in func.function_calls:
|
|
361
|
+
if any(r in call for r in ["read", "load"]):
|
|
362
|
+
inferred["Read"] = True
|
|
363
|
+
if any(w in call for w in ["write", "dump"]):
|
|
364
|
+
inferred["Write"] = True
|
|
365
|
+
if func.has_subprocess:
|
|
366
|
+
inferred["Bash"] = True
|
|
367
|
+
if func.has_network_calls:
|
|
368
|
+
inferred["Network"] = True
|
|
369
|
+
|
|
370
|
+
return inferred
|
|
371
|
+
|
|
372
|
+
def get_detected_tools_list(self) -> list[str]:
|
|
373
|
+
"""
|
|
374
|
+
Get a list of Claude Code tools that are detected in the code.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of tool names that were detected
|
|
378
|
+
"""
|
|
379
|
+
inferred = self.get_inferred_tools()
|
|
380
|
+
return [tool for tool, detected in inferred.items() if detected]
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
"""Semantic analysis modules for static analysis."""
|
|
18
|
+
|
|
19
|
+
from .name_resolver import NameResolver, Scope
|
|
20
|
+
from .type_analyzer import Type, TypeAnalyzer, TypeKind
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"NameResolver",
|
|
24
|
+
"Scope",
|
|
25
|
+
"TypeAnalyzer",
|
|
26
|
+
"Type",
|
|
27
|
+
"TypeKind",
|
|
28
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
"""Name resolution analysis for skill code.
|
|
18
|
+
|
|
19
|
+
Tracks variable definitions and resolves name references to their definitions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Scope:
|
|
27
|
+
"""Represents a lexical scope."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent: Optional["Scope"] = None) -> None:
|
|
30
|
+
"""Initialize scope.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
parent: Parent scope
|
|
34
|
+
"""
|
|
35
|
+
self.parent = parent
|
|
36
|
+
self.symbols: dict[str, Any] = {}
|
|
37
|
+
self.children: list[Scope] = []
|
|
38
|
+
|
|
39
|
+
def define(self, name: str, node: Any) -> None:
|
|
40
|
+
"""Define a symbol in this scope.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: Symbol name
|
|
44
|
+
node: AST node defining the symbol
|
|
45
|
+
"""
|
|
46
|
+
self.symbols[name] = node
|
|
47
|
+
|
|
48
|
+
def lookup(self, name: str) -> Any | None:
|
|
49
|
+
"""Look up a symbol in this scope or parent scopes.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: Symbol name
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
AST node or None if not found
|
|
56
|
+
"""
|
|
57
|
+
if name in self.symbols:
|
|
58
|
+
return self.symbols[name]
|
|
59
|
+
elif self.parent:
|
|
60
|
+
return self.parent.lookup(name)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NameResolver:
|
|
65
|
+
"""Resolves names to their definitions."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, ast_root: ast.AST):
|
|
68
|
+
"""Initialize name resolver.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
ast_root: Root AST node
|
|
72
|
+
"""
|
|
73
|
+
self.ast_root = ast_root
|
|
74
|
+
self.global_scope = Scope()
|
|
75
|
+
self.current_scope = self.global_scope
|
|
76
|
+
self.name_to_def: dict[Any, Any] = {}
|
|
77
|
+
|
|
78
|
+
def resolve(self) -> None:
|
|
79
|
+
"""Resolve all names in the AST."""
|
|
80
|
+
self._resolve_python(self.ast_root)
|
|
81
|
+
|
|
82
|
+
def _resolve_python(self, node: ast.AST) -> None:
|
|
83
|
+
"""Resolve names in Python AST.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
node: Python AST node
|
|
87
|
+
"""
|
|
88
|
+
self._visit_node(node)
|
|
89
|
+
|
|
90
|
+
def _visit_node(self, node: ast.AST) -> None:
|
|
91
|
+
"""Visit a node and its children with proper scope tracking.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
node: AST node to visit
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
97
|
+
self._visit_function(node)
|
|
98
|
+
elif isinstance(node, ast.ClassDef):
|
|
99
|
+
self._visit_class(node)
|
|
100
|
+
elif isinstance(node, ast.Assign):
|
|
101
|
+
self._define_assignment(node)
|
|
102
|
+
for child in ast.iter_child_nodes(node):
|
|
103
|
+
self._visit_node(child)
|
|
104
|
+
elif isinstance(node, ast.Import):
|
|
105
|
+
self._define_import(node)
|
|
106
|
+
elif isinstance(node, ast.ImportFrom):
|
|
107
|
+
self._define_import_from(node)
|
|
108
|
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
109
|
+
self._resolve_name(node)
|
|
110
|
+
else:
|
|
111
|
+
for child in ast.iter_child_nodes(node):
|
|
112
|
+
self._visit_node(child)
|
|
113
|
+
|
|
114
|
+
def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
115
|
+
"""Visit function with proper scope management.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
node: Function definition node
|
|
119
|
+
"""
|
|
120
|
+
self.current_scope.define(node.name, node)
|
|
121
|
+
|
|
122
|
+
func_scope = Scope(parent=self.current_scope)
|
|
123
|
+
self.current_scope.children.append(func_scope)
|
|
124
|
+
old_scope = self.current_scope
|
|
125
|
+
self.current_scope = func_scope
|
|
126
|
+
|
|
127
|
+
for arg in node.args.args:
|
|
128
|
+
func_scope.define(arg.arg, arg)
|
|
129
|
+
|
|
130
|
+
for child in node.body:
|
|
131
|
+
self._visit_node(child)
|
|
132
|
+
|
|
133
|
+
self.current_scope = old_scope
|
|
134
|
+
|
|
135
|
+
def _visit_class(self, node: ast.ClassDef) -> None:
|
|
136
|
+
"""Visit class with proper scope management.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
node: Class definition node
|
|
140
|
+
"""
|
|
141
|
+
self.current_scope.define(node.name, node)
|
|
142
|
+
|
|
143
|
+
class_scope = Scope(parent=self.current_scope)
|
|
144
|
+
self.current_scope.children.append(class_scope)
|
|
145
|
+
old_scope = self.current_scope
|
|
146
|
+
self.current_scope = class_scope
|
|
147
|
+
|
|
148
|
+
for child in node.body:
|
|
149
|
+
self._visit_node(child)
|
|
150
|
+
|
|
151
|
+
self.current_scope = old_scope
|
|
152
|
+
|
|
153
|
+
def _define_assignment(self, node: ast.Assign) -> None:
|
|
154
|
+
"""Define variables from assignment.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
node: Assignment node
|
|
158
|
+
"""
|
|
159
|
+
for target in node.targets:
|
|
160
|
+
if isinstance(target, ast.Name):
|
|
161
|
+
self.current_scope.define(target.id, node)
|
|
162
|
+
elif isinstance(target, ast.Tuple):
|
|
163
|
+
for elt in target.elts:
|
|
164
|
+
if isinstance(elt, ast.Name):
|
|
165
|
+
self.current_scope.define(elt.id, node)
|
|
166
|
+
|
|
167
|
+
def _define_import(self, node: ast.Import) -> None:
|
|
168
|
+
"""Define imported names.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
node: Import node
|
|
172
|
+
"""
|
|
173
|
+
for alias in node.names:
|
|
174
|
+
name = alias.asname if alias.asname else alias.name
|
|
175
|
+
self.current_scope.define(name, node)
|
|
176
|
+
|
|
177
|
+
def _define_import_from(self, node: ast.ImportFrom) -> None:
|
|
178
|
+
"""Define names from 'from ... import' statement.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
node: ImportFrom node
|
|
182
|
+
"""
|
|
183
|
+
for alias in node.names:
|
|
184
|
+
name = alias.asname if alias.asname else alias.name
|
|
185
|
+
self.current_scope.define(name, node)
|
|
186
|
+
|
|
187
|
+
def _resolve_name(self, node: ast.Name) -> None:
|
|
188
|
+
"""Resolve a name reference to its definition.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
node: Name node
|
|
192
|
+
"""
|
|
193
|
+
definition = self.current_scope.lookup(node.id)
|
|
194
|
+
if definition:
|
|
195
|
+
self.name_to_def[node] = definition
|
|
196
|
+
|
|
197
|
+
def get_definition(self, node: Any) -> Any | None:
|
|
198
|
+
"""Get the definition for a name usage.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
node: Name usage node
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Definition node or None
|
|
205
|
+
"""
|
|
206
|
+
return self.name_to_def.get(node)
|