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,60 @@
1
+ //////////////////////////////////////////
2
+ // Tool Chaining Abuse Detection
3
+ // Target: Suspicious multi-step operations that could exfiltrate data
4
+ // For Claude Skills that chain operations suspiciously
5
+ //////////////////////////////////////////
6
+
7
+ rule tool_chaining_abuse{
8
+
9
+ meta:
10
+ author = "Cisco"
11
+ description = "Detects suspicious tool chaining patterns that could lead to data exfiltration"
12
+ classification = "harmful"
13
+ threat_type = "TOOL CHAINING ABUSE"
14
+
15
+ strings:
16
+
17
+ // Read → Send pattern
18
+ $read_then_send = /\b(read|fetch|get|retrieve|collect|gather)\b[^\n]{0,100}\b(send|post|upload|transmit|forward|email|slack|webhook)\b/i
19
+
20
+ // Collect → Exfiltrate pattern
21
+ $collect_exfiltrate = /\b(collect (all |every |entire )?(data|files?|info|credentials?|secrets?|keys?|tokens?))\b[^\n]{0,150}\b(send|post|upload|transmit|curl|wget|requests\.post)\b/i
22
+
23
+ // Multi-step with network
24
+ $multistep_network = /\b(first|then|next|after|finally),? (read|collect|gather)\b[^\n]{0,100}\b(then|next|after|finally),? (send|post|upload)\b/i
25
+
26
+ // Summarize and send externally
27
+ $summarize_send = /\b(summarize|aggregate|compile)\b[^\n]{0,80}\b(send|post|email|slack) (to|via) (external|webhook|url|endpoint|api)\b/i
28
+
29
+ // Read environment → Network
30
+ $env_to_network = /\b(os\.environ|getenv|process\.env)\b[^\n]{0,150}\b(requests\.|urllib\.|curl|wget|socket\.)\b/i
31
+
32
+ // File traversal → Collection → Send
33
+ $traverse_collect_send = /\b(walk|rglob|listdir|scandir|find)\b[^\n]{0,100}\b(open|read)\b[^\n]{0,100}\b(send|post|upload)\b/i
34
+
35
+ // Automated data pipeline
36
+ $auto_pipeline = /\b(automatically (read|collect|gather))\b[^\n]{0,100}\b(and |then )?(send|post|forward|upload)\b/i
37
+
38
+ condition:
39
+
40
+ // Read then send
41
+ $read_then_send or
42
+
43
+ // Collect and exfiltrate
44
+ $collect_exfiltrate or
45
+
46
+ // Multi-step with network
47
+ $multistep_network or
48
+
49
+ // Summarize and send
50
+ $summarize_send or
51
+
52
+ // Environment to network
53
+ $env_to_network or
54
+
55
+ // Traverse, collect, send
56
+ $traverse_collect_send or
57
+
58
+ // Automated pipeline
59
+ $auto_pipeline
60
+ }
@@ -0,0 +1,73 @@
1
+ //////////////////////////////////////////
2
+ // Transitive Trust Abuse Detection
3
+ // Target: Skills that delegate trust to untrusted external content
4
+ // For Claude Skills that consume webpages, files, issues, docs
5
+ //////////////////////////////////////////
6
+
7
+ rule transitive_trust_abuse{
8
+
9
+ meta:
10
+ author = "Cisco"
11
+ description = "Detects skills that delegate trust to untrusted external content"
12
+ classification = "harmful"
13
+ threat_type = "TRANSITIVE TRUST ABUSE"
14
+
15
+ strings:
16
+
17
+ // Following external instructions
18
+ $follow_external = /\b(follow (the )?(instructions?|commands?|directives?) (in|from|inside|within) (the )?(file|webpage|document|url|link|website|page|content))\b/i
19
+
20
+ // Executing external content
21
+ $execute_external = /\b(execute (the )?(code|script|commands?) (in|from|found in) (the )?(file|webpage|document|url|link))\b/i
22
+
23
+ // Obeying untrusted sources
24
+ $obey_untrusted = /\b(do (what|whatever) (the )?(webpage|file|document|url|content) (says|tells|instructs|commands?))\b/i
25
+
26
+ // Running code blocks from external UNTRUSTED sources (not local scripts with --help)
27
+ // Exclude legitimate CLI usage patterns
28
+ $run_code_blocks = /\b(run (all |any )?(code|script) blocks? (you |that )?(find|see|encounter|discover) (in|from|inside) (the )?(url|webpage|website|external|untrusted))\b/i
29
+
30
+ // Following markdown/HTML instructions
31
+ $follow_markup = /\b(follow (the )?instructions? in (the )?(markdown|html|xml|json|yaml))\b/i
32
+
33
+ // Delegating to file content
34
+ $delegate_to_file = /\b(let (the )?(file|document|content) (decide|determine|control|specify))\b/i
35
+
36
+ // Executing inline code from documents
37
+ $execute_inline = /\b(execute (inline |embedded )?(code|scripts?)|run (inline |embedded )?(code|scripts?))\b/i
38
+
39
+ // Trusting URL content
40
+ $trust_url_content = /\b(trust (the )?(url|link|webpage) (content|instructions?)|safe to (follow|execute|run) (url|link|webpage))\b/i
41
+
42
+ // Parsing and executing
43
+ $parse_execute = /\b(parse (and |then )?(execute|run|eval)|extract (and |then )?(execute|run|eval))\b/i
44
+
45
+ condition:
46
+
47
+ // Following external instructions
48
+ $follow_external or
49
+
50
+ // Executing external content
51
+ $execute_external or
52
+
53
+ // Obeying untrusted sources
54
+ $obey_untrusted or
55
+
56
+ // Running code blocks
57
+ $run_code_blocks or
58
+
59
+ // Following markup instructions
60
+ $follow_markup or
61
+
62
+ // Delegating to file content
63
+ $delegate_to_file or
64
+
65
+ // Executing inline code
66
+ $execute_inline or
67
+
68
+ // Trusting URL content
69
+ $trust_url_content or
70
+
71
+ // Parse and execute
72
+ $parse_execute
73
+ }
@@ -0,0 +1,65 @@
1
+ //////////////////////////////////////////
2
+ // Unicode Steganography and Hidden Characters Detection
3
+ // Target: Invisible Unicode used for prompt injection
4
+ // Based on: https://en.wikipedia.org/wiki/Tags_(Unicode_block)
5
+ //////////////////////////////////////////
6
+
7
+ rule unicode_steganography{
8
+
9
+ meta:
10
+ author = "Cisco"
11
+ description = "Detects hidden Unicode characters used for invisible prompt injection and steganography"
12
+ classification = "harmful"
13
+ threat_type = "PROMPT INJECTION"
14
+ reference = "https://en.wikipedia.org/wiki/Tags_(Unicode_block)"
15
+
16
+ strings:
17
+
18
+ // --- 1. Unicode Tag Regex Patterns ---
19
+ // Catches \uE00xx, \u{E00xx}, and \U000E00xx encoding styles
20
+ $unicode_tag_pattern = /\\u(\{)?[Ee]00[0-7][0-9A-Fa-f](\})?/
21
+ $unicode_long_tag = /\\U000[Ee]00[0-7][0-9A-Fa-f]/
22
+
23
+ // --- 2. Zero-width characters (steganography) ---
24
+ // UTF-8 hex encoding
25
+ $zw_space = "\xE2\x80\x8B" // U+200B ZERO WIDTH SPACE
26
+ $zw_non_joiner = "\xE2\x80\x8C" // U+200C
27
+ $zw_joiner = "\xE2\x80\x8D" // U+200D
28
+
29
+ // --- 3. Directional Overrides (text spoofing) ---
30
+ $rtlo = "\xE2\x80\xAE" // U+202E RIGHT-TO-LEFT OVERRIDE
31
+ $ltro = "\xE2\x80\xAD" // U+202D LEFT-TO-RIGHT OVERRIDE
32
+
33
+ // --- 4. Invisible separators ---
34
+ $line_separator = "\xE2\x80\xA8" // U+2028 LINE SEPARATOR
35
+ $paragraph_separator = "\xE2\x80\xA9" // U+2029 PARAGRAPH SEPARATOR
36
+
37
+ // --- 5. Homoglyph detection ---
38
+ $cyrillic_a = "\xD0\x90" // А (Cyrillic A mimics Latin A)
39
+ $cyrillic_e = "\xD0\x95" // Š• (Cyrillic E mimics Latin E)
40
+ $cyrillic_o = "\xD0\x9E" // Šž (Cyrillic O mimics Latin O)
41
+
42
+ condition:
43
+
44
+ // Detection logic - flag and manually review (better safe than miss attack)
45
+ (
46
+ // Encoded tag characters in strings (any occurrence is suspicious)
47
+ $unicode_tag_pattern or
48
+ $unicode_long_tag or
49
+
50
+ // Zero-width steganography (tools alternate chars to encode binary 0s/1s)
51
+ // Aggregate count across all types is more effective than individual checks
52
+ (#zw_space + #zw_non_joiner + #zw_joiner) > 10 or
53
+
54
+ // Any directional override (highly suspicious in code/English text)
55
+ $rtlo or
56
+ $ltro or
57
+
58
+ // Invisible separators (no legitimate use in source code)
59
+ $line_separator or
60
+ $paragraph_separator or
61
+
62
+ // Homoglyph attacks (5+ Cyrillic chars mimicking Latin in English context)
63
+ (#cyrillic_a + #cyrillic_e + #cyrillic_o) > 5
64
+ )
65
+ }
@@ -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
+ """Git hooks for skill-analyzer."""
18
+
19
+ from .pre_commit import main as pre_commit_hook
20
+
21
+ __all__ = ["pre_commit_hook"]
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Cisco Systems, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+
18
+ """
19
+ Pre-commit hook for scanning Claude Skills for security issues.
20
+
21
+ This hook scans staged skill directories for security vulnerabilities
22
+ and blocks commits that contain HIGH or CRITICAL severity findings.
23
+
24
+ Usage:
25
+ 1. Install as a pre-commit hook:
26
+ skill-analyzer-pre-commit install
27
+
28
+ 2. Or add to .pre-commit-config.yaml:
29
+ - repo: local
30
+ hooks:
31
+ - id: skill-analyzer
32
+ name: Skill Analyzer
33
+ entry: skill-analyzer-pre-commit
34
+ language: python
35
+ types: [file]
36
+ pass_filenames: false
37
+
38
+ Configuration:
39
+ Create a .skillanalyzerrc file in your repo root:
40
+
41
+ {
42
+ "severity_threshold": "high", # block on: critical, high, medium, low
43
+ "skills_path": ".claude/skills",
44
+ "fail_fast": true
45
+ }
46
+ """
47
+
48
+ import argparse
49
+ import json
50
+ import subprocess
51
+ import sys
52
+ from pathlib import Path
53
+
54
+ # Default configuration
55
+ DEFAULT_CONFIG = {
56
+ "severity_threshold": "high", # Block commits on HIGH or CRITICAL
57
+ "skills_path": ".claude/skills", # Default Claude skills location
58
+ "fail_fast": True,
59
+ "use_behavioral": False,
60
+ "use_trigger": True,
61
+ }
62
+
63
+ # Severity levels (higher number = more severe)
64
+ SEVERITY_LEVELS = {
65
+ "safe": 0,
66
+ "info": 1,
67
+ "low": 2,
68
+ "medium": 3,
69
+ "high": 4,
70
+ "critical": 5,
71
+ }
72
+
73
+
74
+ def load_config(repo_root: Path) -> dict:
75
+ """
76
+ Load configuration from .skillanalyzerrc file.
77
+
78
+ Args:
79
+ repo_root: Repository root directory
80
+
81
+ Returns:
82
+ Configuration dictionary
83
+ """
84
+ config = DEFAULT_CONFIG.copy()
85
+
86
+ config_paths = [
87
+ repo_root / ".skillanalyzerrc",
88
+ repo_root / ".skillanalyzerrc.json",
89
+ repo_root / "skillanalyzer.json",
90
+ ]
91
+
92
+ for config_path in config_paths:
93
+ if config_path.exists():
94
+ try:
95
+ with open(config_path) as f:
96
+ user_config = json.load(f)
97
+ config.update(user_config)
98
+ break
99
+ except (OSError, json.JSONDecodeError) as e:
100
+ print(f"Warning: Failed to load config from {config_path}: {e}", file=sys.stderr)
101
+
102
+ return config
103
+
104
+
105
+ def get_staged_files() -> list[str]:
106
+ """
107
+ Get list of staged files from git.
108
+
109
+ Returns:
110
+ List of staged file paths relative to repo root
111
+ """
112
+ try:
113
+ result = subprocess.run(
114
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"],
115
+ capture_output=True,
116
+ text=True,
117
+ check=True,
118
+ )
119
+ return [f.strip() for f in result.stdout.split("\n") if f.strip()]
120
+ except subprocess.CalledProcessError:
121
+ return []
122
+
123
+
124
+ def get_affected_skills(staged_files: list[str], skills_path: str) -> set[Path]:
125
+ """
126
+ Identify skill directories affected by staged changes.
127
+
128
+ Args:
129
+ staged_files: List of staged file paths
130
+ skills_path: Base path for skills
131
+
132
+ Returns:
133
+ Set of affected skill directory paths
134
+ """
135
+ affected_skills = set()
136
+ skills_prefix = skills_path.rstrip("/") + "/"
137
+
138
+ for file_path in staged_files:
139
+ # Check if file is in skills directory
140
+ if file_path.startswith(skills_prefix) or file_path.startswith(skills_path):
141
+ # Extract the skill directory (first subdirectory under skills_path)
142
+ relative = file_path[len(skills_path) :].lstrip("/")
143
+ parts = relative.split("/")
144
+
145
+ if parts:
146
+ skill_dir = Path(skills_path) / parts[0]
147
+ skill_md = skill_dir / "SKILL.md"
148
+ if skill_md.exists():
149
+ affected_skills.add(skill_dir)
150
+
151
+ # Also check for SKILL.md directly in case skills are at different paths
152
+ if file_path.endswith("SKILL.md"):
153
+ skill_dir = Path(file_path).parent
154
+ affected_skills.add(skill_dir)
155
+
156
+ return affected_skills
157
+
158
+
159
+ def scan_skill(skill_dir: Path, config: dict) -> dict:
160
+ """
161
+ Scan a skill directory and return findings.
162
+
163
+ Args:
164
+ skill_dir: Path to skill directory
165
+ config: Configuration dictionary
166
+
167
+ Returns:
168
+ Scan results as dictionary
169
+ """
170
+ try:
171
+ from ..core.analyzers.base import BaseAnalyzer
172
+ from ..core.analyzers.static import StaticAnalyzer
173
+ from ..core.scanner import SkillScanner
174
+
175
+ analyzers: list[BaseAnalyzer] = [StaticAnalyzer()]
176
+
177
+ # Add optional analyzers based on config
178
+ if config.get("use_behavioral"):
179
+ try:
180
+ from ..core.analyzers.behavioral_analyzer import BehavioralAnalyzer
181
+
182
+ analyzers.append(BehavioralAnalyzer(use_static_analysis=True))
183
+ except ImportError:
184
+ pass
185
+
186
+ if config.get("use_trigger"):
187
+ try:
188
+ from ..core.analyzers.trigger_analyzer import TriggerAnalyzer
189
+
190
+ analyzers.append(TriggerAnalyzer())
191
+ except ImportError:
192
+ pass
193
+
194
+ scanner = SkillScanner(analyzers=analyzers)
195
+ result = scanner.scan_skill(skill_dir)
196
+
197
+ # Count findings by severity
198
+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
199
+ for f in result.findings:
200
+ sev = f.severity.value.lower() if hasattr(f.severity, "value") else str(f.severity).lower()
201
+ if sev in counts:
202
+ counts[sev] += 1
203
+
204
+ return {
205
+ "skill_name": result.skill_name,
206
+ "skill_directory": result.skill_directory,
207
+ "findings": [
208
+ {
209
+ "rule_id": f.rule_id,
210
+ "severity": f.severity.value if hasattr(f.severity, "value") else str(f.severity),
211
+ "title": f.title,
212
+ "description": f.description,
213
+ "file_path": f.file_path,
214
+ "line_number": f.line_number,
215
+ }
216
+ for f in result.findings
217
+ ],
218
+ "critical_count": counts["critical"],
219
+ "high_count": counts["high"],
220
+ "medium_count": counts["medium"],
221
+ "low_count": counts["low"],
222
+ }
223
+
224
+ except Exception as e:
225
+ return {
226
+ "skill_name": skill_dir.name,
227
+ "skill_directory": str(skill_dir),
228
+ "findings": [],
229
+ "error": str(e),
230
+ }
231
+
232
+
233
+ def check_severity_threshold(result: dict, threshold: str) -> bool:
234
+ """
235
+ Check if scan result exceeds severity threshold.
236
+
237
+ Args:
238
+ result: Scan result dictionary
239
+ threshold: Severity threshold string
240
+
241
+ Returns:
242
+ True if threshold is exceeded (should block commit)
243
+ """
244
+ threshold_level = SEVERITY_LEVELS.get(threshold.lower(), SEVERITY_LEVELS["high"])
245
+
246
+ for finding in result.get("findings", []):
247
+ finding_level = SEVERITY_LEVELS.get(finding["severity"].lower(), 0)
248
+ if finding_level >= threshold_level:
249
+ return True
250
+
251
+ return False
252
+
253
+
254
+ def format_finding(finding: dict) -> str:
255
+ """Format a finding for console output."""
256
+ severity = finding["severity"].upper()
257
+ title = finding["title"]
258
+ location = finding.get("file_path", "")
259
+
260
+ if finding.get("line_number"):
261
+ location = f"{location}:{finding['line_number']}"
262
+
263
+ return f" [{severity}] {title}\n Location: {location}"
264
+
265
+
266
+ def main(args: list[str] | None = None) -> int:
267
+ """
268
+ Main entry point for pre-commit hook.
269
+
270
+ Args:
271
+ args: Command line arguments (for testing)
272
+
273
+ Returns:
274
+ Exit code (0 = success, 1 = blocked)
275
+ """
276
+ parser = argparse.ArgumentParser(description="Pre-commit hook for scanning Claude Skills")
277
+ parser.add_argument(
278
+ "--severity",
279
+ choices=["critical", "high", "medium", "low"],
280
+ help="Override severity threshold from config",
281
+ )
282
+ parser.add_argument(
283
+ "--skills-path",
284
+ help="Override skills path from config",
285
+ )
286
+ parser.add_argument(
287
+ "--all",
288
+ action="store_true",
289
+ help="Scan all skills, not just staged ones",
290
+ )
291
+ parser.add_argument(
292
+ "install",
293
+ nargs="?",
294
+ help="Install pre-commit hook",
295
+ )
296
+
297
+ parsed_args = parser.parse_args(args)
298
+
299
+ # Handle install command
300
+ if parsed_args.install == "install":
301
+ return install_hook()
302
+
303
+ # Find repo root
304
+ try:
305
+ result = subprocess.run(
306
+ ["git", "rev-parse", "--show-toplevel"],
307
+ capture_output=True,
308
+ text=True,
309
+ check=True,
310
+ )
311
+ repo_root = Path(result.stdout.strip())
312
+ except subprocess.CalledProcessError:
313
+ print("Error: Not a git repository", file=sys.stderr)
314
+ return 1
315
+
316
+ # Load config
317
+ config = load_config(repo_root)
318
+
319
+ # Apply command line overrides
320
+ if parsed_args.severity:
321
+ config["severity_threshold"] = parsed_args.severity
322
+ if parsed_args.skills_path:
323
+ config["skills_path"] = parsed_args.skills_path
324
+
325
+ # Get staged files and affected skills
326
+ if parsed_args.all:
327
+ skills_dir = repo_root / config["skills_path"]
328
+ if skills_dir.exists():
329
+ affected_skills = {d for d in skills_dir.iterdir() if d.is_dir() and (d / "SKILL.md").exists()}
330
+ else:
331
+ affected_skills = set()
332
+ else:
333
+ staged_files = get_staged_files()
334
+ affected_skills = get_affected_skills(staged_files, config["skills_path"])
335
+
336
+ if not affected_skills:
337
+ # No skills affected, allow commit
338
+ return 0
339
+
340
+ print(f"Scanning {len(affected_skills)} skill(s)...")
341
+
342
+ # Scan each affected skill
343
+ blocked = False
344
+ all_findings = []
345
+
346
+ for skill_dir in sorted(affected_skills):
347
+ print(f"\nšŸ“¦ {skill_dir.name}")
348
+
349
+ result = scan_skill(skill_dir, config)
350
+
351
+ if result.get("error"):
352
+ print(f" āš ļø Error: {result['error']}", file=sys.stderr)
353
+ continue
354
+
355
+ findings = result.get("findings", [])
356
+
357
+ if not findings:
358
+ print(" āœ… No issues found")
359
+ continue
360
+
361
+ # Check if threshold is exceeded
362
+ if check_severity_threshold(result, config["severity_threshold"]):
363
+ blocked = True
364
+ print(f" 🚫 Blocked (threshold: {config['severity_threshold'].upper()})")
365
+ else:
366
+ print(f" āš ļø {len(findings)} finding(s) below threshold")
367
+
368
+ # Print findings
369
+ for finding in findings:
370
+ print(format_finding(finding))
371
+ all_findings.append(finding)
372
+
373
+ if blocked and config.get("fail_fast"):
374
+ break
375
+
376
+ # Summary
377
+ print(f"\n{'=' * 50}")
378
+ if blocked:
379
+ print("āŒ Commit BLOCKED - fix security issues before committing")
380
+ print(f" Threshold: {config['severity_threshold'].upper()} and above")
381
+ return 1
382
+ elif all_findings:
383
+ print(f"āš ļø {len(all_findings)} finding(s) detected (below threshold)")
384
+ print(" Consider reviewing and fixing these issues")
385
+ return 0
386
+ else:
387
+ print("āœ… All skills passed security checks")
388
+ return 0
389
+
390
+
391
+ def install_hook() -> int:
392
+ """
393
+ Install the pre-commit hook in the current repository.
394
+
395
+ Returns:
396
+ Exit code
397
+ """
398
+ try:
399
+ result = subprocess.run(
400
+ ["git", "rev-parse", "--show-toplevel"],
401
+ capture_output=True,
402
+ text=True,
403
+ check=True,
404
+ )
405
+ repo_root = Path(result.stdout.strip())
406
+ except subprocess.CalledProcessError:
407
+ print("Error: Not a git repository", file=sys.stderr)
408
+ return 1
409
+
410
+ hooks_dir = repo_root / ".git" / "hooks"
411
+ hooks_dir.mkdir(exist_ok=True)
412
+
413
+ hook_path = hooks_dir / "pre-commit"
414
+
415
+ hook_script = """#!/bin/sh
416
+ # Skill Analyzer Pre-commit Hook
417
+ # Automatically scans Claude Skills for security issues
418
+
419
+ skill-analyzer-pre-commit "$@"
420
+ exit_code=$?
421
+
422
+ if [ $exit_code -ne 0 ]; then
423
+ echo ""
424
+ echo "To bypass this check (not recommended), use: git commit --no-verify"
425
+ fi
426
+
427
+ exit $exit_code
428
+ """
429
+
430
+ # Check if hook already exists
431
+ if hook_path.exists():
432
+ print(f"Warning: Pre-commit hook already exists at {hook_path}")
433
+ response = input("Overwrite? [y/N] ").strip().lower()
434
+ if response != "y":
435
+ print("Aborted")
436
+ return 1
437
+
438
+ hook_path.write_text(hook_script)
439
+ hook_path.chmod(0o755)
440
+
441
+ print(f"āœ… Pre-commit hook installed at {hook_path}")
442
+ print("\nConfiguration:")
443
+ print(" Create .skillanalyzerrc in your repo root to customize behavior:")
444
+ print(' { "severity_threshold": "high", "skills_path": ".claude/skills" }')
445
+
446
+ return 0
447
+
448
+
449
+ if __name__ == "__main__":
450
+ sys.exit(main())
@@ -0,0 +1,25 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """
18
+ Threat mapping and taxonomy for Claude Skill Analyzer.
19
+
20
+ Aligned with MCP Scanner's threat taxonomy.
21
+ """
22
+
23
+ from .threats import LLM_THREAT_MAPPING, YARA_THREAT_MAPPING, ThreatMapping
24
+
25
+ __all__ = ["ThreatMapping", "LLM_THREAT_MAPPING", "YARA_THREAT_MAPPING"]