#!/usr/bin/env python3
"" "
Regression tests for skill packaging security behavior.
"" "
import sys
import tempfile
import types
import zipfile
from pathlib import Path
from unittest import TestCase, main
from unittest.mock import patch
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0 , str(SCRIPT_DIR))
fake_quick_validate = types.ModuleType("quick_validate" )
fake_quick_validate.validate_skill = lambda _path: (True , "Skill is valid!" )
original_quick_validate = sys.modules.get("quick_validate" )
sys.modules["quick_validate" ] = fake_quick_validate
import package_skill as package_skill_module
from package_skill import package_skill
if original_quick_validate is not None :
sys.modules["quick_validate" ] = original_quick_validate
else :
sys.modules.pop("quick_validate" , None )
class TestPackageSkillSecurity(TestCase):
def setUp(self):
self.temp_dir = Path(tempfile.mkdtemp(prefix="test_skill_" ))
def tearDown(self):
import shutil
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def create_skill(self, name="test-skill" ):
skill_dir = self.temp_dir / name
skill_dir.mkdir(parents=True , exist_ok=True )
(skill_dir / "SKILL.md" ).write_text("---\nname: test-skill\ndescription: test\n---\n" )
(skill_dir / "script.py" ).write_text("print('ok')\n" )
return skill_dir
def test_packages_normal_files(self):
skill_dir = self.create_skill("normal-skill" )
out_dir = self.temp_dir / "out"
out_dir.mkdir()
result = package_skill(str(skill_dir), str(out_dir))
self.assertIsNotNone(result)
skill_file = out_dir / "normal-skill.skill"
self.assertTrue(skill_file.exists())
with zipfile.ZipFile(skill_file, "r" ) as archive:
names = set(archive.namelist())
self.assertIn("normal-skill/SKILL.md" , names)
self.assertIn("normal-skill/script.py" , names)
def test_skips_symlink_to_external_file(self):
skill_dir = self.create_skill("symlink-file-skill" )
outside = self.temp_dir / "outside-secret.txt"
outside.write_text("super-secret\n" )
link = skill_dir / "loot.txt"
out_dir = self.temp_dir / "out"
out_dir.mkdir()
try :
link.symlink_to(outside)
except (OSError, NotImplementedError):
self.skipTest("symlink unsupported on this platform" )
result = package_skill(str(skill_dir), str(out_dir))
self.assertIsNotNone(result)
skill_file = out_dir / "symlink-file-skill.skill"
self.assertTrue(skill_file.exists())
with zipfile.ZipFile(skill_file, "r" ) as archive:
names = set(archive.namelist())
self.assertIn("symlink-file-skill/SKILL.md" , names)
self.assertIn("symlink-file-skill/script.py" , names)
self.assertNotIn("symlink-file-skill/loot.txt" , names)
def test_skips_symlink_directory(self):
skill_dir = self.create_skill("symlink-dir-skill" )
outside_dir = self.temp_dir / "outside"
outside_dir.mkdir()
(outside_dir / "secret.txt" ).write_text("secret\n" )
link = skill_dir / "docs"
out_dir = self.temp_dir / "out"
out_dir.mkdir()
try :
link.symlink_to(outside_dir, target_is_directory=True )
except (OSError, NotImplementedError):
self.skipTest("symlink unsupported on this platform" )
result = package_skill(str(skill_dir), str(out_dir))
self.assertIsNotNone(result)
skill_file = out_dir / "symlink-dir-skill.skill"
with zipfile.ZipFile(skill_file, "r" ) as archive:
names = set(archive.namelist())
self.assertIn("symlink-dir-skill/SKILL.md" , names)
self.assertIn("symlink-dir-skill/script.py" , names)
self.assertNotIn("symlink-dir-skill/docs/secret.txt" , names)
def test_rejects_resolved_path_outside_skill_root(self):
skill_dir = self.create_skill("escape-skill" )
out_dir = self.temp_dir / "out"
out_dir.mkdir()
original_within = package_skill_module._is_within
def fake_is_within(path_obj: Path, root: Path):
if path_obj.name == "script.py" :
return False
return original_within(path_obj, root)
with patch.object(package_skill_module, "_is_within" , fake_is_within):
result = package_skill(str(skill_dir), str(out_dir))
self.assertIsNone(result)
def test_allows_nested_regular_files(self):
skill_dir = self.create_skill("nested-skill" )
nested = skill_dir / "lib" / "helpers"
nested.mkdir(parents=True , exist_ok=True )
(nested / "util.py" ).write_text("def run():\n return 1\n" )
out_dir = self.temp_dir / "out"
out_dir.mkdir()
result = package_skill(str(skill_dir), str(out_dir))
self.assertIsNotNone(result)
skill_file = out_dir / "nested-skill.skill"
with zipfile.ZipFile(skill_file, "r" ) as archive:
names = set(archive.namelist())
self.assertIn("nested-skill/lib/helpers/util.py" , names)
def test_skips_output_archive_when_output_dir_is_skill_dir(self):
skill_dir = self.create_skill("self-output-skill" )
result = package_skill(str(skill_dir), str(skill_dir))
self.assertIsNotNone(result)
skill_file = skill_dir / "self-output-skill.skill"
self.assertTrue(skill_file.exists())
with zipfile.ZipFile(skill_file, "r" ) as archive:
names = set(archive.namelist())
self.assertIn("self-output-skill/SKILL.md" , names)
self.assertIn("self-output-skill/script.py" , names)
self.assertNotIn("self-output-skill/self-output-skill.skill" , names)
if __name__ == "__main__" :
main()
Messung V0.5 in Prozent C=98 H=96 G=96
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland