Skip to content
Cisco AI Defense logo
CiscoAI Security

Python SDK — Skill Scanner

Python SDK

Embed skill scanning directly in Python applications. The SDK provides SkillScanner, convenience functions, and typed models for findings, results, and reports.


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,
    use_virustotal=False,
    virustotal_api_key=None,
    virustotal_upload_files=False,
    policy=None,
)
ParameterTypeDescription
analyzerslist[BaseAnalyzer] or NoneCustom analyzer list; None uses defaults (static + bytecode + pipeline)
use_virustotalboolEnable VirusTotal binary scanning
virustotal_api_keystr or NoneVirusTotal API key (required when enabled)
virustotal_upload_filesboolUpload unknown files to VT (vs hash-only lookup)
policyScanPolicy or NoneScan policy; None loads built-in defaults

Instance Methods

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

Scan a single skill package directory.

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.

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

# Cross-skill overlap analysis
report = scanner.scan_directory("/path/to/skills", recursive=True, check_overlap=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"],
))

list_analyzers() -> list[str]

Return names of all configured analyzers.

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

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 optional analyzers and policy parameters.


Working with Results

ScanResult (Single Skill)

AttributeTypeDescription
skill_namestrName from SKILL.md manifest
skill_directorystrAbsolute path to the scanned skill
findingslist[Finding]All security findings
scan_duration_secondsfloatWall-clock scan time
analyzers_usedlist[str]Analyzer names that ran
analyzability_scorefloat or NonePercentage of content inspected
is_safeboolTrue when no CRITICAL or HIGH findings
max_severitySeverityHighest severity across all findings
result.get_findings_by_severity(Severity.HIGH)
result.get_findings_by_category(ThreatCategory.DATA_EXFILTRATION)
result.to_dict()

Report (Multi-Skill)

AttributeTypeDescription
scan_resultslist[ScanResult]Per-skill results
total_skills_scannedintNumber of skills processed
total_findingsintSum of all findings
critical_countintTotal CRITICAL findings
high_countintTotal HIGH findings
safe_countintSkills with is_safe == True

Finding

AttributeTypeDescription
idstrUnique finding identifier
rule_idstrRule identifier (e.g., DATA_EXFIL_HTTP_POST)
categoryThreatCategoryThreat category enum
severitySeveritySeverity level
titlestrHuman-readable title
descriptionstrDetailed explanation
file_pathstr or NoneRelative path within the skill
line_numberint or NoneLine number (when available)
snippetstr or NoneCode snippet context
remediationstr or NoneSuggested fix
analyzerstr or NoneWhich analyzer produced this
metadatadictExtra context (YARA rule, matched pattern, 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

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")

Example Scripts