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,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
|