diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b8bcf4f4eff2c..7ba53f9cddc8b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,36 +1,39 @@ - +## Type of Change - +- [ ] Bug fix (non-breaking change) +- [ ] New feature (non-breaking change) +- [ ] Breaking change (fix or feature that would cause issues) +- [ ] Documentation update +- [ ] Code quality improvement +- [ ] Security enhancement -Trac ticket: +## Testing -## Use of AI Tools +Please describe how you tested your changes: - +- [ ] My code follows the project's coding standards +- [ ] I have performed a self-review of my code +- [ ] I have commented my code where necessary +- [ ] I have updated documentation as needed +- [ ] My changes generate no new warnings + +## Related Issues + +Closes # --- -**This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See [GitHub Pull Requests for Code Review](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/) in the Core Handbook for more details.** + +**Note:** This is our own fork. We don't require WordPress Trac tickets — just describe your changes here! \ No newline at end of file diff --git a/.github/workflows/php-code-quality.yml b/.github/workflows/php-code-quality.yml new file mode 100644 index 0000000000000..5935f03dd8e9d --- /dev/null +++ b/.github/workflows/php-code-quality.yml @@ -0,0 +1,50 @@ +name: PHP Code Quality + +on: + push: + branches: [trunk, main] + pull_request: + branches: [trunk] + +jobs: + phpstan: + name: PHPStan Static Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + + - name: Install dependencies + run: composer install --no-interaction + + - name: Run PHPStan + run: vendor/bin/phpstan analyze --error-format=github + continue-on-error: true + + php-cs-fixer: + name: PHP CS Fixer + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + + - name: Install dependencies + run: composer install --no-interaction + + - name: Run PHP CS Fixer (dry-run) + run: vendor/bin/php-cs-fixer fix --dry-run --diff --format=github + continue-on-error: true \ No newline at end of file diff --git a/.github/workflows/promote-to-production.yml b/.github/workflows/promote-to-production.yml new file mode 100644 index 0000000000000..f1d22abd0bfe8 --- /dev/null +++ b/.github/workflows/promote-to-production.yml @@ -0,0 +1,119 @@ +name: Promote to Production + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + +jobs: + validate: + name: Validate PR for Production + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + + - name: Install dependencies + run: composer install --no-interaction + + - name: Run PHPStan + id: phpstan + run: vendor/bin/phpstan analyze --error-format=github --no-progress + continue-on-error: true + + - name: Run PHP CS Fixer (dry-run) + id: cs-fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff --format=github + continue-on-error: true + + - name: Check for required files + id: files + run: | + echo "Checking for required documentation..." + if [ -f "docs/SYNC-WORKFLOW.md" ] && [ -f "docs/BRANCH-STRATEGY.md" ]; then + echo "✅ Documentation present" + else + echo "⚠️ Some documentation may be missing" + fi + + echo "Checking branch status..." + git log main..trunk --oneline > /tmp/commits.txt + COMMITS=$(wc -l < /tmp/commits.txt) + echo "Commits to promote: $COMMITS" + echo "commits_count=$COMMITS" >> $GITHUB_OUTPUT + + - name: Show commit summary + run: | + echo "=== Commits since last production release ===" + git log main..trunk --oneline + + check-changes: + name: Check for security changes + runs-on: ubuntu-latest + needs: validate + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for security-related file changes + run: | + SECURITY_PATTERNS="security|ai-client|firewall|protect|scan" + CHANGES=$(git diff main..trunk --name-only | grep -iE "$SECURITY_PATTERNS" || true) + + if [ -n "$CHANGES" ]; then + echo "=== Security-related changes detected ===" + echo "$CHANGES" + echo "security_changes=1" >> $GITHUB_OUTPUT + else + echo "No security-related file changes detected" + echo "security_changes=0" >> $GITHUB_OUTPUT + fi + + label-pr: + name: Label PR + runs-on: ubuntu-latest + needs: [validate, check-changes] + if: github.event_name == 'pull_request' + + steps: + - name: Add promotion label + run: | + gh pr edit ${{ github.event.pull_request.number }} --add-label "promote-to-production" 2>/dev/null || true + + # Add security label if security changes detected + if [ "${{ needs.check-changes.outputs.security_changes }}" == "1" ]; then + gh pr edit ${{ github.event.pull_request.number }} --add-label "security" 2>/dev/null || true + echo "Added security label" + fi + + summary: + name: Promotion Summary + runs-on: ubuntu-latest + needs: [validate, check-changes] + if: always() + + steps: + - name: Create summary + run: | + echo "## Production Promotion Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Validation Results" >> $GITHUB_STEP_SUMMARY + echo "- PHPStan: ${{ steps.phpstan.outcome }}" >> $GITHUB_STEP_SUMMARY + echo "- PHP CS Fixer: ${{ steps.cs-fixer.outcome }}" >> $GITHUB_STEP_SUMMARY + echo "- Files check: ${{ steps.files.outcome }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Changes Summary" >> $$GITHUB_STEP_SUMMARY + echo "- Commits to promote: ${{ needs.validate.outputs.commits_count }}" >> $GITHUB_STEP_SUMMARY + echo "- Security changes: ${{ needs.check-changes.outputs.security_changes }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000000000..4ec7daba9567b --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,46 @@ +name: Security Scan + +on: + push: + branches: [trunk] + schedule: + # Run weekly on Sunday + - cron: '0 0 * * 0' + +jobs: + dependency-check: + name: Check for vulnerable dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: Install Composer + run: composer install --no-interaction --prefer-dist + + - name: Run security audit + run: | + composer audit --no-interaction || true + continue-on-error: true + + php-lint: + name: PHP Syntax Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Find PHP files + run: | + find src/wp-includes src/wp-admin -name "*.php" -type f | head -50 + + - name: Lint PHP files + run: | + find src/wp-includes src/wp-admin -name "*.php" -type f -exec php -l {} \; 2>&1 | grep -v "No syntax errors" || true \ No newline at end of file diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml new file mode 100644 index 0000000000000..18ce6050faabe --- /dev/null +++ b/.github/workflows/upstream-sync.yml @@ -0,0 +1,46 @@ +name: Upstream Sync Check + +on: + schedule: + # Run every 3 hours + - cron: '0 */3 * * *' + workflow_dispatch: + +jobs: + sync-check: + name: Check upstream sync status + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/WordPress/wordpress-develop.git + git fetch upstream + + - name: Check sync status + id: sync + run: | + LOCAL=$(git rev-parse HEAD) + UPSTREAM=$(git rev-parse upstream/trunk) + + if [ "$LOCAL" = "$UPSTREAM" ]; then + echo "status=in_sync" >> $GITHUB_OUTPUT + echo "✅ In sync with upstream ($LOCAL)" + else + COMMITS=$(git rev-list --count HEAD..upstream/trunk) + echo "status=behind" >> $GITHUB_OUTPUT + echo "commits_behind=$COMMITS" >> $GITHUB_OUTPUT + echo "⚠️ Behind upstream by $COMMITS commit(s)" + git log --oneline HEAD..upstream/trunk + fi + + - name: Show upstream commits + if: steps.sync.outputs.status == 'behind' + run: | + echo "=== Recent upstream commits ===" + git log --oneline upstream/trunk -10 \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa5edc8a9d7f8..fbcc0d2ad61be 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ wp-tests-config.php /gutenberg /tests/phpunit/build /wp-cli.local.yml -/phpstan.neon /*.tsbuildinfo /jsdoc /composer.lock @@ -117,3 +116,10 @@ wp-tests-config.php # Visual regression test diffs tests/visual-regression/specs/__snapshots__ + +# Local project docs (not committed) +docs/implementation-plan.md +docs/knowledge-base.md + +# Code quality tools +.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..eb3184c8520c2 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,69 @@ +exclude(['vendor', 'node_modules', 'tests/phpunit', 'build', 'gutenberg']) + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests'); + +$config = new PhpCsFixer\Config('WordPress'); +$config + ->setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => true, + 'blank_line_after_namespace' => true, + 'cast_spaces' => true, + 'class_definition' => ['single_line' => false], + 'concat_space' => ['spacing' => 'one'], + 'function_typehint_space' => true, + 'lowercase_cast' => true, + 'lowercase_keywords' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_short_bool_cast' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => true, + 'phpdoc_align' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_scalar' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'return_type_declaration' => true, + 'short_scalar_cast' => true, + 'single_line_after_imports' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => true, + ]) + ->setFinder($finder) + ->setCacheFile('.php-cs-fixer.cache') + ->setUsingCache(true); + +return $config; \ No newline at end of file diff --git a/README-ORIG.md b/README-ORIG.md new file mode 100644 index 0000000000000..4c27999495f55 --- /dev/null +++ b/README-ORIG.md @@ -0,0 +1,183 @@ +# WordPress + +Welcome to the WordPress development repository! Please check out the [contributor handbook](https://make.wordpress.org/core/handbook/) for information about how to open bug reports, contribute patches, test changes, write documentation, or get involved in any way you can. + +* [Getting Started](#getting-started) +* [Credentials](#credentials) + +## Getting Started + +### Local development + +WordPress is a PHP, MySQL, and JavaScript based project, and uses Node for its JavaScript dependencies. A local development environment is available to quickly get up and running. + +You will need a basic understanding of how to use the command line on your computer. This will allow you to set up the local development environment, to start it and stop it when necessary, and to run the tests. + +You will need Node and npm installed on your computer. Node is a JavaScript runtime used for developer tooling, and npm is the package manager included with Node. If you have a package manager installed for your operating system, setup can be as straightforward as: + +* macOS: `brew install node` +* Windows: `choco install nodejs` +* Ubuntu: `apt install nodejs npm` + +If you are not using a package manager, see the [Node.js download page](https://nodejs.org/en/download/) for installers and binaries. + +**Note:** WordPress currently only officially supports Node.js `20.x` and npm `10.x`. + +You will also need a container environment such as [Docker Desktop](https://www.docker.com/products/docker-desktop) installed and running on your computer. The container environment is the virtualization software that powers the local development environment and can be installed just like any other regular application. + +**Note:** WordPress currently only officially supports Docker but several container environments are available and should generally be compatible, such as [Colima](https://github.com/abiosoft/colima), [OrbStack](https://orbstack.dev/), [Podman Desktop](https://podman-desktop.io/), and [Rancher Desktop](https://rancherdesktop.io/). + +### Development Environment Commands + +Ensure your container environment is running before using these commands. + +#### To start the development environment for the first time + +You can get started using the local development environment with these steps: + +1. Go to https://github.com/WordPress/wordpress-develop and fork the repository to your own GitHub account. +1. Then clone the forked repository to your computer using `git clone https://github.com//wordpress-develop.git`. +1. Navigate into the directory for the cloned repository using `cd wordpress-develop`. +1. Add the origin repo as an `upstream` remote via `git remote add upstream https://github.com/WordPress/wordpress-develop.git`. +1. Then you can keep your branches up to date via `git pull --ff upstream/trunk`, for example. + +Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone --remote` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: +1. Fork the repository to your account (use the `--org` flag to clone into an organization). +1. Clone the repository to your machine. +1. Add `WordPress/wordpress-develop` as `upstream` and set it to the default `remote` repository + +After this, remember to run `cd wordpress-develop`. + +Once you have forked and cloned the repository to your computer, run the following commands in a terminal: + +``` +npm install +npm run build:dev +npm run env:start +npm run env:install +``` + +Your WordPress site will be accessible at http://localhost:8889. You can see or change configurations in the `.env` file located at the root of the project directory. + +#### To watch for changes + +If you're making changes to WordPress core files, you should start the file watcher in order to build or copy the files as necessary: + +``` +npm run dev +``` + +To stop the watcher, press `ctrl+c`. + +#### To run a [WP-CLI](https://make.wordpress.org/cli/handbook/) command + +``` +npm run env:cli -- +``` + +WP-CLI has [many useful commands](https://developer.wordpress.org/cli/commands/) you can use to work on your WordPress site. Where the documentation mentions running `wp`, run `npm run env:cli --` instead. For example: + +``` +npm run env:cli -- help +``` + +#### To run the tests + +These commands run the PHP and end-to-end test suites, respectively: + +``` +npm run test:php +npm run test:e2e +``` + +You can pass extra parameters into the PHP tests by adding `--` and then the [command-line options](https://docs.phpunit.de/en/10.4/textui.html#command-line-options): + +``` +npm run test:php -- --filter +npm run test:php -- --group +``` + +#### Generating a code coverage report +PHP code coverage reports are [generated daily](https://github.com/WordPress/wordpress-develop/actions/workflows/test-coverage.yml) and [submitted to Codecov.io](https://app.codecov.io/gh/WordPress/wordpress-develop). + +After the local container environment has [been installed and started](#to-start-the-development-environment-for-the-first-time), the following command can be used to generate a code coverage report. + +``` +npm run test:coverage +``` + +The command will generate three coverage reports in HTML, PHP, and text formats, saving them in the `coverage` folder. + +**Note:** xDebug is required to generate a code coverage report, which can slow down PHPUnit significantly. Passing selection-based options such as `--group` or `--filter` can decrease the overall time required but will result in an incomplete report. + +#### To restart the development environment + +You may want to restart the environment if you've made changes to the configuration in the `docker-compose.yml` or `.env` files. Restart the environment with: + +``` +npm run env:restart +``` + +#### To stop the development environment + +You can stop the environment when you're not using it to preserve your computer's power and resources: + +``` +npm run env:stop +``` + +#### To start the development environment again + +Starting the environment again is a single command: + +``` +npm run env:start +``` + +#### Resetting the development environment + +The development environment can be reset. This will destroy the database and attempt to remove the pulled container images. + +``` +npm run env:reset +``` + +### Apple Silicon machines and old MySQL/MariaDB versions + +Older MySQL and MariaDB container images do not support Apple Silicon processors (M1, M2, etc.). This is true for: + +- MySQL versions 5.7 and earlier +- MariaDB 5.5 + +When using these versions on an Apple Silicon machine, you must create a `docker-compose.override.yml` file with the following contents: + +``` +services: + + mysql: + platform: linux/amd64 +``` + +Additionally, the "Use Rosetta for x86/AMD64 emulation on Apple Silicon" setting in your container environment (if applicable) needs to be disabled for this workaround. + +## Credentials + +These are the default environment credentials: + +* Database Name: `wordpress_develop` +* Username: `root` +* Password: `password` + +To login to the site, navigate to http://localhost:8889/wp-admin. + +* Username: `admin` +* Password: `password` + +**Note:** With Codespaces, open the portforwarded URL from the ports tab in the terminal, and append `/wp-admin` to login to the site. + +To generate a new password (recommended): + +1. Go to the Dashboard +2. Click the Users menu on the left +3. Click the Edit link below the admin user +4. Scroll down and click 'Generate password'. Either use this password (recommended) or change it, then click 'Update User'. If you use the generated password be sure to save it somewhere (password manager, etc). diff --git a/README.md b/README.md index 4c27999495f55..10c1017d40f14 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,160 @@ -# WordPress +# WordPress AI Security Edition -Welcome to the WordPress development repository! Please check out the [contributor handbook](https://make.wordpress.org/core/handbook/) for information about how to open bug reports, contribute patches, test changes, write documentation, or get involved in any way you can. +

+ + WordPress 6.7+ + + + GPL v2+ + + Status: Alpha +

