cisco-ai-skill-scanner 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. cisco_ai_skill_scanner-1.0.0.dist-info/METADATA +253 -0
  2. cisco_ai_skill_scanner-1.0.0.dist-info/RECORD +100 -0
  3. cisco_ai_skill_scanner-1.0.0.dist-info/WHEEL +4 -0
  4. cisco_ai_skill_scanner-1.0.0.dist-info/entry_points.txt +4 -0
  5. cisco_ai_skill_scanner-1.0.0.dist-info/licenses/LICENSE +17 -0
  6. skillanalyzer/__init__.py +45 -0
  7. skillanalyzer/_version.py +34 -0
  8. skillanalyzer/api/__init__.py +25 -0
  9. skillanalyzer/api/api.py +34 -0
  10. skillanalyzer/api/api_cli.py +78 -0
  11. skillanalyzer/api/api_server.py +634 -0
  12. skillanalyzer/api/router.py +527 -0
  13. skillanalyzer/cli/__init__.py +25 -0
  14. skillanalyzer/cli/cli.py +816 -0
  15. skillanalyzer/config/__init__.py +26 -0
  16. skillanalyzer/config/config.py +149 -0
  17. skillanalyzer/config/config_parser.py +122 -0
  18. skillanalyzer/config/constants.py +85 -0
  19. skillanalyzer/core/__init__.py +24 -0
  20. skillanalyzer/core/analyzers/__init__.py +75 -0
  21. skillanalyzer/core/analyzers/aidefense_analyzer.py +872 -0
  22. skillanalyzer/core/analyzers/base.py +53 -0
  23. skillanalyzer/core/analyzers/behavioral/__init__.py +30 -0
  24. skillanalyzer/core/analyzers/behavioral/alignment/__init__.py +45 -0
  25. skillanalyzer/core/analyzers/behavioral/alignment/alignment_llm_client.py +240 -0
  26. skillanalyzer/core/analyzers/behavioral/alignment/alignment_orchestrator.py +216 -0
  27. skillanalyzer/core/analyzers/behavioral/alignment/alignment_prompt_builder.py +422 -0
  28. skillanalyzer/core/analyzers/behavioral/alignment/alignment_response_validator.py +136 -0
  29. skillanalyzer/core/analyzers/behavioral/alignment/threat_vulnerability_classifier.py +198 -0
  30. skillanalyzer/core/analyzers/behavioral_analyzer.py +453 -0
  31. skillanalyzer/core/analyzers/cross_skill_analyzer.py +490 -0
  32. skillanalyzer/core/analyzers/llm_analyzer.py +440 -0
  33. skillanalyzer/core/analyzers/llm_prompt_builder.py +270 -0
  34. skillanalyzer/core/analyzers/llm_provider_config.py +215 -0
  35. skillanalyzer/core/analyzers/llm_request_handler.py +284 -0
  36. skillanalyzer/core/analyzers/llm_response_parser.py +81 -0
  37. skillanalyzer/core/analyzers/meta_analyzer.py +845 -0
  38. skillanalyzer/core/analyzers/static.py +1105 -0
  39. skillanalyzer/core/analyzers/trigger_analyzer.py +341 -0
  40. skillanalyzer/core/analyzers/virustotal_analyzer.py +463 -0
  41. skillanalyzer/core/exceptions.py +77 -0
  42. skillanalyzer/core/loader.py +377 -0
  43. skillanalyzer/core/models.py +300 -0
  44. skillanalyzer/core/reporters/__init__.py +26 -0
  45. skillanalyzer/core/reporters/json_reporter.py +65 -0
  46. skillanalyzer/core/reporters/markdown_reporter.py +209 -0
  47. skillanalyzer/core/reporters/sarif_reporter.py +246 -0
  48. skillanalyzer/core/reporters/table_reporter.py +195 -0
  49. skillanalyzer/core/rules/__init__.py +19 -0
  50. skillanalyzer/core/rules/patterns.py +165 -0
  51. skillanalyzer/core/rules/yara_scanner.py +157 -0
  52. skillanalyzer/core/scanner.py +437 -0
  53. skillanalyzer/core/static_analysis/__init__.py +27 -0
  54. skillanalyzer/core/static_analysis/cfg/__init__.py +21 -0
  55. skillanalyzer/core/static_analysis/cfg/builder.py +439 -0
  56. skillanalyzer/core/static_analysis/context_extractor.py +742 -0
  57. skillanalyzer/core/static_analysis/dataflow/__init__.py +25 -0
  58. skillanalyzer/core/static_analysis/dataflow/forward_analysis.py +715 -0
  59. skillanalyzer/core/static_analysis/interprocedural/__init__.py +21 -0
  60. skillanalyzer/core/static_analysis/interprocedural/call_graph_analyzer.py +406 -0
  61. skillanalyzer/core/static_analysis/interprocedural/cross_file_analyzer.py +190 -0
  62. skillanalyzer/core/static_analysis/parser/__init__.py +21 -0
  63. skillanalyzer/core/static_analysis/parser/python_parser.py +380 -0
  64. skillanalyzer/core/static_analysis/semantic/__init__.py +28 -0
  65. skillanalyzer/core/static_analysis/semantic/name_resolver.py +206 -0
  66. skillanalyzer/core/static_analysis/semantic/type_analyzer.py +200 -0
  67. skillanalyzer/core/static_analysis/taint/__init__.py +21 -0
  68. skillanalyzer/core/static_analysis/taint/tracker.py +252 -0
  69. skillanalyzer/core/static_analysis/types/__init__.py +36 -0
  70. skillanalyzer/data/__init__.py +30 -0
  71. skillanalyzer/data/prompts/boilerplate_protection_rule_prompt.md +26 -0
  72. skillanalyzer/data/prompts/code_alignment_threat_analysis_prompt.md +901 -0
  73. skillanalyzer/data/prompts/llm_response_schema.json +71 -0
  74. skillanalyzer/data/prompts/skill_meta_analysis_prompt.md +303 -0
  75. skillanalyzer/data/prompts/skill_threat_analysis_prompt.md +263 -0
  76. skillanalyzer/data/prompts/unified_response_schema.md +97 -0
  77. skillanalyzer/data/rules/signatures.yaml +440 -0
  78. skillanalyzer/data/yara_rules/autonomy_abuse.yara +66 -0
  79. skillanalyzer/data/yara_rules/code_execution.yara +61 -0
  80. skillanalyzer/data/yara_rules/coercive_injection.yara +115 -0
  81. skillanalyzer/data/yara_rules/command_injection.yara +54 -0
  82. skillanalyzer/data/yara_rules/credential_harvesting.yara +115 -0
  83. skillanalyzer/data/yara_rules/prompt_injection.yara +71 -0
  84. skillanalyzer/data/yara_rules/script_injection.yara +83 -0
  85. skillanalyzer/data/yara_rules/skill_discovery_abuse.yara +57 -0
  86. skillanalyzer/data/yara_rules/sql_injection.yara +73 -0
  87. skillanalyzer/data/yara_rules/system_manipulation.yara +65 -0
  88. skillanalyzer/data/yara_rules/tool_chaining_abuse.yara +60 -0
  89. skillanalyzer/data/yara_rules/transitive_trust_abuse.yara +73 -0
  90. skillanalyzer/data/yara_rules/unicode_steganography.yara +65 -0
  91. skillanalyzer/hooks/__init__.py +21 -0
  92. skillanalyzer/hooks/pre_commit.py +450 -0
  93. skillanalyzer/threats/__init__.py +25 -0
  94. skillanalyzer/threats/threats.py +480 -0
  95. skillanalyzer/utils/__init__.py +28 -0
  96. skillanalyzer/utils/command_utils.py +129 -0
  97. skillanalyzer/utils/di_container.py +154 -0
  98. skillanalyzer/utils/file_utils.py +86 -0
  99. skillanalyzer/utils/logging_config.py +96 -0
  100. skillanalyzer/utils/logging_utils.py +71 -0
@@ -0,0 +1,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"]