+<?php
+
+/**
+ * @file
+ * Contains \Drupal\security_review\Checks\QueryErrors.
+ */
+
+namespace Drupal\security_review\Checks;
+
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\security_review\Check;
+use Drupal\security_review\CheckResult;
+
+/**
+ * Checks for abundant query errors.
+ */
+class QueryErrors extends Check {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNamespace() {
+ return 'Security Review';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ return 'Query errors';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run() {
+ // If dblog is not enabled return with hidden INFO.
+ if (!$this->moduleHandler()->moduleExists('dblog')) {
+ return $this->createResult(CheckResult::INFO, [], FALSE);
+ }
+
+ $result = CheckResult::SUCCESS;
+ $findings = [];
+ $last_result = $this->lastResult();
+ $visible = FALSE;
+
+ // Prepare the query.
+ $query = $this->database()->select('watchdog', 'w');
+ $query->fields('w', [
+ 'severity',
+ 'type',
+ 'timestamp',
+ 'message',
+ 'variables',
+ 'hostname',
+ ]);
+ $query->condition('type', 'php')->condition('severity', RfcLogLevel::ERROR);
+ if ($last_result instanceof CheckResult) {
+ // Only check entries that got recorded since the last run of the check.
+ $query->condition('timestamp', $last_result->time(), '>=');
+ }
+
+ // Execute the query.
+ $db_result = $query->execute();
+
+ // Count the number of query errors per IP.
+ $entries = [];
+ foreach ($db_result as $row) {
+ // Get the message.
+ if ($row->variables === 'N;') {
+ $message = $row->message;
+ }
+ else {
+ $message = $this->t($row->message, unserialize($row->variables));
+ }
+
+ // Get the IP.
+ $ip = $row->hostname;
+
+ // Search for query errors.
+ $message_contains_sql = strpos($message, 'SQL') !== FALSE;
+ $message_contains_select = strpos($message, 'SELECT') !== FALSE;
+ if ($message_contains_sql && $message_contains_select) {
+ $entry_for_ip = &$entries[$ip];
+
+ if (!isset($entry_for_ip)) {
+ $entry_for_ip = 0;
+ }
+ $entry_for_ip++;
+ }
+ }
+
+ // Filter the IPs with more than 10 query errors.
+ if (!empty($entries)) {
+ foreach ($entries as $ip => $count) {
+ if ($count > 10) {
+ $findings[] = $ip;
+ }
+ }
+ }
+
+ if (!empty($findings)) {
+ $result = CheckResult::FAIL;
+ $visible = TRUE;
+ }
+
+ return $this->createResult($result, $findings, $visible);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function help() {
+ $paragraphs = [];
+ $paragraphs[] = $this->t('Database errors triggered from the same IP may be an artifact of a malicious user attempting to probe the system for weaknesses like SQL injection or information disclosure.');
+
+ return [
+ '#theme' => 'check_help',
+ '#title' => $this->t('Abundant query errors from the same IP'),
+ '#paragraphs' => $paragraphs,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function evaluate(CheckResult $result) {
+ $findings = $result->findings();
+ if (empty($findings)) {
+ return [];
+ }
+
+ $paragraphs = [];
+ $paragraphs[] = $this->t('The following IPs were observed with an abundance of query errors.');
+
+ return [
+ '#theme' => 'check_evaluation',
+ '#paragraphs' => $paragraphs,
+ '#items' => $result->findings(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function evaluatePlain(CheckResult $result) {
+ $findings = $result->findings();
+ if (empty($findings)) {
+ return '';
+ }
+
+ $output = $this->t('Suspicious IP addresses:') . ":\n";
+ foreach ($findings as $ip) {
+ $output .= "\t" . $ip . "\n";
+ }
+
+ return $output;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMessage($result_const) {
+ switch ($result_const) {
+ case CheckResult::FAIL:
+ return $this->t('Query errors from the same IP. These may be a SQL injection attack or an attempt at information disclosure.');
+
+ default:
+ return $this->t('Unexpected result.');
+ }
+ }
+
+}