mirror of
https://github.com/moodle/moodle.git
synced 2025-07-28 01:41:03 +02:00
- `notifylevel` will change what gets sent as part of the antivirus notification emails based on the scan type. For example if set on SCAN_RESULT_FOUND, it will not notify for any detections, if set to SCAN_RESULT_ERROR, it will notify for both detections and errors. - `threshold` will determine how far the lookback is when displaying the status of the /reports/status (System Status) page. It will display as an ERROR state if there has been scanner issues within this certain threshold period - As part of the above, scanner errors will now trigger a new event which will be logged as antivirus_scan_data_error or antivirus_scan_file_error. Due to the nature of it reading from the logs table, it only works currently for the "Standard logging" logstore.
331 lines
14 KiB
PHP
331 lines
14 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle 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 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle 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.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Manager class for antivirus integration.
|
|
*
|
|
* @package core_antivirus
|
|
* @copyright 2015 Ruslan Kabalin, Lancaster University.
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace core\antivirus;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
/**
|
|
* Class used for various antivirus related stuff.
|
|
*
|
|
* @package core_antivirus
|
|
* @copyright 2015 Ruslan Kabalin, Lancaster University.
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class manager {
|
|
|
|
/**
|
|
* Returns list of enabled antiviruses.
|
|
*
|
|
* @return array Array ('antivirusname'=>stdClass antivirus object).
|
|
*/
|
|
private static function get_enabled() {
|
|
global $CFG;
|
|
|
|
$active = array();
|
|
if (empty($CFG->antiviruses)) {
|
|
return $active;
|
|
}
|
|
|
|
foreach (explode(',', $CFG->antiviruses) as $e) {
|
|
if ($antivirus = self::get_antivirus($e)) {
|
|
if ($antivirus->is_configured()) {
|
|
$active[$e] = $antivirus;
|
|
}
|
|
}
|
|
}
|
|
return $active;
|
|
}
|
|
|
|
/**
|
|
* Scan file using all enabled antiviruses, throws exception in case of infected file.
|
|
*
|
|
* @param string $file Full path to the file.
|
|
* @param string $filename Name of the file (could be different from physical file if temp file is used).
|
|
* @param bool $deleteinfected whether infected file needs to be deleted.
|
|
* @throws \core\antivirus\scanner_exception If file is infected.
|
|
* @return void
|
|
*/
|
|
public static function scan_file($file, $filename, $deleteinfected) {
|
|
global $USER;
|
|
$antiviruses = self::get_enabled();
|
|
$notifylevel = (int)get_config('antivirus', 'notifylevel');
|
|
foreach ($antiviruses as $antivirus) {
|
|
// Attempt to scan, catching internal exceptions.
|
|
try {
|
|
$result = $antivirus->scan_file($file, $filename);
|
|
} catch (\core\antivirus\scanner_exception $e) {
|
|
$notice = $antivirus->get_scanning_notice();
|
|
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
|
|
|
|
// Log scan error event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\antivirus_scan_file_error::create($params);
|
|
$event->trigger();
|
|
|
|
// If there was a scanner exception (such as ClamAV denying
|
|
// upload), send messages (on error and above), and rethrow.
|
|
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
|
|
$notice = $antivirus->get_scanning_notice();
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
|
|
$notice = $antivirus->get_scanning_notice();
|
|
if ($result === $antivirus::SCAN_RESULT_FOUND) {
|
|
// Infection found, send notification.
|
|
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice);
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
|
|
// Move to quarantine folder.
|
|
$zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice);
|
|
// If file not stored due to disabled quarantine, store a message.
|
|
if (empty($zipfile)) {
|
|
$zipfile = get_string('quarantinedisabled', 'antivirus');
|
|
}
|
|
|
|
// Log file infected event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\virus_infected_file_detected::create($params);
|
|
$event->trigger();
|
|
|
|
if ($deleteinfected) {
|
|
unlink($file);
|
|
}
|
|
|
|
// Get custom message to display to user from antivirus engine.
|
|
$displaymessage = $antivirus->get_virus_found_message();
|
|
$placeholders = array_merge(['item' => $filename], $displaymessage['placeholders']);
|
|
|
|
throw new \core\antivirus\scanner_exception(
|
|
$displaymessage['string'],
|
|
'',
|
|
$placeholders,
|
|
null,
|
|
$displaymessage['component']
|
|
);
|
|
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
|
|
// Here we need to generate a different incident based on an error.
|
|
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
|
|
|
|
// Log scan error event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\antivirus_scan_file_error::create($params);
|
|
$event->trigger();
|
|
|
|
// Send a notification if required (error or above).
|
|
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scan data steam using all enabled antiviruses, throws exception in case of infected data.
|
|
*
|
|
* @param string $data The variable containing the data to scan.
|
|
* @throws \core\antivirus\scanner_exception If data is infected.
|
|
* @return void
|
|
*/
|
|
public static function scan_data($data) {
|
|
global $USER;
|
|
$antiviruses = self::get_enabled();
|
|
$notifylevel = (int)get_config('antivirus', 'notifylevel');
|
|
foreach ($antiviruses as $antivirus) {
|
|
// Attempt to scan, catching internal exceptions.
|
|
try {
|
|
$result = $antivirus->scan_data($data);
|
|
} catch (\core\antivirus\scanner_exception $e) {
|
|
$notice = $antivirus->get_scanning_notice();
|
|
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
|
|
|
|
// Log scan error event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\antivirus_scan_file_error::create($params);
|
|
$event->trigger();
|
|
|
|
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
|
|
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
|
|
$notice = $antivirus->get_scanning_notice();
|
|
$filename = get_string('datastream', 'antivirus');
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
|
|
$filename = get_string('datastream', 'antivirus');
|
|
$notice = $antivirus->get_scanning_notice();
|
|
|
|
if ($result === $antivirus::SCAN_RESULT_FOUND) {
|
|
// Infection found, send notification.
|
|
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice);
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
|
|
// Copy data to quarantine folder.
|
|
$zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidentdetails, $notice);
|
|
// If file not stored due to disabled quarantine, store a message.
|
|
if (empty($zipfile)) {
|
|
$zipfile = get_string('quarantinedisabled', 'antivirus');
|
|
}
|
|
|
|
// Log file infected event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\virus_infected_data_detected::create($params);
|
|
$event->trigger();
|
|
|
|
// Get custom message to display to user from antivirus engine.
|
|
$displaymessage = $antivirus->get_virus_found_message();
|
|
$placeholders = array_merge(['item' => get_string('datastream', 'antivirus')], $displaymessage['placeholders']);
|
|
|
|
throw new \core\antivirus\scanner_exception(
|
|
$displaymessage['string'],
|
|
'',
|
|
$placeholders,
|
|
null,
|
|
$displaymessage['component']
|
|
);
|
|
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
|
|
// Here we need to generate a different incident based on an error.
|
|
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
|
|
|
|
// Log scan error event.
|
|
$params = [
|
|
'context' => \context_system::instance(),
|
|
'relateduserid' => $USER->id,
|
|
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
|
|
];
|
|
$event = \core\event\antivirus_scan_data_error::create($params);
|
|
$event->trigger();
|
|
|
|
// Send a notification if required (error or above).
|
|
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
|
|
self::send_antivirus_messages($antivirus, $incidentdetails);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns instance of antivirus.
|
|
*
|
|
* @param string $antivirusname name of antivirus.
|
|
* @return object|bool antivirus instance or false if does not exist.
|
|
*/
|
|
public static function get_antivirus($antivirusname) {
|
|
global $CFG;
|
|
|
|
$classname = '\\antivirus_' . $antivirusname . '\\scanner';
|
|
if (!class_exists($classname)) {
|
|
return false;
|
|
}
|
|
return new $classname();
|
|
}
|
|
|
|
/**
|
|
* Get the list of available antiviruses.
|
|
*
|
|
* @return array Array ('antivirusname'=>'localised antivirus name').
|
|
*/
|
|
public static function get_available() {
|
|
$antiviruses = array();
|
|
foreach (\core_component::get_plugin_list('antivirus') as $antivirusname => $dir) {
|
|
$antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname);
|
|
}
|
|
return $antiviruses;
|
|
}
|
|
|
|
/**
|
|
* This function puts all relevant information into the messages required, and sends them.
|
|
*
|
|
* @param \core\antivirus\scanner $antivirus the scanner engine.
|
|
* @param string $incidentdetails details of the incident.
|
|
* @return void
|
|
*/
|
|
public static function send_antivirus_messages(\core\antivirus\scanner $antivirus, string $incidentdetails) {
|
|
$messages = $antivirus->get_messages();
|
|
|
|
// If there is no messages, and a virus is found, we should generate one, then send it.
|
|
if (empty($messages)) {
|
|
$antivirus->message_admins($antivirus->get_scanning_notice(), FORMAT_MOODLE, 'infected');
|
|
$messages = $antivirus->get_messages();
|
|
}
|
|
|
|
foreach ($messages as $message) {
|
|
|
|
// Check if the information is already in the current scanning notice.
|
|
if (!empty($antivirus->get_scanning_notice()) &&
|
|
strpos($antivirus->get_scanning_notice(), $message->fullmessage) === false) {
|
|
// This is some extra information. We should append this to the end of the incident details.
|
|
$incidentdetails .= \html_writer::tag('pre', $message->fullmessage);
|
|
}
|
|
|
|
// Now update the message to the detailed version, and format.
|
|
$message->name = 'infected';
|
|
$message->fullmessagehtml = $incidentdetails;
|
|
$message->fullmessageformat = FORMAT_MOODLE;
|
|
$message->fullmessage = format_text_email($incidentdetails, $message->fullmessageformat);
|
|
|
|
// Now we must check if message is going to a real account.
|
|
// It may be an email that needs to be sent to non-user address.
|
|
if ($message->userto->id === -1) {
|
|
// If this doesnt exist, send a regular email.
|
|
email_to_user(
|
|
$message->userto,
|
|
get_admin(),
|
|
$message->subject,
|
|
$message->fullmessage,
|
|
$message->fullmessagehtml
|
|
);
|
|
} else {
|
|
// And now we can send.
|
|
message_send($message);
|
|
}
|
|
}
|
|
}
|
|
}
|