Author SHA1 Message Date
Serrindipity 983f078228 working poc for supervisord.conf 2025-12-08 21:46:17 -08:00
Serrindipity 583fc55d0b add analyze_plugins from metrics 2025-12-08 20:12:48 -08:00
Serrindipity 26661ff970 add subpaths in supervisord 2025-12-08 20:05:29 -08:00
Serrindipity c3d861cb51 ai generated analyze_plugins (i swear i checked it tho) 2025-12-02 13:32:49 -08:00
Serrindipity 7cc41c352d clean up and remove duplicates 2025-12-02 13:17:50 -08:00
ashdxt ac341827ff supervisord config 2025-12-02 00:50:24 -08:00
Serrindipity dd70946a53 update reqs 2025-12-01 22:40:57 -08:00
Serrindipity 40a36fa9c6 move metrics scripts into scripts dir 2025-12-01 22:36:07 -08:00
Serrindipity 51194f9778 Merge branch 'deliverable' of https://git.1337.berkeley.edu/Committee/Secure-OCF-Web-Hosting into deliverable 2025-12-01 22:34:50 -08:00
serrindipity dc4d7d5f18 Merge pull request 'metrics' (#20) from metrics into deliverable
Reviewed-on: #20
2025-12-02 06:34:41 +00:00
Serrindipity 8d9d6e4146 move utils into scripts 2025-12-01 22:30:24 -08:00
PopsicleSnow 431f69dc24 comment out alloy 2025-12-01 22:27:18 -08:00
PopsicleSnow 53e908177c comment out alloy 2025-12-01 22:26:48 -08:00
Serrindipity 3018d484d5 move userdata and delete old docker dev 2025-12-01 22:17:39 -08:00
Serrindipity 5a532ec968 Merge branch 'deliverable' of https://git.1337.berkeley.edu/Committee/Secure-OCF-Web-Hosting into deliverable 2025-12-01 22:12:16 -08:00
Serrindipity 9dc39831a0 add output_recursive_plugins util that writes all plugins to json file 2025-12-01 22:12:07 -08:00
PopsicleSnow e3e352dbc6 remove daemon.json and add service specific logging config 2025-12-01 22:08:39 -08:00
PopsicleSnow 7f056088a1 Merge branch 'deliverable' of https://git.1337.berkeley.edu/Committee/Secure-OCF-Web-Hosting into deliverable 2025-12-01 21:58:53 -08:00
PopsicleSnow 3c408903fb add loki docker connection with plugin and daemon 2025-12-01 21:58:50 -08:00
Serrindipity 5efa770fe7 add option to get all plugins 2025-12-01 21:45:18 -08:00
Serrindipity f8546ed60f CVE_scanner now takes in command line arguments ai powered development lmao 2025-12-01 21:42:29 -08:00
Serrindipity 59fe04d6bd what did i even do 2025-12-01 21:25:59 -08:00
Serrindipity 62e5ce8e43 move CVE scanner script into its own directory 2025-12-01 21:05:01 -08:00
Serrindipity 7cc24f6271 Merge branch 'deliverable' of https://git.1336.berkeley.edu/Committee/Secure-OCF-Web-Hosting into deliverable 2025-12-01 21:02:58 -08:00
Serrindipity b2d415a1a7 add working docker compose 2025-12-01 21:02:43 -08:00
nini.b-612 833985a65f Updated Deliverable Branch Docker
Updated the docker to emulate certain parts of the logging branch docker file
2025-12-02 04:42:21 +00:00
serrindipity 5875b7f7cf Merge pull request 'scanning' (#18) from scanning into deliverable
Reviewed-on: #18
2025-12-02 04:31:23 +00:00
arvindrai 76d71bb285 Add scripts/IMPORTANT.txt 2025-12-02 04:20:35 +00:00
arvindrai c25761f7fb Upload files to "scripts"
a better CVE
2025-12-02 03:28:18 +00:00
arvindrai ea2e544a76 Add scripts/CVE_scanner_new.py
updated with a new and better script
2025-12-02 03:21:11 +00:00
arvindrai ac3b422377 Add requirements.txt
added requirements.txt for the extra modules
2025-12-02 02:36:11 +00:00
arvindrai 0d8e4d8ccb Upload files to "scripts"
added a new python file that updates the CVE list using API calls
2025-12-02 02:31:15 +00:00
Serrindipity 04f22c031d add mostly tested utils to get list of plugins 2025-11-25 13:29:54 -08:00
Serrindipity 3b0da4c5a2 add deliverable folder 2025-11-24 23:22:02 -08:00
serrindipity 048f22ba2c Merge pull request 'Update WordPress-Docker/docker-compose.yml' (#16) from logging into main
Reviewed-on: #16
2025-11-05 05:08:14 +00:00
nini.b-612 0e6ecf1f16 Update WordPress-Docker/docker-compose.yml
Added potential Grafana Loki installation code (commented it out for now just to be safe). Some changes can be made to the current commented out code, as recommended by ChatGPT.
2025-11-04 19:01:58 +00:00
arvindrai e27b0c57db Upload files to "/"
sorted list of the original .json of vulnerabilities
2025-10-29 04:58:44 +00:00
arvindrai 564222d082 Upload files to "Figures" 2025-10-29 04:22:27 +00:00
arvindrai d0db901e65 Upload files to "Figures"
CVSS Scores
2025-10-29 03:32:15 +00:00
arvindrai 6d785b4252 Upload files to "scripts"
Python script which plots a histogram of the distribution of CVSS severities
2025-10-29 03:31:31 +00:00
arvindrai 8f78abb808 Add scripts/CVE_scanner.py
Python script that scans an array of selected plugins against the names found in our CVE.
2025-10-29 03:19:27 +00:00
arvindrai c6550981a4 Delete scripts
was a file, not a directory
2025-10-29 03:12:52 +00:00
arvindrai cccd9eedd0 Add scripts 2025-10-29 03:10:09 +00:00
kuljit 08d299d78d Merge pull request 'update readme' (#15) from readme-update into main
Reviewed-on: #15
2025-10-29 02:59:12 +00:00
PopsicleSnow 90283531f5 update readme 2025-10-28 19:55:55 -07:00
kuljit 2617e7f7e7 Merge pull request 'test-wp-sites' (#14) from test-wp-sites into main
Reviewed-on: #14
2025-10-22 18:51:05 +00:00
arvindrai 4056d29124 added cve database 2025-10-22 00:45:52 -04:00
dtan6 205bafb94b added security_rating.py description 2025-10-14 21:26:45 -07:00
dtan6 6b3c1e2bd6 got rid of downloads since it doesnt seem to be a metric anymore 2025-10-14 21:24:16 -07:00
dtan6 d18b433d01 created the security_rating.py script 2025-10-14 21:12:25 -07:00
dtan6 01e86c78d0 Created the security_rating.py script, which will output a security rating if you type in the name of a WP plugin ( not sure if it works 100 % ) 2025-10-14 21:09:07 -07:00
douglastsui 0b40e4e799 Add WordPress-Metrics/README.md 2025-10-15 00:14:02 +00:00
unknown 7b1217a2ef Initial Commit for scraping, querying, analyzing plugins 2025-10-14 17:04:11 -07:00
serrindipity 30395938ed Merge pull request 'Add script to output plugins using wp-cli' (#11) from plugin-list into main
Reviewed-on: #11
2025-10-14 07:00:04 +00:00
Pratham Rangwala 4b45a4abee Add script to output plugins using wp-cli 2025-10-13 21:14:27 -07:00
1330 changed files with 1687322 additions and 1549 deletions
+4
View File
@@ -0,0 +1,4 @@
scripts/supervisord.pid
scripts/supervisord.log
scripts/metrics/__pycache__
scripts/CVE_scanner/__pycache__
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

-121
View File
@@ -1,121 +0,0 @@
# WordPress Docker Setup
This repository provides a Dockerized environment for running WordPress locally using Docker Compose. It's designed for quick development and testing without complex installations.
## Architecture
- **Apache Server**: Single Apache container with `mod_userdir` enabled
- **User Data**: Shared volume containing student org home directories
- **WordPress**: Separate WordPress + MariaDB containers for testing
- **Structure**: Each org has `/home/<orgname>/public_html/` served at `http://localhost:8080/~<orgname>`
## 🚀 Quick Start
1. **Clone the repository:**
```bash
git clone https://git.1337.berkeley.edu/Committee/Secure-OCF-Web-Hosting.git
cd Secure-OCF-Web-Hosting/WordPress-Docker
```
2. **Install Docker Desktop:**
- [macOS](https://docs.docker.com/desktop/setup/install/mac-install/)
- [Windows](https://docs.docker.com/desktop/setup/install/windows-install/)
3. **Build and start the containers:**
```bash
docker-compose up -d
```
4. **Access WordPress:**
Open your browser and navigate to:
```
http://localhost:8081
```
Follow the on-screen instructions to complete the WordPress setup.
### Access sites:
- **Student org sites**:
- http://localhost:8080/~studentorg1
- http://localhost:8080/~studentorg2
- http://localhost:8080/~studentorg3
- **WordPress**: http://localhost:8081
### Add new student org:
```bash
docker exec -it ocf_apache_server bash
mkdir -p /home/neworg/public_html
echo "<h1>New Org</h1>" > /home/neworg/public_html/index.html
chown -R www-data:www-data /home/neworg
```
### View Apache logs:
```bash
docker logs ocf_apache_server
```
## Volume management
> [!WARNING] Contact JQ or another officer if you are editing the default setup and don't understand the following documentation.
There are 3 important mount points. `userdata`, `wp-content`, and `backup.sql` are all files / directories on the host machine that are then mounted on the respective containers.
Currently, backup.sql is mounted to the database container, `userdata` doesn't do anything, and `wp-content` is mounted to the `wordpress` container. They are set up for minimum reproducibility: i.e. there is an admin user with the default page. Credentials are in the `docker-compose.yml` file. If you'd like to make changes to the default, it is imperative that you make a backup of the sql file. You should then copy that `backupX.sql` file to the one in the repo so that the database will update.
```bash
docker compose exec -it db /bin/bash
mariadb-dump -u wp_user -p wp_db > backupX.sql # Change X to the number of the userdata volume you're editing.
# Then enter the password for wp_user, which should be in the docker compose.
exit
docker compose cp db:/backupX.sql ./backupX.sql
```
As a one-liner:
```bash
docker compose exec -it db "mariadb-dump -u wp_user -p wp_db > backupX.sql" # Make sure you change X!
```
The rest should auto-update. If you don't want to make a change to the `wp-content` directory or database, you can them to your `.gitignore`.
## ⚙️ Managing Containers
* **Start containers:**
```bash
docker-compose start
```
* **Stop containers:**
```bash
docker-compose stop
```
* **Stop and remove containers:**
```bash
docker-compose down
```
Add the `-v` flag to also remove volumes:
```bash
docker-compose down -v
```
## 🔐 Accessing phpMyAdmin
If phpMyAdmin is included in your setup:
* Open your browser and navigate to:
```
http://localhost:8081
```
-28
View File
@@ -1,28 +0,0 @@
# Dockerfile (apache-static)
FROM httpd:2.4
LABEL maintainer="random@gmail.com"
# Enable required Apache modules for user directories and PHP
RUN sed -i \
-e 's/#LoadModule userdir_module/LoadModule userdir_module/' \
-e 's/#LoadModule vhost_alias_module/LoadModule vhost_alias_module/' \
-e 's/#LoadModule rewrite_module/LoadModule rewrite_module/' \
-e 's/#Include conf\/extra\/httpd-userdir.conf/Include conf\/extra\/httpd-userdir.conf/' \
/usr/local/apache2/conf/httpd.conf
# Add custom configuration for user directories
COPY httpd-userdir.conf /usr/local/apache2/conf/extra/httpd-userdir.conf
# Create example student org directories with basic content
RUN mkdir -p /home/studentorg1/public_html && \
echo "<h1>Welcome to Student Org 1's OCF Page</h1><p>This is a test page.</p>" > /home/studentorg1/public_html/index.html && \
mkdir -p /home/studentorg2/public_html && \
echo "<h1>Welcome to Student Org 2's OCF Page</h1><p>This is a test page.</p>" > /home/studentorg2/public_html/index.html && \
mkdir -p /home/studentorg3/public_html && \
echo "<h1>Welcome to Student Org 3's OCF Page</h1><p>This is a test page.</p>" > /home/studentorg3/public_html/index.html
# Set proper ownership
RUN chown -R www-data:www-data /home
EXPOSE 80
@@ -1,24 +0,0 @@
# Settings for user home directories
<VirtualHost *:80>
UseCanonicalName Off
# Map subdomains like user.ocf.local to /home/user/public_html
VirtualDocumentRoot /home/%1/public_html
ServerAlias *.ocf.local
# Mimics OCF structure where user directories are in /home
<Directory /home/*/public_html>
AllowOverride All
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
Require all granted
# Allow .htaccess usage for student customization
<IfModule mod_rewrite.c>
RewriteEngine On
</IfModule>
</Directory>
</VirtualHost>
@@ -1,20 +0,0 @@
# Settings for user home directories
#
# Required module: mod_userdir
UserDir /home/*/public_html
#
# Control access to UserDir directories.
# Mimics OCF structure where user directories are in /home
#
<Directory "/home/*/public_html">
AllowOverride All
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
Require all granted
# Allow .htaccess usage for student customization
<IfModule mod_rewrite.c>
RewriteEngine On
</IfModule>
</Directory>
File diff suppressed because one or more lines are too long
-28
View File
@@ -1,28 +0,0 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19-11.8.3-MariaDB, for debian-linux-gnu (aarch64)
--
-- Host: localhost Database: wp_db
-- ------------------------------------------------------
-- Server version 11.8.3-MariaDB-ubu2404
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
-- Dump completed on 2025-10-22 5:21:45
File diff suppressed because one or more lines are too long
-63
View File
@@ -1,63 +0,0 @@
services:
apache:
build:
context: ./apache
ports:
- "8080:80"
volumes:
- userdata1:/home/user1/public_html
- userdata2:/home/user2/public_html
- userdata3:/home/user3/public_html
container_name: apache-static
restart: unless-stopped
db:
image: mariadb:11
container_name: wp_db
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wp_db
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_pass
volumes:
- ./backup2.sql:/docker-entrypoint-initdb.d/backup.sql # CHANGEME to edit different WordPress sites
restart: unless-stopped
wordpress:
image: wordpress:latest
container_name: wordpress_dev_container
depends_on:
- db
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_pass
WORDPRESS_DB_NAME: wp_db
ports:
- "8081:80" # WordPress site -> http://localhost:8081
volumes:
- userdata2:/var/www/html/wp-content # CHANGEME to edit different WordPress sites
restart: unless-stopped
volumes:
# Default user:
# username: admin
# password: GH^*m)qprg7VOZJ(JJ
userdata1:
driver: local
driver_opts:
type: none
device: ${PWD}/userdata1
o: bind
userdata2:
driver: local
driver_opts:
type: none
device: ${PWD}/userdata2
o: bind
userdata3:
driver: local
driver_opts:
type: none
device: ${PWD}/userdata3
o: bind
-19
View File
@@ -1,19 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Local Apache Static Landing</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background:#0f172a; color:#e6eef8; text-align:center; padding-top:8vh; }
h1 { color:#10b981; }
p { color:#cbd5e1; }
a { color:#93c5fd; text-decoration:none; }
</style>
</head>
<body>
<h1>Apache Static Host</h1>
<p>This is the static landing page container.</p>
<p><a href="http://localhost:8081/wp-admin" target="_blank">Open WordPress setup (wp-admin)</a></p>
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
# Secure OCF Web Hosting - Setup Guide
## Prerequisites
- Docker and Docker Compose installed
## Setup Instructions
### 1. Install Docker Loki Plugin
Choose the appropriate command for your system architecture:
**For AMD64 hosts:**
```bash
docker plugin install grafana/loki-docker-driver:3.3.2-amd64 --alias loki --grant-all-permissions
```
**For ARM64 hosts:**
```bash
docker plugin install grafana/loki-docker-driver:3.3.2-arm64 --alias loki --grant-all-permissions
```
### 2. Configure User Data Volume
Edit `docker-compose.yml` and update the `USERDATA` volume to point to the volume you want to scan.
## Running Instructions
### Start the Stack
```bash
docker-compose up -d
```
### Stop the Stack
```bash
docker-compose down
```
### View Logs
```bash
docker-compose logs -f
```
## Accessing Services
- **Grafana**: http://localhost:3000
- Username: `admin`
- Password: `ocfsecure`
+77
View File
@@ -0,0 +1,77 @@
services:
tools:
image: debian:latest
container_name: dev_tools
tty: true
depends_on:
- loki
restart: unless-stopped
volumes:
- USERDATA:/scan # Put any volumes you want scanned here
- ../scripts:/scripts
- ../scripts/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf
command: sh -c "
apt-get update && apt-get install -y python3 python3-venv python3-pip supervisor &&
cd /scripts &&
python3 -m venv .venv &&
. .venv/bin/activate &&
pip3 install -r requirements.txt &&
supervisord -c /etc/supervisor/conf.d/supervisord.conf
"
working_dir: /scripts
logging:
driver: loki
options:
loki-url: "http://localhost:3100/loki/api/v1/push"
mode: "non-blocking"
# Grafana Loki Stack
# Install docker loki plugin:
# Change -amd64 to -arm64 in the image tag for ARM64 hosts.
# docker plugin install grafana/loki-docker-driver:3.3.2-arm64 --alias loki --grant-all-permissions
loki:
image: grafana/loki:3.0.0
container_name: loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki/loki-config.yaml:/etc/loki/local-config.yaml
- loki-data:/loki
restart: unless-stopped
grafana:
image: grafana/grafana:10.4.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=ocfsecure
depends_on:
- loki
restart: unless-stopped
# alloy:
# image: grafana/alloy:latest
# container_name: alloy
# volumes:
# - ./alloy-config.yml:/etc/alloy/config.alloy
# - ./logs:/var/log/scanner
# ports:
# - "12345:12345" # optional web UI/debug
# command: run /etc/alloy/config.alloy
# environment:
# - ALLOY_ENABLE_WEB=true
# depends_on:
# - loki
# restart: unless-stopped
volumes:
USERDATA: # Put the volume with your user data here
driver: local
driver_opts:
type: none
o: bind
device: ../test_vol # Change this to the path of your user data
loki-data: # persist loki data
+17
View File
@@ -0,0 +1,17 @@
{
"directory": "/scan/person1/public_html",
"wordpress": true,
"name_of_detector": "Blacklisted Plugins",
"timestamp": "2024-06-20T12:34:56Z",
"plugins": {
"vulnerable_plugin": {
"version": "1.2.3",
"status": "vulnerable",
"reason": "Custom between each detector (Good luck aggregating this )"
},
"safe_plugin": {
"status": "safe",
"reason": "Custom between each detector (Good luck aggregating this )"
}
}
}
+48
View File
@@ -0,0 +1,48 @@
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
instance_addr: 127.0.0.1
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-05-15
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
analytics:
reporting_enabled: false
limits_config:
retention_period: 30d
compactor:
working_directory: /tmp/loki/retention
delete_request_store: filesystem
retention_enabled: true
retention_delete_delay: 2h
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Usage:
# ./export-wp-plugins.sh [base_directory] [output_file]
BASE_DIR="${1:-/var/www}"
OUTPUT_FILE="${2:-plugins.csv}"
# Check for WP-CLI
if ! command -v wp &>/dev/null; then
echo "WP-CLI not found. Install WP-CLI first: https://wp-cli.org/"
exit 1
fi
# Loop through subdirectories
for dir in "$BASE_DIR"/*/; do
if [ -f "$dir/wp-config.php" ]; then
site=$(basename "$dir")
echo "=== $site ===" | tee -a "$OUTPUT_FILE"
wp --allow-root --path="$dir" plugin list --format=csv>> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
fi
done
echo "Plugins saved to $OUTPUT_FILE"
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""CVE Scanner for WordPress plugins using Wordfence vulnerability database."""
import argparse
import csv
import json
import os
import sys
def load_json_local(path: str) -> dict:
"""Load JSON data from a local file.
Args:
path: Filesystem path to the JSON file.
Returns:
Parsed JSON data as a dictionary or list.
"""
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_json_from_url(url: str) -> dict:
"""Load JSON data from a URL.
Args:
url: URL to fetch JSON from.
Returns:
Parsed JSON data as a dictionary or list.
"""
import requests
r = requests.get(url, timeout=30)
r.raise_for_status()
return r.json()
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
Parsed arguments namespace.
"""
parser = argparse.ArgumentParser(
description="Scan WordPress plugins for CVE vulnerabilities using Wordfence data."
)
parser.add_argument(
"--input",
"-i",
help="Path to local vulnerabilities JSON file (default: wordfence_vulnerabilities.json)",
default="wordfence_vulnerabilities.json"
)
parser.add_argument(
"--url",
"-u",
help="URL to download vulnerabilities JSON if local file not found",
default=""
)
parser.add_argument(
"--plugins",
"-p",
nargs="+",
help="List of plugin slugs to check (space-separated)",
default=[]
)
parser.add_argument(
"--plugins-file",
"-f",
help="Path to file containing plugin slugs (one per line)",
default=None
)
parser.add_argument(
"--out-csv",
"-c",
help="Output CSV file path (if not specified, CSV output is skipped)",
default=None
)
parser.add_argument(
"--out-json",
"-j",
help="Output JSON file path (if not specified, prints to stdout)",
default=None
)
return parser.parse_args()
def load_plugin_list(plugins: list[str], plugins_file: str | None) -> set[str]:
"""Load and combine plugin slugs from CLI args and optional file.
Args:
plugins: List of plugin slugs from command line.
plugins_file: Optional path to file with plugin slugs (one per line).
Returns:
Set of plugin slugs to check.
"""
plugin_set = set(plugins)
if plugins_file and os.path.exists(plugins_file):
with open(plugins_file, "r", encoding="utf-8") as f:
for line in f:
slug = line.strip()
if slug and not slug.startswith("#"):
plugin_set.add(slug)
return plugin_set
def main():
"""Main entry point for the CVE scanner."""
args = parse_args()
# Load plugin list
bad_plugins = load_plugin_list(args.plugins, args.plugins_file)
if not bad_plugins:
print("Warning: No plugins specified to check. Use --plugins or --plugins-file.")
print("Proceeding to load vulnerability data anyway...")
# Load vulnerability data
data = None
# Prioritize URL if specified
if args.url:
print(f"Downloading from URL: {args.url}")
data = load_json_from_url(args.url)
elif os.path.exists(args.input):
print(f"Loading local file: {args.input}")
data = load_json_local(args.input)
else:
raise SystemExit(f"Error: Local file '{args.input}' not found and --url not provided.")
# Extract vulnerability entries
if isinstance(data, dict) and "vulnerabilities" in data:
entries = data["vulnerabilities"]
elif isinstance(data, list):
entries = data
else:
entries = []
if isinstance(data, dict):
for v in data.values():
if isinstance(v, list):
entries = v
break
# Find matches
matches = []
for entry in entries:
try:
if entry.get("type") != "plugin":
continue
slug = entry.get("slug", "").strip()
if not slug:
continue
if slug in bad_plugins:
matches.append({
"slug": slug,
"title": entry.get("title"),
"cve": entry.get("cve"),
"severity": entry.get("severity"),
"cvss_score": entry.get("cvss_score"),
"cve_link": entry.get("cve_link"),
"raw": entry
})
except Exception:
continue
# Display results
if not matches:
print("No matches found for the specified plugins.")
else:
print(f"\nFound {len(matches)} vulnerability matches:")
for m in matches:
print(f"- {m['slug']} | CVE: {m.get('cve')} | sev: {m.get('severity')} | CVSS: {m.get('cvss_score')}")
print(f" link: {m.get('cve_link')}")
# Write CSV output (only if specified)
if args.out_csv:
with open(args.out_csv, "w", newline='', encoding="utf-8") as csvf:
writer = csv.DictWriter(csvf, fieldnames=["slug", "title", "cve", "severity", "cvss_score", "cve_link"])
writer.writeheader()
for m in matches:
writer.writerow({
"slug": m["slug"],
"title": m.get("title"),
"cve": m.get("cve"),
"severity": m.get("severity"),
"cvss_score": m.get("cvss_score"),
"cve_link": m.get("cve_link")
})
print(f"\nSaved CSV results to {args.out_csv}")
# Write JSON output (to file or stdout)
if args.out_json:
with open(args.out_json, "w", encoding="utf-8") as jf:
json.dump(matches, jf, ensure_ascii=False, indent=2)
print(f"Saved JSON results to {args.out_json}")
else:
# Print to stdout if no JSON file specified
print("\nJSON output:")
print(json.dumps(matches, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
import json
import matplotlib.pyplot as plt
# path to your vulnerability JSON file
json_path = "wordfence_vulnerabilities.json"
def load_cvss_scores(path):
"""Load JSON data and extract all CVSS scores into a list"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
scores = []
for entry in data:
# make sure cvss_score exists and is numeric
if "cvss_score" in entry and isinstance(entry["cvss_score"], (int, float)):
scores.append(entry["cvss_score"])
return scores
def plot_hist(scores):
"""Plot a histogram of CVSS scores"""
plt.figure(figsize=(8, 5))
plt.hist(scores, bins=10, edgecolor="black", color="skyblue")
plt.title("Distribution of CVSS Scores in Vulnerability Data")
plt.xlabel("CVSS Score")
plt.ylabel("Number of Vulnerabilities")
plt.grid(alpha=0.4)
plt.savefig("cvss_distribution.png")
plt.show()
if __name__ == "__main__":
scores = load_cvss_scores(json_path)
print(f"Loaded {len(scores)} CVSS scores.")
print("Sample:", scores[:10])
if scores:
plot_hist(scores)
else:
print("⚠️ No CVSS scores found in the JSON file.")
+2
View File
@@ -0,0 +1,2 @@
get_CVE.py **MUST** be ran before better_cve_scanner can be ran since it runs using a list that is fetched daily.
CVE_scanner.py runs on a static list called wordfence_vulnerabilities.json that is stored in the same directory, which is an old list of major vulnerabilities.
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import requests
import os
URL = "https://www.wordfence.com/api/intelligence/v2/vulnerabilities/scanner"
OUT_FILE = "wordfence_CVE.json"
def main():
print("API request.....")
r = requests.get(URL, timeout=60)
r.raise_for_status()
script_dir = os.path.dirname(os.path.abspath(__file__))
out_path = os.path.join(script_dir, OUT_FILE)
with open(out_path, "w", encoding="utf-8") as f:
f.write(r.text)
print("File Saved!")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
# WordPress Metrics
## scrape_wordpress_plugins.py
- Uses requests to scrape the whole plugins list using https://api.wordpress.org/plugins/
- Scrape in pages of 100 plugins, takes about ~30 mins to complete
- Stores as output.json
## analyze_data.py
- Calculate basic statistics (percentiles, mean, median, sd ...)
- Visualize them with matplot in a logrithrimic scale
## query_plugin.py
- Change slug to equal the target plugins (should be able to directly plug the outputs from detect_plugins.sh)
- TODO: Implement warning system
## security_rating.py
- Implements some sort of basic rating system factoring in number of active installations, last-update date, and ratings
- sample use case: python3 security_rating.py {plugin name}
+74
View File
@@ -0,0 +1,74 @@
import json
import matplotlib.pyplot as plt
import numpy as np
def print_info(name, data):
# Convert the list to a NumPy array
data_np = np.array(data)
# Calculate basic statistics
mean = np.mean(data_np)
median = np.median(data_np)
std_dev = np.std(data_np)
variance = np.var(data_np)
min_val = np.min(data_np)
max_val = np.max(data_np)
sum_val = np.sum(data_np)
# Calculate percentiles
percentile_25 = np.percentile(data_np, 25)
percentile_50 = np.percentile(data_np, 50) # This is the same as the median
percentile_75 = np.percentile(data_np, 75)
percentile_90 = np.percentile(data_np, 90)
# Print the results
print(f"\n\nDescriptive Statistics for {name}:")
print(f"Mean: {mean}")
print(f"Median: {median}")
print(f"Standard Deviation: {std_dev}")
print(f"Variance: {variance}")
print(f"Minimum: {min_val}")
print(f"Maximum: {max_val}")
print(f"Sum: {sum_val}")
print("\nPercentiles:")
print(f"25th Percentile: {percentile_25}")
print(f"50th Percentile (Median): {percentile_50}")
print(f"75th Percentile: {percentile_75}")
print(f"90th Percentile: {percentile_90}")
with open('output.json', 'r') as f:
json_data = json.load(f)
# print(len(data))
downloaded = [plugin["downloaded"] for plugin in json_data]
rating = [plugin["rating"] for plugin in json_data]
active_installs = [plugin["active_installs"] for plugin in json_data]
num_ratings = [plugin["num_ratings"] for plugin in json_data]
print_info("downloaded", downloaded)
print_info("rating", rating)
print_info("active_installs", active_installs)
print_info("num_ratings", num_ratings)
# plt.hist(downloaded, bins=500, log=True)
# plt.show()
fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(8, 6))
axs[0,0].hist(downloaded, bins=100, log=True)
axs[0,0].set_title("Distribution of downloaded")
axs[0,1].hist(rating, bins=20, log=True)
axs[0,1].set_title("Distribution of rating")
axs[1,0].hist(active_installs, bins=10, log=True)
axs[1,0].set_title("Distribution of active_installs")
axs[1,1].hist(num_ratings, bins=100, log=True)
axs[1,1].set_title("Distribution of num_ratings")
# Add labels and title
# plt.xlabel("Value")
# plt.ylabel("Frequency (log)")
# Display the plot
plt.show()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Analyze WordPress plugins for security vulnerabilities using security ratings."""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
# Import security_rating functions
from security_rating import get_plugin_info, calculate_security_score
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
Parsed arguments namespace.
"""
parser = argparse.ArgumentParser(
description="Analyze WordPress plugins for security issues using security ratings."
)
parser.add_argument(
"--input",
"-i",
required=True,
help="Path to input JSON file (from output_recursive_plugins)"
)
parser.add_argument(
"--output",
"-o",
help="Path to output JSON file (if not specified, prints to stdout)",
default=None
)
parser.add_argument(
"--detector-name",
"-d",
help="Name of detector to use in output (default: Security Rating Analyzer)",
default="Security Rating Analyzer"
)
return parser.parse_args()
def analyze_plugin(slug: str) -> dict:
"""Analyze a single plugin and return its status.
Args:
slug: Plugin slug to analyze.
Returns:
Dict with status and reason fields.
"""
plugin_info = get_plugin_info(slug)
if not plugin_info:
return {
"status": "unknown",
"reason": f"Could not fetch plugin information from WordPress.org API"
}
score = calculate_security_score(plugin_info)
# Determine status based on score
# Safe: score >= 65 (Excellent or Good)
# Vulnerable: score < 65 (Moderate or Poor)
if score >= 85:
status = "safe"
reason = f"Excellent security rating ({score}/100) — Actively maintained and trusted"
elif score >= 65:
status = "safe"
reason = f"Good security rating ({score}/100) — Generally safe but monitor updates"
elif score >= 45:
status = "vulnerable"
reason = f"Moderate security rating ({score}/100) — May be outdated or less popular"
else:
status = "vulnerable"
reason = f"Poor security rating ({score}/100) — High risk of security issues"
result = {
"status": status,
"reason": reason,
"score": score
}
# Add version if available
version = plugin_info.get("version")
if version:
result["version"] = version
return result
def analyze_directory(directory_name: str, plugins_dict: dict, detector_name: str) -> dict:
"""Analyze all plugins in a directory.
Args:
directory_name: Name of the directory being analyzed.
plugins_dict: Dict mapping plugin slug to absolute path.
detector_name: Name of the detector.
Returns:
Analysis result in the specified format.
"""
timestamp = datetime.now(timezone.utc).isoformat()
analyzed_plugins = {}
print(f"\nAnalyzing plugins for directory: {directory_name}")
print(f"Found {len(plugins_dict)} plugins to analyze")
for slug, path in plugins_dict.items():
print(f" Analyzing {slug}...", end=" ", flush=True)
result = analyze_plugin(slug)
analyzed_plugins[slug] = result
print(f"{result['status']}")
return {
"directory": directory_name,
"wordpress": True,
"name_of_detector": detector_name,
"timestamp": timestamp,
"plugins": analyzed_plugins
}
def main():
"""Main entry point for the plugin analyzer."""
args = parse_args()
# Load input JSON
try:
with open(args.input, "r", encoding="utf-8") as f:
input_data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file '{args.input}' not found.", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.input}': {e}", file=sys.stderr)
sys.exit(1)
# Analyze each directory
results = []
for directory_name, plugins_dict in input_data.items():
if not plugins_dict:
print(f"Skipping {directory_name} (no plugins found)")
continue
analysis = analyze_directory(directory_name, plugins_dict, args.detector_name)
results.append(analysis)
# Output results
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nWrote analysis results to {args.output}")
else:
print("\nAnalysis results:")
print(json.dumps(results, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
import requests
slug = "akismet"
response = requests.get("https://api.wordpress.org/plugins/info/1.2/?action=query_plugins"
f"&slug={slug}")
response_json = response.json()
response_json = response_json['plugins'][0]
# print(response_json)
print(response_json['downloaded'])
print(response_json['rating'])
print(response_json['active_installs'])
print(response_json['num_ratings'])
# TODO: Implement warning system
@@ -0,0 +1,37 @@
from time import sleep
import requests
import json
from tqdm import tqdm
starting_page = 1
request_per_page = 100
response = requests.get(f"https://api.wordpress.org/plugins/info/1.2/?action=query_plugins"
f"&request[page]={starting_page}"
f"&request[per_page]={request_per_page}")
response_json = response.json()
# print(response_json)
page_no = response_json['info']['pages']
plugins_info_list = []
plugins_info_str = ""
for i in tqdm(range(page_no)):
response = requests.get(f"https://api.wordpress.org/plugins/info/1.2/?action=query_plugins"
f"&request[page]={i+1}"
f"&request[per_page]={request_per_page}")
response_json = response.json()
plugins_info_list.append(response_json['plugins'])
# plugins_info_str += str(response_json['plugins'])
sleep(0.1)
flattened_list = [item for sublist in plugins_info_list for item in sublist]
plugins_info_json = json.dumps(flattened_list, indent=4)
print(plugins_info_json)
filename = "output.json"
# Open the file in write mode ('w') and use json.dump() to write the data
with open(filename, 'w') as f:
f.write(plugins_info_json)
+101
View File
@@ -0,0 +1,101 @@
import requests
import datetime
import sys
from dateutil import parser
import datetime
def get_plugin_info(slug):
"""Fetch plugin info from the WordPress API."""
url = f"https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]={slug}"
response = requests.get(url)
if response.status_code != 200:
print(f"Error: Unable to fetch data for plugin '{slug}'.")
return None
return response.json()
def calculate_security_score(plugin):
"""Calculate a security score (0-100) for a WordPress plugin."""
score = 0
# last updated
last_updated_str = plugin.get("last_updated")
if last_updated_str:
try:
last_updated = parser.parse(last_updated_str)
now = datetime.datetime.now(datetime.timezone.utc)
if last_updated.tzinfo is None:
last_updated = last_updated.replace(tzinfo=datetime.timezone.utc)
days_since_update = (now - last_updated).days
if days_since_update <= 90:
score += 40
elif days_since_update <= 180:
score += 30
elif days_since_update <= 365:
score += 20
else:
score += 10
except Exception as e:
print(f"Warning: Could not parse date '{last_updated_str}': {e}")
score += 0
else:
score += 0
# user rating
rating = plugin.get("rating")
if isinstance(rating, (int, float)):
score += (rating / 100) * 30
else:
score += 0
# active installs
installs = plugin.get("active_installs")
if isinstance(installs, int):
if installs >= 1_000_000:
score += 20
elif installs >= 100_000:
score += 15
elif installs >= 10_000:
score += 10
elif installs > 0:
score += 5
else:
score += 0
return round(score, 1)
def rate_plugin(slug):
plugin = get_plugin_info(slug)
if not plugin:
return
score = calculate_security_score(plugin)
name = plugin.get("name", slug)
last_updated = plugin.get("last_updated", "Unknown")
rating = plugin.get("rating", "Unknown")
installs = plugin.get("active_installs", "Unknown")
print(f"\n🔍 Security Rating for '{name}'")
print("=" * (25 + len(name)))
print(f"Last Updated: {last_updated}")
print(f"User Rating: {rating}/100")
print(f"Active Installs: {installs}")
print(f"→ Security Score: {score}/100")
if score >= 85:
print("Excellent — Actively maintained and trusted")
elif score >= 65:
print("Good — Generally safe but monitor updates")
elif score >= 45:
print("Moderate — May be outdated or less popular")
else:
print("Poor — High risk of security issues")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python plugin_security_rating.py <plugin-slug>")
sys.exit(1)
slug = sys.argv[1]
rate_plugin(slug)
+4
View File
@@ -0,0 +1,4 @@
requests
numpy
matplotlib
python-dateutil
+29
View File
@@ -0,0 +1,29 @@
[supervisord]
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
logfile_maxbytes = 0
nodaemon=true
user=root
directory=/scripts
[program:detect_plugins]
command=python3 utils/detect_plugins.sh -h
; makes it run before CVE_scanner and CVSS_scores
priority=10
[program:cve_scanner]
command=python3 CVE_scanner/CVE_scanner.py -h
; makes it output to stdout
redirect_stderr= true
priority=20
[program:CVSS_scores]
command=python3 CVE_scanner/CVSS_scores.py -h
priority=30
[program:metrics]
command=python3 metrics/analyze_plugins.py -h
priority=20
+1
View File
@@ -0,0 +1 @@
__pycache__/*
+358
View File
@@ -0,0 +1,358 @@
import os
import re
def _get_plugin_dirs(directory: str) -> tuple[str, str]:
"""Return (WP_PLUGIN_DIR, WPMU_PLUGIN_DIR) for a WordPress install.
If `wp-config.php` exists, this will try to resolve custom constants such
as WP_PLUGIN_DIR and WPMU_PLUGIN_DIR. Otherwise it returns the default
`wp-content/plugins` and `wp-content/mu-plugins` paths under `directory`.
"""
config_path = os.path.join(directory, "wp-config.php")
# Default standard WordPress locations
plugin_path = os.path.join(directory, "wp-content", "plugins")
mu_plugin_path = os.path.join(directory, "wp-content", "mu-plugins")
if not os.path.exists(config_path):
print(f"Notice: {config_path} not found. Returning default paths.")
return plugin_path, mu_plugin_path
# READ mode ('r') is critical. 'w' would wipe the file.
with open(config_path, "r", encoding="utf-8", errors="ignore") as config:
content = config.read()
def resolve_php_path(php_value_str: str, root_dir: str) -> str:
"""Resolve simple PHP path expressions used in wp-config.php.
Args:
php_value_str: Right-hand value extracted from the define() call.
root_dir: Filesystem path to the WordPress root (dirname of wp-config.php).
Returns:
Normalized filesystem path produced from the PHP expression.
"""
# 1. Handle dirname(__FILE__)
# In wp-config, __FILE__ is the config file itself, so dirname is the root_dir
# We replace it with the actual root_dir enclosed in quotes to make it a string literal for parsing
simulated_val = php_value_str.replace("dirname(__FILE__)", f"'{root_dir}'")
# 2. Remove PHP comments if present (//)
simulated_val = simulated_val.split("//")[0]
# 3. Split by concatenation operator '.'
parts = simulated_val.split(".")
clean_parts = []
for part in parts:
part = part.strip()
# Strip quotes (' or ") from string literals
if (part.startswith("'") and part.endswith("'")) or (
part.startswith('"') and part.endswith('"')
):
clean_parts.append(part[1:-1])
else:
# If we encounter a variable we didn't substitute, likely can't resolve statically
pass
# 4. Join to get full path
full_path = "".join(clean_parts)
return os.path.normpath(full_path)
# Regex looks for: define( 'CONSTANT_NAME', value );
# It is case-insensitive and allows for various spacing/quoting styles
plugin_regex = re.compile(
r"define\s*\(\s*['\"]WP_PLUGIN_DIR['\"]\s*,\s*(.*?)\s*\)\s*;", re.IGNORECASE
)
mu_regex = re.compile(
r"define\s*\(\s*['\"]WPMU_PLUGIN_DIR['\"]\s*,\s*(.*?)\s*\)\s*;", re.IGNORECASE
)
plugin_match = plugin_regex.search(content)
if plugin_match:
# Extract the value part (e.g., dirname(__FILE__) . '/plugins')
raw_value = plugin_match.group(1)
plugin_path = resolve_php_path(raw_value, directory)
mu_match = mu_regex.search(content)
if mu_match:
raw_value = mu_match.group(1)
mu_plugin_path = resolve_php_path(raw_value, directory)
return plugin_path, mu_plugin_path
def _get_plugins_from_dirs(plugin_dirs: list[str]) -> dict[str, str]:
"""Scan plugin directories and return mapping of slug -> main PHP path.
Args:
plugin_dirs: List of plugin directories to scan.
Returns:
Mapping from plugin slug to absolute path of the plugin's main PHP file.
Notes:
Only files containing a WordPress plugin header (e.g. "Plugin Name:")
are considered; this mirrors how WordPress discovers admin-visible plugins.
"""
plugins: dict[str, str] = {}
# Regex to detect the Plugin Name header used by WordPress plugin main files
header_re = re.compile(r"Plugin\s+Name\s*:\s*(.+)", re.IGNORECASE)
def has_plugin_header(file_path: str) -> bool:
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
# Read a limited amount of the file; headers are near the top
sample = fh.read(8192)
except OSError:
return False
return bool(header_re.search(sample))
for p_dir in plugin_dirs:
if not p_dir:
continue
if not os.path.isdir(p_dir):
# skip non-existent plugin dirs silently
continue
try:
entries = sorted(os.listdir(p_dir))
except OSError:
continue
for entry in entries:
# Skip hidden files/dirs
if entry.startswith("."):
continue
entry_path = os.path.join(p_dir, entry)
# Single-file plugin (foo.php) directly in plugins dir
if os.path.isfile(entry_path) and entry.lower().endswith(".php"):
if has_plugin_header(entry_path):
slug = os.path.splitext(entry)[0]
# Keep first discovered instance (do not override)
plugins.setdefault(slug, entry_path)
continue
# Directory-based plugin
if os.path.isdir(entry_path):
# Candidate files to check (prefer directory-named php file)
candidates: list[str] = []
dir_named = os.path.join(entry_path, f"{entry}.php")
if os.path.isfile(dir_named):
candidates.append(dir_named)
# Add any top-level php files in the plugin directory
try:
for f in os.listdir(entry_path):
if f.lower().endswith(".php"):
candidates.append(os.path.join(entry_path, f))
except OSError:
# unreadable directory; skip
continue
found = None
for c in candidates:
if has_plugin_header(c):
found = c
break
if found:
slug = entry
plugins.setdefault(slug, found)
return plugins
def get_plugins(directory: str) -> dict[str, str]:
"""Return admin-visible plugins for the WordPress install at ``directory``.
Args:
directory: Path to the WordPress installation root (where wp-config.php lives).
Returns:
Mapping of plugin slug to the absolute path of the plugin main file.
"""
p_dir, _mu_dir = _get_plugin_dirs(directory)
return _get_plugins_from_dirs([p_dir])
def _get_obfuscated_plugins(plugin_dirs: list[str]) -> dict[str, str]:
"""Return plugins that may lack a plugin header (e.g. mu-plugins).
Args:
plugin_dirs: List of directories to scan for obfuscated/mu plugins.
Returns:
Mapping of slug to absolute filesystem path. Includes top-level .php
files and one representative .php file from subdirectories.
"""
obf_plugins: dict[str, str] = {}
for p_dir in plugin_dirs:
if not p_dir:
continue
if not os.path.isdir(p_dir):
continue
try:
entries = sorted(os.listdir(p_dir))
except OSError:
continue
for entry in entries:
if entry.startswith("."):
continue
entry_path = os.path.join(p_dir, entry)
# Top-level php files are executed as mu-plugins
if os.path.isfile(entry_path) and entry.lower().endswith(".php"):
slug = os.path.splitext(entry)[0]
obf_plugins.setdefault(slug, entry_path)
continue
# Directory: pick a representative php file if present
if os.path.isdir(entry_path):
candidates: list[str] = []
dir_named = os.path.join(entry_path, f"{entry}.php")
if os.path.isfile(dir_named):
candidates.append(dir_named)
try:
for f in os.listdir(entry_path):
if f.lower().endswith(".php"):
candidates.append(os.path.join(entry_path, f))
except OSError:
continue
if candidates:
chosen = candidates[0]
slug = entry
obf_plugins.setdefault(slug, chosen)
return obf_plugins
def get_mu_plugins(directory: str) -> dict[str, str]:
"""Return mu-plugins (files executed without standard headers).
Args:
directory: Path to the WordPress installation root.
Returns:
Mapping of slug to absolute path for mu-plugins (may lack headers).
"""
_p_dir, mu_dir = _get_plugin_dirs(directory)
return _get_obfuscated_plugins([mu_dir])
def output_recursive_plugins(scan_directory: str, outfile: str):
"""Writes mu-plugins and normal admin-visible plugins to outfile
Outputs a format like:
```json
{ 'user1' : {
'plugin1' : '/abs/path/to/plugin1',
'plugin2' : '/abs/path/to/plugin2',
},
'user2' : {
'plugin1' : ...
}
}
```
Args:
scan_directory: Path to directory containing WordPress installation subdirectories.
outfile: Path to output JSON file.
Returns:
Writes a mapping of directory in scan_directory to dict of plugin slug to absolute path for all plugins (may lack headers).
"""
import json
result = {}
if not os.path.isdir(scan_directory):
print(f"Error: {scan_directory} is not a directory")
return
try:
subdirs = sorted(os.listdir(scan_directory))
except OSError as e:
print(f"Error reading {scan_directory}: {e}")
return
for subdir in subdirs:
# Skip hidden directories
if subdir.startswith("."):
continue
subdir_path = os.path.join(scan_directory, subdir)
# Only process directories
if not os.path.isdir(subdir_path):
continue
# Get plugin directories for this WordPress install
p_dir, mu_dir = _get_plugin_dirs(subdir_path)
# Collect both normal and mu-plugins
all_plugins = {}
# Get normal plugins (with headers)
normal_plugins = _get_plugins_from_dirs([p_dir])
all_plugins.update(normal_plugins)
# Get mu-plugins (may lack headers)
mu_plugins = _get_obfuscated_plugins([mu_dir])
all_plugins.update(mu_plugins)
# Only add to result if we found any plugins
if all_plugins:
result[subdir] = all_plugins
# Write to output file
with open(outfile, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Wrote plugin data for {len(result)} directories to {outfile}")
def _test():
# Change this to test against a real directory on your machine
target_dir = "/var/www/html"
# For demonstration, we'll create a fake dummy file in the current folder
# to show how the parser works without needing a real WP installation.
dummy_config = """
<?php
// Standard default style
define( 'DB_NAME', 'database_name_here' );
// Custom plugin directory style
define( 'WP_PLUGIN_DIR', dirname(__FILE__) . '/custom/location/plugins' );
// Hardcoded absolute path style
define( 'WPMU_PLUGIN_DIR', '/usr/local/share/wordpress/mu-plugins' );
?>
"""
with open("wp-config.php", "w") as f:
f.write(dummy_config)
print(f"Testing with dummy config in: {os.getcwd()}")
p_dir, mu_dir = _get_plugin_dirs(os.getcwd())
print(f"Detected Plugin Dir: {p_dir}")
print(f"Detected MU Plugin Dir: {mu_dir}")
# Clean up dummy file
os.remove("wp-config.php")
# --- Example Usage ---
if __name__ == "__main__":
_test()
+107
View File
@@ -0,0 +1,107 @@
import os
import tempfile
import unittest
from pathlib import Path
from deliverable.utils.plugins import _get_plugins_from_dirs, _get_obfuscated_plugins
def write_file(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
class TestGetPluginsFromDirs(unittest.TestCase):
def test_single_file_plugin_detected(self):
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "plugins"
p.mkdir()
single = p / "hello.php"
write_file(
single,
"""<?php
/*
Plugin Name: Hello Plugin
*/
// plugin body
""",
)
res = _get_plugins_from_dirs([str(p)])
self.assertIn("hello", res)
self.assertEqual(
os.path.normpath(res["hello"]), os.path.normpath(str(single))
)
def test_directory_plugin_with_dir_named_file(self):
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "plugins"
plugin_dir = p / "akismet"
plugin_dir.mkdir(parents=True)
main = plugin_dir / "akismet.php"
write_file(
main,
"""<?php
/*
Plugin Name: Akismet Mock
*/
""",
)
res = _get_plugins_from_dirs([str(p)])
self.assertIn("akismet", res)
self.assertEqual(
os.path.normpath(res["akismet"]), os.path.normpath(str(main))
)
def test_directory_plugin_with_other_php_file(self):
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "plugins"
plugin_dir = p / "sample"
plugin_dir.mkdir(parents=True)
main = plugin_dir / "main.php"
write_file(
main,
"""<?php
/*
Plugin Name: Sample Plugin
*/
""",
)
res = _get_plugins_from_dirs([str(p)])
# slug is the directory name
self.assertIn("sample", res)
self.assertEqual(
os.path.normpath(res["sample"]), os.path.normpath(str(main))
)
def test_files_without_header_ignored(self):
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "plugins"
p.mkdir()
noheader = p / "noheader.php"
write_file(noheader, "<?php\n// no plugin header here\n")
res = _get_plugins_from_dirs([str(p)])
self.assertNotIn("noheader", res)
def test_mu_plugins_include_no_header(self):
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "mu-plugins"
p.mkdir()
noheader = p / "bootstrap.php"
write_file(
noheader, "<?php\n// initialization file without plugin header\n"
)
res = _get_obfuscated_plugins([str(p)])
# mu-plugin should be included even without a header
self.assertIn("bootstrap", res)
self.assertEqual(
os.path.normpath(res["bootstrap"]), os.path.normpath(str(noheader))
)
if __name__ == "__main__":
unittest.main()

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

Before

Width:  |  Height:  |  Size: 904 B

After

Width:  |  Height:  |  Size: 904 B

Some files were not shown because too many files have changed in this diff Show More