MDL-66222 antivirus: Added antivirus failure reporting

This commit is contained in:
Nathan Nguyen
2020-08-21 11:43:56 +10:00
committed by Peter Burnett
parent a0fc902eb1
commit adbe92ce0a
22 changed files with 1326 additions and 8 deletions
+23
View File
@@ -167,6 +167,29 @@ if ($hassiteconfig) {
$ADMIN->add('modules', new admin_category('antivirussettings', new lang_string('antiviruses', 'antivirus')));
$temp = new admin_settingpage('manageantiviruses', new lang_string('antivirussettings', 'antivirus'));
$temp->add(new admin_setting_manageantiviruses());
// Common settings.
$temp->add(new admin_setting_heading('antiviruscommonsettings', new lang_string('antiviruscommonsettings', 'antivirus'), ''));
// Alert email.
$temp->add(new admin_setting_configtext('antivirus/notifyemail',
new lang_string('notifyemail', 'antivirus'),
new lang_string('notifyemail_help', 'antivirus'), '', PARAM_EMAIL)
);
// Enable quarantine.
$temp->add(new admin_setting_configcheckbox('antivirus/enablequarantine',
new lang_string('enablequarantine', 'antivirus'),
new lang_string('enablequarantine_help', 'antivirus',
\core\antivirus\quarantine::DEFAULT_QUARANTINE_FOLDER), 0));
// Quarantine time.
$temp->add(new admin_setting_configduration('antivirus/quarantinetime',
new lang_string('quarantinetime', 'antivirus'),
new lang_string('quarantinetime_desc', 'antivirus'),
\core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME)
);
$ADMIN->add('antivirussettings', $temp);
$plugins = core_plugin_manager::instance()->get_plugins_of_type('antivirus');
core_collator::asort_objects_by_property($plugins, 'displayname');
+29
View File
@@ -24,9 +24,38 @@
$string['actantivirushdr'] = 'Available antivirus plugins';
$string['antiviruses'] = 'Antivirus plugins';
$string['antiviruscommonsettings'] = 'Common antivirus settings';
$string['antivirussettings'] = 'Manage antivirus plugins';
$string['configantivirusplugins'] = 'Please choose the antivirus plugins you wish to use and arrange them in order of being applied.';
$string['confirmdelete'] = 'Do you really want to delete this file';
$string['confirmdeleteall'] = 'Do you really want to delete all files';
$string['datastream'] = 'Data';
$string['datainfecteddesc'] = 'There is a virus infected data';
$string['datainfectedname'] = 'Data infected';
$string['emailsubject'] = '{$a} :: Antivirus notification';
$string['enablequarantine'] = 'Enable quarantine';
$string['enablequarantine_help'] = 'When quarantine is enabled, any files which are detected as viruses will be kept in a quarantine folder for later inspection ([dataroot]/{$a}).
The upload into Moodle will still fail.
If you have any file system level virus scanning in place, the quarantine folder should be excluded from the antivirus check to avoid detecting the quarantined files.';
$string['fileinfectedname'] = 'File infected';
$string['incidencedetails'] = 'Infected file detected:
Report: {$a->report}
File name: {$a->filename}
File size: {$a->filesize}
File content hash: {$a->contenthash}
File content type: {$a->contenttype}
Uploaded by: {$a->author}
IP: {$a->ipaddress}
REFERER: {$a->referer}
Date: {$a->date}
{$a->notice}';
$string['notifyemail'] = 'Antivirus alert email';
$string['notifyemail_help'] = 'If set, then only the specified email will be notified when a virus is detected.
If blank, then all site admins will be notified by email when a virus is detected.';
$string['privacy:metadata'] = 'The Antivirus system does not store any personal data.';
$string['quarantinedfiles'] = 'Antivirus quarantined files';
$string['quarantinetime'] = 'Maximum quarantine time';
$string['quarantinetime_desc'] = 'Quarantined files older than specified period will be removed.';
$string['taskcleanup'] = 'Clean up quarantined files.';
$string['unknown'] = 'Unknown';
$string['virusfound'] = '{$a->item} has been scanned by a virus checker and found to be infected!';
+5
View File
@@ -228,6 +228,9 @@ class scanner extends \core\antivirus\scanner {
$notice .= "\n\n". implode("\n", $output);
$this->set_scanning_notice($notice);
return self::SCAN_RESULT_ERROR;
} else {
$notice = "\n\n". implode("\n", $output);
$this->set_scanning_notice($notice);
}
return (int)$return;
@@ -384,6 +387,8 @@ class scanner extends \core\antivirus\scanner {
$parts = explode(' ', $message);
$status = array_pop($parts);
if ($status === 'FOUND') {
$notice = "\n\n" . $output;
$this->set_scanning_notice($notice);
return self::SCAN_RESULT_FOUND;
} else {
$notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2));
+38 -2
View File
@@ -67,11 +67,28 @@ class manager {
* @return void
*/
public static function scan_file($file, $filename, $deleteinfected) {
global $USER;
$antiviruses = self::get_enabled();
foreach ($antiviruses as $antivirus) {
$result = $antivirus->scan_file($file, $filename);
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found.
// Infection found, send notification.
$notice = $antivirus->get_scanning_notice();
$incidencedetails = $antivirus->get_incidence_details($file, $filename, $notice);
$antivirus->message_admins($notice, FORMAT_MOODLE, 'infected');
// Move to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidencedetails, $notice);
// Log file infected event.
$params = array(
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
);
$event = \core\event\antivirus_file_infected::create($params);
$event->trigger();
if ($deleteinfected) {
unlink($file);
}
@@ -83,15 +100,34 @@ class manager {
/**
* Scan data steam using all enabled antiviruses, throws exception in case of infected data.
*
* @param string $data The varaible containing the data to scan.
* @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();
foreach ($antiviruses as $antivirus) {
$result = $antivirus->scan_data($data);
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found, send notification.
$filename = get_string('datastream', 'antivirus');
$notice = $antivirus->get_scanning_notice();
$incidencedetails = $antivirus->get_incidence_details('', $filename, $notice);
$antivirus->message_admins($notice, FORMAT_MOODLE, 'infected');
// Copy data to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidencedetails, $notice);
// Log file infected event.
$params = array(
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
);
$event = \core\event\antivirus_data_infected::create($params);
$event->trigger();
throw new \core\antivirus\scanner_exception('virusfound', '', array('item' => get_string('datastream', 'antivirus')));
}
}
+264
View File
@@ -0,0 +1,264 @@
<?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/>.
/**
* Quarantine file
*
* @package core_antivirus
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/filelib.php');
/**
* Quarantine file
*
* @package core_antivirus
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quarantine {
/** Default quarantine folder */
const DEFAULT_QUARANTINE_FOLDER = 'antivirus_quarantine';
/** Zip infected file */
const FILE_ZIP_INFECTED = '_infected_file.zip';
/** Zip all infected file */
const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip';
/** Incidence details file */
const FILE_HTML_DETAILS = '_details.html';
/** Incidence details file */
const DEFAULT_QUARANTINE_TIME = DAYSECS * 28;
/** Date format in filename */
const FILE_NAME_DATE_FORMAT = '%Y%m%d%H%M%S';
/**
* Move the infected file to the quarantine folder
*
* @param string $file infected file
* @param string $filename infected file name
* @param string $incidencedetails incidence details
* @param string $notice notice details
* @throws \dml_exception
*/
public static function quarantine_file($file, $filename, $incidencedetails, $notice) {
if (!self::is_allowed_quarantine()) {
return;
}
// Generate file names.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
$detailsfilename = $date . self::FILE_HTML_DETAILS;
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidencedetails, FORMAT_MOODLE));
$ziparchive->add_file_from_pathname($filename, $file);
$ziparchive->close();
}
$zipfile = basename($zipfilepath);
self::create_infected_file_record($filename, $zipfile, $notice);
return $zipfile;
}
/**
* Move the infected file to the quarantine folder
*
* @param string $data data which is infected
* @param string $filename infected file name
* @param string $incidencedetails incidence details
* @param string $notice notice details
* @throws \dml_exception
*/
public static function quarantine_data($data, $filename, $incidencedetails, $notice) {
if (!self::is_allowed_quarantine()) {
return;
}
// Generate file names.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
$detailsfilename = $date . self::FILE_HTML_DETAILS;
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidencedetails, FORMAT_MOODLE));
$ziparchive->add_file_from_string($filename, $data);
$ziparchive->close();
}
$zipfile = basename($zipfilepath);
self::create_infected_file_record($filename, $zipfile, $notice);
return $zipfile;
}
/**
* Check if the virus quarantine is allowed
*
* @return bool
* @throws \dml_exception
*/
public static function is_allowed_quarantine() {
return !empty(get_config("antivirus", "enablequarantine"));
}
/**
* Get quarantine folder
*
* @return string path of quarantine folder
*/
public static function get_quarantine_folder() {
global $CFG;
$quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER;
if (!file_exists($quarantinefolder)) {
make_upload_directory(self::DEFAULT_QUARANTINE_FOLDER);
}
return $quarantinefolder . DIRECTORY_SEPARATOR;
}
/**
* Download quarantined file
*
* @param string $filename name of file to be downloaded
*/
public static function download_quarantined_file($filename) {
$file = self::get_quarantine_folder() . $filename;
// send_file($file, $filename);
}
/**
* Delete quarantined file
*
* @param string $filename name of file to be deleted
*/
public static function delete_quarantined_file($filename) {
self::delete_infected_file_record($filename);
$file = self::get_quarantine_folder() . $filename;
if (file_exists($file)) {
// unlink($file);
}
}
/**
* Download all quarantined files
*
*/
public static function download_all_quarantined_files() {
$files = new \DirectoryIterator(self::get_quarantine_folder());
// Add all infected file to a zip file.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT);
$zipfilename = $date . self::FILE_ZIP_ALL_INFECTED;
$zipfilepath = self::get_quarantine_folder() . DIRECTORY_SEPARATOR . $zipfilename;
$tempfilestocleanup = [];
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
foreach ($files as $file) {
if (!$file->isDot()) {
$filename = $file->getFilename();
$filepath = $file->getPathname();
$ziparchive->add_file_from_pathname($filename, $filepath);
}
}
$ziparchive->close();
}
// Clean up temp files.
foreach ($tempfilestocleanup as $tempfile) {
if (file_exists($tempfile)) {
unlink($tempfile);
}
}
// send_temp_file($zipfilepath, $zipfilename);
}
/**
* Return array of quarantined files
*
* @return array list of quarantined files
*/
public static function get_quarantined_files() {
$files = new \DirectoryIterator(self::get_quarantine_folder());
$filestosort = [];
foreach ($files as $file) {
$filename = $file->getFilename();
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
$filestosort[$filename] = $file->getPathname();
}
}
krsort($filestosort, SORT_NATURAL);
return $filestosort;
}
/**
* Clean up quarantine folder
*
* @param int $timetocleanup time to clean up
*/
public static function clean_up_quarantine_folder($timetocleanup) {
$files = new \DirectoryIterator(self::get_quarantine_folder());
// Clean up the folder.
foreach ($files as $file) {
$filename = $file->getFilename();
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
$modifiedtime = $file->getMTime();
if ($modifiedtime <= $timetocleanup) {
unlink($file->getPathname());
self::delete_infected_file_record($filename);
}
}
}
}
/**
* Create an infected file record
*
* @param string $filename original file name
* @param string $zipfile quarantined file name
* @param string $reason failure reason
* @throws \dml_exception
*/
private static function create_infected_file_record($filename, $zipfile, $reason) {
global $DB, $USER;
$record = new \stdClass();
$record->filename = $filename;
$record->quarantinedfile = $zipfile;
$record->author = fullname($USER);
$record->reason = $reason;
$record->timecreated = time();
$DB->insert_record('infected_files', $record);
}
/**
* Delete an infected_file_record
*
* @param string $zipfile quarantined file name
* @throws \dml_exception
*/
private static function delete_infected_file_record($zipfile) {
global $DB;
$DB->delete_records('infected_files', ['quarantinedfile' => $zipfile]);
}
}
+56 -6
View File
@@ -133,27 +133,77 @@ abstract class scanner {
* Email admins about antivirus scan outcomes.
*
* @param string $notice The body of the email to be sent.
* @param string $format The body format.
* @param string $eventname event name
* @return void
* @throws \coding_exception
* @throws \moodle_exception
*/
public function message_admins($notice) {
public function message_admins($notice, $format = FORMAT_PLAIN, $eventname = 'errors') {
$noticehtml = $format !== FORMAT_PLAIN ? format_text($notice, $format) : '';
$site = get_site();
$subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
$notifyemail = get_config('antivirus', 'notifyemail');
if (!empty($notifyemail)) {
$user = new \stdClass();
$user->id = -1;
$user->email = $notifyemail;
email_to_user($user, get_admin(), $subject, $noticehtml);
return;
}
$admins = get_admins();
foreach ($admins as $admin) {
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
$eventdata->component = 'moodle';
$eventdata->name = 'errors';
$eventdata->name = $eventname;
$eventdata->userfrom = get_admin();
$eventdata->userto = $admin;
$eventdata->subject = $subject;
$eventdata->fullmessage = $notice;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '';
$eventdata->fullmessageformat = $format;
$eventdata->fullmessagehtml = $noticehtml;
$eventdata->smallmessage = '';
message_send($eventdata);
}
}
}
/**
* Return incidence details
*
* @param string $file full path to the file
* @param string $filename original name of the file
* @param string $notice notice from antivirus
* @return string the incidence details
* @throws \coding_exception
*/
public function get_incidence_details($file = '', $filename = '', $notice = '') {
global $USER;
if (empty($notice)) {
$notice = $this->get_scanning_notice();
}
$content = new \stdClass();
$unknown = get_string('unknown', 'antivirus');;
$content->filename = !empty($filename) ? $filename : $unknown;
if (!empty($file)) {
$content->filesize = filesize($file);
$content->contenthash = \file_storage::hash_from_string(file_get_contents($file));
$content->contenttype = mime_content_type($file);
} else {
$content->filesize = $unknown;
$content->contenthash = $unknown;
$content->contenttype = $unknown;
}
$content->author = \core_user::is_real_user($USER->id) ? fullname($USER) . " ($USER->username)" : $unknown;
$content->ipaddress = getremoteaddr();
$content->date = userdate(time(), get_string('strftimedatetimeshort'));
$content->referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $unknown;
$content->notice = $notice;
$report = new \moodle_url('/report/infectedfiles/index.php');
$content->report = $report->out();
return get_string('incidencedetails', 'antivirus', $content);
}
}
@@ -0,0 +1,75 @@
<?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/>.
/**
* Data infected event
*
* @package core
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\event;
defined('MOODLE_INTERNAL') || die();
/**
* Data infected event
*
* @package core
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_data_infected extends \core\event\base {
/**
* Event data
*/
protected function init() {
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Return event description
*
* @return string description
* @throws \coding_exception
*/
public function get_description() {
return isset($this->other['incidencedetails']) ?
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected data';
}
/**
* Return event name
*
* @return string name
* @throws \coding_exception
*/
public static function get_name() {
return get_string('datainfectedname', 'antivirus');
}
/**
* Return event report link
* @return \moodle_url
* @throws \moodle_exception
*/
public function get_url() {
return new \moodle_url('/report/infectedfiles/index.php');
}
}
@@ -0,0 +1,75 @@
<?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/>.
/**
* Fle infected event
*
* @package core
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\event;
defined('MOODLE_INTERNAL') || die();
/**
* Fle infected event
*
* @package core
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_file_infected extends \core\event\base {
/**
* Event data
*/
protected function init() {
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Return event description
*
* @return string description
* @throws \coding_exception
*/
public function get_description() {
return isset($this->other['incidencedetails']) ?
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected file';
}
/**
* Return event name
*
* @return string name
* @throws \coding_exception
*/
public static function get_name() {
return get_string('fileinfectedname', 'antivirus');
}
/**
* Return event report link
* @return \moodle_url
* @throws \moodle_exception
*/
public function get_url() {
return new \moodle_url('/report/infectedfiles/index.php');
}
}
@@ -0,0 +1,62 @@
<?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/>.
/**
* Clean up task for core antivirus
*
* @package core_antivirus
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
defined('MOODLE_INTERNAL') || die();
/**
* Clean up task for core antivirus
*
* @package core_antivirus
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_cleanup_task extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task.
*
* @return string
*/
public function get_name() {
return get_string('taskcleanup', 'antivirus');
}
/**
* Processes workflows.
*/
public function execute() {
$quarantinetime = get_config('antivirus', 'quarantinetime');
if (empty($quarantinetime)) {
$quarantinetime = \core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME;
set_config('quarantinetime', $quarantinetime, 'antivirus');
}
$timetocleanup = time() - $quarantinetime;
\core\antivirus\quarantine::clean_up_quarantine_folder($timetocleanup);
}
}
+10
View File
@@ -4275,5 +4275,15 @@
<INDEX NAME="instance" UNIQUE="false" FIELDS="contextid, contenttype, instanceid"/>
</INDEXES>
</TABLE>
<TABLE NAME="infected_files" COMMENT="Store virus infected file details">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="filename" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Original file name"/>
<FIELD NAME="quarantinedfile" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Quarantine zip file"/>
<FIELD NAME="author" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="User who uploaded the infected files"/>
<FIELD NAME="reason" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Failure reason"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
</TABLE>
</TABLES>
</XMLDB>
+5
View File
@@ -142,4 +142,9 @@ $messageproviders = array (
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
),
],
// Infected files.
'infected' => array (
'capability' => 'moodle/site:config'
),
);
+9
View File
@@ -401,4 +401,13 @@ $tasks = array(
'dayofweek' => '*',
'month' => '*'
),
array(
'classname' => 'core\task\antivirus_cleanup_task',
'blocking' => 0,
'minute' => '0',
'hour' => '0',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
);
+20
View File
@@ -2539,5 +2539,25 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2020072300.01);
}
if ($oldversion < 2020080500.01) {
// Define table to store virus infected details.
$table = new xmldb_table('infected_files');
// Adding fields.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('filename', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('quarantinedfile', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null, null);
$table->add_field('author', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('reason', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
// Adding keys.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
upgrade_main_savepoint(true, 2020080500.01);
}
return true;
}
+126
View File
@@ -85,6 +85,67 @@ class core_antivirus_testcase extends advanced_testcase {
$this->assertFileNotExists($this->tempfile);
}
public function test_manager_send_message_to_user_email_scan_file_virus() {
$sink = $this->redirectEmails();
$exception = null;
try {
set_config('notifyemail', '[email protected]', 'antivirus');
\core\antivirus\manager::scan_file($this->tempfile, 'FOUND', true);
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
$result = $sink->get_messages();
$this->assertCount(1, $result);
$this->assertContains('[email protected]', $result[0]->to);
$sink->close();
}
public function test_manager_send_message_to_admin_email_scan_file_virus() {
$sink = $this->redirectMessages();
$exception = null;
try {
\core\antivirus\manager::scan_file($this->tempfile, 'FOUND', true);
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
$result = $sink->get_messages();
$admins = array_keys(get_admins());
$this->assertCount(1, $admins);
$this->assertCount(1, $result);
$this->assertEquals($result[0]->useridto, reset($admins));
$sink->close();
}
public function test_manager_quarantine_file_virus() {
try {
set_config('enablequarantine', true, 'antivirus');
\core\antivirus\manager::scan_file($this->tempfile, 'FOUND', true);
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
// Quarantined files.
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(1, count($quarantinedfiles));
// Clean up.
\core\antivirus\quarantine::clean_up_quarantine_folder(time());
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(0, count($quarantinedfiles));
}
public function test_manager_none_quarantine_file_virus() {
try {
\core\antivirus\manager::scan_file($this->tempfile, 'FOUND', true);
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(0, count($quarantinedfiles));
}
public function test_manager_scan_data_no_virus() {
// Run mock scanning.
$this->assertEmpty(\core\antivirus\manager::scan_data('OK'));
@@ -100,4 +161,69 @@ class core_antivirus_testcase extends advanced_testcase {
$this->expectException(\core\antivirus\scanner_exception::class);
$this->assertEmpty(\core\antivirus\manager::scan_data('FOUND'));
}
public function test_manager_send_message_to_user_email_scan_data_virus() {
$sink = $this->redirectEmails();
set_config('notifyemail', '[email protected]', 'antivirus');
$exception = null;
try {
\core\antivirus\manager::scan_data('FOUND');
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
$result = $sink->get_messages();
$this->assertCount(1, $result);
$this->assertContains('[email protected]', $result[0]->to);
$sink->close();
}
public function test_manager_send_message_to_admin_email_scan_data_virus() {
$sink = $this->redirectMessages();
$exception = null;
try {
\core\antivirus\manager::scan_data('FOUND');
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
$result = $sink->get_messages();
$admins = array_keys(get_admins());
$this->assertCount(1, $admins);
$this->assertCount(1, $result);
$this->assertEquals($result[0]->useridto, reset($admins));
$sink->close();
}
public function test_manager_quarantine_data_virus() {
set_config('enablequarantine', true, 'antivirus');
$exception = null;
try {
\core\antivirus\manager::scan_data('FOUND');
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
// Quarantined files.
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(1, count($quarantinedfiles));
// Clean up.
\core\antivirus\quarantine::clean_up_quarantine_folder(time());
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(0, count($quarantinedfiles));
}
public function test_manager_none_quarantine_data_virus() {
$exception = null;
try {
\core\antivirus\manager::scan_data('FOUND');
} catch (\core\antivirus\scanner_exception $ex) {
$exception = $ex;
}
$this->assertNotEmpty($exception);
// No Quarantined files.
$quarantinedfiles = \core\antivirus\quarantine::get_quarantined_files();
$this->assertEquals(0, count($quarantinedfiles));
}
}
@@ -0,0 +1,219 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_infectedfiles\output;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/tablelib.php');
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class infectedfiles_table extends \table_sql implements \renderable {
/**
* Table constructor
*
* @param int $uniqueid table id
* @param \moodle_url $url page url
* @param int $page current page
* @param int $perpage number or record per page
* @throws \coding_exception
*/
public function __construct($uniqueid, \moodle_url $url, $page = 0, $perpage = 30) {
parent::__construct($uniqueid);
$this->set_attribute('class', 'report_infectedfiles');
// Set protected properties.
$this->pagesize = $perpage;
$this->page = $page;
// Define columns in the table.
$this->define_table_columns();
// Define configs.
$this->define_table_configs($url);
}
/**
* Table columns and corresponding headers
*
* @throws \coding_exception
*/
protected function define_table_columns() {
$cols = array(
'filename' => get_string('filename', 'report_infectedfiles'),
'quarantinedfile' => get_string('quarantinedfile', 'report_infectedfiles'),
'author' => get_string('author', 'report_infectedfiles'),
'reason' => get_string('reason', 'report_infectedfiles'),
'timecreated' => get_string('timecreated', 'report_infectedfiles'),
'actions' => get_string('actions'),
);
$this->define_columns(array_keys($cols));
$this->define_headers(array_values($cols));
}
/**
* Define table configuration
*
* @param \moodle_url $url
*/
protected function define_table_configs(\moodle_url $url) {
// Set table url.
$this->define_baseurl($url);
// Set table configs.
$this->collapsible(false);
$this->sortable(false);
$this->pageable(true);
}
/**
* Builds the SQL query.
*
* @param bool $count When true, return the count SQL.
* @return array containing sql to use and an array of params.
*/
protected function get_sql_and_params($count = false) {
if ($count) {
$select = "COUNT(1)";
} else {
$select = "*";
}
$sql = "SELECT $select
FROM {infected_files}";
$params = array();
if (!$count) {
$sql .= " ORDER BY timecreated DESC";
}
return array($sql, $params);
}
/**
* Get data.
*
* @param int $pagesize number of records to fetch
* @param bool $useinitialsbar initial bar
* @throws \dml_exception
*/
public function query_db($pagesize, $useinitialsbar = true) {
global $DB;
list($countsql, $countparams) = $this->get_sql_and_params(true);
list($sql, $params) = $this->get_sql_and_params();
$total = $DB->count_records_sql($countsql, $countparams);
$this->pagesize($pagesize, $total);
$this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
}
/**
* Custom actions column
*
* @param \stdClass $row an incidence record
* @return string content of action column
* @throws \coding_exception
* @throws \moodle_exception
*/
protected function col_actions($row) {
global $OUTPUT;
$filename = $row->quarantinedfile;
$zipfile = \core\antivirus\quarantine::get_quarantine_folder() . $filename;
if (!file_exists($zipfile)) {
return '';
}
$links = '';
$managefilepage = new \moodle_url('/report/infectedfiles/manage_infected_files.php');
// Download.
$downloadparams = ['filename' => $filename, 'action' => 'download', 'sesskey' => sesskey()];
$downloadurl = new \moodle_url($managefilepage, $downloadparams);
$icon = $OUTPUT->pix_icon('t/download', get_string('download'));
$downloadlink = \html_writer::link($downloadurl, $icon);
$links .= ' ' . $downloadlink;
// Delete.
$deleteparams = ['filename' => $filename, 'action' => 'confirmdelete', 'sesskey' => sesskey()];
$deleteurl = new \moodle_url($managefilepage, $deleteparams);
$icon = $OUTPUT->pix_icon('t/delete', get_string('delete'));
$deletelink = \html_writer::link($deleteurl, $icon);
$links .= ' ' . $deletelink;
return $links;
}
/**
* Custom time column
*
* @param \stdClass $row an incidence record
* @return string time created in user-friendly format
*/
protected function col_timecreated($row) {
return userdate($row->timecreated);
}
/**
* Display table with download all and delete all buttons
*
* @param int $pagesize number or records perpage
* @param bool $useinitialsbar use the bar or not
* @param string $downloadhelpbutton help button
* @throws \coding_exception
* @throws \moodle_exception
*/
public function display($pagesize, $useinitialsbar, $downloadhelpbutton='') {
$this->out($pagesize, $useinitialsbar, $downloadhelpbutton);
$managefilepage = new \moodle_url('/report/infectedfiles/manage_infected_files.php');
// Delete All.
$button = \html_writer::tag('button', get_string('deleteall'), ['class' => 'btn btn-primary']);
$deleteallparams = ['action' => 'confirmdeleteall', 'sesskey' => sesskey()];
$deleteallurl = new \moodle_url($managefilepage, $deleteallparams);
echo \html_writer::link($deleteallurl, $button);
echo "&nbsp";
// Download All.
$button = \html_writer::tag('button', get_string('downloadall'), ['class' => 'btn btn-primary']);
$downloadallparams = ['action' => 'downloadall', 'sesskey' => sesskey()];
$downloadallurl = new \moodle_url($managefilepage, $downloadallparams);
echo \html_writer::link($downloadallurl, $button);
}
}
@@ -0,0 +1,54 @@
<?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/>.
/**
* Infected file report renderer
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_infectedfiles\output;
defined('MOODLE_INTERNAL') || die();
/**
* Infected file report renderer
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Render the table
*
* @param infectedfiles_table $table table of infected files
* @return false|string return html code of the table
* @throws \coding_exception
* @throws \moodle_exception
*/
protected function render_infectedfiles_table(infectedfiles_table $table) {
ob_start();
$table->display($table->pagesize, false);
$o = ob_get_contents();
ob_end_clean();
return $o;
}
}
@@ -0,0 +1,47 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_infectedfiles\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* This plugin does not contain any personal data
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
}
+36
View File
@@ -0,0 +1,36 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('reportinfectedfiles', '', null, '', array('pagelayout' => 'report'));
$page = optional_param('page', 0, PARAM_INT);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
$table = new \report_infectedfiles\output\infectedfiles_table('report-infectedfiles-report-table', $PAGE->url, $page);
$table->define_baseurl($PAGE->url);
echo $PAGE->get_renderer('report_infectedfiles')->render($table);
echo $OUTPUT->footer();
@@ -0,0 +1,34 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['author'] = 'Author';
$string['confirmdelete'] = 'Do you really want to delete this file';
$string['confirmdeleteall'] = 'Do you really want to delete all files';
$string['filename'] = 'File name';
$string['infectedfiles'] = 'Infected files';
$string['privacy:metadata'] = 'This plugin does not contain any personal data';
$string['pluginname'] = 'Infected files';
$string['quarantinedfile'] = 'Quarantined file';
$string['reason'] = 'Failure reason';
$string['timecreated'] = 'Time created';
@@ -0,0 +1,77 @@
<?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/>.
/**
* Manage infected files.
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/adminlib.php');
use \core\antivirus\quarantine;
require_admin();
require_sesskey();
$action = optional_param('action', '', PARAM_TEXT);
$reportpage = new moodle_url('/report/infectedfiles/index.php');
$thispage = new moodle_url('/report/infectedfiles/manage_infected_files.php');
$PAGE->set_context(context_system::instance());
$PAGE->set_url($thispage);
$PAGE->navbar->add(get_string('infectedfiles', 'report_infectedfiles'), $reportpage);
switch ($action) {
case 'download':
$filename = required_param('filename', PARAM_TEXT);
quarantine::download_quarantined_file($filename);
case 'downloadall':
quarantine::download_all_quarantined_files();
case 'confirmdelete':
$filename = required_param('filename', PARAM_TEXT);
$deleteparams = ['filename' => $filename, 'action' => 'delete', 'sesskey' => sesskey()];
$confirmeddelete = new single_button(new moodle_url($thispage, $deleteparams), get_string('delete'), 'post');
$cancel = new single_button(new moodle_url($reportpage), get_string('cancel'), 'post');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
echo $OUTPUT->confirm(get_string('confirmdelete', 'report_infectedfiles'), $confirmeddelete, $cancel);
echo $OUTPUT->footer();
die;
case 'delete':
$filename = required_param('filename', PARAM_TEXT);
quarantine::delete_quarantined_file($filename);
redirect($reportpage);
case 'confirmdeleteall':
require_sesskey();
$deleteallparams = ['action' => 'deleteall', 'sesskey' => sesskey()];
$confirmeddeleteall = new single_button(new moodle_url($thispage, $deleteallparams), get_string('delete'), 'post');
$cancel = new single_button(new moodle_url($reportpage), get_string('cancel'), 'post');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
echo $OUTPUT->confirm(get_string('confirmdeleteall', 'report_infectedfiles'), $confirmeddeleteall, $cancel);
echo $OUTPUT->footer();
die;
case 'deleteall':
require_sesskey();
// Remove file until current time.
quarantine::clean_up_quarantine_folder(time());
redirect($reportpage);
default:
break;
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$ADMIN->add('reports', new admin_externalpage('reportinfectedfiles',
get_string('infectedfiles', 'report_infectedfiles'),
"$CFG->wwwroot/report/infectedfiles/index.php"));
$settings = null;
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Infected file report
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2020031800;
$plugin->requires = 2019111200;
$plugin->component = 'report_infectedfiles';