-* [Getting Started](#getting-started) -* [Credentials](#credentials) +> ⚠️ **WARNING: This is an experimental fork** — Not intended for production use without thorough testing. +> +> **This is NOT an official WordPress project.** Created and maintained by [Chris Bunting](https://github.com/cbuntingde) with AI agent assistance. Not affiliated with, endorsed by, or connected to the WordPress Foundation or Automattic. -## Getting Started +## Overview -### Local development +WordPress AI Security Edition is a fork of WordPress core that integrates AI-powered security features directly into the CMS. This project aims to make WordPress practically unhackable by leveraging modern AI capabilities for threat detection, vulnerability prevention, and real-time security monitoring. -WordPress is a PHP, MySQL, and JavaScript based project, and uses Node for its JavaScript dependencies. A local development environment is available to quickly get up and running. +## Features -You will need a basic understanding of how to use the command line on your computer. This will allow you to set up the local development environment, to start it and stop it when necessary, and to run the tests. +### 🔒 AI-Powered Security -You will need Node and npm installed on your computer. Node is a JavaScript runtime used for developer tooling, and npm is the package manager included with Node. If you have a package manager installed for your operating system, setup can be as straightforward as: +- **Exploit Detection** — AI analyzes code patterns to identify and block potential vulnerabilities before they can be exploited +- **Real-time Monitoring** — Continuous protection against DDoS attacks, brute force attempts, and anomalous behavior +- **Plugin/Theme Scanning** — Automated vulnerability detection for extensions using static analysis and AI pattern recognition +- **Security Dashboard** — Configure AI settings, view alerts, and monitor site security from WordPress Admin -* macOS: `brew install node` -* Windows: `choco install nodejs` -* Ubuntu: `apt install nodejs npm` +### 🤖 AI Provider Support -If you are not using a package manager, see the [Node.js download page](https://nodejs.org/en/download/) for installers and binaries. +Built-in support for multiple AI providers with automatic failover: -**Note:** WordPress currently only officially supports Node.js `20.x` and npm `10.x`. +| Provider | Type | Status | +|----------|------|--------| +| DeepSeek | Cloud | ✅ Built-in | +| OpenRouter | Cloud | ✅ Built-in | +| Ollama | Local | ✅ Built-in | +| xAI (Grok) | Cloud | ✅ Built-in | +| Mistral | Cloud | ✅ Built-in | -You will also need a container environment such as [Docker Desktop](https://www.docker.com/products/docker-desktop) installed and running on your computer. The container environment is the virtualization software that powers the local development environment and can be installed just like any other regular application. +*Additional providers available via plugin extension.* -**Note:** WordPress currently only officially supports Docker but several container environments are available and should generally be compatible, such as [Colima](https://github.com/abiosoft/colima), [OrbStack](https://orbstack.dev/), [Podman Desktop](https://podman-desktop.io/), and [Rancher Desktop](https://rancherdesktop.io/). +### 🏗️ Architecture -### Development Environment Commands +The AI security layer integrates with WordPress core through: -Ensure your container environment is running before using these commands. +- **`wp-includes/ai-client/`** — Core AI client infrastructure +- **`wp-content/plugins/ai-security/`** — Security features plugin (planned) +- WordPress REST API extensions for secure AI communication +- Hook-based architecture for minimal core modification -#### To start the development environment for the first time +## Documentation -You can get started using the local development environment with these steps: +### For Users -1. Go to https://github.com/WordPress/wordpress-develop and fork the repository to your own GitHub account. -1. Then clone the forked repository to your computer using `git clone https://github.com//wordpress-develop.git`. -1. Navigate into the directory for the cloned repository using `cd wordpress-develop`. -1. Add the origin repo as an `upstream` remote via `git remote add upstream https://github.com/WordPress/wordpress-develop.git`. -1. Then you can keep your branches up to date via `git pull --ff upstream/trunk`, for example. +- [Installation Guide](./docs/installation.md) — Setup and configuration +- [Security Dashboard Guide](./docs/security-dashboard.md) — Using the AI security UI +- [AI Provider Configuration](./docs/ai-providers.md) — Setting up AI connections -Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone --remote` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: -1. Fork the repository to your account (use the `--org` flag to clone into an organization). -1. Clone the repository to your machine. -1. Add `WordPress/wordpress-develop` as `upstream` and set it to the default `remote` repository +### For Developers -After this, remember to run `cd wordpress-develop`. +- [Architecture Overview](./docs/architecture.md) — System design and components +- [Contributing Guide](./CONTRIBUTING.md) — How to contribute to this project +- [Coding Standards](./docs/coding-standards.md) — Code style and quality requirements +- [Security Policy](./SECURITY.md) — Reporting vulnerabilities -Once you have forked and cloned the repository to your computer, run the following commands in a terminal: +### Project Management -``` -npm install -npm run build:dev -npm run env:start -npm run env:install -``` - -Your WordPress site will be accessible at http://localhost:8889. You can see or change configurations in the `.env` file located at the root of the project directory. - -#### To watch for changes - -If you're making changes to WordPress core files, you should start the file watcher in order to build or copy the files as necessary: - -``` -npm run dev -``` - -To stop the watcher, press `ctrl+c`. - -#### To run a [WP-CLI](https://make.wordpress.org/cli/handbook/) command +- [Project Board](https://github.com/cbuntingde/wordpress-develop/projects) — Track progress and tasks +- [Milestones](https://github.com/cbuntingde/wordpress-develop/milestones) — Release planning -``` -npm run env:cli -- -``` - -WP-CLI has [many useful commands](https://developer.wordpress.org/cli/commands/) you can use to work on your WordPress site. Where the documentation mentions running `wp`, run `npm run env:cli --` instead. For example: - -``` -npm run env:cli -- help -``` - -#### To run the tests - -These commands run the PHP and end-to-end test suites, respectively: - -``` -npm run test:php -npm run test:e2e -``` +## Quick Start -You can pass extra parameters into the PHP tests by adding `--` and then the [command-line options](https://docs.phpunit.de/en/10.4/textui.html#command-line-options): +```bash +# Clone the repository +git clone https://github.com/cbuntingde/wordpress-develop.git +cd wordpress-develop -``` -npm run test:php -- --filter -npm run test:php -- --group -``` - -#### Generating a code coverage report -PHP code coverage reports are [generated daily](https://github.com/WordPress/wordpress-develop/actions/workflows/test-coverage.yml) and [submitted to Codecov.io](https://app.codecov.io/gh/WordPress/wordpress-develop). - -After the local container environment has [been installed and started](#to-start-the-development-environment-for-the-first-time), the following command can be used to generate a code coverage report. - -``` -npm run test:coverage -``` - -The command will generate three coverage reports in HTML, PHP, and text formats, saving them in the `coverage` folder. - -**Note:** xDebug is required to generate a code coverage report, which can slow down PHPUnit significantly. Passing selection-based options such as `--group` or `--filter` can decrease the overall time required but will result in an incomplete report. - -#### To restart the development environment - -You may want to restart the environment if you've made changes to the configuration in the `docker-compose.yml` or `.env` files. Restart the environment with: +# Install dependencies +npm install +npm run build:dev -``` -npm run env:restart +# Run local development server +npm run dev:server ``` -#### To stop the development environment +## Requirements -You can stop the environment when you're not using it to preserve your computer's power and resources: - -``` -npm run env:stop -``` +- **PHP** 8.1+ (8.2+ recommended) +- **Node.js** 18+ +- **MySQL** 8.0+ or **MariaDB** 10.5+ +- **WordPress** 6.7+ compatibility target -#### To start the development environment again - -Starting the environment again is a single command: +## Project Structure ``` -npm run env:start +wordpress-develop/ +├── src/ # WordPress source (wp-includes, wp-admin) +│ └── wp-includes/ +│ └── ai-client/ # AI integration layer +├── tests/ # Test suites +├── tools/ # Build and deployment tools +├── docs/ # Project documentation +└── CONTRIBUTING.md # Contribution guidelines ``` -#### Resetting the development environment +## Roadmap -The development environment can be reset. This will destroy the database and attempt to remove the pulled container images. +### Phase 1: Foundation (Current) +- [x] AI client infrastructure in core +- [x] Multiple provider support (DeepSeek, OpenRouter, Ollama, xAI, Mistral) +- [x] Basic security hooks and filters -``` -npm run env:reset -``` +### Phase 2: Core Security (In Progress) +- [ ] Exploit detection engine +- [ ] Real-time monitoring system +- [ ] Security dashboard UI -### Apple Silicon machines and old MySQL/MariaDB versions +### Phase 3: Extension Scanning +- [ ] Plugin vulnerability scanner +- [ ] Theme security analysis +- [ ] Dependency vulnerability checking -Older MySQL and MariaDB container images do not support Apple Silicon processors (M1, M2, etc.). This is true for: +### Phase 4: Production Readiness +- [ ] Performance optimization +- [ ] Comprehensive testing +- [ ] Security audit -- MySQL versions 5.7 and earlier -- MariaDB 5.5 +See [Milestones](https://github.com/cbuntingde/wordpress-develop/milestones) for detailed timelines. -When using these versions on an Apple Silicon machine, you must create a `docker-compose.override.yml` file with the following contents: +## Support -``` -services: +### Community - mysql: - platform: linux/amd64 -``` +- **GitHub Issues** — Report bugs and request features +- **Discussions** — Ask questions and share ideas -Additionally, the "Use Rosetta for x86/AMD64 emulation on Apple Silicon" setting in your container environment (if applicable) needs to be disabled for this workaround. +### Commercial Support -## Credentials +This is an open-source project. For commercial implementation assistance, contact the maintainer. -These are the default environment credentials: +## License -* Database Name: `wordpress_develop` -* Username: `root` -* Password: `password` +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. -To login to the site, navigate to http://localhost:8889/wp-admin. +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -* Username: `admin` -* Password: `password` +## Acknowledgments -**Note:** With Codespaces, open the portforwarded URL from the ports tab in the terminal, and append `/wp-admin` to login to the site. +- [WordPress](https://wordpress.org/) — The core project +- [AI Connector Pro](https://github.com/cbuntingde/ai-connector-pro) — Provider implementations +- [Contributors](https://github.com/cbuntingde/wordpress-develop/graphs/contributors) — Community involvement -To generate a new password (recommended): +--- -1. Go to the Dashboard -2. Click the Users menu on the left -3. Click the Edit link below the admin user -4. Scroll down and click 'Generate password'. Either use this password (recommended) or change it, then click 'Update User'. If you use the generated password be sure to save it somewhere (password manager, etc). +

+ WordPress AI Security Edition
+ Making WordPress Unhackable 🚀 +

\ No newline at end of file diff --git a/composer.json b/composer.json index 17f53c2116f71..8067d118dedec 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "wp-coding-standards/wpcs": "~3.3.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", "phpstan/phpstan": "2.1.39", - "yoast/phpunit-polyfills": "^1.1.0" + "yoast/phpunit-polyfills": "^1.1.0", + "szepeviktor/phpstan-wordpress": "^2.0" }, "config": { "allow-plugins": { diff --git a/docs/BRANCH-STRATEGY.md b/docs/BRANCH-STRATEGY.md new file mode 100644 index 0000000000000..22b6a2b22aa9d --- /dev/null +++ b/docs/BRANCH-STRATEGY.md @@ -0,0 +1,79 @@ +# Branch Strategy + +## Branches + +| Branch | Purpose | Protected? | +|--------|---------|------------| +| `trunk` | Active development — all our AI security work | ✅ Yes | +| `main` | Stable/production-ready — only synced from trunk | ✅ Yes | + +## Workflow + +``` +upstream/wordpress-develop (official) + ↓ (sync) + trunk (our dev work) + ↓ (after testing & approval) + main (frozen/push to production) +``` + +## Making Changes + +1. **Create a branch from trunk** (for any feature/fix): + ```bash + git checkout -b feature/your-feature-name trunk + ``` + +2. **Work on your branch** — commit changes + +3. **Open a PR to trunk** — for review + +4. **After approval** — merge to trunk + +5. **When ready for production** — PR from trunk to main + +## Key Rules + +- ❌ Never push directly to `main` +- ❌ Never commit AI security features directly to `main` +- ✅ All changes start as a branch off `trunk` +- ✅ PRs required to merge to `trunk` +- ✅ After thorough testing, PR from `trunk` to `main` + +## Syncing Upstream + +1. Sync to `trunk` first (from upstream/trunk) +2. Test/verify changes +3. Later, merge `trunk` to `main` when stable + +## Moving to Production (trunk → main) + +When trunk has stable, tested changes ready for production: + +1. **Create PR on GitHub:** + - Base: `main` ← Compare: `trunk` + - Title: "Promote to production - [description]" + - Description: List what changes are being promoted + +2. **Review requirements:** + - Must pass all CI/CD checks + - Code quality (phpstan, php-cs-fixer) + - Security scan + +3. **After approval:** Merge PR to main + +**Commands (if doing locally):** +```bash +# Make sure trunk is up to date +git checkout trunk +git pull origin trunk + +# Create PR branch +git checkout -b promote-to-main trunk + +# Push and create PR via GitHub +git push -u origin promote-to-main +# Then open PR on GitHub UI +``` + +**Note:** Main is now the default branch on GitHub. Trunk is our working branch. \ No newline at end of file diff --git a/docs/SYNC-WORKFLOW.md b/docs/SYNC-WORKFLOW.md new file mode 100644 index 0000000000000..dab6d995c387d --- /dev/null +++ b/docs/SYNC-WORKFLOW.md @@ -0,0 +1,89 @@ +# WordPress Sync Workflow + +## Overview + +This document defines the workflow for keeping WordPress AI Security Edition in sync with upstream WordPress core without losing our custom changes. + +## Remotes + +| Remote | URL | Purpose | +|--------|-----|---------| +| `origin` | `https://github.com/cbuntingde/wordpress-develop.git` | Our fork (our changes) | +| `upstream` | `https://github.com/WordPress/wordpress-develop.git` | Official WordPress | + +## Branch Strategy + +- **Working Branch:** `trunk` — all our work happens here +- **Sync Process:** never overwrite — always merge, never rebase onto unstable + +## Sync Checklist (Before Any Merge) + +1. ✅ **Check uncommitted changes** — must be committed or stashed +2. ✅ **Fetch upstream** — `git fetch upstream` +3. ✅ **Compare commits** — see what's new +4. ✅ **Review changes** — inspect files that will be updated +5. ✅ **Merge with review** — `git merge upstream/trunk --no-ff` +6. ✅ **Run code quality checks** — phpstan + php-cs-fixer +7. ✅ **Test if needed** — npm test, phpunit +8. ✅ **Push to origin** — only after verification + +## Automated Sync Check + +- **Job:** `wordpress-sync-check` — runs every 3 hours +- **Location:** `scripts/sync-check.sh` +- **Action:** Reports to chat when upstream has new commits + +## When Upstream Has Updates + +1. I notify you in chat with: + - Number of commits behind + - List of upstream commits + - Files that will change + +2. You decide: + - **Approve sync** — I merge and we review the result + - **Defer** — wait for another time + - **Investigate** — look at specific commits first + +3. After merge: + - Run PHPStan analysis + - Run PHP CS Fixer (dry-run) + - Verify nothing broke + +4. Push to origin only after you approve + +## Critical Rules + +- ❌ **Never** rebase our trunk onto upstream (we lose history) +- ❌ **Never** force push to trunk (could lose work) +- ❌ **Never** merge without reviewing what's coming in +- ✅ **Always** use `--no-ff` when merging upstream +- ✅ **Always** run quality checks after any merge +- ✅ **Always** get your approval before pushing to origin + +## Troubleshooting + +### Merge conflicts +If there are conflicts, I will: +1. Show you which files have conflicts +2. Help you resolve them +3. Not push until resolved + +### Breaking changes +If upstream changes break our AI security features: +1. I identify the issue +2. We fix before pushing +3. Document the conflict for future reference + +## Quick Commands + +```bash +# Manual sync check +cd ~/wordpress-develop && bash scripts/sync-check.sh + +# Fetch & view upstream changes +git fetch upstream && git log trunk..upstream/trunk + +# Merge (with review) +git merge upstream/trunk --no-ff +``` \ No newline at end of file diff --git a/docs/ai-connector-pro-spec.md b/docs/ai-connector-pro-spec.md new file mode 100644 index 0000000000000..eb86fe7d43991 --- /dev/null +++ b/docs/ai-connector-pro-spec.md @@ -0,0 +1,230 @@ +# AI Connector Pro - Plugin Specification + +**Version:** 1.0 +**Date:** 2026-03-25 +**Plugin Name:** AI Connector Pro +**Purpose:** Unified plugin to add multiple AI providers to WordPress + +--- + +## Overview + +This plugin provides a unified interface for connecting WordPress to multiple AI providers (Anthropic, Google, OpenAI, DeepSeek, OpenRouter, Ollama, xAI/Grok). It registers each provider with WordPress's AI Client and Connectors systems. + +--- + +## Supported Providers + +| Provider | API | Models | Status | +|----------|-----|--------|--------| +| **OpenAI** | `api.openai.com` | GPT-4o, GPT-4o Mini, o1, o1-mini | ✅ In core (template) | +| **Anthropic** | `api.anthropic.com` | Claude 3.5 Sonnet, 3 Opus, 3 Haiku | ✅ In core (template) | +| **Google** | `generativelanguage.googleapis.com` | Gemini 2.0, 1.5 Pro/Flash | ✅ In core (template) | +| **DeepSeek** | `api.deepseek.com` | DeepSeek Chat, Coder | 🆕 New | +| **OpenRouter** | `openrouter.ai` | Aggregates 100+ models | 🆕 New | +| **Ollama** | localhost | Local models (Llama, Mistral, etc.) | 🆕 New | +| **xAI** | `api.x.ai` | Grok-2, Grok-2 Vision | 🆕 New | +| **Mistral** | `api.mistral.ai` | Codestral, Mistral Large | 🆕 New | + +--- + +## Architecture + +``` +ai-connector-pro/ +├── ai-connector-pro.php # Main plugin file +├── includes/ +│ ├── class-ai-connector-pro.php # Main plugin class +│ ├── providers/ +│ │ ├── abstract-provider.php # Base provider class +│ │ ├── class-deepseek.php # DeepSeek provider +│ │ ├── class-openrouter.php # OpenRouter provider +│ │ ├── class-ollama.php # Ollama (local) +│ │ ├── class-xai.php # xAI/Grok provider +│ │ └── class-mistral.php # Mistral provider +│ ├── class-settings.php # Admin settings +│ └── class-connector.php # WP Connector integration +└── readme.txt +``` + +--- + +## Provider Implementation Pattern + +Each provider implements `ProviderInterface` and provides: + +### 1. Metadata +```php +public static function metadata(): ProviderMetadata { + return new ProviderMetadata( + id: 'deepseek', + name: 'DeepSeek', + type: ProviderTypeEnum::CLOUD, + credentialsUrl: 'https://platform.deepseek.com/api-keys', + authenticationMethod: new RequestAuthenticationMethod( + RequestAuthenticationMethod::API_KEY + ), + description: 'Open-source AI models for code and chat' + ); +} +``` + +### 2. Model Factory +```php +public static function model(string $modelId, ?ModelConfig $config = null): ModelInterface { + // Return appropriate model class based on modelId +} +``` + +### 3. Availability Checker +```php +public static function availability(): ProviderAvailabilityInterface { + // Check if API is reachable +} +``` + +### 4. Model Metadata Directory +```php +public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface { + // Return directory of available models +} +``` + +--- + +## Key Features + +### 1. Multi-Provider Support +- All providers registered with WordPress AI Client +- Users select provider in admin settings +- Fallback to backup provider if one fails + +### 2. Unified API Key Management +- Secure storage in wp_options (encrypted) +- Individual keys per provider +- Auto-detect existing keys from separate plugins + +### 3. Model Selection +- Dropdown in settings for each provider +- Shows available models per provider +- Default model configuration + +### 4. Connection Testing +- Test button in settings +- Validates API key works +- Shows available models + +### 5. Cost Tracking (Future) +- Track API usage per provider +- Budget alerts +- Usage reports + +--- + +## Admin Settings Page + +``` +Settings → AI Connectors +├── Provider Status (which providers are configured) +├── OpenAI Settings +│ ├── API Key [______________] +│ ├── Default Model [GPT-4o] +│ └── [Test Connection] +├── Anthropic Settings +│ ├── API Key [______________] +│ ├── Default Model [Claude 3.5 Sonnet] +│ └── [Test Connection] +├── DeepSeek Settings +│ ├── API Key [______________] +│ ├── Default Model [DeepSeek Chat] +│ └── [Test Connection] +├── OpenRouter Settings +│ ├── API Key [______________] +│ └── [Test Connection] +├── Ollama Settings +│ ├── Endpoint [http://localhost:11434] +│ ├── Available Models (auto-detected) +│ └── [Test Connection] +├── xAI Settings +│ ├── API Key [______________] +│ ├── Default Model [Grok-2] +│ └── [Test Connection] +└── Save Changes +``` + +--- + +## Integration Points + +### 1. Register with WP AI Client +```php +add_action('wp_ai_client_registry_init', function($registry) { + // Register each provider class + $registry->registerProvider(\AIConnectorPro\Providers\DeepSeek::class); + $registry->registerProvider(\AIConnectorPro\Providers\OpenRouter::class); + // ... +}); +``` + +### 2. Register with Connectors API +```php +// Connectors are auto-created from AI Client providers +// Custom settings UI can override descriptions +``` + +### 3. Settings API +```php +// Store API keys securely +register_setting('ai_connector_pro', 'ai_connector_pro_keys'); +``` + +--- + +## Security Considerations + +1. **API Key Encryption** - Use `wp_encrypt()` / `wp_decrypt()` for storage +2. **Capability Check** - Only `manage_options` can configure +3. **Output Escaping** - All settings escaped in admin UI +4. **Nonce Verification** - All form submissions verified + +--- + +## Implementation Phases + +### Phase 1: Core Plugin Structure +- [ ] Plugin scaffolding +- [ ] Base provider abstract class +- [ ] Settings page framework + +### Phase 2: New Providers +- [ ] DeepSeek implementation +- [ ] OpenRouter implementation +- [ ] Ollama implementation +- [ ] xAI implementation +- [ ] Mistral implementation + +### Phase 3: Advanced Features +- [ ] Connection testing +- [ ] Model auto-detection +- [ ] Usage tracking + +--- + +## Testing + +- Unit tests for each provider +- Integration tests with WordPress AI Client +- Manual testing with real API keys (sandbox mode) + +--- + +## References + +- WordPress AI Client: `wp-includes/ai-client/` +- WordPress Connectors: `wp-includes/connectors.php` +- Provider Interface: `wp-includes/php-ai-client/src/Providers/Contracts/ProviderInterface.php` +- Core provider examples: Built-in templates (Anthropic, Google, OpenAI) + +--- + +*This spec is a living document.* \ No newline at end of file diff --git a/docs/code-quality-todo.md b/docs/code-quality-todo.md new file mode 100644 index 0000000000000..ef067b86846e0 --- /dev/null +++ b/docs/code-quality-todo.md @@ -0,0 +1,46 @@ +# Code Quality TODO + +> This file tracks code scanning and validation tasks + +## PHP Syntax Check + +When PHP is available, run: + +```bash +# Check all our new PHP files for syntax errors +find wp-includes/php-ai-client/src/Providers/AiConnectorPro -name "*.php" -exec php -l {} \; +find wp-includes/ai-connector-pro.php -exec php -l {} \; +``` + +## WordPress Coding Standards + +```bash +# Install WordPress coding standards if needed +composer require wp-coding-standards/wpcs:^3.0 + +# Run PHPCS on our files +./vendor/bin/phpcs --standard=WordPress wp-includes/ai-connector-pro.php +./vendor/bin/phpcs --standard=WordPress wp-includes/php-ai-client/src/Providers/AiConnectorPro/ +``` + +## Tasks + +- [ ] PHP syntax check on ai-connector-pro.php +- [ ] PHP syntax check on AiConnectorPro providers +- [ ] Run PHPCS on all new files +- [ ] Check for deprecated function usage +- [ ] Verify hook priorities are correct +- [ ] Check nonce verification in all form handlers + +## Manual Code Review Checklist + +- [ ] All inputs sanitized +- [ ] All outputs escaped +- [ ] Nonce verification on forms +- [ ] Capability checks on admin actions +- [ ] Proper error handling +- [ ] PHPDoc blocks complete + +--- + +*Updated: 2026-03-25* \ No newline at end of file diff --git a/docs/project-management.md b/docs/project-management.md new file mode 100644 index 0000000000000..db036243dc190 --- /dev/null +++ b/docs/project-management.md @@ -0,0 +1,88 @@ +# WordPress AI Security Edition - Project Management + +**Version:** 1.0 +**Date:** 2026-03-25 +**Status:** Active Development + +--- + +## Project Goals + +1. Enterprise-grade AI security integration into WordPress core +2. Keep fork updated with upstream WordPress +3. Comprehensive documentation for maintainability +4. Zero critical errors in implementation + +--- + +## Current Tasks + +### Documentation Phase +- [ ] Review all WordPress core security documentation +- [ ] Document all AI Client APIs and interfaces +- [ ] Document Abilities API usage +- [ ] Create API reference for security modules + +### Code Quality Phase +- [ ] Scan core for PHP errors/warnings +- [ ] Verify provider implementations +- [ ] Check hook/filter usage +- [ ] Validate database schema + +### Implementation Phase (from implementation-plan.md) + +#### Phase 1: Foundation +- [ ] 1.1 Create directory structure: wp-includes/ai-security/ +- [ ] 1.2 Build AI Security Client wrapper +- [ ] 1.3 Add settings page in WP Admin +- [ ] 1.4 Configure AI provider settings +- [ ] 1.5 Basic hook into init for request inspection + +--- + +## Progress Tracking + +### Completed +- [x] Fork created from wordpress-develop +- [x] README with warning +- [x] Knowledge base created +- [x] Implementation plan created +- [x] AI providers added to core (5 built-in) +- [x] Plugin repo created (ai-connector-pro) +- [x] AI Security module implemented (Phase 1-6 foundation) + +### In Progress +- [ ] Documentation review +- [ ] Code scanning + +### Pending +- [ ] Implementation of security features + +--- + +## Code Quality Standards + +- PHP 8.1+ strict typing +- WordPress coding standards (PHPCS) +- PHPDoc for all classes/methods +- Unit test coverage for critical paths +- No PHP warnings/errors +- Proper nonce verification +- Capability checks on all admin actions + +--- + +## Upstream Sync Strategy + +1. Monitor WordPress Trac for security releases +2. Monthly sync with upstream trunk +3. Security patches applied immediately +4. Test on staging before production + +--- + +## Notes + +- Use branches for feature development +- PR to trunk, then Chris reviews before merge +- Keep MEMORY.md updated with decisions \ No newline at end of file diff --git a/docs/team.md b/docs/team.md new file mode 100644 index 0000000000000..eced6e75bd57b --- /dev/null +++ b/docs/team.md @@ -0,0 +1,41 @@ +# WordPress AI Security Edition - Team Structure + +## Project Leads +- **Sage** (me) - Project lead, your interface, delegation +- **Lead Developer** - Implementation, code, PRs +- **Research Agent** - Security research, docs, knowledge +- **Code Review Agent** - Quality, security, best practices + +## Current Team Members + +### Lead Developer +- Handles implementation tasks +- Creates branches, commits, PRs +- Works on: AI Connector Pro, security features + +### Research Agent +- Security research and documentation +- Knowledge base updates +- Threat analysis + +## Active Tasks + +### In Progress +- AI Connector Pro v1.0.0 - initial release + +### Backlog +- Fix provider model class references +- Test on WordPress 7.0 +- Security feature implementation +- Upstream WordPress sync + +## Workflow +1. You give me a task +2. I delegate to appropriate team member +3. Agent works and reports back +4. I summarize results for you + +## Notes +- Using subagent sessions for delegation +- Each agent has isolated workspace +- Agents can spawn other agents if needed \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000000..509ec5e9306e5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +# PHPStan configuration for WordPress AI Security Edition + +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + level: 5 + paths: + - src/wp-includes/ai-client + excludePaths: + - vendor/* + - node_modules/* + ignoreErrors: + - '#Function .+ not found#' + - '#Class .+ not found#' \ No newline at end of file diff --git a/scripts/sync-check.sh b/scripts/sync-check.sh new file mode 100644 index 0000000000000..726313f4af2d9 --- /dev/null +++ b/scripts/sync-check.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# WordPress Sync Manager - Safe upstream sync workflow +# Ensures our custom changes are never overwritten + +set -e + +REPO_DIR="/home/gsxrchris/.openclaw/workspace/wordpress-develop" +cd "$REPO_DIR" + +echo "==========================================" +echo "WordPress AI Security Edition Sync Check" +echo "==========================================" +echo "" + +# Check for uncommitted changes +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "❌ ERROR: You have uncommitted changes!" + echo "Please commit or stash them before syncing." + exit 1 +fi + +# Fetch upstream +echo "Fetching upstream..." +git fetch upstream + +# Compare commits +LOCAL=$(git rev-parse trunk) +UPSTREAM=$(git rev-parse upstream/trunk) + +if [ "$LOCAL" = "$UPSTREAM" ]; then + echo "✅ Already in sync with upstream" + echo "Commit: $LOCAL" + exit 0 +fi + +COMMITS_BEHIND=$(git rev-list --count trunk..upstream/trunk) +echo "⚠️ Upstream has $COMMITS_BEHIND new commit(s)" +echo "" + +# Show what we're about to merge +echo "=== Upstream commits to merge ===" +git log --oneline trunk..upstream/trunk +echo "" + +# Show files that will be changed +echo "=== Files that will be updated ===" +git diff --stat trunk...upstream/trunk +echo "" + +echo "==========================================" +echo "ACTION REQUIRED: Review above changes" +echo "==========================================" +echo "" +echo "To merge these changes, run:" +echo " cd $REPO_DIR" +echo " git merge upstream/trunk --no-ff" +echo "" +echo "After merging, run analysis:" +echo " vendor/bin/phpstan analyze" +echo " vendor/bin/php-cs-fixer fix --dry-run" +echo "" +echo "Then push to origin/trunk after review" \ No newline at end of file diff --git a/src/wp-includes/ai-connector-pro.php b/src/wp-includes/ai-connector-pro.php new file mode 100644 index 0000000000000..687eabb1f5390 --- /dev/null +++ b/src/wp-includes/ai-connector-pro.php @@ -0,0 +1,45 @@ +registerProvider( $provider_class ); + } + } +} + +// Register when WordPress is loaded enough +add_action( 'init', __NAMESPACE__ . '\\register_providers', 5 ); \ No newline at end of file diff --git a/src/wp-includes/ai-security/abilities/register-abilities.php b/src/wp-includes/ai-security/abilities/register-abilities.php new file mode 100644 index 0000000000000..a56cb3debde46 --- /dev/null +++ b/src/wp-includes/ai-security/abilities/register-abilities.php @@ -0,0 +1,802 @@ + __( 'Scan Plugin for Vulnerabilities', 'ai-security' ), + 'description' => __( 'Analyze a plugin directory for security vulnerabilities using AI.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\scan_plugin_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) + ); + + // Scan theme for vulnerabilities + wp_register_ability( + 'security/scan-theme', + array( + 'label' => __( 'Scan Theme for Vulnerabilities', 'ai-security' ), + 'description' => __( 'Analyze a theme directory for security vulnerabilities using AI.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\scan_theme_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) + ); + + // Get security status + wp_register_ability( + 'security/get-status', + array( + 'label' => __( 'Get Security Status', 'ai-security' ), + 'description' => __( 'Get current AI Security status, including connection state and threat levels.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\get_status_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) + ); + + // Get recent threats + wp_register_ability( + 'security/get-threats', + array( + 'label' => __( 'Get Recent Threats', 'ai-security' ), + 'description' => __( 'Get list of recent security threats and events.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\get_threats_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) + ); + + // Block IP + wp_register_ability( + 'security/block-ip', + array( + 'label' => __( 'Block IP Address', 'ai-security' ), + 'description' => __( 'Block a specific IP address from accessing the site.', 'ai-security' ), + 'category' => 'security', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'ip' => array( + 'type' => 'string', + 'description' => __( 'IP address to block', 'ai-security' ), + ), + 'reason' => array( + 'type' => 'string', + 'description' => __( 'Reason for blocking', 'ai-security' ), + ), + ), + 'required' => array( 'ip' ), + ), + 'execute_callback' => __NAMESPACE__ . '\\block_ip_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => false, + ), + ), + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\\register_abilities', 20 ); + +/** + * Scan plugin ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function scan_plugin_ability( array $input = array() ): array { + $plugin_slug = $input['plugin'] ?? ''; + + if ( empty( $plugin_slug ) ) { + return array( + 'success' => false, + 'error' => 'Plugin slug is required', + ); + } + + $analyzer = \WordPress\AI_Security\Analyzer::get_instance(); + $result = $analyzer->scan_plugin( $plugin_slug ); + + return array( + 'success' => true, + 'plugin' => $plugin_slug, + 'files_scanned' => $result['files_scanned'] ?? 0, + 'findings' => $result['findings'] ?? array(), + 'status' => $result['status'] ?? 'unknown', + ); +} + +/** + * Scan theme ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function scan_theme_ability( array $input = array() ): array { + $theme_slug = $input['theme'] ?? ''; + + if ( empty( $theme_slug ) ) { + return array( + 'success' => false, + 'error' => 'Theme slug is required', + ); + } + + $themes = wp_get_themes(); + $theme = $themes[ $theme_slug ] ?? null; + + if ( ! $theme ) { + return array( + 'success' => false, + 'error' => 'Theme not found', + ); + } + + $analyzer = \WordPress\AI_Security\Analyzer::get_instance(); + $result = $analyzer->scan_theme( $theme ); + + return array( + 'success' => true, + 'theme' => $theme_slug, + 'files_scanned' => $result['files_scanned'] ?? 0, + 'findings' => $result['findings'] ?? array(), + 'status' => $result['status'] ?? 'unknown', + ); +} + +/** + * Get security status ability callback. + * + * @since 7.1.0 + * @return array + */ +function get_status_ability(): array { + $client = \WordPress\AI_Security\Client::get_instance(); + $status = $client->get_status(); + + $logger = \WordPress\AI_Security\Audit_Logger::get_instance(); + $critical = $logger->get_logs_by_severity( 'critical', 10 ); + $high = $logger->get_logs_by_severity( 'high', 10 ); + + return array( + 'success' => true, + 'ai_connected' => $status['connected'], + 'ai_support' => $status['ai_support'], + 'providers_count' => $status['providers_count'], + 'critical_threats' => count( $critical ), + 'high_threats' => count( $high ), + 'blocked_ips' => count( get_option( 'ai_security_blocked_ips', array() ) ), + ); +} + +/** + * Get recent threats ability callback. + * + * @since 7.1.0 + * @return array + */ +function get_threats_ability(): array { + $logger = \WordPress\AI_Security\Audit_Logger::get_instance(); + $logs = $logger->get_recent_logs( 20 ); + + $threats = array_filter( + $logs, + function ( $log ) { + return in_array( $log['severity'], array( 'critical', 'high', 'medium' ), true ); + } + ); + + return array( + 'success' => true, + 'count' => count( $threats ), + 'threats' => array_values( $threats ), + ); +} + +/** + * Block IP ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function block_ip_ability( array $input = array() ): array { + $ip = $input['ip'] ?? ''; + $reason = $input['reason'] ?? 'Blocked via AI ability'; + + if ( empty( $ip ) ) { + return array( + 'success' => false, + 'error' => 'IP address is required', + ); + } + + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + $result = $firewall->block_ip( $ip, $reason ); + + return array( + 'success' => $result, + 'ip' => $ip, + 'message' => $result ? 'IP blocked successfully' : 'Failed to block IP', + ); +} + +/** + * Unblock IP ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function unblock_ip_ability( array $input = array() ): array { + $ip = $input['ip'] ?? ''; + + if ( empty( $ip ) ) { + return array( + 'success' => false, + 'error' => 'IP address is required', + ); + } + + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + $result = $firewall->unblock_ip( $ip ); + + return array( + 'success' => $result, + 'ip' => $ip, + 'message' => $result ? 'IP unblocked successfully' : 'Failed to unblock IP', + ); +} + +/** + * Get firewall rules ability callback. + * + * @since 7.1.0 + * @return array + */ +function get_firewall_rules_ability(): array { + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + $rules = $firewall->get_rules(); + $blocked = $firewall->get_blocked_ips(); + $stats = $firewall->get_stats(); + + return array( + 'success' => true, + 'rules_count' => count( $rules ), + 'enabled_rules' => $stats['enabled_rules'], + 'blocked_ips' => count( $blocked ), + 'rules' => $rules, + 'recently_blocked' => array_slice( $blocked, -10, true ), + ); +} + +/** + * Add firewall rule ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function add_firewall_rule_ability( array $input = array() ): array { + $pattern = $input['pattern'] ?? ''; + $action = $input['action'] ?? 'block'; + + if ( empty( $pattern ) ) { + return array( + 'success' => false, + 'error' => 'Pattern is required', + ); + } + + if ( ! in_array( $action, array( 'block', 'log', 'challenge' ), true ) ) { + return array( + 'success' => false, + 'error' => 'Invalid action. Must be block, log, or challenge', + ); + } + + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + $result = $firewall->add_rule( + array( + 'pattern' => $pattern, + 'action' => $action, + ) + ); + + return array( + 'success' => $result, + 'pattern' => $pattern, + 'action' => $action, + 'message' => $result ? 'Rule added successfully' : 'Failed to add rule', + ); +} + +/** + * Get audit log summary ability callback. + * + * @since 7.1.0 + * @return array + */ +function get_audit_summary_ability(): array { + $logger = \WordPress\AI_Security\Audit_Logger::get_instance(); + $stats = $logger->get_stats(); + + $recent_critical = $logger->get_logs_by_severity( 'critical', 5 ); + $recent_high = $logger->get_logs_by_severity( 'high', 5 ); + + // Group by event type + $all_logs = $logger->get_recent_logs( 500 ); + $eventTypes = array(); + foreach ( $all_logs as $log ) { + $event = $log['event'] ?? 'unknown'; + $eventTypes[ $event ] = ( $eventTypes[ $event ] ?? 0 ) + 1; + } + + // Sort by count + arsort( $eventTypes ); + + return array( + 'success' => true, + 'summary' => $stats, + 'critical_events' => $recent_critical, + 'high_events' => $recent_high, + 'top_event_types' => array_slice( $eventTypes, 0, 10, true ), + 'total_events' => $stats['total'], + ); +} + +/** + * Generate security report ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function generate_report_ability( array $input = array() ): array { + $period = $input['period'] ?? 'weekly'; + $format = $input['format'] ?? 'summary'; + + $logger = \WordPress\AI_Security\Audit_Logger::get_instance(); + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + + $stats = $logger->get_stats(); + $logs = $logger->get_recent_logs( 100 ); + $blocked = $firewall->get_blocked_ips(); + $rules = $firewall->get_rules(); + + $report = array( + 'generated_at' => current_time( 'mysql' ), + 'period' => $period, + 'security_summary' => array( + 'total_events' => $stats['total'], + 'critical' => $stats['critical'], + 'high' => $stats['high'], + 'medium' => $stats['medium'], + 'low' => $stats['low'], + 'blocked_ips' => count( $blocked ), + 'firewall_rules' => count( $rules ), + ), + 'recent_threats' => array_filter( + $logs, + function ( $log ) { + return in_array( $log['severity'], array( 'critical', 'high' ), true ); + } + ), + 'recommendations' => generate_recommendations( $stats, $blocked, $rules ), + ); + + if ( 'json' === $format ) { + return array( + 'success' => true, + 'format' => 'json', + 'report' => json_encode( $report, JSON_PRETTY_PRINT ), + ); + } elseif ( 'html' === $format ) { + $html = $logger->generate_report_html( $logs ); + return array( + 'success' => true, + 'format' => 'html', + 'html' => $html, + ); + } + + // Summary format + return array( + 'success' => true, + 'format' => 'summary', + 'summary' => $report['security_summary'], + 'top_threats' => array_slice( $report['recent_threats'], 0, 5 ), + 'recommendations' => $report['recommendations'], + ); +} + +/** + * Generate security recommendations based on data. + * + * @since 7.1.0 + * @param array $stats Statistics. + * @param array $blocked Blocked IPs. + * @param array $rules Firewall rules. + * @return array + */ +function generate_recommendations( array $stats, array $blocked, array $rules ): array { + $recommendations = array(); + + if ( $stats['critical'] > 5 ) { + $recommendations[] = 'High number of critical events. Consider reviewing recent threats immediately.'; + } + + if ( $stats['high'] > 20 ) { + $recommendations[] = 'Elevated high-severity events detected. Review firewall rules and blocked IPs.'; + } + + if ( count( $blocked ) > 50 ) { + $recommendations[] = 'Large number of blocked IPs detected. Consider enabling country blocking for known malicious regions.'; + } + + if ( count( $rules ) < 3 ) { + $recommendations[] = 'Few firewall rules configured. Consider adding rules for common attack patterns (SQL injection, XSS).'; + } + + if ( empty( $recommendations ) ) { + $recommendations[] = 'Security status looks good. Continue monitoring regularly.'; + } + + return $recommendations; +} + +/** + * Autonomous response ability callback. + * + * @since 7.1.0 + * @param array $input Input parameters. + * @return array + */ +function autonomous_response_ability( array $input = array() ): array { + $level = $input['action_level'] ?? 'analyze'; + + $logger = \WordPress\AI_Security\Audit_Logger::get_instance(); + $firewall = \WordPress\AI_Security\Firewall::get_instance(); + + $recent_critical = $logger->get_logs_by_severity( 'critical', 20 ); + $recent_high = $logger->get_logs_by_severity( 'high', 20 ); + + $actions_taken = array(); + + if ( 'analyze' === $level ) { + // Just analyze and report + return array( + 'success' => true, + 'action_level' => 'analyze', + 'critical_count' => count( $recent_critical ), + 'high_count' => count( $recent_high ), + 'analysis' => 'Analysis complete. No actions taken (analyze mode).', + 'recommendations' => generate_recommendations( + $logger->get_stats(), + $firewall->get_blocked_ips(), + $firewall->get_rules() + ), + ); + } + + // Respond mode - block high-threat IPs + if ( 'respond' === $level || 'full' === $level ) { + // Extract IPs from recent critical/high threats and block them + $ips_to_block = array(); + $threats = array_merge( $recent_critical, $recent_high ); + + foreach ( $threats as $threat ) { + $ip = $threat['ip'] ?? ''; + if ( $ip && ! isset( $ips_to_block[ $ip ] ) ) { + $ips_to_block[ $ip ] = $threat['severity']; + } + } + + // Block top 10 most threatening IPs + $blocked_count = 0; + foreach ( $ips_to_block as $ip => $severity ) { + if ( $blocked_count >= 10 ) { + break; + } + // Skip private IPs + if ( preg_match( '/^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.|127\.)/', $ip ) ) { + continue; + } + $firewall->block_ip( $ip, 'Auto-blocked by AI: ' . $severity . ' threat' ); + $actions_taken[] = 'Blocked IP: ' . $ip . ' (' . $severity . ')'; + $blocked_count++; + } + } + + // Full mode - also add firewall rules + if ( 'full' === $level ) { + // Add common attack pattern rules + $existing_rules = $firewall->get_rules(); + $has_sql_injection_rule = false; + $has_xss_rule = false; + + foreach ( $existing_rules as $rule ) { + if ( strpos( $rule['pattern'], 'union.*select' ) !== false ) { + $has_sql_injection_rule = true; + } + if ( strpos( $rule['pattern'], 'script|' ) !== false ) { + $has_xss_rule = true; + } + } + + if ( ! $has_sql_injection_rule ) { + $firewall->add_rule( + array( + 'pattern' => '(?i)(union.*select|union.*all.*select|--|\/\*|\*\/)', + 'action' => 'block', + ) + ); + $actions_taken[] = 'Added firewall rule: SQL injection patterns'; + } + + if ( ! $has_xss_rule ) { + $firewall->add_rule( + array( + 'pattern' => '(?i)( 'block', + ) + ); + $actions_taken[] = 'Added firewall rule: XSS patterns'; + } + + $actions_taken[] = 'Full autonomous response completed'; + } + + return array( + 'success' => true, + 'action_level' => $level, + 'actions_taken' => $actions_taken, + 'summary' => array( + 'critical_analyzed' => count( $recent_critical ), + 'high_analyzed' => count( $recent_high ), + 'ips_blocked' => count( $actions_taken ), + ), + ); +} + +/** + * Unblock IP ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/unblock-ip', + array( + 'label' => __( 'Unblock IP Address', 'ai-security' ), + 'description' => __( 'Unblock a previously blocked IP address.', 'ai-security' ), + 'category' => 'security', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'ip' => array( + 'type' => 'string', + 'description' => __( 'IP address to unblock', 'ai-security' ), + ), + ), + 'required' => array( 'ip' ), + ), + 'execute_callback' => __NAMESPACE__ . '\\unblock_ip_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) +); + +/** + * Get firewall rules ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/get-firewall-rules', + array( + 'label' => __( 'Get Firewall Rules', 'ai-security' ), + 'description' => __( 'List all firewall rules and their status.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\get_firewall_rules_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) +); + +/** + * Add firewall rule ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/add-firewall-rule', + array( + 'label' => __( 'Add Firewall Rule', 'ai-security' ), + 'description' => __( 'Add a new firewall rule to block or log requests matching a pattern.', 'ai-security' ), + 'category' => 'security', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'pattern' => array( + 'type' => 'string', + 'description' => __( 'Regex pattern to match', 'ai-security' ), + ), + 'action' => array( + 'type' => 'string', + 'description' => __( 'Action: block, log, or challenge', 'ai-security' ), + 'enum' => array( 'block', 'log', 'challenge' ), + ), + ), + 'required' => array( 'pattern', 'action' ), + ), + 'execute_callback' => __NAMESPACE__ . '\\add_firewall_rule_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) +); + +/** + * Get audit log summary ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/get-audit-summary', + array( + 'label' => __( 'Get Audit Log Summary', 'ai-security' ), + 'description' => __( 'Get a summary of security events grouped by type and severity.', 'ai-security' ), + 'category' => 'security', + 'execute_callback' => __NAMESPACE__ . '\\get_audit_summary_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) +); + +/** + * Generate security report ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/generate-report', + array( + 'label' => __( 'Generate Security Report', 'ai-security' ), + 'description' => __( 'Generate a security report covering the specified time period.', 'ai-security' ), + 'category' => 'security', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'period' => array( + 'type' => 'string', + 'description' => __( 'Time period: daily, weekly, or monthly', 'ai-security' ), + 'enum' => array( 'daily', 'weekly', 'monthly' ), + ), + 'format' => array( + 'type' => 'string', + 'description' => __( 'Format: json, html, or summary', 'ai-security' ), + 'enum' => array( 'json', 'html', 'summary' ), + ), + ), + 'required' => array( 'period' ), + ), + 'execute_callback' => __NAMESPACE__ . '\\generate_report_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + ) +); + +/** + * Autonomous threat response ability. + * + * @since 7.1.0 + */ +wp_register_ability( + 'security/autonomous-response', + array( + 'label' => __( 'Execute Autonomous Response', 'ai-security' ), + 'description' => __( 'Analyze recent threats and automatically take protective actions.', 'ai-security' ), + 'category' => 'security', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'action_level' => array( + 'type' => 'string', + 'description' => __( 'Response level: analyze, respond, or full', 'ai-security' ), + 'enum' => array( 'analyze', 'respond', 'full' ), + ), + ), + 'required' => array( 'action_level' ), + ), + 'execute_callback' => __NAMESPACE__ . '\\autonomous_response_ability', + 'permission_callback' => 'current_user_can_manage_options', + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => false, + ), + ), + ) +); \ No newline at end of file diff --git a/src/wp-includes/ai-security/ai-security.php b/src/wp-includes/ai-security/ai-security.php new file mode 100644 index 0000000000000..958a5e3a2e05f --- /dev/null +++ b/src/wp-includes/ai-security/ai-security.php @@ -0,0 +1,755 @@ + +
+

+ +
+

+ + + + + + + + + + + + + + + + + +
+ + ✓ + + ✗ + +
✓
✓
+
+ +
+

+

+ + + + + + +

+
+
+ +
+

+

+ +
+ + +
+ + scan_all_extensions(); + echo '
' . esc_html( print_r( $results, true ) ) . '
'; + } + ?> +
+ sanitize_text_field( $_POST['rule_pattern'] ?? '' ), + 'action' => sanitize_text_field( $_POST['rule_action'] ?? 'block' ), + ); + if ( ! empty( $rule['pattern'] ) ) { + $firewall->add_rule( $rule ); + echo '

' . esc_html__( 'Rule added.', 'ai-security' ) . '

'; + } + } + + if ( isset( $_POST['ai_security_delete_rule'] ) && check_admin_referer( 'ai_security_firewall' ) ) { + $rule_id = sanitize_text_field( $_POST['rule_id'] ?? '' ); + if ( $rule_id ) { + $firewall->delete_rule( $rule_id ); + echo '

' . esc_html__( 'Rule deleted.', 'ai-security' ) . '

'; + } + } + + if ( isset( $_POST['ai_security_toggle_rule'] ) && check_admin_referer( 'ai_security_firewall' ) ) { + $rule_id = sanitize_text_field( $_POST['rule_id'] ?? '' ); + $enabled = isset( $_POST['rule_enabled'] ); + if ( $rule_id ) { + $firewall->toggle_rule( $rule_id, $enabled ); + } + } + + // Block IP form + if ( isset( $_POST['ai_security_block_ip'] ) && check_admin_referer( 'ai_security_firewall' ) ) { + $ip = sanitize_text_field( $_POST['block_ip'] ?? '' ); + if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) { + $firewall->block_ip( $ip, 'Manual block from admin' ); + echo '

' . esc_html__( 'IP blocked.', 'ai-security' ) . '

'; + } + } + + // Unblock IP + if ( isset( $_POST['ai_security_unblock_ip'] ) && check_admin_referer( 'ai_security_firewall' ) ) { + $ip = sanitize_text_field( $_POST['unblock_ip'] ?? '' ); + if ( $ip ) { + $firewall->unblock_ip( $ip ); + echo '

' . esc_html__( 'IP unblocked.', 'ai-security' ) . '

'; + } + } + + $rules = $firewall->get_rules(); + $blocked_ips = $firewall->get_blocked_ips(); + $blocked_countries = $firewall->get_blocked_countries(); + + // Handle country blocking form + if ( isset( $_POST['ai_security_block_countries'] ) && check_admin_referer( 'ai_security_firewall' ) ) { + $countries = sanitize_text_field( $_POST['blocked_countries'] ?? '' ); + $country_list = array_filter( array_map( 'trim', explode( ',', $countries ) ) ); + $firewall->set_blocked_countries( $country_list ); + echo '

' . esc_html__( 'Country blocking updated.', 'ai-security' ) . '

'; + $blocked_countries = $firewall->get_blocked_countries(); + } + ?> +
+

+ + get_stats(); ?> +
+

+

+ | + / | + +

+
+ +

+
+ +

+ + +

+

+
+ +

+
+ + + + + + + + + + +
+ +

+
+ +
+

+ +

+
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + onchange="this.form.submit()"> + +
+
+
+ + + + +
+
+ +

+
+ + + +
+ + + + + + + + + + + + + + + $data ) : ?> + + + + + + + + + +
+
+ + + + +
+
+
+ get_logs_by_severity( $severity, 1000 ); + } elseif ( $event ) { + $all_logs = $logger->get_logs_by_event( $event, 1000 ); + } else { + $all_logs = $logger->get_recent_logs( 1000 ); + } + + // Apply search filter + if ( $search ) { + $all_logs = array_filter( + $all_logs, + function ( $log ) use ( $search ) { + $search_lower = strtolower( $search ); + return str_contains( strtolower( $log['details'] ?? '' ), $search_lower ) + || str_contains( strtolower( $log['event'] ?? '' ), $search_lower ) + || str_contains( strtolower( $log['ip'] ?? '' ), $search_lower ); + } + ); + } + + // Paginate + $total_logs = count( $all_logs ); + $total_pages = ceil( $total_logs / $per_page ); + $offset = ( $page - 1 ) * $per_page; + $logs = array_slice( $all_logs, $offset, $per_page ); + + // Export handling + if ( isset( $_POST['ai_security_export_logs'] ) && check_admin_referer( 'ai_security_logs' ) ) { + $format = sanitize_text_field( $_POST['export_format'] ?? 'json' ); + $export_data = $logger->export( $format ); + + header( 'Content-Type: application/octet-stream' ); + header( 'Content-Disposition: attachment; filename="security-logs-' . date( 'Y-m-d' ) . '.' . $format . '"' ); + echo $export_data; + exit; + } + + // Clear logs + if ( isset( $_POST['ai_security_clear_logs'] ) && check_admin_referer( 'ai_security_logs' ) ) { + $logger->clear_logs(); + echo '

