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.
281 lines
8.4 KiB
PHP
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;
|
|
}
|
|
}
|