diff --git a/plugins/python_3_script/.CHECKSUM b/plugins/python_3_script/.CHECKSUM index 50c0bbbc4f..6df0fd1e97 100644 --- a/plugins/python_3_script/.CHECKSUM +++ b/plugins/python_3_script/.CHECKSUM @@ -1,7 +1,7 @@ { - "spec": "29d983b124964524c684549fd1ceb355", - "manifest": "c40652105e23b83e0975e0991a587371", - "setup": "2909819c387092e88d56e1079094e6a0", + "spec": "35a664c2015a27eb5a5026d746f7213c", + "manifest": "50ec47def361c8d3e6ecfd7503f1964d", + "setup": "f02f843d20f1de6d92372cdf5a8656e5", "schemas": [ { "identifier": "run/schema.py", diff --git a/plugins/python_3_script/Dockerfile b/plugins/python_3_script/Dockerfile index e049c6bae0..bb0edc9d94 100755 --- a/plugins/python_3_script/Dockerfile +++ b/plugins/python_3_script/Dockerfile @@ -1,4 +1,4 @@ -FROM rapid7/insightconnect-python-3-38-plugin:5 +FROM rapid7/insightconnect-python-3-slim-plugin:5 LABEL organization=rapid7 LABEL sdk=python @@ -7,21 +7,20 @@ 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 +ENV PYTHONUSERBASE=/var/cache/python_dependencies +ENV PYTHONPATH=/var/cache/python_dependencies -ADD ./plugin.spec.yaml /plugin.spec.yaml -ADD . /python/src +RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests libxslt-dev libxml2-dev gcc g++ -y WORKDIR /python/src -# Add any package dependencies here -RUN apt-get update && \ - apt-get install --no-install-recommends --no-install-suggests -y libxslt-dev libxml2-dev gcc g++ && \ - apt-get clean -y -ENV PYTHONUSERBASE=/var/cache/python_dependencies \ - PYTHONPATH=/var/cache/python_dependencies +ADD ./plugin.spec.yaml /plugin.spec.yaml +ADD ./requirements.txt /python/src/requirements.txt + +RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + +ADD . /python/src -# End package dependencies -RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi RUN python setup.py build && python setup.py install # User to run plugin code. The two supported users are: root, nobody diff --git a/plugins/python_3_script/bin/icon_python_3_script b/plugins/python_3_script/bin/icon_python_3_script index 4a1514180f..e0007b9ea9 100755 --- a/plugins/python_3_script/bin/icon_python_3_script +++ b/plugins/python_3_script/bin/icon_python_3_script @@ -6,7 +6,7 @@ from sys import argv Name = "Python 3 Script" Vendor = "rapid7" -Version = "4.0.6" +Version = "4.0.7" Description = "Python is a programming language that lets you work quickly and integrate systems more effectively. This plugin allows you to run Python 3 code. It includes Python 3.8.1 and its standard library as well as the following 3rd party libraries" diff --git a/plugins/python_3_script/help.md b/plugins/python_3_script/help.md index 8ff2c4d219..2b21b08488 100644 --- a/plugins/python_3_script/help.md +++ b/plugins/python_3_script/help.md @@ -6,7 +6,7 @@ * [maya 0.6.1](https://pypi.python.org/pypi/maya) * [lxml 4.9.2](http://lxml.de/) * [beautifulsoup 4.12.2](https://www.crummy.com/software/BeautifulSoup/) -* [pyyaml 6.0.0](http://pyyaml.org/) +* [pyyaml 6.0.1](http://pyyaml.org/) * [records 0.5.3](https://github.com/kennethreitz/records) The Python 3 Script plugin also allows you to load custom modules via its connection parameters. @@ -23,7 +23,7 @@ Also, this plugin allows you to provide additional credentials in the connection # Supported Product Versions -* Python 3.8.1 +* Python 3.9.18 # Documentation @@ -134,6 +134,7 @@ If installation fails, try increasing the `Timeout` connection input to `900` (1 # Version History +* 4.0.7 - Updated the SDK | Updated Python version to `3.9.18` | Added handler to run function separately * 4.0.6 - Added empty `__init__.py` file to `unit_test` folder | Refreshed with new tooling * 4.0.5 - Updated the SDK version to include output masking | Updated all dependencies to the newest versions * 4.0.4 - Update Pyyaml to version 6.0.0 diff --git a/plugins/python_3_script/icon_python_3_script/actions/run/action.py b/plugins/python_3_script/icon_python_3_script/actions/run/action.py index cef2870517..3659a6bd87 100755 --- a/plugins/python_3_script/icon_python_3_script/actions/run/action.py +++ b/plugins/python_3_script/icon_python_3_script/actions/run/action.py @@ -1,16 +1,19 @@ +import subprocess # nosec B404 import sys -from typing import Any, Dict +from typing import Any, Dict, Union +from uuid import uuid4 import insightconnect_plugin_runtime from insightconnect_plugin_runtime.exceptions import PluginException from insightconnect_plugin_runtime.helper import clean +from icon_python_3_script.util.constants import DEFAULT_ENCODING, DEFAULT_PROCESS_TIMEOUT, INDENTATION_CHARACTER +from icon_python_3_script.util.util import extract_output_from_stdout + from .schema import Component, Input, RunInput, RunOutput sys.path.append("/var/cache/python_dependencies/lib/python3.8/site-packages") -INDENTATION_CHARACTER = " " * 4 - class Run(insightconnect_plugin_runtime.Action): def __init__(self): @@ -28,15 +31,15 @@ def run(self, params={}): self.logger.info(f"Function: (below)\n\n{params.get(Input.FUNCTION)}\n") try: - out = self._exec_python_function(function_=function_, params=params) + output = self._execute_function_as_process(function_, params.get(Input.INPUT, {})) except Exception as error: raise PluginException(cause="Could not run supplied script", data=str(error)) try: - if out is None: + if output is None: raise PluginException( cause="Output type was None", assistance="Ensure that output has a non-None data type" ) - return out + return output except UnboundLocalError: raise PluginException( cause="No output was returned.", assistance="Check supplied script to ensure that it returns output" @@ -45,32 +48,69 @@ def run(self, params={}): @staticmethod def _exec_python_function(function_: str, params: Dict[str, Any]) -> Dict[str, Any]: """ - Executes python function and returning it's data + Executes python function and returning its data :param function_: Python script function :type: str + :param params: Parameters to be added to the function :type: Dict[str, Any] + :return: Output of the functions response :rtype: Dict[str, Any] """ exec(function_) # noqa: B102 function_name = function_.split(" ")[1].split("(")[0] - out = locals()[function_name](params.get(Input.INPUT)) - return out + return locals()[function_name](params.get(Input.INPUT)) + + @staticmethod + def _execute_function_as_process(function_: str, parameters: Dict[str, Any]) -> Union[Dict[str, Any], None]: + """ + Execute a function as a separate process and return its data. + + :param function_: The declaration of the run function to execute. + :type: str + + :param parameters: The input parameters to pass to the function. + :type: Dict[str, Any] + + :return: The result of the function execution. + :rtype: Union[Dict[str, Any], None] + """ + + execution_id = f"Python3Script-ActionRun-{uuid4()}:" + function_name = function_.split(" ")[1].split("(")[0] + try: + output = subprocess.check_output( # nosec B603, B607 + [ + "python", + "-c", + f'import sys\n\n{function_}\nsys.stdout.write("{execution_id}" + str({function_name}({parameters})))', + ], + shell=False, + stderr=subprocess.PIPE, + timeout=DEFAULT_PROCESS_TIMEOUT, + ) + return extract_output_from_stdout(output.decode(DEFAULT_ENCODING), execution_id) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + raise PluginException(error.stderr.decode(DEFAULT_ENCODING)) + @staticmethod def _add_credentials_to_function( - self, function_: str, credentials: Dict[str, str], indentation_character: str = INDENTATION_CHARACTER + function_: str, credentials: Dict[str, str], indentation_character: str = INDENTATION_CHARACTER ) -> str: """ This function adds credentials to the function entered by the user. It looks for connection script - credentials: username, password, secret_key, and adds all of them to variables inside of the function. + credentials: username, password, secret_key, and adds all of them to variables inside the function. :param function_: Python script function :type: str + :param credentials: Credentials to be added :type: Dict[str, str] + :param indentation_character: Indentation character used in function :type: str + :return: Function string appended by the credential variables :rtype: str """ @@ -90,6 +130,7 @@ def _check_indentation_character(function_: str) -> str: This function returns the indentation character that is used in the input python's function :param function_: Python script function :type: str + :return: Indentation character that is used :rtype: str """ diff --git a/plugins/python_3_script/icon_python_3_script/util/constants.py b/plugins/python_3_script/icon_python_3_script/util/constants.py new file mode 100644 index 0000000000..1c3af70640 --- /dev/null +++ b/plugins/python_3_script/icon_python_3_script/util/constants.py @@ -0,0 +1,3 @@ +INDENTATION_CHARACTER = " " * 4 +DEFAULT_ENCODING = "utf-8" +DEFAULT_PROCESS_TIMEOUT = 5 * 60 diff --git a/plugins/python_3_script/icon_python_3_script/util/util.py b/plugins/python_3_script/icon_python_3_script/util/util.py new file mode 100644 index 0000000000..5e828c12aa --- /dev/null +++ b/plugins/python_3_script/icon_python_3_script/util/util.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Union + +import yaml + + +def extract_output_from_stdout(input_stdout: str, output_prefix: str) -> Union[Dict[str, Any], None]: + """ + Extract output from a string representing standard output. + + This function parses the provided `input_stdout` string and extracts any output data + that start with the specified `output_prefix`. The extracted output is returned as a + dictionary where the keys are the extracted output lines without the prefix. + + :param input_stdout: The string representing the standard output to extract from. + :type: str + + :param output_prefix: The prefix indicating the lines to extract from `input_stdout`. + :type: str + + :return: A dictionary containing the extracted output. + :rtype: Union[Dict[str, Any], None] + """ + + if output_prefix in input_stdout: + function_output = yaml.safe_load(input_stdout[input_stdout.index(output_prefix) + len(output_prefix) :]) + if isinstance(function_output, str) and function_output.lower().strip() == "none": + return None + return function_output + return None diff --git a/plugins/python_3_script/plugin.spec.yaml b/plugins/python_3_script/plugin.spec.yaml index 9e39010e7c..69a714a7b3 100644 --- a/plugins/python_3_script/plugin.spec.yaml +++ b/plugins/python_3_script/plugin.spec.yaml @@ -7,12 +7,17 @@ vendor: rapid7 support: rapid7 status: [] description: Python is a programming language that lets you work quickly and integrate systems more effectively. This plugin allows you to run Python 3 code. It includes Python 3.8.1 and its standard library as well as the following 3rd party libraries -version: 4.0.6 -supported_versions: ["Python 3.8.1"] +version: 4.0.7 +supported_versions: ["Python 3.9.18"] sdk: type: full version: 5 user: root + packages: + - libxslt-dev + - libxml2-dev + - gcc + - g++ enable_cache: true resources: source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/python_3_script @@ -42,7 +47,7 @@ connection: required: true type: integer default: 60 - example: '120' + example: 120 script_secret_key: title: Script Secret Key description: Credential secret key available in script as python variable (`secret_key`) diff --git a/plugins/python_3_script/requirements.txt b/plugins/python_3_script/requirements.txt index d8f5289dc4..68894bd4b8 100755 --- a/plugins/python_3_script/requirements.txt +++ b/plugins/python_3_script/requirements.txt @@ -6,6 +6,6 @@ maya==0.6.1 lxml==4.9.2 datetime==5.1 setuptools==65.5.1 -pyyaml==6.0.0 +PyYAML==6.0.1 beautifulsoup4==4.12.2 parameterized==0.8.1 diff --git a/plugins/python_3_script/setup.py b/plugins/python_3_script/setup.py index 55bef5a068..f164a22c3d 100755 --- a/plugins/python_3_script/setup.py +++ b/plugins/python_3_script/setup.py @@ -3,7 +3,7 @@ setup(name="python_3_script-rapid7-plugin", - version="4.0.6", + version="4.0.7", description="Python is a programming language that lets you work quickly and integrate systems more effectively. This plugin allows you to run Python 3 code. It includes Python 3.8.1 and its standard library as well as the following 3rd party libraries", author="rapid7", author_email="", diff --git a/plugins/python_3_script/unit_test/inputs/run_no_credentials.json.inp b/plugins/python_3_script/unit_test/inputs/run_no_credentials.json.inp index aae2488ec5..db8ab93c7f 100644 --- a/plugins/python_3_script/unit_test/inputs/run_no_credentials.json.inp +++ b/plugins/python_3_script/unit_test/inputs/run_no_credentials.json.inp @@ -1,5 +1,5 @@ { - "function": "def run(params={}):\n\treturn {'some_output': params.get('some_input')", + "function": "def run(params={}):\n\treturn {'some_output': params.get('some_input')}", "input": { "some_input": "no_credentials" }