diff --git a/README.md b/README.md index 7c88f430..27ddd3b9 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ be on your way to contributing! ## Changelog -* 2.47.18 - `HelpInputOutputValidator` | `SpecPropertiesValidator` - Update to enable `placeholder` and `tooltip` validation +* 2.47.18 - `HelpInputOutputValidator` | `SpecPropertiesValidator` - Update to enable `placeholder` and `tooltip` validation | `RuntimeValidator` - Added SDK version validation * 2.47.17 - `SpecPropertiesValidator` - Added new excludeProduct field validator * 2.47.16 - `HelpInputOutputValidator` - Update error message from `icon-plugin` to `insight-plugin` | `DockerValidator` - Print full error message and change instances of `icon-plugin` to `insight-plugin` * 2.47.15 - `TitleValidator` - Change validator to print all issues rather than break on the first diff --git a/icon_validator/constants.py b/icon_validator/constants.py new file mode 100644 index 00000000..8017240c --- /dev/null +++ b/icon_validator/constants.py @@ -0,0 +1 @@ +DEFAULT_TIMEOUT = 3 diff --git a/icon_validator/rules/plugin_validators/runtime_validator.py b/icon_validator/rules/plugin_validators/runtime_validator.py index 04290395..9ba71258 100644 --- a/icon_validator/rules/plugin_validators/runtime_validator.py +++ b/icon_validator/rules/plugin_validators/runtime_validator.py @@ -1,8 +1,12 @@ -import os import glob +import os +import re +from typing import List, Union -from icon_validator.rules.validator import KomandPluginValidator +import requests +from icon_validator.constants import DEFAULT_TIMEOUT from icon_validator.exceptions import ValidationException +from icon_validator.rules.validator import KomandPluginValidator class RuntimeValidator(KomandPluginValidator): @@ -45,31 +49,71 @@ def validate_caching(spec): for path in paths: for root, dirs, files in os.walk(path): for file in files: - with open(os.path.join(root, file), 'r') as open_file: + with open(os.path.join(root, file), "r") as open_file: file_str = open_file.read().replace("\n", "") if "cache" in file_str: - raise ValidationException(f"Cloud ready plugins cannot contain caching. " - f"Update {str(os.path.join(root, file))}.") + raise ValidationException( + f"Cloud ready plugins cannot contain caching. " + f"Update {str(os.path.join(root, file))}." + ) - @staticmethod - def validate_dockerfile(spec, latest_images): + def validate_dockerfile(self, spec, latest_images): if "setup.py" in os.listdir(spec.directory): with open(f"{spec.directory}/setup.py", "r") as setup_file: setup_str = setup_file.read().replace("\n", "") if "insightconnect-plugin-runtime" in setup_str: with open(f"{spec.directory}/Dockerfile", "r") as docker_file: - docker_str = docker_file.read().replace("\n", "") + docker_str = docker_file.read() + docker_image = next( + filter(lambda image: image in docker_str, latest_images), + None, + ) if not any(image in docker_str for image in latest_images): - raise ValidationException("insightconnect-plugin-runtime is being used in setup.py. " - "Update Dockerfile accordingly to use latest base image.") + raise ValidationException( + "insightconnect-plugin-runtime is being used in setup.py. " + "Update Dockerfile accordingly to use latest base image." + ) + + current_tag = self._parse_image_tag(docker_image, docker_str) + latest_tags = self._get_latest_runtime_tags(docker_image) + if current_tag and current_tag not in latest_tags: + raise ValidationException( + f"The current base image tag ({current_tag}) set is not latest. " + f"Please update Dockerfile accordingly to use latest base image." + f" Current latest tags are: {latest_tags}" if latest_tags else "" + ) + + @staticmethod + def _parse_image_tag(image_name: str, dockerfile_content: str) -> Union[str, None]: + match = re.search(f"{image_name}:\s*(.*)", dockerfile_content) + if match: + return match.group(1) + return None + + @staticmethod + def _get_latest_runtime_tags(image_name: str) -> List[str]: + latest_tags = ["latest"] + try: + response = requests.request( + "GET", + f"https://hub.docker.com/v2/repositories/{image_name}/tags", + params={"page_size": 3, "ordering": "last_updated"}, + timeout=DEFAULT_TIMEOUT, + ) + response.raise_for_status() + return latest_tags + [tag.get("name") for tag in response.json().get("results", [])] + except Exception: + return latest_tags def validate(self, spec): - latest_images = ["rapid7/insightconnect-python-3-plugin", - "rapid7/insightconnect-python-3-slim-plugin"] - RuntimeValidator.validate_dockerfile(spec, latest_images) + latest_images = [ + "rapid7/insightconnect-python-3-plugin", + "rapid7/insightconnect-python-3-slim-plugin", + ] + self.validate_dockerfile(spec, latest_images) with open(f"{spec.directory}/Dockerfile", "r") as file: docker_str = file.read().replace("\n", "") diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM new file mode 100755 index 00000000..06adccc0 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM @@ -0,0 +1,23 @@ +{ + "spec": "7b971f1ccf8214be1b140fe212ade68d", + "manifest": "c4fe1b5757c0fbf72a28679cc6303e30", + "setup": "9d01465bd990dde0f93e26a9fbda8a33", + "schemas": [ + { + "identifier": "decode/schema.py", + "hash": "e8816b23112cbb301c9255c29323c4a8" + }, + { + "identifier": "encode/schema.py", + "hash": "70afbd79bd72035de62b32983ee57ba3" + }, + { + "identifier": "connection/schema.py", + "hash": "da5382221ca2a33a2f854e17b068d502" + }, + { + "identifier": "test_task/schema.py", + "hash": "930a8c07873126927dbcaf6d8e5226fa" + } + ] +} \ No newline at end of file diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore new file mode 100755 index 00000000..93dc53fb --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.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/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile new file mode 100755 index 00000000..4f5ebafe --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile @@ -0,0 +1,21 @@ +FROM rapid7/insightconnect-python-3-plugin:4 +LABEL organization=komand +LABEL sdk=python +LABEL type=plugin + +ENV SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_DIR /etc/ssl/certs +ENV REQUESTS_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt + +ADD ./plugin.spec.yaml /plugin.spec.yaml +ADD . /python/src + +WORKDIR /python/src +# Add any package dependencies here + +# End package dependencies +RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +RUN python setup.py build && python setup.py install + + +ENTRYPOINT ["/usr/local/bin/komand_base64"] diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile new file mode 100755 index 00000000..cb85f96b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/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/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 new file mode 100755 index 00000000..17ff2c3b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# GENERATED BY KOMAND SDK - DO NOT EDIT +import os +import json +from sys import argv + +Name = "Base64" +Vendor = "rapid7" +Version = "1.1.2" +Description = "Encode and decode data using the base64 alphabet" + + +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 komand_base64 import connection, actions, triggers, tasks + + class ICONBase64(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_action(actions.Decode()) + + self.add_action(actions.Encode()) + + self.add_task(tasks.TestTask()) + + + """Run plugin""" + cli = insightconnect_plugin_runtime.CLI(ICONBase64()) + cli.run() + + +if __name__ == "__main__": + main() diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png new file mode 100644 index 00000000..abf154a2 Binary files /dev/null and b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png differ diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/help.md b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/help.md new file mode 100755 index 00000000..db7edb56 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/help.md @@ -0,0 +1,173 @@ +# Description + +[Base64](https://en.wikipedia.org/wiki/Base64) is a common binary-to-text encoding scheme used in various protocols and software such as MIME to carry data stored in binary formats across channels that only reliably support text content. This plugin allows data to be Base64-encoded or decoded using the standard Base64 alphabet. + +# Key Features + +* Encode data in Base64 to transfer binary data, image files, etc. in a text format +* Decode Base64 encoded text to reveal the plaintext + +# Requirements + +_This plugin does not contain any requirements._ + +# Supported Product Versions + +* 1.0.0 +* 1.0.1 + +# Documentation + +## Setup + +_This plugin does not contain a connection._ +## Technical Details + +### Actions + +#### Encoder + +This action is used to encode data to Base64. + +##### Input + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|content|string|None|True|Data to encode|None|This is a string| + +Example input: + +``` +{ + "content": "This is a string" +} +``` + +##### Output + +|Name|Type|Required|Description|Example| +|----|----|--------|-----------| +|data|bytes|True|Encoded data result|This is a string| + +Example output: + +``` +{ + "errors": "replace" +} +``` + +#### Decoder + +This action is used to decode Base64 to data. + +##### Input + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|base64|bytes|None|True|Data to decode|None|==Md| +|errors|string|nothing|False|How errors should be handled when decoding Base64|["replace", "ignore", "nothing"]|replace| + +Example input: + +``` +{ + "base64": "==Md", + "errors": "replace" +} +``` + +##### Output + +|Name|Type|Required|Description|Example| +|----|----|--------|-----------| +|data|string|True|Decoded data result|==Md| + +Example output: + +``` +{ + "errors": "replace" +} +``` + +### Triggers + +_This plugin does not contain any triggers._ + +### Tasks + +#### Test Task + +This task is used to decode Base64 to data. + +Supported schedule types for this task include: + - cron + - minutes + - hours + - datetime + +##### Input + +|Name|Type|Default|Required|Description|Enum|Example| +|----|----|-------|--------|-----------|----|-------| +|base64|bytes|None|True|Data to decode|None|1234| +|errors|string|nothing|False|How errors should be handled when decoding Base64|["replace", "ignore", "nothing"]|replace| + +Example input: + +``` +{ + "base64": 12345, + "errors": "replace" +} +``` + +##### Output + +|Name|Type|Required|Description|Example| +|----|----|--------|-----------| +|data|string|True|Decoded data result|12345| + +Example output: + +``` +{ + "errors": "replace" +} +``` + +### Custom Output Types + +_This plugin does not contain any custom output types._ + +## Troubleshooting + +For the Base64 decode action, be sure that the input contains valid Base64 data. + +If the Base64 you're decoding contains any non UTF-8 characters the plugin will fail. To remedy this issue, there's a +option to set how errors are to be handled. These options are "replace" and "ignore". Replace will change all non UTF-8 +characters to `\uffd` or `?`. While ignore will drop the character from the output. + +# Version History + +* 1.1.2 - New spec and help.md format for the Hub +* 1.1.1 - Fixed issue where action Decode required error parameter +* 1.1.0 - Bug fix in decode action, added an option for error handling +* 1.0.0 - Support web server mode +* 0.2.2 - Generate plugin with new schema +* 0.2.1 - SSL bug fix in SDK +* 0.2.0 - Plugin variable naming and description improvements, add required outputs +* 0.1.1 - Bug fix in output variables +* 0.1.0 - Initial plugin + +# Links + +* [Base64](https://en.wikipedia.org/wiki/Base64) + +## References + +* [Base64](https://en.wikipedia.org/wiki/Base64) +* [Python Base64 Encode](https://docs.python.org/2/library/base64.html#base64.standard_b64encode) +* [Python Base64 Decode](https://docs.python.org/2/library/base64.html#base64.standard_b64decode) + diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/icon.png b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/icon.png new file mode 100644 index 00000000..ffe52275 Binary files /dev/null and b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/icon.png differ diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/__init__.py new file mode 100755 index 00000000..bace8db8 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/__init__.py @@ -0,0 +1 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/__init__.py new file mode 100755 index 00000000..c06b0d63 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/__init__.py @@ -0,0 +1,3 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .decode.action import Decode +from .encode.action import Encode diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/__init__.py new file mode 100755 index 00000000..0dfaf704 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .action import Decode diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/action.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/action.py new file mode 100755 index 00000000..dad38bba --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/action.py @@ -0,0 +1,29 @@ +import insightconnect_plugin_runtime +from .schema import DecodeInput, DecodeOutput, Input, Output, Component +from insightconnect_plugin_runtime.exceptions import PluginException +import base64 + + +class Decode(insightconnect_plugin_runtime.Action): + + def __init__(self): + super(self.__class__, self).__init__( + name='decode', + description=Component.DESCRIPTION, + input=DecodeInput(), + output=DecodeOutput()) + + def run(self, params={}): + try: + data = params.get(Input.BASE64) + errors = params.get(Input.ERRORS) + result = base64.standard_b64decode(data) + if errors in ["replace", "ignore"]: + return {Output.DATA: result.decode('utf-8', errors=errors)} + else: + return {Output.DATA: result.decode('utf-8')} + except Exception as e: + self.logger.error("An error has occurred while decoding ", e) + raise PluginException(cause="Internal error", + assistance='An error has occurred while decoding ', + data=e) diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/schema.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/schema.py new file mode 100755 index 00000000..825de548 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/schema.py @@ -0,0 +1,76 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Component: + DESCRIPTION = "Decode Base64 to data" + + +class Input: + BASE64 = "base64" + ERRORS = "errors" + + +class Output: + DATA = "data" + + +class DecodeInput(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "base64": { + "type": "string", + "title": "Base64", + "displayType": "bytes", + "description": "Data to decode", + "format": "bytes", + "order": 1 + }, + "errors": { + "type": "string", + "title": "Errors", + "description": "How errors should be handled when decoding Base64", + "default": "nothing", + "enum": [ + "replace", + "ignore", + "nothing" + ], + "order": 2 + } + }, + "required": [ + "base64" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class DecodeOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "data": { + "type": "string", + "title": "Decoded Data", + "description": "Decoded data result", + "order": 1 + } + }, + "required": [ + "data" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/__init__.py new file mode 100755 index 00000000..66ce46be --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .action import Encode diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/action.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/action.py new file mode 100755 index 00000000..b298a761 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/action.py @@ -0,0 +1,18 @@ +import insightconnect_plugin_runtime +from .schema import EncodeInput, EncodeOutput, Input, Output, Component +import base64 + + +class Encode(insightconnect_plugin_runtime.Action): + + def __init__(self): + super(self.__class__, self).__init__( + name='encode', + description=Component.DESCRIPTION, + input=EncodeInput(), + output=EncodeOutput()) + + def run(self, params={}): + string = params[Input.CONTENT].encode('utf-8') + result = base64.standard_b64encode(string) + return {Output.DATA: result.decode('utf-8')} diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/schema.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/schema.py new file mode 100755 index 00000000..0dce088d --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/schema.py @@ -0,0 +1,63 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Component: + DESCRIPTION = "Encode data to Base64" + + +class Input: + CONTENT = "content" + + +class Output: + DATA = "data" + + +class EncodeInput(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "content": { + "type": "string", + "title": "Content", + "description": "Data to encode", + "order": 1 + } + }, + "required": [ + "content" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class EncodeOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "data": { + "type": "string", + "title": "Encoded Data", + "displayType": "bytes", + "description": "Encoded data result", + "format": "bytes", + "order": 1 + } + }, + "required": [ + "data" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/__init__.py new file mode 100755 index 00000000..a515dcf6 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .connection import Connection diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/connection.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/connection.py new file mode 100755 index 00000000..61487617 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/connection.py @@ -0,0 +1,11 @@ +import insightconnect_plugin_runtime +from .schema import ConnectionSchema + + +class Connection(insightconnect_plugin_runtime.Connection): + + def __init__(self): + super(self.__class__, self).__init__(input=ConnectionSchema()) + + def connect(self, params): + pass diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/schema.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/schema.py new file mode 100755 index 00000000..b93c5a7c --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/schema.py @@ -0,0 +1,15 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Input: + pass + +class ConnectionSchema(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + {} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/__init__.py new file mode 100755 index 00000000..a175e108 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .test_task.task import TestTask diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/__init__.py new file mode 100755 index 00000000..68bb73bc --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from .task import TestTask diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/schema.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/schema.py new file mode 100755 index 00000000..c291b50b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/schema.py @@ -0,0 +1,114 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +import insightconnect_plugin_runtime +import json + + +class Component: + DESCRIPTION = "Decode Base64 to data" + + +class Input: + BASE64 = "base64" + ERRORS = "errors" + + +class State: + LAST_EVENT_ID = "last_event_id" + LAST_EVENT_TIME = "last_event_time" + LAST_RUN_TIME = "last_run_time" + + +class Output: + DATA = "data" + + +class TestTaskInput(insightconnect_plugin_runtime.Input): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "base64": { + "type": "string", + "title": "Base64", + "displayType": "bytes", + "description": "Data to decode", + "format": "bytes", + "order": 1 + }, + "errors": { + "type": "string", + "title": "Errors", + "description": "How errors should be handled when decoding Base64", + "default": "nothing", + "enum": [ + "replace", + "ignore", + "nothing" + ], + "order": 2 + } + }, + "required": [ + "base64" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class TestTaskState(insightconnect_plugin_runtime.State): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "last_event_id": { + "type": "integer", + "title": "Last Event ID", + "description": "The identifier of the last retrieved event", + "order": 2 + }, + "last_event_time": { + "type": "string", + "title": "Last Event Time", + "description": "The datetime of the last retrieved event", + "order": 1 + }, + "last_run_time": { + "type": "string", + "title": "Last Run Time", + "description": "The time of the last time events were retrieved", + "order": 3 + } + } +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) + + +class TestTaskOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(""" + { + "type": "object", + "title": "Variables", + "properties": { + "data": { + "type": "string", + "title": "Decoded Data", + "description": "Decoded data result", + "order": 1 + } + }, + "required": [ + "data" + ] +} + """) + + def __init__(self): + super(self.__class__, self).__init__(self.schema) diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/task.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/task.py new file mode 100755 index 00000000..1f1edf4b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/task.py @@ -0,0 +1,18 @@ +import insightconnect_plugin_runtime +from .schema import TestTaskInput, TestTaskOutput, TestTaskState, Input, Output, Component +# Custom imports below + + +class TestTask(insightconnect_plugin_runtime.Task): + + def __init__(self): + super(self.__class__, self).__init__( + name='test_task', + description=Component.DESCRIPTION, + input=TestTaskInput(), + output=TestTaskOutput(), + state=TestTaskState()) + + def run(self, params={}): + # TODO: Implement run function + return {}, {} diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/triggers/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/triggers/__init__.py new file mode 100755 index 00000000..bace8db8 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/triggers/__init__.py @@ -0,0 +1 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/util/__init__.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/util/__init__.py new file mode 100755 index 00000000..bace8db8 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/util/__init__.py @@ -0,0 +1 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/plugin.spec.yaml b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/plugin.spec.yaml new file mode 100755 index 00000000..ec9f8c3c --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/plugin.spec.yaml @@ -0,0 +1,116 @@ +plugin_spec_version: v2 +extension: plugin +products: [insightconnect] +name: base64 +title: Base64 +description: Encode and decode data using the base64 alphabet +version: 1.1.2 +vendor: rapid7 +support: community +status: [] +supported_versions: ['1.0.0', '1.0.1'] +resources: + source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/base64 + license_url: https://github.com/rapid7/insightconnect-plugins/blob/master/LICENSE +tags: +- base64 +- encoder +- decoder +- utilities +hub_tags: + use_cases: [data_utility] + keywords: [base64, encoder, decoder, utilities] + features: [] +actions: + encode: + title: Encoder + description: Encode data to Base64 + input: + content: + type: string + description: Data to encode + example: This is a string + required: true + output: + data: + title: Encoded Data + description: Encoded data result + type: bytes + required: true + example: This is a string + decode: + title: Decoder + description: Decode Base64 to data + input: + base64: + type: bytes + description: Data to decode + required: true + example: "==Md" + errors: + type: string + description: How errors should be handled when decoding Base64 + default: nothing + example: replace + enum: + - replace + - ignore + - nothing + required: false + output: + data: + title: Decoded Data + description: Decoded data result + type: string + required: true + example: "==Md" +tasks: + test_task: + title: Test Task + description: Decode Base64 to data + input: + base64: + type: bytes + description: Data to decode + required: true + example: 1234 + errors: + type: string + description: How errors should be handled when decoding Base64 + default: nothing + example: replace + enum: + - replace + - ignore + - nothing + required: false + output: + data: + title: Decoded Data + description: Decoded data result + type: string + required: true + example: 12345 + schedule: + title: Schedule for Retrieving Events + type: string + enum: + - "cron" + - "minutes" + - "hours" + - "datetime" + default: cron + state: + last_event_time: + title: Last Event Time + description: The datetime of the last retrieved event + type: string + last_event_id: + title: Last Event ID + description: The identifier of the last retrieved event + type: integer + last_run_time: + title: Last Run Time + description: The time of the last time events were retrieved + type: string + diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/requirements.txt b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/requirements.txt new file mode 100755 index 00000000..82bb7163 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/requirements.txt @@ -0,0 +1,3 @@ +# 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 \ No newline at end of file diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/setup.py b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/setup.py new file mode 100755 index 00000000..f106a417 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/setup.py @@ -0,0 +1,14 @@ +# GENERATED BY KOMAND SDK - DO NOT EDIT +from setuptools import setup, find_packages + + +setup(name="base64-rapid7-plugin", + version="1.1.2", + description="Encode and decode data using the base64 alphabet", + author="rapid7", + author_email="", + url="", + packages=find_packages(), + install_requires=['insightconnect-plugin-runtime'], # Add third-party dependencies to requirements.txt, not here! + scripts=['bin/komand_base64'] + ) diff --git a/unit_test/plugin_examples/version_validator/plugin.spec_bad.yaml b/unit_test/plugin_examples/version_validator/plugin.spec_bad.yaml index 222c5b9c..e7eec576 100755 --- a/unit_test/plugin_examples/version_validator/plugin.spec_bad.yaml +++ b/unit_test/plugin_examples/version_validator/plugin.spec_bad.yaml @@ -1,3 +1,3 @@ plugin_spec_version: v2 name: active_directory_ldap -version: 5.3.3 \ No newline at end of file +version: 9.0.2 \ No newline at end of file diff --git a/unit_test/test_validate_plugin/test_validate_plugin.py b/unit_test/test_validate_plugin/test_validate_plugin.py index ca82eb9e..a2e9213d 100644 --- a/unit_test/test_validate_plugin/test_validate_plugin.py +++ b/unit_test/test_validate_plugin/test_validate_plugin.py @@ -26,6 +26,7 @@ from icon_validator.rules.plugin_validators.help_input_output_validator import convert_to_valid_datetime from icon_validator.rules.plugin_validators.name_validator import NameValidator from icon_validator.rules.plugin_validators.output_validator import OutputValidator +from icon_validator.rules.plugin_validators.runtime_validator import RuntimeValidator import requests from unittest.mock import MagicMock, patch @@ -37,6 +38,8 @@ class TestPluginValidate(unittest.TestCase): NAME_TESTS_DIRECTORY = "plugin_examples/name_tests" GOOD_PLUGIN_DIRECTORY = "plugin_examples/good_plugin" + GOOD_PLUGIN_WITH_TASK_DIRECTORY = "plugin_examples/good_plugin_with_task" + GOOD_PLUGIN_SDK_NOT_LATEST = "plugin_examples/good_plugin_with_task_sdk_not_latest" @parameterized.expand([ ('2023-12-24 12:56:15+05:00', '2023-12-24T12:56:15+05:00'), @@ -776,6 +779,18 @@ def test_schema_output_validator(self) -> None: result = validate(directory_to_test, file_to_test, False, True, [OutputValidator()]) self.assertEqual(result, 0) + def test_runtime_version_validator(self) -> None: + directory_to_test = self.GOOD_PLUGIN_WITH_TASK_DIRECTORY + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [RuntimeValidator()]) + self.assertEqual(result, 0) + + def test_runtime_version_validator_failing(self) -> None: + directory_to_test = self.GOOD_PLUGIN_SDK_NOT_LATEST + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [RuntimeValidator()]) + self.assertEqual(result, 1) + @staticmethod def replace_requirements(path, text): f = open(path, 'w')