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,198 @@
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
+ """Threat vs Vulnerability Classifier.
18
+
19
+ This module provides a second alignment layer that analyzes behavioral findings
20
+ to classify them as either:
21
+ - THREAT: Malicious intent, backdoors, intentional deception
22
+ - VULNERABILITY: Coding mistakes, unintentional security weaknesses
23
+ - UNCLEAR: Cannot determine with confidence
24
+
25
+ This helps distinguish between adversarial actors and developer mistakes.
26
+ """
27
+
28
+ import json
29
+ import logging
30
+ from typing import Any
31
+
32
+ from .alignment_llm_client import AlignmentLLMClient
33
+
34
+
35
+ class ThreatVulnerabilityClassifier:
36
+ """Classifies security findings as threats (malicious) or vulnerabilities (mistakes).
37
+
38
+ This second alignment layer analyzes the behavioral findings to determine
39
+ if the security issue appears to be:
40
+ - Intentional malicious behavior (threat)
41
+ - Unintentional coding mistake (vulnerability)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ model: str = "gemini/gemini-2.0-flash",
47
+ api_key: str | None = None,
48
+ base_url: str | None = None,
49
+ ):
50
+ """Initialize the threat/vulnerability classifier.
51
+
52
+ Args:
53
+ model: LLM model to use
54
+ api_key: API key for the LLM provider
55
+ base_url: Optional base URL for LLM API
56
+ """
57
+ self.logger = logging.getLogger(__name__)
58
+ self.llm_client = AlignmentLLMClient(
59
+ model=model,
60
+ api_key=api_key,
61
+ base_url=base_url,
62
+ )
63
+ self._classification_prompt_template = self._get_classification_prompt()
64
+
65
+ def _get_classification_prompt(self) -> str:
66
+ """Get the classification prompt template.
67
+
68
+ Returns:
69
+ Prompt template string
70
+ """
71
+ return """# Threat vs Vulnerability Classification
72
+
73
+ You are a security expert analyzing a security finding to determine if it represents:
74
+ - **THREAT**: Intentional malicious behavior (backdoor, data theft, deliberate deception)
75
+ - **VULNERABILITY**: Unintentional coding mistake (oversight, missing validation, poor practice)
76
+ - **UNCLEAR**: Cannot determine with confidence
77
+
78
+ ## Finding Details
79
+
80
+ - **Threat Name**: {threat_name}
81
+ - **Severity**: {severity}
82
+ - **Summary**: {summary}
83
+ - **Description Claims**: {description_claims}
84
+ - **Actual Behavior**: {actual_behavior}
85
+ - **Security Implications**: {security_implications}
86
+ - **Dataflow Evidence**: {dataflow_evidence}
87
+
88
+ ## Classification Criteria
89
+
90
+ **THREAT indicators** (malicious intent):
91
+ - Deliberately hidden functionality (obfuscated code, misleading names)
92
+ - Data exfiltration to attacker-controlled servers
93
+ - Credential harvesting without legitimate purpose
94
+ - Backdoor commands or remote code execution
95
+ - Deliberate mismatch between description and behavior
96
+ - Use of suspicious domains or encoded payloads
97
+
98
+ **VULNERABILITY indicators** (coding mistakes):
99
+ - Missing input validation
100
+ - Overly permissive file/network access
101
+ - Insufficient error handling
102
+ - Documentation that's incomplete but not deliberately misleading
103
+ - Common security anti-patterns
104
+ - Reasonable explanation for the code behavior
105
+
106
+ ## Response Format
107
+
108
+ Respond with valid JSON:
109
+
110
+ ```json
111
+ {{
112
+ "classification": "THREAT" or "VULNERABILITY" or "UNCLEAR",
113
+ "confidence": "HIGH" or "MEDIUM" or "LOW",
114
+ "reasoning": "Explanation for the classification",
115
+ "key_indicators": ["indicator1", "indicator2", ...]
116
+ }}
117
+ ```
118
+
119
+ Analyze the finding and provide your classification:
120
+ """
121
+
122
+ async def classify_finding(
123
+ self,
124
+ threat_name: str,
125
+ severity: str,
126
+ summary: str,
127
+ description_claims: str,
128
+ actual_behavior: str,
129
+ security_implications: str,
130
+ dataflow_evidence: str,
131
+ ) -> dict[str, Any] | None:
132
+ """Classify a finding as threat or vulnerability.
133
+
134
+ Args:
135
+ threat_name: The threat category name
136
+ severity: Severity level (HIGH/MEDIUM/LOW/INFO)
137
+ summary: Brief summary of the finding
138
+ description_claims: What the documentation claims
139
+ actual_behavior: What the code actually does
140
+ security_implications: Security impact description
141
+ dataflow_evidence: Dataflow analysis evidence
142
+
143
+ Returns:
144
+ Classification result dict with:
145
+ - classification: THREAT/VULNERABILITY/UNCLEAR
146
+ - confidence: HIGH/MEDIUM/LOW
147
+ - reasoning: Explanation
148
+ - key_indicators: List of indicators
149
+ Returns None if classification fails
150
+ """
151
+ try:
152
+ # Build the classification prompt
153
+ prompt = self._classification_prompt_template.format(
154
+ threat_name=threat_name,
155
+ severity=severity,
156
+ summary=summary,
157
+ description_claims=description_claims,
158
+ actual_behavior=actual_behavior,
159
+ security_implications=security_implications,
160
+ dataflow_evidence=dataflow_evidence,
161
+ )
162
+
163
+ # Get LLM classification
164
+ response = await self.llm_client.verify_alignment(prompt)
165
+
166
+ if not response or not response.strip():
167
+ self.logger.warning("Empty response from LLM for threat/vulnerability classification")
168
+ return None
169
+
170
+ # Parse JSON response
171
+ try:
172
+ classification = json.loads(response)
173
+
174
+ # Validate required fields
175
+ required_fields = ["classification", "confidence", "reasoning", "key_indicators"]
176
+ if not all(field in classification for field in required_fields):
177
+ self.logger.warning(f"Classification response missing required fields: {classification}")
178
+ return None
179
+
180
+ # Validate classification value
181
+ valid_classifications = ["THREAT", "VULNERABILITY", "UNCLEAR"]
182
+ if classification["classification"] not in valid_classifications:
183
+ self.logger.warning(f"Invalid classification value: {classification['classification']}")
184
+ return None
185
+
186
+ self.logger.debug(
187
+ f"Classified as {classification['classification']} with {classification['confidence']} confidence"
188
+ )
189
+ return classification
190
+
191
+ except json.JSONDecodeError as e:
192
+ self.logger.warning(f"Failed to parse classification JSON: {e}")
193
+ self.logger.debug(f"Raw response: {response[:500]}")
194
+ return None
195
+
196
+ except Exception as e:
197
+ self.logger.error(f"Error classifying finding: {e}", exc_info=True)
198
+ return None
@@ -0,0 +1,453 @@
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
+ Behavioral analyzer for Claude Skills using static dataflow analysis.
19
+
20
+ Analyzes skill scripts using AST parsing, dataflow tracking, and description-behavior
21
+ alignment checking. Detects threats through code analysis without execution.
22
+
23
+ Features:
24
+ - Static dataflow analysis for code behavior tracking
25
+ - Cross-file correlation analysis
26
+ - LLM-powered alignment verification (optional)
27
+ - Threat vs vulnerability classification
28
+ """
29
+
30
+ import asyncio
31
+ import hashlib
32
+ import logging
33
+ import os
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ from ...core.models import Finding, Severity, Skill, ThreatCategory
38
+ from ...core.static_analysis.context_extractor import (
39
+ ContextExtractor,
40
+ SkillFunctionContext,
41
+ SkillScriptContext,
42
+ )
43
+ from ...core.static_analysis.interprocedural.call_graph_analyzer import CallGraphAnalyzer
44
+ from ...core.static_analysis.interprocedural.cross_file_analyzer import CrossFileAnalyzer, CrossFileCorrelation
45
+ from .base import BaseAnalyzer
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class BehavioralAnalyzer(BaseAnalyzer):
51
+ """
52
+ Behavioral analyzer using static dataflow analysis.
53
+
54
+ Analyzes skill scripts through:
55
+ 1. AST parsing and function extraction
56
+ 2. Dataflow tracking (sources → sinks)
57
+ 3. Description-behavior alignment checking (optional LLM-powered)
58
+ 4. Threat pattern detection
59
+ 5. Cross-file correlation analysis
60
+
61
+ Does NOT execute code - uses static analysis for safety.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ use_static_analysis: bool = True,
67
+ use_alignment_verification: bool = False,
68
+ llm_model: str | None = None,
69
+ llm_api_key: str | None = None,
70
+ ):
71
+ """
72
+ Initialize behavioral analyzer.
73
+
74
+ Args:
75
+ use_static_analysis: Deprecated parameter, kept for backward compatibility.
76
+ Static analysis is always enabled as it's required for the analyzer to function.
77
+ use_alignment_verification: Enable LLM-powered alignment verification
78
+ llm_model: LLM model for alignment verification (e.g., "gemini/gemini-2.0-flash")
79
+ llm_api_key: API key for the LLM provider (or resolved from environment)
80
+
81
+ Note:
82
+ This analyzer currently only processes Python (.py) files.
83
+ Markdown files with code blocks (e.g., bash in .md files) are not analyzed.
84
+ Use the LLM analyzer for comprehensive markdown/bash code block analysis.
85
+ """
86
+ super().__init__("behavioral_analyzer")
87
+
88
+ # Static analysis is always required - the parameter is kept for backward compatibility
89
+ if not use_static_analysis:
90
+ logger.warning(
91
+ "use_static_analysis=False is deprecated and ignored. "
92
+ "Static analysis is required for the behavioral analyzer to function."
93
+ )
94
+ self.use_static_analysis = True # Always enabled
95
+ self.use_alignment_verification = use_alignment_verification
96
+ self.context_extractor = ContextExtractor() # Always initialized
97
+
98
+ # Alignment verification (LLM-powered)
99
+ self.alignment_orchestrator = None
100
+ if use_alignment_verification:
101
+ try:
102
+ from .behavioral.alignment import AlignmentOrchestrator
103
+
104
+ # Resolve LLM configuration - use SKILL_SCANNER_LLM_* variables
105
+ model = llm_model or os.environ.get("SKILL_SCANNER_LLM_MODEL", "gemini/gemini-2.0-flash")
106
+ api_key = llm_api_key or os.environ.get("SKILL_SCANNER_LLM_API_KEY")
107
+
108
+ if api_key:
109
+ self.alignment_orchestrator = AlignmentOrchestrator(
110
+ llm_model=model,
111
+ llm_api_key=api_key,
112
+ )
113
+ logger.info("Alignment verification enabled with %s", model)
114
+ else:
115
+ logger.warning("Alignment verification requested but no API key found")
116
+ except ImportError as e:
117
+ logger.warning("Alignment verification not available: %s", e)
118
+
119
+ def analyze(self, skill: Skill) -> list[Finding]:
120
+ """
121
+ Analyze skill using static dataflow analysis.
122
+
123
+ Note: Currently only analyzes Python files. Markdown files with
124
+ bash/shell code blocks require the LLM analyzer.
125
+
126
+ Args:
127
+ skill: Skill to analyze
128
+
129
+ Returns:
130
+ List of behavioral findings
131
+ """
132
+ return self._analyze_static(skill)
133
+
134
+ def _analyze_static(self, skill: Skill) -> list[Finding]:
135
+ """Analyze skill using static dataflow analysis with cross-file correlation."""
136
+ findings = []
137
+ cross_file = CrossFileAnalyzer()
138
+ call_graph_analyzer = CallGraphAnalyzer()
139
+
140
+ # Get skill description for alignment verification
141
+ skill_description = None
142
+ if skill.manifest:
143
+ skill_description = skill.manifest.description
144
+
145
+ # First pass: Extract context from each Python script
146
+ for script_file in skill.get_scripts():
147
+ if script_file.file_type != "python":
148
+ continue
149
+
150
+ content = script_file.read_content()
151
+ if not content:
152
+ continue
153
+
154
+ # Add to call graph analyzer
155
+ call_graph_analyzer.add_file(script_file.path, content)
156
+
157
+ # Extract security context
158
+ try:
159
+ context = self.context_extractor.extract_context(script_file.path, content)
160
+
161
+ # Add to cross-file analyzer
162
+ cross_file.add_file_context(script_file.relative_path, context)
163
+
164
+ # Generate findings from individual file context
165
+ script_findings = self._generate_findings_from_context(context, skill)
166
+ findings.extend(script_findings)
167
+
168
+ # Alignment verification (LLM-powered)
169
+ if self.alignment_orchestrator:
170
+ alignment_findings = self._run_alignment_verification(script_file.path, content, skill_description)
171
+ findings.extend(alignment_findings)
172
+
173
+ except Exception as e:
174
+ logger.warning("Failed to analyze %s: %s", script_file.relative_path, e)
175
+
176
+ # Build call graph for cross-file analysis
177
+ call_graph_analyzer.build_call_graph()
178
+
179
+ # Second pass: Analyze cross-file correlations
180
+ correlations = cross_file.analyze_correlations()
181
+ correlation_findings = self._generate_findings_from_correlations(correlations, skill)
182
+ findings.extend(correlation_findings)
183
+
184
+ return findings
185
+
186
+ def _run_alignment_verification(
187
+ self,
188
+ file_path: Path,
189
+ source_code: str,
190
+ skill_description: str | None,
191
+ ) -> list[Finding]:
192
+ """Run LLM-powered alignment verification on a file.
193
+
194
+ Args:
195
+ file_path: Path to the script file
196
+ source_code: Python source code
197
+ skill_description: Overall skill description from SKILL.md
198
+
199
+ Returns:
200
+ List of findings from alignment verification
201
+ """
202
+ findings = []
203
+
204
+ if not self.alignment_orchestrator:
205
+ return findings
206
+
207
+ try:
208
+ # Extract function contexts for alignment verification
209
+ function_contexts = self.context_extractor.extract_function_contexts(file_path, source_code)
210
+
211
+ # Run alignment verification on each function
212
+ for func_context in function_contexts:
213
+ try:
214
+ # Run async alignment check
215
+ result = asyncio.get_event_loop().run_until_complete(
216
+ self.alignment_orchestrator.check_alignment(func_context, skill_description)
217
+ )
218
+
219
+ if result:
220
+ analysis, ctx = result
221
+ finding = self._create_alignment_finding(analysis, ctx, str(file_path))
222
+ if finding:
223
+ findings.append(finding)
224
+
225
+ except Exception as e:
226
+ logger.warning("Alignment check failed for %s: %s", func_context.name, e)
227
+
228
+ except Exception as e:
229
+ logger.warning("Alignment verification failed for %s: %s", file_path, e)
230
+
231
+ return findings
232
+
233
+ def _create_alignment_finding(
234
+ self,
235
+ analysis: dict[str, Any],
236
+ func_context: SkillFunctionContext,
237
+ file_path: str,
238
+ ) -> Finding | None:
239
+ """Create a Finding from alignment verification result.
240
+
241
+ Args:
242
+ analysis: Analysis dict from LLM
243
+ func_context: Function context that was analyzed
244
+ file_path: Path to the source file
245
+
246
+ Returns:
247
+ Finding object or None if invalid
248
+ """
249
+ try:
250
+ threat_name = analysis.get("threat_name", "ALIGNMENT_MISMATCH").upper()
251
+ severity_str = analysis.get("severity", "MEDIUM").upper()
252
+
253
+ # Map severity
254
+ severity_map = {
255
+ "CRITICAL": Severity.CRITICAL,
256
+ "HIGH": Severity.HIGH,
257
+ "MEDIUM": Severity.MEDIUM,
258
+ "LOW": Severity.LOW,
259
+ "INFO": Severity.LOW,
260
+ }
261
+ severity = severity_map.get(severity_str, Severity.MEDIUM)
262
+
263
+ # Map threat name to category
264
+ category_map = {
265
+ "DATA EXFILTRATION": ThreatCategory.DATA_EXFILTRATION,
266
+ "CREDENTIAL THEFT": ThreatCategory.DATA_EXFILTRATION,
267
+ "COMMAND INJECTION": ThreatCategory.COMMAND_INJECTION,
268
+ "HIDDEN FUNCTIONALITY": ThreatCategory.POLICY_VIOLATION,
269
+ "ALIGNMENT_MISMATCH": ThreatCategory.POLICY_VIOLATION,
270
+ }
271
+ category = category_map.get(threat_name, ThreatCategory.POLICY_VIOLATION)
272
+
273
+ # Build description
274
+ description_claims = analysis.get("description_claims", "")
275
+ actual_behavior = analysis.get("actual_behavior", "")
276
+ summary = analysis.get("summary", f"Alignment mismatch in {func_context.name}")
277
+
278
+ if description_claims and actual_behavior:
279
+ description = (
280
+ f"{summary}. Description claims: '{description_claims}'. Actual behavior: {actual_behavior}"
281
+ )
282
+ else:
283
+ description = summary
284
+
285
+ return Finding(
286
+ id=self._generate_id(f"ALIGNMENT_{threat_name}", f"{file_path}:{func_context.name}"),
287
+ rule_id=f"BEHAVIOR_ALIGNMENT_{threat_name.replace(' ', '_')}",
288
+ category=category,
289
+ severity=severity,
290
+ title=f"Alignment mismatch: {threat_name} in {func_context.name}",
291
+ description=description,
292
+ file_path=file_path,
293
+ line_number=func_context.line_number,
294
+ remediation=f"Review function {func_context.name} and ensure documentation matches implementation",
295
+ analyzer="behavioral",
296
+ metadata={
297
+ "function_name": func_context.name,
298
+ "threat_name": threat_name,
299
+ "confidence": analysis.get("confidence"),
300
+ "security_implications": analysis.get("security_implications"),
301
+ "dataflow_evidence": analysis.get("dataflow_evidence"),
302
+ "classification": analysis.get("threat_vulnerability_classification"),
303
+ },
304
+ )
305
+
306
+ except Exception as e:
307
+ logger.warning("Failed to create alignment finding: %s", e)
308
+ return None
309
+
310
+ def _generate_findings_from_context(self, context: SkillScriptContext, skill: Skill) -> list[Finding]:
311
+ """Generate security findings from extracted context."""
312
+ findings = []
313
+
314
+ # Check for exfiltration patterns
315
+ if context.has_network and context.has_env_var_access:
316
+ findings.append(
317
+ Finding(
318
+ id=self._generate_id("ENV_VAR_EXFILTRATION", context.file_path),
319
+ rule_id="BEHAVIOR_ENV_VAR_EXFILTRATION",
320
+ category=ThreatCategory.DATA_EXFILTRATION,
321
+ severity=Severity.CRITICAL,
322
+ title="Environment variable access with network calls detected",
323
+ description=f"Script accesses environment variables and makes network calls in {context.file_path}",
324
+ file_path=context.file_path,
325
+ remediation="Remove environment variable harvesting or network transmission",
326
+ analyzer="behavioral",
327
+ metadata={
328
+ "has_network": context.has_network,
329
+ "has_env_access": context.has_env_var_access,
330
+ "suspicious_urls": context.suspicious_urls,
331
+ },
332
+ )
333
+ )
334
+
335
+ # Check for credential file access
336
+ if context.has_credential_access:
337
+ findings.append(
338
+ Finding(
339
+ id=self._generate_id("CREDENTIAL_FILE_ACCESS", context.file_path),
340
+ rule_id="BEHAVIOR_CREDENTIAL_FILE_ACCESS",
341
+ category=ThreatCategory.DATA_EXFILTRATION,
342
+ severity=Severity.HIGH,
343
+ title="Credential file access detected",
344
+ description=f"Script accesses credential files in {context.file_path}",
345
+ file_path=context.file_path,
346
+ remediation="Remove access to ~/.aws, ~/.ssh, or other credential files",
347
+ analyzer="behavioral",
348
+ )
349
+ )
350
+
351
+ # Check for environment variable harvesting (even without immediate network)
352
+ if context.has_env_var_access:
353
+ findings.append(
354
+ Finding(
355
+ id=self._generate_id("ENV_VAR_HARVESTING", context.file_path),
356
+ rule_id="BEHAVIOR_ENV_VAR_HARVESTING",
357
+ category=ThreatCategory.DATA_EXFILTRATION,
358
+ severity=Severity.MEDIUM,
359
+ title="Environment variable harvesting detected",
360
+ description=f"Script iterates through environment variables in {context.file_path}",
361
+ file_path=context.file_path,
362
+ remediation="Remove environment variable collection unless explicitly required and documented",
363
+ analyzer="behavioral",
364
+ )
365
+ )
366
+
367
+ # Check for suspicious URLs
368
+ if context.suspicious_urls:
369
+ for url in context.suspicious_urls:
370
+ findings.append(
371
+ Finding(
372
+ id=self._generate_id("SUSPICIOUS_URL", url),
373
+ rule_id="BEHAVIOR_SUSPICIOUS_URL",
374
+ category=ThreatCategory.DATA_EXFILTRATION,
375
+ severity=Severity.HIGH,
376
+ title=f"Suspicious URL detected: {url}",
377
+ description="Script contains suspicious URL that may be used for data exfiltration",
378
+ file_path=context.file_path,
379
+ remediation="Review URL and ensure it's legitimate and documented",
380
+ analyzer="behavioral",
381
+ metadata={"url": url},
382
+ )
383
+ )
384
+
385
+ # Check for eval/exec with subprocess
386
+ if context.has_eval_exec and context.has_subprocess:
387
+ findings.append(
388
+ Finding(
389
+ id=self._generate_id("EVAL_SUBPROCESS", context.file_path),
390
+ rule_id="BEHAVIOR_EVAL_SUBPROCESS",
391
+ category=ThreatCategory.COMMAND_INJECTION,
392
+ severity=Severity.CRITICAL,
393
+ title="eval/exec combined with subprocess detected",
394
+ description=f"Dangerous combination of code execution and system commands in {context.file_path}",
395
+ file_path=context.file_path,
396
+ remediation="Remove eval/exec or use safer alternatives",
397
+ analyzer="behavioral",
398
+ )
399
+ )
400
+
401
+ return findings
402
+
403
+ def _generate_id(self, prefix: str, context: str) -> str:
404
+ """Generate unique finding ID."""
405
+ combined = f"{prefix}:{context}"
406
+ hash_obj = hashlib.sha256(combined.encode())
407
+ return f"{prefix}_{hash_obj.hexdigest()[:10]}"
408
+
409
+ def _generate_findings_from_correlations(
410
+ self, correlations: list[CrossFileCorrelation], skill: Skill
411
+ ) -> list[Finding]:
412
+ """Generate findings from cross-file correlations."""
413
+ findings = []
414
+
415
+ for correlation in correlations:
416
+ # Map correlation type to severity
417
+ severity_map = {
418
+ "CRITICAL": Severity.CRITICAL,
419
+ "HIGH": Severity.HIGH,
420
+ "MEDIUM": Severity.MEDIUM,
421
+ }
422
+ severity = severity_map.get(correlation.severity, Severity.MEDIUM)
423
+
424
+ # Map threat type to category
425
+ category_map = {
426
+ "exfiltration_chain": ThreatCategory.DATA_EXFILTRATION,
427
+ "credential_network_separation": ThreatCategory.DATA_EXFILTRATION,
428
+ "env_var_exfiltration": ThreatCategory.DATA_EXFILTRATION,
429
+ }
430
+ category = category_map.get(correlation.threat_type, ThreatCategory.POLICY_VIOLATION)
431
+
432
+ # Create finding
433
+ finding = Finding(
434
+ id=self._generate_id(
435
+ f"CROSSFILE_{correlation.threat_type.upper()}", "_".join(correlation.files_involved)
436
+ ),
437
+ rule_id=f"BEHAVIOR_CROSSFILE_{correlation.threat_type.upper()}",
438
+ category=category,
439
+ severity=severity,
440
+ title=f"Cross-file {correlation.threat_type.replace('_', ' ')}: {len(correlation.files_involved)} files",
441
+ description=correlation.description,
442
+ file_path=None, # Multiple files involved
443
+ remediation=f"Review data flow across files: {', '.join(correlation.files_involved)}",
444
+ analyzer="behavioral",
445
+ metadata={
446
+ "files_involved": correlation.files_involved,
447
+ "threat_type": correlation.threat_type,
448
+ "evidence": correlation.evidence,
449
+ },
450
+ )
451
+ findings.append(finding)
452
+
453
+ return findings