From c4a87b753f09306436ba0c787669bc6a28b7edb5 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Mon, 18 Mar 2024 14:34:17 +0200 Subject: [PATCH 1/5] fix: validation (#975) --- keep/api/routes/workflows.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py index c27c545bdb..f4ae01077c 100644 --- a/keep/api/routes/workflows.py +++ b/keep/api/routes/workflows.py @@ -184,13 +184,20 @@ def run_workflow( # Finally, run it try: # if its event that was triggered by the UI with the Modal - if "test-workflow" in body.get("fingerprint", ""): + if "test-workflow" in body.get("fingerprint", "") or not body: # some random - body["id"] = body.get("fingerprint") + body["id"] = body.get("fingerprint", "manual-run") + body["name"] = body.get("fingerprint", "manual-run") body["lastReceived"] = datetime.datetime.now( tz=datetime.timezone.utc ).isoformat() - alert = AlertDto(**body) + try: + alert = AlertDto(**body) + except TypeError: + raise HTTPException( + status_code=400, + detail="Invalid alert format", + ) workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow( workflow_id, tenant_id, created_by, alert ) From e88bc165e5c68cb5260f82c3d57353460c0d36d0 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Mon, 18 Mar 2024 15:52:15 +0200 Subject: [PATCH 2/5] feat: auto supress (#978) --- examples/workflows/autosupress.yml | 16 ++++++++++++++++ keep/providers/mock_provider/mock_provider.py | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 examples/workflows/autosupress.yml diff --git a/examples/workflows/autosupress.yml b/examples/workflows/autosupress.yml new file mode 100644 index 0000000000..d9952098e3 --- /dev/null +++ b/examples/workflows/autosupress.yml @@ -0,0 +1,16 @@ +workflow: + id: autosupress + description: demonstrates how to automatically suppress alerts + triggers: + - type: alert + filters: + - key: name + value: r"(somename)" + actions: + - name: dismiss-alert + provider: + type: mock + with: + enrich_alert: + - key: dismissed + value: "true" diff --git a/keep/providers/mock_provider/mock_provider.py b/keep/providers/mock_provider/mock_provider.py index 12d207d283..a8af0f9458 100644 --- a/keep/providers/mock_provider/mock_provider.py +++ b/keep/providers/mock_provider/mock_provider.py @@ -23,6 +23,14 @@ def _query(self, **kwargs): """ return kwargs.get("command_output") + def _notify(self, **kwargs): + """This is mock provider that just return the command output. + + Returns: + _type_: _description_ + """ + return kwargs + def dispose(self): """ No need to dispose of anything, so just do nothing. From e0aa579fb881b48767fcc42dddde1e8b606aaf05 Mon Sep 17 00:00:00 2001 From: Daniel1Maymon Date: Mon, 18 Mar 2024 16:21:19 +0200 Subject: [PATCH 3/5] feat(doc): Update Keep CLI installation information requiring Python 3.11 (#976) Co-authored-by: Shahar Glazner --- docs/cli/installation.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cli/installation.mdx b/docs/cli/installation.mdx index 469c461783..3020dd4712 100644 --- a/docs/cli/installation.mdx +++ b/docs/cli/installation.mdx @@ -2,6 +2,14 @@ title: "Installation" --- Missing an installation? submit a new installation request and we will add it as soon as we can. + + +We recommend to install Keep CLI with Python version 3.11 for optimal compatibility and performance. +This choice ensures seamless integration with all dependencies, including pyarrow, which currently does not support Python 3.12 + + +Need Keep CLI on other versions? Feel free to contact us! + ## Clone and install (Option 1) ### Install From 75cb502121b46036b41d3fd005d62552a8fc59b1 Mon Sep 17 00:00:00 2001 From: Furkan Pehlivan <65170388+pehlicd@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:26:21 +0100 Subject: [PATCH 4/5] feat: add new functions (#977) Co-authored-by: Shahar Glazner --- docs/workflows/functions/last.mdx | 27 ++++++++++++++++++++++++++ docs/workflows/functions/lowercase.mdx | 24 +++++++++++++++++++++++ docs/workflows/functions/uppercase.mdx | 24 +++++++++++++++++++++++ keep/functions/__init__.py | 18 +++++++---------- tests/test_functions.py | 25 ++++++++++++++++-------- 5 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 docs/workflows/functions/last.mdx create mode 100644 docs/workflows/functions/lowercase.mdx create mode 100644 docs/workflows/functions/uppercase.mdx diff --git a/docs/workflows/functions/last.mdx b/docs/workflows/functions/last.mdx new file mode 100644 index 0000000000..a3ec04cee0 --- /dev/null +++ b/docs/workflows/functions/last.mdx @@ -0,0 +1,27 @@ +--- +title: "last(iterable)" +sidebarTitle: "last" +--- + +### Input + +An iterable. + +### Output + +The last item of the iterable. + +### Example + +```yaml +actions: + - name: keep-slack + foreach: "{{steps.this.results}}" + condition: + - type: threshold + value: "keep.last(keep.split({{ foreach.value }}, ' '))" + # each line looks like: + # '2023-02-09 20:08:16,773 INFO: uvicorn.access -: 127.0.0.1:53948 - "GET /test2 HTTP/1.1" 503' + # where the "503" is the number of the + compare_to: 200 +``` diff --git a/docs/workflows/functions/lowercase.mdx b/docs/workflows/functions/lowercase.mdx new file mode 100644 index 0000000000..e945f4b624 --- /dev/null +++ b/docs/workflows/functions/lowercase.mdx @@ -0,0 +1,24 @@ +--- +title: "string(string)" +sidebarTitle: "lowercase" +--- + +### Input + +A string. + +### Output + +Returns the string which is lowercased. + +### Example + +```yaml +actions: + - name: trigger-slack + condition: + - type: equals + value: keep.lowercase('ABC DEF') + compare_to: "abc def" + compare_type: eq +``` diff --git a/docs/workflows/functions/uppercase.mdx b/docs/workflows/functions/uppercase.mdx new file mode 100644 index 0000000000..45f3f66727 --- /dev/null +++ b/docs/workflows/functions/uppercase.mdx @@ -0,0 +1,24 @@ +--- +title: "string(string)" +sidebarTitle: "uppercase" +--- + +### Input + +A string. + +### Output + +Returns the string which is uppercased. + +### Example + +```yaml +actions: + - name: trigger-slack + condition: + - type: equals + value: keep.uppercase('abc def') + compare_to: "ABC DEF" + compare_type: eq +``` diff --git a/keep/functions/__init__.py b/keep/functions/__init__.py index 2e994a9461..6a17609472 100644 --- a/keep/functions/__init__.py +++ b/keep/functions/__init__.py @@ -15,37 +15,38 @@ def all(iterable) -> bool: g = groupby(iterable) return next(g, True) and not next(g, False) - def diff(iterable: iter) -> bool: # Opposite of all - returns True if any element is different return not all(iterable) - def len(iterable=[]) -> int: return _len(iterable) +def uppercase(string) -> str: + return string.upper() + +def lowercase(string) -> str: + return string.lower() def split(string, delimeter) -> list: return string.strip().split(delimeter) - def strip(string) -> str: return string.strip() - def first(iterable): return iterable[0] +def last(iterable): + return iterable[-1] def utcnow() -> datetime.datetime: dt = datetime.datetime.now(datetime.timezone.utc) return dt - def utcnowiso() -> str: return utcnow().isoformat() - def substract_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime: """ Substract minutes from a datetime object @@ -59,28 +60,23 @@ def substract_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime: """ return dt - datetime.timedelta(minutes=minutes) - def to_utc(dt: datetime.datetime | str) -> datetime.datetime: if isinstance(dt, str): dt = parser.parse(dt) utc_dt = dt.astimezone(pytz.utc) return utc_dt - def datetime_compare(t1, t2) -> float: diff = (t1 - t2).total_seconds() / 3600 return diff - def json_dumps(data: str | dict) -> str: if isinstance(data, str): data = json.loads(data) return json.dumps(data, indent=4, default=str) - def encode(string) -> str: return urllib.parse.quote(string) - def dict_to_key_value_list(d: dict) -> list: return [f"{k}:{v}" for k, v in d.items()] diff --git a/tests/test_functions.py b/tests/test_functions.py index 958b11335b..8af6e8086f 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -32,19 +32,18 @@ ("empty list", [], False), ], ) + def test_functions_diff(test_description, given, expected): assert ( functions.diff(given) == expected ), f"{test_description}: Expected {given} to return {expected}" - def test_keep_len_function(): """ Test the len function """ assert functions.len([1, 2, 3]) == 3 - def test_keep_all_function(): """ Test the all function @@ -52,7 +51,6 @@ def test_keep_all_function(): assert functions.all([1, 1, 1]) == True assert functions.all([1, 1, 0]) == False - def test_keep_diff_function(): """ Test the diff function @@ -60,7 +58,6 @@ def test_keep_diff_function(): assert functions.diff([1, 1, 1]) == False assert functions.diff([1, 1, 0]) == True - def test_keep_split_function(): """ Test the split function @@ -68,6 +65,17 @@ def test_keep_split_function(): assert functions.split("a,b,c", ",") == ["a", "b", "c"] assert functions.split("a|b|c", "|") == ["a", "b", "c"] +def test_keep_uppercase_function(): + """ + Test the uppercase function + """ + assert functions.uppercase("a") == "A" + +def test_keep_lowercase_function(): + """ + Test the lowercase function + """ + assert functions.lowercase("A") == "a" def test_keep_strip_function(): """ @@ -75,13 +83,17 @@ def test_keep_strip_function(): """ assert functions.strip(" a ") == "a" - def test_keep_first_function(): """ Test the first function """ assert functions.first([1, 2, 3]) == 1 +def test_keep_last_function(): + """ + Test the last function + """ + assert functions.last([1, 2, 3]) == 3 def test_keep_utcnow_function(): """ @@ -91,7 +103,6 @@ def test_keep_utcnow_function(): assert isinstance(dt.tzinfo, type(datetime.timezone.utc)) assert isinstance(dt, datetime.datetime) - def test_keep_to_utc_function(): """ Test the to_utc function @@ -103,7 +114,6 @@ def test_keep_to_utc_function(): now_utc = functions.to_utc(now) assert now_utc.tzinfo == pytz.utc - def test_keep_datetime_compare_function(): """ Test the datetime_compare function @@ -114,7 +124,6 @@ def test_keep_datetime_compare_function(): assert int(functions.datetime_compare(dt2, dt1)) == 1 assert int(functions.datetime_compare(dt1, dt1)) == 0 - def test_keep_encode_function(): """ Test the encode function From 5f5bb6fc987c5b6a3d82a502c78e97e59584a567 Mon Sep 17 00:00:00 2001 From: talboren Date: Mon, 18 Mar 2024 18:09:54 +0200 Subject: [PATCH 5/5] feat(provider): keep provider get distinct alerts with filter (#979) --- keep/api/core/db.py | 2 ++ keep/providers/keep_provider/keep_provider.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/keep/api/core/db.py b/keep/api/core/db.py index b1ba90fc03..ad70afbeaf 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -777,6 +777,8 @@ def get_alerts_with_filters(tenant_id, provider_id=None, filters=None) -> list[A if provider_id: query = query.filter(Alert.provider_id == provider_id) + query = query.order_by(Alert.timestamp.desc()) + # Execute the query alerts = query.all() diff --git a/keep/providers/keep_provider/keep_provider.py b/keep/providers/keep_provider/keep_provider.py index b9f6ee3840..e13fbf6cd8 100644 --- a/keep/providers/keep_provider/keep_provider.py +++ b/keep/providers/keep_provider/keep_provider.py @@ -1,6 +1,7 @@ """ Keep Provider is a class that allows to ingest/digest data from Keep. """ + import logging from typing import Optional @@ -27,7 +28,7 @@ def dispose(self): """ pass - def _query(self, filters, **kwargs): + def _query(self, filters, distinct=True, **kwargs): """ Query Keep for alerts. """ @@ -35,13 +36,17 @@ def _query(self, filters, **kwargs): self.context_manager.tenant_id, filters=filters ) + fingerprints = {} alerts = [] if db_alerts: for alert in db_alerts: + if fingerprints.get(alert.fingerprint) and distinct is True: + continue alert_event = alert.event if alert.alert_enrichment: alert_event["enrichments"] = alert.alert_enrichment.enrichments alerts.append(alert_event) + fingerprints[alert.fingerprint] = True return alerts def validate_config(self):