' . esc_html__( 'Logs cleared.', 'ai-security' ) . '

'; + $logs = array(); + } + + // Generate PDF report + if ( isset( $_POST['ai_security_generate_report'] ) && check_admin_referer( 'ai_security_logs' ) ) { + $logger->export_pdf( 100 ); + } + + // Get stats for summary + $stats = $logger->get_stats(); + ?> +
+

+ + +
+

+

+ | + | + | + | + +

+
+ + +
+ + + + + +
+ + + + + + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 'red', + 'high' => 'orange', + 'medium' => 'blue', + 'low' => 'green', + 'info' => 'gray', + ); + $color = $colors[ $sev ] ?? 'gray'; + ?> + +
+ + + 1 ) : ?> +

+ 'ai-security-logs', 'log_page' => $i ); + if ( $severity ) { + $query_args['severity'] = $severity; + } + if ( $search ) { + $query_args['search'] = $search; + } + echo '' . $i . ' '; + } + ?> +

+ +
+

' . esc_html__( 'Settings saved.', 'ai-security' ) . '

'; + } + + $settings = array( + 'enabled' => get_option( 'ai_security_enabled', true ), + 'auto_block' => get_option( 'ai_security_auto_block', true ), + 'rate_limit' => get_option( 'ai_security_rate_limit', 60 ), + 'failed_login_threshold' => get_option( 'ai_security_failed_login_threshold', 5 ), + 'notifications_enabled' => get_option( 'ai_security_notifications_enabled', true ), + 'notification_email' => get_option( 'ai_security_notification_email', get_option( 'admin_email' ) ), + 'webhook_url' => get_option( 'ai_security_webhook_url', '' ), + ); + ?> +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ > + +
+ > + +
+ + +
+ + +
+ > + +
+ +
+ +

