diff --git a/plugins/armorblox/.CHECKSUM b/plugins/armorblox/.CHECKSUM new file mode 100644 index 0000000000..3093cfc882 --- /dev/null +++ b/plugins/armorblox/.CHECKSUM @@ -0,0 +1,19 @@ +{ + "spec": "406fd799fb05d81464ed8cb863ea1f56", + "manifest": "433b5ba19020a0a518c333be03af4eab", + "setup": "67d906e679bb88b8e56aa98bc008d58b", + "schemas": [ + { + "identifier": "get_remediation_action/schema.py", + "hash": "6ac6405d0147b2df173e4e2fd5df44b5" + }, + { + "identifier": "connection/schema.py", + "hash": "96db429999d91d2e040fc52f6e02fec7" + }, + { + "identifier": "get_incidents/schema.py", + "hash": "cc8737d06f9995ac1c317d6e0093cef6" + } + ] +} diff --git a/plugins/armorblox/.dockerignore b/plugins/armorblox/.dockerignore new file mode 100644 index 0000000000..93dc53fb01 --- /dev/null +++ b/plugins/armorblox/.dockerignore @@ -0,0 +1,9 @@ +unit_test/**/* +unit_test +examples/**/* +examples +tests +tests/**/* +**/*.json +**/*.tar +**/*.gz \ No newline at end of file diff --git a/plugins/armorblox/Dockerfile b/plugins/armorblox/Dockerfile new file mode 100644 index 0000000000..93456a2bca --- /dev/null +++ b/plugins/armorblox/Dockerfile @@ -0,0 +1,28 @@ +FROM rapid7/insightconnect-python-3-38-plugin:4 +# Refer to the following documentation for available SDK parent images: https://komand.github.io/python/sdk.html#version + +LABEL organization=rapid7 +LABEL sdk=python + +# Add any custom package dependencies here +# NOTE: Add pip packages to requirements.txt + +# End package dependencies + +# Add source code +WORKDIR /python/src +ADD ./plugin.spec.yaml /plugin.spec.yaml +ADD . /python/src + +RUN apt-get update && apt-get install -y git + +# Install pip dependencies +RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + +# Install plugin +RUN python setup.py build && python setup.py install + +# User to run plugin code. The two supported users are: root, nobody +USER nobody + +ENTRYPOINT ["/usr/local/bin/icon_armorblox"] \ No newline at end of file diff --git a/plugins/armorblox/Makefile b/plugins/armorblox/Makefile new file mode 100644 index 0000000000..cb85f96b6c --- /dev/null +++ b/plugins/armorblox/Makefile @@ -0,0 +1,53 @@ +# Include other Makefiles for improved functionality +INCLUDE_DIR = ../../tools/Makefiles +MAKEFILES := $(wildcard $(INCLUDE_DIR)/*.mk) +# We can't guarantee customers will have the include files +# - prefix to ignore Makefiles when not present +# https://www.gnu.org/software/make/manual/html_node/Include.html +-include $(MAKEFILES) + +ifneq ($(MAKEFILES),) + $(info [$(YELLOW)*$(NORMAL)] Use ``make menu`` for available targets) + $(info [$(YELLOW)*$(NORMAL)] Including available Makefiles: $(MAKEFILES)) + $(info --) +else + $(warning Makefile includes directory not present: $(INCLUDE_DIR)) +endif + +VERSION?=$(shell grep '^version: ' plugin.spec.yaml | sed 's/version: //') +NAME?=$(shell grep '^name: ' plugin.spec.yaml | sed 's/name: //') +VENDOR?=$(shell grep '^vendor: ' plugin.spec.yaml | sed 's/vendor: //') +CWD?=$(shell basename $(PWD)) +_NAME?=$(shell echo $(NAME) | awk '{ print toupper(substr($$0,1,1)) tolower(substr($$0,2)) }') +PKG=$(VENDOR)-$(NAME)-$(VERSION).tar.gz + +# Set default target explicitly. Make's default behavior is the first target in the Makefile. +# We don't want that behavior due to includes which are read first +.DEFAULT_GOAL := default # Make >= v3.80 (make -version) + + +default: image tarball + +tarball: + $(info [$(YELLOW)*$(NORMAL)] Creating plugin tarball) + rm -rf build + rm -rf $(PKG) + tar -cvzf $(PKG) --exclude=$(PKG) --exclude=tests --exclude=run.sh * + +image: + $(info [$(YELLOW)*$(NORMAL)] Building plugin image) + docker build --pull -t $(VENDOR)/$(NAME):$(VERSION) . + docker tag $(VENDOR)/$(NAME):$(VERSION) $(VENDOR)/$(NAME):latest + +regenerate: + $(info [$(YELLOW)*$(NORMAL)] Regenerating schema from plugin.spec.yaml) + icon-plugin generate python --regenerate + +export: image + $(info [$(YELLOW)*$(NORMAL)] Exporting docker image) + @printf "\n ---> Exporting Docker image to ./$(VENDOR)_$(NAME)_$(VERSION).tar\n" + @docker save $(VENDOR)/$(NAME):$(VERSION) | gzip > $(VENDOR)_$(NAME)_$(VERSION).tar + +# Make will not run a target if a file of the same name exists unless setting phony targets +# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html +.PHONY: default tarball image regenerate diff --git a/plugins/armorblox/bin/icon_armorblox b/plugins/armorblox/bin/icon_armorblox new file mode 100644 index 0000000000..2eefdc967e --- /dev/null +++ b/plugins/armorblox/bin/icon_armorblox @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# GENERATED BY KOMAND SDK - DO NOT EDIT +import os +import json +from sys import argv + +Name = "Armorblox" +Vendor = "armorblox" +Version = "1.0.0" +Description = "Armorblox is an API-based platform that stops targeted email attacks, protects sensitive data, and automates incident response" + + +def main(): + if 'http' in argv: + if os.environ.get("GUNICORN_CONFIG_FILE"): + with open(os.environ.get("GUNICORN_CONFIG_FILE")) as gf: + gunicorn_cfg = json.load(gf) + if gunicorn_cfg.get("worker_class", "sync") == "gevent": + from gevent import monkey + monkey.patch_all() + elif 'gevent' in argv: + from gevent import monkey + monkey.patch_all() + + import insightconnect_plugin_runtime + from icon_armorblox import connection, actions, triggers + + class ICONArmorblox(insightconnect_plugin_runtime.Plugin): + def __init__(self): + super(self.__class__, self).__init__( + name=Name, + vendor=Vendor, + version=Version, + description=Description, + connection=connection.Connection() + ) + self.add_trigger(triggers.GetIncidents()) + + self.add_action(actions.GetRemediationAction()) + + + """Run plugin""" + cli = insightconnect_plugin_runtime.CLI(ICONArmorblox()) + cli.run() + + +if __name__ == "__main__": + main() diff --git a/plugins/armorblox/extension.png b/plugins/armorblox/extension.png new file mode 100644 index 0000000000..f03a385cb5 Binary files /dev/null and b/plugins/armorblox/extension.png differ diff --git a/plugins/armorblox/help.md b/plugins/armorblox/help.md new file mode 100644 index 0000000000..595516dbff --- /dev/null +++ b/plugins/armorblox/help.md @@ -0,0 +1,165 @@ +# Description + +Armorblox is an API-based platform that stops targeted email attacks, protects sensitive data, and automates incident response + +# Key Features + +* Fetches incidents detected by Armorblox for the given tenant. +* Retrieves the remediation action for a given incident. + +# Requirements + +* Requires an API key from the product. + +# Supported Product Versions + +* 1.0.0 + +# Documentation + +## Setup + +The connection configuration accepts the following parameters: + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|api_key|credential_secret_key|None|True|Armorblox API Key|None|9de5069c5afe602b2ea0a04b66beb2c0| +|tenant_name|string|None|True|Armorblox Tenant Name|None|my-tenant-name| + +Example input: + +``` +{ + "api_key": "9de5069c5afe602b2ea0a04b66beb2c0", + "tenant_name": "my-tenant-name" +} +``` +## Technical Details + +### Actions + +#### Get Remediation Action + +This action is used to fetch remediation action of an incident identified by Armorblox. + +##### Input + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|incident_id|string|None|True|An integer number identifying the incident|None|3490| + +Example input: + +``` +{ + "incident_id": 3490 +} +``` + +##### Output + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|remediation_details|string|True|Remediation action of the requested incident identified by Armorblox| + +Example output: +``` +{ + "remediation_details": "ALERT" +} +``` + +### Triggers + +#### Get Incidents + +This trigger is used to get a list of incidents identified by Armorblox. By default, it starts querying for all the incidents since the previous day. + +##### Input + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|interval|integer|600|False|Polling interval in seconds|None|600| + +Example input: + +``` +{ + "interval": 600 +} +``` + +##### Output + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|incidents|[]incident|True|A list of incidents identified by Armorblox| + +Example output: + +``` +{ + "incidents": "some incidents" +} +``` + +### Custom Output Types + +#### engagement + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|Forwarded Mail Count|string|False|Forwarded Mail Count| +|Reply Mail Count|string|False|Reply Mail Count| + +#### final_detection_tag + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|Detection tag ID|string|False|Detection tag ID| +|Detection tag name|string|False|Detection tag name| + +#### incident + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|App Name|string|False|App Name| +|Incident Date|date|False|None| +|Engagements|engagement|False|Engagements| +|External senders|[]string|False|List of external senders| +|External users|[]user|False|List of external users| +|Detection tags|[]final_detection_tag|False|Detection tags| +|Folder categories|[]string|False|Folder categories| +|Incident ID|string|False|Incident ID| +|Incident Type|string|False|Incident Type| +|Object Type|string|False|Object Type| +|policy_names|[]string|False|List of policies| +|Priority|string|False|Priority of the incident| +|Remediation Action|[]string|False|Remediation Action| +|resolution_state|string|False|Resolution State| +|SCL Score|integer|False|None| +|Is email tagged|boolean|False|Is email tagged| +|Subject|string|False|Subject| +|users|[]user|False|List of users| + +#### user + +|Name|Type|Required|Description| +|----|----|--------|-----------| +|User email|string|False|User email| +|Is User VIP|boolean|False|Is User VIP| +|user name|string|False|User name| + + +## Troubleshooting + +_This plugin does not contain any troubleshooting information._ + +# Version History + +* 1.0.0 - Initial plugin + +# Links + +* [Armorblox](https://www.armorblox.com/) +## References diff --git a/plugins/armorblox/icon.png b/plugins/armorblox/icon.png new file mode 100644 index 0000000000..f03a385cb5 Binary files /dev/null and b/plugins/armorblox/icon.png differ diff --git a/plugins/armorblox/icon_armorblox/__init__.py b/plugins/armorblox/icon_armorblox/__init__.py new file mode 100644 index 0000000000..bace8db897 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/__init__.py @@ -0,0 +1 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT diff --git a/plugins/armorblox/icon_armorblox/actions/__init__.py b/plugins/armorblox/icon_armorblox/actions/__init__.py new file mode 100644 index 0000000000..efb4d57683 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/actions/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .get_remediation_action.action import GetRemediationAction diff --git a/plugins/armorblox/icon_armorblox/actions/get_remediation_action/__init__.py b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/__init__.py new file mode 100644 index 0000000000..baa5558dbc --- /dev/null +++ b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .action import GetRemediationAction diff --git a/plugins/armorblox/icon_armorblox/actions/get_remediation_action/action.py b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/action.py new file mode 100644 index 0000000000..7384886e8f --- /dev/null +++ b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/action.py @@ -0,0 +1,18 @@ +import insightconnect_plugin_runtime +from .schema import GetRemediationActionInput, GetRemediationActionOutput, Input, Output, Component +# Custom imports below + + +class GetRemediationAction(insightconnect_plugin_runtime.Action): + + def __init__(self): + super(self.__class__, self).__init__( + name='get_remediation_action', + description=Component.DESCRIPTION, + input=GetRemediationActionInput(), + output=GetRemediationActionOutput()) + + def run(self, params={}): + remediation_details = self.connection.api.get_remediation_action(params.get(Input.INCIDENT_ID)) + return {Output.REMEDIATION_DETAILS: remediation_details} + \ No newline at end of file diff --git a/plugins/armorblox/icon_armorblox/actions/get_remediation_action/schema.py b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/schema.py new file mode 100644 index 0000000000..f18eb8e9b0 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/actions/get_remediation_action/schema.py @@ -0,0 +1,61 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Component: + DESCRIPTION = "Fetch remediation action of an incident identified by Armorblox" + + +class Input: + INCIDENT_ID = "incident_id" + + +class Output: + REMEDIATION_DETAILS = "remediation_details" + + +class GetRemediationActionInput(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "incident_id": { + "type": "string", + "title": "Incident ID", + "description": "An integer number identifying the incident", + "order": 1 + } + }, + "required": [ + "incident_id" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class GetRemediationActionOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "remediation_details": { + "type": "string", + "title": "Remediation Details", + "description": "Remediation action of the requested incident identified by Armorblox", + "order": 1 + } + }, + "required": [ + "remediation_details" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/plugins/armorblox/icon_armorblox/connection/__init__.py b/plugins/armorblox/icon_armorblox/connection/__init__.py new file mode 100644 index 0000000000..a515dcf6b0 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/connection/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .connection import Connection diff --git a/plugins/armorblox/icon_armorblox/connection/connection.py b/plugins/armorblox/icon_armorblox/connection/connection.py new file mode 100644 index 0000000000..dc28208558 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/connection/connection.py @@ -0,0 +1,27 @@ +import insightconnect_plugin_runtime +from .schema import ConnectionSchema, Input +from insightconnect_plugin_runtime.exceptions import PluginException, ConnectionTestException +# Custom imports below +from icon_armorblox.util.api import ArmorbloxAPI + + +class Connection(insightconnect_plugin_runtime.Connection): + + def __init__(self): + super(self.__class__, self).__init__(input=ConnectionSchema()) + self.api = None + + def connect(self, params): + self.logger.info("Connect: Connecting...") + api_key = params.get(Input.API_KEY, {}).get("secretKey") + tenant_name = params.get(Input.TENANT_NAME) + self.api = ArmorbloxAPI(api_key = api_key, tenant_name = tenant_name, logger=self.logger) + + def test(self): + try: + self.api.test_api() + return {"success": True} + except PluginException as error: + raise ConnectionTestException( + cause=error.cause, assistance=error.assistance, data=error.data + ) diff --git a/plugins/armorblox/icon_armorblox/connection/schema.py b/plugins/armorblox/icon_armorblox/connection/schema.py new file mode 100644 index 0000000000..c36ae38a9b --- /dev/null +++ b/plugins/armorblox/icon_armorblox/connection/schema.py @@ -0,0 +1,58 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Input: + API_KEY = "api_key" + TENANT_NAME = "tenant_name" + + +class ConnectionSchema(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "api_key": { + "$ref": "#/definitions/credential_secret_key", + "title": "Armorblox API Key", + "description": "Armorblox API Key", + "order": 2 + }, + "tenant_name": { + "type": "string", + "title": "Tenant Name", + "description": "Armorblox Tenant Name", + "order": 1 + } + }, + "required": [ + "api_key", + "tenant_name" + ], + "definitions": { + "credential_secret_key": { + "id": "credential_secret_key", + "type": "object", + "title": "Credential: Secret Key", + "description": "A shared secret key", + "properties": { + "secretKey": { + "type": "string", + "title": "Secret Key", + "displayType": "password", + "description": "The shared secret key", + "format": "password" + } + }, + "required": [ + "secretKey" + ] + } + } +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/plugins/armorblox/icon_armorblox/triggers/__init__.py b/plugins/armorblox/icon_armorblox/triggers/__init__.py new file mode 100644 index 0000000000..6d2f23951a --- /dev/null +++ b/plugins/armorblox/icon_armorblox/triggers/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .get_incidents.trigger import GetIncidents diff --git a/plugins/armorblox/icon_armorblox/triggers/get_incidents/__init__.py b/plugins/armorblox/icon_armorblox/triggers/get_incidents/__init__.py new file mode 100644 index 0000000000..f079359f16 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/triggers/get_incidents/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .trigger import GetIncidents diff --git a/plugins/armorblox/icon_armorblox/triggers/get_incidents/schema.py b/plugins/armorblox/icon_armorblox/triggers/get_incidents/schema.py new file mode 100644 index 0000000000..a359f7aafb --- /dev/null +++ b/plugins/armorblox/icon_armorblox/triggers/get_incidents/schema.py @@ -0,0 +1,324 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Component: + DESCRIPTION = "Get a list of incidents identified by Armorblox. By default, it starts querying for all the incidents since the previous day" + + +class Input: + + INTERVAL = "interval" + + +class Output: + + INCIDENTS = "incidents" + + +class GetIncidentsInput(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "interval": { + "type": "integer", + "title": "Fetch Interval", + "description": "Polling interval in seconds", + "default": 600, + "order": 1 + } + } +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class GetIncidentsOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "incidents": { + "type": "array", + "title": "Incidents", + "description": "A list of incidents identified by Armorblox", + "items": { + "$ref": "#/definitions/incident" + }, + "order": 1 + } + }, + "required": [ + "incidents" + ], + "definitions": { + "engagement": { + "type": "object", + "title": "engagement", + "properties": { + "fwd_mail_count": { + "type": "string", + "title": "Forwarded Mail Count", + "description": "Forwarded Mail Count", + "order": 1 + }, + "reply_mail_count": { + "type": "string", + "title": "Reply Mail Count", + "description": "Reply Mail Count", + "order": 2 + } + } + }, + "final_detection_tag": { + "type": "object", + "title": "final_detection_tag", + "properties": { + "detection_tag_id": { + "type": "string", + "title": "Detection tag ID", + "description": "Detection tag ID", + "order": 1 + }, + "detection_tag_name": { + "type": "string", + "title": "Detection tag name", + "description": "Detection tag name", + "order": 2 + } + } + }, + "incident": { + "type": "object", + "title": "incident", + "properties": { + "app_name": { + "type": "string", + "title": "App Name", + "description": "App Name", + "order": 9 + }, + "date": { + "type": "string", + "title": "Incident Date", + "displayType": "date", + "description": "Date of the incident", + "format": "date-time", + "order": 3 + }, + "engagements": { + "$ref": "#/definitions/engagement", + "title": "Engagements", + "description": "Engagements", + "order": 14 + }, + "external_senders": { + "type": "array", + "title": "External senders", + "description": "List of external senders", + "items": { + "type": "string" + }, + "order": 10 + }, + "external_users": { + "type": "array", + "title": "External users", + "description": "List of external users", + "items": { + "$ref": "#/definitions/user" + }, + "order": 17 + }, + "final_detection_tags": { + "type": "array", + "title": "Detection tags", + "description": "Detection tags", + "items": { + "$ref": "#/definitions/final_detection_tag" + }, + "order": 18 + }, + "folder_categories": { + "type": "array", + "title": "Folder categories", + "description": "Folder categories", + "items": { + "type": "string" + }, + "order": 11 + }, + "id": { + "type": "string", + "title": "Incident ID", + "description": "Incident ID", + "order": 8 + }, + "incident_type": { + "type": "string", + "title": "Incident Type", + "description": "Incident Type", + "order": 13 + }, + "object_type": { + "type": "string", + "title": "Object Type", + "description": "Type of the object", + "order": 7 + }, + "policy_names": { + "type": "array", + "title": "policy_names", + "description": "List of policies", + "items": { + "type": "string" + }, + "order": 4 + }, + "priority": { + "type": "string", + "title": "Priority", + "description": "Priority of the incident", + "order": 1 + }, + "remediation_actions": { + "type": "array", + "title": "Remediation Action", + "description": "Remediation Action", + "items": { + "type": "string" + }, + "order": 15 + }, + "resolution_state": { + "type": "string", + "title": "resolution_state", + "description": "Incident resolution state", + "order": 6 + }, + "scl_score": { + "type": "integer", + "title": "SCL Score", + "order": 12 + }, + "tagged": { + "type": "boolean", + "title": "Is email tagged", + "description": "Is email tagged", + "order": 2 + }, + "title": { + "type": "string", + "title": "Subject", + "description": "Mail subject", + "order": 5 + }, + "users": { + "type": "array", + "title": "users", + "description": "List of users", + "items": { + "$ref": "#/definitions/user" + }, + "order": 16 + } + }, + "definitions": { + "engagement": { + "type": "object", + "title": "engagement", + "properties": { + "fwd_mail_count": { + "type": "string", + "title": "Forwarded Mail Count", + "description": "Forwarded Mail Count", + "order": 1 + }, + "reply_mail_count": { + "type": "string", + "title": "Reply Mail Count", + "description": "Reply Mail Count", + "order": 2 + } + } + }, + "final_detection_tag": { + "type": "object", + "title": "final_detection_tag", + "properties": { + "detection_tag_id": { + "type": "string", + "title": "Detection tag ID", + "description": "Detection tag ID", + "order": 1 + }, + "detection_tag_name": { + "type": "string", + "title": "Detection tag name", + "description": "Detection tag name", + "order": 2 + } + } + }, + "user": { + "type": "object", + "title": "user", + "properties": { + "email": { + "type": "string", + "title": "User email", + "description": "Email of the user", + "order": 2 + }, + "is_vip": { + "type": "boolean", + "title": "Is User VIP", + "description": "Is User VIP", + "order": 3 + }, + "name": { + "type": "string", + "title": "User name", + "description": "Name of the user", + "order": 1 + } + } + } + } + }, + "user": { + "type": "object", + "title": "user", + "properties": { + "email": { + "type": "string", + "title": "User email", + "description": "Email of the user", + "order": 2 + }, + "is_vip": { + "type": "boolean", + "title": "Is User VIP", + "description": "Is User VIP", + "order": 3 + }, + "name": { + "type": "string", + "title": "User name", + "description": "Name of the user", + "order": 1 + } + } + } + } +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/plugins/armorblox/icon_armorblox/triggers/get_incidents/trigger.py b/plugins/armorblox/icon_armorblox/triggers/get_incidents/trigger.py new file mode 100644 index 0000000000..5e8953cb50 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/triggers/get_incidents/trigger.py @@ -0,0 +1,31 @@ +import insightconnect_plugin_runtime +import time +from datetime import datetime, timedelta +from icon_armorblox.util.constants import ARMORBLOX_INCIDENT_API_TIME_FORMAT, ARMORBLOX_INCIDENT_API_TIME_DELTA_IN_DAYS, DEFAULT_INTERVAL_VALUE +from .schema import GetIncidentsInput, GetIncidentsOutput, Input, Output, Component +# Custom imports below + +class GetIncidents(insightconnect_plugin_runtime.Trigger): + + def __init__(self): + super(self.__class__, self).__init__( + name='get_incidents', + description=Component.DESCRIPTION, + input=GetIncidentsInput(), + output=GetIncidentsOutput()) + + def run(self, params={}): + """Run the trigger""" + fetch_interval = params.get(Input.INTERVAL, DEFAULT_INTERVAL_VALUE) + # First fetch + last_fetch_time = (datetime.utcnow() - timedelta(days={ARMORBLOX_INCIDENT_API_TIME_DELTA_IN_DAYS})).strftime( + {ARMORBLOX_INCIDENT_API_TIME_FORMAT}) + while True: + current_time = datetime.utcnow().replace(second=0).strftime({ARMORBLOX_INCIDENT_API_TIME_FORMAT}) + events = self.connection.api.get_incidents(from_date=last_fetch_time, to_date=current_time) + if events: + self.send({Output.INCIDENTS: events}) + else: + self.logger.info(f"No events retrieved. Sleeping for {fetch_interval} seconds....") + last_fetch_time = current_time + time.sleep(fetch_interval) diff --git a/plugins/armorblox/icon_armorblox/util/__init__.py b/plugins/armorblox/icon_armorblox/util/__init__.py new file mode 100644 index 0000000000..bace8db897 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/util/__init__.py @@ -0,0 +1 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT diff --git a/plugins/armorblox/icon_armorblox/util/api.py b/plugins/armorblox/icon_armorblox/util/api.py new file mode 100644 index 0000000000..7c3c4e7689 --- /dev/null +++ b/plugins/armorblox/icon_armorblox/util/api.py @@ -0,0 +1,56 @@ +from insightconnect_plugin_runtime.helper import clean +from insightconnect_plugin_runtime.exceptions import PluginException +from logging import Logger +from urllib.parse import urlsplit +from armorblox.client import Client + + +class ArmorbloxAPI(Client): + + def __init__(self, api_key: str, tenant_name: str, logger = Logger): + super().__init__(api_key=api_key, instance_name=tenant_name) + self.logger = logger + self.incidents_list = [] + + def process_incidents(self, params): + self.incidents_list = [] + try: + response_json, next_page_token, total_count = self.incidents.list(params=params) + self.incidents_list.extend(response_json) + while next_page_token: + params["page_token"] = next_page_token + response_json, next_page_token, total_count = self.incidents.list(params=params) + self.incidents_list.extend(response_json) + except Exception as credentials_exp: + PluginException('Incorrect Credentials. ' + str(credentials_exp)) + + def get_incidents(self, from_date: str = None, to_date: str = None): + """ + Hits the Armorblox API and fetch incidents. + + :param from_date: Custom time filter parameter + :param to_date: Custom time filter parameter + + :return: List of incidents + """ + params = { + "from_date": from_date, + "to_date": to_date, + "orderBy": "ASC" + } + self.process_incidents(params) + return self.incidents_list + + def get_remediation_action(self, incident_id): + """ + Returns the remediation action(s) for the input incident. + """ + rm_action_response = self.incidents.get(incident_id) + if 'remediation_actions' in rm_action_response.keys(): + remediation_actions = rm_action_response['remediation_actions'][0] + else: + remediation_actions = '' + return remediation_actions + + def test_api(self): + return self.get_incidents() diff --git a/plugins/armorblox/icon_armorblox/util/constants.py b/plugins/armorblox/icon_armorblox/util/constants.py new file mode 100644 index 0000000000..36cdb9441c --- /dev/null +++ b/plugins/armorblox/icon_armorblox/util/constants.py @@ -0,0 +1,3 @@ +ARMORBLOX_INCIDENT_API_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +ARMORBLOX_INCIDENT_API_TIME_DELTA_IN_DAYS = 1 +DEFAULT_INTERVAL_VALUE = 600 diff --git a/plugins/armorblox/plugin.spec.yaml b/plugins/armorblox/plugin.spec.yaml new file mode 100644 index 0000000000..f836a86358 --- /dev/null +++ b/plugins/armorblox/plugin.spec.yaml @@ -0,0 +1,206 @@ +plugin_spec_version: v2 +extension: plugin +products: [insightconnect] +name: armorblox +title: Armorblox +description: Armorblox is an API-based platform that stops targeted email attacks, protects sensitive data, and automates incident response +version: 1.0.0 +vendor: armorblox +author_email: app-publishers@armorblox.com +support: community +supported_versions: ["1.0.0"] +status: [] +tags: ["alerts", "attack", "breach", "compliance", "email", "forensics", "gdpr", "hipaa", "incident response", "it", "machine Learning", "malware", "pii", "scam", "security", "security analytics", "spam", "threat intelligence"] +hub_tags: + use_cases: ['cloud_security', 'remediation_management', 'threat_detection_and_response'] + keywords: ['threat_intelligence'] + features: [] +resources: + source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/armorblox + license_url: https://github.com/rapid7/insightconnect-plugins/blob/master/LICENSE + vendor_url: https://armorblox.com/ +enable_cache: true + +types: + user: + name: + title: User Name + description: Name of the user + type: string + required: false + email: + title: User Email + description: Email of the user + type: string + required: false + is_vip: + title: Is User VIP + description: Is User VIP + type: boolean + required: false + final_detection_tag: + detection_tag_id: + title: Detection Tag ID + description: Detection tag ID + type: string + required: false + detection_tag_name: + title: Detection Tag name + description: Detection tag name + type: string + required: false + engagement: + fwd_mail_count: + title: Forwarded Mail Count + description: Forwarded Mail Count + type: string + required: false + reply_mail_count: + title: Reply Mail Count + description: Reply Mail Count + type: string + required: false + incident: + priority: + title: Priority + description: Priority of the incident + type: string + required: false + tagged: + title: Is email tagged + description: Is email tagged + type: boolean + required: false + date: + title: Incident Date + description: Date of the incident + type: date + required: false + policy_names: + title: policy Names + description: List of policies + type: '[]string' + required: false + title: + title: Title + description: Mail subject + type: 'string' + required: false + resolution_state: + title: Resolution State + description: Incident resolution state + type: string + required: false + object_type: + title: Object Type + description: Type of the object + type: string + required: false + id: + title: Incident ID + description: Incident ID + type: string + required: false + app_name: + title: App Name + description: App Name + type: string + required: false + external_senders: + title: External Senders + description: List of external senders + type: "[]string" + required: + folder_categories: + title: Folder Categories + description: Folder categories + type: "[]string" + required: false + scl_score: + title: SCL Score + description: + type: integer + required: false + incident_type: + title: Incident Type + description: Incident Type + type: string + required: + engagements: + title: Engagements + description: Engagements + type: engagement + required: false + remediation_actions: + title: Remediation Action + description: Remediation Action + type: "[]string" + required: false + users: + title: Users + description: List of users + type: "[]user" + required: false + external_users: + title: External Users + description: List of external users + type: "[]user" + required: false + final_detection_tags: + title: Detection Tags + description: Detection tags + type: "[]final_detection_tag" + required: false + +connection: + tenant_name: + title: Tenant Name + description: Armorblox Tenant Name + type: string + required: true + example: my-tenant-name + api_key: + title: Armorblox API Key + description: Armorblox API Key + type: credential_secret_key + required: true + example: 9de5069c5afe602b2ea0a04b66beb2c0 + +triggers: + get_incidents: + title: Get Incidents + description: Get a list of incidents identified by Armorblox. By default, it starts querying for all the incidents since the previous day + input: + interval: + title: Fetch Interval + description: Polling interval in seconds + type: integer + required: false + default: 600 + example: 600 + output: + incidents: + title: Incidents + description: A list of incidents identified by Armorblox + type: "[]incident" + required: true + example: "{\"incidents\": \"some incidents\"}" + +actions: + get_remediation_action: + title: Get Remediation Action + description: Fetch remediation action of an incident identified by Armorblox + input: + incident_id: + title: Incident ID + description: An integer number identifying the incident + type: string + required: true + example: 3490 + output: + remediation_details: + title: Remediation Details + description: Remediation action of the requested incident identified by Armorblox + type: string + required: true + example: ALERT diff --git a/plugins/armorblox/requirements.txt b/plugins/armorblox/requirements.txt new file mode 100644 index 0000000000..0fba9e45c0 --- /dev/null +++ b/plugins/armorblox/requirements.txt @@ -0,0 +1,4 @@ +# List third-party dependencies here, separated by newlines. +# All dependencies must be version-pinned, eg. requests==1.2.0 +# See: https://pip.pypa.io/en/stable/user_guide/#requirements-files +armorblox-sdk==0.1.4 \ No newline at end of file diff --git a/plugins/armorblox/setup.py b/plugins/armorblox/setup.py new file mode 100644 index 0000000000..b379e014e4 --- /dev/null +++ b/plugins/armorblox/setup.py @@ -0,0 +1,14 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from setuptools import setup, find_packages + + +setup(name="armorblox-armorblox-plugin", + version="1.0.0", + description="Armorblox is an API-based platform that stops targeted email attacks, protects sensitive data, and automates incident response", + author="armorblox", + author_email="", + url="", + packages=find_packages(), + install_requires=['insightconnect-plugin-runtime'], # Add third-party dependencies to requirements.txt, not here! + scripts=['bin/icon_armorblox'] + ) diff --git a/plugins/armorblox/unit_test/__init__.py b/plugins/armorblox/unit_test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/armorblox/unit_test/payloads/get_remediation_action.json b/plugins/armorblox/unit_test/payloads/get_remediation_action.json new file mode 100644 index 0000000000..c8064ba4b9 --- /dev/null +++ b/plugins/armorblox/unit_test/payloads/get_remediation_action.json @@ -0,0 +1,49 @@ +{ + "priority":"HIGH", + "tagged":false, + "date":"2022-08-16T06:51:51Z", + "users":[ + { + "name":"John Doe", + "email":"john.doe@acme.com", + "is_vip":false + } + ], + "policy_names":[ + "Internal Payment Fraud" + ], + "title":"This is IMPERSONATION_VIP test 2022-08-16 12:21", + "remediation_actions":[ + "WILL_AUTO_REMEDIATE" + ], + "resolution_state":"OPEN_INCIDENT_RESOLUTION_STATE", + "object_type":"CONTENT_MAIL", + "id":"63431", + "research_status":"TRUE_POSITIVE", + "app_name":"MICROSOFT_OUTLOOK", + "external_users":[ + { + "name":"Todd Adams", + "email":"joseph_garcia@faketest-bailey.com", + "is_vip":false + } + ], + "external_senders":[ + "joseph_garcia@faketest-bailey.com" + ], + "folder_categories":[ + "SPAM" + ], + "scl_score":5, + "incident_type":"THREAT_INCIDENT_TYPE", + "final_detection_tags":[ + { + "detection_tag_id":"4", + "detection_tag_name":"invoice" + } + ], + "engagements":{ + "fwd_mail_count":"0", + "reply_mail_count":"0" + } +} diff --git a/plugins/armorblox/unit_test/test_connection.py b/plugins/armorblox/unit_test/test_connection.py new file mode 100644 index 0000000000..5457b1417f --- /dev/null +++ b/plugins/armorblox/unit_test/test_connection.py @@ -0,0 +1,28 @@ +import os +import sys + +sys.path.append(os.path.abspath("../")) +import logging +from unittest import TestCase, mock + +from insightconnect_plugin_runtime.exceptions import ConnectionTestException + +from icon_armorblox.connection.connection import Connection +from icon_armorblox.connection.schema import Input + + +class TestConnection(TestCase): + def setUp(self) -> None: + self.connection = Connection() + self.connection.logger = logging.getLogger("connection logger") + + def test_connection_ok(self): + self.connection.connect( + { + Input.API_KEY: "any-api-key", + Input.TENANT_NAME: "my-tenant-name", + } + ) + response = self.connection.test() + expected_response = [] + self.assertEqual(response, expected_response) diff --git a/plugins/armorblox/unit_test/test_get_remediation_action.py b/plugins/armorblox/unit_test/test_get_remediation_action.py new file mode 100644 index 0000000000..0cb324c128 --- /dev/null +++ b/plugins/armorblox/unit_test/test_get_remediation_action.py @@ -0,0 +1,24 @@ +import sys +import os +sys.path.append(os.path.abspath('../')) + +from unittest import TestCase +from icon_armorblox.connection.connection import Connection +from icon_armorblox.actions.get_remediation_action import GetRemediationAction +from icon_armorblox.actions.get_remediation_action.schema import Input, Output +import json +import logging +from unit_test.util import Util +from unittest.mock import patch +from parameterized import parameterized + + +@patch("requests.get", side_effect=Util.mocked_requests) +class TestGetIndicatorDetails(TestCase): + def setUp(self) -> None: + self.action = Util.default_connector(GetRemediationAction()) + @parameterized.expand([("10597"),("11081"),("11063")]) + def test_get_remediation_action(self, mock_post: Mock, incident_id: str) -> None: + actual = self.action.run({Input.INCIDENT_ID: incident_id}) + expected = {'remediation_details': 'WILL_AUTO_REMEDIATE'} + self.assertEqual(actual, expected) diff --git a/plugins/armorblox/unit_test/util.py b/plugins/armorblox/unit_test/util.py new file mode 100644 index 0000000000..ea1d01264e --- /dev/null +++ b/plugins/armorblox/unit_test/util.py @@ -0,0 +1,44 @@ +import json +import logging +import os +from icon_armorblox.connection.connection import Connection +from icon_armorblox.connection.schema import Input + + +class Util: + @staticmethod + def default_connector(action, params: object = None): + default_connection = Connection() + default_connection.logger = logging.getLogger("connection logger") + params = { + Input.API_KEY: {"api_key": {"secretKey": ""}}, + Input.TENANT_NAME : "tenant_name", + } + default_connection.connect(params) + action.connection = default_connection + action.logger = logging.getLogger("action logger") + return action + + + @staticmethod + def mocked_requests(*args, **kwargs): + class MockResponse: + def __init__(self, filename, status_code): + self.filename = filename + self.status_code = status_code + + def json(self): + f = open(os.path.join( + os.path.dirname(os.path.realpath(__file__)), f"payloads/{self.filename}.json" + )) + result = json.load(f) + f.close() + return result + if args[0] == "https://tenant_name.armorblox.io/api/v1beta1/organizations/tenant_name/incidents/10597": + return MockResponse("get_remediation_action", 200) + elif args[0] == "https://tenant_name.armorblox.io/api/v1beta1/organizations/tenant_name/incidents/11081": + return MockResponse("get_remediation_action", 200) + elif args[0] == "https://tenant_name.armorblox.io/api/v1beta1/organizations/tenant_name/incidents/11063": + return MockResponse("get_remediation_action", 200) + + raise Exception("Not implemented")