diff --git a/docs/alert_aggregators/basic_concatenation.md b/docs/alert_aggregators/basic_concatenation.md index 5e37b904..d7c60c1c 100644 --- a/docs/alert_aggregators/basic_concatenation.md +++ b/docs/alert_aggregators/basic_concatenation.md @@ -15,6 +15,7 @@ alert_aggregators: BasicConcatAggregator: method_type: "basic_concat_aggregator" buffer_size: 3 + deduplicate_values: False auto_config: False ``` diff --git a/docs/examples/alert_aggregators/basic_concat.py b/docs/examples/alert_aggregators/basic_concat.py index ea1bc53a..ce66e7c6 100644 --- a/docs/examples/alert_aggregators/basic_concat.py +++ b/docs/examples/alert_aggregators/basic_concat.py @@ -10,6 +10,7 @@ "BasicConcatAggregator": { "method_type": "basic_concat_aggregator", "buffer_size": 3, + "deduplicate_values": False, "auto_config": False, } } diff --git a/src/detectmatelibrary/_testutils/dummy_alert_aggregator.py b/src/detectmatelibrary/_testutils/dummy_alert_aggregator.py new file mode 100644 index 00000000..ccc13058 --- /dev/null +++ b/src/detectmatelibrary/_testutils/dummy_alert_aggregator.py @@ -0,0 +1,27 @@ +from detectmatelibrary.common.alert_aggregator import (CoreAlertAggregatorConfig, CoreAlertAggregator) +from detectmatelibrary.schemas import DetectorSchema, AggregateSchema +from typing import Any, Optional + + +class DummyAlertAggregatorConfig(CoreAlertAggregatorConfig): + method_type: str = "dummy_alert_aggregator" + buffer_size: int = 3 + + +class DummyAlertAggregator(CoreAlertAggregator): + def __init__( + self, + name: str = "DummyAlertAggregator", + config: Optional[DummyAlertAggregatorConfig | dict[str, Any]] = DummyAlertAggregatorConfig(), + ) -> None: + if isinstance(config, dict): + config = DummyAlertAggregatorConfig.from_dict(config, name) + buffer_size: int = config.buffer_size # type: ignore + super().__init__(name=name, buffer_size=buffer_size, config=config) + + def aggregate_alerts( + self, input_: list[DetectorSchema] | DetectorSchema, output_: AggregateSchema + ) -> bool: + output_["description"] = "Dummy alert aggregation" + output_["alertsObtain"]["type"] = "Anomalies aggregated by DummyAlertAggregator" + return True diff --git a/src/detectmatelibrary/_testutils/dummy_detector.py b/src/detectmatelibrary/_testutils/dummy_detector.py index b4277883..4fb0e931 100644 --- a/src/detectmatelibrary/_testutils/dummy_detector.py +++ b/src/detectmatelibrary/_testutils/dummy_detector.py @@ -1,7 +1,7 @@ from detectmatelibrary.common.detector import CoreDetector, CoreDetectorConfig from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary import schemas -from typing import List, Any +from typing import List, Any, Optional class DummyDetectorConfig(CoreDetectorConfig): @@ -15,12 +15,14 @@ class DummyDetector(CoreDetector): def __init__( self, name: str = "DummyDetector", + buffer_mode: BufferMode = BufferMode.NO_BUF, + buffer_size: Optional[int] = None, config: DummyDetectorConfig | dict[str, Any] = DummyDetectorConfig() ) -> None: if isinstance(config, dict): config = DummyDetectorConfig.from_dict(config, name) - super().__init__(name=name, buffer_mode=BufferMode.NO_BUF, config=config) + super().__init__(name=name, buffer_mode=buffer_mode, buffer_size=buffer_size, config=config) self._call_count = 0 def detect( diff --git a/src/detectmatelibrary/alert_aggregation/basic_concat.py b/src/detectmatelibrary/alert_aggregation/basic_concat.py index 45883cf8..e53d52ed 100644 --- a/src/detectmatelibrary/alert_aggregation/basic_concat.py +++ b/src/detectmatelibrary/alert_aggregation/basic_concat.py @@ -1,15 +1,12 @@ -from detectmatelibrary.common.alert_aggregator import ( - CoreAlertAggregatorConfig, CoreAlertAggregator -) - +from detectmatelibrary.common.alert_aggregator import (CoreAlertAggregatorConfig, CoreAlertAggregator) from detectmatelibrary.schemas import DetectorSchema, AggregateSchema - from typing import Any, Optional class BasicConcatAggregationConfig(CoreAlertAggregatorConfig): method_type: str = "basic_concat_aggregator" buffer_size: int = 3 + deduplicate_values: bool = False class BasicConcatAggregation(CoreAlertAggregator): @@ -20,13 +17,18 @@ def __init__( ) -> None: if isinstance(config, dict): config = BasicConcatAggregationConfig.from_dict(config, name) - buffer_size: int = config.buffer_size # type: ignore + self.deduplicate_values: bool = config.deduplicate_values # type: ignore super().__init__(name=name, buffer_size=buffer_size, config=config) def aggregate_alerts( - self, input_: list[DetectorSchema], output_: AggregateSchema # type: ignore + self, input_: list[DetectorSchema] | DetectorSchema, output_: AggregateSchema ) -> bool: - output_["description"] = "Basic aggregation by alert concatenation" + if self.deduplicate_values: + output_["detectorIDs"] = list(dict.fromkeys(output_["detectorIDs"])) + output_["detectorTypes"] = list(dict.fromkeys(output_["detectorTypes"])) + output_["alertIDs"] = list(dict.fromkeys(output_["alertIDs"])) + output_["logIDs"] = list(dict.fromkeys(output_["logIDs"])) + output_["extractedTimestamps"] = list(dict.fromkeys(output_["extractedTimestamps"])) return True diff --git a/src/detectmatelibrary/tools/workspace/create_workspace.py b/src/detectmatelibrary/tools/workspace/create_workspace.py index 0bfb3480..21ec1e88 100644 --- a/src/detectmatelibrary/tools/workspace/create_workspace.py +++ b/src/detectmatelibrary/tools/workspace/create_workspace.py @@ -14,7 +14,8 @@ META_FILES = ["LICENSE.md", ".gitignore", ".pre-commit-config.yaml"] DATA_FILES = { "parser": TEMPLATE_DIR / "data/logs.json", - "detector": TEMPLATE_DIR / "data/parsed_log.json" + "detector": TEMPLATE_DIR / "data/parsed_log.json", + "alert_aggregator": TEMPLATE_DIR / "data/alerts.json" } @@ -38,12 +39,14 @@ def create_tests(type_: str, name: str, workspace_root: Path, pkg_name: str) -> - Reads template tests from src/tools/workspace/templates/test_templates/ - Rewrites the import to point to . - - Renames CustomParser/CustomDetector to the camelized class name + - Renames CustomParser/CustomDetector/CustomAlertAggregator to the camelized class name """ tests_dir = workspace_root / "tests" tests_dir.mkdir(parents=True, exist_ok=True) - template_file = TEMPLATE_DIR / "test_templates" / f"test_Custom{type_.capitalize()}.py" + # base names in the template (CustomParser/CustomDetector/CustomAlertAggregator) + base_class = f"Custom{"".join(part.capitalize() for part in type_.split("_"))}" + template_file = TEMPLATE_DIR / "test_templates" / f"test_{base_class}.py" test_file = tests_dir / f"test_{name}.py" if not template_file.exists(): @@ -52,7 +55,6 @@ def create_tests(type_: str, name: str, workspace_root: Path, pkg_name: str) -> return template_content = template_file.read_text() - base_class = f"Custom{type_.capitalize()}" # base names in the template (CustomParser/CustomDetector) # The exact import line in the template, e.g: # from ..CustomParser import CustomParser, CustomParserConfig # from ..CustomDetector import CustomDetector, CustomDetectorConfig @@ -98,7 +100,7 @@ def create_workspace(type_: str, name: str, target_dir: Path) -> None: pkg_dir.mkdir(parents=True, exist_ok=False) # Template selection - template_file = TEMPLATE_DIR / f"Custom{type_.capitalize()}.py" + template_file = TEMPLATE_DIR / f"Custom{"".join(part.capitalize() for part in type_.split("_"))}.py" module_name = normalize_package_name(name) target_code_file = pkg_dir / f"{module_name}.py" @@ -109,7 +111,7 @@ def create_workspace(type_: str, name: str, target_dir: Path) -> None: template_content = template_file.read_text() # Replace default class name inside template - original_class = f"Custom{type_.capitalize()}" + original_class = f"Custom{"".join(part.capitalize() for part in type_.split("_"))}" new_class = camelize(name) template_content = template_content.replace(original_class, new_class) template_content = template_content.replace(f"custom_{type_}", f"{name}_{type_}") @@ -164,7 +166,7 @@ def main() -> None: create_cmd.add_argument( "--type", required=True, - choices=["parser", "detector"], + choices=["parser", "detector", "alert_aggregator"], help="Type of component to generate", ) diff --git a/src/detectmatelibrary/tools/workspace/templates/CustomAlertAggregator.py b/src/detectmatelibrary/tools/workspace/templates/CustomAlertAggregator.py new file mode 100644 index 00000000..fdfa9d4b --- /dev/null +++ b/src/detectmatelibrary/tools/workspace/templates/CustomAlertAggregator.py @@ -0,0 +1,35 @@ +from detectmatelibrary.common.alert_aggregator import (CoreAlertAggregatorConfig, CoreAlertAggregator) +from detectmatelibrary.schemas import DetectorSchema, AggregateSchema +from typing import Any, Optional +from detectmatelibrary.helper.from_to import From + + +class CustomAlertAggregatorConfig(CoreAlertAggregatorConfig): + method_type: str = "custom_alert_aggregator" + buffer_size: int = 3 + + +class CustomAlertAggregator(CoreAlertAggregator): + def __init__( + self, + name: str = "CustomAlertAggregator", + config: Optional[CustomAlertAggregatorConfig | dict[str, Any]] = CustomAlertAggregatorConfig(), + ) -> None: + if isinstance(config, dict): + config = CustomAlertAggregatorConfig.from_dict(config, name) + buffer_size: int = config.buffer_size # type: ignore + super().__init__(name=name, buffer_size=buffer_size, config=config) + + def aggregate_alerts( + self, input_: list[DetectorSchema] | DetectorSchema, output_: AggregateSchema + ) -> bool: + output_["description"] = "Custom alert aggregation" + output_["alertsObtain"]["type"] = "Anomalies aggregated by CustomAlertAggregator" + return True + + +if __name__ == "__main__": + print(aggregator := CustomAlertAggregator()) + print("Running with data...") + for alerts in From.json(aggregator, "data.json"): + print(alerts) diff --git a/src/detectmatelibrary/tools/workspace/templates/CustomDetector.py b/src/detectmatelibrary/tools/workspace/templates/CustomDetector.py index 07f6b8db..c36aea09 100644 --- a/src/detectmatelibrary/tools/workspace/templates/CustomDetector.py +++ b/src/detectmatelibrary/tools/workspace/templates/CustomDetector.py @@ -1,5 +1,4 @@ from typing import Any, List - from detectmatelibrary.common.detector import CoreDetector, CoreDetectorConfig from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary.helper.from_to import From @@ -24,7 +23,6 @@ def __init__( name: str = "CustomDetector", config: CustomDetectorConfig | dict[str, Any] = CustomDetectorConfig(), ) -> None: - # Allow passing either a config instance or a plain dict if isinstance(config, dict): config = CustomDetectorConfig.from_dict(config, name) @@ -43,9 +41,7 @@ def detect( :param output_: DetectorSchema instance to be mutated in-place :return: Detection result (True/False) """ - output_["description"] = "Dummy detection process" # Description of the detection - # Alternating pattern: True, False, True, False, etc self._call_count += 1 pattern = [True, False] @@ -53,14 +49,11 @@ def detect( if result: output_["score"] = 1.0 # Score of the detector output_["alertsObtain"]["type"] = "Anomaly detected by CustomDetector" # Additional info - return result if __name__ == "__main__": - print(detector := CustomDetector()) - print("Running with data...") for alerts in From.json(detector, "data.json"): print(alerts) diff --git a/src/detectmatelibrary/tools/workspace/templates/CustomParser.py b/src/detectmatelibrary/tools/workspace/templates/CustomParser.py index a40a932f..fd7f98a4 100644 --- a/src/detectmatelibrary/tools/workspace/templates/CustomParser.py +++ b/src/detectmatelibrary/tools/workspace/templates/CustomParser.py @@ -1,5 +1,4 @@ from typing import Any - from detectmatelibrary.common.parser import CoreParser, CoreParserConfig from detectmatelibrary.helper.from_to import From from detectmatelibrary import schemas @@ -25,7 +24,6 @@ def __init__( # Allow passing either a config instance or a plain dict if isinstance(config, dict): config = CustomParserConfig.from_dict(config, name) - super().__init__(name=name, config=config) def parse( @@ -39,7 +37,6 @@ def parse( :param output_: Parser output schema instance to be mutated in- place """ - # Dummy implementation example (replace with real logic) output_["EventID"] = 2 # Number of the log template output_["variables"].extend(["dummy_variable"]) # Variables found in the log @@ -47,9 +44,7 @@ def parse( if __name__ == "__main__": - print(parser := CustomParser()) - print("Running with data...") for parsed_log in From.json(parser, "data.json"): print(parsed_log) diff --git a/src/detectmatelibrary/tools/workspace/templates/data/alerts.json b/src/detectmatelibrary/tools/workspace/templates/data/alerts.json new file mode 100644 index 00000000..cc8fd2e2 --- /dev/null +++ b/src/detectmatelibrary/tools/workspace/templates/data/alerts.json @@ -0,0 +1,194 @@ +{ + "0": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_10", + "detectionTimestamp": 1790969620, + "logIDs": ["0"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "1": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_11", + "detectionTimestamp": 1790969620, + "logIDs": ["1"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "2": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_12", + "detectionTimestamp": 1790969620, + "logIDs": ["2"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "3": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_13", + "detectionTimestamp": 1790969620, + "logIDs": ["3"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "4": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_14", + "detectionTimestamp": 1790969620, + "logIDs": ["4"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "5": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_15", + "detectionTimestamp": 1790969620, + "logIDs": ["5"], + "score": 1, + "extractedTimestamps": [1642723741], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "6": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_16", + "detectionTimestamp": 1790969620, + "logIDs": ["6"], + "score": 1, + "extractedTimestamps": [1642723752], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "7": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_17", + "detectionTimestamp": 1790969620, + "logIDs": ["7"], + "score": 1, + "extractedTimestamps": [1642723752], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "8": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_18", + "detectionTimestamp": 1790969620, + "logIDs": ["8"], + "score": 1, + "extractedTimestamps": [1642724221], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "9": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_19", + "detectionTimestamp": 1790969620, + "logIDs": ["9"], + "score": 1, + "extractedTimestamps": [1642724221], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "10": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_20", + "detectionTimestamp": 1790969620, + "logIDs": ["10"], + "score": 1, + "extractedTimestamps": [1642724221], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + }, + "11": { + "__version__": "1.0.0", + "detectorID": "MyCoolThing", + "detectorType": "MyCoolThing_detector", + "alertID": "MyCoolThing_21", + "detectionTimestamp": 1790969620, + "logIDs": ["11"], + "score": 1, + "extractedTimestamps": [1642724221], + "description": "Dummy detection process", + "receivedTimestamp": 1790969620, + "alertsObtain": { + "key": "type", + "value": "Anomaly detected by MyCoolThing" + } + } +} diff --git a/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomAlertAggregator.py b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomAlertAggregator.py new file mode 100644 index 00000000..ff111148 --- /dev/null +++ b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomAlertAggregator.py @@ -0,0 +1,49 @@ +import os.path + +from ..CustomAlertAggregator import CustomAlertAggregator, CustomAlertAggregatorConfig +from detectmatelibrary.helper.from_to import From + + +default_args = { + "alert_aggregators": { + "CustomAlertAggregator": { + "method_type": "custom_alert_aggregator", + "auto_config": False, + "params": {}, + } + } +} + + +class TestCustomAlertAggregator: + def test_initialize_default(self) -> None: + alert_aggregator = CustomAlertAggregator(name="CustomAlertAggregator", config=default_args) + assert isinstance(alert_aggregator, CustomAlertAggregator) + assert alert_aggregator.name == "CustomAlertAggregator" + assert isinstance(alert_aggregator.config, CustomAlertAggregatorConfig) + + def test_run_aggregate_alerts_method(self) -> None: + alert_aggregator = CustomAlertAggregator(name="CustomAlertAggregator", config=default_args) + + if os.path.exists("data.json"): + gen = From.json(alert_aggregator, "data.json", do_process=False) + else: + gen = From.json(alert_aggregator, os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../data/alerts.json"), do_process=False) + i = 0 + while True: + try: + data = next(gen) + except StopIteration: + break + output = alert_aggregator.process(data) + if i % 3 == 2: + assert getattr(output, "description") == "Custom alert aggregation" + assert getattr(output, "detectorIDs") == ["MyCoolThing"]*3 + assert getattr(output, "alertsObtain")["type"] == \ + "Anomalies aggregated by CustomAlertAggregator" + assert getattr(output, "detectorTypes") == ["MyCoolThing_detector"]*3 + assert getattr(output, "alertIDs") == [ + f"MyCoolThing_{i + 8}", f"MyCoolThing_{i + 9}", f"MyCoolThing_{i + 10}"] + assert getattr(output, "logIDs") == [str(i-2), str(i-1), str(i)] + i += 1 diff --git a/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomDetector.py b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomDetector.py index a0e6414b..48685722 100644 --- a/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomDetector.py +++ b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomDetector.py @@ -1,6 +1,6 @@ -from typing import Any -from detectmatelibrary import schemas +import os from ..CustomDetector import CustomDetector, CustomDetectorConfig +from detectmatelibrary.helper.from_to import From default_args = { @@ -23,15 +23,24 @@ def test_initialize_default(self) -> None: def test_run_detect_method(self) -> None: detector = CustomDetector() - data = schemas.ParserSchema({"log": "test log"}) - output: Any = schemas.DetectorSchema() - - result = detector.detect(data, output) - - assert output.description == "Dummy detection process" - if result: - assert output.score == 1.0 - assert "Anomaly detected" in output.alertsObtain["type"] + if os.path.exists("data.json"): + gen = From.json(detector, "data.json", do_process=False) else: - assert output.score == 0.0 - assert len(output.alertsObtain) == 0 + gen = From.json(detector, os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../data/parsed_log.json"), do_process=False) + i = 0 + while True: + try: + data = next(gen) + except StopIteration: + break + assert detector.process(data) is None + output = detector.process(data) + assert getattr(output, "description") == "Dummy detection process" + assert getattr(output, "score") == 1.0 + assert getattr(output, "alertsObtain")["type"] == "Anomaly detected by CustomDetector" + assert getattr(output, "detectorID") == "CustomDetector" + assert getattr(output, "detectorType") == "custom_detector" + assert getattr(output, "alertID") == f"CustomDetector_{i + 10}" + assert getattr(output, "logIDs") == [str(i)] + i += 1 diff --git a/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomParser.py b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomParser.py index 855cd6d7..e7328a09 100644 --- a/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomParser.py +++ b/src/detectmatelibrary/tools/workspace/templates/test_templates/test_CustomParser.py @@ -1,6 +1,6 @@ -from typing import Any -from detectmatelibrary import schemas +import os from ..CustomParser import CustomParser, CustomParserConfig +from detectmatelibrary.helper.from_to import From default_args = { @@ -23,10 +23,24 @@ def test_initialize_default(self) -> None: def test_run_parse_method(self) -> None: parser = CustomParser() - input_data = schemas.LogSchema({"log": "test log"}) - output_data: Any = schemas.ParserSchema() - - parser.parse(input_data, output_data) - - assert output_data.variables == ["dummy_variable"] - assert output_data.template == "This is a dummy template" + if os.path.exists("data.json"): + gen = From.json(parser, "data.json", do_process=False) + else: + gen = From.json(parser, os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../data/logs.json"), do_process=False) + i = 0 + while True: + try: + data = next(gen) + except StopIteration: + break + output = parser.process(data) + assert getattr(output, "EventID") == 2 + assert getattr(output, "template") == "This is a dummy template" + assert getattr(output, "logFormatVariables")["Time"] == "0" + assert getattr(output, "parserID") == "CustomParser" + assert getattr(output, "parserType") == "custom_parser" + assert getattr(output, "variables") == ["dummy_variable"] + assert getattr(output, "parsedLogID") == f"CustomParser_{i + 10}" + assert getattr(output, "logID") == str(i) + i += 1 diff --git a/tests/test_alert_aggregators/test_basic_concat.py b/tests/test_alert_aggregators/test_basic_concat.py index d07e1b49..221c14ff 100644 --- a/tests/test_alert_aggregators/test_basic_concat.py +++ b/tests/test_alert_aggregators/test_basic_concat.py @@ -1,58 +1,96 @@ from detectmatelibrary.alert_aggregation.basic_concat import BasicConcatAggregation - - +from detectmatelibrary.schemas import AggregateSchema from detectmatelibrary.utils.aux import time_test_mode - import detectmatelibrary.schemas as schemas +from datetime import datetime time_test_mode() aggregations_config = { "alert_aggregators": { - "Buffer_3": { + "Buffer_4": { "method_type": "basic_concat_aggregator", - "buffer_size": 3, + "buffer_size": 4, + "auto_config": False, + "params": {}, }, - "Buffer_5": { + "Buffer_6": { "method_type": "basic_concat_aggregator", - "buffer_size": 5, + "buffer_size": 6, + "deduplicate_values": True, + "auto_config": False, + "params": {}, }, - } + }, + "auto_config": False } -dummy_schema = { +data_global = [schemas.DetectorSchema({ + "detectorID": str(i), + "detectorType": "dummy", + "alertID": str(i), + "detectionTimestamp": 0, + "logIDs": [f"logID{i}"], + "score": 0.2 + i / 10, + "extractedTimestamps": [int(datetime.now().timestamp() + i)], + "description": "hello there", + "receivedTimestamp": 1, + "alertsObtain": {"99 problems": "but logs aint one"} +}) for i in range(1, 4)] + +data_global += [schemas.DetectorSchema({ "detectorID": "1", "detectorType": "dummy", - "alertID": "2", + "alertID": "1", "detectionTimestamp": 0, - "logIDs": ["logID1"], - "score": 0.2, - "extractedTimestamps": [-1], + "logIDs": [f"logID{1}"], + "score": 0.3, + "extractedTimestamps": [int(datetime.now().timestamp() + 1)], "description": "hello there", "receivedTimestamp": 1, "alertsObtain": {"99 problems": "but logs aint one"} -} +}) for i in range(1, 4)] class TestBasicAggregation: - def test_with_buffer_3(self): - alert_aggregator = BasicConcatAggregation("Buffer_3", aggregations_config) - data = schemas.DetectorSchema(dummy_schema).serialize() - - assert alert_aggregator.process(data) is None - assert alert_aggregator.process(data) is None - assert (result := alert_aggregator.process(data)) is not None - assert isinstance(result, bytes) - - def test_with_buffer_5(self): - alert_aggregator = BasicConcatAggregation("Buffer_5", aggregations_config) - data = schemas.DetectorSchema(dummy_schema).serialize() - - assert alert_aggregator.process(data) is None - assert alert_aggregator.process(data) is None - assert alert_aggregator.process(data) is None - assert alert_aggregator.process(data) is None - assert (result := alert_aggregator.process(data)) is not None - assert isinstance(result, bytes) + def test_with_buffer_4(self): + alert_aggregator = BasicConcatAggregation("Buffer_4", aggregations_config) + data = data_global + + assert alert_aggregator.process(data[0]) is None + assert alert_aggregator.process(data[1]) is None + assert alert_aggregator.process(data[2]) is None + result = alert_aggregator.process(data[3]) + assert result is not None + assert isinstance(result, AggregateSchema) + res_dict = result.as_dict() + assert res_dict["detectorIDs"] == ["1", "2", "3", "1"] + assert res_dict["alertIDs"] == ["1", "2", "3", "1"] + assert res_dict["outputTimestamp"] == 0 + assert res_dict["logIDs"] == ["logID1", "logID2", "logID3", "logID1"] + assert len(res_dict["extractedTimestamps"]) == 4 + assert res_dict["description"] == "Basic aggregation by alert concatenation" + assert res_dict["detectorTypes"] == ["dummy"]*4 + + def test_with_buffer_6(self): + alert_aggregator = BasicConcatAggregation("Buffer_6", aggregations_config) + data = data_global + + assert alert_aggregator.process(data[0]) is None + assert alert_aggregator.process(data[1]) is None + assert alert_aggregator.process(data[2]) is None + assert alert_aggregator.process(data[3]) is None + assert alert_aggregator.process(data[4]) is None + result = alert_aggregator.process(data[5]) + assert result is not None + assert isinstance(result, AggregateSchema) + res_dict = result.as_dict() + assert res_dict["detectorIDs"] == ["1", "2", "3"] + assert res_dict["alertIDs"] == ["1", "2", "3"] + assert res_dict["outputTimestamp"] == 0 + assert res_dict["logIDs"] == ["logID1", "logID2", "logID3"] + assert len(res_dict["extractedTimestamps"]) == 3 + assert res_dict["description"] == "Basic aggregation by alert concatenation" + assert res_dict["detectorTypes"] == ["dummy"] diff --git a/tests/test_data/logs.log b/tests/test_data/logs.log index f9a8ab75..7fe515f7 100644 --- a/tests/test_data/logs.log +++ b/tests/test_data/logs.log @@ -6,4 +6,13 @@ type=CONFIG_CHANGE msg=audit(1757673850.283:4): op=set audit_failure=1 old=1 aui type=SYSCALL msg=audit(1757673850.283:4): arch=c00000b7 syscall=206 success=yes exit=60 a0=3 a1=7fffc27ad8f0 a2=3c a3=0 items=0 ppid=696 pid=710 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null)ARCH=aarch64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root" type=PROCTITLE msg=audit(1757673850.283:4): proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F61756469742E72756C6573 type=CONFIG_CHANGE msg=audit(1757673850.283:5): op=set audit_backlog_wait_time=60000 old=15000 auid=4294967295 ses=4294967295 res=1AUID="unset" +type=SYSCALL msg=audit(1757673850.283:5): arch=c00000b7 syscall=206 success=yes exit=60 a0=3 a1=7fffc27ad8f0 a2=3c a3=0 items=0 ppid=696 pid=710 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null)ARCH=aarch64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root" +type=DAEMON_START msg=audit(1757673850.240:4506): op=start ver=3.0.9 format=enriched kernel=6.6.62+rpt-rpi-2712 auid=4294967295 pid=691 uid=0 ses=4294967295 res=successAUID="unset" UID="root" +type=CONFIG_CHANGE msg=audit(1757673850.283:3): op=set audit_backlog_limit=8192 old=64 auid=4294967295 ses=4294967295 res=1AUID="unset" +type=SYSCALL msg=audit(1757673850.283:3): arch=c00000b7 syscall=206 success=yes exit=60 a0=3 a1=7fffc27ad8f0 a2=3c a3=0 items=0 ppid=696 pid=710 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null)ARCH=aarch64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root" +type=PROCTITLE msg=audit(1757673850.283:3): proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F61756469742E72756C6573 +type=CONFIG_CHANGE msg=audit(1757673850.283:4): op=set audit_failure=1 old=1 auid=4294967295 ses=4294967295 res=1AUID="unset" +type=SYSCALL msg=audit(1757673850.283:4): arch=c00000b7 syscall=206 success=yes exit=60 a0=3 a1=7fffc27ad8f0 a2=3c a3=0 items=0 ppid=696 pid=710 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null)ARCH=aarch64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root" +type=PROCTITLE msg=audit(1757673850.283:4): proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F61756469742E72756C6573 +type=CONFIG_CHANGE msg=audit(1757673850.283:5): op=set audit_backlog_wait_time=60000 old=15000 auid=4294967295 ses=4294967295 res=1AUID="unset" type=SYSCALL msg=audit(1757673850.283:5): arch=c00000b7 syscall=206 success=yes exit=60 a0=3 a1=7fffc27ad8f0 a2=3c a3=0 items=0 ppid=696 pid=710 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null)ARCH=aarch64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root" \ No newline at end of file diff --git a/tests/test_pipelines/test_basic_pipelines.py b/tests/test_pipelines/test_basic_pipelines.py index 53d95d70..4bf466ab 100644 --- a/tests/test_pipelines/test_basic_pipelines.py +++ b/tests/test_pipelines/test_basic_pipelines.py @@ -1,5 +1,7 @@ -from detectmatelibrary.common.detector import CoreDetector, BufferMode -from detectmatelibrary.common.parser import CoreParser +from detectmatelibrary._testutils.dummy_parser import DummyParser +from detectmatelibrary._testutils.dummy_detector import DummyDetector +from detectmatelibrary._testutils.dummy_alert_aggregator import DummyAlertAggregator +from detectmatelibrary.common.detector import BufferMode from detectmatelibrary.detectors.new_value_combo_detector import NewValueComboDetector from detectmatelibrary.detectors.new_value_detector import NewValueDetector from detectmatelibrary.detectors.random_detector import RandomDetector @@ -9,87 +11,106 @@ import yaml -class MockupParser(CoreParser): - def parse(self, input_, output_): - output_.EventID = 1 - output_.template = "hello" - output_.variables.extend(["a", "b"]) - - -class MockupDetector(CoreDetector): - def detect(self, input_, output_): - output_.score = 0.9 - output_.description = "ciao" - - return True - - config = { "parsers": { "dummy_parser": { - "method_type": "core_parser", + "method_type": "dummy_parser", "auto_config": False, "params": {} } }, "detectors": { "dummy_detector": { - "method_type": "core_detector", + "method_type": "dummy_detector", "auto_config": False, "params": {} } }, + "alert_aggregators": { + "dummy_alert_aggregator": { + "method_type": "dummy_alert_aggregator", + "auto_config": False, + "params": {} + } + } } class TestCaseBasicPipelines: """This pipelines should not crash.""" def test_basic_pipeline(self) -> None: - parser = MockupParser(name="dummy_parser", config=config) - detector = MockupDetector( + parser = DummyParser(name="dummy_parser", config=config) + detector = DummyDetector( name="dummy_detector", config=config, buffer_mode=BufferMode.NO_BUF, buffer_size=None, ) + aggregator = DummyAlertAggregator(name="dummy_alert_aggregator", config=config) - assert (parsed_log := next(From.log(parser, LOG_PATH))) is not None - assert detector.process(parsed_log) is not None + gen = From.log(parser, LOG_PATH) + for i in range(5): + parsed_log = next(gen) + assert parsed_log is not None + processed_data = detector.process(parsed_log) + assert (processed_data is None, processed_data is not None)[i % 2] + if processed_data: + assert aggregator.process(processed_data) is None + parsed_log = next(gen) + processed_data = detector.process(parsed_log) + assert processed_data is not None + assert aggregator.process(processed_data) is not None def test_window_pipeline(self) -> None: - parser = MockupParser(name="dummy_parser", config=config) - detector = MockupDetector( + parser = DummyParser(name="dummy_parser", config=config) + detector = DummyDetector( name="dummy_detector", config=config, buffer_mode=BufferMode.WINDOW, buffer_size=3, ) + aggregator = DummyAlertAggregator(name="dummy_alert_aggregator", config=config) gen = From.log(parser, LOG_PATH) - for _ in range(2): + for i in range(7): parsed_log = next(gen) - assert detector.process(parsed_log) is None - + processed_data = detector.process(parsed_log) + index = i % 2 + if i < 3: + index = 0 + assert (processed_data is None, processed_data is not None)[index] + if processed_data: + assert aggregator.process(processed_data) is None parsed_log = next(gen) - assert detector.process(parsed_log) is not None + processed_data = detector.process(parsed_log) + assert processed_data is not None + assert aggregator.process(processed_data) is not None def test_batch_pipeline(self) -> None: - - parser = MockupParser(name="dummy_parser", config=config) - detector = MockupDetector( + parser = DummyParser(name="dummy_parser", config=config) + detector = DummyDetector( name="dummy_detector", config=config, buffer_mode=BufferMode.BATCH, buffer_size=3, ) + aggregator = DummyAlertAggregator(name="dummy_alert_aggregator", config=config) gen = From.log(parser, LOG_PATH) - for _ in range(2): + for i in range(17): parsed_log = next(gen) - assert detector.process(parsed_log) is None + processed_data = detector.process(parsed_log) + index = i % 2 + if i < 3 or (i + 1) % 3 != 0: + index = 0 + assert (processed_data is None, processed_data is not None)[index] + if processed_data: + assert aggregator.process(processed_data) is None parsed_log = next(gen) - assert detector.process(parsed_log) is not None + processed_data = detector.process(parsed_log) + assert processed_data is not None + assert aggregator.process(processed_data) is not None class TestExamples: diff --git a/tests/test_workspace/test_create_workspace.py b/tests/test_workspace/test_create_workspace.py index d717c910..3f6c55da 100644 --- a/tests/test_workspace/test_create_workspace.py +++ b/tests/test_workspace/test_create_workspace.py @@ -89,6 +89,39 @@ def test_create_detector_workspace(temp_dir: Path): assert (tests_dir / f"test_{ws_name}.py").exists() +@pytest.mark.ignored +def test_create_alert_aggregator_workspace(temp_dir: Path): + ws_name = "myAlertAggregator" + workspace_root = temp_dir + pkg_name = normalize_package_name(ws_name) # myalertaggregator + module_name = normalize_package_name(ws_name) # myalertaggregator + pkg_dir = workspace_root / pkg_name + tests_dir = workspace_root / "tests" + + subprocess.check_call([ + *CLI, + "create", + "--type", "alert_aggregator", + "--name", ws_name, + "--dir", str(workspace_root), + ]) + + assert workspace_root.exists() + assert pkg_dir.exists() + + assert (workspace_root / "LICENSE.md").exists() + assert (workspace_root / ".gitignore").exists() + assert (workspace_root / ".pre-commit-config.yaml").exists() + assert (workspace_root / "README.md").exists() + + py_files = list(pkg_dir.glob("*.py")) + assert len(py_files) == 2 # __init__.py + myAlertAggrogator.py + assert (pkg_dir / f"{module_name}.py").exists() + assert (pkg_dir / "__init__.py").exists() + assert tests_dir.exists() + assert (tests_dir / f"test_{ws_name}.py").exists() + + @pytest.mark.ignored def test_create_workspace_with_dash_name(temp_dir: Path): ws_name = "custom-parser" @@ -209,3 +242,41 @@ def test_generated_parser_tests_pass(temp_dir: Path): finally: os.chdir(old_cwd) sys.path[:] = old_sys_path + + +@pytest.mark.ignored +def test_generated_alert_aggregator_tests_pass(temp_dir: Path): + """Run pytest inside the generated workspace on the generated detector test + file.""" + + ws_name = "MyCoolAlertAggregator" + workspace_root = temp_dir + pkg_dir = workspace_root / "mycoolalertaggregator" + tests_dir = workspace_root / "tests" + test_file = tests_dir / f"test_{ws_name}.py" + + subprocess.check_call([ + *CLI, + "create", + "--type", "alert_aggregator", + "--name", ws_name, + "--dir", str(workspace_root), + ]) + + assert workspace_root.exists() + assert pkg_dir.exists() + assert tests_dir.exists() + assert test_file.exists() + + # run pytest on the generated test file + # and make sure the workspace root is on sys.path so "import mycoolalertaggregator" works + old_cwd = os.getcwd() + old_sys_path = list(sys.path) + try: + os.chdir(workspace_root) + sys.path.insert(0, str(workspace_root)) + result = pytest.main([str(test_file.relative_to(workspace_root))]) + assert result == 0 + finally: + os.chdir(old_cwd) + sys.path[:] = old_sys_path