Skip to content

Commit

Permalink
MDL-66222 antivirus: Improved error email capture
Browse files Browse the repository at this point in the history
  • Loading branch information
Peterburnett committed Aug 21, 2020
1 parent adbe92c commit 8e0e99e
Show file tree
Hide file tree
Showing 22 changed files with 663 additions and 238 deletions.
29 changes: 21 additions & 8 deletions admin/settings/plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,35 @@
$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);
Expand Down
31 changes: 17 additions & 14 deletions lang/en/antivirus.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,35 @@
$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.';
Expand Down
1 change: 1 addition & 0 deletions lang/en/moodle.php
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,7 @@
$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.';
Expand Down
126 changes: 107 additions & 19 deletions lib/classes/antivirus/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,47 @@ public static function scan_file($file, $filename, $deleteinfected) {
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);
}
}
}
Expand All @@ -108,27 +126,48 @@ public static function scan_data($data) {
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);
}
}
}
Expand Down Expand Up @@ -161,4 +200,53 @@ public static function get_available() {
}
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);
}
}
}
}
Loading

0 comments on commit 8e0e99e

Please sign in to comment.