-
Notifications
You must be signed in to change notification settings - Fork 659
test: add missing analyzer unit tests and datafinder date validation #3783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
47e156c
0a1fcce
28bc9f8
4c92dfd
56bdcb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,9 +108,85 @@ def test_is_suspicious(self): | |
| self.assertEqual(std_diffs, tc.expected_si_diffs) | ||
| self.assertEqual(fn_diffs, tc.expected_fn_diffs) | ||
|
|
||
| # Mock the OpenSearch datastore. | ||
| @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) | ||
| def test_analyzer(self): | ||
| """Test analyzer.""" | ||
| # TODO: Write actual tests here. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original TODO was sitting under a placeholder Currently, the
|
||
| self.assertEqual(True, True) | ||
| def test_is_suspicious_no_std_info(self): | ||
| """Test that FileInfo without a std_info_event is never suspicious.""" | ||
| analyzer = ntfs_timestomp.NtfsTimestompSketchPlugin("test", 1) | ||
| file_info = ntfs_timestomp.FileInfo( | ||
| file_reference=1, | ||
| timestamp_desc="Content Modification Time", | ||
| std_info_event=None, | ||
| std_info_timestamp=0, | ||
| file_names=[(MockEvent(), 9000000000)], | ||
| ) | ||
| self.assertFalse(analyzer.is_suspicious(file_info)) | ||
|
|
||
| @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) | ||
| def test_is_suspicious_no_file_names(self): | ||
| """Test that FileInfo with no file_names is never suspicious.""" | ||
| analyzer = ntfs_timestomp.NtfsTimestompSketchPlugin("test", 1) | ||
| file_info = ntfs_timestomp.FileInfo( | ||
| file_reference=1, | ||
| timestamp_desc="Content Modification Time", | ||
| std_info_event=MockEvent(), | ||
| std_info_timestamp=0, | ||
| file_names=[], | ||
| ) | ||
| self.assertFalse(analyzer.is_suspicious(file_info)) | ||
|
|
||
| @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) | ||
| def test_is_suspicious_within_threshold(self): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this already covered by the existing |
||
| """Test that file_names within the threshold are not flagged.""" | ||
| analyzer = ntfs_timestomp.NtfsTimestompSketchPlugin("test", 1) | ||
| # threshold is 10 * 60000000 = 600000000 microseconds by default. | ||
| # A diff smaller than the threshold must not trigger detection. | ||
| fn_event = MockEvent() | ||
| file_info = ntfs_timestomp.FileInfo( | ||
| file_reference=1, | ||
| timestamp_desc="Content Modification Time", | ||
| std_info_event=MockEvent(), | ||
| std_info_timestamp=0, | ||
| file_names=[(fn_event, analyzer.threshold - 1)], | ||
| ) | ||
| self.assertFalse(analyzer.is_suspicious(file_info)) | ||
| # The time_delta attribute must NOT have been set on the file_name event. | ||
| self.assertIsNone(fn_event.source.get("time_delta")) | ||
|
|
||
| @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) | ||
| def test_is_suspicious_timestomped(self): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, this is also already covered by the |
||
| """Test that file_names with differences above the threshold are flagged.""" | ||
| analyzer = ntfs_timestomp.NtfsTimestompSketchPlugin("test", 1) | ||
| fn_event = MockEvent() | ||
| si_event = MockEvent() | ||
| large_diff = analyzer.threshold + 1 | ||
| file_info = ntfs_timestomp.FileInfo( | ||
| file_reference=1, | ||
| timestamp_desc="Content Modification Time", | ||
| std_info_event=si_event, | ||
| std_info_timestamp=0, | ||
| file_names=[(fn_event, large_diff)], | ||
| ) | ||
| self.assertTrue(analyzer.is_suspicious(file_info)) | ||
| # The time_delta attribute must be set on every suspicious file_name. | ||
| self.assertEqual(fn_event.source.get("time_delta"), large_diff) | ||
| # The accumulated time_deltas must be stored on the STD_INFO event. | ||
| self.assertEqual(si_event.source.get("time_deltas"), [large_diff]) | ||
|
|
||
| @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) | ||
| def test_is_suspicious_mixed_file_names(self): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, this seems also to be already covered by |
||
| """Test that a single within-threshold file_name prevents detection.""" | ||
| analyzer = ntfs_timestomp.NtfsTimestompSketchPlugin("test", 1) | ||
| fn_event_large = MockEvent() | ||
| fn_event_small = MockEvent() | ||
| file_info = ntfs_timestomp.FileInfo( | ||
| file_reference=1, | ||
| timestamp_desc="Content Modification Time", | ||
| std_info_event=MockEvent(), | ||
| std_info_timestamp=0, | ||
| file_names=[ | ||
| (fn_event_large, analyzer.threshold + 1), | ||
| (fn_event_small, analyzer.threshold - 1), | ||
| ], | ||
| ) | ||
| # One file_name is within threshold β detection must be suppressed. | ||
| self.assertFalse(analyzer.is_suspicious(file_info)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,13 +14,30 @@ | |
| """The class definitions for the data finder, or the data analyzer.""" | ||
|
|
||
| import logging | ||
| from datetime import datetime | ||
|
|
||
| from timesketch.lib.analyzers import utils | ||
| from timesketch.lib.datastores.opensearch import OpenSearchDataStore | ||
|
|
||
| logger = logging.getLogger("timesketch.data_finder") | ||
|
|
||
|
|
||
| def _is_valid_iso_date(date_string): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That should work well. Would you mind adding a |
||
| """Check whether a string is a valid ISO 8601-formatted date. | ||
|
|
||
| Args: | ||
| date_string (str): The string to validate. | ||
|
|
||
| Returns: | ||
| bool: True if the string is a valid ISO 8601 date, False otherwise. | ||
| """ | ||
| try: | ||
| datetime.fromisoformat(date_string) | ||
| return True | ||
| except (ValueError, TypeError): | ||
| return False | ||
|
|
||
|
|
||
| class DataFinder: | ||
| """The data finder class.""" | ||
|
|
||
|
|
@@ -69,8 +86,13 @@ def can_run(self): | |
| return True | ||
|
|
||
| def set_end_date(self, end_date): | ||
| """Sets the end date of the time period the data finder uses.""" | ||
| # TODO: Implement a check if this is a valid ISO formatted date. | ||
| """Sets the end date of the time period the data finder uses. | ||
|
|
||
| Args: | ||
| end_date (str): An ISO 8601-formatted date string. | ||
| """ | ||
| if not _is_valid_iso_date(end_date): | ||
| logger.warning("end_date [%s] is not a valid ISO 8601 date.", end_date) | ||
| self._end_date = end_date | ||
|
|
||
| def set_indices(self, indices): | ||
|
|
@@ -106,8 +128,13 @@ def set_rule(self, rule_dict): | |
| self._rule = rule_dict | ||
|
|
||
| def set_start_date(self, start_date): | ||
| """Sets the start date of the time period the data finder uses.""" | ||
| # TODO: Implement a check if this is a valid ISO formatted date. | ||
| """Sets the start date of the time period the data finder uses. | ||
|
|
||
| Args: | ||
| start_date (str): An ISO 8601-formatted date string. | ||
| """ | ||
| if not _is_valid_iso_date(start_date): | ||
| logger.warning("start_date [%s] is not a valid ISO 8601 date.", start_date) | ||
| self._start_date = start_date | ||
|
|
||
| def set_timeline_ids(self, timeline_ids): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job setting up
MockDataStoreand runninganalyzer.run().Currently, the assertions only check the high-level summary string returned by run() (e.g., "2 domains discovered"). To ensure that the analyzer is actually modifying the events in the datastore as expected (and to prevent future code changes from silently breaking this), we should assert the actual event tags and attributes too.