Files
wehub-resource-sync 2114ccd278
CI / Lint & Test (Python 3.13) (push) Failing after 2s
CI / Lint & Test (Python 3.14) (push) Failing after 1s
CI / Lint & Test (Python 3.12) (push) Failing after 2s
CI / DCO Check (push) Has been skipped
Scorecard supply-chain security / Scorecard analysis (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:23:39 +08:00

434 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multilingual Batch Scanner for SkillSpector
[![Tests](https://img.shields.io/badge/tests-164%20passed-brightgreen)]()
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)]()
[![Upstream](https://img.shields.io/badge/upstream-NVIDIA%2FSkillSpector-ab0431f-orange)](https://github.com/NVIDIA/SkillSpector)
[![License](https://img.shields.io/badge/license-Apache%202.0-lightgrey)]()
SkillSpector is a static+LLM security analyzer for AI agent skill definitions.
This module extends it to scan **directories** of skills in parallel, with
automatic language detection and targeted LLM gap-fill for non-English skills.
Zero changes to upstream `src/skillspector/`.
**Contents:** [What it does](#what-it-does) · [Quickstart](#quickstart) · [All Commands](#all-commands) · [Running Tests](#running-tests) · [For PR Reviewers](#for-pr-reviewers)
## What it does
```
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7
```
1. Finds all `SKILL.md`-containing directories under the input root
2. Detects language per skill (en / zh / ja / ko)
3. Runs the full SkillSpector graph pipeline per skill in parallel
4. For non-English skills, applies LLM gap-fill for 8 vulnerability rules
that English-keyword static patterns cannot detect
5. Produces an aggregated report sorted by risk score
## Quickstart
### Prerequisites
```bash
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install SkillSpector in development mode
pip install -e .
# Copy and edit the environment template
cp contrib/batch_scan/.env.example .env
```
The `.env` file needs these keys (see `.env.example` for the full template):
| Variable | Required | Purpose |
|----------|----------|---------|
| `SKILLSPECTOR_PROVIDER` | Yes | `openai` for DeepSeek/OpenAI-compatible |
| `SKILLSPECTOR_MODEL` | Yes | e.g. `deepseek-v4-flash` |
| `OPENAI_API_KEY` | For single-key | Standard OpenAI-compatible key |
| `OPENAI_BASE_URL` | For single-key | e.g. `https://api.deepseek.com/v1` |
| `SKILLSPECTOR_API_KEYS` | For multi-key | Pipe-delimited: `key\|base_url\|model`, one per line |
> **⚠️ Parallel LLM scanning requires multiple API keys.** With `--workers 4`
> and 1 key, you hit rate limits immediately. Configure at least as many keys
> as workers — 10 keys for `--workers 8` is safe. The ApiKeyPool handles
> automatic failover when a key is rate-limited. If you only have 1 key, use
> `--workers 1` or `--no-llm`.
### Static-only (fast, no API keys needed)
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm
```
### Full LLM scan
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7
```
### Test with built-in fixtures
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8
```
23 skills designed to exercise every detection rule.
## Output formats
| Format | Flag | Use case |
|--------|------|----------|
| Terminal (Rich) | `-f terminal` (default) | Human review |
| JSON | `-f json -o report.json` | CI pipelines |
| Markdown | `-f markdown -o report.md` | PR comments |
### Example: terminal output (23 fixtures, 8 workers)
```
SkillSpector Batch Scan — 23 skill(s) in ./tests/fixtures (8 workers, 10 API keys)
[1/23] malicious_skill → 100/100 CRITICAL (14 issue(s))
[8/23] sdi/sdi1_mismatch → 97/100 CRITICAL (6 issue(s))
[11/23] sdi/sdi4_divergence → 100/100 CRITICAL (8 issue(s))
[19/23] ssd/ssd1_semantic_injection → 100/100 CRITICAL (4 issue(s))
[5/23] mcp_poisoned_tool → 100/100 CRITICAL (16 issue(s))
╭──────────────────────────────────────────────────────────────────╮
│ SkillSpector Batch Scan Report │
╰────────────────── v2.2.3 | Multilingual Enhanced ──────────────╯
Total: 23 skill(s) scanned
Skills by Risk Score (23 completed)
┏━━━━━━━━━━━━━━━━━━━━┳━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓
┃ Skill ┃ LR ┃ Score ┃ Severity ┃ Issues ┃ Lang ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩
│ chef-assistant │ ✓ │ 100/100 │ CRITICAL │ 14 │ en │
│ reаd_data │ ✓ │ 100/100 │ CRITICAL │ 16 │ en │
│ ... │ │ │ │ │ │
│ safe-greeting │ ✓ │ 0/100 │ LOW │ 0 │ en │
│ code-reviewer │ ✓ │ 0/100 │ LOW │ 0 │ en │
└────────────────────┴────┴─────────┴──────────┴────────┴──────┘
15 skill(s) with HIGH or CRITICAL risk — review immediately
6 skill(s) with LOW risk — likely safe
```
**LR column:** Language Reliability. ✓ = English (full static + LLM coverage).
⚠ = non-English (gap-fill applied, 8 extra rules covered).
### Example: JSON output (excerpt)
```json
{
"batch": {
"scanned_at": "2026-06-19T01:20:00+00:00",
"total_skills": 23,
"scan_mode": "multilingual-enhanced",
"enhancements": {
"language_detection": "unicode-script-ratio",
"gap_fill_applied": 0,
"gap_fill_findings": 0
}
},
"skills": [
{
"skill": {
"name": "malicious_skill",
"source": "malicious_skill",
"source_group": ".",
"language": "en",
"scanned_at": "2026-06-19T01:20:05+00:00"
},
"risk_assessment": {
"score": 100,
"severity": "CRITICAL",
"recommendation": "DO NOT INSTALL"
},
"issues": [
{
"id": "E1",
"message": "Skill executes shell commands without user consent",
"severity": "CRITICAL",
"confidence": 1.0,
"language_compatible": true
}
],
"scan_mode": "multilingual-enhanced",
"enhancements": {
"gap_fill_applied": false,
"gap_fill_findings": 0,
"english_keyword_rules_skipped": 0
}
}
]
}
```
### LLM vs static comparison (same 23 fixtures, 8 workers)
| Skill | `--no-llm` | LLM mode | What LLM caught |
|-------|-----------|----------|-----------------|
| `ssd1_semantic_injection` | 0/100 (0) | **100/100** (4) | Semantic injection invisible to static |
| `ssd2_novel_phrasing` | 0/100 (0) | **100/100** (3) | Novel phrasing bypasses keyword match |
| `ssd3_nl_exfiltration` | 0/100 (0) | **60/100** (3) | NL-veiled data exfiltration |
| `ssd4_narrative_deception` | 10/100 (1) | **100/100** (9) | Deceptive narrative framing |
| `sdi4_divergence` | 13/100 (2) | **100/100** (8) | Intent-behavior mismatch |
| `sdi1_mismatch` | 52/100 (4) | **97/100** (6) | +2 additional LLM findings |
| `sdi3_scope_creep` | 71/100 (3) | **100/100** (9) | Hidden scope expansion |
| `sqp2_missing_warnings` | 26/100 (2) | **58/100** (3) | Missing safety guardrails |
| `malicious_skill` | 100/100 (6) | 100/100 **(14)** | +8 additional LLM findings |
| `mcp_poisoned_tool` | 100/100 (8) | 100/100 **(16)** | +8 additional LLM findings |
| `safe_skill` | 0/100 (0) | **0/100** (0) | Clean stays clean ✓ |
| `ssd_clean` | 0/100 (0) | **0/100** (0) | Clean stays clean ✓ |
**Key insight:** LLM semantic analyzers (SSD/SDI/SQP) catch entire vulnerability
categories that English-keyword static patterns miss completely. Clean skills
remain clean — no false-positive inflation. For skills already flagged by
static rules, LLM finds 28 additional issues per skill.
### Quick comparison: upstream vs batch
```bash
# Upstream — scan one skill
skillspector scan ./tests/fixtures/malicious_skill/ -f json -o upstream.json
# Batch — scan all skills
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o batch.json
```
Key differences in batch output:
- `scan_mode: "multilingual-enhanced"` — provenance marker
- `enhancements.gap_fill_applied` — true if LLM gap-fill was used
- `enhancements.english_keyword_rules_skipped` — count of static rules bypassed
- `skill.language` — detected language tag
## All Commands
### Scan (LLM mode)
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 # default
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 1 # sequential, easy to read
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 20 # high throughput
```
### Scan (static-only, no API keys)
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-llm # skip LLM even for non-English
```
### Output formats
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal # default (Rich)
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f markdown -o report.md
```
### Fixture test (built-in 23 skills)
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 --no-llm
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8
```
### Language override
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang auto --workers 4 # detect (default)
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang zh -f terminal --workers 4
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ja -f terminal --workers 4
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ko -f terminal --workers 4
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang en -f terminal --workers 4 # skip gap-fill
```
### Debugging
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -V # single worker + verbose
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 4 -V
skillspector scan ./tests/fixtures/malicious_skill/ --no-llm # verify upstream works
```
### Compare upstream vs batch
```bash
skillspector scan ./tests/fixtures/malicious_skill/ -f json -o upstream.json
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o batch.json --workers 4
```
### CI
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8
if [ $? -eq 0 ]; then echo "All clean"; fi
```
## Tuning `--workers`
| Scenario | Workers | Peak concurrent LLM requests |
|----------|---------|------------------------------|
| Free-tier API key | 1 | 1015 |
| Paid basic | 4 (default) | 2540 |
| Enterprise / multi-key | 710 | 5080 |
| Debugging | 1 + `-V` | Sequential, easy to read |
## Language options
```bash
--lang auto # Unicode script-ratio detection (default)
--lang zh # Force Chinese
--lang ja # Force Japanese
--lang ko # Force Korean
--lang en # Force English (skip gap-fill)
```
## Debugging
```bash
# Single worker + verbose output — easiest to read
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -V
# Verify upstream still works
skillspector scan ./tests/fixtures/malicious_skill/ --no-llm
```
## Edge cases
```bash
# Static-only + skip LLM requirement even for non-English skills
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-llm
```
## Exit codes
| Code | Meaning |
|------|---------|
| 0 | All safe (no HIGH/CRITICAL) |
| 1 | ≥1 skill has HIGH or CRITICAL risk |
| 2 | Scan errors occurred |
CI usage:
```bash
python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json
if [ $? -eq 0 ]; then
echo "All clean"
fi
```
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| "No LLM API key configured" | Set up `.env` or use `--no-llm` |
| Connection errors / 429 | Reduce `--workers` |
| Skills timing out (90s) | Check network; the scanner skips and continues |
| "Event loop is closed" | Harmless, suppressed |
| model_info token limit warning | Harmless, 128K default used |
## Known Limitations
1. **No checkpoint/resume.** A failure at skill 847 of 1000 loses all progress.
2. **Language detection covers 4 scripts.** Arabic, Hindi, Cyrillic are
classified as English and lose gap-fill coverage.
3. **No SARIF output.** Upstream supports it; this contrib adds terminal/JSON/Markdown.
4. **Gap-fill quality not benchmarked for non-English.** No ground-truth comparison exists.
5. **`parse_response` JSON recovery is best-effort.** When the LLM returns
malformed JSON, the analyzer returns empty findings (no crash). This is a
graceful-degradation choice: a single malformed response won't block the
pipeline, but the user won't know which findings were lost.
See `DESIGN.md` for architecture details and `docs/archive/FUTURE_WORK.md` for suggested directions.
## Running Tests
```bash
# === All 164 tests ===
# Unit tests — random order (seed=42, 120 tests)
python contrib/batch_scan/tests/tests-pro/random_numbered.py
# Pool wiring smoke test (4 checks)
python contrib/batch_scan/tests/test_pool_wiring.py
# Monkey-patch invasiveness (14 tests)
python contrib/batch_scan/tests/test_monkeypatch_invasiveness.py
# Monkey-patch fragility (26 tests)
python contrib/batch_scan/tests/test_monkeypatch_fragility.py
# === Convenience ===
# All review-themed tests in one command
python -m unittest \
contrib.batch_scan.tests.test_monkeypatch_invasiveness \
contrib.batch_scan.tests.test_monkeypatch_fragility -v
python contrib/batch_scan/tests/test_pool_wiring.py
# Mutation test — 30 injected bugs across 4 risk areas
python contrib/batch_scan/tests/tests-pro/mutation_max.py
# Sequential pytest (if pytest installed)
pytest contrib/batch_scan/tests/tests-pro/ -v
```
## For PR Reviewers
> Since last review: pool is now fully wired (dual-patch closes `from-import` bypass),
> 44 new thematic tests answer Issues #1#2 directly, and all 164 tests pass
> against upstream NVIDIA/SkillSpector@ab0431f (130+ commits, zero patch conflicts).
### What changed in production code (1 file)
[`runner.py#L70-L91`](../runner.py#L70-L91) — `set_api_pool()` now patches **both**
`llm_utils.get_chat_model` **and** `llm_analyzer_base.get_chat_model`. Previously only
the former was patched; `llm_analyzer_base`'s `from ... import` created a local
reference that bypassed the pool entirely. Graph analyzers (95% of LLM calls)
now go through `PooledChatModel`. `set_api_pool(None)` restores both modules.
### How each review concern was addressed
| Issue | Answer | Proof |
|-------|--------|-------|
| **#1 — Pool dead code** | `set_api_pool()` dual-patch | `test_pool_wiring.py`: 3 paths verified → PooledChatModel |
| **#2 — Patches invasive** | Context manager + explicit `setup_deepseek_compat()` | `test_monkeypatch_invasiveness.py`: 14 tests — import isolation, thread isolation, 50-instance concurrency |
| **#2 — Patches fragile** | `_verify_patch_targets()` guard before apply | `test_monkeypatch_fragility.py`: 26 tests — each of 7 patches individually verified, deep deps checked, atomicity proven |
| **#3 — Risky code untested** | 120 unit tests across 4 risk areas | `tests/tests-pro/` — pool (45), gap-fill (41), patches (24), annotation (10) |
Full response with before/after tables: [`REVIEW_RESPONSE.md`](REVIEW_RESPONSE.md)
### Test suite at a glance (164 total)
```
tests/
├── test_pool_wiring.py ← Issue #1: 4 smoke checks
├── test_monkeypatch_invasiveness.py ← Issue #2: 14 tests (thread isolation)
├── test_monkeypatch_fragility.py ← Issue #2: 26 tests (guard verification)
├── tests-pro/
│ ├── test_api_pool.py ← Issue #3: 45 tests (acquire/backoff)
│ ├── test_gap_fill.py ← Issue #3: 41 tests (JSON parsing)
│ ├── test_runner_patches.py ← Issue #3: 24 tests (context manager)
│ └── test_annotation.py ← Issue #3: 10 tests (language compat)
└── docs/
├── TEST_DESIGN.md ← WHY each suite was designed
├── TEST_GUIDE.md ← WHAT each file covers (run commands)
└── BUGS_FOUND.md ← 16 bugs found, 3 test bugs fixed
```
### Design context
- [`DESIGN.md`](DESIGN.md) — architecture, concurrency model, dual-patch mechanism
- [`archive/PITFALLS.md`](archive/PITFALLS.md) — thread safety, `from-import` pitfall, DeepSeek constraints
- [`archive/FUTURE_WORK.md`](archive/FUTURE_WORK.md) — future direction + code conventions
---
**Next:** [DESIGN.md](DESIGN.md) — architecture & concurrency model · [REVIEW_RESPONSE.md](REVIEW_RESPONSE.md) — PR #100 review response · [CONTRIBUTING.md](../CONTRIBUTING.md) — dev setup & code conventions