+
+ +

+ +

+
+
+ get_stats(); + $blocked = $firewall->get_blocked_ips(); + $rules = $firewall->get_rules(); + + $to = get_option( 'ai_security_notification_email', get_option( 'admin_email' ) ); + $subject = __( 'Weekly Security Report', 'ai-security' ) . ' - ' . get_bloginfo( 'name' ); + + $body = sprintf( + __( + 'Security Report for %s + +Summary: +- Total Events: %d +- Critical: %d +- High: %d +- Medium: %d +- Low: %d +- Blocked IPs: %d +- Firewall Rules: %d + +Log in to your WordPress admin to view full details. + +--- +AI Security powered by WordPress AI Security Edition', + 'ai-security' + ), + get_bloginfo( 'name' ), + $stats['total'], + $stats['critical'], + $stats['high'], + $stats['medium'], + $stats['low'], + count( $blocked ), + count( $rules ) + ); + + wp_mail( $to, $subject, $body ); +} diff --git a/src/wp-includes/ai-security/analyzer/class-wp-security-analyzer.php b/src/wp-includes/ai-security/analyzer/class-wp-security-analyzer.php new file mode 100644 index 0000000000000..81e1603928755 --- /dev/null +++ b/src/wp-includes/ai-security/analyzer/class-wp-security-analyzer.php @@ -0,0 +1,203 @@ + $this->scan_plugins(), + 'themes' => $this->scan_themes(), + 'scan_time' => current_time( 'mysql' ), + ); + + // Log the scan + $logger = Audit_Logger::get_instance(); + $logger->log( 'scan_completed', 'info', 'Security scan completed. Plugins: ' . count( $results['plugins'] ) . ', Themes: ' . count( $results['themes'] ) ); + + return $results; + } + + /** + * Scan all installed plugins. + * + * @since 7.1.0 + * @return array + */ + public function scan_plugins(): array { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + $results = array(); + + foreach ( $plugins as $plugin_file => $plugin_data ) { + $results[ $plugin_file ] = $this->scan_plugin( $plugin_file ); + } + + return $results; + } + + /** + * Scan a single plugin. + * + * @since 7.1.0 + * @param string $plugin_file Plugin file path. + * @return array + */ + public function scan_plugin( string $plugin_file ): array { + $plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file ); + $files = $this->get_php_files( $plugin_dir ); + + $findings = array(); + $file_count = 0; + + foreach ( $files as $file ) { + $file_count++; + $content = file_get_contents( $file ); + + if ( empty( $content ) ) { + continue; + } + + // Use AI to analyze if available + $client = Client::get_instance(); + $analysis = $client->analyze_code( $content ); + + if ( $analysis && ! empty( $analysis['vulnerabilities'] ) ) { + $findings = array_merge( $findings, $analysis['vulnerabilities'] ); + } + } + + return array( + 'name' => $plugin_file, + 'files_scanned' => $file_count, + 'findings' => $findings, + 'status' => empty( $findings ) ? 'clean' : 'issues_found', + ); + } + + /** + * Scan all installed themes. + * + * @since 7.1.0 + * @return array + */ + public function scan_themes(): array { + $themes = wp_get_themes(); + $results = array(); + + foreach ( $themes as $theme ) { + $results[ $theme->get_stylesheet() ] = $this->scan_theme( $theme ); + } + + return $results; + } + + /** + * Scan a single theme. + * + * @since 7.1.0 + * @param \WP_Theme $theme Theme object. + * @return array + */ + public function scan_theme( \WP_Theme $theme ): array { + $theme_dir = $theme->get_stylesheet_directory(); + $files = $this->get_php_files( $theme_dir ); + + $findings = array(); + $file_count = 0; + + foreach ( $files as $file ) { + $file_count++; + $content = file_get_contents( $file ); + + if ( empty( $content ) ) { + continue; + } + + $client = Client::get_instance(); + $analysis = $client->analyze_code( $content ); + + if ( $analysis && ! empty( $analysis['vulnerabilities'] ) ) { + $findings = array_merge( $findings, $analysis['vulnerabilities'] ); + } + } + + return array( + 'name' => $theme->get_name(), + 'files_scanned' => $file_count, + 'findings' => $findings, + 'status' => empty( $findings ) ? 'clean' : 'issues_found', + ); + } + + /** + * Get all PHP files in a directory. + * + * @since 7.1.0 + * @param string $dir Directory path. + * @return array + */ + private function get_php_files( string $dir ): array { + $files = array(); + + if ( ! is_dir( $dir ) ) { + return $files; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ) + ); + + foreach ( $iterator as $file ) { + if ( $file->isFile() && $file->getExtension() === 'php' ) { + $files[] = $file->getPathname(); + } + } + + return $files; + } +} \ No newline at end of file diff --git a/src/wp-includes/ai-security/client/class-wp-ai-security-client.php b/src/wp-includes/ai-security/client/class-wp-ai-security-client.php new file mode 100644 index 0000000000000..2a30dd21d90ad --- /dev/null +++ b/src/wp-includes/ai-security/client/class-wp-ai-security-client.php @@ -0,0 +1,186 @@ + $ai_support && null !== $registry, + 'ai_support' => $ai_support, + 'providers_count' => $registry ? count( $registry->getRegisteredProviderIds() ) : 0, + ); + } + + /** + * Analyze code for vulnerabilities using AI. + * + * @since 7.1.0 + * @param string $code Code to analyze. + * @return array|null + */ + public function analyze_code( string $code ): ?array { + if ( ! function_exists( 'wp_ai_client_prompt' ) ) { + return null; + } + + $prompt = wp_ai_client_prompt( + 'You are a security expert. Analyze this PHP code for vulnerabilities. ' . + 'Look for: SQL injection, XSS, CSRF, RCE, path traversal, unsafe deserialization, ' . + 'input validation issues, and security best practices violations. ' . + 'Return JSON with: vulnerabilities (array of objects with type, line, severity, description), ' . + 'recommendations (array of strings). If no issues found, return empty arrays.' + ); + + try { + $result = $prompt + ->with_text( $code ) + ->as_json_response( + array( + 'vulnerabilities' => 'array', + 'recommendations' => 'array', + ) + ) + ->generate_text(); + + if ( is_wp_error( $result ) ) { + return null; + } + + $data = json_decode( $result->get_text_content(), true ); + return $data ?? null; + } catch ( \Exception $e ) { + return null; + } + } + + /** + * Analyze a request for threats. + * + * @since 7.1.0 + * @param array $request Request data. + * @return array|null + */ + public function analyze_request( array $request ): ?array { + if ( ! function_exists( 'wp_ai_client_prompt' ) ) { + return null; + } + + $request_summary = json_encode( $request ); + + $prompt = wp_ai_client_prompt( + 'You are a web security expert. Analyze this HTTP request for potential threats. ' . + 'Look for: SQL injection attempts, XSS payloads, command injection, ' . + 'path traversal, brute force patterns, DDoS patterns, and other malicious activity. ' . + 'Return JSON with: is_threat (bool), threat_type (string or null), ' . + 'confidence (0-100), details (string).' + ); + + try { + $result = $prompt + ->with_text( $request_summary ) + ->as_json_response( + array( + 'is_threat' => 'boolean', + 'threat_type' => 'string|null', + 'confidence' => 'integer', + 'details' => 'string', + ) + ) + ->generate_text(); + + if ( is_wp_error( $result ) ) { + return null; + } + + $data = json_decode( $result->get_text_content(), true ); + return $data ?? null; + } catch ( \Exception $e ) { + return null; + } + } + + /** + * Get security recommendations. + * + * @since 7.1.0 + * @param string $context Context (plugin, theme, overall). + * @return array|null + */ + public function get_recommendations( string $context = 'overall' ): ?array { + if ( ! function_exists( 'wp_ai_client_prompt' ) ) { + return null; + } + + $prompt = wp_ai_client_prompt( + 'You are a WordPress security expert. Provide security hardening recommendations ' . + 'for a WordPress ' . $context . '. Focus on: configuration, code practices, ' . + 'monitoring, and prevention. Return JSON with recommendations as an array of strings.' + ); + + try { + $result = $prompt + ->as_json_response( + array( + 'recommendations' => 'array', + ) + ) + ->generate_text(); + + if ( is_wp_error( $result ) ) { + return null; + } + + $data = json_decode( $result->get_text_content(), true ); + return $data ?? null; + } catch ( \Exception $e ) { + return null; + } + } +} \ No newline at end of file diff --git a/src/wp-includes/ai-security/detector/class-wp-threat-detector.php b/src/wp-includes/ai-security/detector/class-wp-threat-detector.php new file mode 100644 index 0000000000000..723290f492889 --- /dev/null +++ b/src/wp-includes/ai-security/detector/class-wp-threat-detector.php @@ -0,0 +1,686 @@ + array( + 'pattern' => '/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE)\b)|(--)|(\/\*)|(\*\/)|(\%27)|(\%22)|(\';)|(\"))/i', + 'severity' => 'high', + 'name' => 'SQL Injection', + ), + 'xss' => array( + 'pattern' => '/(]+src| 'high', + 'name' => 'Cross-Site Scripting (XSS)', + ), + 'command_injection' => array( + 'pattern' => '/(\||;|`|\$\(|&&|\|\||\\\\r|\\\\n|\\\\t)/', + 'severity' => 'critical', + 'name' => 'Command Injection', + ), + 'path_traversal' => array( + 'pattern' => '/(\.\.\/|\.\.\\\\|%2e%2e%2f|%2e%2e\/|\.\.%00)/i', + 'severity' => 'high', + 'name' => 'Path Traversal', + ), + 'xmlrpc' => array( + 'pattern' => '/(system\.multicall|wp\.getUsersBlogs|pingback\.ping)/i', + 'severity' => 'medium', + 'name' => 'XML-RPC Attack', + ), + 'csrf' => array( + 'pattern' => '/(]*action=|]*name=[\'"]_wpnonce)/i', + 'severity' => 'low', + 'name' => 'Potential CSRF', + ), + 'ssrf' => array( + 'pattern' => '/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|metadata\.google)/i', + 'severity' => 'medium', + 'name' => 'Server-Side Request Forgery (SSRF)', + ), + 'ldap_injection' => array( + 'pattern' => '/(\*\)|%28|%29|\(\||\(&|\)\(|\)\(|\(\))/i', + 'severity' => 'medium', + 'name' => 'LDAP Injection', + ), + ); + + /** + * Blocked IPs cache. + * + * @since 7.1.0 + * @var array + */ + private array $blocked_ips = array(); + + /** + * Notification sent timestamps (to prevent spam). + * + * @since 7.1.0 + * @var array + */ + private array $last_notification = array(); + + /** + * Get instance. + * + * @since 7.1.0 + * @return Threat_Detector + */ + public static function get_instance(): Threat_Detector { + if ( null === self::$instance ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Constructor. + * + * @since 7.1.0 + */ + public function __construct() { + $this->load_blocked_ips(); + $this->init_hooks(); + } + + /** + * Load blocked IPs from database. + * + * @since 7.1.0 + */ + private function load_blocked_ips(): void { + $this->blocked_ips = get_option( 'ai_security_blocked_ips', array() ); + } + + /** + * Initialize hooks. + * + * @since 7.1.0 + */ + private function init_hooks(): void { + // Check requests at init priority 1 + add_action( 'init', array( $this, 'check_request' ), 1 ); + + // Track failed logins + add_action( 'wp_login_failed', array( $this, 'track_failed_login' ) ); + + // Schedule cleanup + if ( ! wp_next_scheduled( 'ai_security_cleanup' ) ) { + wp_schedule_event( time(), 'hourly', 'ai_security_cleanup' ); + } + add_action( 'ai_security_cleanup', array( $this, 'cleanup' ) ); + } + + /** + * Check incoming request for threats. + * + * @since 7.1.0 + */ + public function check_request(): void { + // Skip admin requests + if ( is_admin() || wp_doing_cron() ) { + return; + } + + // Check if IP is blocked + $ip = $this->get_client_ip(); + if ( $this->is_ip_blocked( $ip ) ) { + $this->handle_blocked_ip( $ip ); + } + + $request = $_REQUEST; + + // Skip empty requests + if ( empty( $request ) ) { + return; + } + + // Pattern-based detection + foreach ( $this->patterns as $name => $pattern ) { + foreach ( $request as $key => $value ) { + if ( is_string( $value ) && $this->matches_pattern( $value, $pattern['pattern'] ) ) { + $this->handle_threat( + $name, + $pattern['name'], + $pattern['severity'], + 'Pattern detected in parameter: ' . sanitize_text_field( $key ) + ); + } + } + } + + // Rate limiting check + if ( ! $this->check_rate_limit() ) { + $this->handle_threat( + 'rate_limit_exceeded', + 'Rate Limit Exceeded', + 'high', + 'Too many requests from IP' + ); + } + + // AI-based analysis for suspicious requests + if ( $this->should_use_ai_analysis( $request ) ) { + $this->analyze_with_ai( $request ); + } + } + + /** + * Check if string matches pattern. + * + * @since 7.1.0 + * @param string $value Value to check. + * @param string $pattern Regex pattern. + * @return bool + */ + private function matches_pattern( string $value, string $pattern ): bool { + // Skip very long strings (likely false positives) + if ( strlen( $value ) > 5000 ) { + return false; + } + + return preg_match( $pattern, $value ) === 1; + } + + /** + * Determine if request should be analyzed with AI. + * + * @since 7.1.0 + * @param array $request Request data. + * @return bool + */ + private function should_use_ai_analysis( array $request ): bool { + // Only analyze if AI is available + if ( ! function_exists( 'wp_ai_client_prompt' ) ) { + return false; + } + + // Check for suspicious indicators but not clear patterns + $suspicious_indicators = array( + 'eval', 'base64', 'shell_exec', 'system', 'passthru', + 'file_get_contents', 'file_put_contents', 'fopen', 'fwrite', + '../', '..\\', '/etc/', 'C:\\', 'concat', 'char', + ); + + foreach ( $request as $key => $value ) { + if ( is_string( $value ) ) { + $value_lower = strtolower( $value ); + foreach ( $suspicious_indicators as $indicator ) { + if ( strpos( $value_lower, $indicator ) !== false ) { + return true; + } + } + } + } + + return false; + } + + /** + * Track failed login attempt. + * + * @since 7.1.0 + * @param string $username Username that failed. + */ + public function track_failed_login( string $username ): void { + $ip = $this->get_client_ip(); + $now = time(); + + if ( ! isset( $this->failed_logins[ $ip ] ) ) { + $this->failed_logins[ $ip ] = array(); + } + + // Clean old attempts (older than 1 hour) + $this->failed_logins[ $ip ] = array_filter( + $this->failed_logins[ $ip ], + function ( $time ) use ( $now ) { + return ( $now - $time ) < 3600; + } + ); + + $this->failed_logins[ $ip ][] = $now; + + // Block after threshold + $threshold = apply_filters( 'ai_security_failed_login_threshold', 5 ); + if ( count( $this->failed_logins[ $ip ] ) > $threshold ) { + $this->handle_threat( + 'brute_force', + 'Brute Force Attack', + 'high', + 'Too many failed login attempts: ' . count( $this->failed_logins[ $ip ] ) + ); + } + } + + /** + * Check rate limit. + * + * @since 7.1.0 + * @return bool + */ + private function check_rate_limit(): bool { + $ip = $this->get_client_ip(); + $now = time(); + + if ( ! isset( $this->request_rates[ $ip ] ) ) { + $this->request_rates[ $ip ] = array(); + } + + // Remove entries older than 60 seconds + $this->request_rates[ $ip ] = array_filter( + $this->request_rates[ $ip ], + function ( $time ) use ( $now ) { + return ( $now - $time ) < 60; + } + ); + + // Allow max 60 requests per minute (configurable) + $limit = apply_filters( 'ai_security_rate_limit', 60 ); + if ( count( $this->request_rates[ $ip ] ) > $limit ) { + return false; + } + + $this->request_rates[ $ip ][] = $now; + return true; + } + + /** + * Analyze request with AI. + * + * @since 7.1.0 + * @param array $request Request data. + */ + private function analyze_with_ai( array $request ): void { + $client = Client::get_instance(); + + // Create summary of request (sanitized) + $summary = array( + 'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN', + 'uri' => $_SERVER['REQUEST_URI'] ?? '', + 'params' => array(), + ); + + // Add non-sensitive params + foreach ( $request as $key => $value ) { + if ( is_string( $value ) && strlen( $value ) < 200 ) { + $summary['params'][ $key ] = $value; + } + } + + $analysis = $client->analyze_request( $summary ); + + if ( $analysis && isset( $analysis['is_threat'] ) && $analysis['is_threat'] ) { + $confidence = $analysis['confidence'] ?? 0; + $severity = $confidence > 80 ? 'high' : ( $confidence > 50 ? 'medium' : 'low' ); + + $this->handle_threat( + $analysis['threat_type'] ?? 'ai_detected', + 'AI-Detected Threat', + $severity, + $analysis['details'] ?? 'AI analysis flagged potential threat' + ); + } + } + + /** + * Handle detected threat. + * + * @since 7.1.0 + * @param string $type Threat type. + * @param string $name Display name. + * @param string $severity Severity level. + * @param string $details Details. + */ + private function handle_threat( string $type, string $name, string $severity, string $details ): void { + $ip = $this->get_client_ip(); + + // Log the threat + $logger = Audit_Logger::get_instance(); + $logger->log( + 'threat_detected', + $severity, + $type . ': ' . $details, + array( + 'ip' => $ip, + 'threat_type' => $type, + 'threat_name' => $name, + ) + ); + + // Block critical and high severity threats + if ( in_array( $severity, array( 'critical', 'high' ), true ) ) { + $this->block_ip( $ip, $type . ' - ' . $name ); + $this->send_notification( $type, $name, $severity, $details, $ip ); + } else { + // Log but don't block medium/low + $this->send_notification( $type, $name, $severity, $details, $ip ); + } + + /** + * Fire action for other plugins/themes to respond. + * + * @since 7.1.0 + * + * @param string $type Threat type. + * @param string $name Threat name. + * @param string $severity Severity. + * @param string $details Details. + * @param string $ip Client IP. + */ + do_action( 'ai_security_threat_detected', $type, $name, $severity, $details, $ip ); + } + + /** + * Block an IP address. + * + * @since 7.1.0 + * @param string $ip IP address. + * @param string $reason Reason for blocking. + */ + public function block_ip( string $ip, string $reason ): void { + // Add to blocked IPs + $this->blocked_ips[ $ip ] = array( + 'reason' => $reason, + 'blocked_at' => time(), + 'automated' => true, + ); + + update_option( 'ai_security_blocked_ips', $this->blocked_ips ); + + // Log the block + $logger = Audit_Logger::get_instance(); + $logger->log( 'ip_blocked', 'critical', 'Blocked: ' . $ip . ' - ' . $reason ); + + /** + * Fire action when IP is blocked. + * + * @since 7.1.0 + * + * @param string $ip Blocked IP. + * @param string $reason Reason. + */ + do_action( 'ai_security_ip_blocked', $ip, $reason ); + } + + /** + * Check if IP is blocked. + * + * @since 7.1.0 + * @param string $ip IP address. + * @return bool + */ + public function is_ip_blocked( string $ip ): bool { + return isset( $this->blocked_ips[ $ip ] ); + } + + /** + * Handle blocked IP request. + * + * @since 7.1.0 + * @param string $ip IP address. + */ + private function handle_blocked_ip( string $ip ): void { + $reason = $this->blocked_ips[ $ip ]['reason'] ?? 'Blocked'; + + $logger = Audit_Logger::get_instance(); + $logger->log( 'blocked_ip_request', 'medium', 'Blocked IP attempted access: ' . $ip ); + + status_header( 403 ); + wp_die( + esc_html__( 'Access blocked by AI Security. Contact administrator if this is an error.', 'ai-security' ), + esc_html__( 'Access Blocked', 'ai-security' ), + array( 'response' => 403 ) + ); + } + + /** + * Get client IP address. + * + * @since 7.1.0 + * @return string + */ + private function get_client_ip(): string { + $ip = ''; + + // Check for proxies + if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { + $ip = $_SERVER['HTTP_X_REAL_IP']; + } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { + $ip = $_SERVER['REMOTE_ADDR']; + } + + // Take first IP if multiple + $ip = explode( ',', $ip )[0]; + $ip = trim( $ip ); + + return $ip; + } + + /** + * Send threat notification. + * + * @since 7.1.0 + * @param string $type Threat type. + * @param string $name Threat name. + * @param string $severity Severity. + * @param string $details Details. + * @param string $ip Client IP. + */ + private function send_notification( string $type, string $name, string $severity, string $details, string $ip ): void { + // Check if notifications are enabled + $enabled = get_option( 'ai_security_notifications_enabled', true ); + if ( ! $enabled ) { + return; + } + + // Rate limit notifications (max 1 per hour per type) + $notification_key = $type . '_' . $severity; + $now = time(); + $cooldown = 3600; // 1 hour + + if ( isset( $this->last_notification[ $notification_key ] ) ) { + if ( ( $now - $this->last_notification[ $notification_key ] ) < $cooldown ) { + return; + } + } + $this->last_notification[ $notification_key ] = $now; + + // Email notification + $this->send_email_notification( $type, $name, $severity, $details, $ip ); + + // Webhook notification + $this->send_webhook_notification( $type, $name, $severity, $details, $ip ); + } + + /** + * Send email notification. + * + * @since 7.1.0 + * @param string $type Threat type. + * @param string $name Threat name. + * @param string $severity Severity. + * @param string $details Details. + * @param string $ip Client IP. + */ + private function send_email_notification( string $type, string $name, string $severity, string $details, string $ip ): void { + $email = get_option( 'ai_security_notification_email', get_option( 'admin_email' ) ); + + if ( empty( $email ) ) { + return; + } + + $subject = sprintf( + '[%s] AI Security Alert: %s', + get_bloginfo( 'name' ), + $name + ); + + $message = sprintf( + "AI Security Threat Detected\n" . + "==========================\n\n" . + "Type: %s\n" . + "Severity: %s\n" . + "Details: %s\n" . + "IP Address: %s\n" . + "Time: %s\n\n" . + "URL: %s\n" . + "User: %s\n", + $type, + ucfirst( $severity ), + $details, + $ip, + current_time( 'mysql' ), + esc_url( home_url( $_SERVER['REQUEST_URI'] ?? '' ) ), + esc_html( wp_get_current_user()->user_login ?? 'Guest' ) + ); + + wp_mail( $email, $subject, $message ); + } + + /** + * Send webhook notification. + * + * @since 7.1.0 + * @param string $type Threat type. + * @param string $name Threat name. + * @param string $severity Severity. + * @param string $details Details. + * @param string $ip Client IP. + */ + private function send_webhook_notification( string $type, string $name, string $severity, string $details, string $ip ): void { + $webhook_url = get_option( 'ai_security_webhook_url', '' ); + + if ( empty( $webhook_url ) ) { + return; + } + + $payload = array( + 'type' => 'security_threat', + 'site_url' => site_url(), + 'threat' => array( + 'type' => $type, + 'name' => $name, + 'severity' => $severity, + 'details' => $details, + 'ip' => $ip, + 'time' => current_time( 'mysql' ), + ), + ); + + // Send async request + wp_remote_post( + $webhook_url, + array( + 'method' => 'POST', + 'body' => json_encode( $payload ), + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'blocking' => false, + ) + ); + } + + /** + * Cleanup old data. + * + * @since 7.1.0 + */ + public function cleanup(): void { + $now = time(); + + // Clean old failed logins + foreach ( $this->failed_logins as $ip => $times ) { + $this->failed_logins[ $ip ] = array_filter( + $times, + function ( $time ) use ( $now ) { + return ( $now - $time ) < 3600; + } + ); + } + + // Clean old blocked IPs (after 24 hours, unless manual) + $changed = false; + foreach ( $this->blocked_ips as $ip => $data ) { + if ( isset( $data['blocked_at'] ) && isset( $data['automated'] ) && $data['automated'] ) { + if ( ( $now - $data['blocked_at'] ) > 86400 ) { + unset( $this->blocked_ips[ $ip ] ); + $changed = true; + } + } + } + + if ( $changed ) { + update_option( 'ai_security_blocked_ips', $this->blocked_ips ); + } + } + + /** + * Get threat statistics. + * + * @since 7.1.0 + * @return array + */ + public function get_stats(): array { + $logger = Audit_Logger::get_instance(); + + return array( + 'threats_today' => count( $logger->get_logs_by_event( 'threat_detected', 100 ) ), + 'blocked_ips' => count( $this->blocked_ips ), + 'failed_logins' => array_sum( array_map( 'count', $this->failed_logins ) ), + 'patterns_count' => count( $this->patterns ), + ); + } +} \ No newline at end of file diff --git a/src/wp-includes/ai-security/firewall/class-wp-firewall.php b/src/wp-includes/ai-security/firewall/class-wp-firewall.php new file mode 100644 index 0000000000000..3cf035b38e32e --- /dev/null +++ b/src/wp-includes/ai-security/firewall/class-wp-firewall.php @@ -0,0 +1,535 @@ +init_hooks(); + } + + /** + * Initialize hooks. + * + * @since 7.1.0 + */ + private function init_hooks(): void { + // Check blocked IPs at init priority 0 (before anything else) + add_action( 'init', array( $this, 'check_blocked_ips' ), 0 ); + + // Rate limiting check + add_action( 'init', array( $this, 'check_rate_limit' ), 1 ); + + // Apply firewall rules + add_action( 'init', array( $this, 'apply_firewall_rules' ), 2 ); + + // Country blocking check + add_action( 'init', array( $this, 'check_country_block' ), 3 ); + } + + /** + * Check if current IP is blocked. + * + * @since 7.1.0 + */ + public function check_blocked_ips(): void { + $blocked = get_option( 'ai_security_blocked_ips', array() ); + $ip = $this->get_client_ip(); + + if ( isset( $blocked[ $ip ] ) ) { + status_header( 403 ); + wp_die( 'Your IP has been blocked by AI Security.' ); + } + } + + /** + * Block an IP. + * + * @since 7.1.0 + * @param string $ip IP address. + * @param string $reason Reason. + * @return bool + */ + public function block_ip( string $ip, string $reason = 'Manual block' ): bool { + $blocked = get_option( 'ai_security_blocked_ips', array() ); + $blocked[ $ip ] = array( + 'reason' => $reason, + 'blocked_at' => time(), + 'manual' => true, + ); + return update_option( 'ai_security_blocked_ips', $blocked ); + } + + /** + * Unblock an IP. + * + * @since 7.1.0 + * @param string $ip IP address. + * @return bool + */ + public function unblock_ip( string $ip ): bool { + $blocked = get_option( 'ai_security_blocked_ips', array() ); + unset( $blocked[ $ip ] ); + return update_option( 'ai_security_blocked_ips', $blocked ); + } + + /** + * Get all blocked IPs. + * + * @since 7.1.0 + * @return array + */ + public function get_blocked_ips(): array { + return get_option( 'ai_security_blocked_ips', array() ); + } + + /** + * Add firewall rule. + * + * @since 7.1.0 + * @param array $rule Rule configuration. + * @return bool + */ + public function add_rule( array $rule ): bool { + $rules = get_option( 'ai_security_firewall_rules', array() ); + $rules[] = array( + 'id' => uniqid( 'rule_' ), + 'pattern' => $rule['pattern'] ?? '', + 'action' => $rule['action'] ?? 'block', + 'enabled' => true, + 'created' => time(), + ); + return update_option( 'ai_security_firewall_rules', $rules ); + } + + /** + * Get all firewall rules. + * + * @since 7.1.0 + * @return array + */ + public function get_rules(): array { + return get_option( 'ai_security_firewall_rules', array() ); + } + + /** + * Get client IP address. + * + * @since 7.1.0 + * @return string + */ + private function get_client_ip(): string { + $ip = ''; + + if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { + $ip = $_SERVER['HTTP_X_REAL_IP']; + } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { + $ip = $_SERVER['REMOTE_ADDR']; + } + + return explode( ',', $ip )[0]; + } + + /** + * Check rate limits for current request. + * + * @since 7.1.0 + */ + public function check_rate_limit(): void { + $rate_limit = (int) get_option( 'ai_security_rate_limit', 60 ); + if ( $rate_limit <= 0 ) { + return; + } + + $ip = $this->get_client_ip(); + $key = self::RATE_LIMIT_PREFIX . md5( $ip ); + $request = $this->get_request_signature(); + + $transient = get_transient( $key ); + + if ( false === $transient ) { + set_transient( $key, array( $request => 1 ), 60 ); + return; + } + + // Count requests in last minute for this IP + $request_count = 0; + foreach ( $transient as $sig => $count ) { + $request_count += $count; + } + + if ( $request_count >= $rate_limit ) { + $this->handle_rate_limit_exceeded( $ip ); + } else { + $transient[ $request ] = ( $transient[ $request ] ?? 0 ) + 1; + set_transient( $key, $transient, 60 ); + } + } + + /** + * Get request signature for rate limiting. + * + * @since 7.1.0 + * @return string + */ + private function get_request_signature(): string { + $uri = $_SERVER['REQUEST_URI'] ?? ''; + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $query = $_SERVER['QUERY_STRING'] ?? ''; + + return md5( $method . '|' . $uri . '|' . $query ); + } + + /** + * Handle rate limit exceeded. + * + * @since 7.1.0 + * @param string $ip IP address. + */ + private function handle_rate_limit_exceeded( string $ip ): void { + $threshold = (int) get_option( 'ai_security_failed_login_threshold', 5 ); + + // Track rate limit violations + $violations = (int) get_transient( self::RATE_LIMIT_PREFIX . 'violations_' . md5( $ip ) ); + $violations++; + set_transient( self::RATE_LIMIT_PREFIX . 'violations_' . md5( $ip ), $violations, 300 ); + + // Log the event + \WordPress\AI_Security\Audit_Logger::get_instance()->log( + 'rate_limit_exceeded', + 'high', + 'Rate limit exceeded for IP: ' . $ip . ' (' . $violations . ' violations)' + ); + + // Auto-block if threshold reached + if ( $violations >= $threshold && get_option( 'ai_security_auto_block', true ) ) { + $this->block_ip( $ip, 'Rate limit exceeded (' . $violations . ' violations)' ); + } + + status_header( 429 ); + wp_die( + esc_html__( 'Rate limit exceeded. Please try again later.', 'ai-security' ), + esc_html__( 'Too Many Requests', 'ai-security' ), + array( 'response' => 429 ) + ); + } + + /** + * Apply firewall rules to current request. + * + * @since 7.1.0 + */ + public function apply_firewall_rules(): void { + $rules = $this->get_rules(); + if ( empty( $rules ) ) { + return; + } + + $request = $this->get_request_for_matching(); + + foreach ( $rules as $rule ) { + if ( ! $rule['enabled'] ?? true ) { + continue; + } + + $pattern = $rule['pattern'] ?? ''; + if ( empty( $pattern ) ) { + continue; + } + + // Simple pattern matching (supports regex) + if ( @preg_match( '/' . $pattern . '/', $request['uri'] ) + || @preg_match( '/' . $pattern . '/', $request['full'] ) ) { + + $action = $rule['action'] ?? 'block'; + + switch ( $action ) { + case 'block': + $this->block_ip( + $this->get_client_ip(), + 'Firewall rule matched: ' . ( $rule['id'] ?? 'unknown' ) + ); + status_header( 403 ); + wp_die( esc_html__( 'Request blocked by firewall.', 'ai-security' ) ); + break; + + case 'log': + \WordPress\AI_Security\Audit_Logger::get_instance()->log( + 'firewall_block', + 'medium', + 'Firewall rule matched: ' . $pattern + ); + break; + + case 'challenge': + // Simple captcha challenge (could be enhanced) + if ( ! isset( $_REQUEST['ai_security_challenge'] ) ) { + wp_die( + '

