diff --git a/workflows/analyze_url/README.md b/workflows/analyze_url/README.md new file mode 100644 index 0000000..d9e2132 --- /dev/null +++ b/workflows/analyze_url/README.md @@ -0,0 +1,48 @@ +# Analyze URL Using VirusTotal + +The workflow uses VirusTotal to analyze a URL. + +## Required Secrets + +To use this workflow, the following secret is required. To set it up, please follow the respective guide on the linked documentation page. + +- [VirusTotal](https://docs.admyral.dev/integrations/virus_total/virus_total) + +> [!IMPORTANT] +> The workflow currently expects the following secret name: \ +> **VirusTotal**: `virus_total` \ +> If your secret has a different name, please adjust the secret mapping in the workflow function accordingly \ +> e.g `secrets = {"VIRUS_TOTAL_SECRET": "your_secret_name"}` + +## Set Up Workflow + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push analyze_url -f workflows/analyze_url/analyze_url.py --activate +``` + +## Expected Payload + +The workflow expects the following payload schema: + +```json +{ + "url": "your_url_to_analyze" +} +``` + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Input the payload following the expected schema +4. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger analyze_url -p '{"url": "your_url_to_analyze"}' +``` diff --git a/workflows/analyze_url/__init__.py b/workflows/analyze_url/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/analyze_url.py b/workflows/analyze_url/analyze_url.py similarity index 100% rename from workflows/analyze_url.py rename to workflows/analyze_url/analyze_url.py diff --git a/workflows/jira_notification_user_created/README.md b/workflows/jira_notification_user_created/README.md new file mode 100644 index 0000000..8e5910e --- /dev/null +++ b/workflows/jira_notification_user_created/README.md @@ -0,0 +1,48 @@ +# Jira Notify On User Creation + +This workflow monitors Jira for newly created user accounts and sends a Slack notification with relevant details. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Jira](https://docs.admyral.dev/integrations/jira/jira) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Slack**: `slack_secret` \ +> **Jira**: `jira_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"JIRA_SECRET": "your_secret_name"}` \ +> and for **Slack** respectively + +## Set Up Workflow + +1. Open the `jira_notification_user_created.py` file +2. Adjust the `email` parameter with the email of the person to receive the slack notification + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push jira_notification_user_created workflows/jira_notification_user_created/jira_notification_user_created.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger jira_notification_user_created +``` diff --git a/workflows/jira_notification_user_created/__init__.py b/workflows/jira_notification_user_created/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/jira_notification_user_created/jira_notification_user_created.py b/workflows/jira_notification_user_created/jira_notification_user_created.py new file mode 100644 index 0000000..cfbadcc --- /dev/null +++ b/workflows/jira_notification_user_created/jira_notification_user_created.py @@ -0,0 +1,25 @@ +from admyral.workflow import workflow, Webhook, Schedule +from admyral.typings import JsonValue +from admyral.actions import get_jira_audit_records, send_slack_message_to_user_by_email + + +@workflow( + description="Monitors Jira for newly created user accounts and sends a Slack notification with relevant details. " + "This workflow automatically retrieves audit records for user creation events and notifies the specified recipient " + "via Slack with the user ID and creation timestamp.", + triggers=[Webhook(), Schedule(interval_days=1)], +) +def jira_notification_user_created(payload: dict[str, JsonValue]): + # jira get audit records for newly created users + records = get_jira_audit_records( + filter=["User", "created"], + start_date="2024-08-01T00:00:00", + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + # notify via Slack about changes + send_slack_message_to_user_by_email( + email="daniel@admyral.dev", # TODO: set your Slack email here + text=f"*A new user was created*\n\nUser ID: {records[0]['objectItem']['id']}\nCreated on: {records[0]['created']}", + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/list_okta_admins/README.md b/workflows/list_okta_admins/README.md new file mode 100644 index 0000000..e4d3463 --- /dev/null +++ b/workflows/list_okta_admins/README.md @@ -0,0 +1,47 @@ +# List Okta Admins + +This workflow retrieves specifc or all user types and lists all admin users. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Okta](https://docs.admyral.dev/integrations/okta/okta) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Okta**: `okta_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"OKTA_SECRET": "your_secret_name"}` \ + +## Set Up Workflow + +There are no adjustments required for the workflow to work, but you can optionally: + +1. Open the `list_okta_admins.py` file +2. Adjust the search query for the user type of interest + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push list_okta_admins -f workflows/list_okta_admins/list_okta_admins.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger list_okta_admins +``` diff --git a/workflows/list_okta_admins/__init__.py b/workflows/list_okta_admins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/list_okta_admins/list_okta_admins.py b/workflows/list_okta_admins/list_okta_admins.py new file mode 100644 index 0000000..f48c157 --- /dev/null +++ b/workflows/list_okta_admins/list_okta_admins.py @@ -0,0 +1,18 @@ +from admyral.workflow import workflow +from admyral.typings import JsonValue +from admyral.actions import okta_search_users, okta_get_all_user_types + + +@workflow( + description="Retrieves all user types from Okta and lists the corresponding admin users.", +) +def list_okta_admins(payload: dict[str, JsonValue]): + # Step 1: Get all user types + user_types = okta_get_all_user_types(secrets={"OKTA_SECRET": "okta_secret"}) + + # Step 2: Return admin user type + # TODO: Adjust the search query to match the wished user type + okta_search_users( + search=f"type.id eq \"{user_types[0]['id']}\"", + secrets={"OKTA_SECRET": "okta_secret"}, + ) diff --git a/workflows/monitor_github_org_owner_changes/README.md b/workflows/monitor_github_org_owner_changes/README.md new file mode 100644 index 0000000..99e4953 --- /dev/null +++ b/workflows/monitor_github_org_owner_changes/README.md @@ -0,0 +1,60 @@ +# Monitor GitHub Org Owner Changes + +This workflow analyzes the GitHub Audit logs for a specified enterprise. +It is scheduled to run at every full hour and analyze the previous hour. +In case there were changes, a notification via Slack is being sent. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [GitHub Enterprise](https://docs.admyral.dev/integrations/github/github) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Slack**: `slack_secret` \ +> **GitHub Enterprise**: `github_enterprise_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"GITHUB_ENTERPRISE_SECRET": "your_secret_name"}` \ +> and for **Slack** respectively + +## Set Up Workflow + +1. Open the `monitor_github_org_owner_changes.py` file +2. Adjust the `enterprise` and `email` with your enterprise slug and the email of the person to be notified of the respective events via slack + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push get_time_range_of_last_full_hour -a workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py +``` + +```bash +poetry run admyral action push build_info_message_owner_changes -a workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push monitor_github_org_owner_changes -f workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger monitor_github_org_owner_changes +``` diff --git a/workflows/monitor_github_org_owner_changes/__init__.py b/workflows/monitor_github_org_owner_changes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py b/workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py new file mode 100644 index 0000000..e3df7a9 --- /dev/null +++ b/workflows/monitor_github_org_owner_changes/monitor_github_org_owner_changes.py @@ -0,0 +1,85 @@ +from typing import Annotated +from datetime import datetime, timedelta, UTC + +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.action import action, ArgumentMetadata +from admyral.actions import ( + search_github_enterprise_audit_logs, + batched_send_slack_message_to_user_by_email, +) + + +@action( + display_name="Calculate Time Range for Last Full Hour", + display_namespace="Utilities", + description="Calculate the time range for the last full hour", +) +def get_time_range_of_last_full_hour() -> tuple[str, str]: + end_time = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) + start_time = (end_time - timedelta(hours=1)).isoformat().replace("+00:00", "Z") + return (start_time, end_time.isoformat().replace("+00:00", "Z")) + + +@action( + display_name="Build Info message", + display_namespace="GitHub", + description="Builds a message for the slack notification", +) +def build_info_message_owner_changes( + logs: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Logs", + description="The logs to build the message from", + ), + ], + email: Annotated[ + str, + ArgumentMetadata( + display_name="Email", + description="The email to send the message to", + ), + ], +) -> list[tuple[str, str | None, JsonValue]]: + messages = [] + for log in logs: + timestamp = datetime.fromtimestamp(int(log["created_at"]) / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ) + if log["action"] == "org.update_member": + messages.append( + ( + email, + f"Owner change detected in enterprise {log['business']} at {timestamp} by {log['actor']}:\nChanged Permission for {log['user']}: {log['old_permission']} -> {log['permission']}\n", + None, + ) + ) + return messages + + +@workflow( + description="Alert on GitHub Orga Owner Changes", + triggers=[Schedule(cron="0 * * * *")], +) +def monitor_github_org_owner_changes(payload: dict[str, JsonValue]): + start_and_end_time = get_time_range_of_last_full_hour() + + logs = search_github_enterprise_audit_logs( + enterprise="admyral", # TODO: set your enterprise slug here + filter="action:org.update_member", + start_time=start_and_end_time[0], + end_time=start_and_end_time[1], + secrets={"GITHUB_ENTERPRISE_SECRET": "github_enterprise_secret"}, + ) + + if logs: + messages = build_info_message_owner_changes( + logs=logs, + email="daniel@admyral.dev", # TODO: set your Slack email here + ) + + batched_send_slack_message_to_user_by_email( + messages=messages, + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/okta_password_policy_monitoring/README.md b/workflows/okta_password_policy_monitoring/README.md new file mode 100644 index 0000000..d806fec --- /dev/null +++ b/workflows/okta_password_policy_monitoring/README.md @@ -0,0 +1,61 @@ +# Monitor Okta Password Policy Changes + +This workflow monitors changes to the password policies in Okta and sends notifications via Slack with relevant details. The workflow runs at every full hour and checks for updates made during the previous hour. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Okta](https://docs.admyral.dev/integrations/okta/okta) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Okta**: `okta_secret` \ +> **Slack**: `slack_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly. \ +> e.g. `secrets = {"OKTA_SECRET": "your_secret_name"}` and similarly for Slack. + +## Set Up Workflow + +1. Open the `okta_password_policy_monitoring.py` file +2. Adjust the email address in the `send_slack_message_to_user_by_email` action with the email of the Slack user to receive notifications + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push get_time_range_of_last_full_hour -a workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py +``` + +```bash +poetry run admyral action push get_okta_password_policy_update_logs -a workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py +``` + +```bash +poetry run admyral action push format_okta_policy_update_message -a workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push okta_password_policy_monitoring -f workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger okta_password_policy_monitoring +``` diff --git a/workflows/okta_password_policy_monitoring/__init__.py b/workflows/okta_password_policy_monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py b/workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py new file mode 100644 index 0000000..63c3f88 --- /dev/null +++ b/workflows/okta_password_policy_monitoring/okta_password_policy_monitoring.py @@ -0,0 +1,120 @@ +from typing import Annotated +from datetime import datetime, timedelta, UTC +import json + +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.action import action, ArgumentMetadata +from admyral.actions import get_okta_logs, send_slack_message_to_user_by_email + + +@action( + display_name="Calculate Time Range for Last Full Hour", + display_namespace="Utilities", + description="Calculate the time range for the last full hour", +) +def get_time_range_of_last_full_hour() -> tuple[str, str]: + end_time = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) + start_time = (end_time - timedelta(hours=1)).isoformat().replace("+00:00", "Z") + return (start_time, end_time.isoformat().replace("+00:00", "Z")) + + +@action( + display_name="Get Okta Password Policy Update Logs", + display_namespace="Okta", + description="Retrieve Okta password policy update logs for a specified time range", + secrets_placeholders=["OKTA_SECRET"], +) +def get_okta_password_policy_update_logs( + start_time: Annotated[ + str, + ArgumentMetadata( + display_name="Start Time", + description="The start time for the logs to retrieve in ISO 8601 format", + ), + ], + end_time: Annotated[ + str, + ArgumentMetadata( + display_name="End Time", + description="The end time for the logs to retrieve in ISO 8601 format", + ), + ], +) -> list[dict[str, JsonValue]]: + logs = get_okta_logs( + query="policy.lifecycle.update Password", + start_time=start_time, + end_time=end_time, + ) + + return [ + log + for log in logs + if log.get("target", [{}])[0].get("detailEntry", {}).get("policyType") + == "Password" + ] + + +@action( + display_name="Format Okta Policy Update Message", + display_namespace="Okta", + description="Format Okta policy update logs into a readable message", +) +def format_okta_policy_update_message( + logs: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Okta Logs", + description="List of Okta policy update logs", + ), + ], +) -> str: + message = f"Attention: {len(logs)} Okta password policy update(s) detected in the last hour.\n\n" + for log in logs: + message += f"Event ID: {log.get('transaction', {}).get('id')}\n" + message += f"Timestamp: {log.get('published')}\n" + message += f"Actor: {log.get('actor', {}).get('displayName')} ({log.get('actor', {}).get('alternateId')})\n" + + debug_data = log.get("debugContext", {}).get("debugData", {}) + + new_policy = json.loads( + debug_data.get("newPolicyExtensiblePropertiesJson", "{}") + ) + old_policy = json.loads( + debug_data.get("oldPolicyExtensiblePropertiesJson", "{}") + ) + + message += "Changes:\n" + + for key in new_policy: + new_value = str(new_policy.get(key)) + old_value = str(old_policy.get(key)) + + if new_value.lower() != old_value.lower(): + message += f"- {key}: {old_value} -> {new_value}\n" + + message += "---\n" + return message + + +@workflow( + description="Monitor Okta password policy changes and notify via Slack", + triggers=[Schedule(cron="0 * * * *")], +) +def okta_password_policy_monitoring(payload: dict[str, JsonValue]): + time_range = get_time_range_of_last_full_hour() + + logs = get_okta_password_policy_update_logs( + start_time=time_range[0], + end_time=time_range[1], + secrets={"OKTA_SECRET": "okta_secret"}, + ) + + if logs: + message = format_okta_policy_update_message(logs=logs) + + send_slack_message_to_user_by_email( + email="daniel@admyral.dev", + text=message, + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/okta_use_it_or_lose_it/README.md b/workflows/okta_use_it_or_lose_it/README.md new file mode 100644 index 0000000..507eb5d --- /dev/null +++ b/workflows/okta_use_it_or_lose_it/README.md @@ -0,0 +1,68 @@ +# Okta Notify Inactive Users + +This workflow checks for inactive Okta users who have not logged in for a specified period (default: 90 days) and sends a Slack message to that user, asking if they still need access to Okta. + +> [!IMPORTANT] +> In case you want to use this workflow together with the `retool_access_review` workflow, you have to combine the functionality within the `slack_interactivity.py` by adding the respective `if` condition in the `slack_interactivity.py` within the retool_access_review directory. \ +> This is because the Slack API only allows the configuration of one interactivity webhook at a time. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Okta](https://docs.admyral.dev/integrations/okta/okta) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Okta**: `okta_secret` \ +> **Slack**: `slack_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly. \ +> e.g. `secrets = {"OKTA_SECRET": "your_secret_name"}` and similarly for Slack. + +## Set Up Workflow + +There are no adjustments required for the workflow to work, however you can optionally: + +1. Add a search filter to the `search` parameter +2. Adjust, how many users should be checked for inactivity with the `limit` parameter +3. Adjust the threshold, determining if a user should be counted as inactive by changing the value of `inactivity_threshold` + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push filter_inactive_okta_users -a workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py +``` + +```bash +poetry run admyral action push build_okta_inactivity_messages -a workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py +``` + +Use the CLI to push the workflows: + +```bash +poetry run admyral workflow push okta_use_it_or_lose_it -f workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py --activate +``` + +```bash +poetry run admyral workflow push slack_interactivity -f workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger okta_use_it_or_lose_it +``` diff --git a/workflows/okta_use_it_or_lose_it/__init__.py b/workflows/okta_use_it_or_lose_it/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py b/workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py new file mode 100644 index 0000000..adef585 --- /dev/null +++ b/workflows/okta_use_it_or_lose_it/okta_use_it_or_lose_it.py @@ -0,0 +1,150 @@ +from typing import Annotated +import json +from datetime import datetime, timedelta + +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.action import action, ArgumentMetadata +from admyral.actions import ( + batched_send_slack_message_to_user_by_email, + okta_search_users, +) + + +@action( + display_name="Filter Inactive Okta Users", + display_namespace="Okta", + description="Filter Okta users who haven't logged in for a specified number of days", +) +def filter_inactive_okta_users( + users: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Okta Users", + description="List of Okta users to filter", + ), + ], + inactivity_threshold: Annotated[ + int, + ArgumentMetadata( + display_name="Inactivity Threshold", + description="Number of days of inactivity to filter by", + ), + ], +) -> list[dict[str, JsonValue]]: + inactive_users = [] + threshold_date = datetime.now() - timedelta(days=inactivity_threshold) + + for user in users: + last_login = user.get("lastLogin") + if last_login: + last_login_date = datetime.fromisoformat(last_login.rstrip("Z")) + if last_login_date < threshold_date: + inactive_users.append(user) + else: + # If lastLogin is None, the user has never logged in + inactive_users.append(user) + + return inactive_users + + +@action( + display_name="Build Okta Inactivity Messages", + display_namespace="Okta", + description="Build Slack messages for inactive Okta users", +) +def build_okta_inactivity_messages( + inactive_users: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Inactive Users", + description="List of inactive Okta users", + ), + ], +) -> list[tuple[str, str | None, JsonValue]]: + messages = [] + for user in inactive_users: + email = user["profile"]["email"] + first_name = user["profile"]["firstName"] + messages.append( + ( + email, + None, + [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"Hello {first_name}, you haven't logged into Okta for over 90 days. " + "Please confirm if you still need access.", + }, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "okta_use_it_or_lose_it-yes", + "value": json.dumps( + { + "user": email, + "workflow": "okta_use_it_or_lose_it", + "response": "yes", + } + ), + "text": { + "type": "plain_text", + "text": "Yes, I need access", + }, + }, + { + "type": "button", + "action_id": "okta_use_it_or_lose_it-no", + "value": json.dumps( + { + "user": email, + "workflow": "okta_use_it_or_lose_it", + "response": "no", + } + ), + "text": { + "type": "plain_text", + "text": "No, I no longer need access", + }, + }, + ], + }, + ], + ) + ) + return [message for message in messages if not message[0].endswith("@admyral.dev")] + + +@workflow( + description="Check Okta user inactivity and ask if access is still required", + triggers=[Schedule(interval_days=1)], +) +def okta_use_it_or_lose_it(payload: dict[str, JsonValue]): + # Search for all Okta users + all_users = okta_search_users( + search=None, # No filter, get all users + limit=1000, # Adjust as needed + secrets={"OKTA_SECRET": "okta_secret"}, + ) + + # Filter inactive users (90 days threshold) + inactive_users = filter_inactive_okta_users( + users=all_users, + inactivity_threshold=90, + ) + + # Build messages for inactive users + messages = build_okta_inactivity_messages( + inactive_users=inactive_users, + ) + + # Send Slack messages to inactive users + batched_send_slack_message_to_user_by_email( + messages=messages, + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/okta_use_it_or_lose_it/slack_interactivity.py b/workflows/okta_use_it_or_lose_it/slack_interactivity.py new file mode 100644 index 0000000..1e783d9 --- /dev/null +++ b/workflows/okta_use_it_or_lose_it/slack_interactivity.py @@ -0,0 +1,30 @@ +from admyral.workflow import workflow, Webhook +from admyral.typings import JsonValue +from admyral.actions import ( + send_slack_message, + deserialize_json_string, +) + + +""" +Setup + +1. Create a Slack app (https://api.slack.com/apps) +2. Go to "Interactivity & Shortcuts" and enable interactivity +3. In the Request URL field enter the URL of the Admyral Webhook trigger + +""" + +@workflow( + description="This workflow handles Slack interactivity responses.", + triggers=[Webhook()], +) +def slack_interactivity(payload: dict[str, JsonValue]): + # Workflow: Use it or loose it + if payload["actions"][0]["action_id"] == "use_it_or_loose_it-no": + value = deserialize_json_string(serialized_json=payload["actions"][0]["value"]) + send_slack_message( + channel_id="TODO(user): set Slack channel ID", + text=f"Please deactivate Retool access for the following user: {value["user"]}. Reason: User responded with no longer needed.", + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/one_password_user_added_to_vault/README.md b/workflows/one_password_user_added_to_vault/README.md new file mode 100644 index 0000000..06a63fa --- /dev/null +++ b/workflows/one_password_user_added_to_vault/README.md @@ -0,0 +1,59 @@ +# 1Password User Added to Vault + +This workflow monitors 1Password vault events, filtering audit logs to find users who were added to specific vaults. It then sends a Slack notification to the user managing the vault, summarizing the relevant activity. It is scheduled to run at every full hour, checking the previous hour. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [1Password](https://docs.admyral.dev/integrations/1password/1password) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **1Password**: `1password_secret` \ +> **Slack**: `slack_secret`\ +> If your secrets have a different name, please adjust the secret \ +> mappings in the workflow function accordingly \ +> e.g `secrets = {"SLACK_SECRET": "your_secret_name"}` and for 1password respectively. \ + +## Set Up Workflow + +1. Open the `one_password_user_added_to_vault.py` file +2. Adjust the `user_email` with the email of the person who should be notified +3. Adjust the `vault_id` with your Vault ID for which the audit events should be filtered + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push get_time_range_of_last_full_hour -a workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py +``` + +```bash +poetry run admyral action push filter_by_vault_and_build_slack_message -a workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push one_password_user_added_to_vault -f workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger one_password_user_added_to_vault +``` diff --git a/workflows/one_password_user_added_to_vault/__init__.py b/workflows/one_password_user_added_to_vault/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py b/workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py new file mode 100644 index 0000000..d8ecec4 --- /dev/null +++ b/workflows/one_password_user_added_to_vault/one_password_user_added_to_vault.py @@ -0,0 +1,92 @@ +from typing import Annotated +from datetime import datetime, timedelta, UTC + + +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.actions import ( + list_1password_audit_events, + batched_send_slack_message_to_user_by_email, +) +from admyral.action import ArgumentMetadata, action + + +@action( + display_name="Calculate Time Range for Last Full Hour", + display_namespace="Utilities", + description="Calculate the time range for the last full hour", +) +def get_time_range_of_last_full_hour() -> tuple[str, str]: + end_time = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) + start_time = (end_time - timedelta(hours=1)).isoformat().replace("+00:00", "Z") + return (start_time, end_time.isoformat().replace("+00:00", "Z")) + + +@action( + display_name="Filter by 1Password Vault and Build Slack Message", + display_namespace="1Password", + description="Filter audit events by vault and build a Slack message.", +) +def filter_by_vault_and_build_slack_message( + user_email: Annotated[ + str, + ArgumentMetadata( + display_name="User Email", + description="The email of the user who should receive the Slack messages.", + ), + ], + vault_id: Annotated[ + str, + ArgumentMetadata( + display_name="Vault ID", + description="The vault ID to filter audit events by. The vault ID can be found " + "in the 1Password web app in the URL of the vault.", + ), + ], + audit_events: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Audit Events", + description="The list of audit events to filter.", + ), + ], +) -> list[JsonValue]: + messages = [] + for audit_event in audit_events: + if audit_event["object_uuid"] == vault_id: + messages.append( + ( + user_email, + f"User {audit_event['actor_details']['name']} ({audit_event['actor_details']['email']}) " + f"added user {audit_event['aux_details']['name']} ({audit_event["aux_details"]["email"]}) " + f"to vault {vault_id}.", + None, + ) + ) + return messages + + +@workflow( + description="Retrieves all user types from Okta and lists the corresponding admin users.", + triggers=[Schedule(cron="0 * * * *")], +) +def one_password_user_added_to_vault(payload: dict[str, JsonValue]): + start_and_end_time = get_time_range_of_last_full_hour() + + events = list_1password_audit_events( + action_type_filter="grant", + object_type_filter="uva", + start_time=start_and_end_time[0], + end_time=start_and_end_time[1], + secrets={"1PASSWORD_SECRET": "1password_secret"}, + ) + + messages = filter_by_vault_and_build_slack_message( + user_email="daniel@admyral.dev", # TODO: set your email here + vault_id="ut22fmh7v55235s6t5gjd3t4cy", # TODO: set your vault ID here + audit_events=events, + ) + + batched_send_slack_message_to_user_by_email( + messages=messages, secrets={"SLACK_SECRET": "slack_secret"} + ) diff --git a/workflows/panther_alert_handling/README.md b/workflows/panther_alert_handling/README.md new file mode 100644 index 0000000..cce8dd0 --- /dev/null +++ b/workflows/panther_alert_handling/README.md @@ -0,0 +1,73 @@ +# Panther Alert Handling + +This workflow handles alerts from Panther. It uses AI to generate summaries and recommendations, creates a Jira issue for tracking, and notifies the team via Slack. + +> [!IMPORTANT] +> The panther_alert_handling workflow calls the panther_slack_interactivity workflow. \ +> Note, that the Slack API only allows to configure on interactivity webhook at a time, should you also run other workflows using slack interactivity. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Slack](https://docs.admyral.dev/integrations/slack/slack) +- [Jira](https://docs.admyral.dev/integrations/jira/jira) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Slack**: `slack_secret` \ +> **Jira**: `jira_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"SLACK_SECRET": "your_secret_name"}` and for Jira respectively. + +## Set Up Workflow + +1. Open the `panther_alert_handling.py` file +2. Set the Slack channel ID in `channel_id` where the notifications should be sent + +Use the CLI to push the workflows: + +```bash +poetry run admyral workflow push panther_alert_handling -f workflows/panther_alert_handling/panther_alert_handling.py --activate +``` + +```bash +poetry run admyral workflow push panther_slack_interactivity -f workflows/panther_alert_handling/panther_alert_handling.py --activate +``` + +## Expected Payload + +The workflow expects the following payload schema: + +```json +{ + "alert": { + "id": "AWS.ALB.HighVol400s", + "title": "your_alert_title", + "event_details": { + "domain_name": "your_domain" + }, + "account": "your_account", + "mitre_attack": { + "tactic": "your_mitre_attack_tactic", + "technique": "your_mitre_attack_technique" + }, + "severity": "your_alert_priority" + } +} +``` + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger panther_alert_handling -p '{"alert": {"id": "AWS.ALB.HighVol400s", "title": "your_alert_title", "event_details": {"domain_name": "your_domain"}, "account": "your_account", "mitre_attack": {"tactic": "your_mitre_attack_tactic", "technique": "your_mitre_attack_technique"}, "severity": "your_alert_priority"}}' + +``` diff --git a/workflows/panther_alert_handling/__init__.py b/workflows/panther_alert_handling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/panther_alert_handling/panther_alert_handling.py b/workflows/panther_alert_handling/panther_alert_handling.py new file mode 100644 index 0000000..4656a42 --- /dev/null +++ b/workflows/panther_alert_handling/panther_alert_handling.py @@ -0,0 +1,116 @@ +from admyral.workflow import workflow, Webhook +from admyral.typings import JsonValue +from admyral.actions import send_slack_message, create_jira_issue, ai_action + + +@workflow( + description="This workflow handles alerts from Panther.", + triggers=[Webhook()], +) +def panther_alert_handling(payload: dict[str, JsonValue]): + if payload["alert"]["id"] == "AWS.ALB.HighVol400s": + alert_summary = ai_action( + model="gpt-4o", + prompt=f"You are an expert security analyst. You received the subsequent alert. Can you briefly summarize " + "it in a short and precise manner? Can you also provide a recommendation regarding investigation steps? " + f"Here is the alert:\n{payload['alert']}", + ) + + jira_issue = create_jira_issue( + summary=f"[{payload["alert"]["id"]}] {payload["alert"]["title"]}", + project_id="10001", + issue_type="Bug", + description={ + "content": [ + { + "content": [ + { + "text": f"AI Alert Summary:\n{alert_summary}", + "type": "text", + } + ], + "type": "paragraph", + }, + { + "content": [ + { + "text": f"Alert: {payload['alert']}", + "type": "text", + } + ], + "type": "paragraph", + }, + ], + "type": "doc", + "version": 1, + }, + labels=[ + payload["alert"]["mitre_attack"]["tactic"], + payload["alert"]["mitre_attack"]["technique"], + ], + priority=payload["alert"]["severity"], + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + send_slack_message( + channel_id="TODO(user): set correct channel ID here", + text=f"ACTION REQUIRED: High volume of web port 4xx errors to {payload['alert']['event_details']['domain_name']} in account {payload['alert']['account']}", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*ACTION REQUIRED: High volume of web port 4xx errors to {payload['alert']['event_details']['domain_name']} in account {payload['alert']['account']}*", + }, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Please confirm whether the following alert is suspicious:", + }, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"AI Alert Summary:\n{alert_summary}", + }, + }, + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"```\n{payload['alert']}\n```"}, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"Jira Ticket: https://christesting123.atlassian.net/browse/{jira_issue['key']}", + }, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "panther_alert_handling-suspicious", + "value": jira_issue["id"], + "text": { + "type": "plain_text", + "text": "Suspicious alert", + }, + }, + { + "type": "button", + "action_id": "panther_alert_handling-non-suspicious", + "value": jira_issue["id"], + "text": { + "type": "plain_text", + "text": "Non-suspicious alert", + }, + }, + ], + }, + ], + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/panther_alert_handling/panther_slack_interactivity.py b/workflows/panther_alert_handling/panther_slack_interactivity.py new file mode 100644 index 0000000..2b9cab6 --- /dev/null +++ b/workflows/panther_alert_handling/panther_slack_interactivity.py @@ -0,0 +1,71 @@ +from admyral.workflow import workflow, Webhook +from admyral.typings import JsonValue +from admyral.actions import ( + update_jira_issue_status, + comment_jira_issue_status, +) + + +""" +Setup + +1. Create a Slack app (https://api.slack.com/apps) +2. Go to "Interactivity & Shortcuts" and enable interactivity +3. In the Request URL field enter the URL of the Admyral Webhook trigger + +""" + + +@workflow( + description="This workflow handles Slack interactivity responses.", + triggers=[Webhook()], +) +def slack_interactivity(payload: dict[str, JsonValue]): + # Workflow: Panther Alert Handling + if payload["actions"][0]["action_id"] == "panther_alert_handling-suspicious": + jira_comment = comment_jira_issue_status( + issue_id_or_key=payload["actions"][0]["value"], + comment={ + "content": [ + { + "content": [ + { + "text": "Alert was flagged as suspicious.", + "type": "text", + } + ], + "type": "paragraph", + }, + ], + "type": "doc", + "version": 1, + }, + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + if payload["actions"][0]["action_id"] == "panther_alert_handling-non-suspicious": + jira_comment = comment_jira_issue_status( + issue_id_or_key=payload["actions"][0]["value"], + comment={ + "content": [ + { + "content": [ + { + "text": "Alert was flagged as non-suspicious. Issue is automatically closed.", + "type": "text", + } + ], + "type": "paragraph", + }, + ], + "type": "doc", + "version": 1, + }, + secrets={"JIRA_SECRET": "jira_secret"}, + ) + update_jira_issue_status( + issue_id_or_key=payload["actions"][0]["value"], + transition_id="31", + secrets={"JIRA_SECRET": "jira_secret"}, + run_after=[jira_comment], + ) diff --git a/workflows/retool_access_review/README.md b/workflows/retool_access_review/README.md new file mode 100644 index 0000000..ff00247 --- /dev/null +++ b/workflows/retool_access_review/README.md @@ -0,0 +1,62 @@ +# Retool Access Review + +This workflow automates the process of reviewing Retool access permissions by sending Slack messages to managers for their teams' access review. Managers can review each user's group membership and select whether to approve or remove access for different scenarios. + +> [!IMPORTANT] +> In case you want to use this workflow together with the `okta_use_it_or_lose_it` workflow, you have to combine the functionality within the `slack_interactivity.py` by adding the respective `if` condition in the `slack_interactivity.py` within the okta_use_it_or_lose_it directory. \ +> This is because the Slack API only allows the configuration of one interactivity webhook at a time. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Retool](https://docs.admyral.dev/integrations/retool/retool) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> +> **Retool**: `retool_secret` \ +> **Slack**: `slack_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"RETOOL_SECRET": "your_secret_name"}` and for Slack respectively. \ + +## Set Up Workflow + +1. Open the `retool_access_review.py` file +2. Adjust the `manager` parameter in the `build_review_requests_as_slack_message_for_managers` function with the email of the person who should be notified via Slack + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push build_review_requests_as_slack_message_for_managers -a workflows/retool_access_review/retool_access_review.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push retool_access_review -f workflows/retool_access_review/retool_access_review.py --activate +``` + +```bash +poetry run admyral workflow push slack_interactivity -f workflows/retool_access_review/retool_access_review.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger retool_access_review +``` diff --git a/workflows/retool_access_review/__init__.py b/workflows/retool_access_review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/retool_access_review/retool_access_review.py b/workflows/retool_access_review/retool_access_review.py new file mode 100644 index 0000000..ba05c3b --- /dev/null +++ b/workflows/retool_access_review/retool_access_review.py @@ -0,0 +1,175 @@ +from typing import Annotated +import json + +from admyral.workflow import workflow +from admyral.typings import JsonValue +from admyral.actions import ( + list_groups_per_user, + batched_send_slack_message_to_user_by_email, +) +from admyral.action import action, ArgumentMetadata + + +""" + +Pt. 1: Prompting Workflow + +1. Fetch Retool users and their permissions +2. Group users by managers and the associated permissions +3. Send manager slack message and ask for review + Dropdown: + - Approve access + - Appropriate in the past but not needed anymore + - Terminated user + - Suspicious access + + +Pt. 2: Feedback Workflow + +4. If manager disapproves: + 4.1 Remove permissions + 4.2 Check again that permissions were removed + +""" + + +@action( + display_name="Group Users and Permissions by Managers", + display_namespace="Access Review", + description="Groups Retool users and their permissions by their manager.", +) +def build_review_requests_as_slack_message_for_managers( + groups_per_user: Annotated[ + dict[str, JsonValue], + ArgumentMetadata( + display_name="Groups", + description="A list of Retool groups with their members.", + ), + ], +) -> list[tuple[str, str | None, JsonValue]]: + # Group by Manager + # TODO(admyral): fetch managers from Okta + manager = "TODO(user): set some email of a Slack user which will receive the Slack message" + user_groups_per_manager = {manager: groups_per_user} + + # Build review requests + messages = [] + + for manager, groups_per_user_for_manager in user_groups_per_manager.items(): + blocks = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Hello, here is the annual Access Review for the Retool access of your team:", + }, + } + ] + + for user, groups_and_last_active in groups_per_user_for_manager.items(): + blocks.append({"type": "divider"}) + blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"User {user} (Last active: {groups_and_last_active['last_active']}) is assigned to the following groups:", + }, + }, + ) + + for group in groups_and_last_active["groups"]: + blocks.append( + { + "type": "section", + "text": {"type": "mrkdwn", "text": group}, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an option", + "emoji": True, + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Approved access", + "emoji": True, + }, + "value": json.dumps( + { + "group": group, + "user": user, + "response": "keep", + "reason": "Approved access", + } + ), + }, + { + "text": { + "type": "plain_text", + "text": "Appropriate in the past but not needed anymore", + "emoji": True, + }, + "value": json.dumps( + { + "group": group, + "user": user, + "response": "remove", + "reason": "Appropriate in the past but not needed anymore", + } + ), + }, + { + "text": { + "type": "plain_text", + "text": "Terminated user", + "emoji": True, + }, + "value": json.dumps( + { + "group": group, + "user": user, + "response": "remove", + "reason": "Terminated user", + } + ), + }, + { + "text": { + "type": "plain_text", + "text": "Suspicious access", + "emoji": True, + }, + "value": json.dumps( + { + "group": group, + "user": user, + "response": "remove", + "reason": "Suspicious access", + } + ), + }, + ], + "action_id": "access_review", + }, + }, + ) + + messages.append((manager, None, blocks)) + + return messages + + +@workflow( + description="This workflow sends Slack messages to managers for Retool access review.", +) +def retool_access_review(payload: dict[str, JsonValue]): + groups_per_user = list_groups_per_user(secrets={"RETOOL_SECRET": "retool_secret"}) + messages = build_review_requests_as_slack_message_for_managers( + groups_per_user=groups_per_user, + ) + batched_send_slack_message_to_user_by_email( + messages=messages, secrets={"SLACK_SECRET": "slack_secret"} + ) diff --git a/workflows/retool_access_review/slack_interactivity.py b/workflows/retool_access_review/slack_interactivity.py new file mode 100644 index 0000000..08d7ed3 --- /dev/null +++ b/workflows/retool_access_review/slack_interactivity.py @@ -0,0 +1,32 @@ +from admyral.workflow import workflow, Webhook +from admyral.typings import JsonValue +from admyral.actions import ( + send_slack_message, + deserialize_json_string, +) + + +""" +Setup + +1. Create a Slack app (https://api.slack.com/apps) +2. Go to "Interactivity & Shortcuts" and enable interactivity +3. In the Request URL field enter the URL of the Admyral Webhook trigger + +""" + +@workflow( + description="This workflow handles Slack interactivity responses.", + triggers=[Webhook()], +) +def slack_interactivity(payload: dict[str, JsonValue]): + if payload["actions"][0]["action_id"] == "access_review": + value = deserialize_json_string( + serialized_json=payload["actions"][0]["selected_option"]["value"] + ) + if value["response"] == "remove": + send_slack_message( + channel_id="TODO(user): set Slack channel ID", + text=f"Please remove the user {value['user']} from the group \"{value['group']}\" in Retool. Reason: {value['reason']}", + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/retool_use_it_or_loose_it/README.md b/workflows/retool_use_it_or_loose_it/README.md new file mode 100644 index 0000000..7126c59 --- /dev/null +++ b/workflows/retool_use_it_or_loose_it/README.md @@ -0,0 +1,55 @@ +# Retool Notify Inactive Users + +This workflow checks for inactive Retool users who have not logged in for a specified period (default: 60 days) and sends a Slack message to that user, asking if they still need access to Retool. + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Retool](https://docs.admyral.dev/integrations/retool/retool) +- [Slack](https://docs.admyral.dev/integrations/slack/slack) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> +> **Retool**: `retool_secret` \ +> **Slack**: `slack_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"RETOOL_SECRET": "your_secret_name"}` and for Slack respectively. \ + +## Set Up Workflow + +There are no adjustments required for the workflow to work, however you can optionally: + +Adjust the threshold, determining if a user should be counted as inactive by changing the value of `inactivity_threshold` within the workflow function. + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push build_retool_inactivity_question_as_slack_messages -a workflows/retool_use_it_or_loose_it/retool_use_it_or_loose_it.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push retool_use_it_or_loose_it -f workflows/retool_use_it_or_loose_it/retool_use_it_or_loose_it.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger retool_use_it_or_loose_it +``` diff --git a/workflows/retool_use_it_or_loose_it/__init__.py b/workflows/retool_use_it_or_loose_it/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/retool_use_it_or_loose_it/retool_use_it_or_loose_it.py b/workflows/retool_use_it_or_loose_it/retool_use_it_or_loose_it.py new file mode 100644 index 0000000..acb3bb7 --- /dev/null +++ b/workflows/retool_use_it_or_loose_it/retool_use_it_or_loose_it.py @@ -0,0 +1,102 @@ +from typing import Annotated +import json + +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.action import action, ArgumentMetadata +from admyral.actions import ( + batched_send_slack_message_to_user_by_email, + list_retool_inactive_users, +) + + +@action( + display_name="Build Retool Inactivity Question as Slack Messages", + display_namespace="Use It or Loose It", + description="Build a list of Slack messages to send to inactive Retool users " + "and asking them whether they still need access.", +) +def build_retool_inactivity_question_as_slack_messages( + inactive_users: Annotated[ + list[dict[str, JsonValue]], + ArgumentMetadata( + display_name="Inactive Users", + description="A list of inactive users to send messages to.", + ), + ], +) -> list[tuple[str, str | None, JsonValue]]: + messages = [] + for user in inactive_users: + messages.append( + ( + user["email"], + f"Hello {user['first_name']}, you have not logged into Retool for a while. " + "Please confirm if you still need access.", + [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"Hello {user['first_name']}, you have not logged into Retool for a while. " + "Please confirm if you still need access.", + }, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "use_it_or_loose_it-yes", + "value": json.dumps( + { + "user": user["email"], + "workflow": "use_it_or_loose_it", + "response": "yes", + } + ), + "text": { + "type": "plain_text", + "text": "Yes, I need access", + }, + }, + { + "type": "button", + "action_id": "use_it_or_loose_it-no", + "value": json.dumps( + { + "user": user["email"], + "workflow": "use_it_or_loose_it", + "response": "no", + } + ), + "text": { + "type": "plain_text", + "text": "No, I no longer need access", + }, + }, + ], + }, + ], + ) + ) + return messages + + +@workflow( + description="Check Retool user inactivity and ask if access is still required. This worfklow " + "handles the extraction of inactive users and sending messages to them. The response is handled " + "in the Slack interactivity workflow.", + triggers=[Schedule(interval_days=1)], +) +def retool_use_it_or_loose_it(payload: dict[str, JsonValue]): + inactive_users = list_retool_inactive_users( + inactivity_threshold_in_days=60, + secrets={"RETOOL_SECRET": "retool_secret"}, + ) + messages = build_retool_inactivity_question_as_slack_messages( + inactive_users=inactive_users, + ) + batched_send_slack_message_to_user_by_email( + messages=messages, + secrets={"SLACK_SECRET": "slack_secret"}, + ) diff --git a/workflows/template/README.md b/workflows/template/README.md new file mode 100644 index 0000000..7b4f4e2 --- /dev/null +++ b/workflows/template/README.md @@ -0,0 +1,56 @@ +# + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"<>": "your_secret_name"}` \ + +## Set Up Workflow + +1. + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push --activate +``` + +## Expected Payload + +The workflow expects the following payload schema: + +```json +{ + "": "" +} +``` + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger +``` diff --git a/workflows/template/__init__.py b/workflows/template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/vulnerability_sla_breach/README.md b/workflows/vulnerability_sla_breach/README.md new file mode 100644 index 0000000..f00749e --- /dev/null +++ b/workflows/vulnerability_sla_breach/README.md @@ -0,0 +1,51 @@ +# Vulnerability SLA Breach Monitoring + +## Required Secrets + +To use this workflow, the following secrets are required. To set them up, please follow the respective guide on the linked documentation page. + +- [Slack](https://docs.admyral.dev/integrations/slack/slack) +- [Jira](https://docs.admyral.dev/integrations/jira/jira) + +> [!IMPORTANT] +> The workflow currently expects the following secret names: \ +> **Slack**: `slack_secret` \ +> **Jira**: `jira_secret` \ +> If your secrets have a different name, please adjust the secret mappings in the workflow function accordingly \ +> e.g `secrets = {"SLACK_SECRET": "your_secret_name"}` and for Jira respectively. + +## Set Up Workflow + +1. Open the `vulnerability_sla_breach.py` file +2. Adjust the `email` parameter in `transform_jira_to_slack` with the email adress of the person who should be notified via Slack + +Use the CLI to push the custom actions: + +```bash +poetry run admyral action push transform_jira_to_slack -a workflows/transform_jira_to_slack/transform_jira_to_slack.py +``` + +Use the CLI to push the workflow: + +```bash +poetry run admyral workflow push vulnerability_sla_breach -f workflows/transform_jira_to_slack/transform_jira_to_slack.py --activate +``` + +## Expected Payload + +> [!IMPORTANT] +> The workflow doesn't expect any payload. + +## Run Workflow + +Use the Admyral UI: + +1. Open the workflow in the workflow No-Code editor +2. Click on **Run** +3. Click on **Run Workflow** + +Or use the CLI to trigger the workflow: + +```bash +poetry run admyral workflow trigger vulnerability_sla_breach +``` diff --git a/workflows/vulnerability_sla_breach/__init__.py b/workflows/vulnerability_sla_breach/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflows/vulnerability_sla_breach/vulnerability_sla_breach.py b/workflows/vulnerability_sla_breach/vulnerability_sla_breach.py new file mode 100644 index 0000000..bb21939 --- /dev/null +++ b/workflows/vulnerability_sla_breach/vulnerability_sla_breach.py @@ -0,0 +1,112 @@ +from typing import Annotated +from admyral.workflow import workflow, Schedule +from admyral.typings import JsonValue +from admyral.action import action, ArgumentMetadata +from admyral.actions import ( + search_jira_issues, + batched_send_slack_message_to_user_by_email, +) + + +""" +Setup + +1. Create a Slack app (https://api.slack.com/apps) +2. Go to "Interactivity & Shortcuts" and enable interactivity +3. In the Request URL field enter the URL of the Admyral Webhook trigger + +""" + + +@action( + display_name="Transform Jira Issues to Slack", + display_namespace="Jira", + description="Transform Jira Issues to Slack", +) +def transform_jira_to_slack( + jira_tickets: Annotated[ + JsonValue, + ArgumentMetadata( + display_name="Jira tickets", description="Result of Jira Ticket Search" + ), + ], + message_input: Annotated[ + str, + ArgumentMetadata( + display_name="Message Input", + description="Describe the type of message that should be displayed. E.g., Recent SLA breach", + ), + ], +): + + email = "" #TODO: Add email of the user who should receive the Slack messages + result = [] + for jira_ticket in jira_tickets: + key = jira_ticket["key"] + title = jira_ticket["fields"]["summary"] + creator = jira_ticket["fields"]["creator"]["displayName"] + status = jira_ticket["fields"]["status"]["name"] + created = jira_ticket["fields"]["created"] + + result.append( + ( + email, + f"{message_input}: [{key}] {title} \nCreated by: {creator} \nStatus: {status} \nCreated on: {created}", + None, + ) + ) + + return result + + +@workflow( + description="Monitoring of vulnerability SLA breaches", + triggers=[Schedule(interval_days=1)], +) +def vulnerability_sla_breach(payload: dict[str, JsonValue]): + # filter for jira tickets that didn't change in the last 7 days + no_change_last_7_days = search_jira_issues( + jql='project = SJ AND status IN ("In Progress", "To Do") AND updated < -1w AND updated >= -8d', # AND priority IN (Highest, High) + limit=1000, + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + transformed_no_change = transform_jira_to_slack( + jira_tickets=no_change_last_7_days, + message_input="🚨 No progress in the last 7 days 🚨", + ) + + batched_send_slack_message_to_user_by_email( + messages=transformed_no_change, secrets={"SLACK_SECRET": "slack_secret"} + ) + + # filter for jira tickets whose SLA is about to be breached (10 days left) + soon_breached_slas = search_jira_issues( + jql='project = SJ AND status IN ("In Progress", "To Do") AND created = -80d', # AND priority IN (Highest, High) + limit=1000, + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + transformed_soon_breached = transform_jira_to_slack( + jira_tickets=soon_breached_slas, + message_input="🚨 About to be breached SLAs in 10 days 🚨", + ) + + batched_send_slack_message_to_user_by_email( + messages=transformed_soon_breached, secrets={"SLACK_SECRET": "slack_secret"} + ) + + # filter for jira tickets that just breached SLA + breached_slas = search_jira_issues( + jql='project = SJ AND status IN ("In Progress", "To Do") AND created = -91d', # AND priority IN (Highest, High) + limit=1000, + secrets={"JIRA_SECRET": "jira_secret"}, + ) + + transformed_breached = transform_jira_to_slack( + jira_tickets=breached_slas, message_input="🚨 Just breached SLAs 🚨" + ) + + batched_send_slack_message_to_user_by_email( + messages=transformed_breached, secrets={"SLACK_SECRET": "slack_secret"} + )