Files
argus-wp-defence/includes/class-argus-findings.php
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
Automatic WordPress security: local firewall, malware and file-integrity
scanning, vulnerability protection, quarantine, scheduled backups, an
optional page cache, and automatic global threat intelligence.

See README.md for installation, update, and uninstall instructions.
2026-08-09 13:40:16 +00:00

281 lines
8.4 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_Findings {
const STATUS_OPEN = 'open';
const STATUS_ACKNOWLEDGED = 'acknowledged';
const STATUS_RESOLVED = 'resolved';
const STATUS_RISK_ACCEPTED = 'risk_accepted';
public static function record( $category, $severity, array $explain, array $evidence = array(), $initial_status = self::STATUS_OPEN ) {
global $wpdb;
Argus_Explain::require_fields( $explain );
$now = current_time( 'mysql', true );
$table = Argus_DB::table( 'findings' );
$existing_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$table} WHERE category = %s AND title = %s AND status = %s LIMIT 1", // phpcs:ignore
$category,
$explain['what_happened'],
self::STATUS_OPEN
)
);
if ( $existing_id ) {
$wpdb->update(
$table,
array(
'last_seen_at' => $now,
'evidence' => wp_json_encode( $evidence ),
'detail' => wp_json_encode( $explain ),
),
array( 'id' => $existing_id ),
array( '%s', '%s', '%s' ),
array( '%d' )
);
return (int) $existing_id;
}
$wpdb->insert(
$table,
array(
'category' => $category,
'severity' => $severity,
'title' => $explain['what_happened'],
'detail' => wp_json_encode( $explain ),
'remediation_hint' => $explain['what_should_you_do'],
'evidence' => wp_json_encode( $evidence ),
'status' => $initial_status,
'first_seen_at' => $now,
'last_seen_at' => $now,
),
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
);
return (int) $wpdb->insert_id;
}
public static function set_status( $id, $status ) {
global $wpdb;
return $wpdb->update(
Argus_DB::table( 'findings' ),
array( 'status' => $status ),
array( 'id' => $id ),
array( '%s' ),
array( '%d' )
);
}
public static function category_counts() {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$rows = $wpdb->get_results( "SELECT category, COUNT(*) AS cnt FROM {$table} GROUP BY category ORDER BY cnt DESC" ); // phpcs:ignore
$out = array();
foreach ( $rows as $row ) {
$out[ $row->category ] = (int) $row->cnt;
}
return $out;
}
public static function count_open( $category ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
return (int) $wpdb->get_var(
$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE category = %s AND status = %s", $category, self::STATUS_OPEN ) // phpcs:ignore
);
}
public static function open_counts() {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT severity, COUNT(*) AS cnt FROM {$table} WHERE status = %s GROUP BY severity", self::STATUS_OPEN ) // phpcs:ignore
);
$counts = array(
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0,
'info' => 0,
);
foreach ( $rows as $row ) {
if ( isset( $counts[ $row->severity ] ) ) {
$counts[ $row->severity ] = (int) $row->cnt;
}
}
return $counts;
}
public static function recent( $limit = 25, $status = null ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
if ( $status ) {
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} WHERE status = %s ORDER BY last_seen_at DESC LIMIT %d", $status, $limit ) // phpcs:ignore
);
} else {
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY last_seen_at DESC LIMIT %d", $limit ) // phpcs:ignore
);
}
foreach ( $rows as &$row ) {
$row->explain = json_decode( $row->detail, true );
$row->evidence = json_decode( $row->evidence, true );
}
return $rows;
}
const PER_PAGE = 10;
public static function paginated( $page = 1, $status = null, $per_page = self::PER_PAGE ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$page = max( 1, (int) $page );
$offset = ( $page - 1 ) * $per_page;
if ( $status ) {
$total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", $status ) ); // phpcs:ignore
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} WHERE status = %s ORDER BY last_seen_at DESC LIMIT %d OFFSET %d", $status, $per_page, $offset ) // phpcs:ignore
);
} else {
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY last_seen_at DESC LIMIT %d OFFSET %d", $per_page, $offset ) // phpcs:ignore
);
}
foreach ( $rows as &$row ) {
$row->explain = json_decode( $row->detail, true );
$row->evidence = json_decode( $row->evidence, true );
}
return array(
'rows' => $rows,
'total' => $total,
'total_pages' => max( 1, (int) ceil( $total / $per_page ) ),
'page' => $page,
);
}
const NEEDS_ATTENTION_SEVERITIES = array( 'critical', 'high' );
public static function paginated_by_view( $page = 1, $view = 'active', $per_page = self::PER_PAGE ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$page = max( 1, (int) $page );
$offset = ( $page - 1 ) * $per_page;
list( $where, $params ) = self::view_where( $view );
$total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$where}", $params ) ); // phpcs:ignore
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} WHERE {$where} ORDER BY last_seen_at DESC LIMIT %d OFFSET %d", array_merge( $params, array( $per_page, $offset ) ) ) // phpcs:ignore
);
foreach ( $rows as &$row ) {
$row->explain = json_decode( $row->detail, true );
$row->evidence = json_decode( $row->evidence, true );
}
return array(
'rows' => $rows,
'total' => $total,
'total_pages' => max( 1, (int) ceil( $total / $per_page ) ),
'page' => $page,
);
}
public static function view_counts() {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$out = array();
foreach ( array( 'active', 'needs_attention', 'resolved', 'accepted' ) as $view ) {
list( $where, $params ) = self::view_where( $view );
$out[ $view ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$where}", $params ) ); // phpcs:ignore
}
return $out;
}
protected static function view_where( $view ) {
$severity_placeholders = implode( ',', array_fill( 0, count( self::NEEDS_ATTENTION_SEVERITIES ), '%s' ) );
switch ( $view ) {
case 'needs_attention':
return array(
"status IN (%s,%s) AND severity IN ({$severity_placeholders})",
array_merge( array( self::STATUS_OPEN, self::STATUS_ACKNOWLEDGED ), self::NEEDS_ATTENTION_SEVERITIES ),
);
case 'resolved':
return array( 'status = %s', array( self::STATUS_RESOLVED ) );
case 'accepted':
return array( 'status = %s', array( self::STATUS_RISK_ACCEPTED ) );
case 'active':
default:
return array(
"status IN (%s,%s) AND severity NOT IN ({$severity_placeholders})",
array_merge( array( self::STATUS_OPEN, self::STATUS_ACKNOWLEDGED ), self::NEEDS_ATTENTION_SEVERITIES ),
);
}
}
public static function delete( $id ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$row = self::get( $id );
if ( ! $row ) {
return false;
}
Argus_Events::record(
'finding_deleted',
'info',
sprintf( 'Finding deleted: %s', $row->title ),
array( 'finding_id' => $id, 'category' => $row->category, 'severity' => $row->severity )
);
return (bool) $wpdb->delete( $table, array( 'id' => $id ), array( '%d' ) );
}
public static function resolve_all_open() {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", self::STATUS_OPEN ) ); // phpcs:ignore
if ( 0 === $count ) {
return 0;
}
$wpdb->update( $table, array( 'status' => self::STATUS_RESOLVED ), array( 'status' => self::STATUS_OPEN ), array( '%s' ), array( '%s' ) );
Argus_Events::record( 'findings_bulk_resolved', 'info', sprintf( '%d open finding(s) bulk-resolved by an administrator', $count ), array( 'count' => $count ) );
return $count;
}
public static function get( $id ) {
global $wpdb;
$table = Argus_DB::table( 'findings' );
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) ); // phpcs:ignore
if ( $row ) {
$row->explain = json_decode( $row->detail, true );
$row->evidence = json_decode( $row->evidence, true );
}
return $row;
}
}