Metadata-Version: 2.4
Name: plecost
Version: 4.8.0
Summary: The best black-box WordPress security scanner
License: PolyForm-Noncommercial-1.0.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: aiosqlite>=0.19
Requires-Dist: httpx>=0.27
Requires-Dist: packaging>=24
Requires-Dist: rich>=13
Requires-Dist: sqlalchemy[asyncio]>=2.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: asyncpg>=0.29; extra == 'dev'
Requires-Dist: hypothesis>=6; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: fast
Requires-Dist: uvloop>=0.19; extra == 'fast'
Provides-Extra: postgres
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
Description-Content-Type: text/markdown

<div align="center">
  <img src="https://avatars.githubusercontent.com/u/275428243?s=200&u=235faecc7c473dc147aa16990cbada030e4703c5&v=4" alt="Plecost" width="120" />
  <h1>Plecost</h1>
  <p><strong>Professional WordPress Security Scanner</strong></p>
  <p>Async-first, library-friendly, no external API required.</p>

  [![CI](https://github.com/Plecost/plecost/actions/workflows/ci.yml/badge.svg)](https://github.com/Plecost/plecost/actions)
  [![PyPI](https://img.shields.io/pypi/v/plecost.svg)](https://pypi.org/project/plecost/)
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://python.org)
  [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fplecost%2Fplecost-blue)](https://ghcr.io/plecost/plecost)
  [![License: PolyForm NC](https://img.shields.io/badge/License-PolyForm%20NC%201.0-lightgrey)](https://polyformproject.org/licenses/noncommercial/1.0.0/)
</div>

---

## Table of Contents

- [What is Plecost?](#what-is-plecost)
- [Plecost vs WPScan](#plecost-vs-wpscan)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [CVE Database](#cve-database)
- [CVE Detection Engine](#cve-detection-engine)
- [Scanning](#scanning)
- [Detection Modules](#detection-modules)
- [WooCommerce Security](#woocommerce-security)
- [WP eCommerce Security](#wp-ecommerce-security)
- [Output Formats](#output-formats)
- [Library Usage](#library-usage)
- [Environment Variables](#environment-variables)
- [Architecture](#architecture)
- [Troubleshooting](#troubleshooting)
- [Local Test Environment](#local-test-environment)
- [License](#license)

---

## What is Plecost?

Plecost detects vulnerabilities in WordPress installations — core, plugins, and themes — and correlates findings against a daily-updated local CVE database. It runs as a CLI tool, a Python library, or inside task queues like Celery, with a consistent and automation-friendly output format.

**No Ruby. No API key. No subscription. No data sent to third parties on every scan.**


## Plecost vs WPScan

Plecost was built from scratch to fix the limitations teams hit in production when using WPScan: API rate caps, external data dependencies, no library API, no async architecture, and a narrow detection surface.

### At a Glance

| Capability | Plecost v4 | WPScan |
|---|:---:|:---:|
| **Language / runtime** | Python 3.11+ | Ruby |
| **Async concurrent scanning** | ✅ httpx + asyncio | ❌ |
| **Python library API** | ✅ `from plecost import Scanner` | ❌ |
| **API key required** | ❌ never | ⚠️ required for CVE data |
| **CVE data — free tier limit** | ✅ unlimited (local DB) | ❌ 25 API tokens/day |
| **Offline scanning (CVEs included)** | ✅ | ❌ |
| **Data sent to third parties on scan** | ❌ none | ⚠️ every request |
| **Docker native** | ✅ | ✅ |
| **Celery / task queue compatible** | ✅ | ❌ |
| **PostgreSQL support (shared team)** | ✅ | ❌ |

### Scanning Capabilities

| Capability | Plecost v4 | WPScan |
|---|:---:|:---:|
| Fast mode (top 150 plugins / 50 themes) | ✅ | ❌ |
| Deep mode (4,750+ plugins / 900+ themes) | ✅ | ✅ |
| Configurable concurrency (10–50 requests) | ✅ | ✅ |
| Stealth mode (random UA + passive only) | ✅ | ✅ |
| Aggressive mode (50 parallel requests) | ✅ | ✅ |
| Authenticated scans | ✅ | ✅ |
| Proxy support (HTTP + SOCKS5) | ✅ | ✅ |
| Bulk scan (multiple URLs from file) | ✅ | ❌ |
| Pre-flight 403 detection (aborts cleanly) | ✅ | ❌ |
| Per-module options (`--module-option`) | ✅ | ❌ |
| Run / skip individual modules | ✅ | ✅ |
| Selective module list (`--modules`) | ✅ | ❌ |
| Auto-retry with SSL verification disabled | ✅ | ❌ |

### Vulnerability Detection

| Capability | Plecost v4 | WPScan |
|---|:---:|:---:|
| WordPress core fingerprinting | ✅ (meta, readme, RSS, wp-login) | ✅ |
| Plugin detection — passive HTML | ✅ | ✅ |
| Plugin detection — active wordlist | ✅ 4,750+ slugs | ✅ |
| Theme detection — passive HTML | ✅ | ✅ |
| Theme detection — active wordlist | ✅ 900+ themes | ✅ |
| CVE correlation (core + plugins + themes) | ✅ local DB, daily NVD sync, <5% FP rate | ✅ API |
| Exploit availability flag per CVE | ✅ | ✅ |
| CVSS scores per finding | ✅ | ✅ |
| WAF / CDN detection | ✅ 7 providers | ⚠️ limited |
| User enumeration — REST API | ✅ | ✅ |
| User enumeration — author archives | ✅ | ✅ |
| XML-RPC — access, pingback DoS, method list | ✅ 3 checks | ⚠️ basic |
| REST API — disclosure, oEmbed, CORS | ✅ 3 checks | ❌ |
| HTTP security headers | ✅ 8 checks (HSTS, CSP, X-Frame…) | ❌ |
| SSL / TLS misconfiguration | ✅ 3 checks | ❌ |
| Misconfiguration (wp-config, .env, .git…) | ✅ 12 checks | ⚠️ partial |
| Directory listing (wp-content subdirs) | ✅ | ❌ |
| Debug mode / PHP version disclosure | ✅ | ❌ |
| Open user registration | ✅ | ❌ |
| Content / card skimmer analysis | ✅ scripts, iframes, hardcoded keys | ❌ |
| Webshell detection | ✅ 147–523 paths | ❌ |
| Malicious upload detection (PHP in uploads) | ✅ | ❌ |
| **WooCommerce dedicated module** | ✅ 22 checks | ❌ |
| **WP eCommerce dedicated module** | ✅ 22 checks | ❌ |
| Semi-active eCommerce CVE probes | ✅ boolean-only, no time-based | ❌ |
| WooCommerce REST API auth bypass | ✅ CVE-2023-28121 | ❌ |
| WooCommerce IDOR / PII disclosure | ✅ CVE-2023-34000 | ❌ |

### Output and Integration

| Capability | Plecost v4 | WPScan |
|---|:---:|:---:|
| Rich terminal output (color-coded) | ✅ | ✅ |
| JSON output (stable schema) | ✅ | ✅ |
| Verbose real-time progress (`-v`) | ✅ | ✅ |
| Quiet mode (HIGH + CRITICAL only) | ✅ | ❌ |
| **Stable permanent finding IDs** | ✅ 120 IDs (`PC-MOD-NNN`) | ❌ |
| `plecost explain <ID>` — per-finding remediation | ✅ | ❌ |
| Safe to track findings in JIRA / ticketing | ✅ IDs never change | ❌ |
| Remediation ID per finding (`REM-MOD-NNN`) | ✅ | ❌ |
| i18n / multilingual output | ✅ EN + ES | ❌ |
| Included vulnerable test environment (DVWP) | ✅ Docker Compose | ❌ |

### Data Independence

> WPScan sends every scan to `wpscan.com` to look up CVE data. On the free tier you get **25 API tokens per day** — enough for a handful of targets. Plecost keeps the entire CVE database locally, updated daily via GitHub Actions with no per-scan network call to any third-party API.

| | Plecost v4 | WPScan |
|---|:---:|:---:|
| Local CVE database (SQLite / PostgreSQL) | ✅ | ❌ |
| Data sent externally on each scan | ❌ none | ✅ to wpscan.com |
| CVE updates mechanism | GitHub Actions, NVD API v2 | SaaS subscription |
| Daily incremental patch download | ✅ (< 100 KB/day) | — |
| First-run full database download | ✅ `plecost update-db` | — |
| Works fully air-gapped after DB download | ✅ | ❌ |
| SHA256 integrity check before download | ✅ | ❌ |


## Quick Start

```bash
pip install plecost

# Download the CVE database (first time only — takes a few seconds)
plecost update-db

# Scan a target
plecost scan https://target.wordpress.com
```

That's it. No account, no API key, no daemon running in the background.


## Installation

**pip**

```bash
pip install plecost
pip install plecost[fast]      # adds uvloop for higher throughput
pip install plecost[postgres]  # adds asyncpg for PostgreSQL support
```

**Docker**

```bash
docker run --rm ghcr.io/plecost/plecost scan https://target.com

# Save JSON report to local directory
docker run --rm -v $(pwd):/data ghcr.io/plecost/plecost scan https://target.com \
  --output /data/report.json
```

**From source**

```bash
git clone https://github.com/Plecost/plecost.git
cd plecost
pip install -e ".[dev]"
```


## CVE Database

Plecost ships with a **local SQLite database** covering WordPress core, plugins, and themes. It lives at `~/.plecost/db/plecost.db` and is never sent to any external service during scans.

**The database needs to be downloaded once before the first scan, and kept up to date thereafter.**

### First-time setup

```bash
plecost update-db
```

This downloads a pre-built snapshot from [plecost-db releases](https://github.com/Plecost/plecost-db/releases) (~10–50 MB). Subsequent runs only download the daily diff — typically under 100 KB.

### Keeping it current

Run `update-db` regularly (weekly is fine for most use cases):

```bash
plecost update-db
```

Plecost checks a SHA256 checksum before downloading anything. If nothing changed since your last run, no data is transferred.

### How the update mechanism works

The [plecost-db](https://github.com/Plecost/plecost-db) repository runs a GitHub Actions workflow daily at 02:00 UTC. It queries the [NVD API v2.0](https://nvd.nist.gov/developers/vulnerabilities) for all WordPress-related CVEs modified in the last 24 hours, applies Jaro-Winkler fuzzy matching to correlate CVE product names against ~50,000 known plugin/theme slugs, and publishes a small JSON patch file as a release asset.

When you run `plecost update-db`, it:
1. Downloads `index.json` from the `plecost-db` releases (64 bytes)
2. Compares its SHA256 against the local copy
3. Downloads only the missing patch files and applies them in order
4. On first run, downloads `full.json` instead (complete history)

| Run | What's downloaded | Typical size |
|-----|-------------------|--------------|
| First time | `full.json` (all CVEs) | 10–50 MB |
| Daily update | today's patch | < 100 KB |
| Already up to date | nothing (checksum match) | 64 bytes |

### Custom database location

```bash
# SQLite at a custom path
export PLECOST_DB_URL=sqlite:////data/plecost.db
plecost update-db
plecost scan https://target.com

# PostgreSQL (shared team setup)
pip install plecost[postgres]
export PLECOST_DB_URL=postgresql+asyncpg://user:pass@host/plecost
plecost update-db
plecost scan https://target.com
```


## What's New in v4.8.0

### CVE evidence now includes installed version

CVE findings include the detected installed version in their `evidence` object. This makes it clear exactly which version triggered each CVE — no ambiguity about why the finding was reported.

```json
{
  "id": "PC-CVE-CVE-2024-2262",
  "evidence": {
    "cve_id": "CVE-2024-2262",
    "software": "woocommerce-products-filter",
    "version_range": "*–1.4.4",
    "installed_version": "1.3.8.2"
  }
}
```

### XML bomb protection in sitemap/RSS parsing

The users module now caps XML response size at 1 MB before parsing with `ElementTree`. Malformed or adversarially large XML responses (XML bomb DoS) are silently skipped instead of potentially exhausting memory.

---

## What's New in v4.5.0

### Brute force: explicit wordlist activation

`--brute-force` alone now **does not try any passwords**. You must explicitly request a password source:

| Before (v4.4) | After (v4.5) |
|---|---|
| `--brute-force` → uses internal ~70 passwords silently | `--brute-force` → no passwords; warns with PC-BRU-008 |
| (no `--brute-force-defaults` flag) | `--brute-force --brute-force-defaults` → internal list |

This prevents accidental brute-force attempts when `--brute-force` is added to a scan without a deliberate wordlist choice.

### run_many() callback propagation

`Scanner.run_many()` now propagates `on_finding`, `on_module_start`, `on_module_done`, and `on_module_progress` callbacks to each per-target Scanner. Previously these callbacks were silently dropped during bulk scans.

### 23 new finding IDs

v4.5.0 adds 23 new permanent finding IDs across new modules:

| Module | New finding IDs | Description |
|--------|----------------|-------------|
| `misconfigs` | PC-MCFG-013 – PC-MCFG-020 | Extended misconfiguration checks |
| `debug_exposure` | PC-DBG-002 | Extended debug checks |
| `xmlrpc` | PC-XMLRPC-007 | Additional XML-RPC check |
| `timthumb` | PC-TTH-001, PC-TTH-002, PC-TTH-003 | TimThumb vulnerability detection |
| `emergency_scripts` | PC-EMRG-001 – PC-EMRG-005 | Emergency script detection |
| `brute` (XML-RPC) | PC-BRUTE-001 – PC-BRUTE-004 | XML-RPC brute force findings |
| `wp_hardening` | PC-WPH-001 | WordPress hardening checks |
| `brute_force` | PC-BRU-008 | No wordlist configured warning |

All IDs are permanent and supported by `plecost explain <ID>`.

---

## CVE Detection Engine

> **v4.4: CVE engine reliability — 15 database bugs fixed, per-window sync checkpointing, Rich markup escaping.**
>
> **v4.3: New multi-stage normalization pipeline — from 70% false positive rate to under 5%.**

Plecost's CVE detection has always used a local database built from NVD data. What changed in v4.3 is *how* NVD vulnerability records are mapped to WordPress plugin slugs — the step where previous tools (including earlier Plecost versions) silently generate false positives.

### What's new in v4.4

v4.4 is a reliability release focused on the CVE database engine:

- **Rejected CVEs now rehabilitable**: re-upserting a previously-deleted CVE removes it from the suppression list automatically (G2-003)
- **Upsert-wins-delete in same patch**: a CVE listed in both `upsert[]` and `delete[]` of the same daily patch is kept, not suppressed (G2-002)
- **Date poisoning fixed**: malformed dates in patch files no longer corrupt `last_patch_date`, which previously caused all subsequent patches to be silently ignored (G-002)
- **Batch deduplication**: duplicate `(cve_id, slug)` pairs within a single patch no longer cause `IntegrityError` rollbacks (G-012)
- **theme.vulns populated**: the CVE module now surfaces known vulnerabilities for detected themes, matching what it already did for plugins (G2-005)
- **Single `rejected_ids` query per scan**: previously, `SELECT rejected_cves` was executed once per plugin (300+ queries in fast-scan mode); now fetched once and reused (G-016)
- **`run_many()` respects custom DB**: bulk scans (`-T urls.txt`) now correctly use a custom `--db` path instead of falling back to the default location (G2-001)
- **Incremental sync checkpointing**: `sync-db` now saves progress after each completed 90-day NVD window; a transient HTTP 429 no longer forces a full re-sync from scratch (AC-13)
- **NVD 429 retry with backoff**: rate-limit responses from the NVD API trigger automatic retry with exponential backoff instead of silent data loss (AC-14)
- **44 new tests**: `_is_affected()` boundary coverage, `find_all_by_slug()`, `rejected_cves` filtering, end-to-end CVE pipeline integration tests; total coverage 84.4%

### The Problem with Fuzzy Matching

NVD names plugins using CPE syntax (`element_pack`, `word_replacer_pro`). WordPress.org identifies them by slug (`bdthemes-element-pack-lite`, `word-replacer-ultra`). These are rarely the same string.

Previous approaches used fuzzy string matching (Jaro-Winkler, Levenshtein) to bridge this gap. The problem: phonetic similarity is not semantic correctness. `element_pack` scores 0.88 against `elementor` — higher than the actual match `bdthemes-element-pack-lite` (0.73). The result: Wordfence installed → CVEs for a completely different plugin reported. False positive.

### The v4.3 Solution: Cascaded Deterministic Signals

The new engine uses a priority cascade, stopping at the first high-confidence signal:

```
1. Canonical exact match   → contact_form_7  →  contact-form-7      [conf=1.0]
2. URL reference extraction → CVE refs often  →  explicit slug in URL [conf=0.95]
   (65-70% of WordPress CVEs in NVD contain direct wordpress.org/plugins/{slug} links)
3. Jaro-Winkler ≥ 0.90    → stricter threshold eliminates known FPs  [conf=score]
4. No match               → CVE not inserted  (silence > false alarm)
```

Signal #2 is the key insight: **65-70% of WordPress CVEs in NVD already contain the correct slug** in their reference URLs (`plugins.trac.wordpress.org/.../bdthemes-element-pack-lite/`). Extracting it directly gives certainty — not approximation.

### Results

Measured against a live WooCommerce site with 40+ installed plugins:

| | Before (v4.2) | After (v4.3) |
|---|---|---|
| Total findings | 41 | 12 |
| False positives | 29 (71%) | 0 |
| Accuracy | 29% | 100% |

The 12 remaining findings were all independently verified as true positives via manual HTTP requests.


## Scanning

### Basic scan

```
$ plecost scan https://target.com

  Plecost v4.4 — WordPress Security Scanner
  Target: https://target.com

  WordPress 6.4.2 detected  |  WAF: Cloudflare

  Plugins (3)
    woocommerce        8.2.1    VULNERABLE
    contact-form-7     5.8      OK
    elementor          3.17.0   OK

  Findings (7)
    PC-CVE-CVE-2023-28121   WooCommerce SQLi                       CRITICAL
    PC-SSL-001              HTTP does not redirect to HTTPS         HIGH
    PC-HDR-001              Missing Strict-Transport-Security       MEDIUM
    PC-USR-001              User enumeration via REST API           MEDIUM
    PC-XMLRPC-001           XML-RPC interface accessible            MEDIUM
    PC-REST-001             REST API user data exposed              LOW
    PC-MCFG-009             readme.html discloses WP version        LOW

  Summary: 1 Critical  1 High  3 Medium  2 Low  |  Duration: 4.2s
```

### Common options

```bash
# Authenticated scan
plecost scan https://target.com --user admin --password secret

# Route traffic through Burp Suite or OWASP ZAP
plecost scan https://target.com --proxy http://127.0.0.1:8080

# Run only specific detection modules
plecost scan https://target.com --modules fingerprint,plugins,cves

# Aggressive mode — 50 parallel requests (use on internal targets)
plecost scan https://target.com --aggressive

# Deep mode — full wordlist (4750+ plugins, 900+ themes); default scans top 150/50
plecost scan https://target.com --deep

# Stealth mode — random UA, passive detection only, slower
plecost scan https://target.com --stealth

# Save results as JSON
plecost scan https://target.com --output report.json

# Show only HIGH and CRITICAL findings
plecost scan https://target.com --quiet
```

### All scan flags

| Flag | Description | Default |
|------|-------------|---------|
| `--concurrency N` | Parallel requests | 10 |
| `--timeout N` | Request timeout (seconds) | 10 |
| `--proxy URL` | HTTP or SOCKS5 proxy | — |
| `--user / -u` | WordPress username | — |
| `--password / -p` | WordPress password | — |
| `--modules` | Modules to run (comma-separated) | all |
| `--skip-modules` | Modules to skip | — |
| `--stealth` | Passive mode, random UA, slower pacing | off |
| `--aggressive` | Max concurrency (50 requests) | off |
| `--output / -o` | JSON output file | — |
| `--no-verify-ssl` | Skip certificate verification | off |
| `--force` | Scan even if WordPress not detected | off |
| `--deep` | Full wordlist scan (4750+ plugins, 900+ themes); default is top 150/50 | off |
| `--verbose / -v` | Real-time module progress and findings during scan | off |
| `--quiet` | Show only HIGH and CRITICAL findings | off |
| `--module-option` | Module-specific option: `MODULE:KEY=VALUE` (repeatable) | — |
| `--brute-force` | Enable brute force module (requires `--brute-force-defaults` or `--brute-force-wordlist`) | off |
| `--brute-force-defaults` | Use built-in password list (~70 passwords + username variants) | off |
| `--brute-force-wordlist <path>` | External password file (one per line) | — |
| `--brute-force-combine` | Merge external wordlist with internal list (requires `--brute-force-defaults`) | off |


## Detection Modules

Plecost runs **19 async modules** in parallel, wired through an explicit dependency graph. Modules without interdependencies run concurrently from the start; `cves` waits for `plugins` and `themes` to complete before correlating results against the local database.

| Module | What it checks | Finding IDs |
|--------|----------------|-------------|
| `fingerprint` | WordPress version (meta, readme, RSS, wp-login) | PC-FP-001/002 |
| `waf` | WAF/CDN detection (Cloudflare, Sucuri, Wordfence, Imperva, AWS, Akamai, Fastly) | PC-WAF-001 |
| `plugins` | Plugin enumeration — passive HTML + brute-force against `readme.txt` | PC-PLG-NNN |
| `themes` | Theme detection via passive scan + `style.css` brute-force | PC-THM-001 |
| `users` | User enumeration via 7 techniques: REST API (paginated), author archives (1–25/100 IDs), WP sitemap, RSS `<dc:creator>`, Yoast sitemap, oEmbed, HTML author links | PC-USR-001/002/003/004/005 |
| `brute_force` | Credential brute force against `wp-login.php` using detected usernames + internal wordlist or external file. Requires `--brute-force`. Detects rate-limiting and 2FA. | PC-BRU-001/002/003/004/007 |
| `xmlrpc` | XML-RPC access, `pingback.ping` DoS vector, `system.listMethods` | PC-XMLRPC-001/002/003 |
| `rest_api` | REST API link disclosure, oEmbed, CORS misconfiguration | PC-REST-001/002/003 |
| `misconfigs` | 12 checks: `wp-config.php`, `.env`, `.git`, `debug.log`, directory traversal... | PC-MCFG-001–012 |
| `directory_listing` | Open directory listing in `wp-content/` subdirs | PC-DIR-001–004 |
| `http_headers` | Missing HSTS, CSP, X-Frame-Options, X-Content-Type, Referrer-Policy... | PC-HDR-001–008 |
| `ssl_tls` | HTTP→HTTPS redirect, certificate validity, HSTS preload | PC-SSL-001/002/003 |
| `debug_exposure` | Active `WP_DEBUG`, PHP version disclosure via response headers | PC-DBG-001/003 |
| `content_analysis` | Card skimming scripts, suspicious iframes, hardcoded API keys | PC-CNT-001/002/003 |
| `auth` | Authenticated checks: login verification, open user registration | PC-AUTH-001/002 |
| `cves` | CVE correlation for core + plugins + themes against local DB | PC-CVE-{CVE-ID} |
| `webshells` | Webshell detection across upload paths (147–523 paths depending on mode) | PC-WS-NNN |
| `woocommerce` | WooCommerce-specific security checks (see below) | PC-WC-000–021 |
| `wp_ecommerce` | WP eCommerce-specific security checks (see below) | PC-WPEC-000–021 |

Use `plecost explain <ID>` for full technical detail and remediation steps on any finding ID.


## User Enumeration

The `users` module runs 7 techniques in parallel and deduplicates results across all sources:

| Finding ID | Technique | Severity | Trigger |
|------------|-----------|----------|---------|
| `PC-USR-001` | REST API `/wp-json/wp/v2/users` with full pagination | MEDIUM | Endpoint returns user data |
| `PC-USR-002` | Author archive `/?author=N` (IDs 1–25 fast / 1–100 deep, early-stop after 3 misses) | MEDIUM | Redirect to `/author/<slug>/` |
| `PC-USR-003` | WordPress core sitemap `/wp-sitemap-users-1.xml` | INFO | File accessible and contains `<loc>` entries with author slugs |
| `PC-USR-004` | RSS feed `<dc:creator>` tags at `/?feed=rss2` | MEDIUM | Feed contains author names |
| `PC-USR-005` | Yoast SEO author sitemap `/author-sitemap.xml` | INFO | Yoast sitemap accessible |
| *(no finding)* | oEmbed API — only if `author_url` contains `/author/` | — | Used for deduplication only |
| *(no finding)* | Homepage HTML author links matching `/author/<slug>/` | — | Used for deduplication only |

All seven techniques run concurrently. Users discovered by multiple techniques are deduplicated by username (case-insensitive) and appear once in `ScanResult.users`.


## Brute Force

The `brute_force` module is **opt-in** and requires `--brute-force`. It never activates with `--aggressive`.

### Flags

| Flag | Description |
|------|-------------|
| `--brute-force` | Enable brute force. **No passwords are tried by default** — you must also supply `--brute-force-defaults` and/or `--brute-force-wordlist`. |
| `--brute-force-defaults` | Activate the built-in password list (~70 common passwords + dynamic username variants). Requires `--brute-force`. |
| `--brute-force-wordlist <path>` | Use an external password file (one password per line). Requires `--brute-force`. |
| `--brute-force-combine` | Merge external wordlist with the internal list. Only merges internal passwords when `--brute-force-defaults` is also set. |

```bash
# Enable brute force with built-in wordlist only
plecost scan https://target.com --brute-force --brute-force-defaults

# Enable brute force with an external wordlist only
plecost scan https://target.com --brute-force --brute-force-wordlist passwords.txt

# Combine both (external + internal ~70 common passwords)
plecost scan https://target.com --brute-force --brute-force-defaults \
  --brute-force-wordlist passwords.txt --brute-force-combine
```

**Library API:**
```python
ScanOptions(
    url="https://target.com",
    brute_force=True,
    brute_force_use_defaults=True,        # enables built-in ~70 password list
    brute_force_wordlist="/path/to/list.txt",  # optional external wordlist
    brute_force_combine=True,             # merge external + internal
)
```

For each detected username, `--brute-force-defaults` tests ~70 internal passwords plus dynamic variants (e.g. `username`, `username123`, `username2025`, `username!`). External wordlists are capped at 10 000 lines (fast mode) or 50 000 lines (deep mode).

Login success is detected by the presence of a `wordpress_logged_in_*` cookie in the response (`Set-Cookie` header).

| Finding ID | Condition | Severity |
|------------|-----------|----------|
| `PC-BRU-001` | Valid credentials found | CRITICAL (9.8) without 2FA; HIGH (7.5) with 2FA detected |
| `PC-BRU-002` | 2FA detected on `wp-admin` login page | INFO |
| `PC-BRU-003` | No WordPress users discovered — brute force skipped | INFO |
| `PC-BRU-004` | Brute force completed with no valid credentials found | INFO |
| `PC-BRU-007` | Brute force stopped — target is rate-limiting login attempts | INFO |
| `PC-BRU-008` | Brute force enabled but no wordlist configured (neither `--brute-force-defaults` nor `--brute-force-wordlist` provided) | INFO |

Passwords in `PC-BRU-001` evidence are masked: first 2 characters + `***` + last character (e.g. `pa***d`). Rate limiting is detected by HTTP 429/403, body strings (`Too many`, `Locked`, `Blocked`, etc.), or a dropped TCP connection.


## WooCommerce Security

The `woocommerce` module performs dedicated security checks for WooCommerce installations and its official extensions (Payments, Blocks, Stripe Gateway). It runs automatically when WooCommerce is detected.

### Passive checks (always on)

- **Fingerprinting** — detects WooCommerce version, active extensions (Payments, Blocks, Stripe Gateway), and exposed API namespaces
- **REST API without authentication** — checks whether `/wp-json/wc/v3/customers`, `/orders`, `/coupons`, and `/system-status` are accessible without credentials (CRITICAL/HIGH)
- **Sensitive file exposure** — directory listing on `/wp-content/uploads/wc-logs/`, access to `/wp-content/uploads/woocommerce_uploads/`

### Semi-active checks (opt-in)

Semi-active checks send additional HTTP requests that could leave traces in server logs. Enable explicitly:

```bash
plecost scan https://target.com --module-option woocommerce:mode=semi-active
```

| Check | CVE | CVSS |
|-------|-----|------|
| WooCommerce Payments authentication bypass | CVE-2023-28121 | 9.8 Critical |
| WooCommerce Stripe Gateway IDOR (PII disclosure) | CVE-2023-34000 | 7.5 High |

### Authenticated checks (optional)

Provide WooCommerce REST API credentials to unlock additional checks (system configuration disclosure, payment gateway enumeration):

```bash
plecost scan https://target.com \
  --module-option woocommerce:wc_consumer_key=ck_xxx \
  --module-option woocommerce:wc_consumer_secret=cs_xxx
```

### WooCommerce JSON output

When WooCommerce is detected, the scan result includes a dedicated `woocommerce` section:

```json
{
  "woocommerce": {
    "detected": true,
    "version": "8.5.2",
    "active_plugins": ["core", "payments", "blocks", "stripe-gateway"],
    "api_namespaces": ["wc/store/v1", "wc/v3"]
  }
}
```


## WP eCommerce Security

The `wp_ecommerce` module performs dedicated security checks for the **WP eCommerce** (wp-e-commerce) plugin. It runs automatically when WP eCommerce is detected.

> **Important:** WP eCommerce has been abandoned since 2020 (last version: 3.15.1). All current installations are vulnerable to unpatched CVEs. PC-WPEC-003 is always emitted when the plugin is detected.

### Passive checks (always on)

- **Fingerprinting** — detects version via `readme.txt`, active payment gateways (ChronoPay)
- **Directory exposure** — plugin directory listing, `uploads/wpsc/`, `uploads/wpsc/digital/` (digital downloads)
- **Admin scripts** — direct access to `wpsc-admin/db-backup.php` and `wpsc-admin/display-log.php`
- **ChronoPay endpoint** — callback endpoint accessibility check

### Semi-active checks (opt-in)

Enable explicitly:

```bash
plecost scan https://target.com --module-option wpec:mode=semi-active
```

| Check | CVE | CVSS |
|-------|-----|------|
| ChronoPay SQL Injection | CVE-2024-1514 | 9.8 Critical |
| PHP Object Injection via AJAX | CVE-2026-1235 | 8.1 High |

Detection is **boolean-only** (SQL error strings, deserialization patterns) — no time-based probes.

### WP eCommerce JSON output

When WP eCommerce is detected, the scan result includes a dedicated `wp_ecommerce` section:

```json
{
  "wp_ecommerce": {
    "detected": true,
    "version": "3.15.1",
    "active_gateways": ["chronopay"],
    "checks_run": ["readme", "directories", "sensitive_files", "chronopay_endpoint"]
  }
}
```


## Output Formats

### Terminal (default)

Rich-formatted tables with color-coded severities. Use `--quiet` to suppress LOW/MEDIUM findings.

### JSON

```bash
plecost scan https://target.com --output report.json
```

```json
{
  "url": "https://target.com",
  "scanned_at": "2026-04-13T09:00:00Z",
  "is_wordpress": true,
  "wordpress_version": "6.4.2",
  "waf_detected": "Cloudflare",
  "plugins": [{ "slug": "woocommerce", "version": "8.2.1" }],
  "themes": [{ "slug": "twentytwentyfour", "version": "1.2" }],
  "users": ["admin", "editor"],
  "findings": [
    {
      "id": "PC-CVE-CVE-2023-28121",
      "remediation_id": "REM-CVE-CVE-2023-28121",
      "title": "WooCommerce SQLi (CVE-2023-28121)",
      "severity": "CRITICAL",
      "cvss_score": 9.8,
      "description": "...",
      "remediation": "Update WooCommerce to version 7.8.0 or later.",
      "references": ["https://nvd.nist.gov/vuln/detail/CVE-2023-28121"],
      "evidence": {
        "cve_id": "CVE-2023-28121",
        "software": "woocommerce",
        "version_range": "*–7.8.0",
        "installed_version": "8.2.1"
      },
      "module": "cves"
    }
  ],
  "summary": { "critical": 1, "high": 1, "medium": 3, "low": 2 },
  "duration_seconds": 4.2,
  "blocked": false,
  "woocommerce": {
    "detected": true,
    "version": "8.2.1",
    "active_plugins": ["core", "payments", "blocks"],
    "api_namespaces": ["wc/store/v1", "wc/v3"]
  },
  "wp_ecommerce": null
}
```


## Library Usage

Plecost is a first-class Python library. The same logic that powers the CLI is available as an importable API — no subprocess, no parsing CLI output.

### Standalone script

```python
import asyncio
from plecost import Scanner, ScanOptions

async def main():
    options = ScanOptions(
        url="https://target.com",
        concurrency=10,
        timeout=10,
        modules=["fingerprint", "plugins", "cves"],  # None = all modules
    )
    result = await Scanner(options).run()

    print(f"WordPress {result.wordpress_version}  |  WAF: {result.waf_detected}")
    for finding in result.findings:
        print(f"[{finding.severity.value}] {finding.id}: {finding.title}")

asyncio.run(main())
```

### Real-time callbacks

`Scanner` accepts four optional callbacks for real-time progress reporting:

```python
import asyncio
from plecost import Scanner, ScanOptions

def on_finding(finding):
    print(f"[{finding.severity.value}] {finding.id}: {finding.title}")

def on_module_start(module_name: str):
    print(f"  → Starting module: {module_name}")

def on_module_done(module_name: str):
    print(f"  ✓ Done: {module_name}")

def on_progress(module_name: str, current: int, total: int):
    pct = int(current / total * 100) if total else 0
    print(f"  [{module_name}] {pct}% ({current}/{total})")

async def main():
    opts = ScanOptions(url="https://target.com")
    result = await Scanner(
        opts,
        on_finding=on_finding,
        on_module_start=on_module_start,
        on_module_done=on_module_done,
        on_module_progress=on_progress,
    ).run()

asyncio.run(main())
```

### Bulk scanning with run_many()

`run_many()` scans multiple targets sequentially and propagates all callbacks to each per-target scan:

```python
import asyncio
from plecost import Scanner, ScanOptions

findings_by_url: dict[str, list] = {}

def on_finding(finding):
    findings_by_url.setdefault(finding.module, []).append(finding.id)

async def main():
    opts = ScanOptions(
        url="https://placeholder.example",  # overridden per target
        modules=["fingerprint", "plugins", "cves"],
        brute_force=True,
        brute_force_use_defaults=True,
    )
    scanner = Scanner(opts, on_finding=on_finding)
    results = await scanner.run_many([
        "https://site1.example.com",
        "https://site2.example.com",
    ])
    for result in results:
        print(f"{result.url}: {result.summary.critical} critical findings")

asyncio.run(main())
```

### Celery workers

```python
from celery import Celery
from plecost import Scanner, ScanOptions
import asyncio

app = Celery("tasks")

@app.task
def scan_wordpress(url: str) -> dict:
    opts = ScanOptions(url=url, modules=["fingerprint", "plugins", "cves"])
    result = asyncio.run(Scanner(opts).run())
    return {
        "url": result.url,
        "critical": result.summary.critical,
        "findings": [f.id for f in result.findings],
    }
```


## Environment Variables

| Variable | Description | Used by |
|----------|-------------|---------|
| `PLECOST_DB_URL` | Database URL (SQLite or PostgreSQL) | `update-db`, `scan` |
| `PLECOST_TIMEOUT` | Request timeout in seconds | `scan` |
| `PLECOST_OUTPUT` | JSON output file path | `scan` |
| `GITHUB_TOKEN` | GitHub token to avoid download rate limiting | `update-db` |


## Architecture

<img src="docs/architecture.svg" alt="Plecost architecture diagram" width="780"/>

Modules without interdependencies run concurrently from the start. `cves` waits for `plugins` and `themes` to complete so it has a full list of installed software to match against the CVE database.


## Troubleshooting

**"CVE database not found"**

The local database hasn't been downloaded yet:

```bash
plecost update-db
```

**Target returns 429 (rate limiting)**

```bash
# Reduce concurrency
plecost scan https://target.com --concurrency 3

# Or use stealth mode (includes automatic pacing)
plecost scan https://target.com --stealth
```

**SSL certificate errors**

```bash
plecost scan https://target.com --no-verify-ssl
```

> Only use `--no-verify-ssl` in controlled environments.

**Target returns 403 (scanner blocked)**

Plecost detects this automatically on the pre-flight probe and aborts cleanly with finding `PC-PRE-001`. Try a different IP, a proxy, or a different User-Agent:

```bash
plecost scan https://target.com --proxy http://127.0.0.1:8080
plecost scan https://target.com --random-user-agent
```

**WordPress not detected**

```bash
plecost scan https://target.com --force
```


## Local Test Environment

A self-contained Docker Compose environment — **Damn Vulnerable WordPress (DVWP)** — is included for local testing and development. It spins up a fully configured WordPress instance with a curated set of outdated, intentionally vulnerable plugins.

Located at [`tests/dvwp/`](tests/dvwp/).

### Start

```bash
cd tests/dvwp
docker compose up -d
docker compose logs wpcli -f   # watch setup (~60s), wait for plugin table
```

Once `wpcli` exits, the environment is ready:

| URL | Credentials |
|-----|-------------|
| http://localhost:8765 | — |
| http://localhost:8765/wp-admin | `admin` / `admin` |

### Pre-installed vulnerable plugins

| Plugin | Version | CVE |
|--------|---------|-----|
| wpDiscuz | 7.0.4 | CVE-2020-24186 — unauthenticated RCE via file upload (CVSS 9.8) |
| Contact Form 7 | 5.3.1 | CVE-2020-35489 — unrestricted file upload |
| WooCommerce | 5.0.0 | CVE-2021-32790 — multiple |
| WooCommerce Payments | 3.9.0 | CVE-2023-28121 — unauthenticated privilege escalation (CVSS 9.8) |
| WooCommerce Stripe Gateway | 4.3.0 | CVE-2019-15826 — order information disclosure |
| Easy Digital Downloads | 2.11.5 | CVE-2021-39351 — stored XSS |
| Give – Donation Plugin | 2.10.3 | CVE-2021-34634 — SQL injection |
| YITH WooCommerce Wishlist | 2.2.9 | CVE-2021-24987 — stored XSS |
| Ninja Forms | 3.4.34.2 | CVE-2021-34648 — unauthenticated email injection |
| Duplicator | 1.3.26 | CVE-2020-11738 — path traversal |
| Loginizer | 1.6.3 | CVE-2020-27615 — SQL injection |
| Elementor | 3.1.2 | CVE-2022-1329 — authenticated RCE |
| WP Super Cache | 1.7.1 | CVE-2021-33203 — authenticated XSS |
| Wordfence | 7.5.0 | CVE-2021-24875 — reflected XSS |

### Run plecost against it

```bash
plecost scan http://localhost:8765 -v
plecost scan http://localhost:8765 --deep -v
```

### Reset

```bash
docker compose down -v && docker compose up -d
```

See [`tests/dvwp/README.md`](tests/dvwp/README.md) for full details.

> **Warning:** For local/isolated use only. Never expose to the internet.


## License

Plecost is distributed under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/).

**Free for:** personal security research, internal corporate audits, academic and educational use, open source projects, charitable and government organizations.

**Requires a commercial license for:** scanning-as-a-service, inclusion in a commercial product, or any use generating direct or indirect revenue.

For commercial licensing: **cr0hn@cr0hn.com** (Dani) · **ffranz@mrlooquer.com** (Fran)
