diff --git a/.circleci/config.yml b/.circleci/config.yml index da8e680c..c0ae7a06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -86,6 +86,30 @@ jobs: working_directory: ~/openaev/nmap name: Tests for nmap injector command: python -m unittest + test-http-query: + working_directory: ~/openaev + docker: + - image: cimg/python:3.13 + steps: + - checkout + - setup_remote_docker + - run: + working_directory: ~/openaev/http-query + name: Install dependencies for http-query injector + command: pip install -r src/requirements.txt + - run: + working_directory: ~/openaev/http-query + name: Overwrite pyoaev with correct version from CI + command: | + if [ "${CIRCLE_BRANCH}" = "main" ]; then + pip install --force-reinstall git+https://github.com/OpenAEV-Platform/client-python.git@main + else + pip install --force-reinstall git+https://github.com/OpenAEV-Platform/client-python.git@release/current + fi + - run: + working_directory: ~/openaev/http-query + name: Tests for http-query injector + command: python -m unittest build_1: working_directory: ~/openaev docker: @@ -271,6 +295,7 @@ workflows: - linter - test-nmap - test-nuclei + - test-http-query - build_1: filters: tags: @@ -283,6 +308,7 @@ workflows: - linter - test-nmap - test-nuclei + - test-http-query filters: branches: only: @@ -293,6 +319,7 @@ workflows: - linter - test-nmap - test-nuclei + - test-http-query filters: branches: only: diff --git a/http-query/README.md b/http-query/README.md index 5e4e3a64..3ba2f234 100644 --- a/http-query/README.md +++ b/http-query/README.md @@ -84,3 +84,14 @@ python3 openaev_http.py ## Behavior This injector enables new inject contracts, allowing for API calls of the Get, Post, and Put types. + +### Passing headers with the request +The contracts created in OpenAEV include an optional "Headers" parameter. While this field is technically a free-form +text input, the contents passed to it are expected to be in a certain format: `key=value` and together separated with a comma `,`. + +Note the pattern supports whitespace within the keys and values. + +Example: +```plaintext +content-type=application/json,x-custom-header=value for the header +``` \ No newline at end of file diff --git a/http-query/src/helpers/__init__.py b/http-query/src/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/http-query/src/helpers/helpers.py b/http-query/src/helpers/helpers.py new file mode 100644 index 00000000..55bfa0f0 --- /dev/null +++ b/http-query/src/helpers/helpers.py @@ -0,0 +1,42 @@ +class HTTPHelpers: + @staticmethod + def parse_headers(headers_str): + if isinstance(headers_str, list): + return headers_str + headers_list = [] + for kv in headers_str.split(","): + if "=" in kv: + k, v = kv.split("=", 1) + headers_list.append({"key": k.strip(), "value": v.strip()}) + return headers_list + + @staticmethod + def parse_parts(parts_str): + if isinstance(parts_str, list): + return parts_str + parts_list = [] + for kv in parts_str.split("&"): + if "=" in kv: + k, v = kv.split("=", 1) + parts_list.append({"key": k.strip(), "value": v.strip()}) + return parts_list + + @staticmethod + def request_data_parts_body(request_data): + parts = HTTPHelpers.parse_parts( + request_data["injection"]["inject_content"]["parts"] + ) + keys = list(map(lambda p: p["key"], parts)) + values = list(map(lambda p: p["value"], parts)) + return dict(zip(keys, values)) + + @staticmethod + def response_parsing(response): + success = 200 <= response.status_code < 300 + success_status = "SUCCESS" if success else "ERROR" + return { + "url": response.url, + "code": response.status_code, + "status": success_status, + "message": response.text, + } diff --git a/http-query/src/openaev_http.py b/http-query/src/openaev_http.py index 483e5273..7a682506 100644 --- a/http-query/src/openaev_http.py +++ b/http-query/src/openaev_http.py @@ -11,6 +11,7 @@ HTTP_RAW_PUT_CONTRACT, HttpContracts, ) +from helpers.helpers import HTTPHelpers from pyoaev.helpers import OpenAEVConfigHelper, OpenAEVInjectorHelper @@ -43,24 +44,6 @@ def __init__(self): self.config, open("img/icon-http.png", "rb") ) - @staticmethod - def _request_data_parts_body(request_data): - parts = request_data["injection"]["inject_content"]["parts"] - keys = list(map(lambda p: p["key"], parts)) - values = list(map(lambda p: p["value"], parts)) - return dict(zip(keys, values)) - - @staticmethod - def _response_parsing(response): - success = 200 <= response.status_code < 300 - success_status = "SUCCESS" if success else "ERROR" - return { - "url": response.url, - "code": response.status_code, - "status": success_status, - "message": response.text, - } - def attachments_to_files(self, request_data): documents = request_data["injection"].get("inject_documents", []) attachments = list(filter(lambda d: d["document_attached"] is True, documents)) @@ -73,7 +56,9 @@ def attachments_to_files(self, request_data): def http_execution(self, data: Dict): # Build headers - inject_headers = data["injection"]["inject_content"].get("headers", []) + inject_headers = HTTPHelpers.parse_headers( + data["injection"]["inject_content"].get("headers", []) + ) headers = {} for header_definition in inject_headers: headers[header_definition["key"]] = header_definition["value"] @@ -93,35 +78,35 @@ def http_execution(self, data: Dict): # Get if inject_contract == HTTP_GET_CONTRACT: response = session.get(url=url, headers=headers) - return self._response_parsing(response) + return HTTPHelpers.response_parsing(response) # Post if inject_contract == HTTP_RAW_POST_CONTRACT: body = data["injection"]["inject_content"]["body"] response = session.post( url=url, headers=headers, data=body, files=http_files ) - return self._response_parsing(response) + return HTTPHelpers.response_parsing(response) # Put if inject_contract == HTTP_RAW_PUT_CONTRACT: body = data["injection"]["inject_content"]["body"] response = session.put( url=url, headers=headers, data=body, files=http_files ) - return self._response_parsing(response) + return HTTPHelpers.response_parsing(response) # Form Post if inject_contract == HTTP_FORM_POST_CONTRACT: - body = self._request_data_parts_body(data) + body = HTTPHelpers.request_data_parts_body(data) response = session.post( url=url, headers=headers, data=body, files=http_files ) - return self._response_parsing(response) + return HTTPHelpers.response_parsing(response) # Form Put if inject_contract == HTTP_FORM_PUT_CONTRACT: - body = self._request_data_parts_body(data) + body = HTTPHelpers.request_data_parts_body(data) response = session.put( url=url, headers=headers, data=body, files=http_files ) - return self._response_parsing(response) + return HTTPHelpers.response_parsing(response) # Nothing supported return { "code": 400, diff --git a/http-query/test/__init__.py b/http-query/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/http-query/test/helpers/__init__.py b/http-query/test/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/http-query/test/helpers/test_helpers.py b/http-query/test/helpers/test_helpers.py new file mode 100644 index 00000000..d243532c --- /dev/null +++ b/http-query/test/helpers/test_helpers.py @@ -0,0 +1,33 @@ +from unittest import TestCase + +from src.helpers.helpers import HTTPHelpers + + +class HTTPHelpersTest(TestCase): + def test_parse_headers_with_string(self): + input_str = ( + "Content-Type=application/x-www-form-urlencoded,Accept=application/json" + ) + expected = [ + {"key": "Content-Type", "value": "application/x-www-form-urlencoded"}, + {"key": "Accept", "value": "application/json"}, + ] + result = HTTPHelpers.parse_headers(input_str) + self.assertEqual(result, expected) + + def test_parse_parts_with_string(self): + input_str = "msg=test&user=alice" + expected = [ + {"key": "msg", "value": "test"}, + {"key": "user", "value": "alice"}, + ] + result = HTTPHelpers.parse_parts(input_str) + self.assertEqual(result, expected) + + def test_parse_headers_empty_string(self): + result = HTTPHelpers.parse_headers("") + self.assertEqual(result, []) + + def test_parse_parts_empty_string(self): + result = HTTPHelpers.parse_parts("") + self.assertEqual(result, [])