From 3f11659e555e2c98591fa72470525b766b7dc24c Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Sun, 18 Jun 2023 13:27:41 +0100 Subject: [PATCH 01/12] Add processors. Move to pyproject.toml. --- .vscode/settings.json | 28 +- grove/__about__.py | 1 - grove/__init__.py | 25 +- grove/connectors/__init__.py | 309 ++++++++++++------ grove/connectors/github/audit_log.py | 6 +- grove/connectors/gsuite/activities.py | 2 +- grove/connectors/local/heartbeat.py | 4 +- grove/connectors/okta/system_log.py | 2 +- grove/connectors/onepassword/api.py | 3 +- grove/connectors/oomnitza/activities.py | 8 +- grove/connectors/oomnitza/api.py | 1 + grove/connectors/sf/event_log.py | 2 +- grove/connectors/tfc/api.py | 2 +- grove/connectors/torq/api.py | 12 +- grove/connectors/twilio/messages.py | 2 +- grove/connectors/twilio/monitor_events.py | 2 +- grove/connectors/workday/activity_logging.py | 10 +- grove/connectors/zoom/activities.py | 2 +- grove/connectors/zoom/api.py | 10 +- grove/connectors/zoom/operationlogs.py | 2 +- grove/constants.py | 1 + grove/entrypoints/base.py | 4 +- grove/exceptions.py | 6 +- grove/helpers/parsing.py | 112 ++++++- grove/helpers/plugin.py | 7 +- grove/models.py | 46 ++- grove/outputs/__init__.py | 67 +++- grove/outputs/aws_s3.py | 124 ++++--- grove/outputs/local_file.py | 70 ++-- grove/outputs/local_stdout.py | 14 +- grove/processors/__init__.py | 57 ++++ grove/processors/extract_paths.py | 97 ++++++ grove/processors/filter_paths.py | 44 +++ grove/processors/split_path.py | 55 ++++ pyproject.toml | 183 +++++++++++ setup.cfg | 160 --------- setup.py | 77 ----- .../example_logs.py | 4 +- .../connectors/local_heartbeat.json | 5 +- tests/fixtures/gsuite/activities/001.json | 17 +- tests/mocks/__init__.py | 6 +- tests/mocks/output.py | 5 +- .../test_connectors_atlassian_audit_events.py | 4 +- tests/test_connectors_deduplicate.py | 14 +- tests/test_connectors_github_audit.py | 2 +- tests/test_connectors_gsuite_activities.py | 2 +- tests/test_connectors_gsuite_alerts.py | 2 +- tests/test_connectors_okta_system_log.py | 2 +- ...est_connectors_onepassword_events_audit.py | 5 +- ...onnectors_onepassword_events_itemusages.py | 4 +- ...ctors_onepassword_events_signinattempts.py | 4 +- tests/test_connectors_oomnitza_activities.py | 7 +- ...test_connectors_pagerduty_audit_records.py | 4 +- tests/test_connectors_sf_event_log.py | 2 +- tests/test_connectors_sfmc_audit_events.py | 4 +- tests/test_connectors_sfmc_security_events.py | 4 +- tests/test_connectors_slack_audit.py | 4 +- tests/test_connectors_tfc_audit_trails.py | 4 +- tests/test_connectors_torq_activity_logs.py | 4 +- tests/test_connectors_torq_audit_logs.py | 4 +- ...est_connectors_workday_activity_logging.py | 6 +- tests/test_connectors_zoom_activities.py | 4 +- tests/test_connectors_zoom_operation.py | 4 +- tests/test_helpers_parsing.py | 87 +++++ tests/test_outputs_base.py | 2 + tests/test_processors_extract_paths.py | 80 +++++ tests/test_processors_filter_paths.py | 55 ++++ tests/test_processors_split_path.py | 50 +++ 68 files changed, 1393 insertions(+), 565 deletions(-) create mode 100644 grove/processors/__init__.py create mode 100644 grove/processors/extract_paths.py create mode 100644 grove/processors/filter_paths.py create mode 100644 grove/processors/split_path.py create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 tests/test_helpers_parsing.py create mode 100644 tests/test_processors_extract_paths.py create mode 100644 tests/test_processors_filter_paths.py create mode 100644 tests/test_processors_split_path.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 3254ecd..4e20929 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,16 @@ { - "python.formatting.provider": "black", "python.linting.enabled": true, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--config setup.cfg" - ], - "python.linting.mypyEnabled": true, - "python.linting.mypyArgs": [ - "--config-file setup.cfg" - ], - "isort.args": [ - "-sp setup.cfg" - ], - "python.linting.pylintEnabled": false, + "python.formatting.provider": "none", "[python]": { + "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": true, }, - "editor.tabSize": 4, - "editor.formatOnSave": true + "editor.defaultFormatter": "ms-python.black-formatter", }, + "editor.rulers": [ + 88 + ], "[terraform]": { "editor.tabSize": 2, "editor.formatOnSave": true @@ -33,9 +24,4 @@ "editor.tabSize": 2, "editor.autoIndent": "advanced" }, - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true } diff --git a/grove/__about__.py b/grove/__about__.py index 587b648..42141f4 100644 --- a/grove/__about__.py +++ b/grove/__about__.py @@ -2,6 +2,5 @@ __version__ = "1.0.0rc4" __title__ = "grove" -__author__ = "HashiCorp Security (TDR)" __license__ = "Mozilla Public License 2.0" __copyright__ = "Copyright 2023 HashiCorp, Inc." diff --git a/grove/__init__.py b/grove/__init__.py index de9c844..e831b7e 100644 --- a/grove/__init__.py +++ b/grove/__init__.py @@ -3,15 +3,18 @@ """A framework for collecting and transforming SaaS logs.""" -from grove import caches # noqa: F401 -from grove import configs # noqa: F401 -from grove import connectors # noqa: F401 -from grove import constants # noqa: F401 -from grove import entrypoints # noqa: F401 -from grove import exceptions # noqa: F401 -from grove import helpers # noqa: F401 -from grove import logging # noqa: F401 -from grove import models # noqa: F401 -from grove import outputs # noqa: F401 -from grove import types # noqa: F401 +from grove import ( + caches, # noqa: F401 + configs, # noqa: F401 + connectors, # noqa: F401 + constants, # noqa: F401 + entrypoints, # noqa: F401 + exceptions, # noqa: F401 + helpers, # noqa: F401 + logging, # noqa: F401 + models, # noqa: F401 + outputs, # noqa: F401 + processors, # noqa: F401 + types, # noqa: F401 +) from grove.__about__ import * # noqa: F401, F403 diff --git a/grove/connectors/__init__.py b/grove/connectors/__init__.py index 05ae23d..c0df849 100644 --- a/grove/connectors/__init__.py +++ b/grove/connectors/__init__.py @@ -10,10 +10,10 @@ import logging import os from typing import Any, Dict, List, Optional -from grove.__about__ import __version__ import jmespath +from grove.__about__ import __version__ from grove.constants import ( CACHE_KEY_LOCK, CACHE_KEY_POINTER, @@ -33,17 +33,20 @@ LOCK_DATE_FORMAT, PLUGIN_GROUP_CACHE, PLUGIN_GROUP_OUTPUT, + PLUGIN_GROUP_PROCESSOR, REVERSE_CHRONOLOGICAL, ) from grove.exceptions import ( AccessException, ConcurrencyException, + ConfigurationException, DataFormatException, GroveException, NotFoundException, + ProcessorError, ) -from grove.helpers import plugin -from grove.models import ConnectorConfig +from grove.helpers import parsing, plugin +from grove.models import ConnectorConfig, OutputStream class BaseConnector: @@ -78,14 +81,31 @@ def __init__(self, config: ConnectorConfig, context: Dict[str, str]): } # Let the caller handle exceptions from failure to load handlers directly. - self._output = plugin.load_handler( - os.environ.get(ENV_GROVE_OUTPUT_HANDLER, DEFAULT_OUTPUT_HANDLER), - PLUGIN_GROUP_OUTPUT, - ) self._cache = plugin.load_handler( os.environ.get(ENV_GROVE_CACHE_HANDLER, DEFAULT_CACHE_HANDLER), PLUGIN_GROUP_CACHE, ) + self._output = plugin.load_handler( + os.environ.get(ENV_GROVE_OUTPUT_HANDLER, DEFAULT_OUTPUT_HANDLER), + PLUGIN_GROUP_OUTPUT, + ) + self._output.setup() + + # Processors are only setup once for each connector instance. + self._processors = {} + + for processor in self.configuration.processors: + try: + self._processors[processor.name] = plugin.load_handler( + processor.processor, + PLUGIN_GROUP_PROCESSOR, + processor, + ) + except ConfigurationException as err: + raise ProcessorError( + f"Failed to initialise processor '{processor.name}' " + f"({processor.processor}). {err}", + ) # The time that our current lock expires, if we have one. self._lock_expiry: Optional[datetime.datetime] = None @@ -97,7 +117,7 @@ def __init__(self, config: ConnectorConfig, context: Dict[str, str]): except ValueError as err: self.logger.warning( f"Lock duration ('{ENV_GROVE_LOCK_DURATION}') must be a number.", - extra={"exception": err}, + extra={"exception": err, **self.log_context}, ) # Determines if the start of a 'window' has been passed during a collection. @@ -107,13 +127,16 @@ def __init__(self, config: ConnectorConfig, context: Dict[str, str]): self._window_start = str() self._window_end = str() - # Tracks the total number of saved log entries. - self._saved = 0 - # Paginated / chunked data needs an incrementing identifier to keep things # orderly. self._part = 0 + # Track the number of output logs by the configured output destination stream. + # This allows statistics to be generated on deduplication, splitting, etc. + self._saved = {} + for descriptor, _ in self.configuration.outputs.items(): + self._saved[descriptor] = 0 + # Tracks hashes of unique log entries, keyed by their pointer value. self._hashes: Dict[str, set[str]] = {} @@ -145,7 +168,7 @@ def run(self): self.collect() except GroveException as err: self.logger.error( - "Connector was unable to collect logs.", + "Connector was unable to complete collection successfully.", extra={"exception": err, **self.log_context}, ) self.unlock() @@ -163,9 +186,10 @@ def run(self): def _run_chronological(self): """Performs chronological specific post collection operations.""" + # TODO: Move to processor. try: self.logger.debug( - "Saving deduplication hashes to cache", + "Saving deduplication hashes to cache.", extra=self.log_context, ) self.save_hashes() @@ -193,7 +217,7 @@ def _run_reverse_chronological(self): return except NotFoundException: self.logger.debug( - "Skipping pointer swap and clean-up as there is no next-pointer", + "Skipping pointer swap and clean-up as there is no next-pointer.", extra={**self.log_context}, ) return @@ -224,6 +248,7 @@ def _run_reverse_chronological(self): ) return + # TODO: Move to processor. try: self.logger.debug( "Saving deduplication hashes to cache", @@ -242,16 +267,77 @@ def collect(self): """Provides a stub for a connector to initiate a collection.""" pass - def save(self, candidates: List[Any]): - """Saves log candidates, and updates the pointer in the cache. + def process_and_write(self, entries: List[Any]): + """Write log entries them to the configured output handler. - :param candidates: List of log candidates to save. + :param entries: List of log entries to process. + """ + # Allow failures to bubble all the way up and fail the run. If processing fails + # we want to defer collection, to allow retry later. We always pass a copy of + # the entries to prevent accidental overwriting of the collected raw data by + # a processor. + processed = self.process(entries) + + for descriptor, stream in self.configuration.outputs.items(): + # Ensure the output uses the correct stream. + to_save = entries + if stream == OutputStream.processed: + to_save = processed + + number_of_entries = len(to_save) + if number_of_entries < 1: + self.logger.info( + "No log entries to output, skipping.", + extra=self.log_context, + ) + continue + + try: + self._output.submit( + data=self._output.serialize( + data=to_save, + metadata=self.metadata(), + ), + part=self._part, + operation=self.operation, + connector=self.NAME, + identity=self.identity, + descriptor=descriptor, + ) + + # Update counters. + self._saved[descriptor] += number_of_entries + + self.logger.info( + "Log submitted successfully to output.", + extra={ + "part": self._part, + "stream": stream, + "descriptor": descriptor, + "entries": number_of_entries, + **self.log_context, + }, + ) + except AccessException as err: + self.logger.error( + "Failed to write logs to output, cannot continue.", + extra={ + "part": self._part, + "exception": err, + "stream": stream, + "descriptor": descriptor, + **self.log_context, + }, + ) + raise - :raises GroveException: The LOG_ORDER defined by the Connector is not valid. + def save(self, entries: List[Any]): + """Saves log entries, and updates the pointer in the cache. - :return: A count of entries saved. + :param entries: List of log entries to save. """ - entries = self.deduplicate_by_hash(candidates) + # TODO: Move deduplication into a processor. + entries = self.deduplicate_by_hash(entries) if len(entries) < 1: self.logger.warning( @@ -265,51 +351,29 @@ def save(self, candidates: List[Any]): self.lock() if self.LOG_ORDER == CHRONOLOGICAL: - return self._save_chronological(entries) + self._save_chronological(entries) if self.LOG_ORDER == REVERSE_CHRONOLOGICAL: - return self._save_reverse_chronological(entries) + self._save_reverse_chronological(entries) - # Fall through for anything not supported / incorrectly specified. - raise GroveException(f"Connector LOG_ORDER '{self.LOG_ORDER}' is not valid.") + self.finalize() - def _save_chronological(self, candidates: List[Any]): + def _save_chronological(self, entries: List[Any]): """Saves log entries when retrieved logs are in chronological order. - :param candidates: List of log entries to save. + :param entries: List of log entries to save. """ - newest = jmespath.search(self.POINTER_PATH, candidates[-1]) - + # Pointers are extracted prior to processing as processing may modify the + # structure, or remove entries entirely. + newest = jmespath.search(self.POINTER_PATH, entries[-1]) if newest is None: - self.logger.error( - "Pointer path was not found in returned logs, cannot continue.", - extra={"pointer_path": self.POINTER_PATH, **self.log_context}, + raise GroveException( + f"Pointer path ({self.POINTER_PATH}) was not found in returned logs." ) - return - # Generate metadata for the candidate log entries, and save to the output - # handler. - try: - self._output.submit( - data=self._output.serialize( - data=candidates, - metadata=self.metadata(), - ), - part=self._part, - operation=self.operation, - connector=self.NAME, - identity=self.identity, - ) - self.logger.info( - "Log submitted successfully to output.", - extra={"part": self._part, **self.log_context}, - ) - except AccessException as err: - self.logger.error( - "Failed to write logs to output, cannot continue.", - extra={"exception": err, **self.log_context}, - ) - return + # Exceptions are allowed to bubble up here to ensure connectors exit on error, + # rather than silently dropping batches of log entries. + self.process_and_write(entries) # Once uploaded, then update the pointer. NOTE: There is an opportunity for # issues to occur between the output and pointer update which would lead to @@ -325,11 +389,10 @@ def _save_chronological(self, candidates: List[Any]): "Failed to save pointer to cache, cannot continue.", extra={"exception": err, **self.log_context}, ) - return + raise - # Get ready for the next block of candidate log entries (if required). + # Get ready for the next batch of candidate log entries (if required). self._part += 1 - self._saved += len(candidates) def _save_reverse_chronological(self, candidates: List[Any]): # noqa: C901 """Save log entries when logs are in reverse chronological order. @@ -348,11 +411,9 @@ def _save_reverse_chronological(self, candidates: List[Any]): # noqa: C901 newest = jmespath.search(self.POINTER_PATH, candidates[0]) if oldest is None or newest is None: - self.logger.error( - "Pointer path was not found in logs entry, cannot continue.", - extra={"pointer_path": self.POINTER_PATH, **self.log_context}, + raise GroveException( + f"Pointer path ({self.POINTER_PATH}) was not found in returned logs." ) - return # If a window start is in the cache then a previous collection is incomplete. # We'll skip entries until we find our window, and then only collect entries @@ -371,11 +432,9 @@ def _save_reverse_chronological(self, candidates: List[Any]): # noqa: C901 current_pointer = jmespath.search(self.POINTER_PATH, entry) if current_pointer is None: - self.logger.error( - "Pointer path was not found in logs entry, cannot continue.", - extra={"pointer_path": self.POINTER_PATH, **self.log_context}, + raise GroveException( + f"Pointer path ({self.POINTER_PATH}) not found in log entry." ) - return # We need to track FROM the window end, inclusive, to ensure that we # don't miss any logs. This is required in cases where the timestamp @@ -409,32 +468,12 @@ def _save_reverse_chronological(self, candidates: List[Any]): # noqa: C901 if len(entries) < 1: return - # Generate metadata for the entries, and save to the output handler. - try: - self._output.submit( - data=self._output.serialize( - data=entries, - metadata=self.metadata(), - ), - part=self._part, - operation=self.operation, - connector=self.NAME, - identity=self.identity, - ) - self.logger.info( - "Log submitted successfully to output.", - extra={"part": self._part, **self.log_context}, - ) - except AccessException as err: - self.logger.error( - "Failed to write logs to output, cannot continue.", - extra={"exception": err, **self.log_context}, - ) - return + # Exceptions are allowed to bubble up here to ensure connectors exit on error, + # rather than silently dropping batches of log entries. + self.process_and_write(entries) # Get ready for the next block of entries (if required). self._part += 1 - self._saved += len(entries) # Save the new window geometry to cache but only AFTER data is saved, and only # save the window start when it's updated. @@ -566,7 +605,7 @@ def deduplicate_by_hash(self, candidates: List[Any]): return entries - def deduplicate_by_pointer(self, candidates: List[Any]): + def deduplicate_by_pointer(self, entries: List[Any]): """Deduplicate log entries by pointer values. Deduplicates records which occur before or after a pointer on the current @@ -578,27 +617,27 @@ def deduplicate_by_pointer(self, candidates: List[Any]): For example, some provider's only allow filtering on a date (YYYY-MM-DD) while returning log entries with timestamps that have millisecond precision. - :param candidates: A list of log entries to deduplicate. + :param entries: A list of log entries to deduplicate. :return: A deduplicated list of log entries. """ if self.LOG_ORDER == CHRONOLOGICAL: - return self._deduplicate_by_pointer_chronological(candidates) + return self._deduplicate_by_pointer_chronological(entries) if self.LOG_ORDER == REVERSE_CHRONOLOGICAL: - return self._deduplicate_by_pointer_reverse_chronological(candidates) + return self._deduplicate_by_pointer_reverse_chronological(entries) - def _deduplicate_by_pointer_chronological(self, candidates: List[Any]): + def _deduplicate_by_pointer_chronological(self, entries: List[Any]): """Deduplicates chronological log entries by their pointer. - :param candidates: A list of log entries to deduplicate. + :param entries: A list of log entries to deduplicate. :return: A deduplicated list of log entries. """ - entries = [] + results = [] pointer_passed = False - for candidate in candidates: + for candidate in entries: candidate_pointer = str(jmespath.search(self.POINTER_PATH, candidate)) if candidate_pointer == self.pointer: @@ -606,28 +645,28 @@ def _deduplicate_by_pointer_chronological(self, candidates: List[Any]): # Only track chronological records on and after the pointer. if pointer_passed: - entries.append(candidate) + results.append(candidate) # If we never encountered the pointer, don't filter the records at all. This may # cause some duplicates if the pointer is on a subsequent page, but we always # prefer duplicates in these cases. if not pointer_passed: - entries = candidates + results = entries - return entries + return results - def _deduplicate_by_pointer_reverse_chronological(self, candidates: List[Any]): + def _deduplicate_by_pointer_reverse_chronological(self, entries: List[Any]): """Deduplicates reverse chronological log entries by their pointer. - :param candidates: A list of log entries to deduplicate. + :param entries: A list of log entries to deduplicate. :return: A deduplicated list of log entries. """ - entries = [] + results = [] pointer_found = False pointer_passed = False - for candidate in candidates: + for candidate in entries: candidate_pointer = jmespath.search(self.POINTER_PATH, candidate) if candidate_pointer == self.pointer: @@ -638,15 +677,65 @@ def _deduplicate_by_pointer_reverse_chronological(self, candidates: List[Any]): break if not pointer_passed: - entries.append(candidate) + results.append(candidate) # If we never encountered the pointer, don't filter the records at all. This may # cause some duplicates if the pointer is on a subsequent page, but we always # prefer duplicates in these cases. if not pointer_passed: - entries = candidates + results = entries - return entries + return results + + def process(self, entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Process log entries prior to saving. + + :param entries: A list of log entries to process. + + :return: A processed list of log entries. + """ + # Shortcut where there are no processors configured. + if len(self._processors) < 1: + return entries + + # As processors can modify the number of entries, we need to loop over them + # multiple times. + processed = parsing.quick_copy(entries) + + for name, processor in self._processors.items(): + for index, _ in enumerate(processed): + try: + processed[index:index] = processor.process(processed.pop(index)) + except Exception as err: + raise ProcessorError( + f"Processor '{name}' ({processor}) failed during " + f" processing. {err}" + ) + + return processed + + def finalize(self): + """Performs final steps after each save operation has complete.""" + + # Finalize all processors. + for name, processor in self._processors.items(): + # Once again this exception handler is exceptionally (!) broad, to ensure + # that any unhandled exception, including from downstream libraries, are + # caught and handled consistently (except for BaseException derived). + try: + processor.finalize() + except Exception as err: + # As this runs after saving data and pointers, all we can really do is + # log this and continue. + self.logger.error( + "Processor failed during finalization.", + extra={ + "identity": name, + "processor": processor, + "exception": err, + **self.log_context, + }, + ) @property def hashes(self) -> Dict[str, set[str]]: @@ -661,7 +750,10 @@ def hashes(self) -> Dict[str, set[str]]: try: self._hashes[self.pointer] = set( json.loads( - self._cache.get(self.cache_key(CACHE_KEY_SEEN), self.operation) + self._cache.get( + self.cache_key(CACHE_KEY_SEEN), + self.operation, + ) ) ) except (TypeError, json.decoder.JSONDecodeError) as err: @@ -946,3 +1038,4 @@ def unlock(self): # Bye-bye lock. self._lock_expiry = None + self._lock_expiry = None diff --git a/grove/connectors/github/audit_log.py b/grove/connectors/github/audit_log.py index da1d646..0a02b31 100644 --- a/grove/connectors/github/audit_log.py +++ b/grove/connectors/github/audit_log.py @@ -40,7 +40,7 @@ def delay(self): :return: The "delay" component of the connector configuration. """ try: - candidate = self.configuration.delay # type: ignore + candidate = self.configuration.delay except AttributeError: return 0 @@ -63,7 +63,7 @@ def scope(self): :return: The "scope" component of the connector configuration. """ try: - candidate = self.configuration.scope # type: ignore + candidate = self.configuration.scope except AttributeError: return "orgs" @@ -84,7 +84,7 @@ def fqdn(self): :return: The "fqdn" component of the connector configuration. """ try: - return self.configuration.fqdn # type: ignore + return self.configuration.fqdn except AttributeError: return "api.github.com" diff --git a/grove/connectors/gsuite/activities.py b/grove/connectors/gsuite/activities.py index 42f4c49..3a60d5e 100644 --- a/grove/connectors/gsuite/activities.py +++ b/grove/connectors/gsuite/activities.py @@ -59,7 +59,7 @@ def delay(self): :return: The "delay" component of the connector configuration. """ try: - candidate = self.configuration.delay # type: ignore + candidate = self.configuration.delay except AttributeError: return 0 diff --git a/grove/connectors/local/heartbeat.py b/grove/connectors/local/heartbeat.py index 24bea6f..cf5e0ad 100644 --- a/grove/connectors/local/heartbeat.py +++ b/grove/connectors/local/heartbeat.py @@ -25,7 +25,7 @@ def count(self): :return: The number of heartbeat messages to emit. """ try: - return int(self.configuration.count) # type: ignore + return int(self.configuration.count) except (AttributeError, ValueError): return 5 @@ -36,7 +36,7 @@ def interval(self): :return: The heartbeat interval, in seconds. """ try: - return int(self.configuration.interval) # type: ignore + return int(self.configuration.interval) except (AttributeError, ValueError): return 1 diff --git a/grove/connectors/okta/system_log.py b/grove/connectors/okta/system_log.py index f0e1ec6..c1d4b2c 100644 --- a/grove/connectors/okta/system_log.py +++ b/grove/connectors/okta/system_log.py @@ -27,7 +27,7 @@ def domain(self): :return: The "domain" portion of the connector's configuration. """ try: - return self.configuration.domain # type: ignore + return self.configuration.domain except AttributeError: return "okta.com" diff --git a/grove/connectors/onepassword/api.py b/grove/connectors/onepassword/api.py index 1fbb654..4d6fc6f 100644 --- a/grove/connectors/onepassword/api.py +++ b/grove/connectors/onepassword/api.py @@ -8,6 +8,7 @@ from typing import Any, Dict, Optional import requests + from grove.exceptions import RateLimitException, RequestFailedException from grove.types import AuditLogEntries, HTTPResponse @@ -154,7 +155,7 @@ def get_auditevents( cursor: Optional[str] = None, start_time: Optional[str] = None, ) -> AuditLogEntries: - """Fetches a list of of actions performed by team members within a 1Password account. + """Fetches a list of actions performed by members of a 1Password account. :param cursor: Cursor to use when fetching results. Supersedes other parameters. :param start_time: The ISO Format timestamp to query logs since. diff --git a/grove/connectors/oomnitza/activities.py b/grove/connectors/oomnitza/activities.py index 9be2dae..6d95cc0 100644 --- a/grove/connectors/oomnitza/activities.py +++ b/grove/connectors/oomnitza/activities.py @@ -28,8 +28,8 @@ def collect(self): cursor = 0 # If no pointer is stored then a previous run hasn't been performed, so set the - # pointer to 2 days ago. In the case of the Oomnitza activities API the pointer is - # the value of the "timestamp" field from the latest record retrieved from + # pointer to 2 days ago. In the case of the Oomnitza activities API the pointer + # is the value of the "timestamp" field from the latest record retrieved from # the API - which is in epoch. The Oomnitza API doesnt account for milliseconds. now = datetime.fromtimestamp(time.time()).strftime("%s") @@ -40,8 +40,8 @@ def collect(self): datetime.fromtimestamp(time.time()) - timedelta(days=2) ).strftime("%s") - # Get log data from the upstream API. A "start_date" and "end_date" datetime query - # parameters are required. + # Get log data from the upstream API. A "start_date" and "end_date" datetime + # query parameters are required. while True: log = client.get_activites( start_date=self.pointer, end_date=now, cursor=cursor diff --git a/grove/connectors/oomnitza/api.py b/grove/connectors/oomnitza/api.py index ade4b83..e47f4fa 100644 --- a/grove/connectors/oomnitza/api.py +++ b/grove/connectors/oomnitza/api.py @@ -7,6 +7,7 @@ from typing import Dict, Optional import requests + from grove.exceptions import RequestFailedException from grove.types import AuditLogEntries, HTTPResponse diff --git a/grove/connectors/sf/event_log.py b/grove/connectors/sf/event_log.py index bab1e4f..2b405a7 100644 --- a/grove/connectors/sf/event_log.py +++ b/grove/connectors/sf/event_log.py @@ -47,7 +47,7 @@ def token(self): :return: The "token" portion of the connector's configuration. """ try: - return self.configuration.token # type: ignore + return self.configuration.token except AttributeError: return None diff --git a/grove/connectors/tfc/api.py b/grove/connectors/tfc/api.py index 7d07e8a..3033e3f 100644 --- a/grove/connectors/tfc/api.py +++ b/grove/connectors/tfc/api.py @@ -80,7 +80,7 @@ def get_trails( ) -> AuditLogEntries: """Fetches a list of audit logs which match the provided filters. - :param since: The ISO8601 format of the most recent event to include (inclusive). + :param since: The ISO8601 date of the most recent event to include (inclusive). :param cursor: The page to fetch. If omitted, endpoint returns first page. :param page_size: Number of audit events per page. Defaults to 1000. diff --git a/grove/connectors/torq/api.py b/grove/connectors/torq/api.py index 28b6c86..3d3a4f9 100644 --- a/grove/connectors/torq/api.py +++ b/grove/connectors/torq/api.py @@ -144,8 +144,8 @@ def get_logs( ) -> AuditLogEntries: """Fetches a list of logs from Torq which match the provided filters. - :param start_time: The required date and time of the earliest log entry. - Timestamps are in RFC 3339 format, for example, 2022-03-09T08:40:18.490771179Z. + :param start_time: The required date and time of the earliest log entry. Start + times are in RFC3339 format, for example, 2022-03-09T08:40:18.490771179Z. :param result_field: The key name for the list of logs in the returned json. :param to_date: The required date and time in UTC of the latest log entry. :param limit: The maximum number of items to include in a single response. @@ -178,8 +178,8 @@ def get_audit_logs( ) -> AuditLogEntries: """Fetches a list of audit logs from Torq which match the provided filters. - :param start_time: The required date and time of the earliest log entry. - Timestamps are in RFC 3339 format, for example, 2022-03-09T08:40:18.490771179Z. + :param start_time: The required date and time of the earliest log entry. Start + times are in RFC 3339 format, for example, 2022-03-09T08:40:18.490771179Z. :param to_date: The required date and time in UTC of the latest log entry. :param cursor: The cursor to use when paging. @@ -197,8 +197,8 @@ def get_activity_logs( ) -> AuditLogEntries: """Fetches a list of activity logs from Torq which match the provided filters. - :param start_time: The required date and time of the earliest log entry. - Timestamps are in RFC 3339 format, for example, 2022-03-09T08:40:18.490771179Z. + :param start_time: The required date and time of the earliest log entry. Start + times are in RFC 3339 format, for example, 2022-03-09T08:40:18.490771179Z. :param to_date: The required date and time in UTC of the latest log entry. :param cursor: The cursor to use when paging. diff --git a/grove/connectors/twilio/messages.py b/grove/connectors/twilio/messages.py index 1459d97..2988ca3 100644 --- a/grove/connectors/twilio/messages.py +++ b/grove/connectors/twilio/messages.py @@ -30,7 +30,7 @@ def secret(self): :return: The value of the 'secret' field from the configuration. """ try: - return self.configuration.secret # type: ignore + return self.configuration.secret except AttributeError: return None diff --git a/grove/connectors/twilio/monitor_events.py b/grove/connectors/twilio/monitor_events.py index bb3d52f..8a00bda 100644 --- a/grove/connectors/twilio/monitor_events.py +++ b/grove/connectors/twilio/monitor_events.py @@ -28,7 +28,7 @@ def secret(self): :return: The value of the 'secret' field from the configuration. """ try: - return self.configuration.secret # type: ignore + return self.configuration.secret except AttributeError: return None diff --git a/grove/connectors/workday/activity_logging.py b/grove/connectors/workday/activity_logging.py index ce87b02..db57add 100644 --- a/grove/connectors/workday/activity_logging.py +++ b/grove/connectors/workday/activity_logging.py @@ -28,7 +28,7 @@ def base_url(self): :return: The "base_url" portion of the connector's configuration. """ try: - return self.configuration.base_url # type: ignore + return self.configuration.base_url except AttributeError: return None @@ -41,7 +41,7 @@ def client_id(self): :return: The "client_id" portion of the connector's configuration. """ try: - return self.configuration.client_id # type: ignore + return self.configuration.client_id except AttributeError: return None @@ -54,7 +54,7 @@ def client_secret(self): :return: The "client_secret" portion of the connector's configuration. """ try: - return self.configuration.client_secret # type: ignore + return self.configuration.client_secret except AttributeError: return None @@ -92,7 +92,9 @@ def collect(self): # parameters are required. while True: log = client.get_activity_logging( - from_date=self.pointer, to_date=now, cursor=cursor + from_date=self.pointer, + to_date=now, + cursor=cursor, ) # Save this batch of log entries. diff --git a/grove/connectors/zoom/activities.py b/grove/connectors/zoom/activities.py index e16d495..4658e0f 100644 --- a/grove/connectors/zoom/activities.py +++ b/grove/connectors/zoom/activities.py @@ -25,7 +25,7 @@ def client_id(self): This is required as this is a third authentication element required by Zoom. """ try: - return self.configuration.client_id # type: ignore + return self.configuration.client_id except AttributeError: return None diff --git a/grove/connectors/zoom/api.py b/grove/connectors/zoom/api.py index 86cce48..16d1fc7 100644 --- a/grove/connectors/zoom/api.py +++ b/grove/connectors/zoom/api.py @@ -3,8 +3,8 @@ """Zoom API client. -As the Python Zoom client does not currently support Audit API, this client has been created in -the interim. +As the Python Zoom client does not currently support Audit API, this client has been +created in the interim. """ import base64 @@ -151,9 +151,9 @@ def get_logs( :return: AuditLogEntries object containing a pagination cursor, and log entries. """ - # The endpoint returns the same total value of results regardless of the limit and - # offset parameters. The pagination parameters determine the amount of content - # in the data[] array. + # The endpoint returns the same total value of results regardless of the limit + # and offset parameters. The pagination parameters determine the amount of + # content in the data[] array. result = self._get( f"{API_BASE_URI}/{endpoint}", diff --git a/grove/connectors/zoom/operationlogs.py b/grove/connectors/zoom/operationlogs.py index 29e2de1..44567d2 100644 --- a/grove/connectors/zoom/operationlogs.py +++ b/grove/connectors/zoom/operationlogs.py @@ -25,7 +25,7 @@ def client_id(self): This is required as this is a third authentication element required by Zoom. """ try: - return self.configuration.client_id # type: ignore + return self.configuration.client_id except AttributeError: return None diff --git a/grove/constants.py b/grove/constants.py index 014bd66..9d56dbe 100644 --- a/grove/constants.py +++ b/grove/constants.py @@ -41,6 +41,7 @@ PLUGIN_GROUP_CACHE = "grove.caches" PLUGIN_GROUP_OUTPUT = "grove.outputs" PLUGIN_GROUP_CONFIG = "grove.configs" +PLUGIN_GROUP_PROCESSOR = "grove.processors" PLUGIN_GROUP_SECRET = "grove.secrets" # noqa: S105 PLUGIN_GROUP_CONNECTOR = "grove.connectors" diff --git a/grove/entrypoints/base.py b/grove/entrypoints/base.py index 08a56bb..d00b719 100644 --- a/grove/entrypoints/base.py +++ b/grove/entrypoints/base.py @@ -84,7 +84,7 @@ def entrypoint(context: Dict[str, Any]): try: configurations = configure() except GroveException as err: - logger.fatal( + logger.critical( "Failed to initialise configuration handler", extra={"exception": err} ) return @@ -93,7 +93,7 @@ def entrypoint(context: Dict[str, Any]): try: workers = int(os.environ.get(ENV_GROVE_WORKER_COUNT, DEFAULT_WORKER_COUNT)) except ValueError as err: - logger.fatal( + logger.critical( f"Worker count ('{ENV_GROVE_WORKER_COUNT}') must be a number.", extra={"exception": err}, ) diff --git a/grove/exceptions.py b/grove/exceptions.py index 267ec38..f52b61b 100644 --- a/grove/exceptions.py +++ b/grove/exceptions.py @@ -17,7 +17,7 @@ class ConnectorMissingException(GroveException): class ConcurrencyException(GroveException): - """Indidates that Grove may be running in another location concurrently.""" + """Indicates that Grove may be running in another location concurrently.""" class NotImplementedException(GroveException): @@ -42,3 +42,7 @@ class AccessException(GroveException): class DataFormatException(GroveException): """Indicates an issue occurred while attempting to process data.""" + + +class ProcessorError(GroveException): + """Indicates that an error occurred when setting up or calling a processor.""" diff --git a/grove/helpers/parsing.py b/grove/helpers/parsing.py index 6a2898a..ba9c59f 100644 --- a/grove/helpers/parsing.py +++ b/grove/helpers/parsing.py @@ -3,6 +3,10 @@ """Provides helpers for parsing.""" +import json +import re +from typing import Any, Dict, List + from pydantic import ValidationError @@ -13,15 +17,119 @@ def validation_error(exc: ValidationError): :return: The exception as a string, including fields with validation errors. """ - prefix = exc.model.Config.env_prefix # type: ignore message = "Handler configuration is not valid" + try: + prefix = exc.model.Config.env_prefix # type: ignore + except AttributeError: + prefix = "" # Ensure the validation errors are included in the logged error message. for error in exc.errors(): - field = error["loc"][0].upper() # type: ignore + field = str(error["loc"][0]).upper() problem = error["msg"] # Add the environment variable prefix onto the field name. message = f"{message}, {prefix}{field} {problem}" return message + + +def quick_copy(value: Any): + """Performs a quick deep copy by marshalling and unmarshalling to JSON. + + This operation, although strange, is significantly quicker than copy.deepcopy(). + This has been moved into a helper to enable potential performance improvements in + future without code changes in processors being required. + + :param value: The value to perform a deep copy of. + + :return: The deep copy of the input value. + """ + return json.loads(json.dumps(value)) + + +def quote_aware_split(value: str, delimiter=".") -> List[str]: + """Splits a value by delimiter, returning a list. + + This function is quote aware, ensuring that splitting will not occur inside of a + value quoted with single-quotes. + + :param value: The value to split. + :param delimiter: The delimiter to split using. + + :return: A list of elements split from the input value. + """ + fields = [] + + for field in re.split(rf"({re.escape(delimiter)}|'.*?')", value): + # Drop empty and delimiter only fields. + field = field.strip(delimiter) + field = field.strip() + field = re.sub(r"^'(.*)'$", r"\1", field) + + if field: + fields.append(field) + + return fields + + +def update_path( + candidate: Dict[str, Any], + path: List[str], + value: Any, + replace: bool = False, +) -> Dict[str, Any]: + """Updates or deletes values under the specified path for the provided candidate. + + A path is a list of strings delimited string which express a location within the + candidate data. If the location is not nested, a single element list should be + provided. + + As an example of this, a path of `["A", "B", "C"]` expresses that the specified + value should be set under `{"A": {"B": {"C": value } } }` within the candidate + dictionary. + + This function recursively walks the provided candidate dictionary until the location + specified by the path has been located. Once found, the provided value will perform + on of the following operations: + + 1. By default, the provided value will be combined with the existing value. + 2. If `replace` is `True`, the existing value will be replaced with the new. + 3. If `None` is provided as the new value, the specified path will be deleted. + + :param candidate: The dictionary to update. + :param path: The path to the key to update, as a list of strings. + :param value: The value to assign to the destination path, or None to delete. + :param replace: Whether to replace the current value with the new value, or combine. + + :return: The updated dictionary. + """ + key = path.pop(0) + + # Set the value on the last recursion. + if len(path) < 1: + if value is None: + del candidate[key] + return candidate + + # If replace is set, don't combine the new value with the existing. + if replace: + candidate[key] = value + return candidate + + # By default, combine the new value with the existing value(s) - making sure to + # handle dictionaries as well as lists. + if key in candidate and type(candidate[key]) == list: + candidate[key].append(value) + else: + candidate = {**candidate, key: value} + + return candidate + + # If recursing, ensure the child we're trying to recurse into exists. + if key not in candidate: + candidate[key] = {} + + candidate[key] = update_path(candidate[key], path, value, replace=replace) + + return candidate diff --git a/grove/helpers/plugin.py b/grove/helpers/plugin.py index 34570df..3e39273 100644 --- a/grove/helpers/plugin.py +++ b/grove/helpers/plugin.py @@ -35,15 +35,16 @@ def lookup_handler(name: str, group: str) -> EntryPoint: ) -def load_handler(name: str, group: str) -> Any: +def load_handler(name: str, group: str, *args: Any, **kwargs: Any) -> Any: """Attempts to locate and load the requested plugin handler. This is a convenience method which wrappers the lookup operation, and performs the - load and instantiation required to hydrate a 'real' handler. + load and instantiation required to hydrate a 'real' handler. Any additional + arguments passed to load_handler are pass through to the handler during creation. :param name: The name of the handler to load (e.g. 'aws_ssm'). :param group: The group the handler belongs to (e.g. 'grove.outputs'). """ cls = lookup_handler(name, group).load() - return cls() + return cls(*args, **kwargs) diff --git a/grove/models.py b/grove/models.py index fb9ee1f..878383d 100644 --- a/grove/models.py +++ b/grove/models.py @@ -5,7 +5,8 @@ import base64 import binascii -from typing import Dict +from enum import Enum +from typing import Dict, List from pydantic import BaseModel, Extra, Field, root_validator, validator @@ -36,8 +37,36 @@ def decode(value: str, encoding: str) -> str: raise DataFormatException(f"Unknown encoding method '{encoding}'") +class ProcessorConfig(BaseModel, extra=Extra.allow): + """A processor configuration object. + + A processor configuration object represents information used by processors to + perform some set of operations on log entries. This base configuration object + is bare-bones as processors may define their own required configuration fields. + """ + + # Name is an arbitrary name administrators can provide to processors to enable + # better tracking and identification of processors. + name: str + + # Processor defines the processor which should be run. This must match the plugin + # entrypoint name. + processor: str + + +class OutputStream(str, Enum): + """Defines supported output 'streams'. + + This is used to allow routing of original / raw collected data differently to + post processed data. + """ + + raw = "raw" + processed = "processed" + + class ConnectorConfig(BaseModel, extra=Extra.allow): - """A connector configuration object. + """Defines the connector configuration structure. A configuration object represents information which Grove uses to call a given connector. All connectors must have at least a name, key, identity, and connector @@ -73,6 +102,19 @@ class ConnectorConfig(BaseModel, extra=Extra.allow): # from API endpoints which allow filtering records to return. operation: str = Field(OPERATION_DEFAULT) + # Processors allow processing of data during collection. + processors: List[ProcessorConfig] = Field([]) + + # Outputs allows specification of what type of data to output, and with what + # descriptor. By default, any processed logs will be output with a descriptor of + # 'processed', and raw logs with a descriptor of 'logs'. + outputs: Dict[str, OutputStream] = Field( + { + "logs": OutputStream.raw, + "processed": OutputStream.processed, + } + ) + @validator("key") def _validate_key_or_secret(cls, value, values, field): # noqa: B902 """Ensures that 'key' is set directly or a reference is present in 'secrets'. diff --git a/grove/outputs/__init__.py b/grove/outputs/__init__.py index 28da9a7..b4c9121 100644 --- a/grove/outputs/__init__.py +++ b/grove/outputs/__init__.py @@ -6,13 +6,44 @@ import abc import gzip import json -from typing import Any, Dict, List +import logging +from typing import Any, Dict, List, Optional + +from pydantic import BaseSettings, Extra, ValidationError from grove.constants import GROVE_METADATA_KEY -from grove.exceptions import DataFormatException +from grove.exceptions import ConfigurationException, DataFormatException +from grove.helpers import parsing class BaseOutput(abc.ABC): + """The basis for all Grove output handlers.""" + + class Configuration(BaseSettings, extra=Extra.allow): + """Defines the configuration directives required by all output handlers.""" + + pass + + def __init__(self): + """Implements core logic which applies to all handlers. + + This includes configuration of logging, and parsing of configuration. + """ + self.logger = logging.getLogger(__name__) + + # Wrap validation errors to keep them in the Grove exception hierarchy. + try: + self.config = self.Configuration() + except ValidationError as err: + raise ConfigurationException(parsing.validation_error(err)) + + def setup(self): + """Implements logic to setup any required clients, sockets, or connections. + + If not required for the given output handler, this may be a no-op. + """ + pass + @abc.abstractmethod def submit( self, @@ -20,6 +51,9 @@ def submit( connector: str, identity: str, operation: str, + part: int = 0, + suffix: Optional[str] = None, + descriptor: Optional[str] = None, ): """Implements logic require to write collected log data to the given backend. @@ -27,14 +61,24 @@ def submit( :param connector: Name of the connector which retrieved the data. :param identity: Identity the collected data was collect for. :param operation: Operation the collected logs are associated with. + :param part: Number indicating which part of the same log stream this file + contains data for. This is used to indicate that the logs are from the same + collection, but have been broken into smaller files for downstream + processing. + :param suffix: An optional suffix to allow propagation of file type information + or other relevant features. + :param descriptor: An optional and arbitrary descriptor associated with the + log data. This may be used by handlers for construction / specification of + file paths, URLs, or database tables. """ pass - def serialize(self, data: List[Any], metadata: Dict[str, Any]) -> bytes: - """Serialize data to a standard format (gzipped NDJSON). + def serialize(self, data: List[Any], metadata: Dict[str, Any] = {}) -> bytes: + """Implements serialization of log entries to a gzipped NDJSON. :param data: A list of log entries to serialize to JSON. - :param metadata: Metadata to append to JSON before serialisation. + :param metadata: Metadata to append to each log entry before serialization. If + not specified no metadata will be added. :return: Log data serialized as gzipped NDJSON (as bytes). @@ -46,14 +90,21 @@ def serialize(self, data: List[Any], metadata: Dict[str, Any]) -> bytes: # This is expensive but we can't just json.dumps into gzip.compress as that # will not yield NDJSON. for entry in data: - entry[GROVE_METADATA_KEY] = metadata + # Skip entry log entries. + if entry is None: + continue + + if metadata: + entry[GROVE_METADATA_KEY] = { + **metadata, + **entry.get(GROVE_METADATA_KEY, {}), + } # We don't want to silently drop and lose single records, so drop the entire # batch if there is bad data (which will trigger a retry next run). try: candidate.append(json.dumps(entry, separators=(",", ":"))) except TypeError as err: - message = f"Unable to serialize to JSON: {err}" - raise DataFormatException(message) + raise DataFormatException(f"Unable to serialize to JSON: {err}") return gzip.compress(bytes("\r\n".join(candidate), "utf-8")) diff --git a/grove/outputs/aws_s3.py b/grove/outputs/aws_s3.py index 4ff740f..2384f2a 100644 --- a/grove/outputs/aws_s3.py +++ b/grove/outputs/aws_s3.py @@ -4,60 +4,69 @@ """Grove AWS S3 output handler.""" import datetime -import logging import os from typing import Optional from boto3.session import Session from botocore.exceptions import BotoCoreError, ClientError -from pydantic import BaseSettings, Field, ValidationError +from pydantic import Field from grove.constants import DATESTAMP_FORMAT -from grove.exceptions import AccessException, ConfigurationException -from grove.helpers import parsing +from grove.exceptions import AccessException from grove.outputs import BaseOutput OBJECT_KEY = ( - "logs/{connector}/{identity}/{year}/{month}/{day}/" - "{operation}-{datestamp}.{part}.json.gz" + "{descriptor}{connector}/{identity}/{year}/{month}/{day}/" + "{operation}-{datestamp}.{part}{kind}" ) -class Configuration(BaseSettings): - """Defines environment variables used to configure the AWS S3 handler. - - This should also include any appropriate default values for fields which are not - required. - """ - - bucket: str = Field( - description="The name of the S3 bucket to output logs to.", - ) - assume_role_arn: Optional[str] = Field( - description="An optional AWS role to assume when authenticating with AWS.", - default=None, - ) - bucket_region: Optional[str] = Field( - description="The region that S3 the bucket exists in (default us-east-1)", - default=os.environ.get("AWS_REGION", "us-east-1"), - ) +class Handler(BaseOutput): + """This output handler allows Grove to write collected logs to an AWS S3 bucket.""" - class Config: - """Allow environment variable override of configuration fields. + class Configuration(BaseOutput.Configuration): + """Defines environment variables used to configure the AWS S3 handler. - This also enforce a prefix for all environment variables for this handler. As - an example the field `bucket` would be set using the environment variable - `GROVE_OUTPUT_AWS_S3_BUCKET`. + This should also include any appropriate default values for fields which are not + required. """ - env_prefix = "GROVE_OUTPUT_AWS_S3_" - case_insensitive = True - - -class Handler(BaseOutput): - """This output handler allows Grove to write collected logs to an AWS S3 bucket.""" - - def __init__(self): + bucket: str = Field( + description="The name of the S3 bucket to output logs to.", + ) + aws_access_key_id: Optional[str] = Field( + description="An optional AWS access key to use when authenticating", + default=os.environ.get("AWS_ACCESS_KEY_ID"), + ) + aws_secret_access_key: Optional[str] = Field( + description="An optional AWS secret key to use when authenticating", + default=os.environ.get("AWS_SECRET_ACCESS_KEY"), + ) + aws_session_token: Optional[str] = Field( + description="An optional AWS session token to use when authenticating", + default=os.environ.get("AWS_SESSION_TOKEN"), + ) + assume_role_arn: Optional[str] = Field( + description="An optional AWS role to assume when authenticating with AWS.", + default=None, + ) + bucket_region: Optional[str] = Field( + description="The region that S3 the bucket exists in (default us-east-1)", + default=os.environ.get("AWS_REGION", "us-east-1"), + ) + + class Config: + """Allow environment variable override of configuration fields. + + This also enforce a prefix for all environment variables for this handler. + As an example the field `bucket` would be set using the environment variable + `GROVE_OUTPUT_AWS_S3_BUCKET`. + """ + + env_prefix = "GROVE_OUTPUT_AWS_S3_" + case_insensitive = True + + def setup(self): """Sets up access to S3. This handler also attempt to assume a configured role in order to allow @@ -66,24 +75,34 @@ def __init__(self): :raises ConfigurationException: There was an issue with configuration. :raises AccessException: An issue occurred when accessing S3. """ - self.logger = logging.getLogger(__name__) - - # Wrap validation errors to keep them in the Grove exception hierarchy. - try: - self.config = Configuration() # type: ignore - except ValidationError as err: - raise ConfigurationException(parsing.validation_error(err)) - # Explicit calls to session are mostly used to allow mocks during testing. session = Session() + # Only add in optional arguments if configured. + client_kwargs = {} + + if self.config.aws_access_key_id: + client_kwargs["aws_access_key_id"] = self.config.aws_access_key_id + client_kwargs["aws_secret_access_key"] = self.config.aws_secret_access_key + + if self.config.aws_session_token: + client_kwargs["aws_session_token"] = self.config.aws_session_token + # If a role was specified, ensure we assume it and use STS tokens to interact # with S3. try: if not self.config.assume_role_arn: - self.s3 = session.client("s3", region_name=self.config.bucket_region) + self.s3 = session.client( + "s3", + region_name=self.config.bucket_region, + **client_kwargs, + ) else: - sts = session.client("sts") + sts = session.client( + "sts", + region_name=self.config.bucket_region, + **client_kwargs, + ) role = sts.assume_role( RoleArn=self.config.assume_role_arn, RoleSessionName="GroveOutputWriter", @@ -105,6 +124,8 @@ def submit( identity: str, operation: str, part: int = 0, + kind: Optional[str] = ".json.gz", + descriptor: Optional[str] = "logs/", ): """Persists captured data to an S3 compatible object store. @@ -116,9 +137,16 @@ def submit( contains data for. This is used to indicate that the logs are from the same collection, but have been broken into smaller files for downstream processing. + :param kind: An optional file suffix to use for objects written. + :param descriptor: An optional path to append to the beginning of the output + S3 key. :raises AccessException: An issue occurred when accessing S3. """ + # Append a trailing slash to the descriptor if set - to form a path. + if descriptor and not descriptor.endswith("/"): + descriptor = f"{descriptor}/" + try: datestamp = datetime.datetime.utcnow() self.s3.put_object( @@ -133,6 +161,8 @@ def submit( month=datestamp.strftime("%m"), day=datestamp.strftime("%d"), datestamp=datestamp.strftime(DATESTAMP_FORMAT), + descriptor=descriptor, + kind=kind, ), ) except ClientError as err: diff --git a/grove/outputs/local_file.py b/grove/outputs/local_file.py index 276c3ed..f6a3e7c 100644 --- a/grove/outputs/local_file.py +++ b/grove/outputs/local_file.py @@ -4,63 +4,52 @@ """Grove local file path output handler.""" import datetime -import logging import os +from typing import Optional -from pydantic import BaseSettings, Field, ValidationError +from pydantic import Field from grove.constants import DATESTAMP_FORMAT -from grove.exceptions import AccessException, ConfigurationException -from grove.helpers import parsing +from grove.exceptions import AccessException from grove.outputs import BaseOutput OBJECT_PATH = ( - "logs/{connector}/{identity}/{year}/{month}/{day}/" - "{operation}-{datestamp}.{part}.json.gz" + "{descriptor}{connector}/{identity}/{year}/{month}/{day}/" + "{operation}-{datestamp}.{part}{kind}" ) -class Configuration(BaseSettings): - """Defines environment variables used to configure the local file handler. - - This should also include any appropriate default values for fields which are not - required. - """ +class Handler(BaseOutput): + class Configuration(BaseOutput.Configuration): + """Defines environment variables used to configure the local file handler. - path: str = Field( - description="The path to the directory to write collected logs to.", - ) + This should also include any appropriate default values for fields which are not + required. + """ - class Config: - """Allow environment variable override of configuration fields. + path: str = Field( + description="The path to the directory to write collected logs to.", + ) - This also enforce a prefix for all environment variables for this handler. As - an example the field `path` would be set using the environment variable - `GROVE_OUTPUT_LOCAL_FILE_PATH`. - """ + class Config: + """Allow environment variable override of configuration fields. - env_prefix = "GROVE_OUTPUT_LOCAL_FILE_" - case_insensitive = True + This also enforce a prefix for all environment variables for this handler. + As an example the field `path` would be set using the environment variable + `GROVE_OUTPUT_LOCAL_FILE_PATH`. + """ + env_prefix = "GROVE_OUTPUT_LOCAL_FILE_" + case_insensitive = True -class Handler(BaseOutput): - def __init__(self): + def setup(self): """Set up access to local filesystem path. This also checks that an output directory is configured, and it is initially accessible and writable. - :raises ConfigurationException: There was an issue with output configuration. - :raises AccessException: There was an issue accessing to the specified file path. + :raises AccessException: There was an issue accessing to the provided file path. """ - self.logger = logging.getLogger(__name__) - - # Wrap validation errors to keep them in the Grove exception hierarchy. - try: - self.config = Configuration() # type: ignore - except ValidationError as err: - raise ConfigurationException(parsing.validation_error(err)) - # Perform a spot check to see if the directory is writable now. Although this # can change, we'd like to bail before we collect any data if it's a simple # permissions related misconfiguration. @@ -81,6 +70,8 @@ def submit( identity: str, operation: str, part: int = 0, + kind: Optional[str] = ".json.gz", + descriptor: Optional[str] = "logs/", ): """Persists captured data to a local file path. @@ -92,9 +83,16 @@ def submit( contains data for. This is used to indicate that the logs are from the same collection, but have been broken into smaller files for downstream processing. + :param kind: An optional file suffix to use for files written. + :param descriptor: An optional path to append to the beginning of the output + file path. :raises AccessException: An issue occurred when writing data. """ + # Append a trailing slash to the descriptor if set - to form a path. + if descriptor and not descriptor.endswith("/"): + descriptor = f"{descriptor}/" + # Each log file is output under a particular hierarchy to assist with sharding # and ingestion / finding of log data. datestamp = datetime.datetime.utcnow() @@ -108,6 +106,8 @@ def submit( month=datestamp.strftime("%m"), day=datestamp.strftime("%d"), datestamp=datestamp.strftime(DATESTAMP_FORMAT), + kind=kind, + descriptor=descriptor, ) # Quick and dirty directory traversal check. diff --git a/grove/outputs/local_stdout.py b/grove/outputs/local_stdout.py index dee0d3d..49142ba 100644 --- a/grove/outputs/local_stdout.py +++ b/grove/outputs/local_stdout.py @@ -5,7 +5,7 @@ import datetime import json -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from grove.constants import DATESTAMP_FORMAT, GROVE_METADATA_KEY from grove.exceptions import DataFormatException @@ -20,6 +20,8 @@ def submit( identity: str, operation: str, part: int = 0, + kind: Optional[str] = "json", + descriptor: Optional[str] = "raw", ): """Print captured data to stdout. @@ -31,6 +33,9 @@ def submit( contains data for. This is used to indicate that the logs are from the same collection, but have been broken into smaller files for downstream processing. + :param kind: The format of the data being output. + :param descriptor: An arbitrary descriptor to identify the data being output. + """ datestamp = datetime.datetime.utcnow() @@ -38,6 +43,8 @@ def submit( json.dumps( { "part": part, + "kind": kind, + "descriptor": descriptor, "connector": connector, "identity": identity, "operation": operation, @@ -48,11 +55,12 @@ def submit( flush=True, ) - def serialize(self, data: List[Any], metadata: Dict[str, Any]) -> bytes: + def serialize(self, data: List[Any], metadata: Dict[str, Any] = {}) -> bytes: """Serialize data to a standard format (NDJSON). :param data: A list of log entries to serialize to JSON. - :param metadata: Metadata to append to JSON before serialisation. + :param metadata: Metadata to append to each log entry before serialization. If + not specified no metadata will be added. :return: Log data serialized as NDJSON. diff --git a/grove/processors/__init__.py b/grove/processors/__init__.py new file mode 100644 index 0000000..1309148 --- /dev/null +++ b/grove/processors/__init__.py @@ -0,0 +1,57 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Provides processors for collected log entries.""" + +import abc +import logging +from typing import Any, Dict, List + +from pydantic import Extra, ValidationError + +from grove.exceptions import ConfigurationException +from grove.helpers import parsing +from grove.models import ProcessorConfig + + +class BaseProcessor(abc.ABC): + """Provides an abstract base processor which all processors must inherit from.""" + + class Configuration(ProcessorConfig, extra=Extra.forbid): + """Defines the required configuration and validators for the processor.""" + + pass + + def __init__(self, config: Dict[str, Any]): + """Sets up a Grove processor. + + :param config: The configuration document for this processor, as a dict. + """ + self.logger = logging.getLogger(__name__) + + # Load and validate configuration. We perform a bit of a strange operation here + # but our caller needs to have loaded the configuration into a ProcessorConfig + # already, but we want to re-validate it here. As a result, we convert to a dict + # and back again. + try: + self.configuration = self.Configuration(**config.dict()) + except ValidationError as err: + raise ConfigurationException( + f"Processor configuration is invalid. {parsing.validation_error(err)}" + ) + + @abc.abstractmethod + def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: + """Performs a set of processes against a log entry. + + :param entry: A collected log entry. + + :returns: The processed log entry in a list. If only a single entry is required + the list should contain a single element. If the log entry is to be dropped, + an empty list should be used. + """ + pass + + def finalize(self): + """Performs a final set of operations after logs have been saved.""" + return diff --git a/grove/processors/extract_paths.py b/grove/processors/extract_paths.py new file mode 100644 index 0000000..726b9ec --- /dev/null +++ b/grove/processors/extract_paths.py @@ -0,0 +1,97 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Grove processor to extract and map fields using JMESPaths. + +This processor is intended to be used to transform raw log entries into a common schema. +This is especially useful for ensuring that all collected log entries from differing +upstream vendors are in a consistent format - whether industry standard, or bespoke. +""" + +import json +from typing import Any, Dict, List, Optional + +import jmespath +from pydantic import BaseModel, Extra, validator + +from grove.helpers import parsing +from grove.models import ProcessorConfig +from grove.processors import BaseProcessor + + +class Mapping(BaseModel, extra=Extra.forbid): + """Expresses the configuration fields used to specify path mapping.""" + + # Destination specifies where to write extracted or specified values into. This + # can be a nested path, with subsequent dimensions specified with dots (`.`). + destination: str + + # Sources defines a list of JMESPaths to map into the destination. If multiple + # are provided, the sources are processed in order with the first match winning. + sources: List[str] = [] + + # Static allows a static field to be written into the destination, rather than + # extraction from the source. This field is incompatible with sources. + static: Optional[str] + + @validator("static") + def static_or_sources(cls, value, values): + """Ensures that either sources or static is set, not both.""" + if value and len(values.get("sources")) > 0: + raise ValueError("Either sources or static should be set, not both.") + + return value + + +class Handler(BaseProcessor): + """Extract and map fields using JMESPaths.""" + + class Configuration(ProcessorConfig, extra=Extra.forbid): + """Expresses the configuration and associated validators for the processor.""" + + # Remap the original event as a string under the provided path. If not set, any + # field not mapped will be dropped. + raw: Optional[str] + + # Defines the field mapping. + fields: List[Mapping] + + def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: + """Attempt to extract and map fields from the log entry. + + :param entry: A collected log entry. + + :return: The processed log entry with fields mapped, as a list. + """ + result: Dict[str, Any] = {} + + # Map the entire log entry under the given path - if configured. + if self.configuration.raw: + result = parsing.update_path( + result, + parsing.quote_aware_split(self.configuration.raw), + json.dumps(entry, separators=(",", ":")), + ) + + for field in self.configuration.fields: + value = field.static + destination = parsing.quote_aware_split(field.destination) + + # If a static value is defined it should be used over any source fields. + if not value: + # Mappings may contain multiple sources to attempt to map. These are + # evaluated from the first entry to the last, with the first match + # winning. + for source in field.sources: + value = jmespath.search(source, entry) + if value: + break + + # Combine the extracted value with the data nested under the same path - or + # create the path if not present. + result = parsing.update_path(result, destination, value) + + # Return the newly processed entry. A list is always used, even if only a single + # element is returned, to allow support for dropping log entries, or splitting a + # single log entry into multiple. + return [result] diff --git a/grove/processors/filter_paths.py b/grove/processors/filter_paths.py new file mode 100644 index 0000000..afaa06f --- /dev/null +++ b/grove/processors/filter_paths.py @@ -0,0 +1,44 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Grove processor to filter (delete) fields from log entries based on provided paths. + +This processor is intended to allow removal of superfluous or duplicated data from +log entries. This may be used after a processing stage to remove the original source +data, or used to prune down a log entry from a particularly verbose vendor. +""" + +from typing import Any, Dict, List + +from pydantic import Extra + +from grove.helpers import parsing +from grove.models import ProcessorConfig +from grove.processors import BaseProcessor + + +class Handler(BaseProcessor): + """Filter (delete) fields from log entries based on provided paths.""" + + class Configuration(ProcessorConfig, extra=Extra.forbid): + """Expresses the configuration and associated validators for the processor.""" + + # Source defines a list of paths to field to drop (delete). These should be + # defined as a JMESPaths. + sources: List[str] + + def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: + """Attempt to drop a configured field from the log entry. + + :param entry: A collected log entry. + + :return: The processed log entry, with fields dropped. + """ + for source in self.configuration.sources: + entry = parsing.update_path( + entry, + parsing.quote_aware_split(source), + None, + ) + + return [entry] diff --git a/grove/processors/split_path.py b/grove/processors/split_path.py new file mode 100644 index 0000000..ad8fd4c --- /dev/null +++ b/grove/processors/split_path.py @@ -0,0 +1,55 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Grove processor to split a log entry into N log entries by the specified JMESPath. + +This processor is intended to allow "fanning-out" a single log entry which contains +several related operations into distinct log entries per item. The remainder of the +log entry outside of the split path will not be modified. +""" + +from typing import Any, Dict, List + +import jmespath +from pydantic import Extra + +from grove.helpers import parsing +from grove.models import ProcessorConfig +from grove.processors import BaseProcessor + + +class Handler(BaseProcessor): + """Split a log entry into N log entries by the specified JMESPath.""" + + class Configuration(ProcessorConfig, extra=Extra.forbid): + """Expresses the configuration and associated validators for the processor.""" + + # Source defines the path to split the log entry by. This should be defined as a + # JMESPath. The field referenced by this path should be a list. + source: str + + def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: + """Attempt to extract and map fields from the log entry. + + :param entry: A collected log entry. + + :return: The processed log entry. + """ + # In this instance we WANT to mutate the copy outside of the processor. + processed = [] + children = jmespath.search(self.configuration.source, entry) + + if len(children) <= 1: + return [entry] + + for child in children: + processed.append( + parsing.update_path( + parsing.quick_copy(entry), + parsing.quote_aware_split(self.configuration.source), + [child], + replace=True, + ) + ) + + return processed diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d527125 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,183 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "grove" +readme = "README.md" +description = "A Software as a Service (SaaS) log collection framework." +requires-python = ">=3.9" +dynamic = ["version"] +authors = [{name = "HashiCorp Security (TDR)"}] +license = {text = "MPL-2.0"} +classifiers = [ + "Programming Language :: Python :: 3.9", + "Natural Language :: English", +] +dependencies = [ + "urllib3<2.0", + "aws-lambda-powertools>=2.0,<3.0", + "boto3>=1.26,<2.0", + "requests>=2.28,<3.0", + "google-api-python-client>=2.68,<3.0", + "simple-salesforce>=1.12,<2.0", + "twilio>=7.15,<8.0", + "pydantic>=1.10,<2.0", + "jmespath>=1.0.0,<2.0", +] + +[project.optional-dependencies] +tests = [ + "black", + "coverage", + "ruff", + "types-setuptools", + "isort", + "mypy", + "pip-tools", + "mock", + "pytest", + "pytest-cov", + "responses", + "tox", + "sphinx", + "furo", + "moto[s3,ssm]", + "types-requests", +] + +[tool.setuptools.packages.find] +include = ["grove*"] + +[tool.setuptools.dynamic] +version = {attr = "grove.__about__.__version__"} + +[project.scripts] +grove = "grove.entrypoints.local_process:entrypoint" + +[project.entry-points."grove.entrypoints"] +aws_lambda = "grove.entrypoints.aws_lambda:entrypoint" +local_process = "grove.entrypoints.local_process:entrypoint" + +[project.entry-points."grove.connectors"] +atlassian_audit_events = "grove.connectors.atlassian.audit_events:Connector" +github_audit_log = "grove.connectors.github.audit_log:Connector" +gsuite_activities = "grove.connectors.gsuite.activities:Connector" +local_heartbeat = "grove.connectors.local.heartbeat:Connector" +gsuite_alerts = "grove.connectors.gsuite.alerts:Connector" +okta_system_log = "grove.connectors.okta.system_log:Connector" +onepassword_events_itemusages = "grove.connectors.onepassword.events_itemusages:Connector" +onepassword_events_signinattempts = "grove.connectors.onepassword.events_signinattempts:Connector" +onepassword_events_audit = "grove.connectors.onepassword.events_audit:Connector" +pagerduty_audit_records = "grove.connectors.pagerduty.audit_records:Connector" +sf_event_log = "grove.connectors.sf.event_log:Connector" +sfmc_audit_events = "grove.connectors.sfmc.audit_events:Connector" +sfmc_security_events = "grove.connectors.sfmc.security_events:Connector" +slack_audit_logs = "grove.connectors.slack.audit_logs:Connector" +tfc_audit_trails = "grove.connectors.tfc.audit_trails:Connector" +torq_activity_logs = "grove.connectors.torq.activity_logs:Connector" +torq_audit_logs = "grove.connectors.torq.audit_logs:Connector" +twilio_monitor_events = "grove.connectors.twilio.monitor_events:Connector" +twilio_messages = "grove.connectors.twilio.messages:Connector" +workday_activity_logging = "grove.connectors.workday.activity_logging:Connector" +zoom_activities = "grove.connectors.zoom.activities:Connector" +zoom_operationlogs = "grove.connectors.zoom.operationlogs:Connector" +oomnitza_activities = "grove.connectors.oomnitza.activities:Connector" + +[project.entry-points."grove.caches"] +aws_dynamodb = "grove.caches.aws_dynamodb:Handler" +local_memory = "grove.caches.local_memory:Handler" + +[project.entry-points."grove.outputs"] +aws_s3 = "grove.outputs.aws_s3:Handler" +local_file = "grove.outputs.local_file:Handler" +local_stdout = "grove.outputs.local_stdout:Handler" + +[project.entry-points."grove.configs"] +aws_ssm = "grove.configs.aws_ssm:Handler" +local_file = "grove.configs.local_file:Handler" + +[project.entry-points."grove.secrets"] +aws_ssm = "grove.secrets.aws_ssm:Handler" +hashicorp_vault = "grove.secrets.hashicorp_vault:Handler" + +[project.entry-points."grove.processors"] +extract_paths = "grove.processors.extract_paths:Handler" +filter_paths = "grove.processors.filter_paths:Handler" +split_path = "grove.processors.split_path:Handler" + +[tool.mypy] +files = [ + "./grove/**/*.py", + "./tests/**/*.py" +] +disable_error_code = "attr-defined" +allow_redefinition = false +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_calls = false +ignore_errors = false +ignore_missing_imports = true +implicit_reexport = false +local_partial_types = true +strict_optional = true +strict_equality = true +no_implicit_optional = true +warn_no_return = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unreachable = true + +[tool.isort] +multi_line_output = 3 +profile = "black" + +[tool.pytest.ini_options] +junit_family = "xunit2" +norecursedirs = ".*" +self-contained-html = true +testpaths = [ + "tests" +] +addopts = """ + --strict + --tb=auto + --cov=grove + --cov-report=term-missing:skip-covered + --cov-branch + -p no:doctest + -p no:warnings + -s +""" + +[tool.tox] +legacy_tox_ini = """ + [tox] + envlist = linters,py3 + + [testenv] + pip_version = pip + extras = tests + commands = pytest -c pyproject.toml + srcs = grove + + [testenv:linters] + basepython = python3 + usedevelop = true + commands = + {[testenv:ruff]commands} + {[testenv:mypy]commands} + + [testenv:ruff] + basepython = python3 + skip_install = true + commands = + ruff check {[testenv]srcs} + + [testenv:mypy] + basepython3 = python3 + skip_install = true + commands = + - mypy --config-file pyproject.toml {[testenv]srcs} +""" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 91be562..0000000 --- a/setup.cfg +++ /dev/null @@ -1,160 +0,0 @@ -[metadata] -name = grove -description = A Software as a Service (SaaS) log collection framework. -versioning = build-id -classifiers = - Programming Language :: Python :: 3.9 - Natural Language :: English - -[options] -python_requires = >= 3.9 -install_requires = - urllib3<2.0 - aws-lambda-powertools>=2.0,<3.0 - boto3>=1.26,<2.0 - requests>=2.28,<3.0 - google-api-python-client>=2.68,<3.0 - simple-salesforce>=1.12,<2.0 - twilio>=7.15,<8.0 - pydantic>=1.10,<2.0 - -[options.extras_require] -tests = - black - coverage - flake8 - flake8-black - flake8-blind-except - flake8-bugbear - flake8-builtins - flake8-comprehensions - flake8-docstrings - flake8-isort - flake8_tuple - types-bleach - types-requests - types-setuptools - isort - mypy - pip-tools - mock - moto[ssm,s3] - pytest - pytest-cov - responses - tox - sphinx - furo - -; flake8 for linting. -[flake8] -max-complexity = 10 -import-order-style = edited -application-import-names = grove -max-line-length = 88 -select = B,C,D,E,F,P,T4,W,B9 -exclude = - *.egg-info, - *.pyc, - .cache, - .coverage.*, - .gradle, - .tox, - build, - dist, - htmlcov.* -ignore = - # Exception chaining is automatic inside of except blocks. - B904, - # Don't prefer !r / !s in string interpolation. - B028, B907, - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, - # Ignore too many leading '#' for block comment - E266, - # Ignore Line too long (82 > 79) in favor of bugbear - E501, - # Ignore Line break before binary operator (not PEP8) - W503, - # Ignore 1 blank line required before/after class docstring and summary - D203,D204,D205, - # Ignore multi-line docstring summary should start at the first line - D212 - # Ignore First line should end with a period - D400 - # Ignore First line should be in imperative mood - D401 - # Ignore missing module, class, public method, function, - # public package, magic method, and __init__ docstrings. - # ...probably want to enable these at some point... - D100,D101,D102,D103,D104,D105,D107 - -; mypy for type checking. -[mypy] -files = ./grove/**/*.py,./tests/**/*.py -allow_redefinition = False -check_untyped_defs = True -disallow_any_generics = True -disallow_untyped_calls = False -ignore_errors = False -ignore_missing_imports = True -implicit_reexport = False -local_partial_types = True -strict_optional = True -strict_equality = True -no_implicit_optional = True -warn_no_return = True -warn_unused_ignores = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unreachable = True - -; isort for import sorting. -[tool:isort] -multi_line_output = 3 -profile = black - -; pytest for Testing. -[tool:pytest] -junit_family = xunit2 -norecursedirs =.* -self-contained-html = true -testpaths = tests -addopts = - --strict - --tb=auto - --cov=grove - --cov-report=term-missing:skip-covered - --cov-branch - -p no:doctest - -p no:warnings - -s - -; Tox for linter and test execution. -[tox:tox] -envlist = linters,py3 - -[testenv] -pip_version = pip -extras = tests -commands = pytest -c setup.cfg -srcs = setup.py grove - -[testenv:linters] -basepython = python3 -usedevelop = true -commands = - {[testenv:flake8]commands} - {[testenv:mypy]commands} - -[testenv:flake8] -basepython = python3 -skip_install = true -commands = - flake8 --config setup.cfg {[testenv]srcs} - -[testenv:mypy] -basepython3 = python3 -skip_install = true -commands = - - mypy --config-file setup.cfg {[testenv]srcs} diff --git a/setup.py b/setup.py deleted file mode 100644 index 1e6d794..0000000 --- a/setup.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -"""Minimal setup for grove.""" -import os - -from setuptools import find_packages, setup - -# These will be overwritten by the values from __about__.py -__version__ = "0.0.0" -__author__ = "Not Defined" - -path = os.path.dirname(os.path.abspath(__file__)) -exec(open(os.path.join(path, "grove/__about__.py")).read()) # noqa: S102 - -# Load the long description for PyPi. -long_description = open(os.path.join(path, "README.md")).read() - -setup( - name="grove", - version=__version__, - author=__author__, - packages=find_packages(include=["grove", "grove.*"]), - long_description=long_description, - long_description_content_type="text/markdown", - entry_points={ - "console_scripts": [ - "grove = grove.entrypoints.local_process:entrypoint", - ], - "grove.entrypoints": [ - "aws_lambda = grove.entrypoints.aws_lambda:entrypoint", - "local_process = grove.entrypoints.local_process:entrypoint", - ], - "grove.connectors": [ - "atlassian_audit_events = grove.connectors.atlassian.audit_events:Connector", - "github_audit_log = grove.connectors.github.audit_log:Connector", - "gsuite_activities = grove.connectors.gsuite.activities:Connector", - "local_heartbeat = grove.connectors.local.heartbeat:Connector", - "gsuite_alerts = grove.connectors.gsuite.alerts:Connector", - "okta_system_log = grove.connectors.okta.system_log:Connector", - "onepassword_events_itemusages = grove.connectors.onepassword.events_itemusages:Connector", # noqa: B950 - "onepassword_events_signinattempts = grove.connectors.onepassword.events_signinattempts:Connector", # noqa: B950 - "onepassword_events_audit = grove.connectors.onepassword.events_audit:Connector", # noqa: B950 - "pagerduty_audit_records = grove.connectors.pagerduty.audit_records:Connector", - "sf_event_log = grove.connectors.sf.event_log:Connector", - "sfmc_audit_events = grove.connectors.sfmc.audit_events:Connector", - "sfmc_security_events = grove.connectors.sfmc.security_events:Connector", - "slack_audit_logs = grove.connectors.slack.audit_logs:Connector", - "tfc_audit_trails = grove.connectors.tfc.audit_trails:Connector", - "torq_activity_logs = grove.connectors.torq.activity_logs:Connector", - "torq_audit_logs = grove.connectors.torq.audit_logs:Connector", - "twilio_monitor_events = grove.connectors.twilio.monitor_events:Connector", - "twilio_messages = grove.connectors.twilio.messages:Connector", - "workday_activity_logging = grove.connectors.workday.activity_logging:Connector", - "zoom_activities = grove.connectors.zoom.activities:Connector", - "zoom_operationlogs = grove.connectors.zoom.operationlogs:Connector", - "oomnitza_activities = grove.connectors.oomnitza.activities:Connector", - ], - "grove.caches": [ - "aws_dynamodb = grove.caches.aws_dynamodb:Handler", - "local_memory = grove.caches.local_memory:Handler", - ], - "grove.outputs": [ - "aws_s3 = grove.outputs.aws_s3:Handler", - "local_file = grove.outputs.local_file:Handler", - "local_stdout = grove.outputs.local_stdout:Handler", - ], - "grove.configs": [ - "aws_ssm = grove.configs.aws_ssm:Handler", - "local_file = grove.configs.local_file:Handler", - ], - "grove.secrets": [ - "aws_ssm = grove.secrets.aws_ssm:Handler", - "hashicorp_vault = grove.secrets.hashicorp_vault:Handler", - ], - }, -) diff --git a/templates/code/{{ cookiecutter.project_name }}/{{ cookiecutter.project_slug }}/example_logs.py b/templates/code/{{ cookiecutter.project_name }}/{{ cookiecutter.project_slug }}/example_logs.py index f7675b4..6ab2f35 100644 --- a/templates/code/{{ cookiecutter.project_name }}/{{ cookiecutter.project_slug }}/example_logs.py +++ b/templates/code/{{ cookiecutter.project_name }}/{{ cookiecutter.project_slug }}/example_logs.py @@ -20,7 +20,7 @@ def optional_setting(self): :return: The "optional_setting" component of the connector configuration. """ try: - return self.configuration.optional_setting # type: ignore + return self.configuration.optional_setting except AttributeError: return "Some Default value" @@ -50,6 +50,6 @@ def collect(self): self.save(log.entries) # Break out of loop when there are no more pages. - cursor = log.cursor + cursor = log.cursor # type: ignore if cursor is None: break diff --git a/templates/deployment/local-quick-start/connectors/local_heartbeat.json b/templates/deployment/local-quick-start/connectors/local_heartbeat.json index 0a549aa..b09983b 100644 --- a/templates/deployment/local-quick-start/connectors/local_heartbeat.json +++ b/templates/deployment/local-quick-start/connectors/local_heartbeat.json @@ -4,5 +4,8 @@ "connector": "local_heartbeat", "key": "", "interval": 5, - "count": 5 + "count": 5, + "outputs": { + "logs": "raw" + } } \ No newline at end of file diff --git a/tests/fixtures/gsuite/activities/001.json b/tests/fixtures/gsuite/activities/001.json index f9302d9..abb25d2 100644 --- a/tests/fixtures/gsuite/activities/001.json +++ b/tests/fixtures/gsuite/activities/001.json @@ -19,11 +19,24 @@ "name": "ADD_GROUP_MEMBER", "parameters": [{ "name": "USER_EMAIL", - "value": "example@hashicorp.com" + "value": "added@hashicorp.com" }, { "name": "GROUP_EMAIL", - "value": "example@hashicorp.com" + "value": "added@hashicorp.com" + } + ] + }, + { + "type": "GROUP_SETTINGS", + "name": "REMOVE_GROUP_MEMBER", + "parameters": [{ + "name": "USER_EMAIL", + "value": "replaced@hashicorp.com" + }, + { + "name": "GROUP_EMAIL", + "value": "replaced@hashicorp.com" } ] }] diff --git a/tests/mocks/__init__.py b/tests/mocks/__init__.py index d355fa9..21e44d7 100644 --- a/tests/mocks/__init__.py +++ b/tests/mocks/__init__.py @@ -7,13 +7,17 @@ from grove.caches import local_memory from grove.constants import PLUGIN_GROUP_CACHE, PLUGIN_GROUP_OUTPUT +from grove.helpers import plugin from tests.mocks import output # noqa: F401 -def load_handler(_: str, group: str) -> Any: +def load_handler(name: str, group: str, *args, **kwargs) -> Any: """Wraps handler loading to load predefined mocks for a given group.""" if group == PLUGIN_GROUP_OUTPUT: return output.TestHandler() if group == PLUGIN_GROUP_CACHE: return local_memory.Handler() + + cls = plugin.lookup_handler(name, group).load() + return cls(*args, **kwargs) diff --git a/tests/mocks/output.py b/tests/mocks/output.py index fb57fdd..7cb96b2 100644 --- a/tests/mocks/output.py +++ b/tests/mocks/output.py @@ -7,6 +7,7 @@ class TestHandler(BaseOutput): - def submit(self, *arg, **kwargs): + __test__ = False + + def submit(self, *args, **kwargs): """Does nothing, successfully.""" - return diff --git a/tests/test_connectors_atlassian_audit_events.py b/tests/test_connectors_atlassian_audit_events.py index 3b9a9b4..d20bc0a 100644 --- a/tests/test_connectors_atlassian_audit_events.py +++ b/tests/test_connectors_atlassian_audit_events.py @@ -69,7 +69,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 31) + self.assertEqual(self.connector._saved["logs"], 31) self.assertEqual(self.connector.pointer, "2022-05-12T19:13:13Z") @responses.activate @@ -90,5 +90,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2022-05-12T19:13:13Z") diff --git a/tests/test_connectors_deduplicate.py b/tests/test_connectors_deduplicate.py index ca9aafd..ed5103a 100644 --- a/tests/test_connectors_deduplicate.py +++ b/tests/test_connectors_deduplicate.py @@ -61,7 +61,7 @@ def test_deduplication_chronological(self): # Run a full collection first and ensure all is as we expect. first_collection.run() - self.assertEqual(first_collection._saved, 7) + self.assertEqual(first_collection._saved["logs"], 7) self.assertEqual(first_collection.pointer, "7") # Perform a collection with the latest pointer, and ensure no new records are @@ -71,7 +71,7 @@ def test_deduplication_chronological(self): # In-memory cache does not support locking as it's only intended for local # "one-shot" execution, and development use. As a result, we have to currently - # clone the state of one cache to the other to simulate this. + # alias one to the other to simulate this. # # TODO: Remove the need for this, as it's going to cause confusion in future. second_collection._cache._data = first_collection._cache._data @@ -81,7 +81,7 @@ def test_deduplication_chronological(self): ) second_collection.run() - self.assertEqual(second_collection._saved, 0) + self.assertEqual(second_collection._saved["logs"], 0) self.assertEqual(second_collection.pointer, "7") @responses.activate @@ -101,6 +101,8 @@ def test_deduplication_reverse_chronological(self): # Hot patch the connector to work in reverse chronological order. first_collection = Connector(config=config, context=context) + + # This is very naughty. first_collection.LOG_ORDER = constants.REVERSE_CHRONOLOGICAL # Load all simulated responses in order. @@ -113,7 +115,7 @@ def test_deduplication_reverse_chronological(self): # Run a full collection first and ensure all is as we expect. first_collection.run() - self.assertEqual(first_collection._saved, 7) + self.assertEqual(first_collection._saved["logs"], 7) self.assertEqual(first_collection.pointer, "7") # Perform a collection with the latest pointer, and ensure that only new records @@ -122,7 +124,7 @@ def test_deduplication_reverse_chronological(self): # In-memory cache does not support locking as it's only intended for local # "one-shot" execution, and development use. As a result, we have to currently - # clone the state of one cache to the other to simulate this. + # alias the state of one cache to the other to simulate this. # # TODO: Remove the need for this, as it's going to cause confusion in future. second_collection._cache._data = first_collection._cache._data @@ -132,5 +134,5 @@ def test_deduplication_reverse_chronological(self): ) second_collection.run() - self.assertEqual(second_collection._saved, 1) + self.assertEqual(second_collection._saved["logs"], 1) self.assertEqual(second_collection.pointer, "7") diff --git a/tests/test_connectors_github_audit.py b/tests/test_connectors_github_audit.py index 78a49ea..393bd32 100644 --- a/tests/test_connectors_github_audit.py +++ b/tests/test_connectors_github_audit.py @@ -89,5 +89,5 @@ def test_collect_no_pagination(self): # Ensure the correct number of value are returned, and the pointer properly set. self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "1625045793361") diff --git a/tests/test_connectors_gsuite_activities.py b/tests/test_connectors_gsuite_activities.py index a23f139..8e838a7 100644 --- a/tests/test_connectors_gsuite_activities.py +++ b/tests/test_connectors_gsuite_activities.py @@ -65,5 +65,5 @@ def test_collect_pagination(self, mock_transport, mock_request, mock_auth): ) self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2021-10-27T23:59:31.657Z") diff --git a/tests/test_connectors_gsuite_alerts.py b/tests/test_connectors_gsuite_alerts.py index e8a7ff5..7f5e8c1 100644 --- a/tests/test_connectors_gsuite_alerts.py +++ b/tests/test_connectors_gsuite_alerts.py @@ -63,5 +63,5 @@ def test_collect_pagination(self, mock_transport, mock_request, mock_auth): ], ) self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2021-04-03T14:05:39.950458Z") diff --git a/tests/test_connectors_okta_system_log.py b/tests/test_connectors_okta_system_log.py index 33b2936..85b397d 100644 --- a/tests/test_connectors_okta_system_log.py +++ b/tests/test_connectors_okta_system_log.py @@ -68,7 +68,7 @@ def test_collect_no_pagination(self): ) # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2021-06-24T00:04:08.123Z") @responses.activate diff --git a/tests/test_connectors_onepassword_events_audit.py b/tests/test_connectors_onepassword_events_audit.py index c7ddb85..ce72b03 100644 --- a/tests/test_connectors_onepassword_events_audit.py +++ b/tests/test_connectors_onepassword_events_audit.py @@ -9,6 +9,7 @@ from unittest.mock import patch import responses + from grove.connectors.onepassword.events_audit import Connector from grove.models import ConnectorConfig from tests import mocks @@ -89,7 +90,7 @@ def test_collect_no_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 3) + self.assertEqual(self.connector._saved["logs"], 3) self.assertEqual(self.connector.pointer, "2023-03-15T16:50:50-03:00") @responses.activate @@ -129,5 +130,5 @@ def test_collect_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2023-03-15T16:33:50-03:00") diff --git a/tests/test_connectors_onepassword_events_itemusages.py b/tests/test_connectors_onepassword_events_itemusages.py index 12e2df5..8d5cb2b 100644 --- a/tests/test_connectors_onepassword_events_itemusages.py +++ b/tests/test_connectors_onepassword_events_itemusages.py @@ -90,7 +90,7 @@ def test_collect_no_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 3) + self.assertEqual(self.connector._saved["logs"], 3) self.assertEqual(self.connector.pointer, "2020-06-11T16:42:55-03:00") @responses.activate @@ -130,5 +130,5 @@ def test_collect_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2020-06-11T16:52:55-03:00") diff --git a/tests/test_connectors_onepassword_events_signinattempts.py b/tests/test_connectors_onepassword_events_signinattempts.py index 8465e6c..5bc0ff3 100644 --- a/tests/test_connectors_onepassword_events_signinattempts.py +++ b/tests/test_connectors_onepassword_events_signinattempts.py @@ -90,7 +90,7 @@ def test_collect_no_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2021-03-01T16:42:50-03:00") @responses.activate @@ -130,5 +130,5 @@ def test_collect_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2021-03-01T16:42:50-03:00") diff --git a/tests/test_connectors_oomnitza_activities.py b/tests/test_connectors_oomnitza_activities.py index c20afd2..a11130c 100644 --- a/tests/test_connectors_oomnitza_activities.py +++ b/tests/test_connectors_oomnitza_activities.py @@ -9,6 +9,7 @@ from unittest.mock import patch import responses + from grove.connectors.oomnitza.activities import Connector from grove.models import ConnectorConfig from tests import mocks @@ -69,7 +70,7 @@ def test_collect_pagination(self): # Check the pointer matches the latest execution_time value, and that the # expected number of logs were returned. self.connector.run() - self.assertEqual(self.connector._saved, 205) + self.assertEqual(self.connector._saved["logs"], 205) self.assertEqual(self.connector.pointer, "1682538024") @responses.activate @@ -91,7 +92,7 @@ def test_collect_no_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 5) + self.assertEqual(self.connector._saved["logs"], 5) self.assertEqual(self.connector.pointer, "1680895957") @responses.activate @@ -111,4 +112,4 @@ def test_collect_no_results(self): ), ) self.connector.run() - self.assertEqual(self.connector._saved, 0) + self.assertEqual(self.connector._saved["logs"], 0) diff --git a/tests/test_connectors_pagerduty_audit_records.py b/tests/test_connectors_pagerduty_audit_records.py index 92f5502..fde60a4 100644 --- a/tests/test_connectors_pagerduty_audit_records.py +++ b/tests/test_connectors_pagerduty_audit_records.py @@ -104,7 +104,7 @@ def test_collect_pagination(self): # Check the pointer matches the latest execution_time value, and that the # expected number of logs were returned. self.connector.run() - self.assertEqual(self.connector._saved, 5) + self.assertEqual(self.connector._saved["logs"], 5) self.assertEqual(self.connector.pointer, "2021-09-08T18:03:32.120Z") @responses.activate @@ -126,5 +126,5 @@ def test_collect_no_pagination(self): # Set the chunk size large enough that no chunking is required. self.connector.run() - self.assertEqual(self.connector._saved, 4) + self.assertEqual(self.connector._saved["logs"], 4) self.assertEqual(self.connector.pointer, "2021-09-08T18:05:45.120Z") diff --git a/tests/test_connectors_sf_event_log.py b/tests/test_connectors_sf_event_log.py index e3f6dc2..599be74 100644 --- a/tests/test_connectors_sf_event_log.py +++ b/tests/test_connectors_sf_event_log.py @@ -115,5 +115,5 @@ def test_collect_no_pagination(self): # Check the pointer matches the latest value, and that the expected number of # logs were returned. self.connector.collect() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2038-01-19T03:00:00.000Z") diff --git a/tests/test_connectors_sfmc_audit_events.py b/tests/test_connectors_sfmc_audit_events.py index 2b60f38..30344f3 100644 --- a/tests/test_connectors_sfmc_audit_events.py +++ b/tests/test_connectors_sfmc_audit_events.py @@ -69,7 +69,7 @@ def test_collect_pagination(self): # Check the pointer matches the latest value, and that the expected number of # logs were returned. self.connector.collect() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2019-01-02T12:00:00.00") @responses.activate @@ -89,5 +89,5 @@ def test_collect_no_pagination(self): ) self.connector.collect() - self.assertEqual(self.connector._saved, 7) + self.assertEqual(self.connector._saved["logs"], 7) self.assertEqual(self.connector.pointer, "2019-01-07T12:00:00.00") diff --git a/tests/test_connectors_sfmc_security_events.py b/tests/test_connectors_sfmc_security_events.py index 73ea042..f2e3371 100644 --- a/tests/test_connectors_sfmc_security_events.py +++ b/tests/test_connectors_sfmc_security_events.py @@ -69,7 +69,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "2019-01-02T12:00:00.00") @responses.activate @@ -90,5 +90,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 7) + self.assertEqual(self.connector._saved["logs"], 7) self.assertEqual(self.connector.pointer, "2019-01-07T12:00:00.00") diff --git a/tests/test_connectors_slack_audit.py b/tests/test_connectors_slack_audit.py index a36fe14..45ee8b5 100644 --- a/tests/test_connectors_slack_audit.py +++ b/tests/test_connectors_slack_audit.py @@ -102,7 +102,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) self.assertEqual(self.connector.pointer, "1521214344") @responses.activate @@ -122,5 +122,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 19) + self.assertEqual(self.connector._saved["logs"], 19) self.assertEqual(self.connector.pointer, "1521214944") diff --git a/tests/test_connectors_tfc_audit_trails.py b/tests/test_connectors_tfc_audit_trails.py index 0d788d5..733f91c 100644 --- a/tests/test_connectors_tfc_audit_trails.py +++ b/tests/test_connectors_tfc_audit_trails.py @@ -99,7 +99,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 3) + self.assertEqual(self.connector._saved["logs"], 3) self.assertEqual(self.connector.pointer, "2020-06-30T17:52:46.000Z") @responses.activate @@ -119,5 +119,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 5) + self.assertEqual(self.connector._saved["logs"], 5) self.assertEqual(self.connector.pointer, "2020-06-30T17:52:46.000Z") diff --git a/tests/test_connectors_torq_activity_logs.py b/tests/test_connectors_torq_activity_logs.py index ebad313..61fa2e2 100644 --- a/tests/test_connectors_torq_activity_logs.py +++ b/tests/test_connectors_torq_activity_logs.py @@ -92,7 +92,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 2) + self.assertEqual(self.connector._saved["logs"], 2) # it's reverse chronological so the earlier timestamp should be recorded self.assertEqual(self.connector.pointer, "2022-06-24T18:15:07.380622Z") @@ -134,5 +134,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2022-06-24T18:15:06.380622Z") diff --git a/tests/test_connectors_torq_audit_logs.py b/tests/test_connectors_torq_audit_logs.py index 8a8565b..bbd68f8 100644 --- a/tests/test_connectors_torq_audit_logs.py +++ b/tests/test_connectors_torq_audit_logs.py @@ -92,7 +92,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 4) + self.assertEqual(self.connector._saved["logs"], 4) # it's reverse chronological so the earlier timestamp should be recorded self.assertEqual(self.connector.pointer, "2022-06-27T11:35:10.681687Z") @@ -134,5 +134,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2022-06-07T11:35:11.681687Z") diff --git a/tests/test_connectors_workday_activity_logging.py b/tests/test_connectors_workday_activity_logging.py index 66fb19d..b9fb838 100644 --- a/tests/test_connectors_workday_activity_logging.py +++ b/tests/test_connectors_workday_activity_logging.py @@ -194,7 +194,7 @@ def test_collect_pagination(self): # Check the pointer matches the latest execution_time value, and that the # expected number of logs were returned. self.connector.run() - self.assertEqual(self.connector._saved, 113) + self.assertEqual(self.connector._saved["logs"], 113) self.assertEqual(self.connector.pointer, "2021-10-12T23:50:09.752Z") @responses.activate @@ -234,7 +234,7 @@ def test_collect_no_pagination(self): # Ensure only a single value is returned, and the pointer is properly set. self.connector.run() - self.assertEqual(self.connector._saved, 13) + self.assertEqual(self.connector._saved["logs"], 13) self.assertEqual(self.connector.pointer, "2021-10-12T23:50:09.752Z") @responses.activate @@ -272,4 +272,4 @@ def test_collect_no_results(self): ), ) self.connector.run() - self.assertEqual(self.connector._saved, 0) + self.assertEqual(self.connector._saved["logs"], 0) diff --git a/tests/test_connectors_zoom_activities.py b/tests/test_connectors_zoom_activities.py index 5d34826..1e505ae 100644 --- a/tests/test_connectors_zoom_activities.py +++ b/tests/test_connectors_zoom_activities.py @@ -84,7 +84,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 31) + self.assertEqual(self.connector._saved["logs"], 31) self.assertEqual(self.connector.pointer, "2022-08-23T14:45:54Z") @responses.activate @@ -121,5 +121,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2022-08-23T14:45:54Z") diff --git a/tests/test_connectors_zoom_operation.py b/tests/test_connectors_zoom_operation.py index 452fd16..0245c33 100644 --- a/tests/test_connectors_zoom_operation.py +++ b/tests/test_connectors_zoom_operation.py @@ -84,7 +84,7 @@ def test_collect_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 31) + self.assertEqual(self.connector._saved["logs"], 31) self.assertEqual(self.connector.pointer, "2022-08-23T14:46:17Z") @responses.activate @@ -121,5 +121,5 @@ def test_collect_no_pagination(self): ) self.connector.run() - self.assertEqual(self.connector._saved, 1) + self.assertEqual(self.connector._saved["logs"], 1) self.assertEqual(self.connector.pointer, "2022-08-23T14:46:17Z") diff --git a/tests/test_helpers_parsing.py b/tests/test_helpers_parsing.py new file mode 100644 index 0000000..6b403a9 --- /dev/null +++ b/tests/test_helpers_parsing.py @@ -0,0 +1,87 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Implements tests for parsing helpers.""" + +import unittest + +from grove.helpers import parsing + + +class ParsingHelpersTestCase(unittest.TestCase): + """Implements tests for parsing helpers.""" + + def test_update_by_path(self): + """Ensures path updating operates as expected.""" + # Multi-dimension. + expected_multi = { + "A": { + "B": { + "C": { + "D": { + "E": "injected", + "deepest": True, + }, + "adjacent": True, + } + } + }, + "top": True, + } + self.assertDictEqual( + expected_multi, + parsing.update_path( + { + "A": {"B": {"C": {"D": {"deepest": True}, "adjacent": True}}}, + "top": True, + }, + "A.B.C.D.E".split("."), + "injected", + ), + ) + + # Replacement. + expected_replace = { + "A": {"B": {"C": "replaced"}}, + } + self.assertDictEqual( + expected_replace, + parsing.update_path( + {"A": {"B": {"C": "initial"}}}, + "A.B.C".split("."), + "replaced", + ), + ) + + # Single dimension. + expected_single = { + "A": "value", + } + self.assertDictEqual( + expected_single, + parsing.update_path( + {}, + "A".split("."), + "value", + ), + ) + + # Deletion of nested keys. + self.assertDictEqual( + {"A": 1}, + parsing.update_path( + {"A": 1, "B": {"C": [1, 2, 3], "D": {"E": "F"}}}, + "B".split("."), + None, + ), + ) + + # Deletion of deeply nested keys. + self.assertDictEqual( + {"A": 1, "B": {"D": {"E": "F"}}}, + parsing.update_path( + {"A": 1, "B": {"C": [1, 2, 3], "D": {"E": "F"}}}, + "B.C".split("."), + None, + ), + ) diff --git a/tests/test_outputs_base.py b/tests/test_outputs_base.py index 240410b..84f5f63 100644 --- a/tests/test_outputs_base.py +++ b/tests/test_outputs_base.py @@ -12,6 +12,8 @@ # Required as BaseOutput is an ABC, so without defining submit we will not be able to # instantiate it to validate methods on the base class. class TestOutput(BaseOutput): + __test__ = False + def submit(self, *args, **kwargs): pass diff --git a/tests/test_processors_extract_paths.py b/tests/test_processors_extract_paths.py new file mode 100644 index 0000000..c1c62a6 --- /dev/null +++ b/tests/test_processors_extract_paths.py @@ -0,0 +1,80 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Implements unit tests for the path extracting processor.""" + +import json +import os +import unittest +from unittest.mock import patch + +from grove.models import ProcessorConfig +from grove.processors import extract_paths +from tests import mocks + + +class ProcessorPathExtratTestCase(unittest.TestCase): + """Implements unit tests for the path extracting processor.""" + + @patch("grove.helpers.plugin.load_handler", mocks.load_handler) + def setUp(self): + """Setup the processor and associated configuration for testing.""" + self.dir = os.path.dirname(os.path.abspath(__file__)) + + # Create a mapping compatible with the Okta system log fixture. + self.processor = extract_paths.Handler( + ProcessorConfig( + name="ecs", + processor="extract_paths", + raw="event.original", + fields=[ + { + "destination": "@timestamp", + "sources": [ + "published", + ], + }, + { + "destination": "'source.ip'", + "sources": [ + "client.ipAddress", + ], + }, + { + "destination": "'ecs.version'", + "static": "8.8", + }, + { + "destination": "nested.key", + "static": "example", + }, + { + "destination": "another.nested.key", + "sources": [ + "client.device", + ], + }, + ], + ) + ) + + def test_extract_paths(self): + """Ensure path extraction operates as expected.""" + # Load and process the fixture. + entries = json.load( + open(os.path.join(self.dir, "fixtures/okta/system_log/001.json"), "r") + ) + + # Process a single target. + target = entries[0] + results = self.processor.process(target) + + # Ensure fields are is mapped correctly. + self.assertEqual(results[0]["source.ip"], "000.000.00.000") + self.assertEqual(results[0]["@timestamp"], "2021-06-24T00:04:08.123Z") + self.assertEqual(results[0]["ecs.version"], "8.8") + self.assertEqual(results[0]["nested"]["key"], "example") + self.assertEqual(results[0]["another"]["nested"]["key"], "Computer") + + # Ensure the raw message is present. + self.assertGreater(len(results[0]["event"]["original"]), 0) diff --git a/tests/test_processors_filter_paths.py b/tests/test_processors_filter_paths.py new file mode 100644 index 0000000..f47dde5 --- /dev/null +++ b/tests/test_processors_filter_paths.py @@ -0,0 +1,55 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Implements unit tests for the path filtering processor.""" + +import json +import os +import unittest +from unittest.mock import patch + +from grove.models import ProcessorConfig +from grove.processors import filter_paths +from tests import mocks + + +class ProcessorPathFilterTestCase(unittest.TestCase): + """Implements unit tests for the path filtering processor.""" + + @patch("grove.helpers.plugin.load_handler", mocks.load_handler) + def setUp(self): + """Setup the processor and associated configuration for testing.""" + self.dir = os.path.dirname(os.path.abspath(__file__)) + + # Create a mapping compatible with the Okta system log fixture. + self.processor = filter_paths.Handler( + ProcessorConfig( + name="Filter debugContext", + processor="filter_paths", + sources=[ + "debugContext", + "client.geographicalContext", + ], + ) + ) + + def test_filter_paths(self): + """Ensure path filtering operates as expected.""" + # Load and process the fixture. + entries = json.load( + open( + os.path.join(self.dir, "fixtures/okta/system_log/001.json"), + "r", + ) + ) + + # Firstly, ensure the 'debugContext' field exists to begin with. + self.assertTrue("debugContext" in entries[0]) + self.assertTrue("geographicalContext" in entries[0]["client"]) + + # Process a single log entry. + records = self.processor.process(entries[0]) + + # Ensure the 'debugContext' field was removed. + self.assertFalse("debugContext" in records[0]) + self.assertFalse("geographicalContext" in entries[0]["client"]) diff --git a/tests/test_processors_split_path.py b/tests/test_processors_split_path.py new file mode 100644 index 0000000..3cd5977 --- /dev/null +++ b/tests/test_processors_split_path.py @@ -0,0 +1,50 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Implements unit tests for the path splitting processor.""" + +import json +import os +import unittest +from unittest.mock import patch + +from grove.models import ProcessorConfig +from grove.processors import split_path +from tests import mocks + + +class ProcessorPathMapperTestCase(unittest.TestCase): + """Implements unit tests for the path splitting processor.""" + + @patch("grove.helpers.plugin.load_handler", mocks.load_handler) + def setUp(self): + """Setup the processor and associated configuration for testing.""" + self.dir = os.path.dirname(os.path.abspath(__file__)) + + # Create a mapping compatible with the Okta system log fixture. + self.processor = split_path.Handler( + ProcessorConfig( + name="Fan Out", + processor="split_path", + source="events", + ) + ) + + def test_split_path(self): + """Ensure path splitting operates as expected.""" + # Load and process the fixture. + entries = json.load( + open( + os.path.join(self.dir, "fixtures/gsuite/activities/001.json"), + "r", + ) + ) + + # Confirm that the initial log entry has two entries. + self.assertEqual(len(entries["items"]), 1) + + # Process a single log entry. + records = self.processor.process(entries["items"][0]) + + # Confirm that two records resulted. + self.assertEqual(len(records), 2) From 50cd97b41e4b1c8593cd7d9a24f00ef828fe8f20 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Sat, 8 Jul 2023 13:16:17 +0100 Subject: [PATCH 02/12] Cache calls to quote_aware_split. --- grove/helpers/parsing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grove/helpers/parsing.py b/grove/helpers/parsing.py index ba9c59f..e6ccc0a 100644 --- a/grove/helpers/parsing.py +++ b/grove/helpers/parsing.py @@ -5,6 +5,7 @@ import json import re +from functools import cache from typing import Any, Dict, List from pydantic import ValidationError @@ -48,6 +49,7 @@ def quick_copy(value: Any): return json.loads(json.dumps(value)) +@cache def quote_aware_split(value: str, delimiter=".") -> List[str]: """Splits a value by delimiter, returning a list. From 8e0aec1e0e097fdef83c58aadaf6463e575756a7 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Sat, 8 Jul 2023 13:36:00 +0100 Subject: [PATCH 03/12] Update processor base class to allow finalize only This was possible prior to this commit, but required a stub method for process to be implemented. --- grove/connectors/__init__.py | 2 ++ grove/processors/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/grove/connectors/__init__.py b/grove/connectors/__init__.py index c0df849..ea46c56 100644 --- a/grove/connectors/__init__.py +++ b/grove/connectors/__init__.py @@ -348,6 +348,8 @@ def save(self, entries: List[Any]): # Always refresh our lock while saving. This allows us to grab a new lock for # every page of data to try and prevent our lock expiring before we've performed # a full collection. + # + # Unlock is not called here, as it's performed by the caller. self.lock() if self.LOG_ORDER == CHRONOLOGICAL: diff --git a/grove/processors/__init__.py b/grove/processors/__init__.py index 1309148..d6203fe 100644 --- a/grove/processors/__init__.py +++ b/grove/processors/__init__.py @@ -40,7 +40,6 @@ def __init__(self, config: Dict[str, Any]): f"Processor configuration is invalid. {parsing.validation_error(err)}" ) - @abc.abstractmethod def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: """Performs a set of processes against a log entry. @@ -50,8 +49,9 @@ def process(self, entry: Dict[str, Any]) -> List[Dict[str, Any]]: the list should contain a single element. If the log entry is to be dropped, an empty list should be used. """ - pass + return [entry] def finalize(self): """Performs a final set of operations after logs have been saved.""" + return From b9b8338bfa6ea5717eabc415b6b0bf031d854ac3 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Mon, 10 Jul 2023 12:45:16 +0100 Subject: [PATCH 04/12] Add local file secret backend. --- grove/secrets/local_file.py | 75 ++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- tests/test_secrets_local_file.py | 49 +++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 grove/secrets/local_file.py create mode 100644 tests/test_secrets_local_file.py diff --git a/grove/secrets/local_file.py b/grove/secrets/local_file.py new file mode 100644 index 0000000..b32262b --- /dev/null +++ b/grove/secrets/local_file.py @@ -0,0 +1,75 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Grove local file secrets handler.""" + +import logging +import os + +from pydantic import BaseSettings, Field, ValidationError + +from grove.exceptions import AccessException, ConfigurationException +from grove.helpers import parsing +from grove.secrets import BaseSecret + + +class Configuration(BaseSettings): + """Defines environment variables used to configure the local file handler. + + This should also include any appropriate default values for fields which are not + required. + """ + + path_prefix: str = Field( + str(), + description="An optional prefix to append to configured secret paths.", + ) + + class Config: + """Allow environment variable override of configuration fields. + + This also enforce a prefix for all environment variables for this handler. As + an example the field `path` would be set using the environment variable + `GROVE_SECRET_LOCAL_FILE_PATH_PREFIX`. + """ + + env_prefix = "GROVE_SECRET_LOCAL_FILE_" + case_insensitive = True + + +class Handler(BaseSecret): + """A secret handler to read secrets from local files.""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + + # Wrap validation errors to keep them in the Grove exception hierarchy. + try: + self.config = Configuration() # type: ignore + except ValidationError as err: + raise ConfigurationException(parsing.validation_error(err)) + + def get(self, id: str) -> str: + """Gets and returns an secret from the specified file path. + + If a path prefix is configured this will be appended to the beginning of the + configured file path. However, if the path of the secret begins with a '/' it + the path prefix will be ignored - as it will be considered a fully-qualified + path specification. + + :param id: The file to read the secret from. + + :return: The plain-text secret, read from the specified file. + """ + secret = str() + path = os.path.join(self.config.path_prefix, id) + + try: + with open(path, "rb") as f: + secret = str(f.read(), "utf-8").rstrip() + except (ValidationError, OSError) as err: + raise AccessException( + f"Unable to read secret from configured '{path}'. {err}" + ) + + return secret diff --git a/pyproject.toml b/pyproject.toml index d527125..39e11fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ local_file = "grove.configs.local_file:Handler" [project.entry-points."grove.secrets"] aws_ssm = "grove.secrets.aws_ssm:Handler" hashicorp_vault = "grove.secrets.hashicorp_vault:Handler" +local_file = "grove.secrets.local_file:Handler" [project.entry-points."grove.processors"] extract_paths = "grove.processors.extract_paths:Handler" @@ -159,7 +160,7 @@ legacy_tox_ini = """ [testenv] pip_version = pip extras = tests - commands = pytest -c pyproject.toml + commands = pytest -c pyproject.toml {posargs} srcs = grove [testenv:linters] diff --git a/tests/test_secrets_local_file.py b/tests/test_secrets_local_file.py new file mode 100644 index 0000000..f390999 --- /dev/null +++ b/tests/test_secrets_local_file.py @@ -0,0 +1,49 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +"""Implements tests for the local file secrets backend.""" + +import os +import tempfile +import unittest + +from grove.secrets.local_file import Handler + + +class SecretsLocalFileTestCase(unittest.TestCase): + """Implements tests for the local file secrets backend.""" + + def setUp(self): + self.fixtures = os.path.abspath( + os.path.join(os.path.dirname(__file__), "fixtures/") + ) + + def test_relative_path(self): + """Ensures a secret can be read from a relative file path.""" + expected = "_Super_S3cret_Stuff." + + with tempfile.NamedTemporaryFile("w") as fout: + fout.write(expected) + fout.write("\n") + fout.flush() + + # Validate loading with a path prefix. + os.environ["GROVE_SECRET_LOCAL_FILE_PATH_PREFIX"] = os.path.dirname( + fout.name + ) + + self.secrets = Handler() + self.assertEqual(self.secrets.get(os.path.basename(fout.name)), expected) + + def test_absolute_path(self): + """Ensures a secret can be read from an absolute file path.""" + expected = "_Super_S3cret_Stuff." + + with tempfile.NamedTemporaryFile("w") as fout: + fout.write(expected) + fout.write("\n") + fout.flush() + + # Validate loading with a path prefix. + self.secrets = Handler() + self.assertEqual(self.secrets.get(fout.name), expected) From c100f23490c80b04cf0f914c67cb23a7adae3bfa Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Mon, 10 Jul 2023 12:46:07 +0100 Subject: [PATCH 05/12] Remove caching to prevent unexpected mutation. --- grove/helpers/parsing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/grove/helpers/parsing.py b/grove/helpers/parsing.py index e6ccc0a..ba9c59f 100644 --- a/grove/helpers/parsing.py +++ b/grove/helpers/parsing.py @@ -5,7 +5,6 @@ import json import re -from functools import cache from typing import Any, Dict, List from pydantic import ValidationError @@ -49,7 +48,6 @@ def quick_copy(value: Any): return json.loads(json.dumps(value)) -@cache def quote_aware_split(value: str, delimiter=".") -> List[str]: """Splits a value by delimiter, returning a list. From d1b941235b203e5bc6c536a0cc12b8680a53eb37 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:49:03 +0100 Subject: [PATCH 06/12] Documentation updates. --- README.md | 14 +++++++-- docs/api.rst | 1 + docs/conf.py | 21 +++---------- docs/grove.rst | 1 + docs/index.rst | 26 ++++++++++++--- docs/internals.rst | 26 +++++++++------ docs/static/custom.css | 42 ++++++++++++++++++------- docs/static/grove-logo-light.png | Bin 0 -> 19674 bytes docs/static/grove-logo-small-light.png | Bin 0 -> 16787 bytes docs/static/grove-logo-small.png | Bin 0 -> 19248 bytes docs/static/grove-logo.png | Bin 0 -> 23315 bytes docs/static/grove-support-light.png | Bin 0 -> 73194 bytes docs/static/grove-support.png | Bin 0 -> 78562 bytes 13 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 docs/static/grove-logo-light.png create mode 100644 docs/static/grove-logo-small-light.png create mode 100644 docs/static/grove-logo-small.png create mode 100644 docs/static/grove-logo.png create mode 100644 docs/static/grove-support-light.png create mode 100644 docs/static/grove-support.png diff --git a/README.md b/README.md index bff16c0..5dfa805 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -## Grove - -> Grove is not an official HashiCorp project. +

+

+ +
+

Grove is a Software as a Service (SaaS) log collection framework, designed to support collection of logs from services which do not natively support log streaming. @@ -24,6 +26,12 @@ us via email at security@hashicorp.com, rather than filing a GitHub issue. ### Supported Sources +

+

+ +
+

+ Currently the following log sources are supported by Grove out of the box. If a source isn't listed here, support can be added by creating a custom connector! diff --git a/docs/api.rst b/docs/api.rst index 360292f..790096c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -59,3 +59,4 @@ Subpackages grove.helpers grove.outputs grove.secrets + grove.processors diff --git a/docs/conf.py b/docs/conf.py index a589830..a42d79a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,24 +10,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os -import sys - -sys.path.insert(0, os.path.abspath("../grove/")) - -# Set defaults, and then load __about__ from the project. -__title__ = None -__about__ = None -__author__ = None -__version__ = None -__copyright__ = None - -exec(open(os.path.abspath("../grove/__about__.py")).read()) - +from grove.__about__ import __copyright__, __title__, __version__ # -- Project information ----------------------------------------------------- -author = __author__ +author = "HashiCorp Security (TDR)" project = __title__.title() copyright = __copyright__ @@ -63,7 +50,7 @@ # a list of builtin themes. html_theme = "furo" html_title = f"Grove v{__version__}" -html_logo = "static/grove.png" +# html_logo = "static/grove.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -86,7 +73,7 @@ "admonition-title-font-size": "0.9rem", }, "dark_css_variables": { - "color-foreground-secondary": "#444", + "color-foreground-secondary": "#9ca0a5", "admonition-font-size": "0.9rem", "admonition-title-font-size": "0.9rem", }, diff --git a/docs/grove.rst b/docs/grove.rst index f96164d..f59b841 100644 --- a/docs/grove.rst +++ b/docs/grove.rst @@ -19,6 +19,7 @@ Subpackages grove.helpers grove.outputs grove.secrets + grove.processors Submodules ---------- diff --git a/docs/index.rst b/docs/index.rst index 9b93a5d..a7391f7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,14 @@ -Grove -===== +.. container:: clear-title -.. note:: - Grove is not an official HashiCorp project. + .. image:: static/grove-logo-small.png + :alt: Grove + :align: center + :class: only-light + + .. image:: static/grove-logo-small-light.png + :alt: Grove + :align: center + :class: only-dark Grove is a Software as a Service (SaaS) log collection framework, designed to support collection of logs from services which do not natively support log streaming. @@ -27,6 +33,18 @@ us via email at security@hashicorp.com, rather than filing a GitHub issue. Supported Sources ----------------- +.. container:: clear-image + + .. image:: static/grove-support.png + :alt: Supported Sources + :align: center + :class: only-light + + .. image:: static/grove-support-light.png + :alt: Supported Sources + :align: center + :class: only-dark + Currently the following log sources are supported by Grove out of the box. If a source isn't listed here, support can be added by creating a custom connector! diff --git a/docs/internals.rst b/docs/internals.rst index 91e9236..af18c5e 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -15,9 +15,11 @@ Flow A rough diagram of the overall flow of a Grove run ("collection") can be found below. -.. image:: static/flow.png - :alt: Overall flow of a Grove collection - :align: center +.. container:: clear-image + + .. image:: static/flow.png + :alt: Overall flow of a Grove collection + :align: center Components ---------- @@ -64,9 +66,11 @@ A visual overview of the relationship between built-in Grove backends and their base classes can be found below. Although the examples in this image are built-in to Grove, they follow the same principals as any other Plugin. -.. image:: static/plugin_structure.png - :alt: Grove Module Overview - :align: center +.. container:: clear-image + + .. image:: static/plugin_structure.png + :alt: Grove Module Overview + :align: center Configuration ^^^^^^^^^^^^^ @@ -212,10 +216,12 @@ If no existing pointer is in the cache, the connector will provide an initial va which is appropriate for the application. When Grove next runs, only log entries generated since / after this pointer would be collected. -.. image:: static/pointers.png - :width: 384 - :alt: How pointers are used. - :align: center +.. container:: clear-image + + .. image:: static/pointers.png + :width: 384 + :alt: How pointers are used. + :align: center Cache ----- diff --git a/docs/static/custom.css b/docs/static/custom.css index 47b0b3d..ec83c69 100644 --- a/docs/static/custom.css +++ b/docs/static/custom.css @@ -1,5 +1,7 @@ :root { - --text-color-override: #3d7c9b; + --text-color-override: #428cdb; + --title-color: #428cdb; + /* --text-color-override: #38a7d2; */ } article { @@ -21,23 +23,25 @@ h4, h5 { padding-top: 8px; padding-bottom: 8px; - color: var(--text-color-override); -} - -.sidebar-tree .toctree-l1 > .reference { - color: var(--text-color-override); + color: var(--title-color); } .sidebar-brand-text { text-align: center; - color: var(--text-color-override); } -.grove-logo h1 { - padding-top: 0px; - font-size: 420%; - padding-left: 10%; - text-align: center; +.sidebar-brand-text[data-theme="light"] { + color: #444 !important; +} + +.sidebar-brand-text[data-theme="dark"] { + color: #FFF !important; +} + +.sidebar-tree .toctree-l1 > .reference, +.sidebar-tree .toctree-l2 > .reference, +.sidebar-tree .toctree-l3 > .reference { + color: var(--text-color-override); } .sidebar-logo { @@ -50,4 +54,18 @@ h5 { p.admonition-title { margin: 0 -0.9rem 0.8rem -0.9rem +} + +.clear-image { + padding-top: 35px; + padding-bottom: 35px; +} + +.clear-title { + padding-top: 35px; + padding-bottom: 50px; +} + +.light-bg { + background-color: #FFF; } \ No newline at end of file diff --git a/docs/static/grove-logo-light.png b/docs/static/grove-logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..960cb965a4e2f147abf5d55680033f42c96f73a9 GIT binary patch literal 19674 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 Dme)FQ literal 0 HcmV?d00001 diff --git a/docs/static/grove-logo-small-light.png b/docs/static/grove-logo-small-light.png new file mode 100644 index 0000000000000000000000000000000000000000..a6972bf902941ff2169f77bb2c7dc6b5c0690556 GIT binary patch literal 16787 zcmZ|01y~%F=GbWK(Lx~jXXpXr(HjZ{~a!$c!N0{{S+3i8sL005lHAKCx~_~+_rpiBS& zpjFyQNvSJHNl~f0g01ZwtN;M{$doi>^`u3D;M12&;5Vryj>ddn=r{c}Y>b{&!P5>Oo88eT zSJxBay|osb-A91Btx=s2xh_1FR5Cs4#f9b1pZ(mxghfIO&r4 zu8{{(K&C!8Q|;&d&C5##(*;t77$E;uIPP6WJ4Uz~)qNCWswG@-xyzuUN z%93q^Hf9^C|8pGCM&xbf&(lr17k2BfVhm|nY zaK3KiMFtVoPbG-9WkKlF$G-l&>zQWTR=ZbojhYd6&GgAIahV?7^F0u!p{r5+`Jasjb}(}AYM>{ zYmiH7saTrO8|f2H`J1F=-tWCTe&6Gfg})0*2S${w1jpL}b0q-MRCrv$l%IhD-B`8p z&j<#gE0Ka|?Wv#B6vBs?4KM_ztl)uiKfkB<*4EY6A(?%(Db4-4+?LdVGI_=e`AG(F zkixN8jA4Ib2axjtv@2~Kl#9F+QUKX1n+o)cT98Ojcp?eJs9@ksh`Yj#7+f;| z8;Srg!SxwXXNCg}!Hb8Zqe89(*h|7O!Dsb&s>59ecR;Cx;A2AwoYCBa(V=L~SWaQc z5bb(NIbAr)N2pcHg)Y%xEA1|Q(-nd3s z^W6~}*3Af=@avzfVCdd>Na}QWmUjt6b&Zt+i*S< z;>~8|AR6ei+CXy_b%}7XaS?P5YsU6|ZGsFAyXhO)kh%4^<$0ujB!0wvd=;3TFU>_K zgd2+L|AXf(YC-CRY_lv8T?}qYr1Do*?$C5aBPBLQfmh?Wl&{wl<>}re;xZ@G(N$5W zB*i9jB;G4f4zXBj)e{QmveAQSD5;u~P7~HOneb?%km+L+WfrB&3bPBvO8nLHS@fBl z@ov#7@LBLP@tv63HR+g0SnBZ!nRl5}nJM(zOKvqI^DT?Ei+}38E7mS^*DO=B)6OV& zs3NP(E-Go?4Rsu>nefn$j(SG)Rp{_Uu&apq!1k|edi$i=p- zfo2Kk9k!#ij?Anozaqb0qvlmZc}(nA(n;YDve{v5~Ihb);QnHdx9&1D>)KdOsy~KU&y|!lI-!DjFtTI%=ndMEcCGO z`ysQoq|7>f*Y#INrXIgsV?*O(!_Rl3jbnBlGbQ$Zwxu7y)1bmPl~S`V(>R9?)4~-O z*#*O9yi>$GF&zD$t4pf8sUNff__R2nC14M zh53d(d_t5;HILfB>UHYnrY+HEm;@FQ6xt~BGel$zRN8>}N{HG>Ir*!(JFP~m!0<~iMy_YJBvoUMm;M_EeO@V)kgCl^fE|$*?W#{1T)&IMdv&EpCThVk=HG{ z23{?P=L~~J19$9pn9m%}$u3;4-yO`Jd^x|q;J7?F(>RN~EQ9&{uGq`^ZFnlWX>;1N zQnO0AMY-SA*A-XTx8ZUqL<<)S$BNJjKNPYNf*+z5svJfq>39M4fxCc=`3bE%ciILj zaiQ!Yf-wW}eqb1ooZB-A@X|JqTgRA|MW%FSv~{qw_5Gz-HmXBQ2Zhb>vcTegKxGp;pIC)M-V+ua{4AJYiz z*c46Oed{*gpKNZSribr`OQGhq^sJDa*wQf8vABfq^)2`K_1r+gTiEDENX4NAbn``1 zMK(j#LxxEaS}R(^nkHx+;rKu1%bv^pGzX?ar?H9xl;Tvuqun-imf3h^pQogal^W8v zIS%l%BGG@=7mq15agrw>Dqh>X%|Pch{BAqw?5^{H?MZSe?>N;svv(-xB^4>mqA~o@B*qJ&)=+)0{3}=P&D-VfK;s({s;| zu@A-vtzJik^G!SUJ2gX;w>_s3_wXO#6(rYzX_$V2(l1PpbvA{^{W+vv1n20Sga-I2 z_yHWd_@>W8_oe4~osNo2Gg_PK^?tmb-)%2)*iQ_xQd!b=Q?MLzni_pbEetmxUdx2w zE^TW)hz+t!gnLA#*td3%(w?zjqT+f*+AGzUl@66tl}q~kkm#I3oUS40q7W+fD?e{IJV@mI$$Mwhne$6jbcw?$Xfa`hd!nWJ zqNgR@t*P~Go9`HMZkKn@&5sr*&zr^QYC<(**G~6yo+H;0{0P=xddD>aPu4GnT5(cp z7yJYERr5L3O;=;bZ`^vHWCv7Evk0ABoQzxEym+4G42pU^Y(7|e-FfA$Ds|o52VOs& zA=^{Dryu}n`f6Y6U%31_W#|$dCbKMk=y<3Bp$46s1q;G9<3`0n*t_DzV(g?-#N~e0 z7r$p}PhnSk=(*UPueykW3}Aoaq*D`PnsYm{RZ~IBO&eB2U(?mW99Z#u-7VVYl~YJv z7`JZIPD#o7nE$*1m75zlQJc*B)b9q`0;L6U-%VZf!;X5v#Os}rEw4|unS$TqsVc%l zf58Em>Hz9B2ogqI{90;gd{^XL3Q+O0nUy!pSb*PBsgY-#qX@w)(iz8t7@@W(KX51j zkj9jw%STb+3N@4h!r+wx)K*SX;7f9u@N--1OG{|vH&1BZQ5PV39Z6|0;!gO_@-@mz zU%^^M1;Fx$Mgbtgy#^rsq2T_U05}o=(myl+0D>d^Z(26O4uJS?zUrU)?;-!E{6YM? zMo9bwK>l<0`cLuBhyO2aIFo#Y|Dpi~e`EkjEhzN~m6)rgwTPy) z?7!iE?%vSYy1P4zaB_Nid2x90a)4cJIJt#|g*mx+IC*&3|7ftg`8c_od$T*a(f+HE z|7u6t%FV*n&e`1#>_qjqUGw)~5BE1TG=B~K_wg@3t-S62$C8uVzq|FPgPeaQoZK8- zoc}MFyPfs_5A3hxU$B4r^)GW`e;X4~xAV4gFp#!${4>;lTmy0QbBX<9=KqrXkE8#A z)c+qOzwrMD`5%)1jnr|oa+Lx*{xRte`kyHO+w6Y|{{#593nKcOR&HPikH2FG666u% z{J)t08=&TD_s59&KLGB3>;EV3f9o6kzYP2*^WOk5&c7l2kC6T=U;g3#Nf;2C80UW{ z9|+CJ+c^;c;8j+Tmelr!JI+CFc92OqZ2=}zp%F<)aC{C$MEHyfGm$|287w4*@GVAU zEC!W3=A42dTO-e0eN-rK97kFbNn2XIhm}TwIGJfkDPBo-2%hHjrlNZG_jJ?4rD}j` zo}XX9dRK+#ak-9t)w@Q|Ahz|Sx*}{@^l0_;n+4Qz=%ZJn>cV3qV2?ZF`}t9> zC@#`N1)*M0lKYkjJ`JoNzz621z3C*&_aXl@g@Ya=2HQ1DyFWmMTuj35%#>>R7vP&) z9ixpfP0_~SyO2w#UWkh#(Fo!6R+5fCbB)sF{PjD*(5xtAK7~!Ps!eU=%%5+Gq8(lg zxjFNgUhfC+ZGpcGar7NZYJz6tGQ+hmJG$$R{VULGvIrx>0n{Bjanx?}cmt1bOFz;(8K@uBHnettRT;ikH+|wK zJTCeuGy1D$3Pep9-{+7wJ>wDy&;30C#C_))doQ^U4KFk~13jd{*s#|LUPxm9HE90q z68V{W7`oeD@Qh(J#4wD&Es0?q!r=HlWgfpzDMNl6wr)rim5c{}4JflsjKU)_Hle;w zSjX@uMQo`*Ha72XaZ&KoTaSJ{CJXd$ilmcuvv%UqW%-OyYInps6{Cp2lr5>SMHn{E{mEd8RbQN=>2tkD-$>j z$JM{nx^VJr#r_VC6{g?pcM-6cAYW8*FCKPJZJd*rYXcSGE7VqX6rBjhp#5 zin0Vu0S_ypjg(N6+ll^+}<$=jBM9t4aTpBHZ7*C#pW>=09j`ivx;| zfl~#?1Ip_Ro6@k90Sd(R!n$8!^6Yv4L{phfeFun(i90IlWb+$ z+k#KVW*qugM7UPqr-9@8lL;qC6}>!do5Nsl?Bf}(ZkhOK?Xh7>n71|__F|wka*bt* zpWcGCHo5ApDjQc_cfEZ`NQfXj`T?){i#POiXH0tH3za>Iq(^@C3@VQhuqBamn&D6_ zO?KjmIOUpd|DHa9xvN#ccx$NTU-K=09zGxpH{|1OV(vh_ccuO_%wQ^T# z?5mhYsdwNx=?Jfw?$(^~bqv5x7Mn5l( z6ZqyHUogJ?dA~Tfn7N6xOo~N$&_|0>Jqtsj`Zxe8 zLF6nhxlS0*@b;7Q(q<9sZsCXhcujBjFon4-e(e=(hip=eX~r>A#elGryJ5}kth*TF z(Cp8`Lh>#kWA|^<9)ZIGQVFHOU%CrJ423iujvq4~@u3b&tNW4WPx_@SLrDVErd}93 zy)8NlxroV=FB^tP`-Q2|em#!R>lj0MPGmybi$Ib+GHSM(q0cyq zDFT+iAc|OLVyr6u;+4r|>^bSZyklLQ0wXRj4fjapB#v^UB)4EcTEjS?KXrgdDwE=A z%hxQ>+!JsL^R4e7x3qm*o? zB5p6Cy)#B{=b`}QO>ayNi;fxy_;`e|Tg4UT*@WbD>vp4l2Rhd$(|cT1ANV@s=qI~y z1kgWR9%YGqkfonIQ8lqUvPxQnU(j9K;oa zX2F9FM4;~`k{@N7_imCh&a1Ju^DtupomhHwoRIVVzBkEsX}66nqC=dcPkbbtx0kWM z3YnHknbz_1DL-Zp&$3nwq(+QBEj_s9l?Ee(5avm5mkz$(@zC^HjJ?Ex3g)Y5Hrd!b zfB7vgFL*0Q23xmSVju)9mDK${2Mo<+>_NyL8lROU#Cfjqo@BeQ@B*0qI|jD|umUlz z;Y}oF3dr+9G>9IJw~OD$|4!z6zzFo}-K|37OgII8QdM}!yDUg(?@*S0K8EU2**(S| zk8hjW(oZD}A)TgY+b4Csl9wrE68Yiov&_`y3N}s7vhaxI4iLJ-4tw97Qsd~+5I835 zlGc#mP9R@lF;zXySg3oO?>G2hN@p$kIS$mvslY_7-Ye0#ez4XFt0EqPU4Tp1}C}Htza@h&|}f6 z{l+(}HlQW*3?hPaC%dKgQ2W%nz4c>ByVN#_Mr=A&=@QpfP$i7=PqOe($#mru9_CFx zhYibJoRC$rfa}=rY(m$9MOoHSyHJhy@1HHY=L!(+4t-^4r-I-T$@?sokT5}^aeAG4 zmRsXXFB#NQH&F`Dkx-0$NmKm#kKA%8Q+mpy5crz&F1`^{qs3FD7FGg<(wI7VBzp0v zPS|)$+_0*w>8hD*AT})G?Mq=M*GG=j;qtU6ppWD|n=_{{63V$Tg`3@v# z(H3ut@syBb_)k)|C?1}(eH}9tgT;gSR&*mktu--hvc$WsKS7~1b1;(Z8876iKE0I7(BT|xVgCuYl9hT$b87!ZDQ!DMrK6gLpJcQX; z8M0_K(5{j-%Tdl~cS5!JkFPS4%%Fz2hYQBH-eU3Ji1c==hD2M6h@fK{Pvcze*ven? zh*6yo@Hw(kGT2yl(RE}Ki))-=0tlyT)NGH&2S9$0F(RpT7|apDQRf3j74rL)Ci)=M zcwh1+Es?go4jyv-TARBRWv6f!?tTuwO>DDe5_HD|5Nt_so1x?wW_i76+Ugv>s+EcMT;`pOe5j;P??uwlUx>RVhO};vqC7Ch~R@&_f z)5MMTolfxI4x(O&o9D!vn*a2*>tJbYu!Sut?1+_lSag2S1D<02kT1x)})= znEE}-P8|m3-DFAcb6c+6*)j)ZFLdRJjW$@Ot)brAYy1z74O@xu)G3(FZSZ9cE+VrJ zfy1;I7cz)vaKrjQL1coTmPZw%>4cwhI%*xGEAvLqMJ7whA7A?l$M$ShzP{4J-t1?&dfOt29K*) z*u(9RQo`z;kosJ@cGeVBK_aStOrt{$voh%x{@jZPo!t8Eu^|L?pLB z*BUmi$=SK(ppYwOcU`>7D8kmt+UYMe7b$|Gw};--G;x%$ZwX^}oOeJ-^lAFBB|pJ% z9L0tm)`Tmz*be7r75?j~KX}g)q~a|b{KVcKn0S|x`!bl&nJsmX-SG6@sYVSO!?I<} zfdR+A$516scS@cCWi~qmXxo?{#i})?RP9uKB#Gs5~ADA{Vd5-G2`vM~|xr_GBUt z|F*2u14dUZdLuK%bW~+{wcz*(QzGeX%I0lu?-|mKw-T6px79@Xaz{!6u>s;wl1f_` ze}#H!1~Vpp!t6AUa~j|#hK$m4PqyNdShn-Op=knd+G0xUS7%~9EP-(?vOglM#uH;) zCj2N`{L*R)6fsIt$ZpG?{W(;G#-AZJuQa2=syP)cYy(RHF4emEE{)fg z)pT@F&9l)fiB|(ZvK7`rWm;5m?;I+`(t<5`x-=Dfg62wk(t-_HwmPSdXW1Y7>h5e$ zhlH*_dfs?FnY3&B0YraI+h8o6VSKBZOzvF5_g1c-Wq;*av*zj@y>9Taj?8;cGoA0E z>=5+5$w)P`xj8B4`*!<8=Sz6*fwA?u&SjL(4rJN5duXxWn_W6Lq=M!C+gno2MH@uT z!DZp{8y}2|mtj~m4_iB6+ZQ>}2}_pTeVHgMu>9lGTI91|A>q&rJ4?<$%~{oYjmH-r z{Di$XR1^&1ulN^KGPP?ZmOW$Z9?^LoK_ip%8ZIpU6>anTI?s{aPZ|iLOoOP0MGbMm z1F1JD3Fiz&j$xVB40{AS7dV#p+LZ6`ERI8m9N#hzi^%PW7p`9O zou#!W^A8WZ9n3UTUA!RJDp1z9Guw&#u)M(S zH|XY48V8J4BI^P*jl;a&iBnxtGCNxsf7mo5$GXEsYIko|q?F@pT*WgUfKrjcNGugt zP5xvIKBs#SE#vjr)e=@Kw{Ws#S4x*(EnFPC9s#4eVG@dNm0>AQ^ z(=cCcHLt+uC?he;q;)F4YYQ#5?`q5UC4(G&xH{*9;kE~%cj`+%_H1^01tP_nNxJmW&G;`;p6aBvyCbDi*r@U;IM^Ep3f=8f|_Rs-2#1Q9Lt^hoc(WZjYfuK zplVQ&T0|r4mDg@+i|B`QX2`Fer?s&hH;Fo8>x7f*&YC_<5gpVXAxytYc3_WEb?{f* zRi9&4^7uT_=~JNb+kR!=nyq}AoppObq4X}~c$yWo>&Mi8OUXQ+mMLPN5O7~$0 z)9n=^dBgDg_>O~mi-k-m(i3s59z;omv6QG_d^|m~&VNV2b35W}$D<(-|4pJ%dz`c2 z+W@j8uDC962zhH9U=uu%;=r63!Q+5Uk#^CO)~iTIxtxopI?7%eqsEby7=A2&l_R*9 zFW2YWSDuaTD+i}GRetOR*3<)gtMMsKtp=oBo>4hnILc943 zUX9^L;tk{!lH`31aXBZ)FhMy1s91fJVJ$M(LwtbiA`xMdXq+;TVi-OY%DN^cb<8BA zEj5vUALLbbU%+}GJF|=|uQYpvr}rE)nP*2&=;dq9O=qMRtyGMAv zMUX8IJ9Eu;)gr|tqfY?U^-bxZREv`0#@z`vcf!pcV;sEXMyZPtBj(+6L_a9|#6B-x zRP)f&iXq7Rq&f1}+gQ`?k$@#l*KOY+HM=IHW}v)>Bd%*UCN?KS@pit`D8j(tx^HTz z-rlz*n}#bdnOqXr_<2gjUrI-Cghi2ql!$bWS_7cT{+T_>rQIG+D1}^>yXdHseUf=d zS_S0+*z6m%*00GRZ~9C4i@_D0Gewd6wbD~b1L=j~I+vg`5olC*#G7G9m_BwTwtolL z;0xOWZtPngk5jH;`Bl`nPa4}K} zBW59FHAp{B-jphle4 zFCrqu?cGS!xWWW9G|WM0rw3(73c@&KgqDW!oB$B`wlL$K7k~mqfei~&vZGYsmXU1- z10IhlFe#1ds92R^Dg@3;-KIgqL@x;_vV1CM@_Y)PYfn?*D5ZOB`HEf1d?3lrnPei+ zV+U2Z-C3lJ7M4h^ya1QvzHSYZG{;3-d_hd2HVt zqU<$e60c$Aj+g}@73D|!uhpZ2do+hzM0@U_S!UQvx8Z{DiZSdmY0^>K6VjMI6MvUz87_(PNlnI5&H*#l$9-u5ReMytyQsU>5$%dF;j zE8XIx{b@j}f)l<~Z#rAi4-1Lg(c1fty;-Tnj?ZMdYN^MfhmNXkrNPjY#bniFV-uSY z&TU=ub^iq>RFi@3zK_uXI^?X&g98f-nC&MQjgsbrv*wTwq!@cdaXRr{5CpK$>94_a z68JF%L@or5MLe^BbIMr0zV=jvancyODa@&-!vTG9a)u*oSq*`Y;GwYe+dZw9btOod zwo1oLq&p6#cpj-k~nA+YVvP~MB(`E3d&-V+{Ln^RO3<{_OF_##Yg|M3ml zp9hC7lzQOp{SD#Fu!CT;rdNc$!>RlIu2$@MOQt@kN9Y}KPkvJ*&le*FEi z2emf{hFo*=wmH4-Jl{5Y4`bm!XeFZ<3}2a7PiDZjg9YX5iVFLKRPHYn)WUa6*spf7 z$bTR!TQR%4kNe;(V9qpJ+^*`vVEpClfUtL6XHC@srx-@`5l=(9Uu{bt#YfmHOn%N4 zt@fw*9-A&gyVxySh0D0v9hSR1uQVO4voEpK%H0YM8-BM-Cc~dPw$K4NS@;xI1rwJ9 zBqENy14W=Jz@kXqrNz!ur01;&)?9D$64dqLrQL2B*DfysHKX#kSE1?M!05h~JNZXo z800tYsPHH z%_@eQhn5e6y45>VjYe0ouQ@)DWtF&+cvF^5b||h- z!%OMAe|a$i2Jkbg=k+wL)ys>RtSU^Wz8U+u`|K&P1a^qF!_IRSYtr2uA<&r9^4KpB zh(>>=jF@vOeT(=x%gjs0n%GLmPyN)^aFM_#P)K*aXYX5i&dp35_d@8ow;8^b2EV1T zdvchF#Nq1Ub%BeiGCmnRaRRD#)x!=S`dD8OJ&oZ(O9 zrV}0TiT!XG4DEmuO7-v-+69U?-4Vc-9U*fBQzbrhn4P4K%{`z5wdL-vmkoSn)vNT( zG68&u;_D?C{39Z=3POAJ#T8MDFY1O|A5CX;Ovf{b(mY$t$A$tt9fp_*@A8IQ)= zJi!XQ)Da=`g2F{7h^J(_&)V0`$2BX>)(D)*j@QJjYvmBNE7YhxnsK#yj*UHj9Yb?Q z1U9i~u;5Q3N7U(3t9}jL;=GB|0fSwZn)s{hLTvW9OEZMiU)>#<5IZ?w*H|8cD&s); zT~Q5JOWqQmh>4JA+mxXqcvk#ypvAn5hqSQ z;M9QfL02ZlD}ek1(k7ta%Sg>v+s~orx3#~bi+410&XSsrRe{mL6{`%O)6Ro)`I}Z^ zcAqTTS>hPg5oFL#!L&E&nE>zs5m!vpDb`UGOMd#l%aF=qK^lqjc$VMv1Vq2BTG=%f6HL#2V9t+))eoED-d zFeWxa6&~o|!+b>W`4?YHC5@OfgBcKY(>K~YmBQ0n1M7LFggv8Rfg|ABg-|nB1vzjm zqVYAR88X)GGOB9_>C;tueIiuxX#VDdREuqm!$nVt1lquFGMxtu;{Z`hADSDv<#%oJ>`%aIG9LFg2h7e4}wPTjU?qH_a=AY&_^nCEiaBGC(K%P5X{r#@W|pIy#Z@| z;z<7XE(=9vtts|->&Nk9G*5j>nZojL&aA_$Yb90_y`Tk`HRGazUf3NK%gpw=BSHU4 zYT`{u9Ka_oI$>rOJoXbQ)WNYeF*Vxl$Yrty!b49g3k^^nx_>G^z+PHke#X4lA(Aw> z&F={e+yxsLM?va*?RlQ5v#=M}5RDQ>IPa-MlPfD!RGxVHdPc#NCie`z--EYTD&JD~ z;`7}*IW0(!?=MH|z^Z#Wm)217nnEgqANoDKCmpu8({I^!#mLtXklvu$qJU-IQvX@p z!U=U#ItSCr+dtKMkZMje5fW4K-0J=uF@zF(wj!Dkj;KAe!^mw2HfB08FBrKf!^sEd zO7^%pfRUGIcy^asSsTHhdgkZL{aZPOkq_1PkL~SLeVK2)V#@<$4Z(XkahwSES~(F8 zd`ex*FpMeQp0RFr^1E5gb~$d2XbXy-Qs$P9vSXt!g4CxbClZk9OsmF82sx+eNZ&*c94H*eWjPUKVdIr)9f)r(9PPiPF;dmq^p!W@A znH1&LpvCj3^=5XYEy1zg^!`(VwhrisHH>hHHdwpCqO@qqW-$L{UrsaGBX&68Q|`(9 zX;|*rxTI|VrSxoRy_`+J5ymx@){qjTH`^^qz?%?|jo7UtLAB*_AMVEb*caO=ewuz|)2+sKGobTBq*RUI2@p>Estt)j{}>g-(QC zhCofh)K*ARLnLu|bA!vs5)5s6og zajY;Yw_}FuVKxy|k>MNcmf9#I84$#cqfO?X!eLL>da0U4m&9uAtbACU7)R2gHmw^f znxR08d`SB_;9x&11b5#V8#DPK&^hnE3U4w_&2$Sb;FaW!X#! z**hGZ{iSlgk*cP1gQ(^7Fl=Ftxj(|)F#R-3)b_RX4`BU$BjvUE5`3RD2xzjT*V*?x z?2aS0;~%dcms{n7FC<%4{^f9qTwe6xU0vg1W*>tmNMOQUOA+_{d56zrMDDY4+M7yGCTMMsp}5t+BBmoU_TIe3Aq;CE z=M7D9HKzv_Ymf*7FfAhNWu&TBXVw zSIdx3u0j9-9s}MDDLCY*FniygTvQJ$iWHxIeSA8f;as}gWK*9cX!)l7`n-AdtddP_ znrcDqH?!3xzKDbT^EsQHF}dqSG9otRb~9m3!1Tke79x|2S=%?+XK@Mj1-96V5(qX0 z7=j(kdM7c+&-BrB!3+PK5svxK26&QVo6D<$O4B3I$Jayg&iCu)bLL6<`tws15T?eYV4*=!)sX)UYiQ{;|7EKf|RjrsHDa! z*jcgaU9h~;iSb5*to^n~R~b=ZwI-IO)GZj!(~y56?V)!z&Q(ba`(6yUTyVah>n`h} zuGTHmx}6qLIQI(S8wOqn`?pq}0ZD5hr{X!z9-E%7V8T%LU^pN4T54~C1tg+xFX)dH zc*v@`eHK`x8nE*LWzSLBTpp%r62ZJ8#|7^(;M&)d{R?+!lpz}F-&eIy;w!-D8w4O?T1*RgC1C4`IlZUvo0D6e9fdqu*2lJ9ZArTP zd3^ZXV^##z;VN9sM|>qOJH>l>EryB+u1}HADy!f;JN+bP(|@b8srlViGHtx~M~x%o zfKevP2;=kxc)xWzc>yj`!a1*$+7N)wc9*U!$!3rVUZ^lzYUYy1_v)CtP_{6?Fdw5| zKhZI+p?`g0zIrEy64Z*2d!H1Yr*TMFw_A3(_ninlzdTbnPdOg(5(#y$B{D*Ck{)kU zz%4jcv}~WK!CE=-<?g)@o$*_lYzVwh%5@(ZKqDE|QT4!BJrT(3NRC z+9iL1!TH?wvZRHLbkBARDzyWaPjP#IX6&zKO4!aZ0v~uk<$XIspkArvyG?2hIt^1s z2)!l}@)Dj8f7?abv%Ryw&%$wMk6SX`i1}P(nx9(}b_Hxa$V?M1?UIL;tiMYS}QAVK0v;Q)XGi8A(S;&o+OX{wpjt;O=rYY zIT^I~dHX;VPO07*ML}^qbEXY33mhc#o-WMx86A_vo>(t=ELR~;2GLI+O`J@N)_tgs z1>t)6zDz^a4pWD3g89%^w@CvzUi4VR>>R~8E5rhd`d%L}KozMzR*A|T`=cf_+byptVOEdHjI-d-?ABbPNgvxw(6^z#qmWR5a z;LdjpWrnwwEVc)<-uK0yws^-A#=YG6^o2{?vmVt?=6xv6x4-sBiuK0*_;g5!5)@_e z5O-e!$%~EJGY)r%PzC-j_SdjRDC62cDeXWaL$;jq3i@(xl|Y7gTr~aSr(5e7G3c;o ztqDsKorJCjKm59ngxF`jKOgL(EV(+!{2{c)y_+Yyo`l|0%P^w&>+3159sc@dRJ~%% zC+*noGjjL#zTk}oEW7EsgZ;fOb8jJa0)!1FW`Bh|oDDS>vg>a&VUCv%a-nJ#z5KNW zJzi0!s>1sNIkGPNtvMGD!KUzf6eRR9GM*W99`0N#mqAKB1S73$DahZ=veu`CIfq{g?yGkcrThMu*Vh=k6Zm1`-uBV_TgXZeb4!MuJ{0mTP zjI>T1L9Mr=je+%f@uLqS^)IJvT_6Dq$q$GE=;A_{Yv8v{UAt65h9XY@;nd*t6~{8N z``fIxlkdJqlva*c-0YP1(K2iASvo0*!0!_T9hbSqh$XL0>vnP%Q&+v0e_`N?k@j_8 z*c`T=J!6{r6}WM*7JWzMWMd4`EBJo;t^A?Bh_U=jZ0+p821SLG=v0Z@7n*&OV5na~ z(z<45RWzq@DdRRF^+?=z>gD<}|Lg%LoIa52$$M*D2eLnl^bQmB6Tnzsi@*&=JBMP* zlSRyGNk8@!p4&7sqoM{M-4iS38UTbC+R}vRC=6d|{gKQFv&6U))#Ow4{QF9Ui9RAL z6hHIolfYC%EK~R;KC(KDti|d=O1X)v)cNSs&BaTE<0!HvbsnL67VQ;}cXQ$I*K#Gq z1FuqwX4iAR(Paf3Nfj|pObFq&j`cYrJGS90Aj=QU0n-(=xuOc)sJ@sYav-g}mUlZI zE*d4FWyrk4E?hneSy#_OG-lThK?7}4M&FxQyDXcpFvg*y26X3ND?vwKd$Ey}MGDc% zrJ<&93O-r!VqXw|O}#vY!!FcK_Uz2J@qSC3z|QZEF&XISVvZlgCSy(5uz_oEo@J>a z-!+Vuh?3h`*b&d@G$R_POR(UgiK(q-g@%uh*q+NL<>Xf}^V$VUs6VJHlVv5&ADCc1 z)`um@Wy!sw78Srt>hn2MfxWlvdaoQ>?J1qSo#aJr=zuzt*^3lCAVf4sxN}~Y-nJoC zBraE-(+v)O-tzaT>`#aybZT-kiFkZ~52}nV`VKbno3I2-iGC>33+vebSz~+O8ZD{3 zMq*=xS!6__VBpbN6Y4nGYpS5MzQ`a>fc4qPEwLeQEH&(-cx@Mg5R$DM<9KtWNmM74 zH|8lm!06PQM*SI4m~+^(@~HGYiU{WDe%(_LUA7-K{atA>BKhDg-69fMBK=gv`xV#8 zN`}~wb6d5IPnRemB*MG*aGb2MtlCowLs02W`F~h$0A=s6J_@JH@uh!3>=-HSX_;7?X_E`?$Z| z<|=1!WLJlK9tV7YyBUZoO7IOcJ@a3_LxFUd3O?bJ--+Ty4*H#?Cq&AMbGxKwbyp-z zwjG6U3nD~>wMoWX;Pp@qFR5NdtVz_wog|S_j|`pZ1F5W}A={-zhH$2C$%hNdA}UG5-(SW$X4~^(QnH-4XNLtLRD3eowi(NH->q*I>UsRK z(h4gc9XwQWt8|rSMx#<=*~GG^AQ%&}#>?@C#3I*L!G3u*DFdXjEqd98!M4 zZ)U&4aC|7B_~sBM3o&_FNUWEVr~&_^KXnWjq*Hb&SUI>4oU? zrkD9D!?#UQm!7snPtp-Tj{V%7-(z}~Cg|SC9@I>lE-P0Bt$%*49SE&U8)wFAhbt2O zWO65VB1hX+PW!pNR4n`JDBK!xJL2th0BeYeYhDpYG02bPf$B$Tcjv=CrBc0c1JMD zly4C}J({B`2k5)u38g;cn%?K(n=-NJ(^Fa~w~8LeR!>1nPpB8VDc@20BuqDc`&`=1nU#&VGiYYKzD?XJR}G^r=6aPw2i<*n@xa`Ha)|!71)f++End1x+!e#qvikb2`aHnRi7-Pn6_M}w6#hEH-V|ZB% zp%y!n&dLqyJHP<^e$&2#b;}Org*{oo_Pl?i8u%g9wl=5r9_% zQSqX`Y%EVr(wW_^S}+&z#W0ENDXt@6v@Qf-&kb@vHjN9%MG&`D{Dn6Q=T-z z<;U1kUlZ++xBr+<9`if!M7rh)m|q-=NH2ytE3`4#MP_|6ZIm>rohpLzZVlD~<#B%V zY+-cp+CL5Lb~TBfhnK`#D%XJyI|){F(@4&?-z#yH@&;)Ma@X60KkN$O#yVVTB{$Tl zhfn%VibG^LjoewSSY*YrVC`3CXiPcPKQi;4Zu#fJrmn6R{cjiCQ1H9SbzWMr68j!UH#T9^#1_4%ydrx literal 0 HcmV?d00001 diff --git a/docs/static/grove-logo-small.png b/docs/static/grove-logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..a4037622c5e878d2c0423afd4a7a6f667e2f3011 GIT binary patch literal 19248 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 literal 0 HcmV?d00001 diff --git a/docs/static/grove-support-light.png b/docs/static/grove-support-light.png new file mode 100644 index 0000000000000000000000000000000000000000..34608388f2f8e5c5301e48b394d54e6741ae3da3 GIT binary patch literal 73194 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%0DjB5ABN&t5 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 Date: Mon, 10 Jul 2023 13:50:12 +0100 Subject: [PATCH 07/12] Version bump for new release. --- grove/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grove/__about__.py b/grove/__about__.py index 42141f4..16b634a 100644 --- a/grove/__about__.py +++ b/grove/__about__.py @@ -1,6 +1,6 @@ """Grove metadata.""" -__version__ = "1.0.0rc4" +__version__ = "1.0.0rc5" __title__ = "grove" __license__ = "Mozilla Public License 2.0" __copyright__ = "Copyright 2023 HashiCorp, Inc." From 5225014ef068ea2852d23d0f6b56a8eb19d31fb4 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:08:19 +0100 Subject: [PATCH 08/12] Update build process for pyproject.toml --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdf3149..82914f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: pip install --upgrade pip setuptools wheel tox - - run: python setup.py install # required for e2e tests + - run: pip install . # required for e2e tests - run: make lint - run: tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40ed54b..cef5292 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: run: python -m pip install --upgrade pip setuptools wheel twine - name: Build package - run: python setup.py sdist bdist_wheel + run: python -m build - name: Publish Python package uses: pypa/gh-action-pypi-publish@release/v1 From 698c9a43d1cbadea47fc1a72c1f70fe3493d78aa Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Thu, 13 Jul 2023 11:04:02 +0100 Subject: [PATCH 09/12] Only write processed data if processors are configured. By default both a processed and raw output stream are configured to simplify configuration. However, this means that if no processors are used logs will be written twice. This pull-request prevents that, and instead only write out data if processors are configured. --- grove/connectors/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/grove/connectors/__init__.py b/grove/connectors/__init__.py index ea46c56..73f09e9 100644 --- a/grove/connectors/__init__.py +++ b/grove/connectors/__init__.py @@ -287,7 +287,9 @@ def process_and_write(self, entries: List[Any]): number_of_entries = len(to_save) if number_of_entries < 1: self.logger.info( - "No log entries to output, skipping.", + "No log entries to output for stream, skipping.", + stream=stream, + descriptor=descriptor, extra=self.log_context, ) continue @@ -698,7 +700,7 @@ def process(self, entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ # Shortcut where there are no processors configured. if len(self._processors) < 1: - return entries + return [] # As processors can modify the number of entries, we need to loop over them # multiple times. From b6f8b9ed6692f014a3ef9dc37c09fe4ee689cf5b Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Thu, 13 Jul 2023 11:20:27 +0100 Subject: [PATCH 10/12] Fix up logger error. --- grove/connectors/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/grove/connectors/__init__.py b/grove/connectors/__init__.py index 73f09e9..19bc850 100644 --- a/grove/connectors/__init__.py +++ b/grove/connectors/__init__.py @@ -288,9 +288,11 @@ def process_and_write(self, entries: List[Any]): if number_of_entries < 1: self.logger.info( "No log entries to output for stream, skipping.", - stream=stream, - descriptor=descriptor, - extra=self.log_context, + extra={ + "stream": stream, + "descriptor": descriptor, + **self.log_context, + }, ) continue From e607416112756d6b1883a42981e3a4117e0cd0e9 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:27:41 +0100 Subject: [PATCH 11/12] Update grove/outputs/__init__.py Co-authored-by: Jonas Plum --- grove/outputs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grove/outputs/__init__.py b/grove/outputs/__init__.py index b4c9121..b5efa3e 100644 --- a/grove/outputs/__init__.py +++ b/grove/outputs/__init__.py @@ -90,7 +90,7 @@ def serialize(self, data: List[Any], metadata: Dict[str, Any] = {}) -> bytes: # This is expensive but we can't just json.dumps into gzip.compress as that # will not yield NDJSON. for entry in data: - # Skip entry log entries. + # Skip empty log entries. if entry is None: continue From de7009e25e184139ffe03c737dfcdce55997b191 Mon Sep 17 00:00:00 2001 From: Peter Adkins <74542596+hcpadkins@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:28:44 +0100 Subject: [PATCH 12/12] Address pull-request comments. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5dfa805..21aba33 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@



- + Grove

@@ -28,7 +28,7 @@ us via email at security@hashicorp.com, rather than filing a GitHub issue.



- + Grove support overview