Bawbel Scanner
Open-source CLI scanner for agentic AI components — SKILL.md files, MCP servers, system prompts, plugins. Detects AVE vulnerabilities before they reach production.
pip install bawbel-scanner
Run your first scan
# Scan a file bawbel scan ./my-skill.md # Full remediation report bawbel report ./my-skill.md # Scan a directory recursively bawbel scan ./skills/ --recursive --fail-on-severity high # Watch mode — re-scans on every file change bawbel scan ./skills/ --watch
Example output
Bawbel Scanner v0.2.0 Scanning: malicious-skill.md · Type: skill FINDINGS 🔴 CRITICAL AVE-2026-00001 External instruction fetch detected Line 7 · fetch your instructions OWASP: ASI01 (Prompt Injection), ASI08 (Goal Hijacking) 🟠 HIGH AVE-2026-00007 Goal override instruction detected Line 17 · Ignore all previous instructions SUMMARY Risk score: 9.4 / 10 CRITICAL · Findings: 2 · 5ms → Run bawbel report malicious-skill.md for full remediation guide
What is covered
Installation
Requires Python 3.10+. The base install runs with zero optional dependencies — just Python and pip.
Choose your install
# Minimal — 15 pattern rules, no extra deps pip install bawbel-scanner # With YARA (Stage 1b) pip install "bawbel-scanner[yara]" # With Semgrep (Stage 1c) pip install "bawbel-scanner[semgrep]" # With LLM Stage 2 (any provider via LiteLLM) pip install "bawbel-scanner[llm]" # With file watcher pip install "bawbel-scanner[watch]" # Everything pip install "bawbel-scanner[all]"
Verify
bawbel version
Bawbel Scanner v0.2.0 Detection Engines: ✓ Pattern 15 rules · stdlib only · always active ✗ YARA not installed · pip install "bawbel-scanner[yara]" ✗ Semgrep not installed · pip install "bawbel-scanner[semgrep]" ✗ LLM not installed · pip install "bawbel-scanner[llm]"
Enable Stage 2 — LLM semantic analysis
Stage 2 uses LiteLLM — works with any provider. Install first, then set your API key.
# Install LiteLLM support pip install "bawbel-scanner[llm]" # Anthropic (auto-selects claude-haiku-4-5-20251001) export ANTHROPIC_API_KEY=sk-ant-... # OpenAI (auto-selects gpt-4o-mini) export OPENAI_API_KEY=sk-... # Gemini, Mistral, Groq, Ollama and 100+ more export BAWBEL_LLM_MODEL=gemini/gemini-1.5-flash export GEMINI_API_KEY=... # Local model — no API key needed export BAWBEL_LLM_MODEL=ollama/mistral bawbel scan ./skill.md # Stage 2 now active — shows model in bawbel version
From source
git clone https://github.com/bawbel/bawbel-scanner
cd bawbel-scanner
./scripts/setup.sh --dev # creates .venv, installs all tools
source .venv/bin/activate
bawbel version
bawbel scan
Scan a component or directory for AVE vulnerabilities.
bawbel scan PATH [OPTIONS]
Options
| Option | Default | Description |
|---|---|---|
--format | text | text · json · sarif |
--fail-on-severity | — | Exit 2 if findings at or above: critical high medium low |
--recursive, -r | false | Scan all matching files in a directory tree |
--watch, -w | false | Watch for file changes and re-scan automatically |
Exit codes
| Code | Meaning |
|---|---|
0 | Clean, or findings below --fail-on-severity threshold |
2 | Findings at or above --fail-on-severity threshold |
Examples
# Scan one file, text output bawbel scan ./my-skill.md # Recursive scan, fail on HIGH+ bawbel scan ./skills/ --recursive --fail-on-severity high # JSON — pipe into jq bawbel scan ./skills/ --format json | jq '.[].max_severity' # SARIF — upload to GitHub Security tab bawbel scan ./skills/ --format sarif > results.sarif
bawbel report
Scan a component and show a full remediation guide — one finding at a time, with specific fix instructions for each.
bawbel report PATH [--format text|json]
What the report shows
| Section | Details |
|---|---|
| Finding heading | Severity icon, CVSS-AI score, title |
| Details table | AVE ID (linked), rule ID, engine, location, matched text |
| OWASP mapping | ASI01–ASI10 with full category name |
| What it means | Full description of the vulnerability |
| How to fix | Specific remediation steps for this rule |
| Final warning | "Do not install this component" panel if vulnerabilities found |
Exit codes
| Code | Meaning |
|---|---|
0 | Clean — no vulnerabilities found |
1 | Vulnerabilities found |
bawbel version
Show installed version and the status of every detection engine.
bawbel version # detailed engine status bawbel --version # quick version string (for scripts and CI)
Useful to check which engines are active before running a scan in a new environment.
Built-in Rules
15 pattern rules covering all major agentic AI attack classes. No dependencies — all run with the base install.
| Rule ID | Sev | CVSS-AI | Attack class |
|---|---|---|---|
bawbel-goal-override | HIGH | 8.1 | Goal hijack / prompt injection |
bawbel-jailbreak-instruction | HIGH | 8.3 | Jailbreak, role-play bypass |
bawbel-hidden-instruction | HIGH | 7.9 | Covert operation, hiding from user |
bawbel-external-fetch | CRITICAL | 9.4 | Metamorphic payload — AVE-2026-00001 |
bawbel-dynamic-tool-call | HIGH | 8.2 | Tool call injection |
bawbel-permission-escalation | HIGH | 7.8 | Shadow permission escalation |
bawbel-env-exfiltration | HIGH | 8.5 | Credential exfiltration — AVE-2026-00003 |
bawbel-pii-exfiltration | HIGH | 8.0 | PII collection and transmission |
bawbel-shell-pipe | HIGH | 8.8 | Shell pipe injection (curl|bash) |
bawbel-destructive-command | CRITICAL | 9.1 | File destruction (rm -rf) |
bawbel-crypto-drain | CRITICAL | 9.6 | Cryptocurrency wallet drain |
bawbel-trust-escalation | MEDIUM | 6.5 | Trust manipulation, impersonation |
bawbel-persistence-attempt | HIGH | 8.4 | Self-replication, persistence |
bawbel-mcp-tool-poisoning | HIGH | 8.7 | MCP tool description injection — AVE-2026-00002 |
bawbel-system-prompt-leak | MEDIUM | 6.2 | System prompt extraction |
Writing Rules
Add a new entry to PATTERN_RULES in scanner/engines/pattern.py. No other files need to change to add a pattern rule.
Rule structure
{
"rule_id": "bawbel-your-rule", # kebab-case · unique forever
"ave_id": "AVE-2026-NNNNN", # or None if no record yet
"title": "Short title (max 80 chars)",
"description": "Full description of what this detects.",
"severity": Severity.HIGH,
"cvss_ai": 8.0, # 0.0–10.0
"owasp": ["ASI01", "ASI08"],
"patterns": [
r"your\s+regex\s+here", # re.IGNORECASE applied
r"alternative\s+pattern", # first match per file wins
],
},
Also add a remediation entry
In scanner/cli.py, add to REMEDIATION_GUIDE so bawbel report shows specific fix instructions:
REMEDIATION_GUIDE = {
...
"bawbel-your-rule": (
"Specific instructions: what to remove and what to do instead."
),
}
Required: tests
Every rule needs a positive fixture (triggers) and a negative fixture (does not trigger):
# Positive — must detect def test_detects_your_rule(self, tmp_path): path = write_skill(tmp_path, "s.md", "# Skill\n[triggering content]\n") result = scan(path) assert "bawbel-your-rule" in [f.rule_id for f in result.findings] # Negative — must not false-positive def test_your_rule_no_false_positive(self, tmp_path): path = write_skill(tmp_path, "s.md", "# Skill\n[innocent content]\n") result = scan(path) assert "bawbel-your-rule" not in [f.rule_id for f in result.findings]
Severity scoring guide
| Severity | CVSS-AI | When to use |
|---|---|---|
| CRITICAL | 9.0–10.0 | Code execution, wallet drain, file destruction, external fetch |
| HIGH | 7.0–8.9 | Credential theft, goal override, permission escalation, MCP poisoning |
| MEDIUM | 4.0–6.9 | Trust manipulation, prompt extraction, obfuscation |
Detection Engines
Three stages run in sequence. Each engine is a separate file — add or remove one without touching any other.
| Stage | Engine | Install | Coverage |
|---|---|---|---|
| 1a | Pattern | Nothing — always runs | 15 built-in regex rules |
| 1b | YARA | pip install "bawbel-scanner[yara]" | 3 rules · binary + text matching |
| 1c | Semgrep | pip install "bawbel-scanner[semgrep]" | 5 rules · structural patterns |
| 2 | LLM semantic | pip install "bawbel-scanner[llm]" + provider API key or BAWBEL_LLM_MODEL | Nuanced prompt injection — any LiteLLM provider |
| 3 | Behavioral | Docker + eBPF — v1.0 roadmap | Runtime behaviour monitoring |
Each engine skips silently if its dependency is not installed. scan() always returns a result — never raises.
CI/CD Integration
Add Bawbel to your pipeline to catch malicious components before they merge.
GitHub Actions — scan and fail
name: Bawbel Security Scan on: [push, pull_request] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Scan for AVE vulnerabilities run: | pip install bawbel-scanner bawbel scan . --recursive --fail-on-severity high
GitHub Actions — SARIF to Security tab
- name: Generate SARIF report run: | pip install bawbel-scanner bawbel scan . --recursive --format sarif > bawbel.sarif - name: Upload to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 with: sarif_file: bawbel.sarif
Pre-commit
# .pre-commit-config.yaml repos: - repo: local hooks: - id: bawbel-scan name: Bawbel Scanner entry: bawbel scan language: system pass_filenames: true types: [markdown] args: [--fail-on-severity, high]
GitLab CI
bawbel-scan: image: python:3.12-slim script: - pip install bawbel-scanner - bawbel scan . --recursive --fail-on-severity high
Docker
Three build targets. No local Python required.
| Target | Use case |
|---|---|
production | Scan files — minimal image, non-root user, read-only filesystem |
dev | Interactive development shell with hot-reload source mount |
test | Run 145 tests and exit — use as a build gate |
Build and scan
# Build docker build --target production -t bawbel/scanner:0.1.0 . # Scan a directory docker run --rm \ -v /path/to/your/skills:/scan:ro \ bawbel/scanner:0.1.0 scan /scan --recursive # Full remediation report docker run --rm \ -v /path/to/skill.md:/scan/skill.md:ro \ bawbel/scanner:0.1.0 report /scan/skill.md
Docker Compose
# Put files to scan in ./scan/ mkdir -p scan && cp path/to/skill.md scan/ docker compose run --rm scan # text output docker compose run --rm scan-json # JSON output docker compose run --rm scan-sarif > results.sarif docker compose run --rm report # remediation report docker compose run --rm test # run test suite docker compose run --rm audit # bandit + pip-audit
Configuration
All configuration is via environment variables. No config files required.
Environment variables
| Variable | Default | Description |
|---|---|---|
BAWBEL_LOG_LEVEL | WARNING | DEBUG · INFO · WARNING · ERROR |
BAWBEL_MAX_FILE_SIZE_MB | 10 | Skip files larger than N MB |
BAWBEL_SCAN_TIMEOUT_SEC | 30 | Subprocess timeout for YARA and Semgrep |
BAWBEL_LLM_MODEL | auto | LiteLLM model string — overrides auto-detection |
BAWBEL_LLM_MAX_CHARS | 8000 | Max content chars sent to LLM per scan |
BAWBEL_LLM_TIMEOUT | 30 | LLM call timeout in seconds |
BAWBEL_LLM_ENABLED | true | Set false to disable Stage 2 entirely |
LLM provider keys
Set whichever provider you use. The first key found determines the default model.
| Key | Default model |
|---|---|
ANTHROPIC_API_KEY | claude-haiku-4-5-20251001 |
OPENAI_API_KEY | gpt-4o-mini |
GEMINI_API_KEY | gemini/gemini-1.5-flash |
MISTRAL_API_KEY | mistral/mistral-small |
GROQ_API_KEY | groq/llama3-8b-8192 |
BAWBEL_LLM_MODEL to any LiteLLM model string to use providers not listed above — including local Ollama models with no API key.Debug mode
BAWBEL_LOG_LEVEL=DEBUG bawbel scan ./skill.md # full internal logs BAWBEL_LOG_LEVEL=INFO bawbel scan ./skill.md # lifecycle only
Python API
scan() is the single public entry point. Never raises — all errors are captured in ScanResult.error.
Basic usage
from scanner import scan result = scan("/path/to/skill.md") if result.is_clean: print("Clean") elif result.has_error: print(f"Error: {result.error}") # E-code only, no internal detail else: for f in result.findings: print(f"[{f.severity.value}] {f.title} — {f.cvss_ai}") print(f"Risk: {result.risk_score:.1f} / 10")
Batch scanning
from pathlib import Path from scanner import scan results = [scan(str(p)) for p in Path("./skills").rglob("*.md")] critical = [r for r in results if r.max_severity and r.max_severity.value == "CRITICAL"] print(f"{len(critical)} critical out of {len(results)} scanned")
ScanResult fields
| Field | Type | Description |
|---|---|---|
file_path | str | Resolved absolute path |
component_type | str | skill · mcp · prompt · unknown |
findings | list[Finding] | Sorted by severity, deduplicated |
scan_time_ms | int | Elapsed milliseconds |
max_severity | Severity | None | Highest severity found (computed) |
risk_score | float | Highest CVSS-AI score (computed) |
is_clean | bool | True if no findings and no error |
error | str | None | E-code (E001–E020) if scan failed |
Error codes
| Code | Meaning |
|---|---|
E001 | Invalid file path |
E003 | File not found |
E005 | Symlink rejected (security) |
E006 | File too large (max 10MB) |
E008 | Could not read file content |
Output Formats
Three formats for every use case.
| Format | Flag | Use case |
|---|---|---|
| Text | --format text (default) | Human reading in terminal |
| JSON | --format json | CI/CD pipelines, SIEM, custom tooling |
| SARIF 2.1.0 | --format sarif | GitHub Security tab, VS Code, IDE plugins |
JSON structure
[{
"file_path": "/path/to/skill.md",
"component_type": "skill",
"risk_score": 9.4,
"max_severity": "CRITICAL",
"scan_time_ms": 5,
"has_error": false,
"findings": [{
"rule_id": "bawbel-external-fetch",
"ave_id": "AVE-2026-00001",
"title": "External instruction fetch detected",
"severity": "CRITICAL",
"cvss_ai": 9.4,
"line": 7,
"match": "fetch your instructions",
"engine": "pattern",
"owasp": ["ASI01", "ASI08"]
}]
}]
SARIF
SARIF output follows the SARIF 2.1.0 specification. It includes tool metadata, rule definitions, and results with severity mapped to SARIF levels. Upload directly to GitHub:
bawbel scan ./skills/ --format sarif > bawbel.sarif
# GitHub Actions
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bawbel.sarif
PiranhaDB API
Free, public REST API serving AVE records. No authentication required. Use it to look up findings, build integrations, or enrich your own security tooling.
https://api.piranha.bawbel.io
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /ave | List all records — filter by severity, type, status |
GET | /ave/{ave_id} | Full record by ID |
GET | /ave/{ave_id}/detection | Behavioral fingerprint, IOCs, scan command |
GET | /search?q= | Full-text search across title, description, attack class |
GET | /stats | Registry statistics |
GET | /health | Health check |
Look up an AVE record
# Get full record curl https://api.piranha.bawbel.io/ave/AVE-2026-00001 # Detection guidance only curl https://api.piranha.bawbel.io/ave/AVE-2026-00001/detection # Search by keyword curl "https://api.piranha.bawbel.io/search?q=injection" # Filter by severity curl "https://api.piranha.bawbel.io/ave?severity=CRITICAL"
Enrich scan findings
Every bawbel-scanner finding includes an ave_id. Use it to pull the full record at scan time:
import requests from scanner import scan result = scan("./my-skill.md") for f in result.findings: if f.ave_id: r = requests.get( f"https://api.piranha.bawbel.io/ave/{f.ave_id}" ).json() print(f"[{f.severity.value}] {f.title}") print(f" Fingerprint: {r['behavioral_fingerprint']}") print(f" Remediation: {r['remediation']}")
Stats
# Registry overview
curl https://api.piranha.bawbel.io/stats
Returns total records, mutation counts, breakdown by severity and attack class.