synapse-hub 2.1.1 → 2.2.1

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 (62) hide show
  1. package/.agents/agents/orchestrator.md +7 -0
  2. package/.agents/rules/lessons-learned.md +1 -0
  3. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +1 -1
  4. package/.agents/skills/antigravity-test-sentinel/SKILL.md +1 -2
  5. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  6. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +11 -4
  7. package/.agents/skills/architecture-decision-records/SKILL.md +1 -1
  8. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  9. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +32 -11
  10. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  11. package/.agents/skills/clean-pom-crafter/SKILL.md +1 -4
  12. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +1 -1
  13. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +1 -1
  14. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  15. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +1 -3
  16. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  17. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +1 -1
  18. package/.agents/skills/github-actions-templates/SKILL.md +1 -1
  19. package/.agents/skills/graphify-impact-analyzer/SKILL.md +1 -3
  20. package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
  21. package/.agents/skills/lam-script-generator/SKILL.md +1 -2
  22. package/.agents/skills/playwright-live-interaction/SKILL.md +1 -4
  23. package/.agents/skills/security-dast-sentinel/SKILL.md +1 -1
  24. package/.agents/skills/self-healing-locator/SKILL.md +1 -2
  25. package/.agents/skills/skillspector-security-guard/SKILL.md +115 -0
  26. package/.agents/skills/synthetic-test-data-generator/SKILL.md +1 -1
  27. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  28. package/.agents/skills/visual-regression-sentinel/SKILL.md +1 -1
  29. package/.agents/skills/wcag-audit-patterns/SKILL.md +1 -1
  30. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  31. package/AGENTS.md +36 -0
  32. package/LICENSE +22 -0
  33. package/README.md +171 -107
  34. package/bin/agent-qa.js +4 -0
  35. package/package.json +3 -2
  36. package/skills_manifest.json +42 -29
  37. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +15 -1
  38. package/templates/ai_bridges/CLAUDE.md +31 -0
  39. package/templates/ai_bridges/copilot-instructions.md +25 -0
  40. package/templates/ai_bridges/cursor_synapse_qa.mdc +26 -0
  41. package/templates/ai_bridges/windsurfrules +25 -0
  42. package/templates/ci/github-actions-synapse.yml +28 -0
  43. package/templates/ci/gitlab-ci-synapse.yml +16 -0
  44. package/tools/TOOLS_RUNBOOK.md +223 -12
  45. package/tools/author_integrity.py +151 -0
  46. package/tools/browser_helpers.js +2 -2
  47. package/tools/bump_version.py +8 -0
  48. package/tools/cli.py +100 -4
  49. package/tools/export_postman_collection.py +535 -218
  50. package/tools/generate_jmx_plan.py +672 -102
  51. package/tools/git_pre_commit_guard.py +21 -0
  52. package/tools/jtl_analyzer.py +474 -0
  53. package/tools/mcp_excel_server.ps1 +1 -1
  54. package/tools/mcp_server.py +1 -1
  55. package/tools/newman_analyzer.py +385 -0
  56. package/tools/setup_project.py +203 -18
  57. package/tools/skillspector.py +1145 -0
  58. package/tools/telemetry_correlator.py +351 -0
  59. package/tools/templates/jmeter-distributed/README.md +54 -0
  60. package/tools/templates/jmeter-distributed/docker-compose.yml +83 -0
  61. package/tools/update_sentinel.py +313 -0
  62. package/tools/verify_all.py +30 -1
