Skip to content

Commit

Permalink
Added an import helper for reading configs. (#1229)
Browse files Browse the repository at this point in the history
* Adding an import helper for reading configs.

* Adding minor changes to analyzer result behavior.

* Minor change.

* Minor list change

* Update importer_client/python/timesketch_import_client/helper.py

Co-authored-by: Johan Berggren <jberggren@gmail.com>
  • Loading branch information
kiddinn and berggren committed May 27, 2020
1 parent 829fb28 commit 5964c6e
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 117 deletions.
2 changes: 1 addition & 1 deletion api_client/python/setup.py
Expand Up @@ -21,7 +21,7 @@

setup(
name='timesketch-api-client',
version='20200525',
version='20200527',
description='Timesketch API client',
license='Apache License, Version 2.0',
url='http://www.timesketch.org/',
Expand Down
53 changes: 40 additions & 13 deletions api_client/python/timesketch_api_client/analyzer.py
Expand Up @@ -46,20 +46,22 @@ def _fetch_data(self):
if not objects:
return {}

result_dict = {}
for result in objects[0]:
result_id = result.get('analysissession_id')
if result_id != self._session_id:
continue
status_list = result.get('status', [])
if len(status_list) != 1:
return {}
continue
status = status_list[0]

timeline = result.get('timeline', {})

return {
'id': result_id,
'analyzer': result.get('analyzer_name', 'N/A'),
result_dict['id'] = result_id
result_dict.setdefault('analyzers', [])
result_dict['analyzers'].append({
'name': result.get('analyzer_name', 'N/A'),
'results': result.get('result'),
'description': result.get('description', 'N/A'),
'user': result.get('user', {}).get('username', 'System'),
Expand All @@ -74,9 +76,9 @@ def _fetch_data(self):
'username', 'System'),
'timeline_name': timeline.get('name', 'N/A'),
'timeline_deleted': timeline.get('deleted', False),
}
})

return {}
return result_dict

@property
def id(self):
Expand All @@ -87,25 +89,50 @@ def id(self):
def log(self):
"""Returns back logs from the analyzer session, if there are any."""
data = self._fetch_data()
return data.get('log', 'No recorded logs.')
return_strings = []
for entry in data.get('analyzers', []):
return_strings.append(
'[{0:s}] = {1:s}'.format(
entry.get('name', 'No Name'),
entry.get('log', 'No recorded logs.')))
return '\n'.join(return_strings)

@property
def results(self):
"""Returns the results from the analyzer session."""
data = self._fetch_data()
return data.get('results', 'No results yet.')
return_strings = []
for entry in data.get('analyzers', []):
results = entry.get('results')
if not results:
results = 'No results yet.'
return_strings.append(
'[{0:s}] = {1:s}'.format(
entry.get('name', 'No Name'), results))
return '\n'.join(return_strings)

@property
def status(self):
"""Returns the current status of the analyzer run."""
data = self._fetch_data()
return data.get('status', 'Unknown')
return_strings = []
for entry in data.get('analyzers', []):
return_strings.append(
'[{0:s}] = {1:s}'.format(
entry.get('name', 'No Name'),
entry.get('status', 'Unknown.')))
return '\n'.join(return_strings)

@property
def status_string(self):
"""Returns a longer version of a status string."""
data = self._fetch_data()
return '[{0:s}] Status: {1:s}'.format(
data.get('status_date', datetime.datetime.utcnow().isoformat()),
data.get('status', 'Unknown')
)
return_strings = []
for entry in data.get('analyzers', []):
return_strings.append(
'{0:s} - {1:s}: {2:s}'.format(
entry.get('name', 'No Name'),
entry.get(
'status_date', datetime.datetime.utcnow().isoformat()),
entry.get('status', 'Unknown.')))
return '\n'.join(return_strings)
26 changes: 21 additions & 5 deletions api_client/python/timesketch_api_client/sketch.py
Expand Up @@ -290,16 +290,22 @@ def list_aggregations(self, include_labels=None, exclude_labels=None):
aggregations.append(aggregation_obj)
return aggregations

def get_analyzer_status(self):
def get_analyzer_status(self, as_sessions=False):
"""Returns a list of started analyzers and their status.
Args:
as_sessions (bool): optional, if set to True then a list of
AnalyzerResult objects will be returned. Defaults to
returning a list of dicts.
Returns:
A list of dict objects that contains status information
of each analyzer run. The dict contains information about
what timeline it ran against, the results and current
status of the analyzer run.
If "as_sessions" is set then a list of AnalyzerResult gets
returned, otherwise a list of dict objects that contains
status information of each analyzer run. The dict contains
information about what timeline it ran against, the
results and current status of the analyzer run.
"""
stats_list = []
sessions = []
for timeline_obj in self.list_timelines():
resource_uri = (
'{0:s}/sketches/{1:d}/timelines/{2:d}/analysis').format(
Expand All @@ -310,17 +316,27 @@ def get_analyzer_status(self):
if not objects:
continue
for result in objects[0]:
session_id = result.get('analysissession_id')
stat = {
'index': timeline_obj.index,
'timeline_id': timeline_obj.id,
'session_id': session_id,
'analyzer': result.get('analyzer_name', 'N/A'),
'results': result.get('result', 'N/A'),
'status': 'N/A',
}
if as_sessions and session_id:
sessions.append(analyzer.AnalyzerResult(
timeline_id=timeline_obj.id, session_id=session_id,
sketch_id=self.id, api=self.api))
status = result.get('status', [])
if len(status) == 1:
stat['status'] = status[0].get('status', 'N/A')
stats_list.append(stat)

if as_sessions:
return sessions

return stats_list

def get_aggregation(self, aggregation_id):
Expand Down
2 changes: 1 addition & 1 deletion importer_client/python/setup.py
Expand Up @@ -24,7 +24,7 @@

setup(
name='timesketch-import-client',
version='20200514',
version='20200527',
description='Timesketch Import Client',
license='Apache License, Version 2.0',
url='http://www.timesketch.org/',
Expand Down
15 changes: 8 additions & 7 deletions importer_client/python/timesketch_import_client/data/__init__.py
Expand Up @@ -35,26 +35,27 @@ def load_config(file_path=''):
used that comes with the tool.
Returns:
A list with dicts containing the loaded YAML config.
dict: a dict with the key being a config file identifier and the value
being another dict with the configuration items.
"""
if not file_path:
base_path = os.path.dirname(__file__)
file_path = os.path.join(base_path, DEFAULT_FILE)

if not file_path.endswith('.yaml'):
logger.error('Can\'t load a config that is not a YAML file.')
return []
return {}

if not os.path.isfile(file_path):
logger.error('File path does not exist, unable to load YAML config.')
return []
return {}

with codecs.open(file_path, 'r') as fh:
try:
data = yaml.safe_load(fh)
return data
except (AttributeError, yaml.parser.ParserError) as e:
logger.error('Unable to parse YAML file, with error: %s', e)
return []
if not data:
return []
return data
return {}

return {}
155 changes: 155 additions & 0 deletions importer_client/python/timesketch_import_client/helper.py
@@ -0,0 +1,155 @@
# Copyright 2020 Google Inc. All rights reserved.
#
# 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.
"""Timesketch data importer."""
from __future__ import unicode_literals

import logging
import os

from timesketch_import_client import data as data_config


logger = logging.getLogger('ts_import_helper')


class ImportHelper:
"""Import helper class."""

def __init__(self, load_default=True):
"""Initialize the helper class."""
if load_default:
self._data = data_config.load_config()
else:
self._data = {}

def _configure_streamer(self, streamer, config_dict):
"""Sets up the streamer based on a configuration dict.
Args:
streamer (ImportStreamer): an import streamer object.
config_dict (dict): A dictionary that contains
configuration details for the streamer.
"""
message = config_dict.get('message')
if message:
streamer.set_message_format_string(message)

timestamp_desc = config_dict.get('timestamp_desc')
if timestamp_desc:
streamer.set_timestamp_description(timestamp_desc)

separator = config_dict.get('separator')
if separator:
streamer.set_csv_delimiter(separator)

encoding = config_dict.get('encoding')
if encoding:
streamer.set_text_encoding(encoding)

datetime_string = config_dict.get('datetime')
if datetime_string:
streamer.set_datetime_column(datetime_string)

def add_config(self, file_path):
"""Loads a YAML config file describing the log file config.
This function reads a YAML config file, and adds the config
to it's config data. This configuration can then be used to
setup an import streamer object.
Args:
file_path (str): the path to the config file.
Raises:
ValueError: if the file path does not exist or if the config
is not valid or cannot be read.
"""
if not os.path.isfile(file_path):
raise ValueError(
'Unable to open file: [{0:s}], it does not exist.'.format(
file_path))

if not os.access(file_path, os.R_OK):
raise ValueError(
'Unable to open file: [{0:s}], cannot open it for '
'read, please check permissions.'.format(file_path))


config = data_config.load_config(file_path)
if not isinstance(config, dict):
raise ValueError(
'Unable to read config file since it does not produce a dict')

if not all([isinstance(x, dict) for x in config.values()]):
raise ValueError(
'The config needs to a dict that contains other dict '
'attributes.')

self._data.update(config)

def add_config_dict(self, config, config_name='manual'):
"""Add a config dict describing the log file config.
Args:
config (dict): a single dict with the config needed for setting
up the streamer.
config_name (str): the name of the config to be added. This is
optional, with the default value set to "manual".
"""
self._data[config_name] = config

def configure_streamer(self, streamer, data_type='', columns=None):
"""Go through loaded config and setup a streamer if there is a match.
This function takes a streamer object and compares the loaded config
to see if there is a match to the data_type and/or columns that are
supplied to the function and sets the streamer up if a matching
config is discovered.
The helper will use the first match in the config to setup the
streamer (it will exit as soon as it finds a match).
Args:
streamer (ImportStreamer): an import streamer object.
data_type (str): optional data type that is used for matching
with the loaded config.
columns (List[str]): optional list of strings with column names.
"""
for config_name, config in self._data.items():
conf_data_type = config.get('data_type')
if data_type and conf_data_type and conf_data_type == data_type:
logger.info('Using config %s for streamer.', config_name)
self._configure_streamer(streamer, config)
return

if not columns:
continue

column_set = set(columns)
column_string = config.get('columns', '')
column_subset_string = config.get('columns_subset', '')
if not any([column_string, column_subset_string]):
continue

conf_columns = set(column_string.split(','))
if conf_columns and column_set == conf_columns:
logger.info('Using config %s for streamer.', config_name)
self._configure_streamer(streamer, config)
return

columns_subset = set(column_subset_string.split(','))
if columns_subset and columns_subset.issubset(column_set):
logger.info('Using config %s for streamer.', config_name)
self._configure_streamer(streamer, config)
return

0 comments on commit 5964c6e

Please sign in to comment.