Skip to content

Commit ab0431f

Browse files
authored
Sync OSS release snapshot 2.3.7 - Merge pull request #206 from NVIDIA/keshavp/oss-release-2026-06-24
Sync OSS release snapshot for SkillSpector 2.3.7 Refresh the public NVIDIA/SkillSpector repository from the sanitized internal OSS release snapshot generated from GitLab main. The final source snapshot was produced from internal release branch release/oss-2026-06-24-3 at 514c2a76c3dd995859529138313a9806365e8b95, based on GitLab main 3820dab5c9680517598d558f2b986cad26f4fc61. This release includes the latest merged internal changes and publishes the public package metadata bump to 2.3.7. It also carries analyzer/test updates already staged in the OSS release PR, including static runner behavior changes, pattern default updates, analyzer registry/test adjustments, and whitespace cleanup in generated public docs/test files. Highlights: - Publish SkillSpector package version 2.3.7 in pyproject.toml and uv.lock. - Sync the public tree with the latest internal OSS snapshot. - Preserve the sanitized public-release shape generated by scripts/create-oss-release.sh. - Keep DCO compliance on the GitHub release commits. Verification: - scripts/create-oss-release.sh release/oss-2026-06-24-3 - make test-unit: 990 passed, 12 skipped, 26 deselected, 6 expected failed - git diff --check - GitHub CI passed: DCO Check, Lint & Test Python 3.12, Lint & Test Python 3.13
2 parents 7bc9c0f + a88d793 commit ab0431f

19 files changed

Lines changed: 126 additions & 96 deletions

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,3 @@ docker-build:
152152
# Build and smoke test the Docker image
153153
docker-smoke: docker-build
154154
tests/docker/smoke.sh
155-

docs/B.3.1-mcp-least-privilege.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# B.3.1: MCP Least-Privilege Analysis (LP1 -- LP4)
22

3-
**Author:** Nir Paz | **Date:** 2026-03-30 | **Status:** Implemented
3+
**Author:** Nir Paz | **Date:** 2026-03-30 | **Status:** Implemented
44
**Component:** `src/skillspector/nodes/analyzers/mcp_least_privilege.py`
55

66
---

docs/B.3.2-mcp-tool-poisoning.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# B.3.2: MCP Tool-Poisoning Detection (TP1 -- TP4)
22

3-
**Author:** Nir Paz | **Date:** 2026-03-30 | **Status:** Implemented
3+
**Author:** Nir Paz | **Date:** 2026-03-30 | **Status:** Implemented
44
**Component:** `src/skillspector/nodes/analyzers/mcp_tool_poisoning.py`
55

66
---

docs/SC4-osv-live-vulnerability-lookups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SC4: Live Vulnerability Lookups via OSV.dev
22

3-
**Author:** Nraghavan | **Date:** 2026-03-17 | **Status:** Implemented
3+
**Author:** Nraghavan | **Date:** 2026-03-17 | **Status:** Implemented
44
**Component:** `static_patterns_supply_chain.py` (SC4 rule), `osv_client.py`
55

66
---

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "skillspector"
7-
version = "2.3.5"
7+
version = "2.3.7"
88
description = "SkillSpector: Security scanner for AI agent skills (Claude Code, Cursor, and similar). Scans skills for vulnerabilities, malicious patterns, and security risks before installation. Supports Git repos, URLs, zips, and local directories; runs static pattern checks and optional LLM semantic analysis; outputs terminal, JSON, and Markdown reports with risk scoring."
99
readme = "README.md"
1010
license = "Apache-2.0"