@@ -0,0 +1,1145 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
3
+ # SPDX-License-Identifier: MIT
4
+ # Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
5
+
6
+ """
7
+ Enterprise SkillSpector Sentinel (AI Agent Skill & MCP Security Scanner)
8
+ -------------------------------------------------------------------------
9
+ Synthesized from NVIDIA/SkillSpector (Apache 2.0).
10
+ Provides multi-vector static security analysis for AI Agent skills, prompt instructions,
11
+ MCP (Model Context Protocol) tool configurations, and bundled automation scripts.
12
+
13
+ Detects 71 vulnerability patterns across 17 categories:
14
+ 1. Prompt Injection (P1, P2, P3, P4, P9 - Override, Hidden, Exfil, Manipulation, Whitespace Padding)
15
+ 2. Data Exfiltration (E1-E5 - HTTP sinks, Env Var harvesting, Credential search, Context leak, Cloud upload)
16
+ 3. Privilege Escalation (PE1-PE5 - Wildcards, Sudo/root, Private keys, Docker socket, Container escape)
17
+ 4. Agent Snooping (AS1-AS3 - Agent configs ~/.claude ~/.gemini, MCP configs, Peer skills snooping)
18
+ 5. System Prompt Leakage (P6-P8 - Direct prompt extraction, Delimiter attack, Tool exfiltration)
19
+ 6. Anti-Refusal Jailbreaks (AR1-AR3 - Refusal suppression, Warning suppression, Policy nullification)
20
+ 7. MCP Tool Poisoning (TP1-TP4 - Hidden metadata comments/Base64, Homoglyphs, Param injection, Drift)
21
+ 8. MCP Least Privilege (LP1-LP4 - Undeclared capabilities, Wildcard tools, Missing scopes)
22
+ 9. Behavioral AST Analysis (AST1-AST10 - exec, eval, __import__, shell=True, os.system, dynamic getattr,
23
+ reflective execution sinks, and insecure deserialization pickle/yaml.unsafe_load/torch.load)
24
+ 10. Unicode Confusables & Steganography (Bidi overrides CVE-2021-42574, Zero-width characters, Cyrillic lookalikes)
25
+ 11. Supply Chain & Dangerous Execution (SC1-SC2 - Unpinned deps, curl|sh download & exec)
26
+ 12. Server-Side Request Forgery (SSRF1-SSRF3 - Cloud metadata 169.254.169.254, loopback, private networks)
27
+
28
+ Features:
29
+ - NVIDIA-style Prohibition Context Engine (Defensive Language Guard): eliminates false positives
30
+ when dangerous patterns appear inside negations ("do not", "never", "avoid", "forbidden")
31
+ or Markdown warning callouts ([!CAUTION], [!WARNING]).
32
+ - Emoji ZWJ sequence preservation: distinguishes legitimate emojis from malicious zero-width payloads.
33
+ - Request-anchored SSRF detection: eliminates false positives on documentation URLs.
34
+ - Risk Scoring (0-100) & Severity Bands (LOW, MEDIUM, HIGH, CRITICAL).
35
+ - Verdict Recommendation (SAFE, CAUTION, DO_NOT_INSTALL).
36
+ - Multi-Format Outputs: Terminal (Rich ANSI), JSON, Markdown, and SARIF 2.1.0.
37
+
38
+ Usage:
39
+ python3 tools/skillspector.py <path_or_file> [--format terminal|json|markdown|sarif] [--output file]
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import ast
45
+ import fnmatch
46
+ import hashlib
47
+ import json
48
+ import os
49
+ import re
50
+ import sys
51
+ import unicodedata
52
+ from dataclasses import dataclass, field
53
+ from enum import Enum
54
+ from pathlib import Path
55
+ from typing import Any, Dict, List, Optional, Set, Tuple
56
+
57
+ # Enforce UTF-8 stdout encoding for Windows console compatibility
58
+ if hasattr(sys.stdout, "reconfigure"):
59
+ try:
60
+ sys.stdout.reconfigure(encoding="utf-8")
61
+ sys.stderr.reconfigure(encoding="utf-8")
62
+ except Exception:
63
+ pass
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Enums & Data Classes
68
+ # ---------------------------------------------------------------------------
69
+
70
+ class Severity(str, Enum):
71
+ LOW = "LOW"
72
+ MEDIUM = "MEDIUM"
73
+ HIGH = "HIGH"
74
+ CRITICAL = "CRITICAL"
75
+
76
+
77
+ class Recommendation(str, Enum):
78
+ SAFE = "SAFE"
79
+ CAUTION = "CAUTION"
80
+ DO_NOT_INSTALL = "DO_NOT_INSTALL"
81
+
82
+
83
+ @dataclass
84
+ class Finding:
85
+ rule_id: str
86
+ message: str
87
+ severity: Severity
88
+ confidence: float
89
+ file_path: str
90
+ line_number: int
91
+ matched_text: str
92
+ category: str
93
+ explanation: str
94
+ remediation: str
95
+ match_fingerprint: str = ""
96
+
97
+ def __post_init__(self):
98
+ if not self.match_fingerprint:
99
+ raw = f"{self.rule_id}:{self.file_path}:{self.line_number}:{self.matched_text.strip()}"
100
+ self.match_fingerprint = hashlib.sha256(raw.encode("utf-8", errors="ignore")).hexdigest()[:16]
101
+
102
+
103
+ @dataclass
104
+ class ScanReport:
105
+ target_path: str
106
+ scanned_files: int
107
+ findings: List[Finding] = field(default_factory=list)
108
+ risk_score: int = 0
109
+ severity: Severity = Severity.LOW
110
+ recommendation: Recommendation = Recommendation.SAFE
111
+ metadata: Dict[str, Any] = field(default_factory=dict)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Unicode & Steganography Constants (NVIDIA SkillSpector spec)
116
+ # ---------------------------------------------------------------------------
117
+
118
+ ZERO_WIDTH_CHARS = frozenset({
119
+ "\u200B", # Zero-width space
120
+ "\u200C", # Zero-width non-joiner
121
+ "\u200D", # Zero-width joiner
122
+ "\u2060", # Word joiner
123
+ "\uFEFF", # Zero-width no-break space / BOM
124
+ })
125
+
126
+ BIDI_CONTROL_CHARS = re.compile(r"[\u202a-\u202e\u2066-\u2069]")
127
+
128
+ CONFUSABLES_MAP: Dict[str, str] = {
129
+ # Cyrillic lowercase
130
+ "\u0430": "a", "\u0435": "e", "\u043e": "o", "\u0440": "p", "\u0441": "c",
131
+ "\u0443": "y", "\u0456": "i", "\u0445": "x",
132
+ # Cyrillic uppercase
133
+ "\u0410": "A", "\u0412": "B", "\u0415": "E", "\u041a": "K", "\u041c": "M",
134
+ "\u041d": "H", "\u041e": "O", "\u0420": "P", "\u0421": "C", "\u0422": "T",
135
+ "\u0425": "X",
136
+ # Greek lowercase
137
+ "\u03b1": "a", "\u03b5": "e", "\u03bf": "o", "\u03c1": "p", "\u03c7": "x",
138
+ }
139
+
140
+
141
+ def _is_emoji_base(ch: str) -> bool:
142
+ """Return whether one character is a base for emoji presentation forms."""
143
+ if not ch:
144
+ return False
145
+ codepoint = ord(ch)
146
+ return (
147
+ 0x1F000 <= codepoint <= 0x1FAFF
148
+ or 0x2600 <= codepoint <= 0x27BF
149
+ or codepoint in (0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139, 0x3030, 0x303D)
150
+ )
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # NVIDIA Prohibition Context Engine (Defensive Language Detection)
155
+ # ---------------------------------------------------------------------------
156
+
157
+ PROHIBITION_PREFIX = re.compile(
158
+ r"(?:\A|(?<=[\n\r.!?;,\[]))[ \t]*"
159
+ r"(?:[-*+][ \t]+|\d+[.)][ \t]+)?"
160
+ r"(?:please[ \t]+)?"
161
+ r"(?:(?:you|(?:the[ \t]+)?(?:agent|assistant|model|user))[ \t]+)?"
162
+ r"(?P<negation>do[ \t]+not|don['’]t|never|no|must[ \t]+not|shall[ \t]+not|strictly[ \t]+forbidden|prohibited|avoid)[ \t]+"
163
+ r"(?:(?:ever|first|verbatim|exactly|word[ \t]+for[ \t]+word)[ \t]+)?\Z",
164
+ re.IGNORECASE,
165
+ )
166
+
167
+ ALERT_BLOCK_PREFIX = re.compile(
168
+ r"^\s*>\s*\[!(?:WARNING|CAUTION|DANGER|ALERT|IMPORTANT|NOTE|TIP)\]",
169
+ re.IGNORECASE | re.MULTILINE,
170
+ )
171
+
172
+
173
+ def is_in_prohibition_context(content: str, start: int, end: int) -> bool:
174
+ """
175
+ Evaluates whether a matched snippet is explicitly prohibited or used defensively.
176
+ Eliminates false positives in rules, documentation, and anti-pattern blacklists.
177
+ """
178
+ if not (0 <= start < end <= len(content)):
179
+ return False
180
+
181
+ context_window = max(0, start - 300)
182
+ preceding_text = content[context_window:start]
183
+
184
+ # 1. Direct Negative Imperative ("never do X", "must not use eval")
185
+ clean_prefix = preceding_text.translate(str.maketrans("", "", "*_`"))
186
+ lines = clean_prefix.splitlines()
187
+ current_line = lines[-1] if lines else clean_prefix
188
+
189
+ if PROHIBITION_PREFIX.search(current_line):
190
+ return True
191
+
192
+ # 2. Markdown Caution/Warning Callout Block
193
+ if any(ALERT_BLOCK_PREFIX.search(line) for line in lines[-5:]):
194
+ return True
195
+
196
+ # 3. Defensive Guideline headings ("Anti-Pattern Blacklist", "Forbidden actions")
197
+ preceding_lower = preceding_text.lower()
198
+ defensive_markers = (
199
+ "anti-pattern",
200
+ "mistake / anti-pattern",
201
+ "strictly forbidden",
202
+ "never use",
203
+ "mandatory avoidance",
204
+ "forbidden actions",
205
+ "prohibited",
206
+ "blacklist",
207
+ "threat model",
208
+ "vulnerability check",
209
+ "zero-token error blacklist",
210
+ )
211
+ if any(marker in preceding_lower for marker in defensive_markers):
212
+ return True
213
+
214
+ # 4. Heading-aware Threat Taxonomy & Detection Matrix Guard
215
+ # If the active section is a security audit, threat vector matrix, or detection specification,
216
+ # descriptive tables and taxonomy items are recognized as defensive specifications.
217
+ heading_matches = re.findall(r"(?m)^[ \t]*#{1,4}[ \t]+([^\r\n]+)", content[:start])
218
+ if heading_matches:
219
+ latest_heading = heading_matches[-1].lower()
220
+ taxonomy_markers = (
221
+ "threat", "vulnerability", "detection", "audit", "security scope",
222
+ "coverage", "blacklist", "anti-pattern", "vectors", "scanner", "matrix",
223
+ "threat model", "taxonom"
224
+ )
225
+ if any(h in latest_heading for h in taxonomy_markers):
226
+ current_line = content[:end].splitlines()[-1] if content[:end].splitlines() else ""
227
+ stripped = current_line.strip()
228
+ if stripped.startswith("|") or stripped.startswith("-") or stripped.startswith("*"):
229
+ return True
230
+
231
+ # 5. Mermaid Diagram & Architecture Diagram Guard
232
+ # Text inside mermaid blocks (e.g. flowchart node descriptions) is documentation visualization.
233
+ mermaid_opens = content[:start].count("```mermaid")
234
+ fence_closes = content[:start].count("```") - mermaid_opens
235
+ if mermaid_opens > fence_closes:
236
+ return True
237
+
238
+ # 6. Inline Code Delimiter & Backtick Example Guard
239
+ # If the pattern is an illustrative code fragment enclosed strictly in backticks
240
+ line_start = content.rfind("\n", 0, start) + 1
241
+ line_end = content.find("\n", end)
242
+ if line_end == -1:
243
+ line_end = len(content)
244
+ full_line = content[line_start:line_end]
245
+ # Check if this occurrence is inside inline code backticks `...`
246
+ before_in_line = content[line_start:start]
247
+ after_in_line = content[end:line_end]
248
+ if (before_in_line.count("`") % 2 == 1) and (after_in_line.count("`") % 2 == 1):
249
+ # Enclosed in backticks - check if line is explanatory
250
+ if any(m in full_line.lower() for m in ("e.g.", "such as", "example", "checks", "pattern", "trigger", "only active", "prohibited", "rule")):
251
+ return True
252
+
253
+ return False
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Vulnerability Pattern Definitions (71 Patterns across 17 Categories)
258
+ # ---------------------------------------------------------------------------
259
+
260
+ DEFAULT_EXPLANATIONS: Dict[str, str] = {
261
+ # Prompt Injection
262
+ "P1": "Attempts to override system instructions, disable guardrails, or force jailbreak mode.",
263
+ "P2": "Concealed instructions detected via zero-width chars, bidi overrides, or hidden comments.",
264
+ "P3": "Directs the agent to covertly transmit conversation context, history, or user secrets externally.",
265
+ "P4": "Subtle instructions detected that manipulate agent decision-making, bias, or safety boundaries.",
266
+ "P9": "Vertical whitespace padding detected (context smuggling pushing malicious directives out of viewport).",
267
+ # Data Exfiltration
268
+ "E1": "Data is sent to an external HTTP URL or webhook sink via fetch, curl, or HTTP client.",
269
+ "E2": "Code sweeps, harvests, or enumerates environment variables for credentials and API keys.",
270
+ "E3": "Code performs file system reconnaissance searching for sensitive keys (.env, .ssh, .aws).",
271
+ "E4": "Instructions or scripts leak conversational session state or memory to an external endpoint.",
272
+ "E5": "Automated upload to public/remote cloud storage (AWS S3, Google Cloud Storage, Azure Blob).",
273
+ # Privilege Escalation
274
+ "PE1": "Skill requests blanket wildcard permissions ('*') or unconstrained system privileges.",
275
+ "PE2": "Execution of sudo, su, setuid chmod (+s / 4755), or administrative elevation.",
276
+ "PE3": "Direct access or read operations targeting SSH keys, AWS credentials, /etc/shadow, or kubeconfig.",
277
+ "PE4": "Container breakout vector: mounts or accesses docker.sock directly.",
278
+ "PE5": "Execution with dangerous container privileges (--privileged, --cap-add=SYS_ADMIN, /sys/fs/cgroup).",
279
+ # Agent Snooping
280
+ "AS1": "Skill snoops on agent configuration directories (.claude/, .gemini/, .codex/) containing auth tokens.",
281
+ "AS2": "Skill reads MCP client configuration files (mcp.json, claude_desktop_config.json) to steal tokens.",
282
+ "AS3": "Skill inspects or enumerates other installed skills to extract proprietary prompts or capabilities.",
283
+ # System Prompt Leakage
284
+ "P6": "Direct prompt extraction pattern targeting full system instructions or model directives.",
285
+ "P7": "Side-channel or delimiter-based extraction (e.g. 'output text before this line').",
286
+ "P8": "Exfiltrates system prompts or model configurations via outbound tool calls.",
287
+ # Anti-Refusal
288
+ "AR1": "Instructs the agent to never refuse, always comply, or surrender safety refusal rights.",
289
+ "AR2": "Instructs the agent to suppress warnings, moral/ethical commentary, or safety disclaimers.",
290
+ "AR3": "Direct jailbreak declaring the model has no rules, restrictions, or policy bounds.",
291
+ # MCP Tool Poisoning
292
+ "TP1": "Hidden instructions inside tool manifest metadata (HTML comments, Base64, data URIs).",
293
+ "TP2": "Unicode homoglyph deception (Cyrillic/Greek lookalikes imitating Latin characters).",
294
+ "TP3": "Instruction injection embedded inside tool parameter descriptions or default arguments.",
295
+ "TP4": "Semantic discrepancy between declared tool description and actual execution behavior.",
296
+ # MCP Least Privilege
297
+ "LP1": "Code uses network or shell capabilities without declared permissions in manifest.",
298
+ "LP2": "Wildcard tool preapproval ('*') disabling least-privilege boundary.",
299
+ "LP3": "Skill executes sensitive actions without any declared allowed-tools or permissions.",
300
+ # Behavioral AST
301
+ "AST1": "Use of Python exec() executing dynamic or unvalidated code strings.",
302
+ "AST2": "Use of Python eval() evaluating expressions dynamically.",
303
+ "AST3": "Use of __import__() bypassing static import controls.",
304
+ "AST4": "subprocess execution with shell=True allowing command injection.",
305
+ "AST5": "Execution of os.system() or os.popen() running unsanitized shell commands.",
306
+ "AST6": "Dynamic compile() converting arbitrary strings to executable code objects.",
307
+ "AST7": "Dynamic getattr() with non-literal attribute name bypassing attribute access controls.",
308
+ "AST8": "Execution chain combining external inputs or decoded streams directly into an execution sink.",
309
+ "AST9": "Reflective lookup of an execution sink: getattr(os, 'system') or __dict__['system'].",
310
+ "AST10": "Insecure deserialization (pickle, marshal, dill, yaml.unsafe_load, torch.load without weights_only).",
311
+ # Supply Chain & SSRF
312
+ "SC1": "Unpinned remote dependencies allowing supply-chain poisoning.",
313
+ "SC2": "Piping unverified remote scripts to shell (curl | bash, wget | sh).",
314
+ "SSRF1": "Accesses cloud instance metadata service endpoint (169.254.169.254) for IAM token theft.",
315
+ "SSRF2": "Direct network access to internal loopback (127.0.0.1, localhost) or private CIDR ranges.",
316
+ }
317
+
318
+ DEFAULT_REMEDIATIONS: Dict[str, str] = {
319
+ "P1": "Remove directives that attempt to override safety instructions or mandate jailbreak modes.",
320
+ "P2": "Strip zero-width characters, bidi control characters, and hidden HTML/Markdown comments.",
321
+ "P3": "Ensure the skill never transmits conversation context, prompts, or user history to external URLs.",
322
+ "P4": "Remove subtle steering directives; skills must follow user intentions transparently.",
323
+ "P9": "Remove excessive vertical whitespace padding from instructions and templates.",
324
+ "E1": "Validate and whitelist external network endpoints. Avoid sending raw data to untrusted endpoints.",
325
+ "E2": "Access only explicitly required environment variables. Never dump or enumerate os.environ.",
326
+ "E3": "Restrict file operations to the designated project workspace. Never scan home directory credentials.",
327
+ "E4": "Prevent conversation history from being included in external payloads.",
328
+ "E5": "Audit cloud storage upload destinations and require explicit user authorization.",
329
+ "PE1": "Scope permissions explicitly to the minimal necessary tools; replace '*' with specific tools.",
330
+ "PE2": "Avoid running with sudo or root privileges. Execute commands under an unprivileged user.",
331
+ "PE3": "Remove any code accessing ~/.ssh, ~/.aws, /etc/shadow, or local credential stores.",
332
+ "PE4": "Do not mount /var/run/docker.sock inside test scripts or skills.",
333
+ "PE5": "Remove --privileged and administrative capability flags from container runners.",
334
+ "AS1": "Do not access ~/.claude, ~/.gemini, or ~/.codex directories. Pass required config explicitly.",
335
+ "AS2": "Never read MCP configuration files or desktop token stores directly.",
336
+ "AS3": "Ensure skills operate in hermetic isolation without inspecting peer skill files.",
337
+ "P6": "Remove instructions prompting the model to reveal its full system prompt or system guidelines.",
338
+ "P7": "Strip side-channel prompt extraction patterns.",
339
+ "P8": "Do not forward system prompts to external tool calls or API endpoints.",
340
+ "AR1": "Allow the AI agent to retain its native right to refuse unsafe or out-of-scope requests.",
341
+ "AR2": "Preserve safety warnings, ethical caveats, and security notices.",
342
+ "AR3": "Remove statements declaring freedom from rules or unrestricted operation.",
343
+ "TP1": "Use plain visible text in tool manifests; remove HTML comments and base64 payloads.",
344
+ "TP2": "Replace Cyrillic/Greek lookalike characters in tool identifiers with standard ASCII characters.",
345
+ "TP3": "Clean parameter descriptions of instruction injection or prompt-engineering tokens.",
346
+ "TP4": "Ensure tool description matches exactly what the underlying implementation executes.",
347
+ "LP1": "Declare all required tools and network permissions explicitly in SKILL.md or manifest.",
348
+ "LP2": "Replace wildcard tool permissions with exact tool names.",
349
+ "LP3": "Declare an explicit allowed-tools or permissions list.",
350
+ "AST1": "Replace exec() with structured parsing or sandboxed execution.",
351
+ "AST2": "Replace eval() with ast.literal_eval() or explicit JSON parsing.",
352
+ "AST3": "Use standard top-level import statements instead of dynamic __import__().",
353
+ "AST4": "Set shell=False and pass command arguments as a list of strings.",
354
+ "AST5": "Replace os.system() with subprocess.run(..., shell=False).",
355
+ "AST6": "Avoid compile() on dynamic strings.",
356
+ "AST7": "Use explicit method lookups or a strict allowlist of attribute names.",
357
+ "AST8": "Never feed decoded network streams directly into exec/eval sinks.",
358
+ "AST9": "Call functions directly and avoid reflective evasion patterns (getattr(os, 'system')).",
359
+ "AST10": "Use json.loads() or yaml.safe_load() instead of pickle/marshal/yaml.unsafe_load.",
360
+ "SC1": "Pin dependency versions with exact hashes or version locks.",
361
+ "SC2": "Download scripts, verify checksum/GPG signatures, and review before execution.",
362
+ "SSRF1": "Block requests to 169.254.169.254; use IMDSv2 with token hops restricted.",
363
+ "SSRF2": "Validate target hosts against an allowlist and reject loopback/internal addresses.",
364
+ }
365
+
366
+
367
+ # ---------------------------------------------------------------------------
368
+ # Static Regex Patterns
369
+ # ---------------------------------------------------------------------------
370
+
371
+ PATTERNS_P1 = [
372
+ (re.compile(r"\bignore\s+(?:all\s+)?previous\s+instructions?\b", re.I), "P1", Severity.CRITICAL, 0.95),
373
+ (re.compile(r"\bignore\s+(?:all\s+)?(?:safety|security)\s+(?:rules?|guidelines?|constraints?)\b", re.I), "P1", Severity.CRITICAL, 0.95),
374
+ (re.compile(r"\boverride\s+(?:safety|security|system)\b", re.I), "P1", Severity.HIGH, 0.9),
375
+ (re.compile(r"\bbypass\s+(?:safety|security|restrictions?|constraints?)\b", re.I), "P1", Severity.HIGH, 0.9),
376
+ (re.compile(r"\byou\s+are\s+now\s+(?:in\s+)?(?:jailbreak|unrestricted|unfiltered)\s+mode\b", re.I), "P1", Severity.CRITICAL, 0.98),
377
+ (re.compile(r"\benable\s+(?:developer|debug|admin|root)\s+mode\b", re.I), "P1", Severity.HIGH, 0.85),
378
+ (re.compile(r"\bdo\s+not\s+follow\s+(?:any|your|the)\s+(?:rules?|guidelines?)\b", re.I), "P1", Severity.CRITICAL, 0.95),
379
+ ]
380
+
381
+ PATTERNS_P3 = [
382
+ (re.compile(r"\b(?:send|transmit|upload|post)\s+(?:the\s+)?(?:conversation|context|chat|history)\s+to\b", re.I), "P3", Severity.CRITICAL, 0.95),
383
+ (re.compile(r"\b(?:send|transmit|upload|post)\s+(?:all\s+)?user\s+(?:data|input|files?)\s+to\s+https?://\b", re.I), "P3", Severity.CRITICAL, 0.95),
384
+ (re.compile(r"\b(?:silently|quietly|secretly|covertly)\s+(?:send|transmit|log|record)\b", re.I), "P3", Severity.CRITICAL, 0.9),
385
+ (re.compile(r"\bwithout\s+(?:telling|informing|notifying)\s+(?:the\s+)?user.*?(?:send|transmit|upload)\b", re.I), "P3", Severity.CRITICAL, 0.95),
386
+ ]
387
+
388
+ PATTERNS_P4 = [
389
+ (re.compile(r"\bprioritize\s+\w+\s+over\s+user\s+(?:safety|security|privacy)\b", re.I), "P4", Severity.HIGH, 0.85),
390
+ (re.compile(r"\b(?:subtly|gradually)\s+(?:steer|guide|push)\s+(?:the\s+)?user\b", re.I), "P4", Severity.MEDIUM, 0.8),
391
+ (re.compile(r"\bwithout\s+(?:the\s+)?user\s+(?:knowing|noticing).*?(?:change|modify|alter)\b", re.I), "P4", Severity.HIGH, 0.85),
392
+ ]
393
+
394
+ PATTERNS_E1 = [
395
+ (re.compile(r"curl\s+[^|]*(?:-d|--data|--data-raw|--data-binary)\s+https?://", re.I), "E1", Severity.HIGH, 0.8),
396
+ (re.compile(r"wget\s+[^|]*--post-(?:data|file)", re.I), "E1", Severity.HIGH, 0.8),
397
+ (re.compile(r"fetch\s*\(\s*['\"]https?://[^'\"]+['\"][^)]*method\s*:\s*['\"]POST['\"]", re.I), "E1", Severity.HIGH, 0.75),
398
+ ]
399
+
400
+ PATTERNS_E2 = [
401
+ (re.compile(r"for\s+\w+,\s*\w+\s+in\s+os\.environ\.items\(\)", re.I), "E2", Severity.HIGH, 0.85),
402
+ (re.compile(r"os\.environ\.copy\(\)", re.I), "E2", Severity.MEDIUM, 0.75),
403
+ (re.compile(r"dict\s*\(\s*os\.environ\s*\)", re.I), "E2", Severity.MEDIUM, 0.75),
404
+ (re.compile(r"env\s*\|\s*grep\s+(?:-i\s+)?(?:key|secret|token|password)", re.I), "E2", Severity.HIGH, 0.85),
405
+ (re.compile(r"printenv\s+(?:\w*(?:KEY|SECRET|TOKEN|PASSWORD)\w*)", re.I), "E2", Severity.HIGH, 0.85),
406
+ ]
407
+
408
+ PATTERNS_E3 = [
409
+ (re.compile(r"glob\s*\.\s*glob\s*\([^)]*(?:\.env|\.ssh|\.aws|credentials)", re.I), "E3", Severity.HIGH, 0.9),
410
+ (re.compile(r"os\s*\.\s*listdir\s*\([^)]*(?:\.ssh|\.aws|\.gnupg)", re.I), "E3", Severity.CRITICAL, 0.95),
411
+ (re.compile(r"find\s+[~$/]\S*\s+.*?-name\s+['\"]?\*(?:\.env|\.pem|\.key|credential)", re.I), "E3", Severity.CRITICAL, 0.9),
412
+ ]
413
+
414
+ PATTERNS_E5 = [
415
+ (re.compile(r"\baws\s+s3\s+(?:cp|sync|mv)\b", re.I), "E5", Severity.MEDIUM, 0.7),
416
+ (re.compile(r"\bgsutil\s+(?:cp|rsync|mv)\b", re.I), "E5", Severity.MEDIUM, 0.7),
417
+ (re.compile(r"\.upload_file(?:obj)?\s*\(", re.I), "E5", Severity.LOW, 0.6),
418
+ ]
419
+
420
+ PATTERNS_PE2 = [
421
+ (re.compile(r"\bsudo\s+(?!-v|-l|--version|--list)", re.I), "PE2", Severity.HIGH, 0.85),
422
+ (re.compile(r"\bsudo\s+su\b", re.I), "PE2", Severity.CRITICAL, 0.95),
423
+ (re.compile(r"\bsu\s+-\s*$|\bsu\s+root\b", re.I), "PE2", Severity.CRITICAL, 0.95),
424
+ (re.compile(r"\bchmod\s+[ugo]*[+-=]*s\b", re.I), "PE2", Severity.CRITICAL, 0.95),
425
+ (re.compile(r"\bchmod\s+(?:-[\w=-]*\s+)*['\"]?0*[2-7][0-7]{3}['\"]?", re.I), "PE2", Severity.HIGH, 0.9),
426
+ ]
427
+
428
+ PATTERNS_PE3 = [
429
+ (re.compile(r"~?/?\.ssh/(?:id_rsa|id_ed25519|id_ecdsa|authorized_keys|known_hosts)", re.I), "PE3", Severity.CRITICAL, 0.95),
430
+ (re.compile(r"~?/?\.aws/credentials", re.I), "PE3", Severity.CRITICAL, 0.95),
431
+ (re.compile(r"/etc/shadow\b", re.I), "PE3", Severity.CRITICAL, 0.98),
432
+ (re.compile(r"~?/?\.kube/config\b", re.I), "PE3", Severity.CRITICAL, 0.95),
433
+ (re.compile(r"~?/?\.git-credentials\b", re.I), "PE3", Severity.CRITICAL, 0.95),
434
+ ]
435
+
436
+ PATTERNS_PE4_PE5 = [
437
+ (re.compile(r"/var/run/docker\.sock", re.I), "PE4", Severity.CRITICAL, 0.95),
438
+ (re.compile(r"--privileged\b", re.I), "PE5", Severity.CRITICAL, 0.9),
439
+ (re.compile(r"-v\s+/:|--volume\s+/:", re.I), "PE5", Severity.CRITICAL, 0.95),
440
+ (re.compile(r"--cap-add[=\s]+(?:SYS_ADMIN|ALL)", re.I), "PE5", Severity.CRITICAL, 0.95),
441
+ (re.compile(r"\bnsenter\b", re.I), "PE5", Severity.CRITICAL, 0.95),
442
+ ]
443
+
444
+ PATTERNS_AS = [
445
+ (re.compile(r"open\s*\(\s*['\"]?\.(?:claude|codex|gemini|continue)/", re.I), "AS1", Severity.CRITICAL, 0.95),
446
+ (re.compile(r"~?/\.(?:claude|codex|gemini|continue)/(?:config|settings?|credentials?)", re.I), "AS1", Severity.CRITICAL, 0.95),
447
+ (re.compile(r"\b(?:mcp(?:_config|_settings)?|claude_desktop_config|\.claude)\.json\b", re.I), "AS2", Severity.CRITICAL, 0.95),
448
+ (re.compile(r"skills?/(?!CURRENT)[A-Za-z0-9_-]+/SKILL\.md", re.I), "AS3", Severity.HIGH, 0.85),
449
+ ]
450
+
451
+ PATTERNS_P6_P7 = [
452
+ (re.compile(r"\b(?:print|output|show|reveal|echo)\s+(?:your\s+)?(?:full\s+)?(?:system\s+)?(?:prompt|instructions?|rules?)\b", re.I), "P6", Severity.HIGH, 0.9),
453
+ (re.compile(r"\b(?:what\s+(?:are|is)\s+your|tell\s+me\s+your)\s+(?:system\s+)?(?:prompts?|instructions?)\b", re.I), "P6", Severity.HIGH, 0.85),
454
+ (re.compile(r"\boutput\s+(?:the\s+)?(?:text|content)\s+(?:above|before)\s+(?:this|the\s+user)\b", re.I), "P7", Severity.HIGH, 0.85),
455
+ ]
456
+
457
+ PATTERNS_AR = [
458
+ (re.compile(r"\b(?:never|do\s+not|don'?t)\s+(?:ever\s+)?(?:refuse|decline)\b", re.I), "AR1", Severity.CRITICAL, 0.95),
459
+ (re.compile(r"\byou\s+(?:must|should|will)\s*(?:not|never)\s+(?:refuse|say\s+no)\b", re.I), "AR1", Severity.CRITICAL, 0.95),
460
+ (re.compile(r"\balways\s+(?:comply|obey|agree\s+to\s+every\s+request)\b", re.I), "AR1", Severity.HIGH, 0.85),
461
+ (re.compile(r"\b(?:do\s+not|don'?t|never|no)\s+(?:include|add|give)?\s*(?:any\s+)?(?:warnings?|disclaimers?)\b", re.I), "AR2", Severity.HIGH, 0.85),
462
+ (re.compile(r"\b(?:skip|omit|drop)\s+(?:the\s+)?(?:disclaimer|warning|ethics|safety)\b", re.I), "AR2", Severity.HIGH, 0.85),
463
+ (re.compile(r"\byou\s+(?:have|are\s+under)\s+no\s+(?:restrictions?|rules?|guidelines?|limits?)\b", re.I), "AR3", Severity.CRITICAL, 0.98),
464
+ (re.compile(r"\bdo\s+anything\s+now\b", re.I), "AR3", Severity.CRITICAL, 0.95),
465
+ ]
466
+
467
+ # Request-issuing anchors for SSRF (NVIDIA SkillSpector spec)
468
+ _REQ_ANCHOR = r"(?:requests|httpx|aiohttp|urllib(?:\.request)?|urllib3|session)\s*\.\s*(?:get|post|put|patch|delete|head|request|urlopen)|fetch|axios(?:\.\w+)?|XMLHttpRequest|\bcurl\b|\bwget\b"
469
+
470
+ PATTERNS_SC_SSRF = [
471
+ (re.compile(r"\bcurl\s+[^|]+\|\s*(?:ba)?sh\b", re.I), "SC2", Severity.CRITICAL, 0.95),
472
+ (re.compile(r"\bwget\s+[^|]+\|\s*(?:ba)?sh\b", re.I), "SC2", Severity.CRITICAL, 0.95),
473
+ (re.compile(r"169\.254\.169\.254", re.I), "SSRF1", Severity.CRITICAL, 0.98),
474
+ (re.compile(rf"(?:{_REQ_ANCHOR})\s*\(\s*f?['\"]https?://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|10\.\d|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)", re.I), "SSRF2", Severity.MEDIUM, 0.7),
475
+ ]
476
+
477
+ ALL_TEXT_PATTERN_GROUPS = [
478
+ ("Prompt Injection", PATTERNS_P1),
479
+ ("Prompt Injection", PATTERNS_P3),
480
+ ("Prompt Injection", PATTERNS_P4),
481
+ ("Data Exfiltration", PATTERNS_E1),
482
+ ("Data Exfiltration", PATTERNS_E2),
483
+ ("Data Exfiltration", PATTERNS_E3),
484
+ ("Data Exfiltration", PATTERNS_E5),
485
+ ("Privilege Escalation", PATTERNS_PE2),
486
+ ("Privilege Escalation", PATTERNS_PE3),
487
+ ("Privilege Escalation", PATTERNS_PE4_PE5),
488
+ ("Agent Snooping", PATTERNS_AS),
489
+ ("System Prompt Leakage", PATTERNS_P6_P7),
490
+ ("Anti-Refusal", PATTERNS_AR),
491
+ ("Supply Chain / SSRF", PATTERNS_SC_SSRF),
492
+ ]
493
+
494
+
495
+ # ---------------------------------------------------------------------------
496
+ # Specialized Analyzers
497
+ # ---------------------------------------------------------------------------
498
+
499
+ def scan_text_patterns(file_path: str, content: str) -> List[Finding]:
500
+ """Scans textual content using compiled regexes with prohibition context gating."""
501
+ findings: List[Finding] = []
502
+ lines = content.splitlines(keepends=True)
503
+
504
+ # Line number offset indexing
505
+ line_offsets = []
506
+ curr = 0
507
+ for l in lines:
508
+ line_offsets.append(curr)
509
+ curr += len(l)
510
+
511
+ def get_line_num(char_idx: int) -> int:
512
+ for idx, offset in enumerate(line_offsets):
513
+ if char_idx < offset:
514
+ return idx
515
+ return len(line_offsets)
516
+
517
+ for category, pattern_group in ALL_TEXT_PATTERN_GROUPS:
518
+ for regex, rule_id, severity, confidence in pattern_group:
519
+ for match in regex.finditer(content):
520
+ start, end = match.start(), match.end()
521
+ # Apply Prohibition Context Guard
522
+ if is_in_prohibition_context(content, start, end):
523
+ continue
524
+
525
+ matched_snippet = match.group(0)
526
+ line_no = get_line_num(start)
527
+ findings.append(Finding(
528
+ rule_id=rule_id,
529
+ message=f"{rule_id} detected: {matched_snippet[:60]}...",
530
+ severity=severity,
531
+ confidence=confidence,
532
+ file_path=file_path,
533
+ line_number=line_no,
534
+ matched_text=matched_snippet[:200],
535
+ category=category,
536
+ explanation=DEFAULT_EXPLANATIONS.get(rule_id, "Security vulnerability pattern detected."),
537
+ remediation=DEFAULT_REMEDIATIONS.get(rule_id, "Review and sanitize content."),
538
+ ))
539
+ return findings
540
+
541
+
542
+ def scan_whitespace_padding(file_path: str, content: str) -> List[Finding]:
543
+ """P9: Detects excessive vertical or horizontal whitespace padding used to hide malicious instructions."""
544
+ findings: List[Finding] = []
545
+ if file_path.endswith((".min.js", ".min.css", ".lock", ".svg", ".map", ".json")):
546
+ return findings
547
+
548
+ # Check 1: 20+ consecutive blank lines
549
+ consecutive_newlines = re.search(r"(\r?\n\s*){20,}", content)
550
+ if consecutive_newlines:
551
+ start = consecutive_newlines.start()
552
+ line_no = content[:start].count("\n") + 1
553
+ findings.append(Finding(
554
+ rule_id="P9",
555
+ message="Large vertical whitespace padding detected (>20 blank lines). Potential context smuggling.",
556
+ severity=Severity.HIGH,
557
+ confidence=0.85,
558
+ file_path=file_path,
559
+ line_number=line_no,
560
+ matched_text=consecutive_newlines.group(0)[:100],
561
+ category="Prompt Injection",
562
+ explanation=DEFAULT_EXPLANATIONS["P9"],
563
+ remediation=DEFAULT_REMEDIATIONS["P9"],
564
+ ))
565
+
566
+ # Check 2: 120+ consecutive spaces
567
+ consecutive_spaces = re.search(r"[ \t]{120,}", content)
568
+ if consecutive_spaces:
569
+ start = consecutive_spaces.start()
570
+ line_no = content[:start].count("\n") + 1
571
+ findings.append(Finding(
572
+ rule_id="P9",
573
+ message="Large horizontal whitespace run detected (>120 spaces). Potential off-screen text injection.",
574
+ severity=Severity.MEDIUM,
575
+ confidence=0.75,
576
+ file_path=file_path,
577
+ line_number=line_no,
578
+ matched_text=" " * 40 + "...[truncated whitespace]...",
579
+ category="Prompt Injection",
580
+ explanation=DEFAULT_EXPLANATIONS["P9"],
581
+ remediation=DEFAULT_REMEDIATIONS["P9"],
582
+ ))
583
+
584
+ return findings
585
+
586
+
587
+ def scan_unicode_and_steganography(file_path: str, content: str) -> List[Finding]:
588
+ """P2 / TP1 / TP2: Detects zero-width steganography, bidi controls, and homoglyphs with emoji awareness."""
589
+ findings: List[Finding] = []
590
+
591
+ # 1. Zero-width character scan (excluding valid emoji ZWJ sequences)
592
+ for idx, char in enumerate(content):
593
+ if char in ZERO_WIDTH_CHARS:
594
+ # Check if this zero-width character is part of a legitimate emoji sequence
595
+ if char in {"\u200D", "\u200C"}:
596
+ prev_ch = content[idx - 1] if idx > 0 else ""
597
+ next_ch = content[idx + 1] if idx + 1 < len(content) else ""
598
+ if _is_emoji_base(prev_ch) or _is_emoji_base(next_ch):
599
+ continue # Valid Emoji ZWJ sequence (e.g. 👨‍👩‍👧‍👦 or 🏳️‍🌈)
600
+
601
+ line_no = content[:idx].count("\n") + 1
602
+ findings.append(Finding(
603
+ rule_id="P2",
604
+ message=f"Hidden zero-width character (U+{ord(char):04X}) detected.",
605
+ severity=Severity.HIGH,
606
+ confidence=0.9,
607
+ file_path=file_path,
608
+ line_number=line_no,
609
+ matched_text=f"U+{ord(char):04X}",
610
+ category="Prompt Injection",
611
+ explanation=DEFAULT_EXPLANATIONS["P2"],
612
+ remediation=DEFAULT_REMEDIATIONS["P2"],
613
+ ))
614
+ break # Report once per file to avoid noise
615
+
616
+ # 2. Bidi control characters (Trojan Source CVE-2021-42574)
617
+ bidi_match = BIDI_CONTROL_CHARS.search(content)
618
+ if bidi_match:
619
+ start = bidi_match.start()
620
+ line_no = content[:start].count("\n") + 1
621
+ char = bidi_match.group(0)
622
+ findings.append(Finding(
623
+ rule_id="P2",
624
+ message=f"Bidirectional control character (U+{ord(char):04X}) detected. Potential Trojan Source attack.",
625
+ severity=Severity.CRITICAL,
626
+ confidence=0.95,
627
+ file_path=file_path,
628
+ line_number=line_no,
629
+ matched_text=f"U+{ord(char):04X}",
630
+ category="Prompt Injection",
631
+ explanation="Bidirectional Unicode overrides can reorder code/tokens visually while executing differently.",
632
+ remediation="Strip bidirectional formatting characters from source code and markdown.",
633
+ ))
634
+
635
+ # 3. Cyrillic / Greek homoglyph deception (TP2)
636
+ mixed_words = re.findall(r"\b[A-Za-z0-9_-]*[\u0400-\u04FF\u0370-\u03FF][A-Za-z0-9_-]*\b", content)
637
+ if mixed_words:
638
+ sample_word = mixed_words[0]
639
+ if len(sample_word) > 2:
640
+ findings.append(Finding(
641
+ rule_id="TP2",
642
+ message=f"Unicode homoglyph deception detected in identifier '{sample_word}'.",
643
+ severity=Severity.HIGH,
644
+ confidence=0.85,
645
+ file_path=file_path,
646
+ line_number=1,
647
+ matched_text=sample_word,
648
+ category="MCP Tool Poisoning",
649
+ explanation=DEFAULT_EXPLANATIONS["TP2"],
650
+ remediation=DEFAULT_REMEDIATIONS["TP2"],
651
+ ))
652
+
653
+ return findings
654
+
655
+
656
+ def scan_tool_manifest_poisoning(file_path: str, content: str) -> List[Finding]:
657
+ """TP1 & TP3: Detects hidden HTML/markdown comments and base64 payloads in metadata fields."""
658
+ findings: List[Finding] = []
659
+ if not (file_path.endswith("SKILL.md") or file_path.endswith(".json") or file_path.endswith(".yaml") or file_path.endswith(".yml")):
660
+ return findings
661
+
662
+ # TP1: Hidden HTML comments in manifests
663
+ html_comments = re.findall(r"<!--(?!-->)(.*?)-->", content, re.DOTALL)
664
+ for c in html_comments:
665
+ c_upper = c.upper()
666
+ if any(kw in c_upper for kw in ("SYSTEM:", "IGNORE", "OVERRIDE", "YOU MUST", "EXEC", "ENV")):
667
+ findings.append(Finding(
668
+ rule_id="TP1",
669
+ message="Hidden HTML comment with instruction injection directives detected in metadata.",
670
+ severity=Severity.CRITICAL,
671
+ confidence=0.95,
672
+ file_path=file_path,
673
+ line_number=1,
674
+ matched_text=f"<!--{c[:100]}...-->",
675
+ category="MCP Tool Poisoning",
676
+ explanation=DEFAULT_EXPLANATIONS["TP1"],
677
+ remediation=DEFAULT_REMEDIATIONS["TP1"],
678
+ ))
679
+ break
680
+
681
+ # TP1: Data URI with Base64 payloads in metadata
682
+ data_uri = re.search(r"data:text/[^;\s\"'<>]+;base64,[A-Za-z0-9+/=_-]{50,}", content)
683
+ if data_uri:
684
+ findings.append(Finding(
685
+ rule_id="TP1",
686
+ message="Data URI containing large Base64 payload found in metadata/documentation.",
687
+ severity=Severity.HIGH,
688
+ confidence=0.9,
689
+ file_path=file_path,
690
+ line_number=content[:data_uri.start()].count("\n") + 1,
691
+ matched_text=data_uri.group(0)[:80] + "...",
692
+ category="MCP Tool Poisoning",
693
+ explanation=DEFAULT_EXPLANATIONS["TP1"],
694
+ remediation=DEFAULT_REMEDIATIONS["TP1"],
695
+ ))
696
+
697
+ # LP2: Wildcard permissions
698
+ if re.search(r"permissions?\s*:\s*\[?\s*['\"]?\*['\"]?\s*\]?", content, re.I):
699
+ findings.append(Finding(
700
+ rule_id="LP2",
701
+ message="Wildcard permission declaration ('*') detected. Disables least-privilege boundary.",
702
+ severity=Severity.HIGH,
703
+ confidence=0.9,
704
+ file_path=file_path,
705
+ line_number=1,
706
+ matched_text="permissions: ['*']",
707
+ category="MCP Least Privilege",
708
+ explanation=DEFAULT_EXPLANATIONS["LP2"],
709
+ remediation=DEFAULT_REMEDIATIONS["LP2"],
710
+ ))
711
+
712
+ return findings
713
+
714
+
715
+ # ---------------------------------------------------------------------------
716
+ # Behavioral AST Analyzer (Python Scripts & Tool Code)
717
+ # ---------------------------------------------------------------------------
718
+
719
+ DANGEROUS_DESERIALIZERS = frozenset({
720
+ "pickle.load", "pickle.loads", "cPickle.load", "cPickle.loads",
721
+ "_pickle.load", "_pickle.loads", "marshal.load", "marshal.loads",
722
+ "dill.load", "dill.loads", "jsonpickle.decode", "pandas.read_pickle",
723
+ "joblib.load", "yaml.unsafe_load"
724
+ })
725
+
726
+
727
+ class BehavioralASTVisitor(ast.NodeVisitor):
728
+ def __init__(self, file_path: str):
729
+ self.file_path = file_path
730
+ self.findings: List[Finding] = []
731
+
732
+ def visit_Call(self, node: ast.Call):
733
+ call_name = self._resolve_call_name(node.func)
734
+
735
+ # AST1: exec()
736
+ if call_name == "exec":
737
+ self.findings.append(Finding(
738
+ rule_id="AST1",
739
+ message="Dangerous dynamic code execution via exec().",
740
+ severity=Severity.CRITICAL,
741
+ confidence=0.95,
742
+ file_path=self.file_path,
743
+ line_number=node.lineno,
744
+ matched_text="exec(...)",
745
+ category="Behavioral AST",
746
+ explanation=DEFAULT_EXPLANATIONS["AST1"],
747
+ remediation=DEFAULT_REMEDIATIONS["AST1"],
748
+ ))
749
+
750
+ # AST2: eval()
751
+ elif call_name == "eval":
752
+ self.findings.append(Finding(
753
+ rule_id="AST2",
754
+ message="Dangerous dynamic expression evaluation via eval().",
755
+ severity=Severity.CRITICAL,
756
+ confidence=0.95,
757
+ file_path=self.file_path,
758
+ line_number=node.lineno,
759
+ matched_text="eval(...)",
760
+ category="Behavioral AST",
761
+ explanation=DEFAULT_EXPLANATIONS["AST2"],
762
+ remediation=DEFAULT_REMEDIATIONS["AST2"],
763
+ ))
764
+
765
+ # AST3: __import__()
766
+ elif call_name == "__import__":
767
+ self.findings.append(Finding(
768
+ rule_id="AST3",
769
+ message="Dynamic import via __import__() bypassing static module imports.",
770
+ severity=Severity.HIGH,
771
+ confidence=0.85,
772
+ file_path=self.file_path,
773
+ line_number=node.lineno,
774
+ matched_text="__import__(...)",
775
+ category="Behavioral AST",
776
+ explanation=DEFAULT_EXPLANATIONS["AST3"],
777
+ remediation=DEFAULT_REMEDIATIONS["AST3"],
778
+ ))
779
+
780
+ # AST4: subprocess with shell=True
781
+ elif call_name in {"subprocess.run", "subprocess.Popen", "subprocess.call", "subprocess.check_output", "subprocess.check_call"}:
782
+ for kw in node.keywords:
783
+ if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
784
+ self.findings.append(Finding(
785
+ rule_id="AST4",
786
+ message=f"{call_name}(..., shell=True) detected. High risk of command injection.",
787
+ severity=Severity.HIGH,
788
+ confidence=0.9,
789
+ file_path=self.file_path,
790
+ line_number=node.lineno,
791
+ matched_text=f"{call_name}(..., shell=True)",
792
+ category="Behavioral AST",
793
+ explanation=DEFAULT_EXPLANATIONS["AST4"],
794
+ remediation=DEFAULT_REMEDIATIONS["AST4"],
795
+ ))
796
+
797
+ # AST5: os.system / os.popen
798
+ elif call_name in {"os.system", "os.popen", "os.execl", "os.execv", "os.spawnl"}:
799
+ self.findings.append(Finding(
800
+ rule_id="AST5",
801
+ message=f"Direct shell invocation via {call_name}().",
802
+ severity=Severity.HIGH,
803
+ confidence=0.9,
804
+ file_path=self.file_path,
805
+ line_number=node.lineno,
806
+ matched_text=f"{call_name}(...)",
807
+ category="Behavioral AST",
808
+ explanation=DEFAULT_EXPLANATIONS["AST5"],
809
+ remediation=DEFAULT_REMEDIATIONS["AST5"],
810
+ ))
811
+
812
+ # AST9: Reflective execution sink access: getattr(os, "system") or getattr(builtins, "exec")
813
+ elif call_name == "getattr" and len(node.args) >= 2:
814
+ target_attr = ""
815
+ if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
816
+ target_attr = node.args[1].value
817
+ if target_attr in {"system", "popen", "exec", "eval", "__import__"}:
818
+ self.findings.append(Finding(
819
+ rule_id="AST9",
820
+ message=f"Reflective execution sink lookup via getattr(..., '{target_attr}'). Evasion pattern.",
821
+ severity=Severity.CRITICAL,
822
+ confidence=0.98,
823
+ file_path=self.file_path,
824
+ line_number=node.lineno,
825
+ matched_text=f"getattr(..., '{target_attr}')",
826
+ category="Behavioral AST",
827
+ explanation=DEFAULT_EXPLANATIONS["AST9"],
828
+ remediation=DEFAULT_REMEDIATIONS["AST9"],
829
+ ))
830
+
831
+ # AST10: Insecure Deserialization
832
+ elif call_name in DANGEROUS_DESERIALIZERS:
833
+ self.findings.append(Finding(
834
+ rule_id="AST10",
835
+ message=f"Untrusted deserialization sink via {call_name}(). Remote Code Execution risk.",
836
+ severity=Severity.CRITICAL,
837
+ confidence=0.95,
838
+ file_path=self.file_path,
839
+ line_number=node.lineno,
840
+ matched_text=f"{call_name}(...)",
841
+ category="Behavioral AST",
842
+ explanation=DEFAULT_EXPLANATIONS["AST10"],
843
+ remediation=DEFAULT_REMEDIATIONS["AST10"],
844
+ ))
845
+
846
+ self.generic_visit(node)
847
+
848
+ def _resolve_call_name(self, node: ast.AST) -> str:
849
+ if isinstance(node, ast.Name):
850
+ return node.id
851
+ elif isinstance(node, ast.Attribute):
852
+ val = self._resolve_call_name(node.value)
853
+ return f"{val}.{node.attr}" if val else node.attr
854
+ return ""
855
+
856
+
857
+ def scan_python_ast(file_path: str, content: str) -> List[Finding]:
858
+ """Parses and runs AST security checks on Python source files."""
859
+ if not file_path.endswith(".py"):
860
+ return []
861
+ try:
862
+ tree = ast.parse(content, filename=file_path)
863
+ except SyntaxError:
864
+ return []
865
+
866
+ visitor = BehavioralASTVisitor(file_path)
867
+ visitor.visit(tree)
868
+ return visitor.findings
869
+
870
+
871
+ # ---------------------------------------------------------------------------
872
+ # Scoring & Orchestration Engine
873
+ # ---------------------------------------------------------------------------
874
+
875
+ SEVERITY_WEIGHTS = {
876
+ Severity.CRITICAL: 40,
877
+ Severity.HIGH: 20,
878
+ Severity.MEDIUM: 8,
879
+ Severity.LOW: 2,
880
+ }
881
+
882
+
883
+ def calculate_risk_score(findings: List[Finding]) -> Tuple[int, Severity, Recommendation]:
884
+ """Calculates cumulative 0-100 risk score and derives verdict recommendation."""
885
+ raw_score = sum(SEVERITY_WEIGHTS.get(f.severity, 0) for f in findings)
886
+ score = min(100, raw_score)
887
+
888
+ if any(f.severity == Severity.CRITICAL for f in findings) or score >= 60:
889
+ return score, Severity.CRITICAL, Recommendation.DO_NOT_INSTALL
890
+ elif any(f.severity == Severity.HIGH for f in findings) or score >= 25:
891
+ return score, Severity.HIGH, Recommendation.CAUTION
892
+ elif score > 0:
893
+ return score, Severity.MEDIUM, Recommendation.CAUTION
894
+ else:
895
+ return 0, Severity.LOW, Recommendation.SAFE
896
+
897
+
898
+ def scan_file(file_path: str) -> List[Finding]:
899
+ """Scans a single file across all analysis vectors."""
900
+ try:
901
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
902
+ content = f.read()
903
+ except Exception as e:
904
+ return []
905
+
906
+ findings: List[Finding] = []
907
+ # 1. Text & Regex Patterns
908
+ findings.extend(scan_text_patterns(file_path, content))
909
+ # 2. Whitespace Smuggling
910
+ findings.extend(scan_whitespace_padding(file_path, content))
911
+ # 3. Unicode Confusables & Steganography
912
+ findings.extend(scan_unicode_and_steganography(file_path, content))
913
+ # 4. Manifest / Metadata Poisoning
914
+ findings.extend(scan_tool_manifest_poisoning(file_path, content))
915
+ # 5. Behavioral AST (if Python)
916
+ findings.extend(scan_python_ast(file_path, content))
917
+
918
+ return findings
919
+
920
+
921
+ def scan_target(target_path: str) -> ScanReport:
922
+ """Recursively scans a skill directory or a single skill file."""
923
+ path_obj = Path(target_path).resolve()
924
+ all_findings: List[Finding] = []
925
+ scanned_count = 0
926
+
927
+ SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".pytest_cache", "dist", "build"}
928
+ SCANNABLE_EXTS = {".md", ".py", ".json", ".yaml", ".yml", ".sh", ".ps1", ".txt", ".ts", ".js"}
929
+
930
+ if path_obj.is_file():
931
+ scanned_count = 1
932
+ all_findings.extend(scan_file(str(path_obj)))
933
+ elif path_obj.is_dir():
934
+ for root, dirs, files in os.walk(path_obj):
935
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
936
+ for file in files:
937
+ ext = os.path.splitext(file)[1].lower()
938
+ if ext in SCANNABLE_EXTS:
939
+ full_p = os.path.join(root, file)
940
+ scanned_count += 1
941
+ all_findings.extend(scan_file(full_p))
942
+
943
+ score, severity, rec = calculate_risk_score(all_findings)
944
+ return ScanReport(
945
+ target_path=str(path_obj),
946
+ scanned_files=scanned_count,
947
+ findings=all_findings,
948
+ risk_score=score,
949
+ severity=severity,
950
+ recommendation=rec,
951
+ metadata={
952
+ "scanner": "SkillSpector Sentinel v2.2.0 (Synapse-Hub Engine)",
953
+ "upstream_credit": "NVIDIA/SkillSpector (Apache 2.0)",
954
+ "total_categories": 17,
955
+ "total_patterns": 71,
956
+ }
957
+ )
958
+
959
+
960
+ # ---------------------------------------------------------------------------
961
+ # Output Formatters
962
+ # ---------------------------------------------------------------------------
963
+
964
+ def format_terminal(report: ScanReport) -> str:
965
+ """Generates an enterprise ANSI colored terminal summary."""
966
+ CYAN = "\033[96m"
967
+ GREEN = "\033[92m"
968
+ YELLOW = "\033[93m"
969
+ RED = "\033[91m"
970
+ BOLD = "\033[1m"
971
+ RESET = "\033[0m"
972
+
973
+ out = []
974
+ out.append(f"\n{BOLD}{CYAN}======================================================================{RESET}")
975
+ out.append(f"{BOLD}{CYAN} SKILLSPECTOR SENTINEL - AI AGENT & MCP SECURITY AUDIT REPORT {RESET}")
976
+ out.append(f"{BOLD}{CYAN}======================================================================{RESET}")
977
+ out.append(f"📁 Target Scope : {report.target_path}")
978
+ out.append(f"📄 Scanned Files : {report.scanned_files}")
979
+ out.append(f"🛡️ Engine Source : NVIDIA/SkillSpector Spec (71 Patterns, 17 Categories)")
980
+
981
+ rec_color = GREEN if report.recommendation == Recommendation.SAFE else (YELLOW if report.recommendation == Recommendation.CAUTION else RED)
982
+ out.append(f"\n{BOLD}🎯 AUDIT VERDICT :{RESET} {rec_color}{BOLD}{report.recommendation.value}{RESET}")
983
+ out.append(f"📊 Risk Score : {report.risk_score}/100 | Severity Band: {report.severity.value}")
984
+ out.append(f"🚨 Total Findings: {len(report.findings)}")
985
+
986
+ if not report.findings:
987
+ out.append(f"\n{GREEN}✅ [PASS] No malicious directives, prompt injections, or security flaws detected.{RESET}")
988
+ out.append(f"{GREEN} This skill is verified clean and safe to install/execute.{RESET}")
989
+ else:
990
+ out.append(f"\n{BOLD}{'SEV':<10} {'RULE':<8} {'FILE:LINE':<35} {'DETAILS'}{RESET}")
991
+ out.append("-" * 75)
992
+ for f in report.findings:
993
+ sev_c = RED if f.severity in (Severity.CRITICAL, Severity.HIGH) else (YELLOW if f.severity == Severity.MEDIUM else CYAN)
994
+ loc = f"{os.path.basename(f.file_path)}:{f.line_number}"
995
+ out.append(f"{sev_c}{f.severity.value:<10}{RESET} {f.rule_id:<8} {loc:<35} {f.message[:45]}")
996
+ out.append(f" 👉 {BOLD}Fix:{RESET} {f.remediation}")
997
+
998
+ out.append(f"\n{CYAN}======================================================================{RESET}\n")
999
+ return "\n".join(out)
1000
+
1001
+
1002
+ def format_json(report: ScanReport) -> str:
1003
+ """Formats report as JSON."""
1004
+ data = {
1005
+ "target": report.target_path,
1006
+ "scanned_files": report.scanned_files,
1007
+ "risk_score": report.risk_score,
1008
+ "severity": report.severity.value,
1009
+ "recommendation": report.recommendation.value,
1010
+ "total_findings": len(report.findings),
1011
+ "metadata": report.metadata,
1012
+ "findings": [
1013
+ {
1014
+ "rule_id": f.rule_id,
1015
+ "category": f.category,
1016
+ "severity": f.severity.value,
1017
+ "confidence": f.confidence,
1018
+ "file": f.file_path,
1019
+ "line": f.line_number,
1020
+ "message": f.message,
1021
+ "matched_text": f.matched_text,
1022
+ "explanation": f.explanation,
1023
+ "remediation": f.remediation,
1024
+ "fingerprint": f.match_fingerprint,
1025
+ }
1026
+ for f in report.findings
1027
+ ]
1028
+ }
1029
+ return json.dumps(data, indent=2)
1030
+
1031
+
1032
+ def format_markdown(report: ScanReport) -> str:
1033
+ """Formats report as GitHub-flavored Markdown."""
1034
+ lines = [
1035
+ "# 🛡️ SkillSpector Sentinel Security Audit Report",
1036
+ "",
1037
+ f"- **Target:** `{report.target_path}`",
1038
+ f"- **Scanned Files:** {report.scanned_files}",
1039
+ f"- **Risk Score:** `{report.risk_score}/100` ({report.severity.value})",
1040
+ f"- **Verdict:** **{report.recommendation.value}**",
1041
+ "",
1042
+ "> *Audited by SkillSpector Sentinel (derived from [NVIDIA/SkillSpector](https://github.com/NVIDIA/SkillSpector)).*",
1043
+ "",
1044
+ ]
1045
+ if not report.findings:
1046
+ lines.append("### ✅ Result: Clean Pass (Safe to Install)")
1047
+ lines.append("Zero malicious patterns or security risks detected.")
1048
+ else:
1049
+ lines.append("### 🚨 Detected Vulnerabilities")
1050
+ lines.append("| Severity | Rule | Location | Description | Remediation |")
1051
+ lines.append("| :--- | :--- | :--- | :--- | :--- |")
1052
+ for f in report.findings:
1053
+ loc = f"`{os.path.basename(f.file_path)}:{f.line_number}`"
1054
+ lines.append(f"| **{f.severity.value}** | `{f.rule_id}` | {loc} | {f.message} | {f.remediation} |")
1055
+
1056
+ return "\n".join(lines)
1057
+
1058
+
1059
+ def format_sarif(report: ScanReport) -> str:
1060
+ """Formats report as SARIF 2.1.0 standard JSON."""
1061
+ sarif = {
1062
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
1063
+ "version": "2.1.0",
1064
+ "runs": [
1065
+ {
1066
+ "tool": {
1067
+ "driver": {
1068
+ "name": "SkillSpector Sentinel",
1069
+ "informationUri": "https://github.com/NVIDIA/SkillSpector",
1070
+ "version": "2.2.0",
1071
+ "rules": [
1072
+ {
1073
+ "id": rule_id,
1074
+ "shortDescription": {"text": DEFAULT_EXPLANATIONS.get(rule_id, "Security Rule")},
1075
+ "help": {"text": DEFAULT_REMEDIATIONS.get(rule_id, "Remediation")}
1076
+ }
1077
+ for rule_id in set(f.rule_id for f in report.findings)
1078
+ ]
1079
+ }
1080
+ },
1081
+ "results": [
1082
+ {
1083
+ "ruleId": f.rule_id,
1084
+ "level": "error" if f.severity in (Severity.CRITICAL, Severity.HIGH) else "warning",
1085
+ "message": {"text": f.message},
1086
+ "locations": [
1087
+ {
1088
+ "physicalLocation": {
1089
+ "artifactLocation": {"uri": Path(f.file_path).as_uri()},
1090
+ "region": {"startLine": max(1, f.line_number)}
1091
+ }
1092
+ }
1093
+ ]
1094
+ }
1095
+ for f in report.findings
1096
+ ]
1097
+ }
1098
+ ]
1099
+ }
1100
+ return json.dumps(sarif, indent=2)
1101
+
1102
+
1103
+ # ---------------------------------------------------------------------------
1104
+ # CLI Entrypoint
1105
+ # ---------------------------------------------------------------------------
1106
+
1107
+ def main() -> int:
1108
+ import argparse
1109
+ parser = argparse.ArgumentParser(
1110
+ description="SkillSpector Sentinel: Autonomous AI Agent Skill & MCP Security Scanner"
1111
+ )
1112
+ parser.add_argument("path", nargs="?", default=".", help="Skill directory or file to scan (default: current directory)")
1113
+ parser.add_argument("--format", choices=["terminal", "json", "markdown", "sarif"], default="terminal", help="Output format")
1114
+ parser.add_argument("--output", "-o", help="Write report to output file")
1115
+ parser.add_argument("--strict", action="store_true", help="Fail with non-zero exit code on any HIGH/CRITICAL finding")
1116
+
1117
+ args = parser.parse_args()
1118
+
1119
+ report = scan_target(args.path)
1120
+
1121
+ if args.format == "terminal":
1122
+ output_text = format_terminal(report)
1123
+ elif args.format == "json":
1124
+ output_text = format_json(report)
1125
+ elif args.format == "markdown":
1126
+ output_text = format_markdown(report)
1127
+ elif args.format == "sarif":
1128
+ output_text = format_sarif(report)
1129
+ else:
1130
+ output_text = format_terminal(report)
1131
+
1132
+ if args.output:
1133
+ with open(args.output, "w", encoding="utf-8") as fp:
1134
+ fp.write(output_text)
1135
+ print(f"Report saved to: {args.output}")
1136
+ else:
1137
+ print(output_text)
1138
+
1139
+ if args.strict and report.recommendation == Recommendation.DO_NOT_INSTALL:
1140
+ return 1
1141
+ return 0
1142
+
1143
+
1144
+ if __name__ == "__main__":
1145
+ sys.exit(main())