MDL-66222 antivirus: Improved error email capture
This commit is contained in:
@@ -172,22 +172,35 @@ if ($hassiteconfig) {
|
||||
$temp->add(new admin_setting_heading('antiviruscommonsettings', new lang_string('antiviruscommonsettings', 'antivirus'), ''));
|
||||
|
||||
// Alert email.
|
||||
$temp->add(new admin_setting_configtext('antivirus/notifyemail',
|
||||
$temp->add(
|
||||
new admin_setting_configtext(
|
||||
'antivirus/notifyemail',
|
||||
new lang_string('notifyemail', 'antivirus'),
|
||||
new lang_string('notifyemail_help', 'antivirus'), '', PARAM_EMAIL)
|
||||
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));
|
||||
$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',
|
||||
$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)
|
||||
\core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME
|
||||
)
|
||||
);
|
||||
|
||||
$ADMIN->add('antivirussettings', $temp);
|
||||
|
||||
+17
-14
@@ -27,32 +27,35 @@ $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['datainfecteddesc'] = 'Infected data was detected.';
|
||||
$string['datainfectedname'] = 'Data infected';
|
||||
$string['emailadditionalinfo'] = 'Additional details returned from the virus engine: ';
|
||||
$string['emailauthor'] = 'Uploaded by: ';
|
||||
$string['emailcontenthash'] = 'Content hash: ';
|
||||
$string['emailcontenttype'] = 'Content type: ';
|
||||
$string['emaildate'] = 'Date uploaded: ';
|
||||
$string['emailfilename'] = 'Filename: ';
|
||||
$string['emailfilesize'] = 'File size: ';
|
||||
$string['emailgeoinfo'] = 'Geolocation: ';
|
||||
$string['emailinfectedfiledetected'] = 'Infected file detected';
|
||||
$string['emailipaddress'] = 'IP Address: ';
|
||||
$string['emailreferer'] = 'Referer: ';
|
||||
$string['emailreport'] = 'Report: ';
|
||||
$string['emailscanner'] = 'Scanner: ';
|
||||
$string['emailscannererrordetected'] = 'A scanner error occured';
|
||||
$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['fileinfecteddesc'] = 'An infected file was detected.';
|
||||
$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['quarantinedisabled'] = 'Quarantine disabled, file not stored.';
|
||||
$string['quarantinedfiles'] = 'Antivirus quarantined files';
|
||||
$string['quarantinetime'] = 'Maximum quarantine time';
|
||||
$string['quarantinetime_desc'] = 'Quarantined files older than specified period will be removed.';
|
||||
|
||||
@@ -1240,6 +1240,7 @@ $string['messageprovider:gradenotifications'] = 'Grade notifications';
|
||||
$string['messageprovider:messagecontactrequests'] = 'Message contact requests notification';
|
||||
$string['messageprovider:notices'] = 'Notices about minor problems';
|
||||
$string['messageprovider:notices_help'] = 'These are notices that an administrator might be interested in seeing.';
|
||||
$string['messageprovider:infected'] = 'Antivirus failure notifications.';
|
||||
$string['messageprovider:insights'] = 'Insights generated by prediction models';
|
||||
$string['messageprovider:instantmessage'] = 'Personal messages between users';
|
||||
$string['messageprovider:instantmessage_help'] = 'This section configures what happens to messages that are sent to you directly from other users on this site.';
|
||||
|
||||
@@ -70,29 +70,47 @@ class manager {
|
||||
global $USER;
|
||||
$antiviruses = self::get_enabled();
|
||||
foreach ($antiviruses as $antivirus) {
|
||||
$result = $antivirus->scan_file($file, $filename);
|
||||
// Attempt to scan, catching internal exceptions.
|
||||
try {
|
||||
$result = $antivirus->scan_file($file, $filename);
|
||||
} catch (\core\antivirus\scanner_exception $e) {
|
||||
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
|
||||
$notice = $antivirus->get_scanning_notice();
|
||||
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
|
||||
self::send_antivirus_messages($antivirus, $incidentdetails);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$notice = $antivirus->get_scanning_notice();
|
||||
if ($result === $antivirus::SCAN_RESULT_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');
|
||||
$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, $incidencedetails, $notice);
|
||||
$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 = array(
|
||||
$params = [
|
||||
'context' => \context_system::instance(),
|
||||
'relateduserid' => $USER->id,
|
||||
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
|
||||
);
|
||||
$event = \core\event\antivirus_file_infected::create($params);
|
||||
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
|
||||
];
|
||||
$event = \core\event\virus_infected_file_detected::create($params);
|
||||
$event->trigger();
|
||||
|
||||
if ($deleteinfected) {
|
||||
unlink($file);
|
||||
}
|
||||
throw new \core\antivirus\scanner_exception('virusfound', '', array('item' => $filename));
|
||||
} 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);
|
||||
self::send_antivirus_messages($antivirus, $incidentdetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,27 +126,48 @@ class manager {
|
||||
global $USER;
|
||||
$antiviruses = self::get_enabled();
|
||||
foreach ($antiviruses as $antivirus) {
|
||||
$result = $antivirus->scan_data($data);
|
||||
// Attempt to scan, catching internal exceptions.
|
||||
try {
|
||||
$result = $antivirus->scan_data($data);
|
||||
} catch (\core\antivirus\scanner_exception $e) {
|
||||
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
|
||||
$notice = $antivirus->get_scanning_notice();
|
||||
$filename = get_string('datastream', 'antivirus');
|
||||
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
|
||||
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.
|
||||
$filename = get_string('datastream', 'antivirus');
|
||||
$notice = $antivirus->get_scanning_notice();
|
||||
$incidencedetails = $antivirus->get_incidence_details('', $filename, $notice);
|
||||
$antivirus->message_admins($notice, FORMAT_MOODLE, 'infected');
|
||||
$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, $incidencedetails, $notice);
|
||||
$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 = array(
|
||||
$params = [
|
||||
'context' => \context_system::instance(),
|
||||
'relateduserid' => $USER->id,
|
||||
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
|
||||
);
|
||||
$event = \core\event\antivirus_data_infected::create($params);
|
||||
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
|
||||
];
|
||||
$event = \core\event\virus_infected_data_detected::create($params);
|
||||
$event->trigger();
|
||||
|
||||
throw new \core\antivirus\scanner_exception('virusfound', '', array('item' => get_string('datastream', 'antivirus')));
|
||||
} 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);
|
||||
self::send_antivirus_messages($antivirus, $incidentdetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,4 +200,53 @@ class manager {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,27 +47,28 @@ class quarantine {
|
||||
/** Zip all infected file */
|
||||
const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip';
|
||||
|
||||
/** Incidence details file */
|
||||
/** Incident details file */
|
||||
const FILE_HTML_DETAILS = '_details.html';
|
||||
|
||||
/** Incidence details file */
|
||||
/** Incident 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
|
||||
* 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
|
||||
* @param string $file infected file.
|
||||
* @param string $filename infected file name.
|
||||
* @param string $incidentdetails incident details.
|
||||
* @param string $notice notice details.
|
||||
* @return string|null the name of the newly created quarantined file.
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function quarantine_file($file, $filename, $incidencedetails, $notice) {
|
||||
if (!self::is_allowed_quarantine()) {
|
||||
return;
|
||||
public static function quarantine_file(string $file, string $filename, string $incidentdetails, string $notice) : ?string {
|
||||
if (!self::is_quarantine_enabled()) {
|
||||
return null;
|
||||
}
|
||||
// Generate file names.
|
||||
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
|
||||
@@ -77,7 +78,7 @@ class quarantine {
|
||||
// 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($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
|
||||
$ziparchive->add_file_from_pathname($filename, $file);
|
||||
$ziparchive->close();
|
||||
}
|
||||
@@ -87,17 +88,18 @@ class quarantine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the infected file to the quarantine folder
|
||||
* 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
|
||||
* @param string $data data which is infected.
|
||||
* @param string $filename infected file name.
|
||||
* @param string $incidentdetails incident details.
|
||||
* @param string $notice notice details.
|
||||
* @return string|null the name of the newly created quarantined file.
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function quarantine_data($data, $filename, $incidencedetails, $notice) {
|
||||
if (!self::is_allowed_quarantine()) {
|
||||
return;
|
||||
public static function quarantine_data(string $data, string $filename, string $incidentdetails, string $notice) : ?string {
|
||||
if (!self::is_quarantine_enabled()) {
|
||||
return null;
|
||||
}
|
||||
// Generate file names.
|
||||
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
|
||||
@@ -107,7 +109,7 @@ class quarantine {
|
||||
// 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($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
|
||||
$ziparchive->add_file_from_string($filename, $data);
|
||||
$ziparchive->close();
|
||||
}
|
||||
@@ -122,7 +124,7 @@ class quarantine {
|
||||
* @return bool
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function is_allowed_quarantine() {
|
||||
public static function is_quarantine_enabled() : bool {
|
||||
return !empty(get_config("antivirus", "enablequarantine"));
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ class quarantine {
|
||||
*
|
||||
* @return string path of quarantine folder
|
||||
*/
|
||||
public static function get_quarantine_folder() {
|
||||
private static function get_quarantine_folder() : string {
|
||||
global $CFG;
|
||||
$quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER;
|
||||
if (!file_exists($quarantinefolder)) {
|
||||
@@ -141,43 +143,78 @@ class quarantine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Download quarantined file
|
||||
* Checks whether a file exists inside the antivirus quarantine folder.
|
||||
*
|
||||
* @param string $filename name of file to be downloaded
|
||||
* @param string $filename the filename to check.
|
||||
* @return boolean whether file exists.
|
||||
*/
|
||||
public static function download_quarantined_file($filename) {
|
||||
$file = self::get_quarantine_folder() . $filename;
|
||||
// send_file($file, $filename);
|
||||
public static function quarantined_file_exists(string $filename) : bool {
|
||||
$folder = self::get_quarantine_folder();
|
||||
return file_exists($folder . $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete quarantined file
|
||||
* Download quarantined file.
|
||||
*
|
||||
* @param string $filename name of file to be deleted
|
||||
* @param int $fileid the id of file to be downloaded.
|
||||
*/
|
||||
public static function delete_quarantined_file($filename) {
|
||||
self::delete_infected_file_record($filename);
|
||||
public static function download_quarantined_file(int $fileid) {
|
||||
global $DB;
|
||||
|
||||
// Get the filename to be downloaded.
|
||||
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
|
||||
// If file record isnt found, user might be doing something naughty in params, or a stale request.
|
||||
if (empty($filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = self::get_quarantine_folder() . $filename;
|
||||
send_file($file, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete quarantined file.
|
||||
*
|
||||
* @param int $fileid id of file to be deleted.
|
||||
*/
|
||||
public static function delete_quarantined_file(int $fileid) {
|
||||
global $DB;
|
||||
|
||||
// Get the filename to be deleted.
|
||||
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
|
||||
// If file record isnt found, user might be doing something naughty in params, or a stale request.
|
||||
if (empty($filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the file from the folder.
|
||||
$file = self::get_quarantine_folder() . $filename;
|
||||
if (file_exists($file)) {
|
||||
// unlink($file);
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
// Now we are finished with the record, delete the quarantine information.
|
||||
self::delete_infected_file_record($fileid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all quarantined files
|
||||
* Download all quarantined files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function download_all_quarantined_files() {
|
||||
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
||||
// Add all infected file to a zip file.
|
||||
// Add all infected files 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()) {
|
||||
// Only send the actual files.
|
||||
$filename = $file->getFilename();
|
||||
$filepath = $file->getPathname();
|
||||
$ziparchive->add_file_from_pathname($filename, $filepath);
|
||||
@@ -185,29 +222,34 @@ class quarantine {
|
||||
}
|
||||
$ziparchive->close();
|
||||
}
|
||||
|
||||
// Clean up temp files.
|
||||
foreach ($tempfilestocleanup as $tempfile) {
|
||||
if (file_exists($tempfile)) {
|
||||
unlink($tempfile);
|
||||
}
|
||||
}
|
||||
// send_temp_file($zipfilepath, $zipfilename);
|
||||
|
||||
send_temp_file($zipfilepath, $zipfilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of quarantined files
|
||||
* Return array of quarantined files.
|
||||
*
|
||||
* @return array list of quarantined files
|
||||
* @return array list of quarantined files.
|
||||
*/
|
||||
public static function get_quarantined_files() {
|
||||
public static function get_quarantined_files() : array {
|
||||
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
||||
$filestosort = [];
|
||||
|
||||
// Grab all files that match the naming structure.
|
||||
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;
|
||||
}
|
||||
@@ -217,19 +259,37 @@ class quarantine {
|
||||
*
|
||||
* @param int $timetocleanup time to clean up
|
||||
*/
|
||||
public static function clean_up_quarantine_folder($timetocleanup) {
|
||||
public static function clean_up_quarantine_folder(int $timetocleanup) {
|
||||
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
||||
// Clean up the folder.
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// Only delete files that match the correct name structure.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly cleanup the infected files table as well.
|
||||
self::clean_up_infected_records($timetocleanup);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function removes any stale records from the infected files table.
|
||||
*
|
||||
* @param int $timetocleanup the time to cleanup from
|
||||
* @return void
|
||||
*/
|
||||
private static function clean_up_infected_records(int $timetocleanup) {
|
||||
global $DB;
|
||||
|
||||
$select = "timecreated <= ?";
|
||||
$DB->delete_records_select('infected_files', $select, [$timetocleanup]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,25 +300,27 @@ class quarantine {
|
||||
* @param string $reason failure reason
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
private static function create_infected_file_record($filename, $zipfile, $reason) {
|
||||
private static function create_infected_file_record(string $filename, string $zipfile, string $reason) {
|
||||
global $DB, $USER;
|
||||
|
||||
$record = new \stdClass();
|
||||
$record->filename = $filename;
|
||||
$record->quarantinedfile = $zipfile;
|
||||
$record->author = fullname($USER);
|
||||
$record->userid = $USER->id;
|
||||
$record->reason = $reason;
|
||||
$record->timecreated = time();
|
||||
|
||||
$DB->insert_record('infected_files', $record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an infected_file_record
|
||||
* Delete the database record for an infected file.
|
||||
*
|
||||
* @param string $zipfile quarantined file name
|
||||
* @param int $fileid quarantined file id
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
private static function delete_infected_file_record($zipfile) {
|
||||
private static function delete_infected_file_record(int $fileid) {
|
||||
global $DB;
|
||||
$DB->delete_records('infected_files', ['quarantinedfile' => $zipfile]);
|
||||
$DB->delete_records('infected_files', ['id' => $fileid]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
namespace core\antivirus;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once(__DIR__ . '../../../../iplookup/lib.php');
|
||||
|
||||
/**
|
||||
* Base abstract antivirus scanner class.
|
||||
@@ -46,6 +47,8 @@ abstract class scanner {
|
||||
protected $config;
|
||||
/** @var string scanning notice */
|
||||
protected $scanningnotice = '';
|
||||
/** @var array any admin messages generated by a plugin. */
|
||||
protected $messages = [];
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
@@ -130,7 +133,7 @@ abstract class scanner {
|
||||
}
|
||||
|
||||
/**
|
||||
* Email admins about antivirus scan outcomes.
|
||||
* This function pushes given messages into the message queue, which will be sent by the antivirus manager.
|
||||
*
|
||||
* @param string $notice The body of the email to be sent.
|
||||
* @param string $format The body format.
|
||||
@@ -145,15 +148,18 @@ abstract class scanner {
|
||||
|
||||
$subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
|
||||
$notifyemail = get_config('antivirus', 'notifyemail');
|
||||
// If one email address is specified, construct a message to fake account.
|
||||
if (!empty($notifyemail)) {
|
||||
$user = new \stdClass();
|
||||
$user->id = -1;
|
||||
$user->email = $notifyemail;
|
||||
email_to_user($user, get_admin(), $subject, $noticehtml);
|
||||
return;
|
||||
$user->mailformat = 1;
|
||||
$admins = [$user];
|
||||
} else {
|
||||
// Otherwise, we message all admins.
|
||||
$admins = get_admins();
|
||||
}
|
||||
|
||||
$admins = get_admins();
|
||||
foreach ($admins as $admin) {
|
||||
$eventdata = new \core\message\message();
|
||||
$eventdata->courseid = SITEID;
|
||||
@@ -166,29 +172,38 @@ abstract class scanner {
|
||||
$eventdata->fullmessageformat = $format;
|
||||
$eventdata->fullmessagehtml = $noticehtml;
|
||||
$eventdata->smallmessage = '';
|
||||
message_send($eventdata);
|
||||
|
||||
// Now add the message to an array to be sent by the antivirus manager.
|
||||
$this->messages[] = $eventdata;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return incidence details
|
||||
* Return incident 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
|
||||
* @param string $virus if this template is due to a virus found.
|
||||
* @return string the incident details
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function get_incidence_details($file = '', $filename = '', $notice = '') {
|
||||
global $USER;
|
||||
public function get_incident_details($file = '', $filename = '', $notice = '', $virus = true) {
|
||||
global $OUTPUT, $USER;
|
||||
if (empty($notice)) {
|
||||
$notice = $this->get_scanning_notice();
|
||||
}
|
||||
$classname = get_class($this);
|
||||
$component = explode('\\', $classname)[0];
|
||||
|
||||
$content = new \stdClass();
|
||||
$unknown = get_string('unknown', 'antivirus');;
|
||||
$unknown = get_string('unknown', 'antivirus');
|
||||
$content->header = get_string('emailinfectedfiledetected', 'antivirus');
|
||||
$content->filename = !empty($filename) ? $filename : $unknown;
|
||||
if (!empty($file)) {
|
||||
$content->filesize = filesize($file);
|
||||
$content->scanner = $component;
|
||||
// Check for empty file, or file not uploaded.
|
||||
if (!empty($file) && filesize($file) !== false) {
|
||||
$content->filesize = display_size(filesize($file));
|
||||
$content->contenthash = \file_storage::hash_from_string(file_get_contents($file));
|
||||
$content->contenttype = mime_content_type($file);
|
||||
} else {
|
||||
@@ -199,11 +214,28 @@ abstract class scanner {
|
||||
|
||||
$content->author = \core_user::is_real_user($USER->id) ? fullname($USER) . " ($USER->username)" : $unknown;
|
||||
$content->ipaddress = getremoteaddr();
|
||||
$geoinfo = iplookup_find_location(getremoteaddr());
|
||||
$content->geoinfo = $geoinfo['city'] . ', ' . $geoinfo['country'];
|
||||
$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);
|
||||
|
||||
// If this is not due to a virus, we need to change the header line.
|
||||
if (!$virus) {
|
||||
$content->header = get_string('emailscannererrordetected', 'antivirus');
|
||||
}
|
||||
|
||||
return $OUTPUT->render_from_template('core/infected_file_email', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method for messages queued by the antivirus scanner.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_messages() : array {
|
||||
return $this->messages;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class antivirus_data_infected extends \core\event\base {
|
||||
class virus_infected_data_detected extends \core\event\base {
|
||||
/**
|
||||
* Event data
|
||||
*/
|
||||
@@ -50,8 +50,11 @@ class antivirus_data_infected extends \core\event\base {
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function get_description() {
|
||||
return isset($this->other['incidencedetails']) ?
|
||||
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected data';
|
||||
if (isset($this->other['incidentdetails'])) {
|
||||
return format_text($this->other['incidentdetails'], FORMAT_MOODLE);
|
||||
} else {
|
||||
return get_string('datainfecteddesc', 'antivirus');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
+6
-3
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class antivirus_file_infected extends \core\event\base {
|
||||
class virus_infected_file_detected extends \core\event\base {
|
||||
/**
|
||||
* Event data
|
||||
*/
|
||||
@@ -50,8 +50,11 @@ class antivirus_file_infected extends \core\event\base {
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function get_description() {
|
||||
return isset($this->other['incidencedetails']) ?
|
||||
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected file';
|
||||
if (isset($this->other['incidentdetails'])) {
|
||||
return format_text($this->other['incidentdetails'], FORMAT_MOODLE);
|
||||
} else {
|
||||
return get_string('fileinfecteddesc', 'antivirus');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1966,8 +1966,8 @@ class core_plugin_manager {
|
||||
|
||||
'report' => array(
|
||||
'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
|
||||
'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
|
||||
'security', 'stats', 'status', 'performance', 'usersessions'
|
||||
'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
|
||||
'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
|
||||
),
|
||||
|
||||
'repository' => array(
|
||||
|
||||
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @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 {
|
||||
class antivirus_cleanup_task extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Get a descriptive name for this task.
|
||||
|
||||
+9
-5
@@ -4275,15 +4275,19 @@
|
||||
<INDEX NAME="instance" UNIQUE="false" FIELDS="contextid, contenttype, instanceid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="infected_files" COMMENT="Store virus infected file details">
|
||||
<TABLE NAME="infected_files" COMMENT="Table to store 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"/>
|
||||
<FIELD NAME="quarantinedfile" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Quarantine zip file"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The user that uploaded the infected file."/>
|
||||
<FIELD NAME="reason" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The reason for the antivirus failure"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time the infected file was uploaded."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id" COMMENT="Foreign key for the userid"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
+2
-2
@@ -144,7 +144,7 @@ $messageproviders = array (
|
||||
],
|
||||
|
||||
// Infected files.
|
||||
'infected' => array (
|
||||
'capability' => 'moodle/site:config'
|
||||
'infected' => array(
|
||||
'capability' => 'moodle/site:config',
|
||||
),
|
||||
);
|
||||
|
||||
+2
-2
@@ -404,10 +404,10 @@ $tasks = array(
|
||||
array(
|
||||
'classname' => 'core\task\antivirus_cleanup_task',
|
||||
'blocking' => 0,
|
||||
'minute' => '0',
|
||||
'minute' => 'R',
|
||||
'hour' => '0',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
'month' => '*',
|
||||
),
|
||||
);
|
||||
|
||||
+8
-6
@@ -2539,25 +2539,27 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2020072300.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2020080500.01) {
|
||||
if ($oldversion < 2020081400.01) {
|
||||
// Define table to store virus infected details.
|
||||
$table = new xmldb_table('infected_files');
|
||||
|
||||
// Adding fields.
|
||||
// Adding fields to table infected_files.
|
||||
$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('quarantinedfile', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', 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.
|
||||
// Adding keys to table infected_files.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
|
||||
|
||||
// Conditionally launch create table for infected_files.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
upgrade_main_savepoint(true, 2020080500.01);
|
||||
upgrade_main_savepoint(true, 2020081400.01);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/infected_file_email
|
||||
|
||||
Moodle template for infected files emails.
|
||||
|
||||
Context variables required for this template:
|
||||
* report - Hyperlink to the report page,
|
||||
* scanner - Scanning engine that found this file,
|
||||
* filename - Name of the infected file,
|
||||
* filesize - Size of the file,
|
||||
* contenthash - File content hash,
|
||||
* contenttype - Type of uploaded file,
|
||||
* author - User that uploaded the file,
|
||||
* ipaddress - IP address the file was uploaded from,
|
||||
* geoinfo - Geo information about IP address,
|
||||
* referer - The referring page,
|
||||
* identityproviders - List of identiy providers,
|
||||
* date - Date of upload
|
||||
* notice - Notice returned from the scanning engine
|
||||
* additionalinfo - Any additional information from the scanning engine
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"header": "Infected file detected",
|
||||
"report": "http://example.moodle/report/infectedfiles/index.php",
|
||||
"scanner": "antivirus_clamav",
|
||||
"filename": "virus.txt",
|
||||
"filesize": 100,
|
||||
"contenthash": "3395856ce81f2b7382dee72602f798b642f14140",
|
||||
"contenttype": "text/plain",
|
||||
"author": "Example User (exampleuser)",
|
||||
"ipaddress": "192.168.0.1",
|
||||
"geoinfo": "Brisbane, Australia",
|
||||
"referer": "http://example.moodle/user/files.php",
|
||||
"date": "28/05/20, 11:19",
|
||||
"notice": "Clamav scanning has tried 1 time(s). ClamAV has failed to run.",
|
||||
"additionalinfo": "Here is the output from ClamAV: /tmp/phpElIcr2: Not a regular file ERROR"
|
||||
}
|
||||
}}
|
||||
|
||||
<div>
|
||||
<b>{{header}}</b>
|
||||
<div>
|
||||
<p><b>{{#str}} emailreport, antivirus {{/str}}</b><a href={{report}}>{{report}}</a></p>
|
||||
<p><b>{{#str}} emailscanner, antivirus {{/str}}</b>{{scanner}}</p>
|
||||
<br>
|
||||
<p><b>{{#str}} emailfilename, antivirus {{/str}}</b>{{filename}}</p>
|
||||
<p><b>{{#str}} emailfilesize, antivirus {{/str}}</b>{{filesize}}</p>
|
||||
<p><b>{{#str}} emailcontenthash, antivirus {{/str}}</b>{{contenthash}}</p>
|
||||
<p><b>{{#str}} emailcontenttype, antivirus {{/str}}</b>{{contenttype}}</p>
|
||||
<p><b>{{#str}} emaildate, antivirus {{/str}}</b>{{date}}</p>
|
||||
<br>
|
||||
<p><b>{{#str}} emailauthor, antivirus {{/str}}</b>{{author}}</p>
|
||||
<p><b>{{#str}} emailipaddress, antivirus {{/str}}</b>{{ipaddress}}</p>
|
||||
<p><b>{{#str}} emailgeoinfo, antivirus {{/str}}</b>{{geoinfo}}</p>
|
||||
<p><b>{{#str}} emailreferer, antivirus {{/str}}</b><a href={{referer}}>{{referer}}</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<pre>{{notice}}</pre>
|
||||
<br>
|
||||
<p><b>{{#str}} emailadditionalinfo, antivirus {{/str}} </b></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,6 +23,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace report_infectedfiles\output;
|
||||
use report_infectedfiles\table\infectedfiles_table;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
*/
|
||||
namespace report_infectedfiles\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
@@ -34,14 +37,136 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @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 {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
request\plugin\provider,
|
||||
request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* This plugin does not contain any personal data
|
||||
* This plugin stores the userid of infected users.
|
||||
*
|
||||
* @return string
|
||||
* @param collection $collection the collection object to add data to.
|
||||
* @return collection The populated collection.
|
||||
*/
|
||||
public static function get_reason() : string {
|
||||
return 'privacy:metadata';
|
||||
public static function get_metadata(collection $collection) : collection {
|
||||
$collection->add_database_table(
|
||||
'infected_files',
|
||||
[
|
||||
'userid' => 'privacy:metadata:infected_files:userid',
|
||||
'filename' => 'privacy:metadata:infected_files:filename',
|
||||
'timecreated' => 'privacy:metadata:infected_files:timecreated',
|
||||
],
|
||||
'privacy:metadata:infected_files'
|
||||
);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function gets the contexts containing data for a userid.
|
||||
*
|
||||
* @param int $userid The userid to get contexts for.
|
||||
* @return request\contextlist the context list for the user.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : request\contextlist {
|
||||
$contextlist = new request\contextlist();
|
||||
|
||||
// The system context is the only context where information is stored.
|
||||
$contextlist->add_system_context();
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function exports user data on infected files from the contextlist provided.
|
||||
*
|
||||
* @param request\approved_contextlist $contextlist
|
||||
* @return void
|
||||
*/
|
||||
public static function export_user_data(request\approved_contextlist $contextlist) {
|
||||
global $DB;
|
||||
|
||||
foreach ($contextlist as $context) {
|
||||
// We only export from system context.
|
||||
if ($context->contextlevel === CONTEXT_SYSTEM) {
|
||||
|
||||
$userid = $contextlist->get_user()->id;
|
||||
$exportdata = [];
|
||||
|
||||
$records = $DB->get_records('infected_files', ['userid' => $userid]);
|
||||
foreach ($records as $record) {
|
||||
// Export only the data that does not expose internal information.
|
||||
$data = [];
|
||||
$data['userid'] = $record->userid;
|
||||
$data['timecreated'] = $record->timecreated;
|
||||
$data['filename'] = $record->filename;
|
||||
|
||||
$exportdata[] = $data;
|
||||
}
|
||||
|
||||
// Now export this data in the infected files table as subcontext.
|
||||
request\writer::with_context($context)->export_data(
|
||||
[get_string('privacy:metadata:infected_files_subcontext', 'report_infectedfiles')],
|
||||
(object) $exportdata
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As this report tracks potential attempted security violations,
|
||||
* This data should not be deleted at request. This would allow for an
|
||||
* avenue for a malicious user to cover their tracks. This function deliberately
|
||||
* does no deletes.
|
||||
*
|
||||
* @param \context $context the context to delete for.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* As this report tracks potential attempted security violations,
|
||||
* This data should not be deleted at request. This would allow for an
|
||||
* avenue for a malicious user to cover their tracks. This function deliberately
|
||||
* does no deletes.
|
||||
*
|
||||
* @param \core_privacy\local\request\approved_contextlist $contextlist the contextlist to delete for.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_data_for_user(request\approved_contextlist $contextlist) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets the list of users inside of the provided context. In this case, its only system context
|
||||
* which contains users.
|
||||
*
|
||||
* @param \core_privacy\local\request\userlist $userlist
|
||||
* @return void
|
||||
*/
|
||||
public static function get_users_in_context(request\userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context->contextlevel === CONTEXT_SYSTEM) {
|
||||
// If we are checking system context, we need to get all distinct userids from the table.
|
||||
$sql = 'SELECT DISTINCT userid
|
||||
FROM {infected_files}';
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As this report tracks potential attempted security violations,
|
||||
* This data should not be deleted at request. This would allow for an
|
||||
* avenue for a malicious user to cover their tracks. This function deliberately
|
||||
* does no deletes.
|
||||
*
|
||||
* @param request\approved_userlist $userlist
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_data_for_users(request\approved_userlist $userlist) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+68
-27
@@ -22,7 +22,7 @@
|
||||
* @copyright Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace report_infectedfiles\output;
|
||||
namespace report_infectedfiles\table;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -70,7 +70,6 @@ class infectedfiles_table extends \table_sql implements \renderable {
|
||||
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'),
|
||||
@@ -102,7 +101,7 @@ class infectedfiles_table extends \table_sql implements \renderable {
|
||||
* @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) {
|
||||
protected function get_sql_and_params($count = false) : array {
|
||||
if ($count) {
|
||||
$select = "COUNT(1)";
|
||||
} else {
|
||||
@@ -143,48 +142,80 @@ class infectedfiles_table extends \table_sql implements \renderable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column to display the authors fullname from userid.
|
||||
*
|
||||
* @param \stdClass $row the row from sql.
|
||||
* @return string the authors name.
|
||||
*/
|
||||
protected function col_author($row) : string {
|
||||
// Get user fullname from ID.
|
||||
$user = \core_user::get_user($row->userid);
|
||||
$url = new \moodle_url('/user/profile.php', ['id' => $row->userid]);
|
||||
return \html_writer::link($url, fullname($user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Column to display the failure reason.
|
||||
*
|
||||
* @param \stdClass $row the row from sql.
|
||||
* @return string the formatted reason.
|
||||
*/
|
||||
protected function col_reason($row) {
|
||||
return format_text($row->reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom actions column
|
||||
*
|
||||
* @param \stdClass $row an incidence record
|
||||
* @return string content of action column
|
||||
* @param \stdClass $row an incident record.
|
||||
* @return string content of action column.
|
||||
* @throws \coding_exception
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
protected function col_actions($row) {
|
||||
protected function col_actions($row) : string {
|
||||
global $OUTPUT;
|
||||
$filename = $row->quarantinedfile;
|
||||
$zipfile = \core\antivirus\quarantine::get_quarantine_folder() . $filename;
|
||||
if (!file_exists($zipfile)) {
|
||||
$fileid = $row->id;
|
||||
// If the file isn't found, we can do nothing in this column.
|
||||
// This shouldn't happen, unless the file is manually deleted from the server externally.
|
||||
if (!\core\antivirus\quarantine::quarantined_file_exists($filename)) {
|
||||
return '';
|
||||
}
|
||||
$links = '';
|
||||
$managefilepage = new \moodle_url('/report/infectedfiles/manage_infected_files.php');
|
||||
$managefilepage = new \moodle_url('/report/infectedfiles/index.php');
|
||||
|
||||
// Download.
|
||||
$downloadparams = ['filename' => $filename, 'action' => 'download', 'sesskey' => sesskey()];
|
||||
$downloadparams = ['file' => $fileid, '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;
|
||||
|
||||
$downloadconfirm = new \confirm_action(get_string('confirmdownload', 'report_infectedfiles'));
|
||||
$links .= $OUTPUT->action_icon(
|
||||
$downloadurl,
|
||||
new \pix_icon('t/download', get_string('download')),
|
||||
$downloadconfirm
|
||||
);
|
||||
|
||||
// Delete.
|
||||
$deleteparams = ['filename' => $filename, 'action' => 'confirmdelete', 'sesskey' => sesskey()];
|
||||
$deleteparams = ['file' => $fileid, 'action' => 'delete', '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;
|
||||
$deleteconfirm = new \confirm_action(get_string('confirmdelete', 'report_infectedfiles'));
|
||||
$links .= $OUTPUT->action_icon(
|
||||
$deleteurl,
|
||||
new \pix_icon('t/delete', get_string('delete')),
|
||||
$deleteconfirm
|
||||
);
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom time column
|
||||
* Custom time column.
|
||||
*
|
||||
* @param \stdClass $row an incidence record
|
||||
* @return string time created in user-friendly format
|
||||
* @param \stdClass $row an incident record.
|
||||
* @return string time created in user-friendly format.
|
||||
*/
|
||||
protected function col_timecreated($row) {
|
||||
protected function col_timecreated($row) : string {
|
||||
return userdate($row->timecreated);
|
||||
}
|
||||
|
||||
@@ -194,26 +225,36 @@ class infectedfiles_table extends \table_sql implements \renderable {
|
||||
* @param int $pagesize number or records perpage
|
||||
* @param bool $useinitialsbar use the bar or not
|
||||
* @param string $downloadhelpbutton help button
|
||||
* @return void
|
||||
* @throws \coding_exception
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public function display($pagesize, $useinitialsbar, $downloadhelpbutton='') {
|
||||
global $OUTPUT;
|
||||
// Output the table, and then display buttons.
|
||||
$this->out($pagesize, $useinitialsbar, $downloadhelpbutton);
|
||||
$managefilepage = new \moodle_url('/report/infectedfiles/index.php');
|
||||
|
||||
// If there are no rows, dont bother rendering extra buttons.
|
||||
if (empty($this->rawdata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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()];
|
||||
$deleteallparams = ['action' => 'deleteall', 'sesskey' => sesskey()];
|
||||
$deleteallurl = new \moodle_url($managefilepage, $deleteallparams);
|
||||
echo \html_writer::link($deleteallurl, $button);
|
||||
$deletebutton = new \single_button($deleteallurl, get_string('deleteall'), 'post', true);
|
||||
$deletebutton->add_confirm_action(get_string('confirmdeleteall', 'report_infectedfiles'));
|
||||
echo $OUTPUT->render($deletebutton);
|
||||
|
||||
echo " ";
|
||||
|
||||
// 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);
|
||||
$downloadbutton = new \single_button($downloadallurl, get_string('downloadall'), 'post', true);
|
||||
$downloadbutton->add_confirm_action(get_string('confirmdownloadall', 'report_infectedfiles'));
|
||||
echo $OUTPUT->render($downloadbutton);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,12 +25,50 @@
|
||||
|
||||
require(__DIR__.'/../../config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
use \core\antivirus\quarantine;
|
||||
|
||||
admin_externalpage_setup('reportinfectedfiles', '', null, '', array('pagelayout' => 'report'));
|
||||
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
// If action exists, we need to process the actions safely.
|
||||
if (!empty($action)) {
|
||||
// Nothing can be done without a valid sesskey.
|
||||
require_sesskey();
|
||||
// For any cancel actions, just reload the page clean.
|
||||
$cancel = $PAGE->url;
|
||||
|
||||
// Decide on page action.
|
||||
switch ($action) {
|
||||
case 'download':
|
||||
$fileid = required_param('file', PARAM_INT);
|
||||
quarantine::download_quarantined_file($fileid);
|
||||
break;
|
||||
|
||||
case 'downloadall':
|
||||
quarantine::download_all_quarantined_files();
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$fileid = required_param('file', PARAM_INT);
|
||||
quarantine::delete_quarantined_file($fileid);
|
||||
break;
|
||||
|
||||
case 'deleteall':
|
||||
// Remove file until current time.
|
||||
quarantine::clean_up_quarantine_folder(time());
|
||||
break;
|
||||
}
|
||||
|
||||
// Reload page cleanly once actions are processed.
|
||||
redirect($PAGE->url);
|
||||
}
|
||||
|
||||
// Once actions are dealt with, display the page.
|
||||
$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 = new \report_infectedfiles\table\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();
|
||||
|
||||
@@ -23,11 +23,17 @@
|
||||
* @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['confirmdelete'] = 'Do you really wish to delete this file?';
|
||||
$string['confirmdeleteall'] = 'Do you really wish to delete all files?';
|
||||
$string['confirmdownload'] = 'Do you really wish to download this file?';
|
||||
$string['confirmdownloadall'] = 'Do you really wish to download all files?';
|
||||
$string['filename'] = 'File name';
|
||||
$string['infectedfiles'] = 'Infected files';
|
||||
$string['privacy:metadata'] = 'This plugin does not contain any personal data';
|
||||
$string['infectedfiles'] = 'Antivirus failures';
|
||||
$string['privacy:metadata:infected_files'] = 'This table stores information on antivirus failures detected by the system.';
|
||||
$string['privacy:metadata:infected_files:filename'] = 'The name of the infected file uploaded by the user.';
|
||||
$string['privacy:metadata:infected_files:timecreated'] = 'The timestamp of when a user uploaded an infected file.';
|
||||
$string['privacy:metadata:infected_files:userid'] = 'The userid of the user who uploaded an infected file.';
|
||||
$string['privacy:metadata:infected_files_subcontext'] = 'Antivirus failures';
|
||||
$string['pluginname'] = 'Infected files';
|
||||
$string['quarantinedfile'] = 'Quarantined file';
|
||||
$string['reason'] = 'Failure reason';
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2020081400.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2020081400.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.0dev (Build: 20200814)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user