A fully local, automated security and performance auditing tool for WordPress plugins. Provide a WordPress.org plugin URL or a .zip file — the tool downloads, extracts, scans with multiple analysis engines, and generates a comprehensive report.
# Scan any WordPress.org plugin by URL
python3 /var/www/bug-b/check.py --url https://wordpress.org/plugins/contact-form-7/
# Scan a local plugin zip
python3 /var/www/bug-b/check.py --zip /path/to/my-plugin.zipReports are saved to /var/www/bug-b/reports/{plugin-slug}_{timestamp}/
Tools are already installed. To verify or reinstall:
# Install/update all tools (PHPStan, PHPMD, Semgrep)
bash /var/www/bug-b/tools/install.sh
# Verify all tools are functional
bash /var/www/bug-b/tools/verify.sh| Tool | Version | Purpose |
|---|---|---|
| PHP | 8.1 | Runtime |
| PHPCS | 3.10.2 | Static analysis engine |
| WordPress Coding Standards | latest | WP security sniffs |
| VIP WordPress Coding Standards | latest | Enterprise WP sniffs |
| PHPCompatibility | latest | PHP version compatibility |
| VariableAnalysis | latest | Undefined variable detection |
| nikic/php-parser | 5.x | AST-based taint analysis |
| parallel-lint | 1.4 | PHP syntax validation |
| Python | 3.10 | Orchestration runtime |
| Tool | Purpose |
|---|---|
| PHPStan | Deep type and flow analysis |
| PHPMD | Code complexity and mess detection |
| Semgrep | Custom WordPress data-flow rules |
python3 check.py --url <wordpress-org-url> [options]
python3 check.py --zip <path-to-zip> [options]
| Flag | Default | Description |
|---|---|---|
--url URL |
— | WordPress.org plugin page or download URL |
--zip FILE |
— | Path to local plugin .zip file |
--format html,json,md |
all three | Output report formats |
--skip SCANNERS |
none | Comma-separated scanners to skip |
--output DIR |
auto | Custom directory for report output |
--timeout N |
120 | Per-scanner timeout in seconds |
--keep-extracted |
false | Keep extracted plugin source after scan |
# Scan by WordPress.org URL
python3 check.py --url https://wordpress.org/plugins/woocommerce/
# Scan a local zip, HTML report only
python3 check.py --zip ~/downloads/my-plugin.zip --format html
# Skip slow scanners for a quick result
python3 check.py --url https://wordpress.org/plugins/akismet/ --skip phpstan,phpmd,semgrep
# Save report to a specific directory
python3 check.py --zip plugin.zip --output /tmp/my-audit
# Increase timeout for large plugins
python3 check.py --url https://wordpress.org/plugins/elementor/ --timeout 300
# Keep extracted source for manual review
python3 check.py --zip plugin.zip --keep-extractedThe tool runs up to 7 scanners in parallel. Each can be individually skipped with --skip.
Runs PHP_CodeSniffer with security-focused WordPress Coding Standards sniffs.
Detects:
- SQL injection via
$wpdbwithoutprepare() - XSS via unescaped output (
echo $varwithoutesc_html()) - Missing nonce verification (
$_POSTwithoutcheck_ajax_referer()) - Missing input sanitization (
$_GETused raw) - Open redirects (
wp_redirect()without safe URL check) - Missing capability checks (
current_user_can()absent) - Dangerous PHP functions (
eval,exec,system, etc.) - File inclusion from user input
- Dynamic function calls
Runs PHP_CodeSniffer with performance-focused sniffs.
Detects:
posts_per_page: -1(loads all posts)ORDER BY RAND()(forces full table scan)- Slow meta queries with
REGEXP - Uncached remote data fetches
- Missing HTTP request timeout
- Scripts/styles enqueued incorrectly
- Cache values being overridden
- Autoloaded options abuse
Custom PHP taint tracker built on nikic/php-parser. Parses each file into an AST and tracks data flowing from user-controlled sources to dangerous sinks.
Sources tracked: $_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES, get_option(), get_post_meta()
Sinks tracked: $wpdb->query(), echo, eval(), include/require, system(), exec(), unserialize(), file_get_contents(), wp_redirect()
Sanitizers recognized (remove taint): esc_html(), esc_attr(), esc_url(), $wpdb->prepare(), absint(), intval(), sanitize_text_field(), wp_verify_nonce(), and 20+ others
Detects (with data-flow proof): SQL injection, XSS, RCE, LFI, deserialization, open redirect, backdoor obfuscation patterns
High-speed pre-scan using carefully crafted regular expressions. Catches obvious patterns before slower tools run.
Security patterns: backdoors (eval+base64/gzinflate), PHP object injection, direct SQLi, XSS echo, RCE via system functions, LFI via include, SSRF via curl, open redirect, XXE via SimpleXML, timing attacks, hardcoded credentials, variable injection via extract()
Performance patterns: unlimited post queries, ORDER BY RAND, autoloaded options, N+1 queries (get_post_meta/get_posts in loops), SELECT *, suppress_filters bypass, direct mysqli calls
Runs PHPStan at level 5 against the plugin code.
Detects: Undefined variables, incorrect function argument types, always-false conditions, dead code, unreachable statements
Skip with: --skip phpstan (can be slow on large plugins)
Runs PHP Mess Detector with code quality rules.
Detects: High cyclomatic complexity (>15), deep nesting, eval() expressions (security flag), goto statements, unused variables and parameters, excessive class/method length
Skip with: --skip phpmd
Runs Semgrep with a custom YAML ruleset tuned specifically for WordPress plugin vulnerabilities.
Detects: Direct $wpdb->query() concatenation, unescaped superglobal echo, unserialize() on user input, REST API routes with __return_true permission callback, wp_redirect() with user URL, posts_per_page: -1, ORDER BY RAND(), arbitrary file read, command injection, open redirect, hardcoded credentials
Skip with: --skip semgrep
Each scan produces three report files in /var/www/bug-b/reports/{slug}_{timestamp}/:
A single portable HTML file (no CDN, no external dependencies). Open in any browser.
Contains:
- Plugin metadata (name, version, author, active installs, WP requires)
- Color-coded risk score badge (CRITICAL / HIGH / MEDIUM / LOW / CLEAN)
- Bar chart of findings by severity
- Finding cards grouped by severity with code snippets and remediation
- Category breakdown table
- Metadata warnings (suspicious files, outdated bundled libraries)
- Tool coverage matrix (which scanners ran, duration, finding counts)
Structured output for importing into other tools, dashboards, or CI pipelines.
{
"meta": {
"plugin_name": "Contact Form 7",
"plugin_slug": "contact-form-7",
"plugin_version": "6.1.5",
"plugin_author": "Rock Lobster Inc.",
"active_installs": "10000000",
"scan_date": "2026-03-20T00:29:27Z",
"total_php_files": 111,
"total_lines_of_code": 25075
},
"summary": {
"total_findings": 211,
"by_severity": { "CRITICAL": 23, "HIGH": 31, "MEDIUM": 37, "LOW": 1, "INFO": 119 },
"by_category": { "missing_capability": 33, "performance": 24, "rce": 18 },
"risk_score": 9.4,
"risk_label": "CRITICAL"
},
"findings": [
{
"id": "WPBB-001",
"title": "...",
"category": "sqli",
"severity": "CRITICAL",
"cvss_score": 9.8,
"cwe": "CWE-89",
"file": "includes/db.php",
"line": 42,
"code_snippet": "...",
"remediation": "Use $wpdb->prepare()...",
"detected_by": ["phpcs_security", "ast_taint"],
"confidence": "HIGH"
}
],
"tool_coverage": { ... }
}Clean Markdown suitable for pasting into GitHub issues, HackerOne reports, Notion, or Confluence.
Contains: Same structure as HTML — severity tables, per-finding details with file paths, code snippets, CWE/CVSS, remediation, and tool coverage.
The raw/ subdirectory contains the unprocessed JSON from each scanner for debugging or custom processing.
| Vulnerability | CVSS | Scanners |
|---|---|---|
| SQL Injection | 9.8 | phpcs_security, ast_taint, pattern_scanner, semgrep |
| Remote Code Execution | 10.0 | phpcs_security, ast_taint, pattern_scanner, semgrep |
| PHP Object Injection (unserialize) | 9.8 | ast_taint, pattern_scanner, semgrep |
| Backdoor / Obfuscated Code | 10.0 | ast_taint, pattern_scanner |
| Cross-Site Scripting (XSS) | 6.1 | phpcs_security, ast_taint, pattern_scanner, semgrep |
| CSRF / Missing Nonce | 8.8 | phpcs_security, semgrep |
| Missing Capability Check | 8.1 | phpcs_security, semgrep |
| Local File Inclusion | 7.5 | phpcs_security, ast_taint, pattern_scanner |
| SSRF | 7.5 | pattern_scanner, semgrep |
| Open Redirect | 6.1 | phpcs_security, ast_taint, semgrep |
| Arbitrary File Upload | 7.2 | pattern_scanner |
| XXE | 7.5 | pattern_scanner |
| Hardcoded Credentials | — | pattern_scanner, semgrep |
| Timing Attack (strcmp) | — | pattern_scanner |
| Issue | Scanners |
|---|---|
posts_per_page: -1 (unbounded queries) |
phpcs_performance, pattern_scanner, semgrep |
ORDER BY RAND() |
phpcs_performance, semgrep |
| N+1 queries (DB calls inside loops) | pattern_scanner |
Autoloaded options (autoload=yes) |
phpcs_performance, semgrep |
| Missing transient/cache for expensive queries | pattern_scanner |
SELECT * queries |
pattern_scanner |
| Uncached remote HTTP fetches | phpcs_performance |
suppress_filters: true (disables caching) |
pattern_scanner |
| Enqueue scripts outside proper hook | phpcs_performance |
Direct mysqli_query() bypassing $wpdb |
pattern_scanner |
| Slow cron intervals | phpcs_performance |
Central configuration. Key sections:
{
"timeout_seconds": 120,
"max_workers": 4,
"severity_map": {
"sqli": "CRITICAL",
"xss": "HIGH",
"performance": "INFO"
},
"cvss_map": {
"sqli": 9.8,
"rce": 10.0,
"xss": 6.1
}
}To change scanner binary paths, edit the *_binary keys. To adjust how PHPCS sniff codes map to vulnerability categories, edit wpcs_severity_map.
Controls which WPCS/VIP sniffs run. Add or remove <rule ref="..."/> entries to tune findings.
Same structure for performance sniffs.
Custom Semgrep rules in YAML. Add new rules following the existing pattern:
rules:
- id: my-new-rule
pattern: dangerous_function($_POST[$KEY])
message: "Description of the issue"
severity: ERROR
languages: [php]PHPMD ruleset. Adjust complexity thresholds under <properties>.
/var/www/bug-b/
├── check.py # Main entry point
├── config/
│ ├── phpcs-security.xml # PHPCS security ruleset
│ ├── phpcs-performance.xml # PHPCS performance ruleset
│ ├── phpmd-rules.xml # PHPMD ruleset
│ ├── phpstan.neon # PHPStan config
│ ├── semgrep-wordpress.yaml # Custom Semgrep rules
│ └── scanner-config.json # Master config (paths, severity maps, CVSS)
├── scanners/
│ ├── ast_scanner.php # AST taint tracker (nikic/php-parser)
│ ├── ast_runner.py # Python wrapper for ast_scanner.php
│ ├── pattern_scanner.py # Fast regex scanner
│ ├── phpcs_runner.py # PHPCS executor and parser
│ ├── phpstan_runner.py # PHPStan executor and parser
│ ├── phpmd_runner.py # PHPMD executor and parser
│ ├── semgrep_runner.py # Semgrep executor and parser
│ └── metadata_analyzer.py # Plugin structure/header/library analysis
├── reporters/
│ ├── report_builder.py # Deduplicates and aggregates findings
│ ├── html_reporter.py # Generates self-contained HTML
│ ├── json_reporter.py # Generates JSON
│ └── markdown_reporter.py # Generates Markdown
├── tools/
│ ├── install.sh # Installs PHPStan, PHPMD, Semgrep
│ ├── verify.sh # Checks all tools are functional
│ └── vendor/ # Local Composer packages (PHPStan, PHPMD)
├── workspace/
│ ├── downloads/ # Cached downloaded .zip files
│ ├── extracted/ # Extracted plugin source (auto-cleaned)
│ └── results/ # Raw scanner JSON (auto-cleaned)
└── reports/ # All generated reports (permanent)
└── {slug}_{timestamp}/
├── report.html
├── report.json
├── report.md
└── raw/ # Raw per-tool JSON output
When multiple scanners flag the same issue, they are merged into a single finding:
- Dedup key:
file_path + line_number + vulnerability_category - Confidence upgrade: If 2 or more tools agree on the same finding → confidence becomes
HIGH - Severity upgrade: If tools disagree, the most severe rating wins
- Attribution: The
detected_byarray lists all tools that found it
This eliminates noise from overlapping tools while preserving cross-tool validation signals.
| Severity | CVSS Range | Examples |
|---|---|---|
| CRITICAL | 9.0–10.0 | SQLi, RCE, deserialization, backdoor |
| HIGH | 7.0–8.9 | XSS, CSRF, missing nonce, LFI |
| MEDIUM | 4.0–6.9 | Open redirect, missing capability, unsafe upload |
| LOW | 0.1–3.9 | Hardcoded credentials, deprecated functions |
| INFO | 0.0 | Performance issues, code quality, suggestions |
PHPCS returns no results
# Test PHPCS manually
/home/krisha/.config/composer/vendor/bin/phpcs \
--standard=/var/www/bug-b/config/phpcs-security.xml \
--report=json -s /path/to/plugin/Semgrep not found
# Reinstall
pip3 install --user semgrep
# It installs to:
/home/krisha/.local/bin/semgrep --versionPHPStan / PHPMD not found
bash /var/www/bug-b/tools/install.shScan times out on a large plugin
# Increase timeout and skip slower scanners
python3 check.py --url <url> --timeout 300 --skip phpstanToo many false positives
Edit config/phpcs-security.xml and comment out noisy sniff rules with <!-- <rule ref="..."/> -->.
Plugin zip is password protected or corrupt The tool will report "Invalid zip file" and exit. Obtain a valid unprotected zip.
| Flag | Description |
|---|---|
--dir PATH |
Scan an already-extracted directory (no download/zip needed — for CI and pre-commit) |
--fail-on LEVEL |
Exit with code 1 if findings at or above this level exist. Choices: critical high medium low none (default: none) |
Blocks commits that introduce CRITICAL vulnerabilities. Fast — skips PHPStan/PHPMD, typically runs in under 10 seconds.
Install once into your plugin repo:
bash /var/www/bug-b/tools/setup-hooks.sh /path/to/your-plugin-repoThat copies ci/pre-commit-hook.sh to .git/hooks/pre-commit and makes it executable.
What happens on each git commit:
┌─────────────────────────────────────────────┐
│ WordPress Plugin Security Check (pre-commit) │
└─────────────────────────────────────────────┘
Staged PHP files: 3
Scanning: /home/user/my-plugin
[Phase 1] ...
[Phase 4] Running security scanners...
[1/4] phpcs_security: ✓ 12 findings
[2/4] phpcs_performance: ✓ 3 findings
[3/4] ast_taint: ✓ 2 findings
[4/4] pattern_scanner: ✓ 1 findings
╔══════════════════════════════════════════════════╗
║ COMMIT BLOCKED — CRITICAL security issues found ║
╚══════════════════════════════════════════════════╝
Full report: .git/security-report/report.html
Quick view: cat .git/security-report/report.md
To bypass (NOT recommended): git commit --no-verify
Configure the block threshold — edit .git/hooks/pre-commit:
FAIL_ON="critical" # or: high | medium | none
SKIP_SCANNERS="phpstan,phpmd" # keep fast for pre-commitUninstall:
rm /path/to/your-plugin/.git/hooks/pre-commitCopy ci/github-actions.yml into your plugin repo as .github/workflows/security-check.yml.
Triggers on every push to main/master/develop and on all pull requests.
What you get:
- Build fails if CRITICAL or HIGH findings exist (
--fail-on high) - Full HTML + JSON + Markdown report uploaded as a downloadable artifact
- Report content posted as a comment on the Pull Request
- Job summary shows the Markdown report inline in the Actions tab
Setup:
# In your plugin repo
mkdir -p .github/workflows
cp /var/www/bug-b/ci/github-actions.yml .github/workflows/security-check.ymlThen edit line 43 — replace the git clone URL with the actual location of this checker:
- name: Clone WP Bug Bounty Checker
run: git clone https://github.com/YOUR_ORG/bug-b.git /tmp/bug-bChange failure threshold on line 56:
--fail-on high # fail on HIGH or CRITICAL
--fail-on critical # fail only on CRITICAL
--fail-on none # never fail the build (report only)Copy ci/gitlab-ci.yml to your plugin repo as .gitlab-ci.yml (or merge into an existing one).
cp /var/www/bug-b/ci/gitlab-ci.yml /path/to/your-plugin/.gitlab-ci.ymlEdit the git clone line to point to the checker's real location:
- git clone https://github.com/YOUR_ORG/bug-b.git $CHECKER_DIRKey variables (set in GitLab CI/CD settings or edit the file):
variables:
FAIL_ON: "high" # critical | high | medium | low | none
CHECKER_DIR: "/tmp/bug-b"
REPORT_DIR: "$CI_PROJECT_DIR/security-report"The report is stored as a GitLab artifact (downloadable from the pipeline page for 30 days).
| Scenario | Exit Code | Commit/Build |
|---|---|---|
| No PHP files changed | 0 |
✓ Allowed |
| Findings below threshold | 0 |
✓ Allowed |
| Findings at/above threshold | 1 |
✗ Blocked |
| Scanner error (tool missing) | 0 |
✓ Allowed (graceful) |
Recommended thresholds:
| Environment | --fail-on |
Rationale |
|---|---|---|
| Pre-commit hook | critical |
Fast feedback, don't block developers on every warning |
| PR / Merge Request | high |
Catch serious issues before code review |
| Main branch push | critical |
Final gate before deployment |
Edit config/semgrep-wordpress.yaml:
- id: my-custom-rule
pattern: some_dangerous_function($_REQUEST[$KEY])
message: "Custom: dangerous function with user input"
severity: ERROR
languages: [php]
metadata:
category: security
cwe: CWE-XXEdit scanners/pattern_scanner.py, add to SECURITY_PATTERNS or PERFORMANCE_PATTERNS:
{
"id": "my_pattern",
"regex": r"dangerous_call\s*\(\s*\$_(GET|POST)",
"severity": "HIGH",
"category": "rce",
"title": "Dangerous call with user input",
"description": "...",
"cwe": "CWE-78",
"confidence": "HIGH",
},Edit config/scanner-config.json → wpcs_severity_map:
"MyPlugin.Security.MySniff": "sqli"