Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Manager class for antivirus integration.
19
 *
20
 * @package    core_antivirus
21
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core\antivirus;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Class used for various antivirus related stuff.
31
 *
32
 * @package    core_antivirus
33
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
34
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class manager {
37
 
38
    /**
39
     * Returns list of enabled antiviruses.
40
     *
41
     * @return array Array ('antivirusname'=>stdClass antivirus object).
42
     */
43
    private static function get_enabled() {
44
        global $CFG;
45
 
46
        $active = array();
47
        if (empty($CFG->antiviruses)) {
48
            return $active;
49
        }
50
 
51
        foreach (explode(',', $CFG->antiviruses) as $e) {
52
            if ($antivirus = self::get_antivirus($e)) {
53
                if ($antivirus->is_configured()) {
54
                    $active[$e] = $antivirus;
55
                }
56
            }
57
        }
58
        return $active;
59
    }
60
 
61
    /**
62
     * Scan file using all enabled antiviruses, throws exception in case of infected file.
63
     *
64
     * @param string $file Full path to the file.
65
     * @param string $filename Name of the file (could be different from physical file if temp file is used).
66
     * @param bool $deleteinfected whether infected file needs to be deleted.
67
     * @throws \core\antivirus\scanner_exception If file is infected.
68
     * @return void
69
     */
70
    public static function scan_file($file, $filename, $deleteinfected) {
71
        global $USER;
72
        $antiviruses = self::get_enabled();
73
        $notifylevel = (int)get_config('antivirus', 'notifylevel');
74
        foreach ($antiviruses as $antivirus) {
75
            // Attempt to scan, catching internal exceptions.
76
            try {
77
                $result = $antivirus->scan_file($file, $filename);
78
            } catch (\core\antivirus\scanner_exception $e) {
79
                $notice = $antivirus->get_scanning_notice();
80
                $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
81
 
82
                // Log scan error event.
83
                $params = [
84
                    'context' => \context_system::instance(),
85
                    'relateduserid' => $USER->id,
86
                    'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
87
                ];
88
                $event = \core\event\antivirus_scan_file_error::create($params);
89
                $event->trigger();
90
 
91
                // If there was a scanner exception (such as ClamAV denying
92
                // upload), send messages (on error and above), and rethrow.
93
                if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
94
                    $notice = $antivirus->get_scanning_notice();
95
                    self::send_antivirus_messages($antivirus, $incidentdetails);
96
                }
97
 
98
                throw $e;
99
            }
100
 
101
            $notice = $antivirus->get_scanning_notice();
102
            if ($result === $antivirus::SCAN_RESULT_FOUND) {
103
                // Infection found, send notification.
104
                $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice);
105
                self::send_antivirus_messages($antivirus, $incidentdetails);
106
 
107
                // Move to quarantine folder.
108
                $zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice);
109
                // If file not stored due to disabled quarantine, store a message.
110
                if (empty($zipfile)) {
111
                    $zipfile = get_string('quarantinedisabled', 'antivirus');
112
                }
113
 
114
                // Log file infected event.
115
                $params = [
116
                    'context' => \context_system::instance(),
117
                    'relateduserid' => $USER->id,
118
                    'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
119
                ];
120
                $event = \core\event\virus_infected_file_detected::create($params);
121
                $event->trigger();
122
 
123
                if ($deleteinfected) {
124
                    unlink($file);
125
                }
126
 
127
                // Get custom message to display to user from antivirus engine.
128
                $displaymessage = $antivirus->get_virus_found_message();
129
                $placeholders = array_merge(['item' => $filename], $displaymessage['placeholders']);
130
 
131
                throw new \core\antivirus\scanner_exception(
132
                    $displaymessage['string'],
133
                    '',
134
                    $placeholders,
135
                    null,
136
                    $displaymessage['component']
137
                );
138
            } else if ($result === $antivirus::SCAN_RESULT_ERROR) {
139
                // Here we need to generate a different incident based on an error.
140
                $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
141
 
142
                // Log scan error event.
143
                $params = [
144
                    'context' => \context_system::instance(),
145
                    'relateduserid' => $USER->id,
146
                    'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
147
                ];
148
                $event = \core\event\antivirus_scan_file_error::create($params);
149
                $event->trigger();
150
 
151
                // Send a notification if required (error or above).
152
                if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
153
                    self::send_antivirus_messages($antivirus, $incidentdetails);
154
                }
155
            }
156
        }
157
    }
158
 
159
    /**
160
     * Scan data steam using all enabled antiviruses, throws exception in case of infected data.
161
     *
162
     * @param string $data The variable containing the data to scan.
163
     * @throws \core\antivirus\scanner_exception If data is infected.
164
     * @return void
165
     */
