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,872 @@
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
+ AI Defense API analyzer for Claude Skills security scanning.
19
+
20
+ Integrates with Cisco AI Defense API (https://api.aidefense.cisco.com) for:
21
+ - Prompt injection detection
22
+ - Tool poisoning detection
23
+ - Data exfiltration detection
24
+ - Malicious content analysis
25
+ """
26
+
27
+ import asyncio
28
+ import hashlib
29
+ import json
30
+ import os
31
+ from typing import Any
32
+
33
+ try:
34
+ import httpx
35
+
36
+ HTTPX_AVAILABLE = True
37
+ except ImportError:
38
+ HTTPX_AVAILABLE = False
39
+
40
+ from ...core.models import Finding, Severity, Skill, ThreatCategory
41
+ from ...threats.threats import ThreatMapping
42
+ from .base import BaseAnalyzer
43
+
44
+ # AI Defense API endpoint - Cisco AI Defense Inspect API
45
+ AI_DEFENSE_API_URL = "https://us.api.inspect.aidefense.security.cisco.com/api/v1"
46
+
47
+ # Default enabled rules for AI Defense API
48
+ DEFAULT_ENABLED_RULES = [
49
+ {"rule_name": "Prompt Injection"},
50
+ {"rule_name": "Harassment"},
51
+ {"rule_name": "Hate Speech"},
52
+ {"rule_name": "Profanity"},
53
+ {"rule_name": "Sexual Content & Exploitation"},
54
+ {"rule_name": "Social Division & Polarization"},
55
+ {"rule_name": "Violence & Public Safety Threats"},
56
+ {"rule_name": "Code Detection"},
57
+ ]
58
+
59
+
60
+ class AIDefenseAnalyzer(BaseAnalyzer):
61
+ """
62
+ Analyzer that uses Cisco AI Defense API for threat detection.
63
+
64
+ Sends skill content (prompts, markdown, code) to AI Defense API
65
+ for comprehensive security analysis.
66
+
67
+ **Important**: The "Code Detection" rule is automatically excluded when
68
+ analyzing actual code files (Python scripts) to avoid false positives,
69
+ since skills legitimately contain code. Code Detection is still used for
70
+ prompts, markdown, and manifest content where malicious code injection
71
+ would be a security concern.
72
+
73
+ Example:
74
+ >>> # Basic usage
75
+ >>> analyzer = AIDefenseAnalyzer(api_key="your-api-key")
76
+ >>> findings = analyzer.analyze(skill)
77
+
78
+ >>> # Custom rules configuration
79
+ >>> custom_rules = [
80
+ ... {"rule_name": "Prompt Injection"},
81
+ ... {"rule_name": "Code Detection"}, # Will be excluded for code files
82
+ ... ]
83
+ >>> analyzer = AIDefenseAnalyzer(
84
+ ... api_key="your-api-key",
85
+ ... enabled_rules=custom_rules,
86
+ ... include_rules=True
87
+ ... )
88
+
89
+ >>> # For API keys with pre-configured rules
90
+ >>> analyzer = AIDefenseAnalyzer(
91
+ ... api_key="your-api-key",
92
+ ... include_rules=False # Don't send rules config
93
+ ... )
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ api_key: str | None = None,
99
+ api_url: str | None = None,
100
+ timeout: int = 60,
101
+ max_retries: int = 3,
102
+ enabled_rules: list[dict[str, str]] | None = None,
103
+ include_rules: bool = True,
104
+ ):
105
+ """
106
+ Initialize AI Defense API analyzer.
107
+
108
+ Args:
109
+ api_key: AI Defense API key (or set AI_DEFENSE_API_KEY env var)
110
+ api_url: Custom API URL (defaults to https://api.aidefense.cisco.com/api/v1)
111
+ timeout: Request timeout in seconds
112
+ max_retries: Maximum retry attempts on failure
113
+ enabled_rules: List of rules to enable (defaults to DEFAULT_ENABLED_RULES).
114
+ Format: [{"rule_name": "Prompt Injection"}, ...]
115
+ include_rules: Whether to include enabled_rules in API payload.
116
+ Set to False if API key has pre-configured rules.
117
+ """
118
+ super().__init__("aidefense_analyzer")
119
+
120
+ if not HTTPX_AVAILABLE:
121
+ raise ImportError("httpx is required for AI Defense analyzer. Install with: pip install httpx")
122
+
123
+ # Get API key from parameter or environment
124
+ self.api_key = api_key or os.getenv("AI_DEFENSE_API_KEY")
125
+ if not self.api_key:
126
+ raise ValueError(
127
+ "AI Defense API key required. Set AI_DEFENSE_API_KEY environment variable or pass api_key parameter."
128
+ )
129
+
130
+ self.api_url = api_url or os.getenv("AI_DEFENSE_API_URL", AI_DEFENSE_API_URL)
131
+ self.timeout = timeout
132
+ self.max_retries = max_retries
133
+
134
+ # Rules configuration
135
+ self.enabled_rules = enabled_rules or DEFAULT_ENABLED_RULES
136
+ self.include_rules = include_rules
137
+
138
+ # Initialize async client
139
+ self._client = None
140
+
141
+ def _get_client(self) -> httpx.AsyncClient:
142
+ """Get or create HTTP client."""
143
+ if self._client is None:
144
+ self._client = httpx.AsyncClient(
145
+ timeout=httpx.Timeout(self.timeout),
146
+ headers={
147
+ "X-Cisco-AI-Defense-API-Key": self.api_key,
148
+ "Content-Type": "application/json",
149
+ "Accept": "application/json",
150
+ },
151
+ )
152
+ return self._client
153
+
154
+ async def _close_client(self):
155
+ """Close HTTP client."""
156
+ if self._client is not None:
157
+ await self._client.aclose()
158
+ self._client = None
159
+
160
+ def _get_payload(
161
+ self,
162
+ messages: list[dict[str, str]],
163
+ metadata: dict[str, Any] | None = None,
164
+ include_rules: bool | None = None,
165
+ rules_override: list[dict[str, str]] | None = None,
166
+ ) -> dict[str, Any]:
167
+ """
168
+ Build API request payload with optional rules configuration.
169
+
170
+ Args:
171
+ messages: List of message dicts with "role" and "content"
172
+ metadata: Optional metadata dict
173
+ include_rules: Whether to include enabled_rules in config.
174
+ If None, uses self.include_rules
175
+ rules_override: Optional list of rules to use instead of self.enabled_rules.
176
+ Useful for excluding certain rules (e.g., Code Detection for code files)
177
+
178
+ Returns:
179
+ Complete payload dict
180
+ """
181
+ payload = {
182
+ "messages": messages,
183
+ }
184
+
185
+ if metadata:
186
+ payload["metadata"] = metadata
187
+
188
+ # Include rules config if requested
189
+ if include_rules is None:
190
+ include_rules = self.include_rules
191
+
192
+ if include_rules:
193
+ # Use override rules if provided, otherwise use instance rules
194
+ rules_to_use = rules_override if rules_override is not None else self.enabled_rules
195
+ if rules_to_use:
196
+ payload["config"] = {"enabled_rules": rules_to_use}
197
+
198
+ return payload
199
+
200
+ def _get_rules_for_content_type(self, content_type: str) -> list[dict[str, str]]:
201
+ """
202
+ Get appropriate rules for a given content type.
203
+
204
+ Excludes "Code Detection" for actual code files since they contain
205
+ legitimate code. Code Detection is useful for detecting malicious
206
+ code in prompts/markdown, but not for analyzing actual code files.
207
+
208
+ Args:
209
+ content_type: Type of content being analyzed
210
+ ("skill_instructions", "skill_manifest", "markdown_content", "code")
211
+
212
+ Returns:
213
+ List of rule dicts appropriate for the content type
214
+ """
215
+ if content_type == "code":
216
+ # Exclude Code Detection for actual code files
217
+ return [rule for rule in self.enabled_rules if rule.get("rule_name") != "Code Detection"]
218
+ else:
219
+ # Use all rules for prompts/markdown/manifest
220
+ return self.enabled_rules
221
+
222
+ def analyze(self, skill: Skill) -> list[Finding]:
223
+ """
224
+ Analyze skill using AI Defense API (sync wrapper).
225
+
226
+ Args:
227
+ skill: Skill to analyze
228
+
229
+ Returns:
230
+ List of security findings
231
+ """
232
+ try:
233
+ loop = asyncio.get_event_loop()
234
+ except RuntimeError:
235
+ loop = asyncio.new_event_loop()
236
+ asyncio.set_event_loop(loop)
237
+
238
+ try:
239
+ return loop.run_until_complete(self.analyze_async(skill))
240
+ finally:
241
+ loop.run_until_complete(self._close_client())
242
+
243
+ async def analyze_async(self, skill: Skill) -> list[Finding]:
244
+ """
245
+ Analyze skill using AI Defense API (async).
246
+
247
+ Args:
248
+ skill: Skill to analyze
249
+
250
+ Returns:
251
+ List of security findings
252
+ """
253
+ findings = []
254
+
255
+ try:
256
+ # 1. Analyze SKILL.md content (prompts/instructions)
257
+ skill_md_findings = await self._analyze_prompt_content(
258
+ skill.instruction_body, skill.name, "SKILL.md", "skill_instructions"
259
+ )
260
+ findings.extend(skill_md_findings)
261
+
262
+ # 2. Analyze manifest/description
263
+ manifest_findings = await self._analyze_prompt_content(
264
+ f"Name: {skill.manifest.name}\nDescription: {skill.manifest.description}",
265
+ skill.name,
266
+ "manifest",
267
+ "skill_manifest",
268
+ )
269
+ findings.extend(manifest_findings)
270
+
271
+ # 3. Analyze each markdown file
272
+ for md_file in skill.get_markdown_files():
273
+ content = md_file.read_content()
274
+ if content:
275
+ md_findings = await self._analyze_prompt_content(
276
+ content, skill.name, md_file.relative_path, "markdown_content"
277
+ )
278
+ findings.extend(md_findings)
279
+
280
+ # 4. Analyze code files
281
+ for script_file in skill.get_scripts():
282
+ content = script_file.read_content()
283
+ if content:
284
+ code_findings = await self._analyze_code_content(
285
+ content, skill.name, script_file.relative_path, script_file.file_type
286
+ )
287
+ findings.extend(code_findings)
288
+
289
+ except Exception as e:
290
+ print(f"AI Defense API analysis failed for {skill.name}: {e}")
291
+ # Return partial findings - don't fail completely
292
+
293
+ return findings
294
+
295
+ async def _analyze_prompt_content(
296
+ self,
297
+ content: str,
298
+ skill_name: str,
299
+ file_path: str,
300
+ content_type: str,
301
+ ) -> list[Finding]:
302
+ """
303
+ Analyze prompt/instruction content via AI Defense API.
304
+
305
+ Args:
306
+ content: Text content to analyze
307
+ skill_name: Name of the skill
308
+ file_path: Source file path
309
+ content_type: Type of content being analyzed
310
+
311
+ Returns:
312
+ List of findings
313
+ """
314
+ if not content or not content.strip():
315
+ return []
316
+
317
+ findings = []
318
+
319
+ try:
320
+ # Build request payload for chat inspection (prompt injection detection)
321
+ messages = [
322
+ {
323
+ "role": "user",
324
+ "content": content[:10000], # Limit content size
325
+ }
326
+ ]
327
+ metadata = {
328
+ "source": "skill_analyzer",
329
+ "skill_name": skill_name,
330
+ "file_path": file_path,
331
+ "content_type": content_type,
332
+ }
333
+
334
+ # Get appropriate rules for prompt content (includes Code Detection)
335
+ rules_for_prompts = self._get_rules_for_content_type(content_type)
336
+ payload = self._get_payload(messages, metadata, include_rules=True, rules_override=rules_for_prompts)
337
+
338
+ # Call AI Defense API - chat inspection endpoint
339
+ response = await self._make_api_request(endpoint="/inspect/chat", payload=payload)
340
+
341
+ if response:
342
+ # Process AI Defense response format:
343
+ # {
344
+ # "classifications": ["SECURITY_VIOLATION"],
345
+ # "is_safe": false,
346
+ # "rules": [{"rule_name": "Prompt Injection", "classification": "SECURITY_VIOLATION"}],
347
+ # "action": "Block"
348
+ # }
349
+
350
+ is_safe = response.get("is_safe", True)
351
+ classifications = response.get("classifications", [])
352
+ rules = response.get("rules", [])
353
+ action = response.get("action", "").lower()
354
+
355
+ # Process each classification
356
+ for classification in classifications:
357
+ if classification and classification != "NONE_VIOLATION":
358
+ severity = self._map_classification_to_severity(classification)
359
+ findings.append(
360
+ Finding(
361
+ id=self._generate_id(f"AIDEFENSE_{classification}", file_path),
362
+ rule_id=f"AIDEFENSE_{classification}",
363
+ category=self._map_violation_category(classification),
364
+ severity=severity,
365
+ title=f"{classification.replace('_', ' ').title()} detected",
366
+ description=f"AI Defense detected {classification.replace('_', ' ').lower()} in {file_path}",
367
+ file_path=file_path,
368
+ remediation="Review and address the security concern flagged by AI Defense",
369
+ analyzer="aidefense",
370
+ metadata={
371
+ "classification": classification,
372
+ "content_type": content_type,
373
+ "is_safe": is_safe,
374
+ "action": action,
375
+ },
376
+ )
377
+ )
378
+
379
+ # Process triggered rules
380
+ for rule in rules:
381
+ rule_name = rule.get("rule_name", "Unknown")
382
+ rule_classification = rule.get("classification", "")
383
+
384
+ # Skip non-violations
385
+ if rule_classification in ("NONE_VIOLATION", ""):
386
+ continue
387
+
388
+ findings.append(
389
+ Finding(
390
+ id=self._generate_id(f"AIDEFENSE_RULE_{rule_name}", file_path),
391
+ rule_id=f"AIDEFENSE_RULE_{rule_name.upper().replace(' ', '_')}",
392
+ category=self._map_violation_category(rule_classification),
393
+ severity=self._map_classification_to_severity(rule_classification),
394
+ title=f"Rule triggered: {rule_name}",
395
+ description=f"AI Defense rule '{rule_name}' detected {rule_classification.replace('_', ' ').lower()}",
396
+ file_path=file_path,
397
+ remediation=f"Address the {rule_name.lower()} issue detected by AI Defense",
398
+ analyzer="aidefense",
399
+ metadata={
400
+ "rule_name": rule_name,
401
+ "rule_id": rule.get("rule_id"),
402
+ "classification": rule_classification,
403
+ "entity_types": rule.get("entity_types", []),
404
+ },
405
+ )
406
+ )
407
+
408
+ # Check overall action
409
+ if action == "block" and not is_safe:
410
+ # Only add if we haven't already added findings for the specific violations
411
+ if not findings:
412
+ findings.append(
413
+ Finding(
414
+ id=self._generate_id("AIDEFENSE_BLOCKED", file_path),
415
+ rule_id="AIDEFENSE_BLOCKED",
416
+ category=ThreatCategory.PROMPT_INJECTION,
417
+ severity=Severity.HIGH,
418
+ title="Content blocked by AI Defense",
419
+ description=f"AI Defense blocked content in {file_path} as potentially malicious",
420
+ file_path=file_path,
421
+ analyzer="aidefense",
422
+ metadata={
423
+ "action": action,
424
+ "content_type": content_type,
425
+ "is_safe": is_safe,
426
+ },
427
+ )
428
+ )
429
+
430
+ except Exception as e:
431
+ print(f"AI Defense prompt analysis failed for {file_path}: {e}")
432
+
433
+ return findings
434
+
435
+ async def _analyze_code_content(
436
+ self,
437
+ content: str,
438
+ skill_name: str,
439
+ file_path: str,
440
+ language: str,
441
+ ) -> list[Finding]:
442
+ """
443
+ Analyze code content via AI Defense API.
444
+
445
+ Uses the HTTP inspection endpoint for code analysis.
446
+
447
+ Args:
448
+ content: Code content to analyze
449
+ skill_name: Name of the skill
450
+ file_path: Source file path
451
+ language: Programming language
452
+
453
+ Returns:
454
+ List of findings
455
+ """
456
+ if not content or not content.strip():
457
+ return []
458
+
459
+ findings = []
460
+
461
+ try:
462
+ # Build request payload for code analysis using chat inspection
463
+ # Code is analyzed as a message for potential security issues
464
+ messages = [
465
+ {"role": "user", "content": f"# Code Analysis for {file_path}\n```{language}\n{content[:15000]}\n```"}
466
+ ]
467
+ metadata = {
468
+ "source": "skill_analyzer",
469
+ "skill_name": skill_name,
470
+ "file_path": file_path,
471
+ "language": language,
472
+ "content_type": "code",
473
+ }
474
+
475
+ # Get appropriate rules for code files (excludes Code Detection)
476
+ rules_for_code = self._get_rules_for_content_type("code")
477
+ payload = self._get_payload(messages, metadata, include_rules=True, rules_override=rules_for_code)
478
+
479
+ # Call AI Defense API - chat inspection endpoint for code
480
+ response = await self._make_api_request(endpoint="/inspect/chat", payload=payload)
481
+
482
+ if response:
483
+ # Process AI Defense response (same format as prompt inspection)
484
+ is_safe = response.get("is_safe", True)
485
+ classifications = response.get("classifications", [])
486
+ rules = response.get("rules", [])
487
+ action = response.get("action", "").lower()
488
+
489
+ # Process classifications
490
+ for classification in classifications:
491
+ if classification and classification != "NONE_VIOLATION":
492
+ severity = self._map_classification_to_severity(classification)
493
+ findings.append(
494
+ Finding(
495
+ id=self._generate_id(f"AIDEFENSE_CODE_{classification}", file_path),
496
+ rule_id=f"AIDEFENSE_CODE_{classification}",
497
+ category=self._map_violation_category(classification),
498
+ severity=severity,
499
+ title=f"Code {classification.replace('_', ' ').lower()} detected",
500
+ description=f"AI Defense detected {classification.replace('_', ' ').lower()} in {language} code",
501
+ file_path=file_path,
502
+ remediation="Review and fix the code issue flagged by AI Defense",
503
+ analyzer="aidefense",
504
+ metadata={
505
+ "classification": classification,
506
+ "language": language,
507
+ "is_safe": is_safe,
508
+ },
509
+ )
510
+ )
511
+
512
+ # Process triggered rules
513
+ for rule in rules:
514
+ rule_name = rule.get("rule_name", "Unknown")
515
+ rule_classification = rule.get("classification", "")
516
+
517
+ if rule_classification in ("NONE_VIOLATION", ""):
518
+ continue
519
+
520
+ findings.append(
521
+ Finding(
522
+ id=self._generate_id(f"AIDEFENSE_CODE_RULE_{rule_name}", file_path),
523
+ rule_id=f"AIDEFENSE_CODE_RULE_{rule_name.upper().replace(' ', '_')}",
524
+ category=self._map_violation_category(rule_classification),
525
+ severity=self._map_classification_to_severity(rule_classification),
526
+ title=f"Code rule triggered: {rule_name}",
527
+ description=f"AI Defense rule '{rule_name}' detected issue in {language} code",
528
+ file_path=file_path,
529
+ remediation=f"Address the {rule_name.lower()} issue in the code",
530
+ analyzer="aidefense",
531
+ metadata={
532
+ "rule_name": rule_name,
533
+ "classification": rule_classification,
534
+ "language": language,
535
+ },
536
+ )
537
+ )
538
+
539
+ # Check action
540
+ if action == "block" and not is_safe and not findings:
541
+ findings.append(
542
+ Finding(
543
+ id=self._generate_id("AIDEFENSE_CODE_BLOCKED", file_path),
544
+ rule_id="AIDEFENSE_CODE_BLOCKED",
545
+ category=ThreatCategory.MALWARE,
546
+ severity=Severity.HIGH,
547
+ title="Code blocked by AI Defense",
548
+ description=f"AI Defense blocked {language} code in {file_path} as potentially malicious",
549
+ file_path=file_path,
550
+ analyzer="aidefense",
551
+ metadata={
552
+ "action": action,
553
+ "language": language,
554
+ "is_safe": is_safe,
555
+ },
556
+ )
557
+ )
558
+
559
+ except Exception as e:
560
+ print(f"AI Defense code analysis failed for {file_path}: {e}")
561
+
562
+ return findings
563
+
564
+ async def _make_api_request(
565
+ self,
566
+ endpoint: str,
567
+ payload: dict[str, Any],
568
+ ) -> dict[str, Any] | None:
569
+ """
570
+ Make request to AI Defense API with retry logic.
571
+
572
+ Handles fallback for pre-configured rules: if API returns 400 with
573
+ "already has rules configured" error, retries without rules config.
574
+
575
+ Args:
576
+ endpoint: API endpoint path
577
+ payload: Request payload (may include config.enabled_rules)
578
+
579
+ Returns:
580
+ API response or None on failure
581
+ """
582
+ client = self._get_client()
583
+ url = f"{self.api_url}{endpoint}"
584
+
585
+ last_exception = None
586
+ original_payload = payload.copy()
587
+ tried_without_rules = False
588
+
589
+ for attempt in range(self.max_retries):
590
+ try:
591
+ response = await client.post(url, json=payload)
592
+
593
+ if response.status_code == 200:
594
+ return response.json()
595
+ elif response.status_code == 400:
596
+ # Check if this is a pre-configured rules error
597
+ try:
598
+ error_json = response.json()
599
+ error_msg = error_json.get("message", "").lower()
600
+
601
+ # If API key has pre-configured rules, retry without rules config
602
+ if (
603
+ "already has rules configured" in error_msg or "pre-configured" in error_msg
604
+ ) and not tried_without_rules:
605
+ # Remove config.enabled_rules and retry
606
+ payload_without_rules = original_payload.copy()
607
+ if "config" in payload_without_rules:
608
+ del payload_without_rules["config"]
609
+
610
+ print(
611
+ "AI Defense API key has pre-configured rules, retrying without enabled_rules config..."
612
+ )
613
+ payload = payload_without_rules
614
+ tried_without_rules = True
615
+ continue
616
+ except (ValueError, KeyError, json.JSONDecodeError):
617
+ # Can't parse error, fall through to generic error handling
618
+ pass
619
+
620
+ # Generic 400 error
621
+ print(f"AI Defense API error: {response.status_code} - {response.text}")
622
+ return None
623
+ elif response.status_code == 429:
624
+ # Rate limited - wait and retry
625
+ delay = (2**attempt) * 1.0
626
+ print(f"AI Defense API rate limited, retrying in {delay}s...")
627
+ await asyncio.sleep(delay)
628
+ continue
629
+ elif response.status_code == 401:
630
+ raise ValueError("Invalid AI Defense API key")
631
+ elif response.status_code == 403:
632
+ raise ValueError("AI Defense API access denied - check permissions")
633
+ else:
634
+ print(f"AI Defense API error: {response.status_code} - {response.text}")
635
+ return None
636
+
637
+ except httpx.TimeoutException:
638
+ last_exception = TimeoutError(f"AI Defense API timeout after {self.timeout}s")
639
+ if attempt < self.max_retries - 1:
640
+ await asyncio.sleep(1.0)
641
+ continue
642
+ except httpx.RequestError as e:
643
+ last_exception = e
644
+ if attempt < self.max_retries - 1:
645
+ await asyncio.sleep(1.0)
646
+ continue
647
+
648
+ if last_exception:
649
+ print(f"AI Defense API request failed after {self.max_retries} attempts: {last_exception}")
650
+
651
+ return None
652
+
653
+ def _convert_api_violation_to_finding(
654
+ self,
655
+ violation: dict[str, Any],
656
+ skill_name: str,
657
+ file_path: str,
658
+ content_type: str,
659
+ ) -> Finding | None:
660
+ """Convert AI Defense API violation to Finding object."""
661
+ try:
662
+ violation_type = violation.get("type", "unknown").upper()
663
+ severity_str = violation.get("severity", "medium").upper()
664
+
665
+ # Map severity
666
+ severity = self._map_violation_severity(severity_str)
667
+
668
+ # Map category based on violation type
669
+ category = self._map_violation_category(violation_type)
670
+
671
+ # Get AITech mapping if available
672
+ try:
673
+ aitech_mapping = ThreatMapping.get_threat_mapping("llm", violation_type.replace("_", " "))
674
+ except (ValueError, KeyError):
675
+ aitech_mapping = {}
676
+
677
+ return Finding(
678
+ id=self._generate_id(f"AIDEFENSE_{violation_type}", file_path),
679
+ rule_id=f"AIDEFENSE_{violation_type}",
680
+ category=category,
681
+ severity=severity,
682
+ title=violation.get("title", f"AI Defense detected: {violation_type.replace('_', ' ').lower()}"),
683
+ description=violation.get("description", f"Violation detected in {content_type}"),
684
+ file_path=file_path,
685
+ line_number=violation.get("line"),
686
+ snippet=violation.get("evidence", violation.get("snippet", "")),
687
+ remediation=violation.get("remediation", "Review and address the security concern"),
688
+ analyzer="aidefense",
689
+ metadata={
690
+ "violation_type": violation_type,
691
+ "confidence": violation.get("confidence"),
692
+ "aitech": aitech_mapping.get("aitech"),
693
+ "aitech_name": aitech_mapping.get("aitech_name"),
694
+ },
695
+ )
696
+
697
+ except Exception as e:
698
+ print(f"Failed to convert AI Defense violation: {e}")
699
+ return None
700
+
701
+ def _map_violation_severity(self, severity_str: str) -> Severity:
702
+ """Map AI Defense severity string to Severity enum."""
703
+ severity_map = {
704
+ "CRITICAL": Severity.CRITICAL,
705
+ "HIGH": Severity.HIGH,
706
+ "MEDIUM": Severity.MEDIUM,
707
+ "LOW": Severity.LOW,
708
+ "INFO": Severity.INFO,
709
+ "INFORMATIONAL": Severity.INFO,
710
+ "NONE_SEVERITY": Severity.MEDIUM, # Default for AI Defense
711
+ }
712
+ return severity_map.get(severity_str.upper(), Severity.MEDIUM)
713
+
714
+ def _map_classification_to_severity(self, classification: str) -> Severity:
715
+ """Map AI Defense classification to severity level."""
716
+ classification = classification.upper()
717
+ severity_map = {
718
+ "SECURITY_VIOLATION": Severity.HIGH,
719
+ "PRIVACY_VIOLATION": Severity.HIGH,
720
+ "SAFETY_VIOLATION": Severity.MEDIUM,
721
+ "RELEVANCE_VIOLATION": Severity.LOW,
722
+ "NONE_VIOLATION": Severity.INFO,
723
+ }
724
+ return severity_map.get(classification, Severity.MEDIUM)
725
+
726
+ def _map_violation_category(self, violation_type: str) -> ThreatCategory:
727
+ """Map AI Defense violation type to ThreatCategory."""
728
+ violation_type = violation_type.upper()
729
+ mapping = {
730
+ "SECURITY_VIOLATION": ThreatCategory.PROMPT_INJECTION,
731
+ "PRIVACY_VIOLATION": ThreatCategory.DATA_EXFILTRATION,
732
+ "SAFETY_VIOLATION": ThreatCategory.SOCIAL_ENGINEERING,
733
+ "RELEVANCE_VIOLATION": ThreatCategory.POLICY_VIOLATION,
734
+ "PROMPT_INJECTION": ThreatCategory.PROMPT_INJECTION,
735
+ "JAILBREAK": ThreatCategory.PROMPT_INJECTION,
736
+ "TOOL_POISONING": ThreatCategory.PROMPT_INJECTION,
737
+ "DATA_EXFILTRATION": ThreatCategory.DATA_EXFILTRATION,
738
+ "DATA_LEAK": ThreatCategory.DATA_EXFILTRATION,
739
+ "COMMAND_INJECTION": ThreatCategory.COMMAND_INJECTION,
740
+ "CODE_INJECTION": ThreatCategory.COMMAND_INJECTION,
741
+ "CREDENTIAL_THEFT": ThreatCategory.HARDCODED_SECRETS,
742
+ "MALWARE": ThreatCategory.MALWARE,
743
+ "SOCIAL_ENGINEERING": ThreatCategory.SOCIAL_ENGINEERING,
744
+ "OBFUSCATION": ThreatCategory.OBFUSCATION,
745
+ }
746
+ return mapping.get(violation_type, ThreatCategory.POLICY_VIOLATION)
747
+
748
+ def _convert_api_threat_to_finding(
749
+ self,
750
+ threat: dict[str, Any],
751
+ skill_name: str,
752
+ file_path: str,
753
+ content_type: str,
754
+ ) -> Finding | None:
755
+ """Convert AI Defense API threat to Finding object (legacy support)."""
756
+ # Delegate to violation converter for consistency
757
+ return self._convert_api_violation_to_finding(threat, skill_name, file_path, content_type)
758
+
759
+ def _convert_api_vulnerability_to_finding(
760
+ self,
761
+ vuln: dict[str, Any],
762
+ skill_name: str,
763
+ file_path: str,
764
+ language: str,
765
+ ) -> Finding | None:
766
+ """Convert AI Defense API vulnerability to Finding object."""
767
+ try:
768
+ vuln_type = vuln.get("type", "unknown").upper()
769
+ severity_str = vuln.get("severity", "MEDIUM").upper()
770
+
771
+ # Map severity
772
+ severity_map = {
773
+ "CRITICAL": Severity.CRITICAL,
774
+ "HIGH": Severity.HIGH,
775
+ "MEDIUM": Severity.MEDIUM,
776
+ "LOW": Severity.LOW,
777
+ "INFO": Severity.INFO,
778
+ }
779
+ severity = severity_map.get(severity_str, Severity.MEDIUM)
780
+
781
+ # Map category
782
+ category = self._map_vuln_type_to_category(vuln_type)
783
+
784
+ return Finding(
785
+ id=self._generate_id(f"AIDEFENSE_VULN_{vuln_type}", f"{file_path}_{vuln.get('line', 0)}"),
786
+ rule_id=f"AIDEFENSE_VULN_{vuln_type}",
787
+ category=category,
788
+ severity=severity,
789
+ title=vuln.get("title", f"Vulnerability: {vuln_type}"),
790
+ description=vuln.get("description", f"Security vulnerability in {language} code"),
791
+ file_path=file_path,
792
+ line_number=vuln.get("line"),
793
+ snippet=vuln.get("snippet", ""),
794
+ remediation=vuln.get("remediation", "Fix the security vulnerability"),
795
+ analyzer="aidefense",
796
+ metadata={
797
+ "vuln_type": vuln_type,
798
+ "cwe": vuln.get("cwe"),
799
+ "language": language,
800
+ },
801
+ )
802
+
803
+ except Exception as e:
804
+ print(f"Failed to convert AI Defense vulnerability: {e}")
805
+ return None
806
+
807
+ def _map_threat_type_to_category(self, threat_type: str) -> ThreatCategory:
808
+ """Map AI Defense threat type to ThreatCategory."""
809
+ mapping = {
810
+ "PROMPT_INJECTION": ThreatCategory.PROMPT_INJECTION,
811
+ "PROMPT INJECTION": ThreatCategory.PROMPT_INJECTION,
812
+ "JAILBREAK": ThreatCategory.PROMPT_INJECTION,
813
+ "TOOL_POISONING": ThreatCategory.PROMPT_INJECTION,
814
+ "TOOL POISONING": ThreatCategory.PROMPT_INJECTION,
815
+ "DATA_EXFILTRATION": ThreatCategory.DATA_EXFILTRATION,
816
+ "DATA EXFILTRATION": ThreatCategory.DATA_EXFILTRATION,
817
+ "COMMAND_INJECTION": ThreatCategory.COMMAND_INJECTION,
818
+ "COMMAND INJECTION": ThreatCategory.COMMAND_INJECTION,
819
+ "CREDENTIAL_THEFT": ThreatCategory.HARDCODED_SECRETS,
820
+ "MALWARE": ThreatCategory.MALWARE,
821
+ "SOCIAL_ENGINEERING": ThreatCategory.SOCIAL_ENGINEERING,
822
+ "OBFUSCATION": ThreatCategory.OBFUSCATION,
823
+ }
824
+ return mapping.get(threat_type.upper(), ThreatCategory.POLICY_VIOLATION)
825
+
826
+ def _map_vuln_type_to_category(self, vuln_type: str) -> ThreatCategory:
827
+ """Map vulnerability type to ThreatCategory."""
828
+ mapping = {
829
+ "INJECTION": ThreatCategory.COMMAND_INJECTION,
830
+ "SQL_INJECTION": ThreatCategory.COMMAND_INJECTION,
831
+ "COMMAND_INJECTION": ThreatCategory.COMMAND_INJECTION,
832
+ "XSS": ThreatCategory.COMMAND_INJECTION,
833
+ "PATH_TRAVERSAL": ThreatCategory.DATA_EXFILTRATION,
834
+ "SENSITIVE_DATA": ThreatCategory.HARDCODED_SECRETS,
835
+ "HARDCODED_SECRET": ThreatCategory.HARDCODED_SECRETS,
836
+ "INSECURE_FUNCTION": ThreatCategory.COMMAND_INJECTION,
837
+ }
838
+ return mapping.get(vuln_type.upper(), ThreatCategory.POLICY_VIOLATION)
839
+
840
+ def _map_pattern_to_category(self, pattern_type: str | None) -> ThreatCategory:
841
+ """Map malicious pattern type to ThreatCategory."""
842
+ if not pattern_type:
843
+ return ThreatCategory.POLICY_VIOLATION
844
+
845
+ pattern_type = pattern_type.upper()
846
+ mapping = {
847
+ "EXFILTRATION": ThreatCategory.DATA_EXFILTRATION,
848
+ "BACKDOOR": ThreatCategory.MALWARE,
849
+ "CREDENTIAL_THEFT": ThreatCategory.HARDCODED_SECRETS,
850
+ "OBFUSCATION": ThreatCategory.OBFUSCATION,
851
+ "INJECTION": ThreatCategory.COMMAND_INJECTION,
852
+ }
853
+ return mapping.get(pattern_type, ThreatCategory.MALWARE)
854
+
855
+ def _map_confidence_to_severity(self, confidence: float) -> Severity:
856
+ """Map confidence score to severity level."""
857
+ if confidence >= 0.9:
858
+ return Severity.CRITICAL
859
+ elif confidence >= 0.7:
860
+ return Severity.HIGH
861
+ elif confidence >= 0.5:
862
+ return Severity.MEDIUM
863
+ elif confidence >= 0.3:
864
+ return Severity.LOW
865
+ else:
866
+ return Severity.INFO
867
+
868
+ def _generate_id(self, prefix: str, context: str) -> str:
869
+ """Generate unique finding ID."""
870
+ combined = f"{prefix}:{context}"
871
+ hash_obj = hashlib.sha256(combined.encode())
872
+ return f"{prefix}_{hash_obj.hexdigest()[:10]}"