# -*- coding: utf-8 -*-
"""Parser for Trend Micro Antivirus logs.
Trend Micro uses two log files to track the scans (both manual/scheduled and
real-time) and the web reputation (network scan/filtering).
Currently only the first log is supported.
"""
from dfdatetime import definitions as dfdatetime_definitions
from dfdatetime import posix_time as dfdatetime_posix_time
from dfdatetime import time_elements as dfdatetime_time_elements
from plaso.containers import events
from plaso.lib import errors
from plaso.parsers import dsv_parser
from plaso.parsers import manager
[docs]
class TrendMicroAVEventData(events.EventData):
"""Trend Micro AV Log event data.
Attributes:
action (str): action.
filename (str): filename.
offset (int): offset of the line relative to the start of the file, from
which the event data was extracted.
path (str): path.
scan_type (str): scan_type.
threat (str): threat.
written_time (dfdatetime.DateTimeValues): date and time the log entry was
written.
"""
DATA_TYPE = 'av:trendmicro:scan'
[docs]
def __init__(self):
"""Initializes event data."""
super(TrendMicroAVEventData, self).__init__(data_type=self.DATA_TYPE)
self.action = None
self.filename = None
self.offset = None
self.path = None
self.scan_type = None
self.threat = None
self.written_time = None
[docs]
class TrendMicroUrlEventData(events.EventData):
"""Trend Micro Web Reputation Log event data.
Attributes:
application_name (str): application name.
block_mode (str): operation mode.
credibility_rating (int): credibility rating.
credibility_score (int): credibility score.
group_code (str): group code.
group_name (str): group name.
ip (str): IP address.
offset (int): offset of the line relative to the start of the file, from
which the event data was extracted.
policy_identifier (int): policy identifier.
threshold (int): threshold value.
url (str): accessed URL.
written_time (dfdatetime.DateTimeValues): entry written date and time.
"""
DATA_TYPE = 'av:trendmicro:webrep'
[docs]
def __init__(self):
"""Initializes event data."""
super(TrendMicroUrlEventData, self).__init__(data_type=self.DATA_TYPE)
self.application_name = None
self.block_mode = None
self.credibility_rating = None
self.credibility_score = None
self.group_code = None
self.group_name = None
self.ip = None
self.offset = None
self.policy_identifier = None
self.threshold = None
self.url = None
self.written_time = None
[docs]
class TrendMicroBaseParser(dsv_parser.DSVParser):
"""Common code for parsing Trend Micro log files.
The file format is reminiscent of CSV, but is not quite the same; the
delimiter is a three-character sequence and there is no provision for quoting
or escaping.
"""
# pylint: disable=abstract-method
DELIMITER = '<;>'
# Subclasses must define a list of column names.
COLUMNS = ()
# Subclasses must define a minimum number of columns value.
_MINIMUM_NUMBER_OF_COLUMNS = None
def _CreateDictReader(self, line_reader):
"""Iterates over the log lines and provide a reader for the values.
Args:
line_reader (iter): yields each line in the log file.
Yields:
dict[str, str]: column values keyed by column header.
Raises:
WrongParser: if a log line cannot be parsed.
"""
for line in line_reader:
stripped_line = line.strip()
values = stripped_line.split(self.DELIMITER)
number_of_values = len(values)
number_of_columns = len(self.COLUMNS)
if number_of_values < self._MINIMUM_NUMBER_OF_COLUMNS:
raise errors.WrongParser(
'Expected at least {0:d} values, found {1:d}'.format(
self._MINIMUM_NUMBER_OF_COLUMNS, number_of_values))
if number_of_values > number_of_columns:
raise errors.WrongParser(
'Expected at most {0:d} values, found {1:d}'.format(
number_of_columns, number_of_values))
yield dict(zip(self.COLUMNS, values))
def _ParseTimestamp(self, parser_mediator, row):
"""Provides a timestamp for the given row.
If the Trend Micro log comes from a version that provides a POSIX timestamp,
use that directly; it provides the advantages of UTC and of second
precision. Otherwise fall back onto the local-timezone date and time.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfvfs.
row (dict[str, str]): fields of a single row, as specified in COLUMNS.
Returns:
dfdatetime.interface.DateTimeValue: date and time value or None.
"""
timestamp = row.get('timestamp', None)
if timestamp is not None:
try:
timestamp = int(timestamp, 10)
except (ValueError, TypeError):
parser_mediator.ProduceExtractionWarning(
'Unable to parse timestamp value: {0!s}'.format(timestamp))
return dfdatetime_posix_time.PosixTime(timestamp=timestamp)
# The timestamp is not available; parse the local date and time instead.
try:
date_time = self._ConvertToTimestamp(row['date'], row['time'])
except ValueError as exception:
date_time = None
parser_mediator.ProduceExtractionWarning((
'Unable to parse time string: "{0:s} {1:s}" with error: '
'{2!s}').format(repr(row['date']), repr(row['time']), exception))
return date_time
def _ConvertToTimestamp(self, date, time):
"""Converts date and time strings into a timestamp.
Recent versions of Office Scan write a log field with a Unix timestamp.
Older versions may not write this field; their logs only provide a date and
a time expressed in the local time zone. This functions handles the latter
case.
Args:
date (str): date as an 8-character string in the YYYYMMDD format.
time (str): time as a 3 or 4-character string in the [H]HMM format or a
6-character string in the HHMMSS format.
Returns:
dfdatetime.TimeElements: date and time value.
Raises:
ValueError: if the date and time values cannot be parsed.
"""
if len(date) != 8:
raise ValueError('Unsupported date string: {0!s}'.format(date))
# The time consist of a hours and minutes value where the hours value has
# no leading zero.
if len(time) not in (3, 4):
raise ValueError('Unsupported time string: {0!s}'.format(time))
try:
year = int(date[:4], 10)
month = int(date[4:6], 10)
day = int(date[6:8], 10)
except (TypeError, ValueError):
raise ValueError('Unable to parse date string: {0!s}'.format(date))
try:
hour = int(time[:-2], 10)
minutes = int(time[-2:], 10)
except (TypeError, ValueError):
raise ValueError('Unable to parse time string: {0!s}'.format(date))
time_elements_tuple = (year, month, day, hour, minutes, 0)
date_time = dfdatetime_time_elements.TimeElements(
precision=dfdatetime_definitions.PRECISION_1_MINUTE,
time_elements_tuple=time_elements_tuple)
date_time.is_local_time = True
return date_time
[docs]
class OfficeScanVirusDetectionParser(TrendMicroBaseParser):
"""Parses the Trend Micro Office Scan Virus Detection Log."""
NAME = 'trendmicro_vd'
DATA_FORMAT = 'Trend Micro Office Scan Virus Detection log file'
COLUMNS = [
'date', 'time', 'threat', 'action', 'scan_type', 'unused1',
'path', 'filename', 'unused2', 'timestamp', 'unused3', 'unused4']
_MINIMUM_NUMBER_OF_COLUMNS = 8
_SUPPORTED_SCAN_RESULTS = frozenset([
0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 25])
[docs]
def ParseRow(self, parser_mediator, row_offset, row):
"""Parses a line of the log file and produces events.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfvfs.
row_offset (int): offset of the line from which the row was extracted.
row (dict[str, str]): fields of a single row, as specified in COLUMNS.
"""
date_time = self._ParseTimestamp(parser_mediator, row)
if date_time is None:
return
try:
action = int(row['action'], 10)
except (ValueError, TypeError):
action = None
try:
scan_type = int(row['scan_type'], 10)
except (ValueError, TypeError):
scan_type = None
event_data = TrendMicroAVEventData()
event_data.action = action
event_data.filename = row['filename']
event_data.offset = row_offset
event_data.path = row['path']
event_data.scan_type = scan_type
event_data.threat = row['threat']
event_data.written_time = date_time
parser_mediator.ProduceEventData(event_data)
[docs]
def VerifyRow(self, parser_mediator, row):
"""Verifies if a line of the file is in the expected format.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfvfs.
row (dict[str, str]): fields of a single row, as specified in COLUMNS.
Returns:
bool: True if this is the correct parser, False otherwise.
"""
if len(row) < self._MINIMUM_NUMBER_OF_COLUMNS:
return False
# Check the date format!
# If it doesn't parse, then this isn't a Trend Micro AV log.
try:
timestamp = self._ConvertToTimestamp(row['date'], row['time'])
except (ValueError, TypeError):
return False
if timestamp is None:
return False
# Check that the action value is plausible.
try:
action = int(row['action'], 10)
except (ValueError, TypeError):
return False
return action in self._SUPPORTED_SCAN_RESULTS
[docs]
class OfficeScanWebReputationParser(TrendMicroBaseParser):
"""Parses the Trend Micro Office Scan Web Reputation detection log."""
NAME = 'trendmicro_url'
DATA_FORMAT = 'Trend Micro Office Web Reputation log file'
COLUMNS = (
'date', 'time', 'block_mode', 'url', 'group_code', 'group_name',
'credibility_rating', 'policy_identifier', 'application_name',
'credibility_score', 'ip', 'threshold', 'timestamp', 'unused')
_MINIMUM_NUMBER_OF_COLUMNS = 12
_SUPPORTED_BLOCK_MODES = frozenset([0, 1])
[docs]
def ParseRow(self, parser_mediator, row_offset, row):
"""Parses a line of the log file and produces events.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfvfs.
row_offset (int): offset of the line from which the row was extracted.
row (dict[str, str]): fields of a single row, as specified in COLUMNS.
"""
date_time = self._ParseTimestamp(parser_mediator, row)
if date_time is None:
return
event_data = TrendMicroUrlEventData()
event_data.offset = row_offset
event_data.written_time = date_time
# Convert and store integer values.
for field in (
'credibility_rating', 'credibility_score', 'policy_identifier',
'threshold', 'block_mode'):
try:
value = int(row[field], 10)
except (ValueError, TypeError):
value = None
setattr(event_data, field, value)
# Store string values.
for field in ('url', 'group_name', 'group_code', 'application_name', 'ip'):
setattr(event_data, field, row[field])
parser_mediator.ProduceEventData(event_data)
[docs]
def VerifyRow(self, parser_mediator, row):
"""Verifies if a line of the file is in the expected format.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfvfs.
row (dict[str, str]): fields of a single row, as specified in COLUMNS.
Returns:
bool: True if this is the correct parser, False otherwise.
"""
if len(row) < self._MINIMUM_NUMBER_OF_COLUMNS:
return False
try:
timestamp = self._ConvertToTimestamp(row['date'], row['time'])
except ValueError:
return False
if timestamp is None:
return False
try:
block_mode = int(row['block_mode'], 10)
except (ValueError, TypeError):
return False
return block_mode in self._SUPPORTED_BLOCK_MODES
manager.ParsersManager.RegisterParsers([
OfficeScanVirusDetectionParser, OfficeScanWebReputationParser])