Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b30e875be
|
||
|
|
802793f64d
|
||
|
|
3a50eb6500
|
||
|
|
eed2a6d826
|
||
|
|
983f078228
|
||
|
|
583fc55d0b
|
||
|
|
26661ff970
|
||
|
|
7cc41c352d
|
||
|
|
ac341827ff | ||
|
|
dd70946a53
|
||
|
|
40a36fa9c6
|
||
|
|
51194f9778
|
||
|
|
dc4d7d5f18 | ||
|
|
8d9d6e4146
|
||
|
|
431f69dc24 | ||
|
|
53e908177c | ||
|
|
3018d484d5
|
||
|
|
5a532ec968
|
||
|
|
9dc39831a0
|
||
|
|
e3e352dbc6 | ||
|
|
7f056088a1 | ||
|
|
3c408903fb | ||
|
|
5efa770fe7
|
||
|
|
f8546ed60f
|
||
|
|
59fe04d6bd
|
||
|
|
62e5ce8e43
|
||
|
|
7cc24f6271
|
||
|
|
b2d415a1a7
|
||
|
|
833985a65f | ||
|
|
5875b7f7cf | ||
|
|
76d71bb285 | ||
|
|
c25761f7fb | ||
|
|
ea2e544a76 | ||
|
|
ac3b422377 | ||
|
|
0d8e4d8ccb | ||
|
|
04f22c031d
|
||
|
|
3b0da4c5a2 | ||
|
|
e27b0c57db | ||
|
|
564222d082 | ||
|
|
d0db901e65 | ||
|
|
6d785b4252 | ||
|
|
8f78abb808 | ||
|
|
c6550981a4 | ||
|
|
cccd9eedd0 | ||
|
|
4056d29124 |
@@ -0,0 +1,6 @@
|
|||||||
|
.DS_Store
|
||||||
|
scripts/supervisord.pid
|
||||||
|
scripts/supervisord.log
|
||||||
|
scripts/metrics/__pycache__
|
||||||
|
scripts/CVE_scanner/__pycache__
|
||||||
|
scripts/plugins.json
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -1,140 +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
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```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 (Default, no plugins)
|
|
||||||
- http://localhost:8080/~studentorg2 (Legit plugins)
|
|
||||||
- http://localhost:8080/~studentorg3 (BAD BAD plugins)
|
|
||||||
- **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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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
|
|
||||||
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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
services:
|
||||||
|
tools:
|
||||||
|
image: debian:latest
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- loki
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- USERDATA:/scan # Put any volumes you want scanned here
|
||||||
|
- ../scripts:/scripts
|
||||||
|
- ../scripts/crontab:/etc/cron.d/crontab
|
||||||
|
command: sh -c "
|
||||||
|
apt-get -qq update && apt-get -qq install -y python3 python3-venv python3-pip supervisor cron &&
|
||||||
|
chmod 0644 /etc/cron.d/crontab &&
|
||||||
|
cd /scripts &&
|
||||||
|
python3 -m venv .venv &&
|
||||||
|
. .venv/bin/activate &&
|
||||||
|
pip3 install -q -r requirements.txt &&
|
||||||
|
chmod +x /scripts/entrypoint.sh &&
|
||||||
|
/scripts/entrypoint.sh &&
|
||||||
|
cron -f
|
||||||
|
"
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 )"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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.")
|
||||||
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* * * * * root echo "Tools container is online." > /proc/1/fd/1 2>/proc/1/fd/2
|
||||||
|
00 00 * * * root /scripts/entrypoint.sh > /proc/1/fd/1 2>/proc/1/fd/2
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
python3 /scripts/utils/plugins.py
|
||||||
|
python3 /scripts/metrics/analyze_plugins.py -i /scripts/plugins.json
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
requests
|
||||||
|
numpy
|
||||||
|
matplotlib
|
||||||
|
python-dateutil
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/*
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
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__":
|
||||||
|
SCANDIR = "/scan"
|
||||||
|
OUTFILE = "/scripts/plugins.json"
|
||||||
|
output_recursive_plugins(SCANDIR, OUTFILE)
|
||||||
@@ -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: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
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 |