LEFT | RIGHT |
1 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- |
2 """Parser for Trend Micro Antivirus logs. | 2 """Parser for Trend Micro Antivirus logs. |
3 | 3 |
4 Trend Micro uses two log files to track the scans (both manual/scheduled and | 4 Trend Micro uses two log files to track the scans (both manual/scheduled and |
5 real-time) and the web reputation (network scan/filtering). | 5 real-time) and the web reputation (network scan/filtering). |
6 | 6 |
7 Currently only the first log is supported. | 7 Currently only the first log is supported. |
8 """ | 8 """ |
9 | 9 |
10 from __future__ import unicode_literals | 10 from __future__ import unicode_literals |
| 11 |
| 12 from dfdatetime import definitions as dfdatetime_definitions |
| 13 from dfdatetime import posix_time as dfdatetime_posix_time |
| 14 from dfdatetime import time_elements as dfdatetime_time_elements |
11 | 15 |
12 from plaso.containers import events | 16 from plaso.containers import events |
13 from plaso.containers import time_events | 17 from plaso.containers import time_events |
14 from plaso.lib import errors | 18 from plaso.lib import errors |
15 from plaso.lib import definitions | 19 from plaso.lib import definitions |
16 from plaso.lib import timelib | 20 from plaso.formatters import trendmicroav as formatter |
17 from plaso.parsers import dsv_parser | 21 from plaso.parsers import dsv_parser |
18 from plaso.parsers import manager | 22 from plaso.parsers import manager |
19 | |
20 _SCAN_RESULTS = { | |
21 0: "Success (clean)", | |
22 1: "Success (move)", | |
23 2: "Success (delete)", | |
24 3: "Success (rename)", | |
25 4: "Pass > Deny access", | |
26 5: "Failure (clean)", | |
27 6: "Falure (move)", | |
28 7: "Failure (delete)", | |
29 8: "Failure (rename)", | |
30 10: "Failure (clean), moved", | |
31 11: "Failure (clean), deleted", | |
32 12: "Failure (clean), renamed", | |
33 13: "Pass > Deny access", | |
34 14: "Failure (clean), move also failed", | |
35 15: "Failure (clean), delete also failed", | |
36 16: "Failure (clean), rename also failed", | |
37 25: "Passed a potential security risk" | |
38 } | |
39 | |
40 _SCAN_TYPES = { | |
41 0: "Manual scan", | |
42 1: "Real-time scan", | |
43 2: "Scheduled scan", | |
44 3: "Scan Now scan", | |
45 4: "DCS scan" | |
46 } | |
47 | |
48 _BLOCK_MODES = { | |
49 0: "Internal filter", | |
50 1: "Whitelist only" | |
51 } | |
52 | 23 |
53 | 24 |
54 class TrendMicroAVEventData(events.EventData): | 25 class TrendMicroAVEventData(events.EventData): |
55 """Trend Micro AV Log event data. | 26 """Trend Micro AV Log event data. |
56 | 27 |
57 Attributes: | 28 Attributes: |
58 action (str): action. | 29 action (str): action. |
59 threat (str): threat. | 30 threat (str): threat. |
60 filename (str): filename. | 31 filename (str): filename. |
61 scan_type (str): scan_type. | 32 scan_type (str): scan_type. |
62 trigger_location (str): trigger location. | |
63 username (str): username. | |
64 """ | 33 """ |
65 | 34 |
66 DATA_TYPE = 'av:trendmicro:scan' | 35 DATA_TYPE = 'av:trendmicro:scan' |
67 | 36 |
68 def __init__(self): | 37 def __init__(self): |
69 """Initializes event data.""" | 38 """Initializes event data.""" |
70 super(TrendMicroAVEventData, self).__init__(data_type=self.DATA_TYPE) | 39 super(TrendMicroAVEventData, self).__init__(data_type=self.DATA_TYPE) |
71 self.threat = None | 40 self.threat = None |
72 self.action = None | 41 self.action = None |
73 self.path = None | 42 self.path = None |
74 self.filename = None | 43 self.filename = None |
75 self.scan_type = None | 44 self.scan_type = None |
76 | 45 |
77 | 46 |
78 # pylint: disable=abstract-method | 47 # pylint: disable=abstract-method |
79 class TrendMicroBaseParser(dsv_parser.DSVParser): | 48 class TrendMicroBaseParser(dsv_parser.DSVParser): |
80 """Common code for parsing Trend Micro log files.""" | 49 """Common code for parsing Trend Micro log files. |
81 DELIMITER = '<;>' | |
82 | |
83 # Subclasses must define an integer MIN_COLUMNS value. | |
84 MIN_COLUMNS = None | |
85 | |
86 # Subclasses must define a list of field names. | |
87 COLUMNS = () | |
88 | |
89 def __init__(self, *args, **kwargs): | |
90 kwargs['encoding'] = 'cp1252' | |
91 super(TrendMicroBaseParser, self).__init__(*args, **kwargs) | |
92 | |
93 def _CreateDictReader(self, parser_mediator, line_reader): | |
94 """Iterate over the log lines and provide a reader for the values. | |
95 | 50 |
96 The file format is reminiscent of CSV, but is not quite the same; the | 51 The file format is reminiscent of CSV, but is not quite the same; the |
97 delimiter is a three-character sequence and there is no provision for | 52 delimiter is a three-character sequence and there is no provision for |
98 quoting or escaping. | 53 quoting or escaping. |
| 54 """ |
| 55 |
| 56 DELIMITER = '<;>' |
| 57 |
| 58 # Subclasses must define an integer MIN_COLUMNS value. |
| 59 MIN_COLUMNS = None |
| 60 |
| 61 # Subclasses must define a list of field names. |
| 62 COLUMNS = () |
| 63 |
| 64 def __init__(self, *args, **kwargs): |
| 65 """Initializes the parser. |
| 66 |
| 67 The TrendMicro AV writes a text logfile encoded in the CP1252 charset; |
| 68 unless otherwise specified, the parser class needs to know this. |
| 69 """ |
| 70 kwargs.setdefault('encoding', 'cp1252') |
| 71 super(TrendMicroBaseParser, self).__init__(*args, **kwargs) |
| 72 |
| 73 def _CreateDictReader(self, parser_mediator, line_reader): |
| 74 """Iterates over the log lines and provide a reader for the values. |
99 | 75 |
100 Args: | 76 Args: |
101 parser_mediator (ParserMediator): mediates interactions between parsers | 77 parser_mediator (ParserMediator): mediates interactions between parsers |
102 and other components, such as storage and dfvfs. | 78 and other components, such as storage and dfvfs. |
103 line_reader (iter): yields each line in the log file. | 79 line_reader (iter): yields each line in the log file. |
| 80 |
| 81 Yields: |
| 82 A dictionary of column values keyed by column header. |
104 """ | 83 """ |
105 for line in line_reader: | 84 for line in line_reader: |
106 try: | 85 try: |
107 values = line.decode(self._encoding).strip().split(self.DELIMITER) | 86 line = line.decode(self._encoding) |
108 except UnicodeDecodeError, exception: | 87 except UnicodeDecodeError as exception: |
109 raise errors.UnableToParseFile( | 88 raise errors.UnableToParseFile( |
110 "Unexpected binary content in file: {0:s}".format(exception)) | 89 "Unexpected binary content in file: {0:s}".format(exception)) |
| 90 stripped_line = line.strip() |
| 91 values = stripped_line.split(self.DELIMITER) |
111 if len(values) < self.MIN_COLUMNS: | 92 if len(values) < self.MIN_COLUMNS: |
112 raise errors.UnableToParseFile( | 93 raise errors.UnableToParseFile( |
113 "Expected at least {0:d} values, found {1:d}".format( | 94 "Expected at least {0:d} values, found {1:d}".format( |
114 self.MIN_COLUMNS, len(values))) | 95 self.MIN_COLUMNS, len(values))) |
115 if len(values) > len(self.COLUMNS): | 96 if len(values) > len(self.COLUMNS): |
116 raise errors.UnableToParseFile( | 97 raise errors.UnableToParseFile( |
117 "Expected at most {0:d} values, found {1:d}".format( | 98 "Expected at most {0:d} values, found {1:d}".format( |
118 len(self.COLUMNS), len(values))) | 99 len(self.COLUMNS), len(values))) |
119 yield dict(zip(self.COLUMNS, values)) | 100 yield dict(zip(self.COLUMNS, values)) |
120 | 101 |
121 def _ParseTimestamp(self, parser_mediator, row): | 102 def _ParseTimestamp(self, parser_mediator, row): |
122 """Provides a timestamp for the given row. | 103 """Provides a timestamp for the given row. |
123 | 104 |
124 If the Trend Micro log comes from a version that provides a Unix timestamp, | 105 If the Trend Micro log comes from a version that provides a Unix timestamp, |
125 use that directly; it provides the advantages of UTC and of second | 106 use that directly; it provides the advantages of UTC and of second |
126 precision. Otherwise fall back onto the local-timezone date and time. | 107 precision. Otherwise fall back onto the local-timezone date and time. |
127 | 108 |
128 Args: | 109 Args: |
129 parser_mediator (ParserMediator): mediates interactions between parsers | 110 parser_mediator (ParserMediator): mediates interactions between parsers |
130 and other components, such as storage and dfvfs. | 111 and other components, such as storage and dfvfs. |
131 row (dict[str, str]): fields of a single row, as specified in COLUMNS. | 112 row (dict[str, str]): fields of a single row, as specified in COLUMNS. |
132 | 113 |
133 Returns: | 114 Returns: |
134 int: a timestamp integer containing the number of micro seconds since | 115 dfdatetime.interface.DateTimeValue: the parsed timestamp. |
135 January 1, 1970, 00:00:00 UTC. | |
136 """ | 116 """ |
137 if 'timestamp' in row: | 117 if 'timestamp' in row: |
138 try: | 118 try: |
139 return timelib.Timestamp.FromPosixTime(int(row['timestamp'])) | 119 return dfdatetime_posix_time.PosixTime(timestamp=int(row['timestamp'])) |
140 except errors.TimestampError as exception: | 120 except ValueError as exception: |
141 parser_mediator.ProduceExtractionError( | 121 parser_mediator.ProduceExtractionError( |
142 'Log line has a timestamp field: [{0:s}], but it is invalid: {1:s}' | 122 'Log line has a timestamp field: [{0:s}], but it is invalid: {1:s}' |
143 .format(repr(row['timestamp']), exception)) | 123 .format(repr(row['timestamp']), exception)) |
144 else: | 124 |
145 # The Unix timestamp is not available; parse the local date and time. | 125 # The Unix timestamp is not available; parse the local date and time. |
146 try: | 126 try: |
147 return self._ConvertToTimestamp( | 127 return self._ConvertToTimestamp(row['date'], row['time']) |
148 row['date'], row['time'], parser_mediator.timezone) | 128 except ValueError as exception: |
149 except errors.TimestampError as exception: | 129 parser_mediator.ProduceExtractionError( |
150 parser_mediator.ProduceExtractionError( | 130 'Unable to parse time string: [{0:s} {1:s}] with error {2:s}' |
151 'Unable to parse time string: [{0:s} {1:s}] with error {2:s}' | 131 .format(repr(row['date']), repr(row['time']), exception)) |
152 .format(repr(row['date']), repr(row['time']), exception)) | 132 |
153 | 133 |
154 | 134 def _ConvertToTimestamp(self, date, time): |
155 def _ConvertToTimestamp(self, date, time, timezone): | |
156 """Converts date and time strings into a timestamp. | 135 """Converts date and time strings into a timestamp. |
157 | 136 |
158 Recent versions of Office Scan write a log field with a Unix timestamp. | 137 Recent versions of Office Scan write a log field with a Unix timestamp. |
159 Older versions may not write this field; their logs only provide a date and | 138 Older versions may not write this field; their logs only provide a date and |
160 a time expressed in the local time zone. This functions handles the latter | 139 a time expressed in the local time zone. This functions handles the latter |
161 case. | 140 case. |
162 | 141 |
163 The date value is an 8-character string in the YYYYMMDD format. | 142 Args: |
164 | 143 date (str): date as an 8-character string in the YYYYMMDD format. |
165 The time value may be a 4-character string in the HHMM format or a | 144 time (str): time as a 3 or 4-character string in the [H]HMM format or a |
166 6-character string in the HHMMSS format. | 145 6-character string in the HHMMSS format. |
167 | |
168 Args: | |
169 timestamp (str): Unix time in seconds since the epoch. | |
170 date (str): date. | |
171 time (str): time. | |
172 timezone (pytz.timezone): timezone of the date and time. | |
173 | 146 |
174 Returns: | 147 Returns: |
175 int: a timestamp integer containing the number of micro seconds since | 148 dfdatetime_time_elements.TimestampElements: the parsed timestamp. |
176 January 1, 1970, 00:00:00 UTC. | |
177 | 149 |
178 Raises: | 150 Raises: |
179 TimestampError: if the timestamp is badly formed or unable to transfer | 151 ValueError: if the date/time values cannot be parsed. |
180 the supplied date and time into a timestamp. | 152 """ |
181 """ | 153 # Check that the strings have the correct length. |
| 154 if len(date) != 8: |
| 155 raise ValueError('date has wrong length: len({0!s}) != 8'.format( |
| 156 repr(date))) |
| 157 if len(time) < 3 or len(time) > 4: |
| 158 raise ValueError('time has wrong length: len({0!s}) not in (3, 4)'.format( |
| 159 repr(time))) |
| 160 |
| 161 # Extract the date. |
182 year = int(date[:4]) | 162 year = int(date[:4]) |
183 month = int(date[4:6]) | 163 month = int(date[4:6]) |
184 day = int(date[6:8]) | 164 day = int(date[6:8]) |
185 hour = int(time[:2]) | 165 |
186 minutes = int(time[2:4]) | 166 # Extract the time. Note that a single-digit hour value has no leading zero. |
187 seconds = 0 | 167 hour = int(time[:-2]) |
188 if len(time) > 4: | 168 minutes = int(time[-2:]) |
189 seconds = time[4:6] | 169 |
190 return timelib.Timestamp.FromTimeParts( | 170 time_elements_tuple = (year, month, day, hour, minutes, 0) |
191 year, month, day, hour, minutes, seconds, timezone=timezone) | 171 date_time = dfdatetime_time_elements.TimeElements( |
192 | 172 time_elements_tuple=time_elements_tuple) |
193 def _VerifyLengthAndTimestamp(self, parser_mediator, row): | 173 date_time.is_local_time = True |
194 """Verifies the number of elements and the timestamp in the row. | 174 # TODO: add functionality to dfdatetime to control precision. |
195 | 175 date_time._precision = dfdatetime_definitions.PRECISION_1_MINUTE # pylint:
disable=protected-access |
196 Checks that the number of elements matches the format described by | 176 |
197 COLUMNS and MIN_COLUMNS, and that the 'date' and 'time' fields | 177 return date_time |
198 express a plausible timestamp. | |
199 | |
200 Args: | |
201 parser_mediator (ParserMediator): mediates interactions between parsers | |
202 and other components, such as storage and dfvfs. | |
203 row (dict[str, str]): fields of a single row, as specified in COLUMNS. | |
204 | |
205 Returns: | |
206 bool: True if this may be the correct parser, False otherwise. | |
207 """ | |
208 field_count = len(row) | |
209 if field_count < self.MIN_COLUMNS or field_count > len(self.COLUMNS): | |
210 return False | |
211 | |
212 # Check the date format! | |
213 # If it doesn't parse, then this isn't a Trend Micro AV log. | |
214 timestamp = self._ConvertToTimestamp( | |
215 row['date'], row['time'], parser_mediator.timezone) | |
216 | |
217 return timestamp is not None | |
218 | 178 |
219 | 179 |
220 class OfficeScanVirusDetectionParser(TrendMicroBaseParser): | 180 class OfficeScanVirusDetectionParser(TrendMicroBaseParser): |
221 """Parses the Trend Micro Office Scan Virus Detection Log.""" | 181 """Parses the Trend Micro Office Scan Virus Detection Log.""" |
222 | 182 |
223 NAME = 'trendmicro_vd' | 183 NAME = 'trendmicro_vd' |
224 DESCRIPTION = 'Parser for Trend Micro Office Scan Virus Detection log files.' | 184 DESCRIPTION = 'Parser for Trend Micro Office Scan Virus Detection log files.' |
225 | 185 |
226 COLUMNS = ( | 186 COLUMNS = [ |
227 'date', 'time', 'threat', 'action', 'scan_type', 'unused1', | 187 'date', 'time', 'threat', 'action', 'scan_type', 'unused1', |
228 'path', 'filename', 'unused2', 'timestamp', 'unused3', 'unused4') | 188 'path', 'filename', 'unused2', 'timestamp', 'unused3', 'unused4'] |
229 MIN_COLUMNS = 8 | 189 MIN_COLUMNS = 8 |
230 | 190 |
231 def ParseRow(self, parser_mediator, row_offset, row): | 191 def ParseRow(self, parser_mediator, row_offset, row): |
232 """Parses a line of the log file and produces events. | 192 """Parses a line of the log file and produces events. |
233 | 193 |
234 Args: | 194 Args: |
235 parser_mediator (ParserMediator): mediates interactions between parsers | 195 parser_mediator (ParserMediator): mediates interactions between parsers |
236 and other components, such as storage and dfvfs. | 196 and other components, such as storage and dfvfs. |
237 row_offset (int): line number of the row. | 197 row_offset (int): line number of the row. |
238 row (dict[str, str]): fields of a single row, as specified in COLUMNS. | 198 row (dict[str, str]): fields of a single row, as specified in COLUMNS. |
239 """ | 199 """ |
240 | 200 |
241 timestamp = self._ParseTimestamp(parser_mediator, row) | 201 timestamp = self._ParseTimestamp(parser_mediator, row) |
242 | 202 |
243 if timestamp is None: | 203 if timestamp is None: |
244 return | 204 return |
245 | 205 |
246 event_data = TrendMicroAVEventData() | 206 event_data = TrendMicroAVEventData() |
247 event_data.offset = row_offset | 207 event_data.offset = row_offset |
248 event_data.threat = row['threat'] | 208 event_data.threat = row['threat'] |
249 event_data.action = _SCAN_RESULTS[int(row['action'])] | 209 event_data.action = int(row['action']) |
250 event_data.path = row['path'] | 210 event_data.path = row['path'] |
251 event_data.filename = row['filename'] | 211 event_data.filename = row['filename'] |
252 event_data.scan_type = _SCAN_TYPES[int(row['scan_type'])] | 212 event_data.scan_type = int(row['scan_type']) |
253 | 213 |
254 event = time_events.TimestampEvent( | 214 event = time_events.DateTimeValuesEvent( |
255 timestamp, definitions.TIME_DESCRIPTION_WRITTEN) | 215 timestamp, definitions.TIME_DESCRIPTION_WRITTEN) |
256 parser_mediator.ProduceEventWithEventData(event, event_data) | 216 parser_mediator.ProduceEventWithEventData(event, event_data) |
257 | 217 |
258 def VerifyRow(self, parser_mediator, row): | 218 def VerifyRow(self, parser_mediator, row): |
259 """Verifies if a line of the file is in the expected format. | 219 """Verifies if a line of the file is in the expected format. |
260 | 220 |
261 Args: | 221 Args: |
262 parser_mediator (ParserMediator): mediates interactions between parsers | 222 parser_mediator (ParserMediator): mediates interactions between parsers |
263 and other components, such as storage and dfvfs. | 223 and other components, such as storage and dfvfs. |
264 row (dict[str, str]): fields of a single row, as specified in COLUMNS. | 224 row (dict[str, str]): fields of a single row, as specified in COLUMNS. |
265 | 225 |
266 Returns: | 226 Returns: |
267 bool: True if this is the correct parser, False otherwise. | 227 bool: True if this is the correct parser, False otherwise. |
268 """ | 228 """ |
269 if not self._VerifyLengthAndTimestamp(parser_mediator, row): | 229 if len(row) < self.MIN_COLUMNS: |
| 230 return False |
| 231 |
| 232 # Check the date format! |
| 233 # If it doesn't parse, then this isn't a Trend Micro AV log. |
| 234 timestamp = self._ConvertToTimestamp(row['date'], row['time']) |
| 235 |
| 236 if timestamp is None: |
270 return False | 237 return False |
271 | 238 |
272 # Check that the action value is plausible | 239 # Check that the action value is plausible |
273 try: | 240 try: |
274 action = int(row['action']) | 241 action = int(row['action']) |
275 except ValueError: | 242 except ValueError: |
276 return False | 243 return False |
277 if action not in _SCAN_RESULTS: | 244 if action not in formatter.SCAN_RESULTS: |
278 return False | 245 return False |
279 | 246 |
280 # All checks passed. | 247 # All checks passed. |
281 return True | 248 return True |
282 | 249 |
283 | 250 |
284 class TrendMicroUrlEventData(events.EventData): | 251 class TrendMicroUrlEventData(events.EventData): |
285 """Trend Micro Web Reputation Log event data. | 252 """Trend Micro Web Reputation Log event data. |
286 | 253 |
287 Attributes: | 254 Attributes: |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
336 """ | 303 """ |
337 | 304 |
338 timestamp = self._ParseTimestamp(parser_mediator, row) | 305 timestamp = self._ParseTimestamp(parser_mediator, row) |
339 | 306 |
340 if timestamp is None: | 307 if timestamp is None: |
341 return | 308 return |
342 | 309 |
343 event_data = TrendMicroUrlEventData() | 310 event_data = TrendMicroUrlEventData() |
344 event_data.offset = row_offset | 311 event_data.offset = row_offset |
345 | 312 |
346 # Parse and store block mode. | |
347 event_data.block_mode = _BLOCK_MODES[int(row['block_mode'])] | |
348 | |
349 # Convert and store integer values. | 313 # Convert and store integer values. |
350 for field in ('cred_rating', 'cred_score', 'policy_id', 'threshold'): | 314 for field in ('cred_rating', 'cred_score', 'policy_id', 'threshold', |
| 315 'block_mode'): |
351 setattr(event_data, field, int(row[field])) | 316 setattr(event_data, field, int(row[field])) |
352 | 317 |
353 # Store string values. | 318 # Store string values. |
354 for field in ('url', 'group_name', 'group_code', 'appname', 'ip'): | 319 for field in ('url', 'group_name', 'group_code', 'appname', 'ip'): |
355 setattr(event_data, field, row[field]) | 320 setattr(event_data, field, row[field]) |
356 | 321 |
357 event = time_events.TimestampEvent( | 322 event = time_events.DateTimeValuesEvent( |
358 timestamp, definitions.TIME_DESCRIPTION_WRITTEN) | 323 timestamp, definitions.TIME_DESCRIPTION_WRITTEN) |
359 parser_mediator.ProduceEventWithEventData(event, event_data) | 324 parser_mediator.ProduceEventWithEventData(event, event_data) |
360 | 325 |
361 def VerifyRow(self, parser_mediator, row): | 326 def VerifyRow(self, parser_mediator, row): |
362 """Verifies if a line of the file is in the expected format. | 327 """Verifies if a line of the file is in the expected format. |
363 | 328 |
364 Args: | 329 Args: |
365 parser_mediator (ParserMediator): mediates interactions between parsers | 330 parser_mediator (ParserMediator): mediates interactions between parsers |
366 and other components, such as storage and dfvfs. | 331 and other components, such as storage and dfvfs. |
367 row (dict[str, str]): fields of a single row, as specified in COLUMNS. | 332 row (dict[str, str]): fields of a single row, as specified in COLUMNS. |
368 | 333 |
369 Returns: | 334 Returns: |
370 bool: True if this is the correct parser, False otherwise. | 335 bool: True if this is the correct parser, False otherwise. |
371 """ | 336 """ |
372 if not self._VerifyLengthAndTimestamp(parser_mediator, row): | 337 if len(row) < self.MIN_COLUMNS: |
| 338 return False |
| 339 |
| 340 # Check the date format! |
| 341 # If it doesn't parse, then this isn't a Trend Micro AV log. |
| 342 timestamp = self._ConvertToTimestamp(row['date'], row['time']) |
| 343 |
| 344 if timestamp is None: |
373 return False | 345 return False |
374 | 346 |
375 # Check the block mode. | 347 # Check the block mode. |
376 try: | 348 try: |
377 if int(row['block_mode']) not in _BLOCK_MODES: | 349 if int(row['block_mode']) not in formatter.BLOCK_MODES: |
378 return False | 350 return False |
379 except ValueError: | 351 except ValueError: |
380 return False | 352 return False |
381 | 353 |
382 return True | 354 return True |
383 | 355 |
384 | 356 |
385 manager.ParsersManager.RegisterParser(OfficeScanVirusDetectionParser) | 357 manager.ParsersManager.RegisterParsers([ |
386 manager.ParsersManager.RegisterParser(OfficeScanWebReputationParser) | 358 OfficeScanVirusDetectionParser, |
| 359 OfficeScanWebReputationParser]) |
LEFT | RIGHT |