166
    public static function scan_data($data) {
167
        global $USER;
168
        $antiviruses = self::get_enabled();
169
        $notifylevel = (int)get_config('antivirus', 'notifylevel');
170
        foreach ($antiviruses as $antivirus) {
171
            // Attempt to scan, catching internal exceptions.
172
            try {
173
                $result = $antivirus->scan_data($data);
174
            } catch (\core\antivirus\scanner_exception $e) {
175
                $notice = $antivirus->get_scanning_notice();
176
                $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
177
 
178
                // Log scan error event.
179
                $params = [
180
                    'context' => \context_system::instance(),
181
                    'relateduserid' => $USER->id,
182
                    'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
183
                ];
184
                $event = \core\event\antivirus_scan_file_error::create($params);
185
                $event->trigger();
186
 
187
                // If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
188
                if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
189
                    $notice = $antivirus->get_scanning_notice();
190
                    $filename = get_string('datastream', 'antivirus');
191
                    self::send_antivirus_messages($antivirus, $incidentdetails);
192
                }
193
 
194
                throw $e;
195
            }
196
 
197
            $filename = get_string('datastream', 'antivirus');
198
            $notice = $antivirus->get_scanning_notice();
199
 
200
            if ($result === $antivirus::SCAN_RESULT_FOUND) {
201
                // Infection found, send notification.
202
                $incidentdetails = $antivirus->get_incident_details('', $filename, $notice);
203
                self::send_antivirus_messages($antivirus, $incidentdetails);
204
 
205
                // Copy data to quarantine folder.
206
                $zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidentdetails, $notice);
207
                // If file not stored due to disabled quarantine, store a message.
208
                if (empty($zipfile)) {
209
                    $zipfile = get_string('quarantinedisabled', 'antivirus');
210
                }
211
 
212
                // Log file infected event.
213
                $params = [
214
                    'context' => \context_system::instance(),
215
                    'relateduserid' => $USER->id,
216
                    'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
217
                ];
218
                $event = \core\event\virus_infected_data_detected::create($params);
219
                $event->trigger();
220
 
221
                // Get custom message to display to user from antivirus engine.
222
                $displaymessage = $antivirus->get_virus_found_message();
223
                $placeholders = array_merge(['item' => get_string('datastream', 'antivirus')], $displaymessage['placeholders']);
224
 
225
                throw new \core\antivirus\scanner_exception(
226
                    $displaymessage['string'],
227
                    '',
228
                    $placeholders,
229
                    null,
230
                    $displaymessage['component']
231
                );
232
            } else if ($result === $antivirus::SCAN_RESULT_ERROR) {
233
                // Here we need to generate a different incident based on an error.
234
                $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
235
 
236
                // Log scan error event.
237
                $params = [
238
                    'context' => \context_system::instance(),
239
                    'relateduserid' => $USER->id,
240
                    'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
241
                ];
242
                $event = \core\event\antivirus_scan_data_error::create($params);
243
                $event->trigger();
244
 
245
                // Send a notification if required (error or above).
246
                if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
247
                    self::send_antivirus_messages($antivirus, $incidentdetails);
248
                }
249
            }
250
        }
251
    }
252
 
253
    /**
254
     * Returns instance of antivirus.
255
     *
256
     * @param string $antivirusname name of antivirus.
257
     * @return object|bool antivirus instance or false if does not exist.
258
     */
259
    public static function get_antivirus($antivirusname) {
260
        global $CFG;
261
 
262
        $classname = '\\antivirus_' . $antivirusname . '\\scanner';
263
        if (!class_exists($classname)) {
264
            return false;
265
        }
266
        return new $classname();
267
    }
268
 
269
    /**
270
     * Get the list of available antiviruses.
271
     *
272
     * @return array Array ('antivirusname'=>'localised antivirus name').
273
     */
274
    public static function get_available() {
275
        $antiviruses = array();
276
        foreach (\core_component::get_plugin_list('antivirus') as $antivirusname => $dir) {
277
            $antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname);
278
        }
279
        return $antiviruses;
280
    }
281
 
282
    /**
283
     * This function puts all relevant information into the messages required, and sends them.
284
     *
285
     * @param \core\antivirus\scanner $antivirus the scanner engine.
286
     * @param string $incidentdetails details of the incident.
287
     * @return void
288
     */
289
    public static function send_antivirus_messages(\core\antivirus\scanner $antivirus, string $incidentdetails) {
290
        $messages = $antivirus->get_messages();
291
 
292
        // If there is no messages, and a virus is found, we should generate one, then send it.
293
        if (empty($messages)) {
294
            $antivirus->message_admins($antivirus->get_scanning_notice(), FORMAT_MOODLE, 'infected');
295
            $messages = $antivirus->get_messages();
296
        }
297
 
298
        foreach ($messages as $message) {
299
 
300
            // Check if the information is already in the current scanning notice.
301
            if (!empty($antivirus->get_scanning_notice()) &&
302
                strpos($antivirus->get_scanning_notice(), $message->fullmessage) === false) {
303
                // This is some extra information. We should append this to the end of the incident details.
304
                $incidentdetails .= \html_writer::tag('pre', $message->fullmessage);
305
            }
306
 
307
            // Now update the message to the detailed version, and format.
308
            $message->name = 'infected';
309
            $message->fullmessagehtml = $incidentdetails;
310
            $message->fullmessageformat = FORMAT_MOODLE;
311
            $message->fullmessage = format_text_email($incidentdetails, $message->fullmessageformat);
312
 
313
            // Now we must check if message is going to a real account.
314
            // It may be an email that needs to be sent to non-user address.
315
            if ($message->userto->id === -1) {
316
                // If this doesnt exist, send a regular email.
317
                email_to_user(
318
                    $message->userto,
319
                    get_admin(),
320
                    $message->subject,
321
                    $message->fullmessage,
322
                    $message->fullmessagehtml
323
                );
324
            } else {
325
                // And now we can send.
326
                message_send($message);
327
            }
328
        }
329
    }
330
}