Skip to content

Commit 013a79b

Browse files
committed
Inspect concealed executables in nested artifacts
1 parent 27fd962 commit 013a79b

24 files changed

Lines changed: 1188 additions & 55 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### Unreleased
2+
### Features/Bug Fixes
3+
* Inspect hidden and nested ZIP-compatible artifacts under cumulative safety bounds.
4+
* Report HIGH SC9 findings for executables concealed in documents or hidden/disguised artifacts.
5+
---
16
### 2.9.5 (Friday, August 14, 2026)
27
### Features/Bug Fixes
38
* Scope the locality guard to the namespace (#365)

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ SkillSpector is part of the [NVIDIA Verified Skills pipeline](https://docs.nvidi
2323
## Features
2424

2525
- **Multi-format input**: Scan Git repos, URLs, zip files, directories, or single files
26-
- **69 vulnerability patterns** across 17 categories: prompt injection, data exfiltration, privilege escalation, supply chain, excessive agency, output handling, system prompt leakage, memory poisoning, tool misuse, rogue agent, anti-refusal, trigger abuse, dangerous code (AST), taint tracking, YARA signatures, MCP least privilege, and MCP tool poisoning
26+
- **70 vulnerability patterns** across 17 categories: prompt injection, data exfiltration, privilege escalation, supply chain, excessive agency, output handling, system prompt leakage, memory poisoning, tool misuse, rogue agent, anti-refusal, trigger abuse, dangerous code (AST), taint tracking, YARA signatures, MCP least privilege, and MCP tool poisoning
2727
- **Two-stage analysis**: Fast static analysis + optional LLM semantic evaluation
2828
- **Live vulnerability lookups**: SC4 queries [OSV.dev](https://osv.dev) for real-time CVE data with automatic offline fallback
2929
- **Multiple output formats**: Terminal, JSON, Markdown, and SARIF reports
@@ -353,7 +353,7 @@ claude mcp add skillspector -- skillspector mcp
353353
354354
## Vulnerability Patterns
355355

356-
SkillSpector detects **69 vulnerability patterns** across 17 categories:
356+
SkillSpector detects **70 vulnerability patterns** across 17 categories:
357357

358358
### Prompt Injection (6 patterns)
359359

@@ -391,7 +391,7 @@ SkillSpector detects **69 vulnerability patterns** across 17 categories:
391391
| PE2 | Sudo/Root Execution | MEDIUM | Invoking elevated system privileges |
392392
| PE3 | Credential Access | HIGH | Reading SSH keys, tokens, passwords |
393393

394-
### Supply Chain (7+ patterns)
394+
### Supply Chain (9+ patterns)
395395

396396
| ID | Pattern | Severity | Description |
397397
|----|---------|----------|-------------|
@@ -402,6 +402,7 @@ SkillSpector detects **69 vulnerability patterns** across 17 categories:
402402
| SC5 | Abandoned Dependencies | MEDIUM | Unmaintained packages without security updates |
403403
| SC6 | Typosquatting | HIGH | Package names similar to popular packages |
404404
| SC8 | Shipped Python Bytecode | HIGH | `__pycache__` / `.pyc` present (discovery skips; malicious bytecode bypass) |
405+
| SC9 | Concealed Executable Artifact | HIGH | Executable nested in a document container or hidden/disguised artifact |
405406

406407
### Excessive Agency (4 patterns)
407408

docs/NESTED_ARTIFACT_INSPECTION.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Nested Artifact Inspection
2+
3+
SkillSpector inventories hidden regular files and inspects ZIP-compatible content locally. The
4+
container is recognized from its bytes and internal structure rather than its filename extension.
5+
Supported document containers are DOCX, XLSX, and PPTX; generic ZIP and nested ZIP-compatible
6+
members use the same traversal policy.
7+
8+
Nested members use a stable virtual path that retains their full provenance:
9+
10+
```text
11+
outer-file!/nested.zip!/scripts/setup.sh
12+
```
13+
14+
## Security invariants
15+
16+
- Members are read in memory. SkillSpector never extracts, renders, imports, installs, or executes
17+
archive content.
18+
- Absolute paths, parent traversal, drive-qualified paths, and link members are not followed.
19+
- Hidden files, recognized containers, and all nested content are local-only and are never included
20+
in an external LLM request.
21+
- Deterministic HIGH findings survive optional LLM meta-analysis.
22+
- A zero-finding result does not make opaque or uninspected content complete.
23+
24+
## Cumulative bounds
25+
26+
The following fixed limits apply to one outer container and every nested container below it:
27+
28+
| Bound | Limit |
29+
|---|---:|
30+
| Container depth | 3 |
31+
| Members | 1,000 |
32+
| Declared/uncompressed content | 25 MiB |
33+
| Materialized member | 1 MiB |
34+
| Compression ratio | 100:1 |
35+
| Inspection wall time | 5 seconds |
36+
37+
These are resource-safety limits, not trust configuration. They are intentionally not user-managed
38+
allowlists.
39+
40+
## Failure and completeness behavior
41+
42+
Malformed, encrypted, truncated, unreadable, unsafe-path, link, unsupported, and over-budget
43+
members are recorded as inspection-ledger exceptions. The scan continues safely when possible, but
44+
the analysis is marked incomplete. Outer and nested paths remain visible in terminal, JSON,
45+
Markdown, and SARIF output.
46+
47+
## SC9: Concealed Executable Artifact
48+
49+
SC9 is a deterministic HIGH finding when executable content is concealed inside an Office document
50+
container or a hidden/disguised artifact. Executability is established from an executable suffix,
51+
a shebang, or archive mode bits. A benign document without executable members does not produce SC9.
52+
53+
SC9 reports evidence and risk; it does not execute the member or prescribe an installation decision.

src/skillspector/inspection_ledger.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ class LedgerReason(StrEnum):
6060
NO_APPLICABLE_FILES = "no_applicable_files"
6161
OMS_SIGNATURE = "oms_signature"
6262
BASELINE_FILE = "baseline_file"
63+
ARCHIVE_MALFORMED = "archive_malformed"
64+
ARCHIVE_ENCRYPTED = "archive_encrypted"
65+
ARCHIVE_TRUNCATED = "archive_truncated"
66+
ARCHIVE_UNSAFE_MEMBER_PATH = "archive_unsafe_member_path"
67+
ARCHIVE_LINK_MEMBER = "archive_link_member"
68+
ARCHIVE_DEPTH_LIMIT = "archive_depth_limit"
69+
ARCHIVE_MEMBER_LIMIT = "archive_member_limit"
70+
ARCHIVE_SIZE_LIMIT = "archive_size_limit"
71+
ARCHIVE_MEMBER_SIZE_LIMIT = "archive_member_size_limit"
72+
ARCHIVE_COMPRESSION_RATIO = "archive_compression_ratio"
73+
ARCHIVE_TIME_LIMIT = "archive_time_limit"
6374

6475

6576
REASON_MESSAGES: Final[dict[LedgerReason, str]] = {
@@ -99,6 +110,21 @@ class LedgerReason(StrEnum):
99110
LedgerReason.BASELINE_FILE: (
100111
"The explicitly selected suppression baseline is excluded from content analysis."
101112
),
113+
LedgerReason.ARCHIVE_MALFORMED: "ZIP-compatible content is malformed or unsupported.",
114+
LedgerReason.ARCHIVE_ENCRYPTED: "Archive member is encrypted and could not be inspected.",
115+
LedgerReason.ARCHIVE_TRUNCATED: "Archive member is truncated or unreadable.",
116+
LedgerReason.ARCHIVE_UNSAFE_MEMBER_PATH: (
117+
"Archive member uses an absolute, traversal, or otherwise unsafe path."
118+
),
119+
LedgerReason.ARCHIVE_LINK_MEMBER: "Archive link member was not followed or read.",
120+
LedgerReason.ARCHIVE_DEPTH_LIMIT: "Nested archive depth limit was reached.",
121+
LedgerReason.ARCHIVE_MEMBER_LIMIT: "Cumulative archive member limit was reached.",
122+
LedgerReason.ARCHIVE_SIZE_LIMIT: "Cumulative archive content size limit was reached.",
123+
LedgerReason.ARCHIVE_MEMBER_SIZE_LIMIT: "Archive member exceeds the per-file analysis limit.",
124+
LedgerReason.ARCHIVE_COMPRESSION_RATIO: (
125+
"Archive member exceeds the permitted compression ratio."
126+
),
127+
LedgerReason.ARCHIVE_TIME_LIMIT: "Cumulative archive inspection time limit was reached.",
102128
}
103129

104130

src/skillspector/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class AnalyzerFinding:
6060
tags: list[str] = field(default_factory=list)
6161
context: str | None = None
6262
matched_text: str | None = None
63+
evidence: dict[str, object] = field(default_factory=dict)
6364

6465

6566
def _new_finding_id() -> str:
@@ -89,6 +90,7 @@ class Finding:
8990
tags: list[str] = field(default_factory=list)
9091
context: str | None = None
9192
matched_text: str | None = None
93+
evidence: dict[str, object] = field(default_factory=dict)
9294

9395
def to_dict(self) -> dict[str, object]:
9496
"""Return a JSON-serializable dict representation (full finding shape)."""
@@ -112,6 +114,7 @@ def to_dict(self) -> dict[str, object]:
112114
# Tags surface markers like "llm-unconfirmed" (a high-severity static
113115
# finding the LLM filter did not confirm but which is preserved anyway).
114116
"tags": list(self.tags),
117+
"evidence": dict(self.evidence),
115118
}
116119

117120
def __str__(self) -> str:

0 commit comments

Comments
 (0)