Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/skillspector/nested_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import annotations

import io
import re
import stat
import struct
import time
Expand Down Expand Up @@ -382,6 +383,38 @@ def _zip_member_is_link(info: zipfile.ZipInfo) -> bool:
b"\xbf\xba\xfe\xca",
)

_TYPESCRIPT_DECLARATION_SUFFIXES = (".d.ts", ".d.cts", ".d.mts")
_TYPESCRIPT_DECLARATION_MARKERS = re.compile(
r"\b(?:declare|interface|type|import\s+type|export\s+"
r"(?:declare|interface|type|namespace))\b"
)
_TYPESCRIPT_RUNTIME_MARKERS = re.compile(
r"\b(?:require|eval|Function|module\.exports|process\.|console\."
r"|fetch|new|setTimeout|setInterval|exec(?:Sync)?|spawn(?:Sync)?|"
r"child_process)\b|\b(?:const|let|var)\s+[A-Za-z_$]"
r"[\w$]*\s*=|(?:^|[;{}])\s*[A-Za-z_$][\w$]*\s*\([^)]*\)"
)


def _looks_like_typescript_declaration(path: str, data: bytes) -> bool:
"""Recognize clearly inert TypeScript declaration content conservatively.

Declaration suffixes alone are not trusted: a file named ``evil.d.cts``
can still contain executable CommonJS. Unknown or non-text content stays
executable so this check cannot create a name-based security bypass.
"""
name = Path(path).name.lower()
if not name.endswith(_TYPESCRIPT_DECLARATION_SUFFIXES) or not data:
return False
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
return False
text = re.sub(r"/\*.*?\*/|//[^\r\n]*", "", text, flags=re.DOTALL)
if not text.strip() or _TYPESCRIPT_RUNTIME_MARKERS.search(text):
return False
return bool(_TYPESCRIPT_DECLARATION_MARKERS.search(text))


def has_binary_executable_magic(data: bytes) -> bool:
"""Return whether canonical bytes begin with supported executable magic."""
Expand All @@ -392,7 +425,12 @@ def is_executable_content(path: str, data: bytes, mode: int = 0) -> bool:
"""Classify filesystem and archive content with one static-only policy."""
suffix = Path(path).suffix.lower()
executable_magic = data.startswith(b"#!") or has_binary_executable_magic(data)
return suffix in _EXECUTABLE_SUFFIXES or executable_magic or bool(mode & 0o111)
declaration_only = _looks_like_typescript_declaration(path, data)
return (
(suffix in _EXECUTABLE_SUFFIXES and not declaration_only)
or executable_magic
or bool(mode & 0o111)
)


def _member_executable(info: zipfile.ZipInfo, safe_name: str, data: bytes) -> bool:
Expand Down
14 changes: 14 additions & 0 deletions tests/nodes/test_nested_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
_apply_inventory_overrides,
_mark_inventory_exception,
inspect_nested_artifacts,
is_executable_content,
)
from skillspector.nodes.analyzers.static_patterns_supply_chain import (
_analyze_concealed_executables,
Expand Down Expand Up @@ -66,6 +67,19 @@ def _document_members(**extra: bytes) -> dict[str, bytes]:
}


def test_typescript_declaration_files_are_not_executable_by_name_alone() -> None:
declaration = b"export interface Options { retries?: number; }\nexport type Result = string;\n"

assert not is_executable_content("types.d.cts", declaration)
assert not is_executable_content("types.d.mts", declaration)


def test_runtime_code_in_typescript_declaration_named_file_stays_executable() -> None:
runtime = b'declare const marker: string;\nrequire("child_process").execSync(marker);\n'

assert is_executable_content("evil.d.cts", runtime)


def _with_unsupported_compression(data: bytes, method: int = 99) -> bytes:
encoded = bytearray(data)
local_header = encoded.find(b"PK\x03\x04")
Expand Down
Loading