Repository: google/timesketch

Issue:
add_view in analyzers interface needs to be modified to prevent duplicates
If an analyzer adds a view and it is run on more than a single timeline, it will create multiple views that are the exact copy of one another.

The add_view function in the analyzer interface needs to be changed so that it first checks to see if the view has already been added before it is added to the sketch.


Discussion:

Resolving patch:
diff --git a/timesketch/lib/analyzers/browser_search.py b/timesketch/lib/analyzers/browser_search.py
--- a/timesketch/lib/analyzers/browser_search.py
+++ b/timesketch/lib/analyzers/browser_search.py
@@ -212,7 +212,8 @@ def run(self):
 
         if simple_counter > 0:
             self.sketch.add_view(
-                'Browser Search', query_string='tag:"browser_search"')
+                view_name='Browser Search', analyzer_name=self.NAME,
+                query_string='tag:"browser_search"')
 
         return (
             'Browser Search completed with {0:d} search results '
diff --git a/timesketch/lib/analyzers/interface.py b/timesketch/lib/analyzers/interface.py
--- a/timesketch/lib/analyzers/interface.py
+++ b/timesketch/lib/analyzers/interface.py
@@ -198,12 +198,13 @@ def __init__(self, sketch_id):
         if not self.sql_sketch:
             raise RuntimeError('No such sketch')
 
-    def add_view(self, name, query_string=None, query_dsl=None,
-                 query_filter=None):
+    def add_view(self, view_name, analyzer_name, query_string=None,
+                 query_dsl=None, query_filter=None):
         """Add saved view to the Sketch.
 
         Args:
-            name: The name of the view.
+            view_name: The name of the view.
+            analyzer_name: The name of the analyzer.
             query_string: Elasticsearch query string.
             query_dsl: Dictionary with Elasticsearch DSL query.
             query_filter: Dictionary with Elasticsearch filters.
@@ -219,11 +220,13 @@ def add_view(self, name, query_string=None, query_dsl=None,
         if not query_filter:
             query_filter = {'indices': '_all'}
 
-        view = View(name=name, sketch=self.sql_sketch, user=None,
-                    query_string=query_string, query_filter=query_filter,
-                    query_dsl=query_dsl, searchtemplate=None)
-
+        name = '[{0:s}] {1:s}'.format(analyzer_name, view_name)
+        view = View.get_or_create(name=name, sketch=self.sql_sketch, user=None)
+        view.query_string = query_string
         view.query_filter = view.validate_filter(query_filter)
+        view.query_dsl = query_dsl
+        view.searchtemplate = None
+
         db_session.add(view)
         db_session.commit()
         return view
diff --git a/timesketch/lib/analyzers/phishy_domains.py b/timesketch/lib/analyzers/phishy_domains.py
--- a/timesketch/lib/analyzers/phishy_domains.py
+++ b/timesketch/lib/analyzers/phishy_domains.py
@@ -270,7 +270,8 @@ def run(self):
 
         if similar_domain_counter:
             self.sketch.add_view(
-                'Phishy Domains', query_string='tag:"phishy_domain"')
+                view_name='Phishy Domains', analyzer_name=self.NAME,
+                query_string='tag:"phishy_domain"')
 
         return (
             'Domain extraction ({0:d} domains discovered with {1:d} TLDs) '


Test patch:
diff --git a/timesketch/lib/analyzers/interface_test.py b/timesketch/lib/analyzers/interface_test.py
--- a/timesketch/lib/analyzers/interface_test.py
+++ b/timesketch/lib/analyzers/interface_test.py
@@ -56,7 +56,8 @@ def test_sketch(self):
     def test_add_view(self):
         """Test adding a view to a sketch."""
         sketch = interface.Sketch(sketch_id=self.SKETCH_ID)
-        view = sketch.add_view('MockView', query_string='test')
+        view = sketch.add_view(
+            view_name='MockView', analyzer_name="Test", query_string='test')
         self.assertIsInstance(view, View)
 
     def test_get_all_instances(self):