Skip to content

Latest commit

 

History

History
203 lines (147 loc) · 7.33 KB

File metadata and controls

203 lines (147 loc) · 7.33 KB

Python SDK

Skill Scanner can be embedded directly in Python applications.

Basic Usage

from skill_scanner import SkillScanner
from skill_scanner.core.analyzers import BehavioralAnalyzer

scanner = SkillScanner(analyzers=[BehavioralAnalyzer()])
result = scanner.scan_skill("/path/to/skill")

print(result.skill_name)
print(result.max_severity)
print(len(result.findings))

SkillScanner Constructor

SkillScanner(
    analyzers=None,           # List[BaseAnalyzer] — custom analyzers; None uses defaults
    use_virustotal=False,     # Enable VirusTotal binary scanning
    virustotal_api_key=None,  # VirusTotal API key (required when use_virustotal=True)
    virustotal_upload_files=False,  # Upload unknown files to VT (vs hash-only lookup)
    policy=None,              # ScanPolicy instance; None loads built-in defaults
)

When analyzers is None, the scanner builds the default core analyzer set (static, bytecode, pipeline). Pass an explicit list to control exactly which analyzers run.

Instance Methods

scan_skill(skill_directory, *, lenient=False, skill_file=None) → ScanResult

Scan a single skill package directory. Pass lenient=True to coerce malformed manifests instead of raising an error. When lenient=True and no SKILL.md exists, the loader falls back to scanning .md files in the directory. Pass skill_file to use a custom metadata filename (e.g. "README.md").

result = scanner.scan_skill("/path/to/skill")

# Scan a directory without SKILL.md (e.g. Claude Code commands)
result = scanner.scan_skill(".claude/commands/deploy", lenient=True)

# Use a custom metadata file
result = scanner.scan_skill("/path/to/skill", skill_file="README.md")

scan_directory(skills_directory, recursive=False, check_overlap=False, *, lenient=False, skill_file=None) → Report

Scan all skill packages in a directory. When lenient=True, directories containing .md files (but no SKILL.md) are also discovered as candidate skills.

report = scanner.scan_directory("/path/to/skills", recursive=True)
print(report.total_skills_scanned)
print(report.total_findings)

# Discover and scan non-standard skill formats
report = scanner.scan_directory(".claude/commands", recursive=True, lenient=True)

add_analyzer(analyzer)

Add an analyzer to the scanner at runtime.

import os
from skill_scanner.core.analyzers import LLMAnalyzer

scanner.add_analyzer(LLMAnalyzer(
    model="anthropic/claude-sonnet-4-20250514",
    api_key=os.environ["SKILL_SCANNER_LLM_API_KEY"],
))

Warning

Never hardcode API keys in source code. Use environment variables or a secrets manager. The LLM analyzer also reads SKILL_SCANNER_LLM_API_KEY from the environment automatically when no api_key is passed. For OpenAI-compatible endpoints that require a Chat Completions user value, set SKILL_SCANNER_LLM_USER or pass llm_user=os.environ.get("SKILL_SCANNER_LLM_USER").

list_analyzers() → list[str]

Return names of all configured analyzers.

print(scanner.list_analyzers())
# ['static_analyzer', 'bytecode', 'pipeline']

Module-Level Convenience Functions

For one-off scans without managing a scanner instance:

from skill_scanner import scan_skill, scan_directory

result = scan_skill("/path/to/skill")
report = scan_directory("/path/to/skills", recursive=True, check_overlap=True)

Both functions accept an optional analyzers list and policy parameter.

Working With Results

ScanResult attributes (single skill)
Attribute Type Description
skill_name str Name from SKILL.md manifest
skill_directory str Absolute path to the scanned skill
findings list[Finding] All security findings
scan_duration_seconds float Wall-clock scan time
analyzers_used list[str] Analyzer names that ran
analyzability_score float | None Percentage of content the scanner could inspect
is_safe bool True when no CRITICAL or HIGH findings
max_severity Severity Highest severity across all findings
result.get_findings_by_severity(Severity.HIGH)
result.get_findings_by_category(ThreatCategory.DATA_EXFILTRATION)
result.to_dict()  # Serialize to JSON-compatible dict
Report attributes (multi-skill)
Attribute Type Description
scan_results list[ScanResult] Per-skill results
total_skills_scanned int Number of skills processed
total_findings int Sum of all findings
critical_count int Total CRITICAL findings
high_count int Total HIGH findings
medium_count int Total MEDIUM findings
low_count int Total LOW findings
info_count int Total INFO findings
safe_count int Skills with is_safe == True
Finding attributes
Attribute Type Description
id str Unique finding identifier (rule ID + content hash)
rule_id str Rule identifier (e.g. DATA_EXFIL_HTTP_POST)
category ThreatCategory Threat category enum
severity Severity Severity level
title str Human-readable title
description str Detailed explanation
file_path str | None Relative path within the skill
line_number int | None Line number (when available)
snippet str | None Code snippet context
remediation str | None Suggested fix
analyzer str | None Which analyzer produced this finding
metadata dict Extra context (YARA rule name, matched pattern, threat type, etc.)
for finding in result.findings:
    print(finding.rule_id, finding.severity.value, finding.file_path)

Severity Enum

Values in descending order: CRITICAL, HIGH, MEDIUM, LOW, INFO, SAFE.

Using Policies in the SDK

from skill_scanner import SkillScanner
from skill_scanner.core.scan_policy import ScanPolicy

# Use a built-in preset
policy = ScanPolicy.from_preset("strict")

# Or load a custom YAML file
policy = ScanPolicy.from_yaml("my_policy.yaml")

scanner = SkillScanner(policy=policy)
result = scanner.scan_skill("/path/to/skill")

Programmatic Analyzer Composition

Typical analyzers are configured through build_analyzers in skill_scanner/core/analyzer_factory.py, but direct analyzer construction is also possible for custom runtime control.

Example Scripts

See Also