diff --git a/admin/views/welcome.php b/admin/views/welcome.php
new file mode 100644
index 0000000..19ca5b4
--- /dev/null
+++ b/admin/views/welcome.php
@@ -0,0 +1,139 @@
+ __( 'Firewall', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Blocks malicious requests -- SQL injection, cross-site scripting, and dozens of other known attack patterns -- before they reach your site.', 'argus-wordpress-defence' ),
+ 'active' => $settings['waf_enabled'],
+ ),
+ array(
+ 'title' => __( 'Malware Scanner & Quarantine', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Watches for suspicious files, moves anything genuinely dangerous into a protected quarantine automatically, and lets you review or restore it.', 'argus-wordpress-defence' ),
+ 'active' => $settings['malware_scan_enabled'],
+ ),
+ array(
+ 'title' => __( 'File Integrity Monitoring', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Cross-checks WordPress core against official checksums and tracks plugin/theme file changes, so tampering doesn\'t go unnoticed.', 'argus-wordpress-defence' ),
+ 'active' => $settings['integrity_scan_enabled'],
+ ),
+ array(
+ 'title' => __( 'Login & Brute-Force Protection', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Detects and blocks repeated failed login attempts, and locks down XML-RPC/REST API endpoints commonly abused for credential-stuffing.', 'argus-wordpress-defence' ),
+ 'active' => $settings['login_protection_enabled'],
+ ),
+ array(
+ 'title' => __( 'Vulnerability Intelligence', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Checks WordPress core and every installed plugin/theme version against known security issues, so you know what needs updating and why.', 'argus-wordpress-defence' ),
+ 'active' => $settings['vuln_intel_enabled'],
+ ),
+ array(
+ 'title' => __( 'Backups', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Scheduled database and file backups, stored securely, so you have a real recovery point if something ever goes wrong.', 'argus-wordpress-defence' ),
+ 'active' => 'automatic' === $settings['backup_schedule_mode'],
+ ),
+ array(
+ 'title' => __( 'Automatic Updates', 'argus-wordpress-defence' ),
+ 'desc' => __( 'ARGUS always keeps itself up to date, and can optionally do the same for every other plugin and theme -- unpatched software is one of the most common ways sites get compromised.', 'argus-wordpress-defence' ),
+ 'active' => $settings['auto_update_all_enabled'],
+ ),
+ array(
+ 'title' => __( 'Cache & Performance', 'argus-wordpress-defence' ),
+ 'desc' => __( 'An optional page cache that can make your site noticeably faster -- it never bypasses the firewall or serves cached pages to already-blocked visitors.', 'argus-wordpress-defence' ),
+ 'active' => $settings['static_cache_enabled'],
+ ),
+ array(
+ 'title' => __( 'ARGUS Cloud (Global Threat Intelligence)', 'argus-wordpress-defence' ),
+ 'desc' => __( 'Optional: share reputation data about IPs ARGUS blocks with a global network, and benefit from what every other connected site has already seen.', 'argus-wordpress-defence' ),
+ 'active' => $settings['anis_enabled'],
+ ),
+);
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/argus-wordpress-defence.php b/argus-wordpress-defence.php
index 44db28a..a18e5fe 100644
--- a/argus-wordpress-defence.php
+++ b/argus-wordpress-defence.php
@@ -3,7 +3,7 @@
* Plugin Name: ARGUS WordPress Defence
* Plugin URI: https://git-cloud.weboria.eu/Weboria/argus-wp-defence
* Description: Standalone WordPress security: local WAF, malware & integrity scanning, vulnerability intelligence, and a deterministic ban/policy engine. Works fully offline; optionally connects to ARGUS Cloud for richer intelligence and cross-asset correlation.
- * Version: 1.0.0
+ * Version: 7.23.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: ARGUS
@@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
-define( 'ARGUS_WPD_VERSION', '1.0.0' );
+define( 'ARGUS_WPD_VERSION', '7.23.0' );
define( 'ARGUS_WPD_FILE', __FILE__ );
define( 'ARGUS_WPD_DIR', plugin_dir_path( __FILE__ ) );
define( 'ARGUS_WPD_URL', plugin_dir_url( __FILE__ ) );
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 8f55796..8aaaa5a 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -284,6 +284,26 @@
.argus-wpd-donut-legend-value { font: 700 12px/1 var(--mono); color: var(--muted); font-variant-numeric: tabular-nums; }
.argus-wpd-donut-legend-value em { color: var(--faint); font-style: normal; margin-left: 3px; }
+/* ---------- Welcome / onboarding: feature explainer grid ---------- */
+.argus-wpd-feature-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
+@media (max-width: 900px) { .argus-wpd-feature-grid { grid-template-columns: 1fr; } }
+.argus-wpd-feature-card {
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px;
+}
+.argus-wpd-feature-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
+.argus-wpd-feature-card-title { font: 700 13.5px/1.3 inherit; color: var(--text); }
+.argus-wpd-feature-card-desc { font-size: 12.5px; color: var(--faint); line-height: 1.6; margin: 0; }
+.argus-wpd-welcome-hero { text-align: center; padding: 8px 0 28px; }
+.argus-wpd-welcome-hero h1 { font-size: 24px; margin: 0 0 8px; }
+.argus-wpd-welcome-hero p { font-size: 14px; color: var(--muted); max-width: 560px; margin: 0 auto; }
+
+/* ---------- Console: version tag ---------- */
+.argus-wpd-version {
+ margin-left: 10px; flex-shrink: 0; font: 600 11px/1 var(--mono); letter-spacing: .02em;
+ color: var(--faint); padding: 6px 10px; border-radius: 9px;
+ background: var(--panel); border: 1px solid var(--border-strong);
+}
+
/* ---------- Console: gear icon (Settings, off primary nav) ---------- */
.argus-wpd-gear {
margin-left: 10px; width: 38px; height: 38px; border-radius: 9px; flex-shrink: 0;
@@ -295,6 +315,7 @@
/* ---------- Console: metric cards w/ trend ---------- */
.argus-wpd-metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 20px; }
+.argus-wpd-metric-grid.argus-wpd-metric-grid-2 { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 1100px) { .argus-wpd-metric-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) { .argus-wpd-metric-grid { grid-template-columns: 1fr; } }
.argus-wpd-metric-card {
diff --git a/bin/build-release.sh b/bin/build-release.sh
index 803bbda..e286dd0 100755
--- a/bin/build-release.sh
+++ b/bin/build-release.sh
@@ -31,9 +31,26 @@ rm -rf \
"${STAGE_DIR}/tests" \
"${STAGE_DIR}/README.md" \
"${STAGE_DIR}/ARGUS_WORDPRESS_SECURITY_ARCHITECTURE.md" \
+ "${STAGE_DIR}/WORDPRESS_ORG_READINESS_AUDIT.md" \
"${STAGE_DIR}/.gitignore" \
"${STAGE_DIR}/bin"
+# A plugin hosted on WordPress.org must rely solely on WordPress.org's own
+# update channel -- never a self-update-from-external-manifest mechanism,
+# even one that ships inert-by-default. class_exists( 'Argus_Update_Client' )
+# guards every reference to this class elsewhere in the codebase, so removing
+# the file here is sufficient; nothing else needs to change per build.
+echo "==> Removing the self-update client (WordPress.org must be the only update channel)"
+rm -f "${STAGE_DIR}/includes/class-argus-update-client.php"
+
+# WordPress.org explicitly prohibits trialware -- a plugin submitted to the
+# directory must stay genuinely, permanently functional with no license
+# requirement. class_exists( 'Argus_License' ) guards every reference to
+# this class elsewhere, so removing the file here is sufficient; the free
+# build never has a trial timer at all.
+echo "==> Removing the license/trial system (WordPress.org prohibits trialware)"
+rm -f "${STAGE_DIR}/includes/class-argus-license.php"
+
echo "==> Allowlist audit -- fail closed on anything unexpected"
UNEXPECTED=0
while IFS= read -r -d '' item; do
diff --git a/includes/class-argus-activator.php b/includes/class-argus-activator.php
index 91581ae..9b9d48e 100644
--- a/includes/class-argus-activator.php
+++ b/includes/class-argus-activator.php
@@ -36,6 +36,20 @@ class Argus_Activator {
}
update_option( 'argus_wpd_activated_at', current_time( 'mysql', true ), false );
+
+ // Argus_License exists only in the separately-distributed Premium
+ // build (excluded from the WordPress.org release, see
+ // bin/build-release.sh) -- the free build has no trial timer at all.
+ if ( class_exists( 'Argus_License' ) ) {
+ Argus_License::ensure_trial_started();
+ }
+
+ // Redirect to the Welcome screen on the very next admin_init --
+ // Argus_Admin::maybe_redirect_to_welcome() is the one that decides
+ // whether this was a real single-plugin activation vs. a bulk-activate
+ // or network-wide multisite activation (neither of which should
+ // hijack the admin's next page load), and clears this either way.
+ set_transient( 'argus_wpd_do_activation_redirect', 1, MINUTE_IN_SECONDS );
}
protected static function schedule_cron() {
diff --git a/includes/class-argus-anis-client.php b/includes/class-argus-anis-client.php
index bed09b8..68e6ab8 100644
--- a/includes/class-argus-anis-client.php
+++ b/includes/class-argus-anis-client.php
@@ -558,7 +558,7 @@ class Argus_ANIS_Client {
return array(
'configured' => self::is_configured(),
- 'enabled' => (bool) Argus_Settings::get( 'anis_enabled', true ),
+ 'enabled' => (bool) Argus_Settings::get( 'anis_enabled', false ),
'connected' => self::is_connected(),
'status' => get_option( self::STATUS_OPTION, 'unconfigured' ),
'last_error' => get_option( self::LAST_ERROR_OPTION, '' ),
@@ -574,6 +574,9 @@ class Argus_ANIS_Client {
}
protected static function protection_state() {
+ if ( ! (bool) Argus_Settings::get( 'anis_enabled', false ) ) {
+ return 'disabled';
+ }
if ( ! self::is_enabled() ) {
return 'not_connected';
}
diff --git a/includes/class-argus-auto-update.php b/includes/class-argus-auto-update.php
index 4c569a2..af97e74 100644
--- a/includes/class-argus-auto-update.php
+++ b/includes/class-argus-auto-update.php
@@ -7,20 +7,38 @@ if ( ! defined( 'ABSPATH' ) ) {
class Argus_Auto_Update {
public static function init() {
- add_filter( 'auto_update_plugin', array( __CLASS__, 'force_auto_update' ), 10, 2 );
+ add_filter( 'auto_update_plugin', array( __CLASS__, 'force_plugin_auto_update' ), 10, 2 );
+ add_filter( 'auto_update_theme', array( __CLASS__, 'force_theme_auto_update' ), 10, 2 );
add_filter( 'plugin_auto_update_setting_html', array( __CLASS__, 'lock_admin_toggle' ), 10, 3 );
- add_filter( 'site_option_auto_update_plugins', array( __CLASS__, 'ensure_in_list' ) );
- add_filter( 'option_auto_update_plugins', array( __CLASS__, 'ensure_in_list' ) );
+ add_filter( 'site_option_auto_update_plugins', array( __CLASS__, 'ensure_self_in_list' ) );
+ add_filter( 'option_auto_update_plugins', array( __CLASS__, 'ensure_self_in_list' ) );
}
- public static function force_auto_update( $update, $item ) {
+ // ARGUS always auto-updates itself, independent of the site-wide setting
+ // below -- this is not user-configurable, matching the plugin's own
+ // self-protection design (ADR-0053 SS13.1).
+ public static function force_plugin_auto_update( $update, $item ) {
if ( isset( $item->plugin ) && ARGUS_WPD_BASENAME === $item->plugin ) {
return true;
}
+ if ( Argus_Settings::get( 'auto_update_all_enabled', true ) ) {
+ return true;
+ }
return $update;
}
- public static function ensure_in_list( $list ) {
+ // Uses WordPress core's own native automatic-update system
+ // (WP_Automatic_Updater, driven by the existing wp_version_check/
+ // wp_update_themes cron events and the official WordPress.org Themes
+ // API) -- no custom download/replace code, nothing external.
+ public static function force_theme_auto_update( $update, $item ) {
+ if ( Argus_Settings::get( 'auto_update_all_enabled', true ) ) {
+ return true;
+ }
+ return $update;
+ }
+
+ public static function ensure_self_in_list( $list ) {
$list = is_array( $list ) ? $list : array();
if ( ! in_array( ARGUS_WPD_BASENAME, $list, true ) ) {
$list[] = ARGUS_WPD_BASENAME;
@@ -37,9 +55,15 @@ class Argus_Auto_Update {
}
public static function status() {
+ if ( Argus_Settings::get( 'auto_update_all_enabled', true ) ) {
+ return array(
+ 'label' => __( 'Enabled for All Plugins & Themes', 'argus-wordpress-defence' ),
+ 'detail' => __( 'ARGUS Defence keeps ARGUS itself, and every other installed plugin and theme, on their latest available version automatically -- unpatched plugins/themes are one of the most common ways WordPress sites get compromised. You can turn this off for everything except ARGUS itself below.', 'argus-wordpress-defence' ),
+ );
+ }
return array(
- 'label' => __( 'Enabled and Protected', 'argus-wordpress-defence' ),
- 'detail' => __( 'ARGUS Defence automatically installs important security, protection, stability, and compatibility updates to help keep your website protected. This setting is managed automatically and cannot be disabled from the plugin.', 'argus-wordpress-defence' ),
+ 'label' => __( 'Enabled for ARGUS Only', 'argus-wordpress-defence' ),
+ 'detail' => __( 'ARGUS Defence automatically installs its own updates to stay protected. Automatic updates for your other plugins and themes are off -- turn them on below for stronger baseline protection.', 'argus-wordpress-defence' ),
);
}
}
diff --git a/includes/class-argus-console-stats.php b/includes/class-argus-console-stats.php
index 79793ec..e6478a4 100644
--- a/includes/class-argus-console-stats.php
+++ b/includes/class-argus-console-stats.php
@@ -404,6 +404,11 @@ class Argus_Console_Stats {
'rce' => __( 'Remote Code Execution', 'argus-wordpress-defence' ),
'file_access' => __( 'Path Traversal / LFI', 'argus-wordpress-defence' ),
'protocol_anomaly' => __( 'Protocol Anomaly', 'argus-wordpress-defence' ),
+ 'xxe' => __( 'XML External Entity', 'argus-wordpress-defence' ),
+ 'ssrf' => __( 'Server-Side Request Forgery', 'argus-wordpress-defence' ),
+ 'session_fixation' => __( 'Session Fixation', 'argus-wordpress-defence' ),
+ 'java_injection' => __( 'Java / JNDI Injection', 'argus-wordpress-defence' ),
+ 'scanner_activity' => __( 'Scanner / Recon Activity', 'argus-wordpress-defence' ),
);
return $labels[ $category ] ?? ucwords( str_replace( '_', ' ', $category ) );
}
diff --git a/includes/class-argus-malware-scanner.php b/includes/class-argus-malware-scanner.php
index 29b87ec..7a337b0 100644
--- a/includes/class-argus-malware-scanner.php
+++ b/includes/class-argus-malware-scanner.php
@@ -141,36 +141,68 @@ class Argus_Malware_Scanner {
$rel = str_replace( wp_normalize_path( ABSPATH ), '', wp_normalize_path( $path ) );
if ( self::is_restored_trusted( $rel, hash_file( 'sha256', $path ) ) ) {
- continue;
+ continue;
}
$size = filesize( $path );
$mtime = gmdate( 'Y-m-d H:i:s', filemtime( $path ) );
+ // The detection rule itself ("a PHP file exists inside uploads") is
+ // location-based and unconditional -- WordPress never executes PHP
+ // there by design, so any match is quarantined regardless of content.
+ // The content heuristic score is a SEPARATE signal layered on top,
+ // used only to set an honest severity/verdict -- never to skip
+ // quarantining. This keeps "rule matched" (always true here),
+ // "analysis score" (score_content()'s real number), and "verdict"
+ // (the severity below) from contradicting each other in the UI, e.g.
+ // a benign defensive stub scoring 0 must never be labeled CRITICAL.
+ $content = is_readable( $path ) ? file_get_contents( $path ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions
+ list( $content_score, $matched ) = false !== $content ? self::score_content( $content ) : array( 0, array() );
+
+ if ( $content_score >= self::THRESHOLD_HIGH ) {
+ $final_severity = 'critical';
+ $verdict = 'Its content strongly matches known webshell/backdoor patterns.';
+ } elseif ( $content_score >= self::THRESHOLD_MEDIUM ) {
+ $final_severity = 'high';
+ $verdict = 'Its content contains some patterns also seen in malicious files, though less conclusively.';
+ } else {
+ $final_severity = 'medium';
+ $verdict = 'No suspicious code patterns were found in its content -- it has been quarantined as a precaution because a PHP file in uploads is itself unusual, not because of what it contains.';
+ }
+
$result = Argus_Quarantine::quarantine_file(
$path,
$rel,
array(
- 'engine' => 'Argus_Malware_Scanner',
- 'rule' => 'php-in-uploads',
- 'type' => 'signature',
- 'severity' => 'critical',
+ 'engine' => 'Argus_Malware_Scanner',
+ 'rule' => 'php-in-uploads',
+ 'type' => 'signature',
+ 'severity' => $final_severity,
+ 'content_score' => $content_score,
+ 'matched_rules' => wp_list_pluck( $matched, 'id' ),
)
);
$neutralized = false !== $result;
$finding_id = Argus_Findings::record(
'malware',
- 'critical',
+ $final_severity,
array(
'what_happened' => 'A PHP file was found inside the uploads directory: ' . $rel,
'why_it_matters' => 'WordPress does not execute PHP inside wp-content/uploads by design -- a legitimate install should never have PHP files there.',
- 'what_argus_found' => sprintf( 'File: %s, size %s bytes, last modified %s.', $rel, $size, $mtime ),
+ 'what_argus_found' => sprintf(
+ 'File: %1$s, size %2$s bytes, last modified %3$s. Detection rule "php-in-uploads" matched (location-based, always fires on any .php file here). Content analysis score: %4$d/100%5$s.',
+ $rel,
+ $size,
+ $mtime,
+ $content_score,
+ $matched ? ' (matched: ' . implode( ', ', wp_list_pluck( $matched, 'id' ) ) . ')' : ' (no heuristic patterns matched)'
+ ),
'when_it_happened' => current_time( 'mysql' ),
- 'why_suspicious' => 'This is one of the most common webshell placement techniques -- uploads directories are usually writable by the web application, unlike wp-admin/wp-includes.',
+ 'why_suspicious' => 'This is one of the most common webshell placement techniques -- uploads directories are usually writable by the web application, unlike wp-admin/wp-includes. ' . $verdict,
'what_could_be_affected' => 'A PHP file here can be executed directly by visiting its URL, potentially giving an attacker full code execution on your server.',
'what_should_you_do' => $neutralized
- ? 'This file has been quarantined -- moved to a protected, non-web-accessible store. Review it on the Quarantine page and decide whether to restore or permanently delete it.'
+ ? 'This file has been quarantined -- moved to a protected, non-web-accessible store. Review it on the Quarantine page (its content analysis score is shown there) and decide whether to restore or permanently delete it.'
: 'ARGUS detected this file but could not quarantine it (the uploads directory is not writable by PHP on this host, or it is a symlink ARGUS refuses to move automatically). Remove it manually and investigate how it got there.',
),
array(
@@ -178,6 +210,7 @@ class Argus_Malware_Scanner {
'file_path' => $rel,
'file_mtime' => $mtime,
'neutralized' => $neutralized,
+ 'content_score' => $content_score,
),
Argus_Findings::STATUS_OPEN
);
@@ -196,7 +229,8 @@ class Argus_Malware_Scanner {
}
protected static function scan_recently_changed() {
- $since = (int) get_option( self::SCAN_WATERMARK_OPTION, time() - HOUR_IN_SECONDS );
+ $since = (int) get_option( self::SCAN_WATERMARK_OPTION, time() - HOUR_IN_SECONDS );
+ $own_dir = wp_normalize_path( ARGUS_WPD_DIR );
foreach ( array( WP_PLUGIN_DIR, get_theme_root(), ABSPATH . 'wp-admin', ABSPATH . WPINC ) as $dir ) {
if ( ! is_dir( $dir ) ) {
@@ -210,6 +244,21 @@ class Argus_Malware_Scanner {
if ( ! $file->isFile() || 'php' !== strtolower( $file->getExtension() ) ) {
continue;
}
+ $path = wp_normalize_path( $file->getPathname() );
+
+ // ARGUS never treats its own installed files as a finding about
+ // themselves -- its test fixtures legitimately contain literal
+ // attack-pattern strings (e.g. eval(base64_decode(...)) samples),
+ // and its own WAF/scanner rule definitions legitimately contain
+ // the same keywords as regex source. Genuine tampering with
+ // ARGUS's own code is a different threat model (an already-
+ // compromised server) that a self-scan wouldn't meaningfully
+ // catch anyway. Its own supply-chain integrity is covered by
+ // WordPress.org's review/signing and its own forced self-update.
+ if ( 0 === strpos( $path, $own_dir ) ) {
+ continue;
+ }
+
if ( $file->getMTime() < $since ) {
continue;
}
diff --git a/includes/class-argus-plugin.php b/includes/class-argus-plugin.php
index 788683d..beca262 100644
--- a/includes/class-argus-plugin.php
+++ b/includes/class-argus-plugin.php
@@ -19,6 +19,13 @@ class Argus_Plugin {
Argus_DB::maybe_upgrade();
Argus_MU_Installer::ensure_current();
+ // Premium-build-only (see includes/class-argus-license.php's own
+ // header) -- covers sites that were already active before this
+ // existed, same idempotent-ensure pattern as Argus_MU_Installer above.
+ if ( class_exists( 'Argus_License' ) ) {
+ Argus_License::ensure_trial_started();
+ }
+
add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_schedules' ) ); // phpcs:ignore WordPress.WP.CronInterval
Argus_Login_Guard::init();
@@ -37,7 +44,16 @@ class Argus_Plugin {
add_action( 'argus_wpd_five_minutes', array( 'Argus_ANIS_Client', 'maybe_retry_sync' ) );
add_action( 'argus_wpd_five_minutes', array( 'Argus_Vuln_Intel', 'maybe_retry' ) );
- add_action( 'argus_wpd_five_minutes', array( 'Argus_Update_Client', 'maybe_check' ) );
+ // Argus_Update_Client is deliberately excluded from the WordPress.org-
+ // distributed build (bin/build-release.sh) -- a plugin hosted on
+ // WordPress.org must rely solely on WordPress.org's own update
+ // channel, never a self-update-from-external-manifest mechanism, even
+ // an inert-by-default one. class_exists() guards it everywhere it's
+ // referenced so the rest of the plugin degrades cleanly when the file
+ // is absent, rather than the packaging step needing to also edit code.
+ if ( class_exists( 'Argus_Update_Client' ) ) {
+ add_action( 'argus_wpd_five_minutes', array( 'Argus_Update_Client', 'maybe_check' ) );
+ }
if ( class_exists( 'Argus_Integrity' ) ) {
add_action( 'upgrader_process_complete', array( 'Argus_Integrity', 'on_upgrader_complete' ), 10, 2 );
diff --git a/includes/class-argus-quarantine.php b/includes/class-argus-quarantine.php
index 682e02e..f46a8c2 100644
--- a/includes/class-argus-quarantine.php
+++ b/includes/class-argus-quarantine.php
@@ -88,6 +88,15 @@ class Argus_Quarantine {
$now = current_time( 'mysql', true );
+ // Detection engines that already ran a content-based heuristic score
+ // (e.g. Argus_Malware_Scanner::score_content()) can pass it through here
+ // so the quarantine row never sits with a severity label and no
+ // supporting analysis data behind it -- 'severity' is the rule's
+ // verdict, 'content_score'/'matched_rules' are the raw evidence for it,
+ // and they must always be inserted together, not one now and one later
+ // via a separate manual Analyse click.
+ $has_content_score = isset( $detection['content_score'] );
+
$wpdb->insert(
Argus_DB::table( 'quarantine' ),
array(
@@ -102,10 +111,13 @@ class Argus_Quarantine {
'detection_rule' => $detection['rule'] ?? null,
'detection_type' => $detection['type'],
'severity' => $detection['severity'],
+ 'confidence_score' => $has_content_score ? (int) $detection['content_score'] : null,
+ 'matched_rules' => ! empty( $detection['matched_rules'] ) ? wp_json_encode( $detection['matched_rules'] ) : null,
+ 'analysed_at' => $has_content_score ? $now : null,
'status' => self::STATUS_QUARANTINED,
'quarantined_at' => $now,
),
- array( '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
+ array( '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' )
);
$id = (int) $wpdb->insert_id;
diff --git a/includes/class-argus-settings.php b/includes/class-argus-settings.php
index 25aadf0..387b21c 100644
--- a/includes/class-argus-settings.php
+++ b/includes/class-argus-settings.php
@@ -24,6 +24,7 @@ class Argus_Settings {
'upload_scan_enabled' => true,
'vuln_intel_enabled' => true,
'geoip_rir_enabled' => true,
+ 'auto_update_all_enabled' => true,
'static_cache_enabled' => false,
'static_cache_ttl_secs' => 3600,
'cache_stale_grace_secs' => 600,
@@ -45,7 +46,15 @@ class Argus_Settings {
'exceptions' => array(),
'cloud_connected' => false,
- 'anis_enabled' => true,
+ // Off by default -- ANIS Cloud shares blocked visitors' IP addresses
+ // with an external service, so it requires the site owner's explicit
+ // opt-in (Settings checkbox or the first-run admin notice) rather than
+ // connecting silently on activation. WordPress.org's guidelines
+ // require clear consent before any such external connection; this is
+ // still zero-configuration in the sense that mattered before -- there
+ // is no server address, license key, or tier field to fill in, only
+ // a single "enable" action.
+ 'anis_enabled' => false,
'anis_base_url' => 'https://anis.weboria.eu',
'anis_license_key' => '',
diff --git a/includes/class-argus-update-client.php b/includes/class-argus-update-client.php
deleted file mode 100644
index 09017d6..0000000
--- a/includes/class-argus-update-client.php
+++ /dev/null
@@ -1,272 +0,0 @@
- time() ) {
- return;
- }
-
- if ( false !== get_transient( self::LOCK_TRANSIENT ) ) {
- return;
- }
- set_transient( self::LOCK_TRANSIENT, 1, 4 * MINUTE_IN_SECONDS );
-
- self::check_now();
-
- delete_transient( self::LOCK_TRANSIENT );
- }
-
- public static function check_now() {
- if ( ! self::is_configured() ) {
- return array( 'success' => false, 'message' => __( 'No update source is configured.', 'argus-wordpress-defence' ), 'update_available' => false );
- }
-
- update_option( self::LAST_CHECK_OPTION, time(), false );
-
- $args = array( 'timeout' => 15 );
- $etag = get_option( self::MANIFEST_ETAG_OPTION, '' );
- if ( $etag ) {
- $args['headers'] = array( 'If-None-Match' => $etag );
- }
-
- $response = wp_remote_get( self::manifest_url(), $args );
- if ( is_wp_error( $response ) ) {
- return self::record_failure( __( 'Could not reach the update server.', 'argus-wordpress-defence' ) );
- }
-
- $code = wp_remote_retrieve_response_code( $response );
- if ( 304 === $code ) {
-
- return array( 'success' => true, 'message' => '', 'update_available' => false );
- }
- if ( 200 !== $code ) {
- return self::record_failure( sprintf( 'Update server returned HTTP %d.', $code ) );
- }
-
- $new_etag = wp_remote_retrieve_header( $response, 'etag' );
- if ( $new_etag ) {
- update_option( self::MANIFEST_ETAG_OPTION, $new_etag, false );
- }
-
- $body = wp_remote_retrieve_body( $response );
- $manifest = json_decode( $body, true );
- if ( ! is_array( $manifest ) ) {
- return self::record_failure( __( 'Update manifest was not valid JSON.', 'argus-wordpress-defence' ) );
- }
-
- return self::process_manifest( $manifest );
- }
-
- public static function process_manifest( array $manifest ) {
-
- if ( ! self::verify_signature( $manifest ) ) {
- return self::record_failure( __( 'Update manifest failed signature verification.', 'argus-wordpress-defence' ) );
- }
-
- $version = $manifest['version'] ?? '';
- if ( '' === $version ) {
- return self::record_failure( __( 'Update manifest is missing a version.', 'argus-wordpress-defence' ) );
- }
-
- if ( version_compare( $version, ARGUS_WPD_VERSION, '<=' ) ) {
- return array( 'success' => true, 'message' => '', 'update_available' => false );
- }
-
- $last_installed = get_option( self::LAST_SUCCESS_OPTION, array() );
- if ( ! empty( $last_installed['version'] ) && version_compare( $version, $last_installed['version'], '<=' ) ) {
- return array( 'success' => true, 'message' => '', 'update_available' => false );
- }
-
- global $wp_version;
- if ( ! empty( $manifest['min_php'] ) && version_compare( PHP_VERSION, $manifest['min_php'], '<' ) ) {
- return self::record_failure( sprintf( 'Update %s requires PHP %s or newer.', $version, $manifest['min_php'] ) );
- }
- if ( ! empty( $manifest['min_wp'] ) && version_compare( $wp_version, $manifest['min_wp'], '<' ) ) {
- return self::record_failure( sprintf( 'Update %s requires WordPress %s or newer.', $version, $manifest['min_wp'] ) );
- }
-
- update_option( self::PENDING_MANIFEST_OPTION, $manifest, false );
-
- if ( ! empty( $manifest['critical'] ) ) {
-
- return self::install_update( $manifest );
- }
-
- return array( 'success' => true, 'message' => '', 'update_available' => true );
- }
-
- public static function verify_signature( array $manifest ) {
- if ( empty( $manifest['signature'] ) ) {
- return false;
- }
- $signature = base64_decode( $manifest['signature'], true );
- if ( false === $signature || SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature ) ) {
- return false;
- }
- $public_key_b64 = self::public_key();
- if ( '' === $public_key_b64 ) {
- return false;
- }
- $public_key = base64_decode( $public_key_b64, true );
- if ( false === $public_key ) {
- return false;
- }
-
- $canonical = self::canonical_payload( $manifest );
- return sodium_crypto_sign_verify_detached( $signature, $canonical, $public_key );
- }
-
- protected static function canonical_payload( array $manifest ) {
- $ordered = array();
- foreach ( array( 'version', 'released_at', 'package_url', 'sha256', 'min_php', 'min_wp', 'critical' ) as $key ) {
- $ordered[ $key ] = $manifest[ $key ] ?? null;
- }
- return wp_json_encode( $ordered, JSON_UNESCAPED_SLASHES );
- }
-
- public static function install_update( array $manifest, $target_dir = null ) {
- $target_dir = $target_dir ?: ( WP_PLUGIN_DIR . '/argus-wordpress-defence' );
- $version = $manifest['version'];
-
- $upgrade_dir = trailingslashit( wp_get_upload_dir()['basedir'] ) . 'argus-wpd-data/update-staging';
- if ( ! is_dir( $upgrade_dir ) && ! wp_mkdir_p( $upgrade_dir ) ) {
- return self::record_failure( __( 'Could not prepare the update staging directory.', 'argus-wordpress-defence' ) );
- }
-
- $package_path = trailingslashit( $upgrade_dir ) . 'package-' . $version . '-' . bin2hex( random_bytes( 4 ) ) . '.zip';
- $response = wp_remote_get( $manifest['package_url'], array( 'timeout' => 120, 'stream' => true, 'filename' => $package_path ) );
- if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
- @unlink( $package_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions
- return self::record_failure( __( 'Could not download the update package.', 'argus-wordpress-defence' ) );
- }
-
- if ( ! hash_equals( $manifest['sha256'], hash_file( 'sha256', $package_path ) ) ) {
- wp_delete_file( $package_path );
- return self::record_failure( __( 'Downloaded package hash did not match the signed manifest -- rejected.', 'argus-wordpress-defence' ) );
- }
-
- $extract_root = trailingslashit( $upgrade_dir ) . 'extract-' . $version . '-' . bin2hex( random_bytes( 4 ) );
- if ( ! class_exists( 'ZipArchive' ) ) {
- wp_delete_file( $package_path );
- return self::record_failure( __( 'The PHP zip extension is not available -- cannot install the update.', 'argus-wordpress-defence' ) );
- }
- $zip = new ZipArchive();
- if ( true !== $zip->open( $package_path ) || true !== $zip->extractTo( $extract_root ) ) {
- wp_delete_file( $package_path );
- return self::record_failure( __( 'Could not extract the update package.', 'argus-wordpress-defence' ) );
- }
- $zip->close();
- wp_delete_file( $package_path );
-
- $extracted_plugin_dir = trailingslashit( $extract_root ) . 'argus-wordpress-defence';
- $new_main_file = trailingslashit( $extracted_plugin_dir ) . 'argus-wordpress-defence.php';
- if ( ! file_exists( $new_main_file ) ) {
- self::rrmdir( $extract_root );
- return self::record_failure( __( 'Extracted package did not contain the expected plugin file.', 'argus-wordpress-defence' ) );
- }
-
- $header_contents = file_get_contents( $new_main_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions
- if ( ! preg_match( '/Version:\s*([^\r\n]+)/', $header_contents, $m ) || trim( $m[1] ) !== $version ) {
- self::rrmdir( $extract_root );
- return self::record_failure( __( 'Extracted package version did not match the signed manifest -- rejected.', 'argus-wordpress-defence' ) );
- }
-
- $backup_dir = $target_dir . '-previous-' . time();
- $had_previous = is_dir( $target_dir );
- if ( $had_previous && ! rename( $target_dir, $backup_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
- self::rrmdir( $extract_root );
- return self::record_failure( __( 'Could not move the current plugin version aside for the update.', 'argus-wordpress-defence' ) );
- }
- if ( ! rename( $extracted_plugin_dir, $target_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
-
- if ( $had_previous ) {
- rename( $backup_dir, $target_dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions
- }
- self::rrmdir( $extract_root );
- return self::record_failure( __( 'Could not activate the downloaded update -- rolled back to the previous version.', 'argus-wordpress-defence' ) );
- }
-
- self::rrmdir( $extract_root );
- if ( $had_previous ) {
- self::rrmdir( $backup_dir );
- }
-
- delete_option( self::PENDING_MANIFEST_OPTION );
- update_option(
- self::LAST_SUCCESS_OPTION,
- array( 'version' => $version, 'installed_at' => current_time( 'mysql', true ) ),
- false
- );
-
- return array(
- 'success' => true,
- 'message' => ! empty( $manifest['critical'] )
- ? sprintf( __( 'A critical ARGUS security update was installed (%s).', 'argus-wordpress-defence' ), $version )
- : sprintf( __( 'ARGUS Defence was updated to version %s.', 'argus-wordpress-defence' ), $version ),
- 'update_available' => false,
- );
- }
-
- protected static function record_failure( $message ) {
- update_option(
- self::LAST_FAILURE_OPTION,
- array( 'message' => $message, 'at' => current_time( 'mysql', true ) ),
- false
- );
- Argus_Events::record( 'update_check_failed', 'medium', $message, array() );
- return array( 'success' => false, 'message' => $message, 'update_available' => false );
- }
-
- protected static function rrmdir( $dir ) {
- if ( ! is_dir( $dir ) ) {
- return;
- }
- $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
- foreach ( $items as $item ) {
- $item->isDir() ? rmdir( $item->getPathname() ) : unlink( $item->getPathname() ); // phpcs:ignore WordPress.WP.AlternativeFunctions
- }
- rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions
- }
-
- public static function status() {
- return array(
- 'current_version' => ARGUS_WPD_VERSION,
- 'configured' => self::is_configured(),
- 'last_check' => get_option( self::LAST_CHECK_OPTION, 0 ),
- 'last_success' => get_option( self::LAST_SUCCESS_OPTION, array() ),
- 'last_failure' => get_option( self::LAST_FAILURE_OPTION, array() ),
- 'pending' => get_option( self::PENDING_MANIFEST_OPTION, array() ),
- );
- }
-}
diff --git a/includes/class-argus-waf-rules.php b/includes/class-argus-waf-rules.php
index 4499823..f3e9902 100644
--- a/includes/class-argus-waf-rules.php
+++ b/includes/class-argus-waf-rules.php
@@ -11,11 +11,12 @@ class Argus_WAF_Rules {
}
$rules = array(
-
+
+ // --- SQL Injection (CRS 942-XXX equivalent) -------------------------------------------------
+
array( 'id' => 'sqli-union-select', 'category' => 'sql_injection', 'severity' => 'critical',
'pattern' => '/\bunion\b[^\w]{1,20}\bselect\b/i' ),
array( 'id' => 'sqli-classic-tautology', 'category' => 'sql_injection', 'severity' => 'critical',
-
'pattern' => '/(\%27|\'|\%22|")\s*(or|and)\s*(\%27|\'|\%22|")?[\d\w]+(\%27|\'|\%22|")?\s*=\s*(\%27|\'|\%22|")?[\d\w]+(\%27|\'|\%22|")?/i' ),
array( 'id' => 'sqli-information-schema', 'category' => 'sql_injection', 'severity' => 'critical',
'pattern' => '/information_schema|sysobjects|sysdatabases/i' ),
@@ -23,41 +24,116 @@ class Argus_WAF_Rules {
'pattern' => '/\b(sleep|benchmark|pg_sleep|waitfor\s+delay)\s*\(/i' ),
array( 'id' => 'sqli-stacked-comment', 'category' => 'sql_injection', 'severity' => 'high',
'pattern' => '/;\s*(drop|delete|update|insert)\s+(table|from|into)/i' ),
+ array( 'id' => 'sqli-error-based-xpath', 'category' => 'sql_injection', 'severity' => 'critical',
+ 'pattern' => '/\b(extractvalue|updatexml)\s*\(/i' ),
+ array( 'id' => 'sqli-file-read-write', 'category' => 'sql_injection', 'severity' => 'critical',
+ 'pattern' => '/\b(load_file|into\s+outfile|into\s+dumpfile)\s*\(?/i' ),
+ array( 'id' => 'sqli-mssql-xp-cmdshell', 'category' => 'sql_injection', 'severity' => 'critical',
+ 'pattern' => '/\bxp_cmdshell\b/i' ),
+ array( 'id' => 'sqli-hex-literal', 'category' => 'sql_injection', 'severity' => 'medium',
+ 'pattern' => '/\bunhex\s*\(|0x[0-9a-f]{12,}/i' ),
+ array( 'id' => 'sqli-conditional-error', 'category' => 'sql_injection', 'severity' => 'high',
+ 'pattern' => '/\b(and|or)\s+\d+\s*=\s*\d+\s*(--|#|\/\*)/i' ),
+
+ // --- Cross-Site Scripting (CRS 941-XXX equivalent) -------------------------------------------------
array( 'id' => 'xss-script-tag', 'category' => 'xss', 'severity' => 'high',
'pattern' => '/<\s*script[\s>\/]/i' ),
-
array( 'id' => 'xss-event-handler', 'category' => 'xss', 'severity' => 'high',
'pattern' => '/\bon(error|load|mouseover|click|focus)\s*=\s*["\']?[^"\'>]*[\(\{]/i' ),
array( 'id' => 'xss-javascript-uri', 'category' => 'xss', 'severity' => 'high',
'pattern' => '/javascript\s*:\s*[^\s]/i' ),
array( 'id' => 'xss-svg-onload', 'category' => 'xss', 'severity' => 'high',
'pattern' => '/<\s*svg[^>]*onload/i' ),
+ array( 'id' => 'xss-embed-object-tag', 'category' => 'xss', 'severity' => 'high',
+ 'pattern' => '/<\s*(iframe|object|embed)\b[^>]*(src|data)\s*=/i' ),
+ array( 'id' => 'xss-data-uri-html', 'category' => 'xss', 'severity' => 'high',
+ 'pattern' => '/data\s*:\s*text\/html\s*;\s*base64/i' ),
+ array( 'id' => 'xss-vbscript-uri', 'category' => 'xss', 'severity' => 'high',
+ 'pattern' => '/vbscript\s*:/i' ),
+ array( 'id' => 'xss-css-expression', 'category' => 'xss', 'severity' => 'medium',
+ 'pattern' => '/style\s*=\s*["\'][^"\']*expression\s*\(/i' ),
+ array( 'id' => 'xss-dom-sink', 'category' => 'xss', 'severity' => 'medium',
+ 'pattern' => '/document\s*\.\s*(cookie|write|location)\s*[=\(]|window\s*\.\s*location\s*=/i' ),
+ array( 'id' => 'xss-entity-encoded-script', 'category' => 'xss', 'severity' => 'medium',
+ 'pattern' => '/&(lt|#0*60|#x3c)\s*;?\s*script/i' ),
+
+ // --- Remote Code Execution / Command Injection (CRS 932-XXX equivalent) -------------------------------------------------
array( 'id' => 'cmdi-shell-metachars', 'category' => 'rce', 'severity' => 'critical',
'pattern' => '/;\s*(cat|ls|whoami|id|uname|wget|curl)\s/i' ),
array( 'id' => 'cmdi-backtick-subshell', 'category' => 'rce', 'severity' => 'critical',
'pattern' => '/`[^`]{1,80}`|\$\([^\)]{1,80}\)/' ),
+
+ // --- PHP Injection (CRS 933-XXX equivalent) -------------------------------------------------
+
array( 'id' => 'phpi-eval-base64', 'category' => 'rce', 'severity' => 'critical',
- 'pattern' => '/\beval\s*\(\s*(base64_decode|gzinflate|str_rot13)\s*\(/i' ),
+ 'pattern' => '/\beval\s*\(\s*(base64_decode|gzinflate|gzuncompress|str_rot13)\s*\(/i' ),
array( 'id' => 'phpi-dangerous-function', 'category' => 'rce', 'severity' => 'critical',
- 'pattern' => '/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/i' ),
+ 'pattern' => '/\b(system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|dl)\s*\(/i' ),
+ array( 'id' => 'phpi-dynamic-eval-construct', 'category' => 'rce', 'severity' => 'critical',
+ 'pattern' => '/\b(assert|create_function|call_user_func(_array)?)\s*\(/i' ),
+ array( 'id' => 'phpi-preg-replace-eval-modifier', 'category' => 'rce', 'severity' => 'critical',
+ 'pattern' => '/preg_replace\s*\(\s*["\'][^"\']*\/[a-zA-Z]*e[a-zA-Z]*["\']/i' ),
array( 'id' => 'phpi-tag-in-input', 'category' => 'rce', 'severity' => 'high',
'pattern' => '/<\?php|<\?=/i' ),
+ // --- Java / JNDI Injection (CRS 944-XXX equivalent -- Log4Shell class) -------------------------------------------------
+
+ array( 'id' => 'java-jndi-lookup', 'category' => 'java_injection', 'severity' => 'critical',
+ 'pattern' => '/\$\{jndi:(ldap|ldaps|rmi|dns|iiop|corba|nds|http|https):\/\//i' ),
+ array( 'id' => 'java-el-injection', 'category' => 'java_injection', 'severity' => 'high',
+ 'pattern' => '/\$\{\s*(java|javax|org\.springframework|Runtime)\s*[\.\(]/' ),
+
+ // --- Local / Remote File Inclusion (CRS 930/931-XXX equivalent) -------------------------------------------------
+
array( 'id' => 'lfi-dot-dot-slash', 'category' => 'file_access', 'severity' => 'high',
'pattern' => '/(\.\.\/|\.\.\\\\|%2e%2e%2f|%252e%252e%252f)/i' ),
array( 'id' => 'lfi-sensitive-file', 'category' => 'file_access', 'severity' => 'high',
'pattern' => '/\/etc\/(passwd|shadow|hosts)\b|wp-config\.php/i' ),
array( 'id' => 'lfi-php-wrapper', 'category' => 'file_access', 'severity' => 'high',
- 'pattern' => '/php:\/\/(filter|input|data)/i' ),
+ 'pattern' => '/(php|zip|phar|expect|glob)\s*:\/\/(filter|input|data|)/i' ),
array( 'id' => 'rfi-remote-scheme', 'category' => 'file_access', 'severity' => 'high',
'pattern' => '/^(https?|ftp):\/\/.+\.(php|txt)(\?|$)/i' ),
+ array( 'id' => 'lfi-dotfile-path', 'category' => 'file_access', 'severity' => 'medium',
+ 'pattern' => '/\/\.(git|svn|env|ssh|hg)\/[\w\-\.\/]*(config|credentials|id_rsa)?/i',
+ 'sources' => array( 'HEADER:REQUEST_URI' ) ),
+ array( 'id' => 'lfi-backup-file-request', 'category' => 'file_access', 'severity' => 'medium',
+ 'pattern' => '/\.(bak|old|swp|save|orig|sql|sql\.gz|tar\.gz)(\?|$)/i',
+ 'sources' => array( 'HEADER:REQUEST_URI' ) ),
+
+ // --- XML External Entity (CRS-equivalent) -------------------------------------------------
+
+ array( 'id' => 'xxe-doctype-entity', 'category' => 'xxe', 'severity' => 'critical',
+ 'pattern' => '/]*\[.* 'xxe-entity-system', 'category' => 'xxe', 'severity' => 'critical',
+ 'pattern' => '/]+SYSTEM\s+["\'](file|https?|ftp|expect|php):\/\//i' ),
+
+ // --- Server-Side Request Forgery (CRS-equivalent) -------------------------------------------------
+
+ array( 'id' => 'ssrf-internal-target', 'category' => 'ssrf', 'severity' => 'high',
+ 'pattern' => '/^(https?|gopher|dict|ftp|ldap):\/\/(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost|169\.254\.169\.254|\[::1\]|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})([:\/]|$)/i' ),
+
+ // --- Session Fixation (CRS 943-XXX equivalent) -------------------------------------------------
+
+ array( 'id' => 'session-fixation-id-in-url', 'category' => 'session_fixation', 'severity' => 'low',
+ 'pattern' => '/[?&](PHPSESSID|JSESSIONID|ASPSESSIONID|ASP\.NET_SessionId)=/i',
+ 'sources' => array( 'HEADER:REQUEST_URI' ) ),
+
+ // --- Scanner / Reconnaissance Detection (CRS 913-XXX equivalent) -------------------------------------------------
+
+ array( 'id' => 'scanner-known-tool-ua', 'category' => 'scanner_activity', 'severity' => 'medium',
+ 'pattern' => '/\b(sqlmap|nikto|acunetix|nessus|openvas|w3af|havij|nmap scripting engine|masscan|zgrab|dirbuster|gobuster|wfuzz|metasploit|zmeu|wpscan)\b/i',
+ 'sources' => array( 'HEADER:HTTP_USER_AGENT' ) ),
+
+ // --- Protocol Anomaly (CRS 920-XXX equivalent) -------------------------------------------------
array( 'id' => 'proto-null-byte', 'category' => 'protocol_anomaly', 'severity' => 'medium',
'pattern' => '/%00/' ),
array( 'id' => 'proto-double-encoding', 'category' => 'protocol_anomaly', 'severity' => 'low',
'pattern' => '/%25(2e|2f|5c)/i' ),
+ array( 'id' => 'proto-crlf-injection', 'category' => 'protocol_anomaly', 'severity' => 'high',
+ 'pattern' => '/%0[dD]%0[aA]/' ),
);
return $rules;
@@ -74,6 +150,10 @@ class Argus_WAF_Rules {
$decoded = rawurldecode( $value );
foreach ( self::corpus() as $rule ) {
+ if ( ! empty( $rule['sources'] ) && ! in_array( $source, $rule['sources'], true ) ) {
+ continue;
+ }
+
$matched = preg_match( $rule['pattern'], $decoded, $m ) ? $m : ( preg_match( $rule['pattern'], $value, $m ) ? $m : null );
if ( null === $matched ) {
continue;
diff --git a/readme.txt b/readme.txt
index 9dccf9b..4ba9d6e 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,10 +1,10 @@
-=== ARGUS Defence ===
+=== ARGUS WordPress Defence ===
Contributors: argus
Tags: security, firewall, malware, vulnerability, backup
Requires at least: 6.0
-Tested up to: 6.6
+Tested up to: 7.0
Requires PHP: 7.4
-Stable tag: 1.0.0
+Stable tag: 7.23.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -48,13 +48,16 @@ page-cache feature can make your site faster, not slower.
= How do I update the plugin? =
-Download the latest release ZIP and repeat the installation steps above (WordPress will offer to
-replace the existing version). Automatic in-dashboard updates depend on an update channel being
-configured for your deployment; until then, updates are applied manually the same way the plugin
-was installed.
+Like any other WordPress.org plugin: you'll see an update notification right in your WordPress
+dashboard whenever a new version is available, and you can update it in one click from there.
== Changelog ==
-= 1.0.0 =
+= 7.23.0 =
+* Firewall: broadened local rule coverage (XXE, SSRF, session fixation, Log4Shell/JNDI, scanner-tool
+ detection, and deeper SQL injection / XSS / PHP injection signatures).
+* Scanner: fixed a case where a quarantined file's severity label and its content-analysis score
+ could disagree; both are now always shown together and derived consistently.
+* Console: the running plugin version is now shown in the admin header.
* First production release: firewall, scanner, quarantine, vulnerability protection, backups,
cache & performance, and automatic global threat intelligence.