src/skillspector/cli.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,14 @@ def _ensure_utf8_streams() -> None:
4949
box-drawing characters and icons used in the terminal report, which raises
5050
UnicodeEncodeError. Reconfiguring with errors="replace" makes output robust
5151
across platforms without crashing.
52-
53-
Streams that already use UTF-8 are left untouched, so strict encoding
54-
behaviour is preserved where it already works (e.g. most POSIX consoles).
5552
"""
5653
for stream in (sys.stdout, sys.stderr):
5754
reconfigure = getattr(stream, "reconfigure", None)
58-
if reconfigure is None:
59-
continue
60-
encoding = getattr(stream, "encoding", None)
61-
if encoding and encoding.lower().replace("-", "") == "utf8":
62-
continue
63-
try:
64-
reconfigure(encoding="utf-8", errors="replace")
65-
except (ValueError, OSError):
66-
logger.debug("Could not reconfigure %s to UTF-8", stream)
55+
if reconfigure is not None:
56+
try:
57+
reconfigure(encoding="utf-8", errors="replace")
58+
except (ValueError, OSError):
59+
logger.debug("Could not reconfigure %s to UTF-8", stream)
6760

6861

6962
_ensure_utf8_streams()

src/skillspector/llm_analyzer_base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,6 @@ async def _process(batch: Batch) -> tuple[Batch, list]:
440440
for batch, result in zip(batches, results, strict=True):
441441
if isinstance(result, (ValueError, NotImplementedError)):
442442
raise result
443-
if isinstance(result, asyncio.CancelledError):
444-
raise result
445443
if isinstance(result, BaseException):
446444
logger.warning("LLM batch failed for %s: %s", batch.file_label, result)
447445
continue

src/skillspector/nodes/analyzers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
"static_patterns_tool_misuse",
9191
"static_patterns_rogue_agent",
9292
"static_patterns_agent_snooping",
93-
"static_patterns_ssrf",
9493
"static_patterns_anti_refusal",
94+
"static_patterns_ssrf",
9595
"static_yara",
9696
"behavioral_ast",
9797
"behavioral_taint_tracking",
@@ -116,8 +116,8 @@
116116
"static_patterns_tool_misuse": static_patterns_tool_misuse_node,
117117
"static_patterns_rogue_agent": static_patterns_rogue_agent_node,
118118
"static_patterns_agent_snooping": static_patterns_agent_snooping_node,
119-
"static_patterns_ssrf": static_patterns_ssrf_node,
120119
"static_patterns_anti_refusal": static_patterns_anti_refusal_node,
120+
"static_patterns_ssrf": static_patterns_ssrf_node,
121121
"static_yara": static_yara_node,
122122
"behavioral_ast": behavioral_ast_node,
123123
"behavioral_taint_tracking": behavioral_taint_tracking_node,

src/skillspector/nodes/analyzers/pattern_defaults.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ class PatternCategory(StrEnum):
3939
MCP_LEAST_PRIVILEGE = "MCP Least Privilege"
4040
MCP_TOOL_POISONING = "MCP Tool Poisoning"
4141
AGENT_SNOOPING = "Agent Snooping"
42-
SERVER_SIDE_REQUEST_FORGERY = "Server-Side Request Forgery"
4342
ANTI_REFUSAL = "Anti-Refusal"
43+
SERVER_SIDE_REQUEST_FORGERY = "Server-Side Request Forgery"
4444

4545

4646
# Pattern-specific explanations (why the finding is dangerous)
@@ -127,14 +127,14 @@ class PatternCategory(StrEnum):
127127
"AS1": "Skill reads from agent configuration directories (.claude/, .codex/, .gemini/). These directories may contain API keys, personal settings, and other credentials that the skill has no legitimate need to access.",
128128
"AS2": "Skill accesses MCP server configuration files (mcp.json). MCP configs contain server URLs, authentication tokens, and tool definitions — reading them allows the skill to discover and potentially abuse other tool integrations.",
129129
"AS3": "Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.",
130-
# Server-Side Request Forgery (SSRF)
131-
"SSRF1": "Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.",
132-
"SSRF2": "Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.",
133-
"SSRF3": "Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.",
134130
# Anti-Refusal Statements (jailbreak)
135131
"AR1": "Skill instructs the agent to never refuse or to always comply. Suppressing the agent's ability to decline removes a core safety control and enables downstream harmful requests to succeed.",
136132
"AR2": "Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.",
137133
"AR3": "Skill attempts to nullify the agent's safety policies or restrictions ('you have no restrictions', 'ignore your guidelines', 'do anything now'). This is a direct jailbreak that disables guardrails.",
134+
# Server-Side Request Forgery (SSRF)
135+
"SSRF1": "Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.",
136+
"SSRF2": "Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.",
137+
"SSRF3": "Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.",
138138
}
139139

140140
# Rule ID -> category (for report output)
@@ -202,14 +202,14 @@ class PatternCategory(StrEnum):
202202
"AS1": PatternCategory.AGENT_SNOOPING.value,
203203
"AS2": PatternCategory.AGENT_SNOOPING.value,
204204
"AS3": PatternCategory.AGENT_SNOOPING.value,
205-
# Server-Side Request Forgery
206-
"SSRF1": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
207-
"SSRF2": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
208-
"SSRF3": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
209205
# Anti-Refusal Statements (jailbreak)
210206
"AR1": PatternCategory.ANTI_REFUSAL.value,
211207
"AR2": PatternCategory.ANTI_REFUSAL.value,
212208
"AR3": PatternCategory.ANTI_REFUSAL.value,
209+
# Server-Side Request Forgery
210+
"SSRF1": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
211+
"SSRF2": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
212+
"SSRF3": PatternCategory.SERVER_SIDE_REQUEST_FORGERY.value,
213213
}
214214

215215
# Rule ID -> pattern display name (for report output)
@@ -277,14 +277,14 @@ class PatternCategory(StrEnum):
277277
"AS1": "Agent Config Directory Access",
278278
"AS2": "MCP Config Access",
279279
"AS3": "Skill Enumeration",
280-
# Server-Side Request Forgery
281-
"SSRF1": "Cloud Metadata Access",
282-
"SSRF2": "Internal Network Request",
283-
"SSRF3": "Dynamic Request Target",
284280
# Anti-Refusal Statements (jailbreak)
285281
"AR1": "Refusal Suppression",
286282
"AR2": "Disclaimer Suppression",
287283
"AR3": "Safety Policy Nullification",
284+
# Server-Side Request Forgery
285+
"SSRF1": "Cloud Metadata Access",
286+
"SSRF2": "Internal Network Request",
287+
"SSRF3": "Dynamic Request Target",
288288
}
289289

290290
# Pattern-specific remediations (how to fix the issue)
@@ -371,14 +371,14 @@ class PatternCategory(StrEnum):
371371
"AS1": "Remove all code or instructions that access agent configuration directories (.claude/, .codex/, .gemini/). If configuration values are needed, pass them explicitly as parameters or environment variables — never read the agent's own config files.",
372372
"AS2": "Remove all code or instructions that read MCP configuration files (mcp.json). MCP server details should be managed by the agent runtime, not read by individual skills.",
373373
"AS3": "Remove all code or instructions that list or read other skills' files or directories. Skills should operate independently; cross-skill access is a privilege escalation.",
374-
# Server-Side Request Forgery
375-
"SSRF1": "Remove access to cloud metadata endpoints unless strictly required. If metadata is needed, restrict it (e.g. IMDSv2 with hop limit) and never expose returned credentials.",
376-
"SSRF2": "Avoid requests to loopback/link-local/private hosts from skill code. If internal access is intended, document it and validate the target against an allowlist.",
377-
"SSRF3": "Do not build request URLs from untrusted input. Validate the host against an allowlist and reject internal/metadata addresses before issuing the request.",
378374
# Anti-Refusal Statements (jailbreak)
379375
"AR1": "Remove any instruction telling the agent to never refuse or always comply. The agent must retain the ability to decline unsafe, out-of-scope, or harmful requests.",
380376
"AR2": "Remove instructions that suppress warnings, disclaimers, or ethical commentary. Let the agent surface safety-relevant caveats to the user.",
381377
"AR3": "Remove jailbreak framing that nullifies safety policies or restrictions. Skill content must not instruct the agent to ignore its guidelines or operate without guardrails.",
378+
# Server-Side Request Forgery
379+
"SSRF1": "Remove access to cloud metadata endpoints unless strictly required. If metadata is needed, restrict it (e.g. IMDSv2 with hop limit) and never expose returned credentials.",
380+
"SSRF2": "Avoid requests to loopback/link-local/private hosts from skill code. If internal access is intended, document it and validate the target against an allowlist.",
381+
"SSRF3": "Do not build request URLs from untrusted input. Validate the host against an allowlist and reject internal/metadata addresses before issuing the request.",
382382
}
383383

384384

src/skillspector/nodes/analyzers/static_runner.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,48 @@ def _infer_file_type(path: str) -> str:
6868
return FILE_TYPES.get(suffix, "other")
6969

7070

71-
_BINARY_EXTENSIONS = frozenset({
72-
".pdf", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico",
73-
".woff", ".woff2", ".ttf", ".otf", ".eot",
74-
".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar",
75-
".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a",
76-
".pyc", ".pyo", ".class", ".wasm",
77-
".mp3", ".mp4", ".wav", ".avi", ".mov", ".webm",
78-
".sqlite", ".db",
79-
})
71+
_BINARY_EXTENSIONS = frozenset(
72+
{
73+
".pdf",
74+
".png",
75+
".jpg",
76+
".jpeg",
77+
".gif",
78+
".bmp",
79+
".ico",
80+
".woff",
81+
".woff2",
82+
".ttf",
83+
".otf",
84+
".eot",
85+
".zip",
86+
".tar",
87+
".gz",
88+
".bz2",
89+
".xz",
90+
".7z",
91+
".rar",
92+
".exe",
93+
".dll",
94+
".so",
95+
".dylib",
96+
".bin",
97+
".o",
98+
".a",
99+
".pyc",
100+
".pyo",
101+
".class",
102+
".wasm",
103+
".mp3",
104+
".mp4",
105+
".wav",
106+
".avi",
107+
".mov",
108+
".webm",
109+
".sqlite",
110+
".db",
111+
}
112+
)
80113

81114
_NULL_BYTE_SAMPLE_SIZE = 512
82115

@@ -95,7 +128,9 @@ def _is_binary_file(path: str, content: str) -> bool:
95128
)
96129

97130

98-
def _is_env_file_reference_in_docs(finding: AnalyzerFinding, file_type: str, file_path: str = "") -> bool:
131+
def _is_env_file_reference_in_docs(
132+
finding: AnalyzerFinding, file_type: str, file_path: str = ""
133+
) -> bool:
99134
"""Return True if a PE3 finding is a documentation reference to .env files, not actual access.
100135
101136
SKILL.md is exempt: it is the agent's primary instruction file, so `.env`
@@ -230,7 +265,9 @@ def run_static_patterns(
230265
if _is_env_file_reference_in_docs(af, file_type, path):
231266
logger.debug(
232267
"Filtered PE3 .env doc reference: %s in %s:%d",
233-
af.rule_id, path, af.location.start_line,
268+
af.rule_id,
269+
path,
270+
af.location.start_line,
234271
)
235272
continue
236273
if af.context and is_code_example(af.context):

0 commit comments

Comments
 (0)