' . esc_html__( 'Security challenge. Please confirm you are human.', 'ai-security' ) . '

' . + '
' . + '
', + esc_html__( 'Security Check', 'ai-security' ), + array( 'response' => 403 ) + ); + } + break; + } + } + } + } + + /** + * Get request data for rule matching. + * + * @since 7.1.0 + * @return array + */ + private function get_request_for_matching(): array { + $uri = $_SERVER['REQUEST_URI'] ?? ''; + $host = $_SERVER['HTTP_HOST'] ?? ''; + $ref = $_SERVER['HTTP_REFERER'] ?? ''; + + return array( + 'uri' => $uri, + 'full' => $host . $uri, + 'referer' => $ref, + 'query' => $_SERVER['QUERY_STRING'] ?? '', + ); + } + + /** + * Delete a firewall rule. + * + * @since 7.1.0 + * @param string $rule_id Rule ID. + * @return bool + */ + public function delete_rule( string $rule_id ): bool { + $rules = get_option( 'ai_security_firewall_rules', array() ); + foreach ( $rules as $i => $rule ) { + if ( ( $rule['id'] ?? '' ) === $rule_id ) { + unset( $rules[ $i ] ); + return update_option( 'ai_security_firewall_rules', array_values( $rules ) ); + } + } + return false; + } + + /** + * Toggle a firewall rule. + * + * @since 7.1.0 + * @param string $rule_id Rule ID. + * @param bool $enabled Enable or disable. + * @return bool + */ + public function toggle_rule( string $rule_id, bool $enabled ): bool { + $rules = get_option( 'ai_security_firewall_rules', array() ); + foreach ( $rules as $i => $rule ) { + if ( ( $rule['id'] ?? '' ) === $rule_id ) { + $rules[ $i ]['enabled'] = $enabled; + return update_option( 'ai_security_firewall_rules', $rules ); + } + } + return false; + } + + /** + * Get country code from IP using free GeoIP service. + * + * @since 7.1.0 + * @param string $ip IP address. + * @return string|null Country code or null if lookup fails. + */ + public function get_country_from_ip( string $ip ): ?string { + // Skip private/local IPs + if ( $this->is_private_ip( $ip ) ) { + return null; + } + + // Use ip-api.com free service (100 requests/minute, 10k/month) + $response = wp_remote_get( + 'http://ip-api.com/json/' . $ip . '?fields=status,countryCode', + array( 'timeout' => 5 ) + ); + + if ( is_wp_error( $response ) ) { + return null; + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( isset( $data['status'] ) && 'success' === $data['status'] ) { + return $data['countryCode'] ?? null; + } + + return null; + } + + /** + * Check if IP is private/local. + * + * @since 7.1.0 + * @param string $ip IP address. + * @return bool + */ + private function is_private_ip( string $ip ): bool { + $private_ranges = array( + '^10\.', + '^172\.(1[6-9]|2[0-9]|3[0-1])\.', + '^192\.168\.', + '^127\.', + '^localhost$', + ); + + foreach ( $private_ranges as $range ) { + if ( preg_match( '/' . $range . '/', $ip ) ) { + return true; + } + } + + return false; + } + + /** + * Block countries. + * + * @since 7.1.0 + * @param array $countries Array of country codes to block. + * @return bool + */ + public function set_blocked_countries( array $countries ): bool { + return update_option( 'ai_security_blocked_countries', array_map( 'strtoupper', $countries ) ); + } + + /** + * Get blocked countries. + * + * @since 7.1.0 + * @return array + */ + public function get_blocked_countries(): array { + return get_option( 'ai_security_blocked_countries', array() ); + } + + /** + * Check if current country is blocked. + * + * @since 7.1.0 + */ + public function check_country_block(): void { + $blocked_countries = $this->get_blocked_countries(); + if ( empty( $blocked_countries ) ) { + return; + } + + $ip = $this->get_client_ip(); + if ( $this->is_private_ip( $ip ) ) { + return; + } + + // Use cached country if available + $cache_key = 'ai_sec_country_' . md5( $ip ); + $country = get_transient( $cache_key ); + + if ( false === $country ) { + $country = $this->get_country_from_ip( $ip ); + // Cache for 24 hours (country doesn't change) + set_transient( $cache_key, $country ?: 'unknown', DAY_IN_SECONDS ); + } + + if ( $country && in_array( strtoupper( $country ), array_map( 'strtoupper', $blocked_countries ), true ) ) { + $this->block_ip( $ip, 'Country blocked: ' . $country ); + status_header( 403 ); + wp_die( 'Access from your country is not allowed.' ); + } + } + + /** + * Get statistics for dashboard. + * + * @since 7.1.0 + * @return array + */ + public function get_stats(): array { + $blocked_ips = $this->get_blocked_ips(); + $rules = $this->get_rules(); + $countries = $this->get_blocked_countries(); + + $manual_blocks = 0; + $auto_blocks = 0; + + foreach ( $blocked_ips as $ip => $data ) { + if ( isset( $data['manual'] ) && $data['manual'] ) { + $manual_blocks++; + } else { + $auto_blocks++; + } + } + + $enabled_rules = 0; + foreach ( $rules as $rule ) { + if ( $rule['enabled'] ?? true ) { + $enabled_rules++; + } + } + + return array( + 'blocked_ips' => count( $blocked_ips ), + 'manual_blocks' => $manual_blocks, + 'auto_blocks' => $auto_blocks, + 'total_rules' => count( $rules ), + 'enabled_rules' => $enabled_rules, + 'blocked_countries' => count( $countries ), + ); + } +} \ No newline at end of file diff --git a/src/wp-includes/ai-security/logger/class-wp-audit-logger.php b/src/wp-includes/ai-security/logger/class-wp-audit-logger.php new file mode 100644 index 0000000000000..9a227e59135c4 --- /dev/null +++ b/src/wp-includes/ai-security/logger/class-wp-audit-logger.php @@ -0,0 +1,309 @@ +get_logs(); + + $logs[] = array( + 'time' => current_time( 'mysql' ), + 'event' => $event, + 'severity' => $severity, + 'details' => $details, + 'ip' => $this->get_client_ip(), + 'user_id' => get_current_user_id(), + 'context' => $context, + ); + + // Trim old logs + if ( count( $logs ) > self::MAX_LOGS ) { + $logs = array_slice( $logs, -self::MAX_LOGS ); + } + + update_option( self::LOG_OPTION, $logs ); + } + + /** + * Get recent logs. + * + * @since 7.1.0 + * @param int $limit Number of logs to return. + * @return array + */ + public function get_recent_logs( int $limit = 50 ): array { + $logs = $this->get_logs(); + return array_slice( $logs, -$limit ); + } + + /** + * Get logs filtered by severity. + * + * @since 7.1.0 + * @param string $severity Severity to filter. + * @param int $limit Number of logs. + * @return array + */ + public function get_logs_by_severity( string $severity, int $limit = 50 ): array { + $logs = $this->get_logs(); + $filtered = array_filter( + $logs, + function ( $log ) use ( $severity ) { + return isset( $log['severity'] ) && $log['severity'] === $severity; + } + ); + return array_slice( array_values( $filtered ), -$limit ); + } + + /** + * Get logs by event type. + * + * @since 7.1.0 + * @param string $event Event type. + * @param int $limit Number of logs. + * @return array + */ + public function get_logs_by_event( string $event, int $limit = 50 ): array { + $logs = $this->get_logs(); + $filtered = array_filter( + $logs, + function ( $log ) use ( $event ) { + return isset( $log['event'] ) && $log['event'] === $event; + } + ); + return array_slice( array_values( $filtered ), -$limit ); + } + + /** + * Clear all logs. + * + * @since 7.1.0 + */ + public function clear_logs(): void { + delete_option( self::LOG_OPTION ); + } + + /** + * Export logs. + * + * @since 7.1.0 + * @param string $format Format (json, csv). + * @return string + */ + public function export( string $format = 'json' ): string { + $logs = $this->get_logs(); + + if ( 'csv' === $format ) { + $output = "Time,Event,Severity,Details,IP,User ID\n"; + foreach ( $logs as $log ) { + $output .= sprintf( + '"%s","%s","%s","%s","%s","%d"' . "\n", + $log['time'], + $log['event'], + $log['severity'], + str_replace( '"', '""', $log['details'] ), + $log['ip'] ?? '', + $log['user_id'] ?? 0 + ); + } + return $output; + } + + return json_encode( $logs, JSON_PRETTY_PRINT ); + } + + /** + * Get all logs. + * + * @since 7.1.0 + * @return array + */ + private function get_logs(): array { + return get_option( self::LOG_OPTION, array() ); + } + + /** + * Get client IP address. + * + * @since 7.1.0 + * @return string + */ + private function get_client_ip(): string { + $ip = ''; + + if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { + $ip = $_SERVER['HTTP_X_REAL_IP']; + } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { + $ip = $_SERVER['REMOTE_ADDR']; + } + + return explode( ',', $ip )[0]; + } + + /** + * Get log statistics. + * + * @since 7.1.0 + * @return array + */ + public function get_stats(): array { + $logs = $this->get_logs(); + + $stats = array( + 'total' => count( $logs ), + 'critical' => 0, + 'high' => 0, + 'medium' => 0, + 'low' => 0, + 'info' => 0, + ); + + foreach ( $logs as $log ) { + $sev = $log['severity'] ?? 'info'; + if ( isset( $stats[ $sev ] ) ) { + $stats[ $sev ]++; + } else { + $stats['info']++; + } + } + + return $stats; + } + + /** + * Generate PDF report. + * + * @since 7.1.0 + * @param array $logs Logs to include. + * @return string HTML content for PDF. + */ + public function generate_report_html( array $logs ): string { + $stats = $this->get_stats(); + $date = current_time( 'mysql' ); + + $html = ' + + +Security Audit Report - ' . esc_html( $date ) . ' + + +

