15 Commits
Author SHA1 Message Date
serrindipity 3eb78e0a8a Merge pull request 're-merge metrics' (#21) from metrics into main
Reviewed-on: #21
2025-12-09 04:09:33 +00:00
Serrindipity c3d861cb51 ai generated analyze_plugins (i swear i checked it tho) 2025-12-02 13:32:49 -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
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
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
9 changed files with 487 additions and 3 deletions
+22 -3
View File
@@ -28,6 +28,12 @@ This repository provides a Dockerized environment for running WordPress locally
docker-compose up -d
```
OR
```bash
docker compose up -d
```
4. **Access WordPress:**
Open your browser and navigate to:
@@ -40,9 +46,9 @@ This repository provides a Dockerized environment for running WordPress locally
### Access sites:
- **Student org sites**:
- http://localhost:8080/~studentorg1
- http://localhost:8080/~studentorg2
- http://localhost:8080/~studentorg3
- http://localhost:8080/~studentorg1 (Default, no plugins)
- http://localhost:8080/~studentorg2 (Legit plugins)
- http://localhost:8080/~studentorg3 (BAD BAD plugins)
- **WordPress**: http://localhost:8081
### Add new student org:
@@ -58,6 +64,19 @@ chown -R www-data:www-data /home/neworg
docker logs ocf_apache_server
```
## Virtual machine (isolated testing)
If you plan to experiment with or reverse-engineer malicious plugins, do this work only in an isolated virtual machine (VM). We provide a link to a VMware download here:
https://drive.google.com/drive/folders/1lGABtrC7S9C2OpGTAlL6uG4DqqE-1zuR
Guidance:
- Use VMware and create a VM running a disposable OS such as Ubuntu or Kali Linux.
- Perform any development, testing, or analysis of intentionally malicious plugins only inside that VM. Do not deploy or test them on your host machine or any production network.
- Keep the VM network isolated from your production network when possible and snapshot the VM before risky experiments so you can revert changes.
Only analyze or test malware and malicious plugins in controlled, authorized environments. Do not use these techniques to attack or harm others.
## Volume management
> [!WARNING] Contact JQ or another officer if you are editing the default setup and don't understand the following documentation.
+35
View File
@@ -1,4 +1,39 @@
services:
# loki:
# image: grafana/loki:3.0.0
# container_name: loki
# ports:
# - "3100:3100"
# command: -config.file=/etc/loki/local-config.yaml
# 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
apache:
build:
context: ./apache
+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)
+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"