456 lines
14 KiB
Python
456 lines
14 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright 2012 The Plaso Project Authors.
|
|
# Please see the AUTHORS file for details on individual authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""An extension of the objectfilter to provide plaso specific options."""
|
|
|
|
import datetime
|
|
import logging
|
|
|
|
from plaso.formatters import manager as formatters_manager
|
|
|
|
# TODO: Changes this so it becomes an attribute instead of having backend
|
|
# load a front-end library.
|
|
from plaso.frontend import presets
|
|
|
|
from plaso.lib import limit
|
|
from plaso.lib import objectfilter
|
|
from plaso.lib import timelib
|
|
from plaso.lib import utils
|
|
|
|
|
|
class DictObject(object):
|
|
"""A simple object representing a dict object.
|
|
|
|
To filter against an object that is stored as a dictionary the dict
|
|
is converted into a simple object. Since keys can contain spaces
|
|
and/or other symbols they are stripped out to make filtering work
|
|
like it is another object.
|
|
|
|
Example dict:
|
|
{'A value': 234,
|
|
'this (my) key_': 'value',
|
|
'random': True,
|
|
}
|
|
|
|
This object would then allow access to object.thismykey that would access
|
|
the key 'this (my) key_' inside the dict.
|
|
"""
|
|
|
|
def __init__(self, dict_object):
|
|
"""Initialize the object and build a secondary dict."""
|
|
# TODO: Move some of this code to a more value typed system.
|
|
self._dict_object = dict_object
|
|
|
|
self._dict_translated = {}
|
|
for key, value in dict_object.items():
|
|
self._dict_translated[self._StripKey(key)] = value
|
|
|
|
def _StripKey(self, key):
|
|
"""Return a stripped version of the dict key without symbols."""
|
|
try:
|
|
return str(key).lower().translate(None, ' (){}+_=-<>[]')
|
|
except UnicodeEncodeError:
|
|
pass
|
|
|
|
def __getattr__(self, attr):
|
|
"""Return back entries from the dictionary."""
|
|
if attr in self._dict_object:
|
|
return self._dict_object.get(attr)
|
|
|
|
# Special case of getting all the key/value pairs.
|
|
if attr == '__all__':
|
|
ret = []
|
|
for key, value in self._dict_translated.items():
|
|
ret.append(u'{}:{}'.format(key, value))
|
|
return u' '.join(ret)
|
|
|
|
test = self._StripKey(attr)
|
|
if test in self._dict_translated:
|
|
return self._dict_translated.get(test)
|
|
|
|
|
|
class PlasoValueExpander(objectfilter.AttributeValueExpander):
|
|
"""An expander that gives values based on object attribute names."""
|
|
|
|
def __init__(self):
|
|
"""Initialize an attribue value expander."""
|
|
super(PlasoValueExpander, self).__init__()
|
|
self._formatters_manager = formatters_manager.EventFormatterManager
|
|
|
|
def _GetMessage(self, obj):
|
|
"""Return a properly formatted message string."""
|
|
ret = u''
|
|
|
|
try:
|
|
ret, _ = self._formatters_manager.GetMessageStrings(obj)
|
|
except KeyError as exception:
|
|
logging.warning(u'Unable to correctly assemble event: {0:s}'.format(
|
|
exception))
|
|
|
|
return ret
|
|
|
|
def _GetSources(self, obj):
|
|
"""Return a properly formatted source strings."""
|
|
try:
|
|
source_short, source_long = self._formatters_manager.GetSourceStrings(obj)
|
|
except KeyError as exception:
|
|
logging.warning(u'Unable to correctly assemble event: {0:s}'.format(
|
|
exception))
|
|
|
|
return source_short, source_long
|
|
|
|
def _GetValue(self, obj, attr_name):
|
|
ret = getattr(obj, attr_name, None)
|
|
|
|
if ret:
|
|
if isinstance(ret, dict):
|
|
ret = DictObject(ret)
|
|
|
|
if attr_name == 'tag':
|
|
return ret.tags
|
|
|
|
return ret
|
|
|
|
# Check if this is a message request and we have a regular EventObject.
|
|
if attr_name == 'message':
|
|
return self._GetMessage(obj)
|
|
|
|
# Check if this is a source_short request.
|
|
if attr_name in ('source', 'source_short'):
|
|
source_short, _ = self._GetSources(obj)
|
|
return source_short
|
|
|
|
# Check if this is a source_long request.
|
|
if attr_name in ('source_long', 'sourcetype'):
|
|
_, source_long = self._GetSources(obj)
|
|
return source_long
|
|
|
|
def _GetAttributeName(self, path):
|
|
return path[0].lower()
|
|
|
|
|
|
class PlasoExpression(objectfilter.BasicExpression):
|
|
"""A Plaso specific expression."""
|
|
# A simple dictionary used to swap attributes so other names can be used
|
|
# to reference some core attributes (implementation specific).
|
|
swap_source = {
|
|
'date': 'timestamp',
|
|
'datetime': 'timestamp',
|
|
'time': 'timestamp',
|
|
'description_long': 'message',
|
|
'description': 'message',
|
|
'description_short': 'message_short',
|
|
}
|
|
|
|
def Compile(self, filter_implementation):
|
|
self.attribute = self.swap_source.get(self.attribute, self.attribute)
|
|
arguments = [self.attribute]
|
|
op_str = self.operator.lower()
|
|
operator = filter_implementation.OPS.get(op_str, None)
|
|
|
|
if not operator:
|
|
raise objectfilter.ParseError(u'Unknown operator {0:s} provided.'.format(
|
|
self.operator))
|
|
|
|
# Plaso specific implementation - if we are comparing a timestamp
|
|
# to a value, we use our specific implementation that compares
|
|
# timestamps in a "human readable" format.
|
|
if self.attribute == 'timestamp':
|
|
args = []
|
|
for arg in self.args:
|
|
args.append(DateCompareObject(arg))
|
|
self.args = args
|
|
|
|
for arg in self.args:
|
|
if isinstance(arg, DateCompareObject):
|
|
if 'Less' in str(operator):
|
|
TimeRangeCache.SetUpperTimestamp(arg.data)
|
|
else:
|
|
TimeRangeCache.SetLowerTimestamp(arg.data)
|
|
arguments.extend(self.args)
|
|
expander = filter_implementation.FILTERS['ValueExpander']
|
|
ops = operator(arguments=arguments, value_expander=expander)
|
|
if not self.bool_value:
|
|
if hasattr(ops, 'FlipBool'):
|
|
ops.FlipBool()
|
|
|
|
return ops
|
|
|
|
|
|
class ParserList(objectfilter.GenericBinaryOperator):
|
|
"""Matches when a parser is inside a predefined list of parsers."""
|
|
|
|
def __init__(self, *children, **kwargs):
|
|
"""Construct the parser list and retrieve a list of available parsers."""
|
|
super(ParserList, self).__init__(*children, **kwargs)
|
|
self.compiled_list = presets.categories.get(
|
|
self.right_operand.lower(), [])
|
|
|
|
def Operation(self, x, unused_y):
|
|
"""Return a bool depending on the parser list contains the parser."""
|
|
if self.left_operand != 'parser':
|
|
raise objectfilter.MalformedQueryError(
|
|
u'Unable to use keyword "inlist" for other than parser.')
|
|
|
|
if x in self.compiled_list:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class PlasoAttributeFilterImplementation(objectfilter.BaseFilterImplementation):
|
|
"""Does field name access on the lowercase version of names.
|
|
|
|
Useful to only access attributes and properties with Google's python naming
|
|
style.
|
|
"""
|
|
|
|
FILTERS = {}
|
|
FILTERS.update(objectfilter.BaseFilterImplementation.FILTERS)
|
|
FILTERS.update({'ValueExpander': PlasoValueExpander})
|
|
OPS = objectfilter.OP2FN
|
|
OPS.update({'inlist': ParserList,})
|
|
|
|
|
|
class DateCompareObject(object):
|
|
"""A specific class created for date comparison.
|
|
|
|
This object takes a date representation, whether that is a direct integer
|
|
datetime object or a string presenting the date, and uses that for comparing
|
|
against timestamps stored in microseconds in in microseconds since
|
|
Jan 1, 1970 00:00:00 UTC.
|
|
|
|
This makes it possible to use regular comparison operators for date,
|
|
irrelevant of the format the date comes in, since plaso stores all timestamps
|
|
in the same format, which is an integer/long, it is a simple manner of
|
|
changing the input into the same format (int) and compare that.
|
|
"""
|
|
|
|
def __init__(self, data):
|
|
"""Take a date object and use that for comparison.
|
|
|
|
Args:
|
|
data: A string, datetime object or an integer that
|
|
represents the time to compare against. Time should be stored
|
|
as microseconds since UTC in Epoch format.
|
|
|
|
Raises:
|
|
ValueError: if the date string is invalid.
|
|
"""
|
|
self.text = utils.GetUnicodeString(data)
|
|
if type(data) in (int, long):
|
|
self.data = data
|
|
elif type(data) == float:
|
|
self.data = long(data)
|
|
elif type(data) in (str, unicode):
|
|
try:
|
|
self.data = timelib.Timestamp.FromTimeString(
|
|
utils.GetUnicodeString(data))
|
|
except ValueError as exception:
|
|
raise ValueError(u'Wrongly formatted date string: {0:s} - {1:s}'.format(
|
|
data, exception))
|
|
elif type(data) == datetime.datetime:
|
|
self.data = timelib.Timestamp.FromPythonDatetime(data)
|
|
elif isinstance(DateCompareObject, data):
|
|
self.data = data.data
|
|
else:
|
|
raise ValueError(u'Unsupported type: {0:s}.'.format(type(data)))
|
|
|
|
def __cmp__(self, x):
|
|
"""A simple comparison operation."""
|
|
try:
|
|
x_date = DateCompareObject(x)
|
|
return cmp(self.data, x_date.data)
|
|
except ValueError:
|
|
return False
|
|
|
|
def __le__(self, x):
|
|
"""Less or equal comparison."""
|
|
return self.data <= x
|
|
|
|
def __ge__(self, x):
|
|
"""Greater or equal comparison."""
|
|
return self.data >= x
|
|
|
|
def __eq__(self, x):
|
|
"""Check if equal."""
|
|
return x == self.data
|
|
|
|
def __ne__(self, x):
|
|
"""Check if not equal."""
|
|
return x != self.data
|
|
|
|
def __str__(self):
|
|
"""Return a string representation of the object."""
|
|
return self.text
|
|
|
|
|
|
class BaseParser(objectfilter.Parser):
|
|
"""Plaso version of the Parser."""
|
|
|
|
expression_cls = PlasoExpression
|
|
|
|
|
|
class TrueObject(object):
|
|
"""A simple object that always returns true for all comparison.
|
|
|
|
This object is used for testing certain conditions inside filter queries.
|
|
By returning true for all comparisons this object can be used to evaluate
|
|
specific portions of a filter query.
|
|
"""
|
|
|
|
def __init__(self, txt=''):
|
|
"""Save the text object so it can be used when comparing text."""
|
|
self.txt = txt
|
|
|
|
def __getattr__(self, unused_attr):
|
|
"""Return a TrueObject for every attribute request."""
|
|
return self
|
|
|
|
def __eq__(self, unused_x):
|
|
"""Return true for tests of equality."""
|
|
return True
|
|
|
|
def __gt__(self, unused_x):
|
|
"""Return true for checks for greater."""
|
|
return True
|
|
|
|
def __ge__(self, unused_x):
|
|
"""Return true for checks for greater or equal."""
|
|
return True
|
|
|
|
def __lt__(self, unused_x):
|
|
"""Return true for checks of less."""
|
|
return True
|
|
|
|
def __le__(self, unused_x):
|
|
"""Return true for checks of less or equal."""
|
|
return True
|
|
|
|
def __ne__(self, unused_x):
|
|
"""Return true for all not equal comparisons."""
|
|
return True
|
|
|
|
def __iter__(self):
|
|
"""Return a generator so a test for the in keyword can be used."""
|
|
yield self
|
|
|
|
def __str__(self):
|
|
"""Return a string to make regular expression searches possible.
|
|
|
|
Returns:
|
|
A string that containes the original query with some of the matches
|
|
expanded, perhaps several times.
|
|
"""
|
|
# Regular expressions in pfilter may include the following escapes:
|
|
# "\\'\"rnbt\.ws":
|
|
txt = self.txt
|
|
if r'\.' in self.txt:
|
|
txt += self.txt.replace(r'\.', ' _ text _ ')
|
|
|
|
if r'\b' in self.txt:
|
|
txt += self.txt.replace(r'\b', ' ')
|
|
|
|
if r'\s' in self.txt:
|
|
txt += self.txt.replace(r'\s', ' ')
|
|
|
|
return txt
|
|
|
|
|
|
class MockTestFilter(object):
|
|
"""A mock test filter object used to test certain portion of test queries.
|
|
|
|
The logic behind this object is that a single attribute can be isolated
|
|
for comparison. That is to say all calls to attributes will lead to a TRUE
|
|
response, except those attributes that are specifically stated in the
|
|
constructor. This way it is simple to test for instance whether or not
|
|
to include a parser at all, before actually running the tool. The same applies
|
|
to filtering out certain filenames, etc.
|
|
"""
|
|
|
|
def __init__(self, query, **kwargs):
|
|
"""Constructor, only valid attribute is the parser one."""
|
|
self.attributes = kwargs
|
|
self.txt = query
|
|
|
|
def __getattr__(self, attr):
|
|
"""Return TrueObject for all requests except for stored attributes."""
|
|
if attr in self.attributes:
|
|
return self.attributes.get(attr, None)
|
|
|
|
# TODO: Either delete this entire object (MockTestFilter) or implement
|
|
# a false object and return the correct one depending on whether we
|
|
# are looking for a true or negative response (eg "not" keyword included).
|
|
return TrueObject(self.txt)
|
|
|
|
|
|
class TimeRangeCache(object):
|
|
"""A class that stores timeranges from filters."""
|
|
|
|
@classmethod
|
|
def ResetTimeConstraints(cls):
|
|
"""Resets the time constraints."""
|
|
if hasattr(cls, '_lower'):
|
|
del cls._lower
|
|
if hasattr(cls, '_upper'):
|
|
del cls._upper
|
|
|
|
@classmethod
|
|
def SetLowerTimestamp(cls, timestamp):
|
|
"""Sets the lower bound timestamp."""
|
|
if not hasattr(cls, '_lower'):
|
|
cls._lower = timestamp
|
|
return
|
|
|
|
if timestamp < cls._lower:
|
|
cls._lower = timestamp
|
|
|
|
@classmethod
|
|
def SetUpperTimestamp(cls, timestamp):
|
|
"""Sets the upper bound timestamp."""
|
|
if not hasattr(cls, '_upper'):
|
|
cls._upper = timestamp
|
|
return
|
|
|
|
if timestamp > cls._upper:
|
|
cls._upper = timestamp
|
|
|
|
@classmethod
|
|
def GetTimeRange(cls):
|
|
"""Return the first and last timestamp of filter range."""
|
|
first = getattr(cls, '_lower', 0)
|
|
last = getattr(cls, '_upper', limit.MAX_INT64)
|
|
|
|
if first < last:
|
|
return first, last
|
|
else:
|
|
return last, first
|
|
|
|
|
|
def GetMatcher(query, quiet=False):
|
|
"""Return a filter match object for a given query."""
|
|
matcher = None
|
|
try:
|
|
parser = BaseParser(query).Parse()
|
|
matcher = parser.Compile(PlasoAttributeFilterImplementation)
|
|
except objectfilter.ParseError as exception:
|
|
if not quiet:
|
|
logging.error(u'Filter <{0:s}> malformed: {1:s}'.format(
|
|
query, exception))
|
|
|
|
return matcher
|