Security Audit Report

+

Generated: ' . esc_html( $date ) . '

+ +
+
Critical: ' . $stats['critical'] . '
+
High: ' . $stats['high'] . '
+
Medium: ' . $stats['medium'] . '
+
Low: ' . $stats['low'] . '
+
Total: ' . $stats['total'] . '
+
+ +

Recent Events

+ + +'; + + foreach ( $logs as $log ) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + $html .= '
TimeEventSeverityDetailsIP
' . esc_html( $log['time'] ) . '' . esc_html( $log['event'] ) . '' . esc_html( $log['severity'] ) . '' . esc_html( $log['details'] ) . '' . esc_html( $log['ip'] ?? '-' ) . '
'; + + return $html; + } + + /** + * Export as PDF (generates HTML that can be printed to PDF). + * + * @since 7.1.0 + * @param int $limit Number of logs to include. + */ + public function export_pdf( int $limit = 100 ): void { + $logs = $this->get_recent_logs( $limit ); + $html = $this->generate_report_html( $logs ); + + header( 'Content-Type: text/html' ); + header( 'Content-Disposition: attachment; filename="security-report-' . date( 'Y-m-d' ) . '.html"' ); + echo $html; + exit; + } +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-abstract-connector-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-abstract-connector-provider.php new file mode 100644 index 0000000000000..7f48c9fadd88e --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-abstract-connector-provider.php @@ -0,0 +1,240 @@ + + */ + protected static array $models = array(); + + /** + * Model requirements per model. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array(); + + /** + * Get provider metadata. + * + * @since 7.1.0 + * @return ProviderMetadata + */ + public static function metadata(): ProviderMetadata { + $auth_method = null; + if ( ! static::is_local() ) { + $auth_method = new RequestAuthenticationMethod( RequestAuthenticationMethod::API_KEY ); + } + + return new ProviderMetadata( + static::$provider_id, + static::$provider_name, + static::is_local() ? ProviderTypeEnum::SERVER : ProviderTypeEnum::CLOUD, + static::$credentials_url, + $auth_method, + static::$description + ); + } + + /** + * Check if provider is local (like Ollama). + * + * @since 7.1.0 + * @return bool + */ + protected static function is_local(): bool { + return false; + } + + /** + * Get available models. + * + * @since 7.1.0 + * @return array + */ + public static function get_available_models(): array { + return static::$models; + } + + /** + * Create a model instance. + * + * @since 7.1.0 + * @param string $model_id Model identifier. + * @param ModelConfig|null $config Model configuration. + * @return ModelInterface + * @throws InvalidArgumentException If model not found. + */ + public static function model( string $model_id, ?ModelConfig $config = null ): ModelInterface { + $models = static::get_available_models(); + + if ( ! isset( $models[ $model_id ] ) ) { + throw new InvalidArgumentException( + sprintf( + __( 'Model "%s" not found for provider "%s".', 'default' ), + $model_id, + static::$provider_name + ) + ); + } + + $model_class = $models[ $model_id ]; + + if ( ! class_exists( $model_class ) ) { + throw new InvalidArgumentException( + sprintf( + __( 'Model class "%s" not found.', 'default' ), + $model_class + ) + ); + } + + return new $model_class( $model_id, $config ); + } + + /** + * Get provider availability. + * + * @since 7.1.0 + * @return \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface + */ + public static function availability(): \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface { + return new class implements \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface { + public function is_available(): bool { + return true; + } + public function get_status(): string { + return 'available'; + } + }; + } + + /** + * Get model metadata directory. + * + * @since 7.1.0 + * @return \WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface + */ + public static function modelMetadataDirectory(): \WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface { + $requirements = static::$model_requirements; + + return new class( $requirements ) implements \WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface { + private array $requirements; + + public function __construct( array $requirements ) { + $this->requirements = $requirements; + } + + public function get( string $model_id ): ?ModelMetadata { + if ( ! isset( $this->requirements[ $model_id ] ) ) { + return null; + } + return ModelMetadata::fromArray( $this->requirements[ $model_id ] ); + } + + public function has( string $model_id ): bool { + return isset( $this->requirements[ $model_id ] ); + } + + public function all(): array { + $all = array(); + foreach ( $this->requirements as $model_id => $data ) { + $all[ $model_id ] = ModelMetadata::fromArray( $data ); + } + return $all; + } + }; + } + + /** + * Get API base URL. + * + * @since 7.1.0 + * @return string + */ + public static function get_api_base_url(): string { + return static::$api_base_url; + } + + /** + * Get authentication instance. + * + * @since 7.1.0 + * @param string $api_key API key. + * @return \WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface + */ + public static function get_authentication( string $api_key ): \WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface { + return new ApiKeyRequestAuthentication( $api_key ); + } +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-deepseek-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-deepseek-provider.php new file mode 100644 index 0000000000000..a71e5aa14349a --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-deepseek-provider.php @@ -0,0 +1,104 @@ + + */ + protected static array $models = array( + 'deepseek-chat' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'deepseek-coder' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'deepseek-reasoner' => AbstractOpenAiCompatibleTextGenerationModel::class, + ); + + /** + * Model requirements. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array( + 'deepseek-chat' => array( + 'context_window' => 64000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + '_supports_streaming' => true, + ), + 'deepseek-coder' => array( + 'context_window' => 64000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'deepseek-reasoner' => array( + 'context_window' => 64000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => false, + 'supports_streaming' => false, + ), + ); +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-mistral-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-mistral-provider.php new file mode 100644 index 0000000000000..981cae193b4fc --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-mistral-provider.php @@ -0,0 +1,108 @@ + + */ + protected static array $models = array( + 'mistral-large-latest' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'mistral-medium-latest' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'mistral-small-latest' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'codestral-latest' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'codestral-mamba' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'pixtral-large-latest' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'pixtral-12b-2409' => AbstractOpenAiCompatibleTextGenerationModel::class, + ); + + /** + * Model requirements. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array( + 'mistral-large-latest' => array( + 'context_window' => 128000, + 'max_output_tokens' => 16384, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'mistral-small-latest' => array( + 'context_window' => 32000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'codestral-latest' => array( + 'context_window' => 64000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + ); +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-ollama-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-ollama-provider.php new file mode 100644 index 0000000000000..d159babc52a96 --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-ollama-provider.php @@ -0,0 +1,145 @@ + + */ + protected static array $models = array( + 'llama3.3' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'llama3.2' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'llama3.1' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'llama3' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'llama2' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'mistral' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'mixtral' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'phi3' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'phi4' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'qwen2.5' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'codellama' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'deepseek-coder' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'gemma2' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'command-r' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'command-r-plus' => AbstractOpenAiCompatibleTextGenerationModel::class, + ); + + /** + * Model requirements. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array( + 'llama3.3' => array( + 'context_window' => 128000, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'mistral' => array( + 'context_window' => 8192, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'codellama' => array( + 'context_window' => 16384, + 'max_output_tokens' => 4096, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + ); + + /** + * Check if provider is local. + * + * @since 7.1.0 + * @return bool + */ + protected static function is_local(): bool { + return true; + } + + /** + * Get provider availability - checks if Ollama is running. + * + * @since 7.1.0 + * @return \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface + */ + public static function availability(): \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface { + return new class implements \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface { + public function is_available(): bool { + $endpoint = get_option( 'ai_connector_pro_ollama_endpoint', 'http://localhost:11434' ); + $response = wp_remote_get( $endpoint . '/api/tags', array( 'timeout' => 5 ) ); + return ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200; + } + public function get_status(): string { + return $this->is_available() ? 'available' : 'unavailable'; + } + }; + } +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-openrouter-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-openrouter-provider.php new file mode 100644 index 0000000000000..96e1604bc87fd --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-openrouter-provider.php @@ -0,0 +1,123 @@ + + */ + protected static array $models = array( + // Anthropic + 'anthropic/claude-3.5-sonnet' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'anthropic/claude-3-opus' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'anthropic/claude-3-haiku' => AbstractOpenAiCompatibleTextGenerationModel::class, + // OpenAI + 'openai/gpt-4o' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'openai/gpt-4o-mini' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'openai/gpt-4-turbo' => AbstractOpenAiCompatibleTextGenerationModel::class, + // Google + 'google/gemini-2.0-flash-exp' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'google/gemini-1.5-pro' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'google/gemini-1.5-flash' => AbstractOpenAiCompatibleTextGenerationModel::class, + // Meta + 'meta-llama/llama-3.3-70b-instruct' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'meta-llama/llama-3.1-405b-instruct' => AbstractOpenAiCompatibleTextGenerationModel::class, + // Mistral + 'mistralai/mistral-large' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'mistralai/codestral' => AbstractOpenAiCompatibleTextGenerationModel::class, + // DeepSeek + 'deepseek/deepseek-chat' => AbstractOpenAiCompatibleTextGenerationModel::class, + // Qwen + 'qwen/qwen-2.5-72b-instruct' => AbstractOpenAiCompatibleTextGenerationModel::class, + ); + + /** + * Model requirements. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array( + 'anthropic/claude-3.5-sonnet' => array( + 'context_window' => 200000, + 'max_output_tokens' => 8192, + 'supports_vision' => true, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'openai/gpt-4o' => array( + 'context_window' => 128000, + 'max_output_tokens' => 16384, + 'supports_vision' => true, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'google/gemini-1.5-flash' => array( + 'context_window' => 1000000, + 'max_output_tokens' => 8192, + 'supports_vision' => true, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + ); +} \ No newline at end of file diff --git a/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-xai-provider.php b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-xai-provider.php new file mode 100644 index 0000000000000..74e3834874ef1 --- /dev/null +++ b/src/wp-includes/php-ai-client/src/Providers/AiConnectorPro/class-xai-provider.php @@ -0,0 +1,98 @@ + + */ + protected static array $models = array( + 'grok-2-1212' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'grok-2' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'grok-beta' => AbstractOpenAiCompatibleTextGenerationModel::class, + 'grok-vision-beta' => AbstractOpenAiCompatibleTextGenerationModel::class, + ); + + /** + * Model requirements. + * + * @since 7.1.0 + * @var array + */ + protected static array $model_requirements = array( + 'grok-2' => array( + 'context_window' => 131072, + 'max_output_tokens' => 8192, + 'supports_vision' => false, + 'supports_functions' => true, + 'supports_streaming' => true, + ), + 'grok-vision-beta' => array( + 'context_window' => 32768, + 'max_output_tokens' => 4096, + 'supports_vision' => true, + 'supports_functions' => false, + 'supports_streaming' => true, + ), + ); +} \ No newline at end of file diff --git a/src/wp-settings.php b/src/wp-settings.php index dab1d8fd4c0de..370393e50eacf 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -294,6 +294,8 @@ require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-ability-function-resolver.php'; require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-prompt-builder.php'; require ABSPATH . WPINC . '/ai-client.php'; +require ABSPATH . WPINC . '/ai-connector-pro.php'; +require ABSPATH . WPINC . '/ai-security/ai-security.php'; require ABSPATH . WPINC . '/class-wp-connector-registry.php'; require ABSPATH . WPINC . '/connectors.php'; require ABSPATH . WPINC . '/class-wp-icons-registry.php';