├── C2_Profiles └── .keep ├── agent_icons ├── .keep └── vectr.svg ├── documentation-c2 └── .keep ├── Payload_Type ├── __init__.py └── vectr │ ├── vectr │ ├── VectrRequests │ │ ├── __init__.py │ │ ├── VectrAPIClasses.py │ │ └── VectrAPI.py │ ├── agent_functions │ │ ├── __init__.py │ │ ├── testcase_list.py │ │ ├── builder.py │ │ ├── testcase_post_raw.py │ │ ├── testcase_delete.py │ │ ├── testcase_get_raw.py │ │ ├── testcase_update_name.py │ │ ├── testcase_update_operator_guidance.py │ │ ├── testcase_upload_artifact.py │ │ ├── testcase_update_mitre_id.py │ │ ├── vectr.svg │ │ └── testcase_create.py │ ├── __init__.py │ └── browser_scripts │ │ ├── testcase_create.js │ │ └── testcase_list.js │ ├── main.py │ ├── .docker │ ├── requirements.txt │ └── Dockerfile │ └── Dockerfile ├── documentation-payload └── .keep ├── documentation-wrapper └── .keep ├── config.json ├── agent_capabilities.json ├── README.md └── .gitignore /C2_Profiles/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent_icons/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-c2/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-payload/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-wrapper/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/VectrRequests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/vectr/main.py: -------------------------------------------------------------------------------- 1 | import mythic_container 2 | import asyncio 3 | # import the vectr agent 4 | import vectr 5 | 6 | mythic_container.mythic_service.start_and_run_forever() 7 | -------------------------------------------------------------------------------- /Payload_Type/vectr/.docker/requirements.txt: -------------------------------------------------------------------------------- 1 | mythic-container==0.5.28 2 | pydantic==1.10.12 3 | gql==3.4.1 4 | requests-toolbelt==1.0.0 5 | python-dotenv==0.21.0 6 | requests==2.32.3 7 | PyNaCl==1.5.0 -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_payload_type": false, 3 | "exclude_c2_profiles": false, 4 | "exclude_documentation_payload": false, 5 | "exclude_documentation_c2": false, 6 | "exclude_agent_icons": false 7 | } -------------------------------------------------------------------------------- /agent_capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "os": [ 3 | "Windows", 4 | "Linux", 5 | "macOS" 6 | ], 7 | "languages": [ 8 | "python" 9 | ], 10 | "features": { 11 | "mythic": [ 12 | "browser scripts", 13 | "docker" 14 | ], 15 | "custom": [] 16 | }, 17 | "payload_output": [], 18 | "architectures": [], 19 | "c2": [], 20 | "mythic_version": "3.3", 21 | "agent_version": "0.0.1", 22 | "supported_wrappers": [] 23 | } -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/__init__.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os.path 3 | from pathlib import Path 4 | from importlib import import_module, invalidate_caches 5 | import sys 6 | # Get file paths of all modules. 7 | 8 | currentPath = Path(__file__) 9 | searchPath = currentPath.parent / "agent_functions" / "*.py" 10 | modules = glob.glob(f"{searchPath}") 11 | invalidate_caches() 12 | for x in modules: 13 | if not x.endswith("__init__.py") and x[-3:] == ".py": 14 | module = import_module(f"{__name__}.agent_functions." + Path(x).stem) 15 | for el in dir(module): 16 | if "__" not in el: 17 | globals()[el] = getattr(module, el) 18 | 19 | 20 | sys.path.append(os.path.abspath(currentPath.name)) 21 | -------------------------------------------------------------------------------- /Payload_Type/vectr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /Payload_Type/vectr/.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/browser_scripts/testcase_create.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let response_data = JSON.parse(responses[0]); 11 | let data = response_data["testcases"]; 12 | let output_table = []; 13 | for(let i = 0; i < data.length; i++){ 14 | output_table.push({ 15 | "id": {"plaintext": data[i]["id"], "copyIcon": true }, 16 | "name": {"plaintext": data[i]["name"]}, 17 | }); 18 | } 19 | return { 20 | "table": [ 21 | { 22 | "headers": [ 23 | {"plaintext": "id", "type": "string", "width": 100}, 24 | {"plaintext": "name", "type": "string", "fillWidth": true}, 25 | ], 26 | "rows": output_table, 27 | "title": "Created Test Case" 28 | } 29 | ] 30 | } 31 | }catch(error){ 32 | console.log(error); 33 | const combined = responses.reduce( (prev, cur) => { 34 | return prev + cur; 35 | }, ""); 36 | return {'plaintext': combined}; 37 | } 38 | }else{ 39 | return {"plaintext": "No output from command"}; 40 | } 41 | }else{ 42 | return {"plaintext": "No data to display..."}; 43 | } 44 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | VECTR 3 |

4 |
5 | 6 | # VECTR 7 | 8 | This is a Mythic agent for interacting with the 3rd party service, [VECTR](https://github.com/SecurityRiskAdvisors/VECTR). 9 | 10 | This doesn't build a payload, but instead generates a "callback" within Mythic that allows you to interact with VECTR's API. 11 | 12 | Once you have VECTR running, set your API key value, VECTR_API_KEY, as a user secret in the Mythic UI (red key on your operator settings page). 13 | 14 | ## How to install an agent in this format within Mythic 15 | 16 | When it's time for you to test out your install or for another user to install your agent, it's pretty simple. Within Mythic you can run the `mythic-cli` binary to install this in one of three ways: 17 | 18 | * `sudo ./mythic-cli install github https://github.com/user/repo` to install the main branch 19 | * `sudo ./mythic-cli install github https://github.com/user/repo branchname` to install a specific branch of that repo 20 | * `sudo ./mythic-cli install folder /path/to/local/folder/cloned/from/github` to install from an already cloned down version of an agent repo 21 | 22 | Now, you might be wondering _when_ should you or a user do this to properly add your agent to their Mythic instance. There's no wrong answer here, just depends on your preference. The three options are: 23 | 24 | * Mythic is already up and going, then you can run the install script and just direct that agent's containers to start (i.e. `sudo ./mythic-cli start agentName` and if that agent has its own special C2 containers, you'll need to start them too via `sudo ./mythic-cli start c2profileName`). 25 | * Mythic is already up and going, but you want to minimize your steps, you can just install the agent and run `sudo ./mythic-cli start`. That script will first _stop_ all of your containers, then start everything back up again. This will also bring in the new agent you just installed. 26 | * Mythic isn't running, you can install the script and just run `sudo ./mythic-cli start`. 27 | 28 | ## Documentation 29 | 30 | View the rendered documentation by clicking on **Docs -> Agent Documentation** in the upper right-hand corner of the Mythic interface. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | .DS_Store 7 | rabbitmq_config.json 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_list.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | 7 | class TestCaseListArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [] 12 | 13 | async def parse_arguments(self): 14 | pass 15 | 16 | async def parse_dictionary(self, dictionary_arguments): 17 | pass 18 | 19 | 20 | class TestCaseList(CommandBase): 21 | cmd = "list_testcase" 22 | needs_admin = False 23 | help_cmd = "list_testcase" 24 | description = "List test cases from Vectr" 25 | version = 2 26 | author = "@ajpc500" 27 | argument_class = TestCaseListArguments 28 | supported_ui_features = ["vectr:testcase_list"] 29 | browser_script = BrowserScript(script_name="testcase_list", author="@ajpc500") 30 | attackmapping = [] 31 | completion_functions = { 32 | } 33 | 34 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 35 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 36 | TaskID=taskData.Task.ID, 37 | Success=False, 38 | Completed=True, 39 | DisplayParams=f"" 40 | ) 41 | try: 42 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 43 | response_code, response_data = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 44 | return await VectrAPI.process_standard_response( 45 | response_code=response_code, 46 | response_data=response_data, 47 | taskData=taskData, 48 | response=response 49 | ) 50 | except Exception as e: 51 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 52 | TaskID=taskData.Task.ID, 53 | Response=f"{e}".encode("UTF8"), 54 | )) 55 | response.TaskStatus = "Error: Vectr Access Error" 56 | response.Success = False 57 | return response 58 | 59 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 60 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 61 | return resp 62 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/builder.py: -------------------------------------------------------------------------------- 1 | from mythic_container.PayloadBuilder import * 2 | from mythic_container.MythicCommandBase import * 3 | from mythic_container.MythicRPC import * 4 | 5 | from vectr.VectrRequests import VectrAPI 6 | 7 | class Vectr(PayloadType): 8 | name = "vectr" 9 | file_extension = "" 10 | author = "@ajpc500" 11 | supported_os = [ 12 | SupportedOS("vectr") 13 | ] 14 | wrapper = False 15 | wrapped_payloads = [] 16 | note = """ 17 | This payload communicates with an existing Vectr instance. In your settings, add your Vectr API key as a secret with the key "VECTR_API_KEY". 18 | """ 19 | supports_dynamic_loading = False 20 | mythic_encrypts = True 21 | translation_container = None 22 | agent_type = "service" 23 | agent_path = pathlib.Path(".") / "vectr" 24 | agent_icon_path = agent_path / "agent_functions" / "vectr.svg" 25 | agent_code_path = agent_path / "agent_code" 26 | build_parameters = [ 27 | BuildParameter( 28 | name="URL", 29 | description="Vectr API URL", 30 | parameter_type=BuildParameterType.String, 31 | default_value="https://vectr:8081/sra-purpletools-rest" 32 | ), 33 | BuildParameter( 34 | name="org_name", 35 | description="Vectr Organization Name", 36 | parameter_type=BuildParameterType.String, 37 | default_value="MYTHIC" 38 | ), 39 | BuildParameter( 40 | name="assessment_name", 41 | description="Vectr Assessment Name", 42 | parameter_type=BuildParameterType.String, 43 | default_value="Mythic Assessment" 44 | ), 45 | BuildParameter( 46 | name="campaign_name", 47 | description="Vectr Campaign Name", 48 | parameter_type=BuildParameterType.String, 49 | default_value="Mythic Campaign" 50 | ), 51 | BuildParameter( 52 | name="target_db", 53 | description="Vectr Target Database", 54 | parameter_type=BuildParameterType.String, 55 | default_value="MYTHIC" 56 | ), 57 | ] 58 | c2_profiles = [] 59 | 60 | async def build(self) -> BuildResponse: 61 | # this function gets called to create an instance of your payload 62 | resp = BuildResponse(status=BuildStatus.Success) 63 | ip = "127.0.0.1" 64 | 65 | create_callback = await SendMythicRPCCallbackCreate(MythicRPCCallbackCreateMessage( 66 | PayloadUUID=self.uuid, 67 | C2ProfileName="", 68 | User="VECTR", 69 | Host="VECTR", 70 | Domain=self.get_parameter('URL'), 71 | Ip=ip, 72 | IntegrityLevel=3, 73 | )) 74 | if not create_callback.Success: 75 | logger.info(create_callback.Error) 76 | else: 77 | logger.info(create_callback.CallbackUUID) 78 | 79 | update_description = await SendMythicRPCCallbackUpdate(MythicRPCCallbackUpdateMessage( 80 | AgentCallbackUUID=create_callback.CallbackUUID, 81 | Description=f"A: {self.get_parameter('assessment_name')} / C: {self.get_parameter('campaign_name')}" 82 | )) 83 | if not update_description.Success: 84 | logger.info(update_description.Error) 85 | 86 | return resp 87 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_post_raw.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | class TestCasePostRawJsonArguments(TaskArguments): 7 | def __init__(self, command_line, **kwargs): 8 | super().__init__(command_line, **kwargs) 9 | self.args = [ 10 | CommandParameter( 11 | name="test_case_json", 12 | type=ParameterType.String, 13 | description="A raw VECTR test case JSON", 14 | parameter_group_info=[ParameterGroupInfo( 15 | required=True 16 | )] 17 | ) 18 | ] 19 | 20 | async def parse_arguments(self): 21 | if len(self.command_line) == 0: 22 | raise ValueError("Must supply a VECTR test case and new name") 23 | raise ValueError("Must supply named arguments or use the modal") 24 | 25 | async def parse_dictionary(self, dictionary_arguments): 26 | if "test_case_json" in dictionary_arguments: 27 | self.add_arg("test_case_json", dictionary_arguments["test_case_json"]) 28 | 29 | 30 | class TestCasePostRawJson(CommandBase): 31 | cmd = "post_raw_testcase_json" 32 | needs_admin = False 33 | help_cmd = "post_raw_testcase_json -test_case_json '{}'" 34 | description = "POST raw JSON for a VECTR test case" 35 | version = 2 36 | author = "@ajpc500" 37 | argument_class = TestCasePostRawJsonArguments 38 | attackmapping = [] 39 | completion_functions = { 40 | } 41 | 42 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 43 | test_case_json = taskData.args.get_arg("test_case_json") 44 | 45 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 46 | TaskID=taskData.Task.ID, 47 | Success=False, 48 | Completed=True, 49 | DisplayParams=f"" 50 | ) 51 | try: 52 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 53 | 54 | try: 55 | test_case_json = json.loads(test_case_json) 56 | except json.JSONDecodeError as e: 57 | raise ValueError(f"test_case_json is not valid JSON: {e}") 58 | 59 | response_code, response_data = VectrAPI.rest_update_test_case( 60 | rest_vectr.connection_params, rest_vectr.target_db, test_case_json 61 | ) 62 | return await VectrAPI.process_standard_response( 63 | response_code=response_code, 64 | response_data=response_data['message'], 65 | taskData=taskData, 66 | response=response, 67 | as_json=False 68 | ) 69 | 70 | except Exception as e: 71 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 72 | TaskID=taskData.Task.ID, 73 | Response=f"{e}".encode("UTF8"), 74 | )) 75 | response.TaskStatus = "Error: Vectr Access Error" 76 | response.Success = False 77 | return response 78 | 79 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 80 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 81 | return resp 82 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_delete.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseDeleteArguments(TaskArguments): 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="test_case_id", 14 | type=ParameterType.ChooseOne, 15 | dynamic_query_function=self.get_vectr_test_cases, 16 | description="VECTR test case ID", 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True 19 | )] 20 | ), 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) > 0: 25 | if self.command_line[0] == '{': 26 | temp_json = json.loads(self.command_line) 27 | if "test_case_id" in temp_json: 28 | self.add_arg("test_case_id", temp_json["test_case_id"]) 29 | else: 30 | raise ValueError("Must supply a VECTR test case ID") 31 | else: 32 | self.add_arg("test_case_id", self.command_line) 33 | else: 34 | raise ValueError("Must supply a VECTR test case ID") 35 | 36 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 37 | response = PTRPCDynamicQueryFunctionMessageResponse() 38 | 39 | class task_data_mock(BaseModel): 40 | BuildParameters: list 41 | Secrets: dict 42 | 43 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 44 | CallbackID=callback.Callback, 45 | PayloadUUID=callback.PayloadUUID, 46 | PayloadTypes=[callback.PayloadType], 47 | )) 48 | if not payload_resp.Success: 49 | response.Error = payload_resp.Error 50 | return response 51 | if len(payload_resp.Payloads) == 0: 52 | response.Error = "No payloads found" 53 | return response 54 | 55 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 56 | 57 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 58 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 59 | 60 | if response_code != 200: 61 | response.Error = "Error fetching test cases" 62 | return response 63 | 64 | task_ids = [] 65 | for task in tasks: 66 | task_ids.append(f"{task['id']} - {task['name']}") 67 | 68 | response.Success = True 69 | response.Choices = task_ids 70 | return response 71 | 72 | 73 | class TestCaseDelete(CommandBase): 74 | cmd = "delete_testcase" 75 | needs_admin = False 76 | help_cmd = "delete_testcase -test_case_id 1" 77 | description = "Delete a test case from Vectr based on its ID" 78 | version = 2 79 | author = "@ajpc500" 80 | argument_class = TestCaseDeleteArguments 81 | supported_ui_features = ["vectr:testcase_delete"] 82 | attackmapping = [] 83 | completion_functions = { 84 | } 85 | 86 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 87 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 88 | 89 | display_params = f"with test case ID {test_case_id}" 90 | 91 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 92 | TaskID=taskData.Task.ID, 93 | Success=False, 94 | Completed=True, 95 | DisplayParams=display_params 96 | ) 97 | try: 98 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 99 | response_code, response_data = VectrAPI.rest_delete_test_case(rest_vectr.connection_params, rest_vectr.target_db, rest_vectr.campaign_id, [test_case_id]) 100 | 101 | return await VectrAPI.process_standard_response( 102 | response_code=response_code, 103 | response_data=response_data['message'], 104 | taskData=taskData, 105 | response=response, 106 | as_json=False 107 | ) 108 | 109 | except Exception as e: 110 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 111 | TaskID=taskData.Task.ID, 112 | Response=f"{e}".encode("UTF8"), 113 | )) 114 | response.TaskStatus = "Error: Vectr Access Error" 115 | response.Success = False 116 | return response 117 | 118 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 119 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 120 | return resp 121 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_get_raw.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseGetRawJsonArguments(TaskArguments): 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="test_case_id", 14 | type=ParameterType.ChooseOne, 15 | dynamic_query_function=self.get_vectr_test_cases, 16 | description="VECTR test case ID", 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True 19 | )] 20 | ) 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) > 0: 25 | if self.command_line[0] == '{': 26 | temp_json = json.loads(self.command_line) 27 | if "test_case_id" in temp_json: 28 | self.add_arg("test_case_id", temp_json["test_case_id"]) 29 | else: 30 | raise ValueError("Must supply a VECTR test case ID") 31 | else: 32 | self.add_arg("test_case_id", self.command_line) 33 | else: 34 | raise ValueError("Must supply a VECTR test case ID") 35 | 36 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 37 | response = PTRPCDynamicQueryFunctionMessageResponse() 38 | 39 | class task_data_mock(BaseModel): 40 | BuildParameters: list 41 | Secrets: dict 42 | 43 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 44 | CallbackID=callback.Callback, 45 | PayloadUUID=callback.PayloadUUID, 46 | PayloadTypes=[callback.PayloadType], 47 | )) 48 | if not payload_resp.Success: 49 | response.Error = payload_resp.Error 50 | return response 51 | if len(payload_resp.Payloads) == 0: 52 | response.Error = "No payloads found" 53 | return response 54 | 55 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 56 | 57 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 58 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 59 | 60 | if response_code != 200: 61 | response.Error = "Error fetching test cases" 62 | return response 63 | 64 | task_ids = [] 65 | for task in tasks: 66 | task_ids.append(f"{task['id']} - {task['name']}") 67 | 68 | response.Success = True 69 | response.Choices = task_ids 70 | return response 71 | 72 | 73 | class TestCaseGetRawJson(CommandBase): 74 | cmd = "get_raw_testcase_json" 75 | needs_admin = False 76 | help_cmd = "get_raw_testcase_json -test_case_id 1" 77 | description = "Get raw JSON for a VECTR test case" 78 | version = 2 79 | author = "@ajpc500" 80 | argument_class = TestCaseGetRawJsonArguments 81 | supported_ui_features = ["vectr:testcase_get_raw"] 82 | attackmapping = [] 83 | completion_functions = { 84 | } 85 | 86 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 87 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 88 | 89 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 90 | TaskID=taskData.Task.ID, 91 | Success=False, 92 | Completed=True, 93 | DisplayParams=f"for {test_case_id}" 94 | ) 95 | try: 96 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 97 | response_code, response_data = VectrAPI.rest_get_test_case( 98 | rest_vectr.connection_params, 99 | rest_vectr.target_db, 100 | test_case_id 101 | ) 102 | if response_code != 200: 103 | raise Exception(response_data) 104 | 105 | return await VectrAPI.process_standard_response( 106 | response_code=response_code, 107 | response_data=response_data, 108 | taskData=taskData, 109 | response=response 110 | ) 111 | 112 | except Exception as e: 113 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 114 | TaskID=taskData.Task.ID, 115 | Response=f"{e}".encode("UTF8"), 116 | )) 117 | response.TaskStatus = "Error: Vectr Access Error" 118 | response.Success = False 119 | return response 120 | 121 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 122 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 123 | return resp 124 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_update_name.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseUpdateNameArguments(TaskArguments): 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="test_case_id", 14 | type=ParameterType.ChooseOne, 15 | dynamic_query_function=self.get_vectr_test_cases, 16 | description="VECTR test case ID", 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True 19 | )] 20 | ), 21 | CommandParameter( 22 | name="name", 23 | type=ParameterType.String, 24 | description="Name for VECTR test case", 25 | parameter_group_info=[ParameterGroupInfo( 26 | required=True 27 | )] 28 | ), 29 | ] 30 | 31 | async def parse_arguments(self): 32 | if len(self.command_line) == 0: 33 | raise ValueError("Must supply a VECTR test case and new name") 34 | raise ValueError("Must supply named arguments or use the modal") 35 | 36 | async def parse_dictionary(self, dictionary_arguments): 37 | if "test_case_id" in dictionary_arguments: 38 | self.add_arg("test_case_id", dictionary_arguments["test_case_id"]) 39 | if "name" in dictionary_arguments: 40 | self.add_arg("name", dictionary_arguments["name"]) 41 | 42 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 43 | response = PTRPCDynamicQueryFunctionMessageResponse() 44 | 45 | class task_data_mock(BaseModel): 46 | BuildParameters: list 47 | Secrets: dict 48 | 49 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 50 | CallbackID=callback.Callback, 51 | PayloadUUID=callback.PayloadUUID, 52 | PayloadTypes=[callback.PayloadType], 53 | )) 54 | if not payload_resp.Success: 55 | response.Error = payload_resp.Error 56 | return response 57 | if len(payload_resp.Payloads) == 0: 58 | response.Error = "No payloads found" 59 | return response 60 | 61 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 62 | 63 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 64 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 65 | 66 | if response_code != 200: 67 | response.Error = "Error fetching test cases" 68 | return response 69 | 70 | task_ids = [] 71 | for task in tasks: 72 | task_ids.append(f"{task['id']} - {task['name']}") 73 | 74 | response.Success = True 75 | response.Choices = task_ids 76 | return response 77 | 78 | 79 | class TestCaseUpdateName(CommandBase): 80 | cmd = "update_testcase_name" 81 | needs_admin = False 82 | help_cmd = "update_testcase_name -test_case_id 1 -name 'New name'" 83 | description = "Update the name of a test case in VECTR" 84 | version = 2 85 | author = "@ajpc500" 86 | supported_ui_features = ["vectr:testcase_name_update"] 87 | argument_class = TestCaseUpdateNameArguments 88 | attackmapping = [] 89 | completion_functions = { 90 | } 91 | 92 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 93 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 94 | name = taskData.args.get_arg("name") 95 | 96 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 97 | TaskID=taskData.Task.ID, 98 | Success=False, 99 | Completed=True, 100 | DisplayParams=f"for {test_case_id} to '{name}'" 101 | ) 102 | try: 103 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 104 | response_code, response_data = VectrAPI.rest_get_test_case( 105 | rest_vectr.connection_params, 106 | rest_vectr.target_db, 107 | test_case_id 108 | ) 109 | if response_code != 200: 110 | raise Exception(response_data) 111 | 112 | if not response_data.get('redTeam', {}).get('variant', None): 113 | raise Exception("No redTeam.variant field found for test case") 114 | 115 | response_data['redTeam']['variant'] = name 116 | 117 | response_code, response_data = VectrAPI.rest_update_test_case( 118 | rest_vectr.connection_params, rest_vectr.target_db, response_data 119 | ) 120 | return await VectrAPI.process_standard_response( 121 | response_code=response_code, 122 | response_data=response_data['message'], 123 | taskData=taskData, 124 | response=response, 125 | as_json=False 126 | ) 127 | 128 | except Exception as e: 129 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 130 | TaskID=taskData.Task.ID, 131 | Response=f"{e}".encode("UTF8"), 132 | )) 133 | response.TaskStatus = "Error: Vectr Access Error" 134 | response.Success = False 135 | return response 136 | 137 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 138 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 139 | return resp 140 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_update_operator_guidance.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseUpdateOperatorGuidanceArguments(TaskArguments): 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="test_case_id", 14 | type=ParameterType.ChooseOne, 15 | dynamic_query_function=self.get_vectr_test_cases, 16 | description="VECTR test case ID", 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True 19 | )] 20 | ), 21 | CommandParameter( 22 | name="content", 23 | type=ParameterType.String, 24 | description="VECTR test case operator guidance content", 25 | parameter_group_info=[ParameterGroupInfo( 26 | required=True 27 | )] 28 | ), 29 | ] 30 | 31 | async def parse_arguments(self): 32 | if len(self.command_line) == 0: 33 | raise ValueError("Must supply a VECTR test case and new name") 34 | raise ValueError("Must supply named arguments or use the modal") 35 | 36 | async def parse_dictionary(self, dictionary_arguments): 37 | if "test_case_id" in dictionary_arguments: 38 | self.add_arg("test_case_id", dictionary_arguments["test_case_id"]) 39 | if "content" in dictionary_arguments: 40 | self.add_arg("content", dictionary_arguments["content"]) 41 | 42 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 43 | response = PTRPCDynamicQueryFunctionMessageResponse() 44 | 45 | class task_data_mock(BaseModel): 46 | BuildParameters: list 47 | Secrets: dict 48 | 49 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 50 | CallbackID=callback.Callback, 51 | PayloadUUID=callback.PayloadUUID, 52 | PayloadTypes=[callback.PayloadType], 53 | )) 54 | if not payload_resp.Success: 55 | response.Error = payload_resp.Error 56 | return response 57 | if len(payload_resp.Payloads) == 0: 58 | response.Error = "No payloads found" 59 | return response 60 | 61 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 62 | 63 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 64 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 65 | 66 | if response_code != 200: 67 | response.Error = "Error fetching test cases" 68 | return response 69 | 70 | task_ids = [] 71 | for task in tasks: 72 | task_ids.append(f"{task['id']} - {task['name']}") 73 | 74 | response.Success = True 75 | response.Choices = task_ids 76 | return response 77 | 78 | 79 | class TestCaseUpdateOperatorGuidance(CommandBase): 80 | cmd = "update_testcase_operator_guidance" 81 | needs_admin = False 82 | help_cmd = "update_testcase_operator_guidance -test_case_id 1 -content 'New content'" 83 | description = "Update the operator guidance of a test case in VECTR" 84 | version = 2 85 | author = "@ajpc500" 86 | supported_ui_features = ["vectr:testcase_opguidance_update"] 87 | argument_class = TestCaseUpdateOperatorGuidanceArguments 88 | attackmapping = [] 89 | completion_functions = { 90 | } 91 | 92 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 93 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 94 | content = taskData.args.get_arg("content") 95 | 96 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 97 | TaskID=taskData.Task.ID, 98 | Success=False, 99 | Completed=True, 100 | DisplayParams=f"for {test_case_id}" 101 | ) 102 | try: 103 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 104 | response_code, response_data = VectrAPI.rest_get_test_case( 105 | rest_vectr.connection_params, 106 | rest_vectr.target_db, 107 | test_case_id 108 | ) 109 | if response_code != 200: 110 | raise Exception(response_data) 111 | 112 | if not response_data.get('redTeam', {}).get('command', None): 113 | raise Exception("No redTeam.command field found for test case") 114 | 115 | response_data['redTeam']['command'] = content 116 | 117 | response_code, response_data = VectrAPI.rest_update_test_case( 118 | rest_vectr.connection_params, rest_vectr.target_db, response_data 119 | ) 120 | return await VectrAPI.process_standard_response( 121 | response_code=response_code, 122 | response_data=response_data['message'], 123 | taskData=taskData, 124 | response=response, 125 | as_json=False 126 | ) 127 | 128 | except Exception as e: 129 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 130 | TaskID=taskData.Task.ID, 131 | Response=f"{e}".encode("UTF8"), 132 | )) 133 | response.TaskStatus = "Error: Vectr Access Error" 134 | response.Success = False 135 | return response 136 | 137 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 138 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 139 | return resp 140 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/VectrRequests/VectrAPIClasses.py: -------------------------------------------------------------------------------- 1 | # import re 2 | from typing import List, Optional, Dict 3 | from pydantic import BaseModel, Field, validator, root_validator 4 | 5 | 6 | class VectrGQLConnParams(BaseModel): 7 | api_key: str 8 | vectr_gql_url: str 9 | 10 | class VectrRESTConnParams(BaseModel): 11 | api_key: str 12 | vectr_rest_url: str 13 | 14 | """ 15 | VECTR Connection Class Object 16 | """ 17 | class vectr_connection(): 18 | def __init__(self, org_name, connection_params, target_db, campaign_name, campaign_id, assessment_id): 19 | self.org_name = org_name 20 | self.connection_params = connection_params 21 | self.target_db = target_db 22 | self.campaign_name = campaign_name 23 | self.campaign_id = campaign_id 24 | self.assessment_id = assessment_id 25 | 26 | 27 | class TestCase(BaseModel): 28 | name: str = Field(alias="Variant") 29 | description: Optional[str] = Field(alias="Objective") 30 | phase: Optional[str] = Field(alias="Phase") 31 | technique: Optional[str] = Field(alias="MitreID") 32 | tags: Optional[List[str]] = Field(alias="Tags") 33 | organization: Optional[str] = Field(alias="Organizations") 34 | status: Optional[str] = Field(alias="Status") 35 | targets: Optional[List[str]] = Field(alias="TargetAssets") 36 | sources: Optional[List[str]] = Field(alias="SourceIps") 37 | defenses: Optional[List[str]] = Field(alias="ExpectedDetectionLayers") 38 | detectionSteps: Optional[List[str]] = Field(alias="DetectionRecommendations") 39 | alertTriggered: Optional[str] = Field(alias="AlertTriggered") 40 | activityLogged: Optional[str] = Field(alias="ActivityLogged") 41 | outcome: Optional[str] = Field(alias="Outcome") 42 | outcomePath: Optional[str] = Field(alias="Outcome Path") 43 | outcomeNotes: Optional[str] = Field(alias="OutcomeNotes") 44 | alertSeverity: Optional[str] = Field(alias="Alert Severity") 45 | detectionTime: Optional[float] = Field(alias="Detection Time Epoch") 46 | detectingDefenseTools: Optional[List[Dict[str, str]]] = Field(alias="DetectingTools") 47 | references: Optional[List[str]] = Field(alias="References") 48 | redTools: Optional[List[Dict[str, str]]] = Field(alias="Attacker Tools") 49 | operatorGuidance: Optional[str] = Field(alias="Command") 50 | attackStart: Optional[float] = Field(alias="StartTimeEpoch") 51 | attackStop: Optional[float] = Field(alias="StopTimeEpoch") 52 | 53 | @root_validator(pre=True) 54 | def check_technique(cls, values): 55 | if 'MitreID' in values and values['MitreID']: 56 | # everything is fine, MitreID exists, continue 57 | return values 58 | 59 | if 'Method' in values and values['Method']: 60 | values['MitreID'] = values['Method'] 61 | return values 62 | 63 | raise ValueError("Non-empty Method (Attack Technique) or MitreID required for Test Case creation") 64 | 65 | # @TODO - combine for reuse, getting weird behavior with multiple annotations 66 | @validator('sources', pre=True, allow_reuse=True) 67 | def validate_sources(cls, v: str) -> Optional[List[str]]: 68 | sources = v.split(',') 69 | if not sources: 70 | return None 71 | return list(filter(None, sources)) 72 | 73 | @validator('references', pre=True, allow_reuse=True) 74 | def validate_references(cls, v: str) -> Optional[List[str]]: 75 | refs = v.split(',') 76 | if not refs: 77 | return None 78 | return list(filter(None, refs)) 79 | 80 | @validator('tags', pre=True, allow_reuse=True) 81 | def validate_tags(cls, v: str) -> Optional[List[str]]: 82 | tags = v.split(',') 83 | if not tags: 84 | return None 85 | return list(filter(None, tags)) 86 | 87 | @validator('organization', pre=True, allow_reuse=True) 88 | def validate_organization(cls, v: str) -> Optional[str]: 89 | orgs = v.split(',') 90 | if orgs: 91 | return orgs[0] 92 | else: 93 | return None 94 | 95 | @validator('defenses', pre=True, allow_reuse=True) 96 | def validate_defenses(cls, v: str) -> Optional[List[str]]: 97 | defenses = v.split(',') 98 | if not defenses: 99 | return None 100 | return list(filter(None, defenses)) 101 | 102 | @validator('detectionSteps', pre=True, allow_reuse=True) 103 | def validate_detection_steps(cls, v: str) -> Optional[List[str]]: 104 | if not v: 105 | return None 106 | return [v] 107 | 108 | @validator('detectingDefenseTools', pre=True, allow_reuse=True) 109 | def validate_detecting_tools(cls, v: str) -> List[Dict[str, str]]: 110 | tools = [] 111 | tool_names = v.split(',') 112 | tool_names = list(filter(None, tool_names)) 113 | 114 | for tool_name in tool_names: 115 | tools.append({"name": tool_name}) 116 | 117 | return tools 118 | 119 | @validator('redTools', pre=True, allow_reuse=True) 120 | def validate_attack_tools(cls, v: str) -> List[Dict[str, str]]: 121 | tools = [] 122 | tool_names = v.split(',') 123 | tool_names = list(filter(None, tool_names)) 124 | 125 | for tool_name in tool_names: 126 | tools.append({"name": tool_name}) 127 | 128 | return tools 129 | 130 | @validator('targets', pre=True, allow_reuse=True) 131 | def validate_targets(cls, v: str) -> Optional[List[str]]: 132 | targets = v.split(',') 133 | if not targets: 134 | return None 135 | return list(filter(None, targets)) 136 | 137 | @validator('status', pre=True, allow_reuse=True) 138 | def validate_upper_enum1(cls, v: str) -> str: 139 | return v 140 | 141 | @validator('outcome', pre=True, allow_reuse=True) 142 | def validate_upper_enum2(cls, v: str) -> str: 143 | return v 144 | 145 | @validator('outcomeNotes', pre=True, allow_reuse=True) 146 | def validate_outcomeNotes(cls, v: str) -> Optional[str]: 147 | return v 148 | 149 | @validator('alertSeverity', pre=True, allow_reuse=True) 150 | def validate_upper_enum3(cls, v: str) -> str: 151 | return v 152 | 153 | @validator('alertTriggered', pre=True, allow_reuse=True) 154 | def validate_upper_enum4(cls, v: str) -> str: 155 | return v 156 | 157 | @validator('activityLogged', pre=True, allow_reuse=True) 158 | def validate_upper_enum5(cls, v: str) -> str: 159 | return v 160 | 161 | @validator('attackStart', pre=True, allow_reuse=True) 162 | def validate_attack_start(cls, v: str) -> Optional[float]: 163 | if not v: 164 | return None 165 | return float(v) 166 | 167 | @validator('attackStop', pre=True, allow_reuse=True) 168 | def validate_attack_stop(cls, v: str) -> Optional[float]: 169 | if not v: 170 | return None 171 | return float(v) 172 | 173 | @validator('detectionTime', pre=True, allow_reuse=True) 174 | def validate_detection_time(cls, v: str) -> Optional[float]: 175 | if not v: 176 | return None 177 | return float(v) 178 | 179 | 180 | class TestCaseGQLInput(BaseModel): 181 | testCaseData: TestCase 182 | 183 | 184 | class Campaign(BaseModel): 185 | name: str 186 | test_cases: Optional[List[TestCase]] 187 | 188 | 189 | class Assessment(BaseModel): 190 | name: str 191 | campaigns: Optional[Dict[str, Campaign]] 192 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_upload_artifact.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseArtifactUploadArguments(TaskArguments): 9 | 10 | def __init__(self, command_line, **kwargs): 11 | super().__init__(command_line, **kwargs) 12 | self.args = [ 13 | CommandParameter( 14 | name="file", 15 | type=ParameterType.File, 16 | parameter_group_info=[ParameterGroupInfo( 17 | required=True, 18 | ui_position=1, 19 | group_name="Manually Upload New File" 20 | )] 21 | ), 22 | CommandParameter( 23 | name="filename", 24 | type=ParameterType.ChooseOne, 25 | dynamic_query_function=self.get_files, 26 | parameter_group_info=[ParameterGroupInfo( 27 | required=True, 28 | ui_position=1, 29 | group_name="Select Mythic File to Upload" 30 | )] 31 | ), 32 | CommandParameter( 33 | name="description", 34 | type=ParameterType.String, 35 | default_value="", 36 | parameter_group_info=[ 37 | ParameterGroupInfo( 38 | required=False, 39 | ui_position=2, 40 | group_name="Manually Upload New File" 41 | ), 42 | ParameterGroupInfo( 43 | required=False, 44 | ui_position=2, 45 | group_name="Select Mythic File to Upload" 46 | ) 47 | ] 48 | ), 49 | CommandParameter( 50 | name="test_case_id", 51 | type=ParameterType.ChooseOne, 52 | dynamic_query_function=self.get_vectr_test_cases, 53 | parameter_group_info=[ 54 | ParameterGroupInfo( 55 | required=True, 56 | ui_position=3, 57 | group_name="Manually Upload New File" 58 | ), 59 | ParameterGroupInfo( 60 | required=True, 61 | ui_position=3, 62 | group_name="Select Mythic File to Upload" 63 | ) 64 | ] 65 | ) 66 | ] 67 | 68 | async def parse_arguments(self): 69 | self.load_args_from_json_string(self.command_line) 70 | 71 | async def parse_dictionary(self, dictionary_arguments): 72 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 73 | 74 | 75 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 76 | response = PTRPCDynamicQueryFunctionMessageResponse() 77 | 78 | class task_data_mock(BaseModel): 79 | BuildParameters: list 80 | Secrets: dict 81 | 82 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 83 | CallbackID=callback.Callback, 84 | PayloadUUID=callback.PayloadUUID, 85 | PayloadTypes=[callback.PayloadType], 86 | )) 87 | if not payload_resp.Success: 88 | response.Error = payload_resp.Error 89 | return response 90 | if len(payload_resp.Payloads) == 0: 91 | response.Error = "No payloads found" 92 | return response 93 | 94 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 95 | 96 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 97 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 98 | 99 | if response_code != 200: 100 | response.Error = "Error fetching test cases" 101 | return response 102 | 103 | task_ids = [] 104 | for task in tasks: 105 | task_ids.append(f"{task['id']} - {task['name']}") 106 | 107 | response.Success = True 108 | response.Choices = task_ids 109 | return response 110 | 111 | 112 | async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 113 | response = PTRPCDynamicQueryFunctionMessageResponse() 114 | file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 115 | CallbackID=callback.Callback, 116 | LimitByCallback=False, 117 | IsDownloadFromAgent=True, 118 | IsScreenshot=False, 119 | IsPayload=False, 120 | Filename="", 121 | )) 122 | if file_resp.Success: 123 | file_names = [] 124 | for f in file_resp.Files: 125 | if f.Filename not in file_names: 126 | file_names.append(f.Filename) 127 | response.Success = True 128 | response.Choices = file_names 129 | return response 130 | else: 131 | await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( 132 | CallbackId=callback.Callback, 133 | Message=f"Failed to get files: {file_resp.Error}", 134 | MessageLevel="warning" 135 | )) 136 | response.Error = f"Failed to get files: {file_resp.Error}" 137 | return response 138 | 139 | 140 | class TestCaseArtifactUpload(CommandBase): 141 | cmd = "upload_testcase_artifact" 142 | needs_admin = False 143 | help_cmd = "upload_testcase_artifact" 144 | description = "Upload a file as an execution artifact for a VECTR test case" 145 | version = 2 146 | author = "@ajpc500" 147 | argument_class = TestCaseArtifactUploadArguments 148 | supported_ui_features = ["vectr:testcase_artifact_upload"] 149 | attackmapping = [] 150 | completion_functions = { 151 | } 152 | 153 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 154 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 155 | 156 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 157 | TaskID=taskData.Task.ID, 158 | Success=False, 159 | Completed=True, 160 | DisplayParams=f"for test case ID {test_case_id}" 161 | ) 162 | try: 163 | fileMetadata = None 164 | if taskData.args.get_parameter_group_name() == "Manually Upload New File": 165 | searchedFile = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 166 | AgentFileID=taskData.args.get_arg("file") 167 | )) 168 | if not searchedFile.Success: 169 | raise Exception(searchedFile.Error) 170 | if len(searchedFile.Files) != 1: 171 | raise Exception("Failed to get file back from Mythic") 172 | fileMetadata = searchedFile.Files[0] 173 | 174 | else: 175 | searchedFile = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 176 | TaskID=taskData.Task.ID, 177 | Filename=taskData.args.get_arg("filename"), 178 | LimitByCallback=False, 179 | MaxResults=1 180 | )) 181 | if not searchedFile.Success: 182 | raise Exception(searchedFile.Error) 183 | if len(searchedFile.Files) != 1: 184 | raise Exception("Failed to get file back from Mythic") 185 | fileMetadata = searchedFile.Files[0] 186 | 187 | filename = fileMetadata.Filename 188 | 189 | fileContentsResp = await SendMythicRPCFileGetContent(MythicRPCFileGetContentMessage( 190 | AgentFileId=fileMetadata.AgentFileId 191 | )) 192 | if not fileContentsResp.Success: 193 | raise Exception(fileContentsResp.Error) 194 | 195 | encrypted_base64, key_base64, nonce_base64, data_hash = VectrAPI.encrypt_execution_artifact(fileContentsResp.Content) 196 | 197 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 198 | 199 | response_code, response_data = VectrAPI.rest_upload_execution_artifact( 200 | rest_vectr.connection_params, 201 | encrypted_base64, 202 | key_base64, 203 | nonce_base64, 204 | len(fileContentsResp.Content), 205 | data_hash, 206 | filename, 207 | taskData.args.get_arg("description") 208 | ) 209 | if response_code != 200: 210 | raise Exception(response_data) 211 | 212 | execution_artifact_id = response_data['data']['savedData']['id'] 213 | 214 | response_code, response_data = VectrAPI.rest_add_execution_artifact_to_test_case( 215 | rest_vectr.connection_params, rest_vectr.target_db, test_case_id, execution_artifact_id 216 | ) 217 | 218 | return await VectrAPI.process_standard_response( 219 | response_code=response_code, 220 | response_data=response_data['message'], 221 | taskData=taskData, 222 | response=response, 223 | as_json=False 224 | ) 225 | 226 | except Exception as e: 227 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 228 | TaskID=taskData.Task.ID, 229 | Response=f"{e}".encode("UTF8"), 230 | )) 231 | response.TaskStatus = "Error: Vectr Access Error" 232 | response.Success = False 233 | return response 234 | 235 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 236 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 237 | return resp 238 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_update_mitre_id.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | class TestCaseUpdateNameArguments(TaskArguments): 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="test_case_id", 14 | type=ParameterType.ChooseOne, 15 | dynamic_query_function=self.get_vectr_test_cases, 16 | description="VECTR test case ID", 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True, 19 | ui_position=0 20 | )] 21 | ), 22 | CommandParameter( 23 | name="technique_id", 24 | type=ParameterType.ChooseOne, 25 | dynamic_query_function=self.get_vectr_mitre_techniques, 26 | description="MITRE ATT&CK Enterprise technique ID", 27 | parameter_group_info=[ParameterGroupInfo( 28 | required=True, 29 | ui_position=1 30 | )] 31 | ), 32 | CommandParameter( 33 | name="tactic_id", 34 | type=ParameterType.ChooseOne, 35 | dynamic_query_function=self.get_vectr_mitre_tactics, 36 | description="MITRE ATT&CK Enterprise tactic ID", 37 | parameter_group_info=[ParameterGroupInfo( 38 | required=True, 39 | ui_position=2 40 | )] 41 | ) 42 | ] 43 | 44 | async def parse_arguments(self): 45 | if len(self.command_line) == 0: 46 | raise ValueError("Must supply a VECTR test case and new name") 47 | raise ValueError("Must supply named arguments or use the modal") 48 | 49 | async def parse_dictionary(self, dictionary_arguments): 50 | if "test_case_id" in dictionary_arguments: 51 | self.add_arg("test_case_id", dictionary_arguments["test_case_id"]) 52 | if "technique_id" in dictionary_arguments: 53 | self.add_arg("technique_id", dictionary_arguments["technique_id"]) 54 | if "tactic_id" in dictionary_arguments: 55 | self.add_arg("tactic_id", dictionary_arguments["tactic_id"]) 56 | 57 | async def get_vectr_test_cases(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 58 | response = PTRPCDynamicQueryFunctionMessageResponse() 59 | 60 | class task_data_mock(BaseModel): 61 | BuildParameters: list 62 | Secrets: dict 63 | 64 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 65 | CallbackID=callback.Callback, 66 | PayloadUUID=callback.PayloadUUID, 67 | PayloadTypes=[callback.PayloadType], 68 | )) 69 | if not payload_resp.Success: 70 | response.Error = payload_resp.Error 71 | return response 72 | if len(payload_resp.Payloads) == 0: 73 | response.Error = "No payloads found" 74 | return response 75 | 76 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 77 | 78 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 79 | response_code, tasks = VectrAPI.get_testcases_for_campaign_by_id(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id) 80 | 81 | if response_code != 200: 82 | response.Error = "Error fetching test cases" 83 | return response 84 | 85 | task_ids = [] 86 | for task in tasks: 87 | task_ids.append(f"{task['id']} - {task['name']}") 88 | 89 | response.Success = True 90 | response.Choices = task_ids 91 | return response 92 | 93 | 94 | async def get_vectr_mitre_techniques(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 95 | response = PTRPCDynamicQueryFunctionMessageResponse() 96 | 97 | class task_data_mock(BaseModel): 98 | BuildParameters: list 99 | Secrets: dict 100 | 101 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 102 | CallbackID=callback.Callback, 103 | PayloadUUID=callback.PayloadUUID, 104 | PayloadTypes=[callback.PayloadType], 105 | )) 106 | if not payload_resp.Success: 107 | response.Error = payload_resp.Error 108 | return response 109 | if len(payload_resp.Payloads) == 0: 110 | response.Error = "No payloads found" 111 | return response 112 | 113 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 114 | 115 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 116 | response_code, techniques = VectrAPI.rest_get_mitre_techniques(rest_vectr.connection_params) 117 | 118 | if response_code != 200: 119 | response.Error = "Error fetching test cases" 120 | return response 121 | 122 | mitre_ids = [] 123 | techniques.sort(key=lambda x: x['mitreId']) 124 | 125 | for technique in techniques: 126 | mitre_ids.append(f"{technique['mitreId']} - {technique['name']}") 127 | 128 | response.Success = True 129 | response.Choices = mitre_ids 130 | return response 131 | 132 | 133 | async def get_vectr_mitre_tactics(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 134 | response = PTRPCDynamicQueryFunctionMessageResponse() 135 | 136 | class task_data_mock(BaseModel): 137 | BuildParameters: list 138 | Secrets: dict 139 | 140 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 141 | CallbackID=callback.Callback, 142 | PayloadUUID=callback.PayloadUUID, 143 | PayloadTypes=[callback.PayloadType], 144 | )) 145 | if not payload_resp.Success: 146 | response.Error = payload_resp.Error 147 | return response 148 | if len(payload_resp.Payloads) == 0: 149 | response.Error = "No payloads found" 150 | return response 151 | 152 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 153 | 154 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 155 | response_code, tactics = VectrAPI.rest_get_mitre_tactics(rest_vectr.connection_params, rest_vectr.target_db, rest_vectr.assessment_id) 156 | 157 | if response_code != 200: 158 | response.Error = "Error fetching test cases" 159 | return response 160 | 161 | mitre_ids = [] 162 | tactics.sort(key=lambda x: x['mitreId']) 163 | 164 | for tactic in tactics: 165 | mitre_ids.append(f"{tactic['mitreId']} - {tactic['name']}") 166 | 167 | response.Success = True 168 | response.Choices = mitre_ids 169 | return response 170 | 171 | 172 | class TestCaseUpdateName(CommandBase): 173 | cmd = "update_testcase_mitre_id" 174 | needs_admin = False 175 | help_cmd = "update_testcase_mitre_id -test_case_id 1 -technique_id T1059 -tactic_id TA0001" 176 | description = "Update the MITRE tactic and technique of a test case in VECTR" 177 | version = 2 178 | author = "@ajpc500" 179 | supported_ui_features = ["vectr:testcase_mitre_update"] 180 | argument_class = TestCaseUpdateNameArguments 181 | attackmapping = [] 182 | completion_functions = { 183 | } 184 | 185 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 186 | test_case_id = taskData.args.get_arg("test_case_id").split(" - ")[0] 187 | mitre_technique_id = taskData.args.get_arg("technique_id").split(" - ")[0].upper() 188 | mitre_technique_name = taskData.args.get_arg("technique_id").split(" - ")[1] 189 | mitre_tactic_id = taskData.args.get_arg("tactic_id").split(" - ")[0].upper() 190 | 191 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 192 | TaskID=taskData.Task.ID, 193 | Success=False, 194 | Completed=True, 195 | DisplayParams=f"for {test_case_id} to '{mitre_technique_id}'" 196 | ) 197 | try: 198 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 199 | response_code, response_data = VectrAPI.rest_get_test_case( 200 | rest_vectr.connection_params, 201 | rest_vectr.target_db, 202 | test_case_id 203 | ) 204 | if response_code != 200: 205 | raise Exception(response_data) 206 | 207 | # Get MITRE tactics back so we can convert ID to identifier 208 | response_code, tactics = VectrAPI.rest_get_mitre_tactics(rest_vectr.connection_params, rest_vectr.target_db, rest_vectr.assessment_id) 209 | 210 | if response_code != 200: 211 | raise Exception(response_data) 212 | 213 | tactic_id = "" 214 | for tactic in tactics: 215 | if tactic['mitreId'] == mitre_tactic_id: 216 | tactic_id = tactic['id'] 217 | 218 | if not response_data.get('redTeam', {}).get('mitreId', None): 219 | raise Exception("No redteam.mitreId field found for test case") 220 | 221 | response_data['redTeam']['mitreId'] = mitre_technique_id.upper() 222 | response_data['redTeam']['method'] = mitre_technique_name 223 | 224 | if tactic_id: 225 | response_data['phaseId'] = tactic_id 226 | 227 | response_code, response_data = VectrAPI.rest_update_test_case( 228 | rest_vectr.connection_params, rest_vectr.target_db, response_data 229 | ) 230 | return await VectrAPI.process_standard_response( 231 | response_code=response_code, 232 | response_data=response_data['message'], 233 | taskData=taskData, 234 | response=response, 235 | as_json=False 236 | ) 237 | 238 | except Exception as e: 239 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 240 | TaskID=taskData.Task.ID, 241 | Response=f"{e}".encode("UTF8"), 242 | )) 243 | response.TaskStatus = "Error: Vectr Access Error" 244 | response.Success = False 245 | return response 246 | 247 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 248 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 249 | return resp 250 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/browser_scripts/testcase_list.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | function getOutcomeColour(text){ 3 | switch (text) { 4 | case "High": 5 | return {"backgroundColor": "rgb(251, 139, 138)"} 6 | case "Med": 7 | return {"backgroundColor": "rgb(242, 183, 145)"} 8 | case "Low": 9 | return {"backgroundColor": "rgb(177, 213, 154)"} 10 | case "Info": 11 | return {"backgroundColor": "rgb(153, 153, 153)"} 12 | default: 13 | return {} 14 | } 15 | } 16 | 17 | if(task.status.includes("error")){ 18 | const combined = responses.reduce( (prev, cur) => { 19 | return prev + cur; 20 | }, ""); 21 | return {'plaintext': combined}; 22 | }else if(task.completed){ 23 | if(responses.length > 0){ 24 | try{ 25 | let data = JSON.parse(responses[0]); 26 | let output_table = []; 27 | for(let i = 0; i < data.length; i++){ 28 | let outcomeText = ""; 29 | let cellStyle = {}; 30 | if(data[i]["outcome"]["path"] == "TBD"){ 31 | outcomeText = "TBD"; 32 | } else if (data[i]["outcome"]['path'].startsWith("Alerted")) { 33 | outcomeText = `Alerted (${data[i]["outcome"]['abbreviation']})`; 34 | cellStyle = getOutcomeColour(data[i]["outcome"]['abbreviation']); 35 | } else if (data[i]["outcome"]['path'].startsWith("Blocked")) { 36 | if (data[i]["outcome"]['abbreviation'] == "Blocked") { 37 | outcomeText = `Blocked`; 38 | } else { 39 | outcomeText = `Blocked (${data[i]["outcome"]['abbreviation']})`; 40 | } 41 | cellStyle = getOutcomeColour("High"); 42 | } else if (data[i]["outcome"]['path'].startsWith("Logged")) { 43 | outcomeText = `Logged (${data[i]["outcome"]['abbreviation']})`; 44 | cellStyle = getOutcomeColour("Info"); 45 | } else { 46 | outcomeText = "" 47 | cellStyle = {} 48 | } 49 | let execution_time = "" 50 | if (data[i]["attackStart"] != null) { 51 | execution_time = new Date(data[i]["attackStart"]["createTime"]).toString() 52 | } 53 | 54 | let execution_artifact_count = "0" 55 | if (data[i]["executionArtifactIdInfo"] != null) { 56 | execution_artifact_count = data[i]["executionArtifactIdInfo"].length.toString() 57 | } 58 | 59 | output_table.push({ 60 | "id": {"plaintext": data[i]["id"], "copyIcon": true }, 61 | "timestamp": {"plaintext": execution_time}, 62 | "name": {"plaintext": data[i]["name"]}, 63 | "method": {"plaintext": data[i]["method"]}, 64 | "mitreId": {"plaintext": data[i]["mitreId"]}, 65 | "EA": {"plaintext": execution_artifact_count, "startIcon": "upload", "startIconHoverText":"Execution Artifacts"}, 66 | "tags": { 67 | "button": { 68 | "name": data[i]["tags"].length.toString(), 69 | "type": "string", 70 | "value": data[i]["tags"].map(tag => tag.name).join("\n"), 71 | "title": "Tags", 72 | "startIcon": "list", 73 | "hoverText": "View tags" 74 | } 75 | }, 76 | "notes": { 77 | "button": { 78 | "name": "", 79 | "type": "string", 80 | "value": data[i]["operatorGuidance"], 81 | "disabled": data[i]["operatorGuidance"] == null || data[i]["operatorGuidance"] == "", 82 | "title": "Notes", 83 | "startIcon": "list", 84 | "hoverText": "View operator guidance" 85 | } 86 | }, 87 | "desc": { 88 | "button": { 89 | "name": "", 90 | "type": "string", 91 | "value": data[i]["description"], 92 | "disabled": data[i]["description"] == null || data[i]["description"] == "", 93 | "title": "Description", 94 | "startIcon": "list", 95 | "hoverText": "View full description" 96 | } 97 | }, 98 | "outcomeNotes": { 99 | "button": { 100 | "name": "", 101 | "type": "string", 102 | "value": data[i]["outcomeNotes"], 103 | "disabled": data[i]["outcomeNotes"] == null || data[i]["outcomeNotes"] == "", 104 | "title": "Outcome Notes", 105 | "startIcon": "list", 106 | "hoverText": "View outcome notes" 107 | } 108 | }, 109 | "status": {"plaintext": data[i]["status"]}, 110 | "outcome": {"plaintext": outcomeText, "cellStyle": cellStyle}, 111 | "actions": { 112 | "button": { 113 | "name": "Actions", 114 | "type": "menu", 115 | "value": [ 116 | { 117 | "name": "Update Notes", 118 | "type": "task", 119 | "ui_feature": "vectr:testcase_opguidance_update", 120 | "parameters": { 121 | "test_case_id": data[i]["id"] + " - " + data[i]["name"], 122 | "content": data[i]["operatorGuidance"] 123 | }, 124 | "openDialog": true 125 | }, 126 | { 127 | "name": "Update Test Case Name", 128 | "type": "task", 129 | "ui_feature": "vectr:testcase_name_update", 130 | "parameters": { 131 | "test_case_id": data[i]["id"] + " - " + data[i]["name"], 132 | "name": data[i]["name"] 133 | }, 134 | "openDialog": true 135 | }, 136 | { 137 | "name": "Update Test Case MITRE ATT&CK", 138 | "type": "task", 139 | "ui_feature": "vectr:testcase_mitre_update", 140 | "parameters": { 141 | "test_case_id": data[i]["id"] + " - " + data[i]["name"] 142 | }, 143 | "openDialog": true 144 | }, 145 | { 146 | "name": "Upload Execution Artifact", 147 | "type": "task", 148 | "ui_feature": "vectr:testcase_artifact_upload", 149 | "parameters": { 150 | "test_case_id": data[i]["id"] + " - " + data[i]["name"] 151 | }, 152 | "openDialog": true 153 | }, 154 | { 155 | "name": "Delete Test Case", 156 | "type": "task", 157 | "ui_feature": "vectr:testcase_delete", 158 | "parameters": data[i]["id"], 159 | "getConfirmation": true 160 | }, 161 | { 162 | "name": "Get Test Case JSON", 163 | "type": "task", 164 | "ui_feature": "vectr:testcase_get_raw", 165 | "parameters": data[i]["id"] 166 | }, 167 | ] 168 | } 169 | }, 170 | }); 171 | } 172 | return { 173 | "table": [ 174 | { 175 | "headers": [ 176 | {"plaintext": "id", "type": "string", "width": 100}, 177 | {"plaintext": "timestamp", "type": "string", "width": 285}, 178 | {"plaintext": "name", "type": "string", "fillWidth": true}, 179 | {"plaintext": "method", "type": "string", "fillWidth": true}, 180 | {"plaintext": "mitreId", "type": "string", "width": 100}, 181 | {"plaintext": "EA", "type": "string", "width": 50, "disableSort": true}, 182 | {"plaintext": "tags", "type": "button", "width": 70, "disableSort": true}, 183 | {"plaintext": "notes", "type": "button", "width": 70, "disableSort": true}, 184 | {"plaintext": "desc", "type": "button", "cellStyle": {}, "width": 70, "disableSort": true}, 185 | {"plaintext": "outcomeNotes", "type": "button", "cellStyle": {}, "width": 150, "disableSort": true}, 186 | {"plaintext": "status", "type": "string", "width": 125}, 187 | {"plaintext": "outcome", "type": "string", "width": 200}, 188 | {"plaintext": "actions", "type": "button", "width": 90, "disableSort": true}, 189 | ], 190 | "rows": output_table, 191 | "title": "Test Cases" 192 | } 193 | ] 194 | } 195 | }catch(error){ 196 | console.log(error); 197 | const combined = responses.reduce( (prev, cur) => { 198 | return prev + cur; 199 | }, ""); 200 | return {'plaintext': combined}; 201 | } 202 | }else{ 203 | return {"plaintext": "No output from command"}; 204 | } 205 | }else{ 206 | return {"plaintext": "No data to display..."}; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /agent_icons/vectr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/vectr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/agent_functions/testcase_create.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from vectr.VectrRequests import VectrAPI 4 | from gql import gql 5 | 6 | from pydantic import BaseModel 7 | 8 | VECTR_TAG_NAME = "SentToVECTR" 9 | 10 | class TestCaseCreateArguments(TaskArguments): 11 | def __init__(self, command_line, **kwargs): 12 | super().__init__(command_line, **kwargs) 13 | self.args = [ 14 | CommandParameter( 15 | name="task_id", 16 | type=ParameterType.Number, 17 | description="Mythic task display ID", 18 | parameter_group_info=[ParameterGroupInfo( 19 | required=True, 20 | ui_position=0, 21 | )] 22 | ), 23 | CommandParameter( 24 | name="name", 25 | type=ParameterType.String, 26 | description="Name for VECTR test case (default is command executed)", 27 | parameter_group_info=[ParameterGroupInfo( 28 | required=False, 29 | ui_position=1 30 | )] 31 | ), 32 | CommandParameter( 33 | name="tactic_id", 34 | type=ParameterType.ChooseOne, 35 | dynamic_query_function=self.get_vectr_mitre_tactics, 36 | description="MITRE ATT&CK Enterprise tactic ID", 37 | parameter_group_info=[ParameterGroupInfo( 38 | required=False, 39 | ui_position=2 40 | )] 41 | ), 42 | CommandParameter( 43 | name="technique_id", 44 | type=ParameterType.ChooseOne, 45 | dynamic_query_function=self.get_vectr_mitre_techniques, 46 | description="MITRE ATT&CK Enterprise technique ID", 47 | parameter_group_info=[ParameterGroupInfo( 48 | required=False, 49 | ui_position=3 50 | )] 51 | ), 52 | CommandParameter( 53 | name="force_create", 54 | type=ParameterType.Boolean, 55 | description="Force the creation of a VECTR test case for Mythic tasks that have already been imported", 56 | default_value=False, 57 | parameter_group_info=[ParameterGroupInfo( 58 | required=False, 59 | ui_position=4 60 | )] 61 | ) 62 | ] 63 | 64 | async def parse_arguments(self): 65 | if len(self.command_line) == 0: 66 | raise ValueError("Must supply a Mythic task ID") 67 | raise ValueError("Must supply named arguments or use the modal") 68 | 69 | async def parse_dictionary(self, dictionary_arguments): 70 | if "task_id" in dictionary_arguments: 71 | self.add_arg("task_id", dictionary_arguments["task_id"]) 72 | if "name" in dictionary_arguments: 73 | self.add_arg("name", dictionary_arguments["name"]) 74 | if "technique_id" in dictionary_arguments: 75 | self.add_arg("technique_id", dictionary_arguments["technique_id"]) 76 | if "tactic_id" in dictionary_arguments: 77 | self.add_arg("tactic_id", dictionary_arguments["tactic_id"]) 78 | if "force_create" in dictionary_arguments: 79 | self.add_arg("force_create", dictionary_arguments["force_create"]) 80 | 81 | async def get_vectr_mitre_techniques(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 82 | response = PTRPCDynamicQueryFunctionMessageResponse() 83 | 84 | class task_data_mock(BaseModel): 85 | BuildParameters: list 86 | Secrets: dict 87 | 88 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 89 | CallbackID=callback.Callback, 90 | PayloadUUID=callback.PayloadUUID, 91 | PayloadTypes=[callback.PayloadType], 92 | )) 93 | if not payload_resp.Success: 94 | response.Error = payload_resp.Error 95 | return response 96 | if len(payload_resp.Payloads) == 0: 97 | response.Error = "No payloads found" 98 | return response 99 | 100 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 101 | 102 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 103 | response_code, techniques = VectrAPI.rest_get_mitre_techniques(rest_vectr.connection_params) 104 | 105 | if response_code != 200: 106 | response.Error = "Error fetching test cases" 107 | return response 108 | 109 | mitre_ids = [""] 110 | techniques.sort(key=lambda x: x['mitreId']) 111 | 112 | for technique in techniques: 113 | mitre_ids.append(f"{technique['mitreId']} - {technique['name']}") 114 | 115 | response.Success = True 116 | response.Choices = mitre_ids 117 | return response 118 | 119 | 120 | async def get_vectr_mitre_tactics(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 121 | response = PTRPCDynamicQueryFunctionMessageResponse() 122 | 123 | class task_data_mock(BaseModel): 124 | BuildParameters: list 125 | Secrets: dict 126 | 127 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 128 | CallbackID=callback.Callback, 129 | PayloadUUID=callback.PayloadUUID, 130 | PayloadTypes=[callback.PayloadType], 131 | )) 132 | if not payload_resp.Success: 133 | response.Error = payload_resp.Error 134 | return response 135 | if len(payload_resp.Payloads) == 0: 136 | response.Error = "No payloads found" 137 | return response 138 | 139 | task_data = task_data_mock(BuildParameters=payload_resp.Payloads[0].BuildParameters, Secrets=callback.Secrets) 140 | 141 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(task_data) 142 | response_code, tactics = VectrAPI.rest_get_mitre_tactics(rest_vectr.connection_params, rest_vectr.target_db, rest_vectr.assessment_id) 143 | 144 | if response_code != 200: 145 | response.Error = "Error fetching test cases" 146 | return response 147 | 148 | mitre_ids = [""] 149 | tactics.sort(key=lambda x: x['mitreId']) 150 | 151 | for tactic in tactics: 152 | mitre_ids.append(f"{tactic['mitreId']} - {tactic['name']}") 153 | 154 | response.Success = True 155 | response.Choices = mitre_ids 156 | return response 157 | 158 | 159 | 160 | class TestCaseCreate(CommandBase): 161 | cmd = "create_testcase" 162 | needs_admin = False 163 | help_cmd = "create_testcase -task_id 1 -name 'custom name'" 164 | description = "Add a new test case to Vectr based on Mythic task ID" 165 | version = 2 166 | author = "@ajpc500" 167 | argument_class = TestCaseCreateArguments 168 | supported_ui_features = ["vectr:testcase_create"] 169 | browser_script = BrowserScript(script_name="testcase_create", author="@ajpc500") 170 | attackmapping = [] 171 | completion_functions = { 172 | } 173 | 174 | async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 175 | task_id = taskData.args.get_arg("task_id") 176 | testcase_name = taskData.args.get_arg("name") 177 | force_create = taskData.args.get_arg("force_create") 178 | task_has_existing_tag = False 179 | 180 | if taskData.args.get_arg("technique_id"): 181 | mitre_technique_id = taskData.args.get_arg("technique_id").split(" - ")[0].upper() 182 | mitre_technique_name = taskData.args.get_arg("technique_id").split(" - ")[1] 183 | else: 184 | mitre_technique_id = None 185 | 186 | if taskData.args.get_arg("tactic_id"): 187 | mitre_tactic_id = taskData.args.get_arg("tactic_id").split(" - ")[0].upper() 188 | mitre_tactic_name = taskData.args.get_arg("tactic_id").split(" - ")[1] 189 | else: 190 | mitre_tactic_id = None 191 | mitre_tactic_name = None 192 | 193 | display_params = f"with task ID {task_id}" 194 | if testcase_name: 195 | display_params += f" and name '{testcase_name}'" 196 | 197 | if mitre_technique_id or mitre_tactic_name: 198 | mitre_display_values = [mitre_technique_id, mitre_tactic_name] 199 | display_params += f" (ATT&CK: {', '.join([x for x in mitre_display_values if x is not None])})" 200 | 201 | if force_create: 202 | display_params += " (force creating)" 203 | 204 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 205 | TaskID=taskData.Task.ID, 206 | Success=False, 207 | Completed=True, 208 | DisplayParams=display_params 209 | ) 210 | try: 211 | # We'll create task_data to hold the task and any responses. 212 | task_data = {} 213 | 214 | # Fetch the task from Mythic 215 | task_search_response = await SendMythicRPCTaskSearch(MythicRPCTaskSearchMessage(TaskID=taskData.Task.ID, SearchTaskDisplayID=int(task_id))) 216 | 217 | # If we have a task, we'll add it to the task_data 218 | if task_search_response.Success and len(task_search_response.Tasks) > 0: 219 | data = task_search_response.Tasks[0] 220 | task_data['task'] = data.to_json() 221 | else: 222 | # We can have no responses, but no task is a deal breaker so we'll return an error. 223 | response.TaskStatus = "Error: Task ID not found" 224 | response.Success = False 225 | return response 226 | 227 | # We'll fetch the tags on the task before we continue to make sure it isn't a duplicate 228 | tag_search_response = await SendMythicRPCTagSearch(MythicRPCTagSearchMessage( 229 | TaskID=taskData.Task.ID, 230 | SearchTagTaskID=int(task_id), 231 | )) 232 | if tag_search_response.Success and len(tag_search_response.Tags) > 0: 233 | if any(VECTR_TAG_NAME in tag.TagType.Name for tag in tag_search_response.Tags): 234 | if not force_create: 235 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 236 | TaskID=taskData.Task.ID, 237 | Response=f"Error: Task has the '{VECTR_TAG_NAME}' tag, and has already been imported into VECTR. Use the -force_create flag to create a new test case anyway.".encode("UTF8"), 238 | )) 239 | response.TaskStatus = f"Error: Task already imported into VECTR." 240 | response.Success = False 241 | return response 242 | else: 243 | task_has_existing_tag = True 244 | 245 | task_callback_id = task_data['task']['callback_id'] 246 | task_name = task_data['task']['command_name'] 247 | payload_type = task_data['task']['payload_type'] 248 | # Let's get more info on the task itself for a description 249 | task_detail_search_response = await SendMythicRPCCommandSearch(MythicRPCCommandSearchMessage( 250 | SearchPayloadTypeName=payload_type, 251 | SearchCommandNames=[task_name] 252 | )) 253 | if task_detail_search_response.Success and len(task_detail_search_response.Commands) > 0: 254 | task_data['task_metadata'] = task_detail_search_response.Commands[0].to_json() 255 | 256 | # Let's get more info on the callback that ran the task to populate target data 257 | callback_search_response = await SendMythicRPCCallbackSearch(MythicRPCCallbackSearchMessage( 258 | AgentCallbackID=int(task_callback_id) 259 | )) 260 | if callback_search_response.Success and len(callback_search_response.Results) > 0: 261 | for result in callback_search_response.Results: 262 | if result.ID == task_callback_id: 263 | task_data['callback'] = result.to_json() 264 | break 265 | 266 | # Fetch the responses from Mythic 267 | task_data['responses'] = [] 268 | response_search_response = await SendMythicRPCResponseSearch(MythicRPCResponseSearchMessage(TaskID=int(task_id))) 269 | 270 | # If we have responses, we'll add them to the task_data 271 | if response_search_response.Success and len(response_search_response.Responses) > 0: 272 | task_responses = response_search_response.Responses 273 | for task_response in task_responses: 274 | task_data['responses'].append(task_response.to_json()) 275 | 276 | rest_vectr, gql_vectr = VectrAPI.initialise_vectr_connection(taskData) 277 | testcase = VectrAPI.transform_mythic_task_to_testcase(gql_vectr, task_data, testcase_name, mitre_technique_id, mitre_tactic_name) 278 | 279 | response_code, response_data = VectrAPI.create_test_cases(gql_vectr.connection_params, gql_vectr.target_db, gql_vectr.campaign_id, [testcase]) 280 | 281 | if response_code == 200 and not task_has_existing_tag: 282 | try: 283 | # add tag to prevent accidental re-importing later on 284 | create_tag_response = await SendMythicRPCTagTypeGetOrCreate(MythicRPCTagTypeGetOrCreateMessage( 285 | TaskID=taskData.Task.ID, 286 | GetOrCreateTagTypeName=VECTR_TAG_NAME, 287 | GetOrCreateTagTypeDescription="Tasks that have been imported into VECTR", 288 | GetOrCreateTagTypeColor="#ff00fb" 289 | )) 290 | if create_tag_response.TagType: 291 | tag_type_id = create_tag_response.TagType.ID 292 | add_tag_to_task_response = await SendMythicRPCTagCreate(MythicRPCTagCreateMessage( 293 | TagTypeID=int(tag_type_id), 294 | TaskID=int(task_id), 295 | Data={ 296 | "test_case_id": response_data["testcases"][0]['id'], 297 | "test_case_name": testcase.name, 298 | "added_by": task_data['task'].get('operator_username', "") 299 | } 300 | )) 301 | if not add_tag_to_task_response.Success: 302 | raise Exception(f"Add tag failed. {add_tag_to_task_response.Error}") 303 | else: 304 | raise Exception(f"Create or get tag failed. {create_tag_response.Error}") 305 | except Exception as e: 306 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 307 | TaskID=taskData.Task.ID, 308 | Response=f"Error: Successfully created test case in VECTR, but failed to add the '{VECTR_TAG_NAME}' tag to Mythic task '{task_id}'. Error: {e}".encode("UTF8"), 309 | )) 310 | response.TaskStatus = f"Error: Tag assignment failed." 311 | response.Success = False 312 | return response 313 | 314 | 315 | return await VectrAPI.process_standard_response( 316 | response_code=response_code, 317 | response_data=response_data, 318 | taskData=taskData, 319 | response=response 320 | ) 321 | 322 | except Exception as e: 323 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 324 | TaskID=taskData.Task.ID, 325 | Response=f"{e}".encode("UTF8"), 326 | )) 327 | response.TaskStatus = "Error: Vectr Access Error" 328 | response.Success = False 329 | return response 330 | 331 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 332 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 333 | return resp 334 | -------------------------------------------------------------------------------- /Payload_Type/vectr/vectr/VectrRequests/VectrAPI.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from vectr.VectrRequests.VectrAPIClasses import * 3 | from mythic_container.MythicRPC import * 4 | 5 | import nacl.utils 6 | from nacl.encoding import Base64Encoder 7 | from nacl.hash import generichash 8 | from nacl.secret import SecretBox 9 | 10 | from gql import Client, gql 11 | from gql.transport.requests import RequestsHTTPTransport 12 | from pydantic import BaseModel 13 | from typing import Dict 14 | from datetime import datetime, timezone 15 | import requests, re 16 | 17 | # REMOVE ME 18 | import urllib3 19 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 20 | 21 | VECTR_API_KEY = "VECTR_API_KEY" 22 | 23 | def check_valid_values(api_key, url, org_name, assessment_name, campaign_name, target_db) -> bool: 24 | if api_key == "" or api_key is None: 25 | logger.error("missing api key") 26 | return False 27 | if url == "" or url is None: 28 | logger.error("missing url") 29 | return False 30 | if org_name == "" or org_name is None: 31 | logger.error("missing org name") 32 | return False 33 | if assessment_name == "" or assessment_name is None: 34 | logger.error("missing assessment name") 35 | return False 36 | if campaign_name == "" or campaign_name is None: 37 | logger.error("missing campaign name") 38 | return False 39 | if target_db == "" or target_db is None: 40 | logger.error("missing target db") 41 | return False 42 | return True 43 | 44 | 45 | def get_client(connection_params: VectrGQLConnParams): 46 | transport = RequestsHTTPTransport( 47 | url=connection_params.vectr_gql_url, verify=False, retries=1, 48 | headers={"Authorization": "VEC1 " + connection_params.api_key} 49 | ) 50 | return Client(transport=transport, fetch_schema_from_transport=False) 51 | 52 | 53 | def initialise_vectr_connection(taskData): 54 | print("\n[*] Initialising VECTR API:") 55 | 56 | for buildParam in taskData.BuildParameters: 57 | if buildParam.Name == "URL": 58 | url = buildParam.Value 59 | if buildParam.Name == "org_name": 60 | org_name = buildParam.Value 61 | if buildParam.Name == "assessment_name": 62 | assessment_name = buildParam.Value 63 | if buildParam.Name == "campaign_name": 64 | campaign_name = buildParam.Value 65 | if buildParam.Name == "target_db": 66 | target_db = buildParam.Value 67 | if VECTR_API_KEY in taskData.Secrets: 68 | api_key = taskData.Secrets[VECTR_API_KEY] 69 | if not check_valid_values(api_key, url, org_name, assessment_name, campaign_name, target_db): 70 | return 500, f"Missing {VECTR_API_KEY} in User settings or missing Vectr URL, org name, assessment name, campaign name or target db." 71 | 72 | connection_params = VectrGQLConnParams( 73 | api_key=api_key, 74 | vectr_gql_url=url + "/graphql" 75 | ) 76 | 77 | rest_connection_params = VectrRESTConnParams( 78 | api_key=api_key, 79 | vectr_rest_url=url 80 | ) 81 | 82 | org_id = get_org_id_for_campaign_and_assessment_data( 83 | connection_params=connection_params, 84 | org_name=org_name 85 | ) 86 | print(f" - Assessment Name: {assessment_name}") 87 | print(f" - Target DB: {target_db}") 88 | 89 | try: 90 | assessment_id = get_assessment_by_name(connection_params, target_db, assessment_name) 91 | print(f" - Using existing assessment with ID: {assessment_id}") 92 | except RuntimeError as e: 93 | created_assessment_detail = create_assessment(connection_params, target_db, org_id, assessment_name) 94 | assessment_id = created_assessment_detail.get(assessment_name).get("id") 95 | logger.info(f" - Created assessment with ID: {assessment_id}") 96 | 97 | try: 98 | response_code, response_data = get_campaign_by_name(connection_params, target_db, campaign_name) 99 | if response_code != 200: 100 | raise RuntimeError(f"Error getting campaign by name: {response_data}") 101 | campaign_id = response_data 102 | print(f" - Using existing campaign with ID: {campaign_id}\n") 103 | except RuntimeError as e: 104 | cpgn = { campaign_name: Campaign(name=campaign_name, test_cases=[]) } 105 | created_campaigns = create_campaigns( 106 | connection_params, 107 | target_db, 108 | org_id, 109 | cpgn, 110 | str(assessment_id) 111 | ) 112 | campaign_id = created_campaigns.get(campaign_name).get("id") 113 | logger.info(f" - Created campaign with ID: {campaign_id}\n") 114 | 115 | return vectr_connection(org_name, rest_connection_params, target_db, campaign_name, campaign_id, assessment_id), vectr_connection(org_name, connection_params, target_db, campaign_name, campaign_id, assessment_id) 116 | 117 | 118 | 119 | def create_assessment(connection_params: VectrGQLConnParams, 120 | db: str, 121 | org_id: str, 122 | assessment_name: str) -> Dict[str, dict]: 123 | """Creates a named VECTR Assessment (Assessment Group) in the target database 124 | 125 | Parameters 126 | ---------- 127 | connection_params : VectrGQLConnParams 128 | Connection parameters for the target VECTR instance including api key and url 129 | db : str 130 | The database target where the assessment will be created 131 | This only includes selectable databases, template operations are separate 132 | org_id : str 133 | The org_id to which this Assessment will belong 134 | assessment_name: str 135 | The name of the Assessment to be created 136 | 137 | Returns 138 | ------- 139 | Dict[str, dict] 140 | An Assessment name-keyed dict of objects with the id and name of a created Assessment 141 | """ 142 | client = get_client(connection_params) 143 | assessment_mutation = gql( 144 | """ 145 | mutation ($input: CreateAssessmentInput!) { 146 | assessment { 147 | create(input: $input) { 148 | assessments { 149 | id, name, description, createTime 150 | } 151 | } 152 | } 153 | } 154 | """ 155 | ) 156 | 157 | assessment_vars = { 158 | "input": { 159 | "db": db, 160 | "assessmentData": [ 161 | { 162 | "name": assessment_name, 163 | "organizationIds": [org_id] 164 | } 165 | ] 166 | } 167 | } 168 | 169 | assessments = {} 170 | 171 | result = client.execute(assessment_mutation, variable_values=assessment_vars) 172 | if "assessment" in result.keys(): 173 | assessment_type_res = result["assessment"] 174 | if "create" in assessment_type_res: 175 | create_res = assessment_type_res["create"] 176 | if "assessments" in create_res: 177 | assessments_created = create_res["assessments"] 178 | 179 | for assessment in assessments_created: 180 | assessments[assessment["name"]] = {"id": assessment["id"], "name": assessment["name"]} 181 | 182 | return assessments 183 | 184 | 185 | def create_campaigns(connection_params: VectrGQLConnParams, 186 | db: str, 187 | org_id: str, 188 | campaigns: Dict[str, Campaign], 189 | parent_assessment_id: str) -> Dict[str, dict]: 190 | """Creates VECTR Campaigns in the target Assessment and Database 191 | 192 | Parameters 193 | ---------- 194 | connection_params : VectrGQLConnParams 195 | Connection parameters for the target VECTR instance including api key and url 196 | db : str 197 | The database target where the Campaigns will be created 198 | This only includes selectable databases, template operations are separate 199 | org_id : str 200 | The org_id to which the Campaigns will belong 201 | campaigns: Dict[str, Campaign] 202 | Campaigns to be created 203 | parent_assessment_id: str 204 | The ID of the parent Assessment for the Campaigns 205 | 206 | Returns 207 | ------- 208 | Dict[str, dict] 209 | A Campaign name-keyed dict of objects with the id and name of created Campaigns 210 | """ 211 | client = get_client(connection_params) 212 | campaign_mutation = gql( 213 | """ 214 | mutation ($input: CreateCampaignInput!) { 215 | campaign { 216 | create(input: $input) { 217 | campaigns { 218 | id, name, createTime 219 | } 220 | } 221 | } 222 | } 223 | """ 224 | ) 225 | 226 | campaign_data = [] 227 | for campaign_name in campaigns.keys(): 228 | campaign_data.append({ 229 | "name": campaign_name, 230 | "organizationIds": [org_id] 231 | }) 232 | 233 | campaign_vars = { 234 | "input": { 235 | "db": db, 236 | "assessmentId": parent_assessment_id, 237 | "campaignData": campaign_data 238 | } 239 | } 240 | 241 | campaigns = {} 242 | 243 | result = client.execute(campaign_mutation, variable_values=campaign_vars) 244 | 245 | if "campaign" in result.keys(): 246 | campaign_type_res = result["campaign"] 247 | if "create" in campaign_type_res: 248 | create_res = campaign_type_res["create"] 249 | if "campaigns" in create_res: 250 | campaigns_created = create_res["campaigns"] 251 | 252 | for campaign in campaigns_created: 253 | campaigns[campaign["name"]] = {"id": campaign["id"], "name": campaign["name"]} 254 | 255 | return campaigns 256 | 257 | def rest_delete_test_case(connection_params: VectrRESTConnParams, 258 | db: str, 259 | campaign_id: str, 260 | test_cases: List[int]): 261 | 262 | response = requests.post( 263 | connection_params.vectr_rest_url + "/testcases/delete?databaseName=" + db, 264 | json={ 265 | "includes":{ 266 | "ids": [ str(tc) for tc in test_cases ] 267 | } 268 | }, 269 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 270 | verify=False 271 | ) 272 | return response.status_code, response.json() 273 | 274 | 275 | def rest_upload_execution_artifact( 276 | connection_params: VectrRESTConnParams, 277 | encrypted_base64: str, 278 | key_base64: str, 279 | nonce_base64: str, 280 | file_size: int, 281 | file_hash: str, 282 | file_name: str, 283 | description: str, 284 | ): 285 | 286 | response = requests.post( 287 | connection_params.vectr_rest_url + "/executionArtifacts/uploadDocument?collectionName=ExecutionArtifacts", 288 | json={ 289 | "documentContents": encrypted_base64, 290 | "overwriteExisting": True, 291 | "allowDupeHash": True, 292 | "metadata":{ 293 | "description": description, 294 | "version": "latest", 295 | "encryptionKey": key_base64, 296 | "hash": file_hash, 297 | "filename": file_name, 298 | "label": file_name, 299 | "nonce": nonce_base64, 300 | "size": file_size 301 | } 302 | }, 303 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 304 | verify=False 305 | ) 306 | return response.status_code, response.json() 307 | 308 | 309 | def rest_get_test_case(connection_params: VectrRESTConnParams, 310 | db: str, 311 | test_case_id: int): 312 | response = requests.get( 313 | connection_params.vectr_rest_url + "/testcases/" + str(test_case_id) + "?databaseName=" + db, 314 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 315 | verify=False 316 | ) 317 | test_case_response = response.json() 318 | if len(test_case_response.get("data"))==1: 319 | test_case_data = test_case_response.get("data")[0] 320 | else: 321 | raise Exception("Error getting test case data") 322 | 323 | return response.status_code, test_case_data 324 | 325 | 326 | def rest_update_test_case(connection_params: VectrRESTConnParams, 327 | db: str, 328 | test_case: dict): 329 | response = requests.put( 330 | connection_params.vectr_rest_url + "/testcases?databaseName=" + db, 331 | json=[{ 332 | "timelineEventData": [], 333 | "testCaseData": test_case 334 | }], 335 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 336 | verify=False 337 | ) 338 | return response.status_code, response.json() 339 | 340 | 341 | def rest_add_execution_artifact_to_test_case(connection_params: VectrRESTConnParams, 342 | db: str, 343 | test_case_id: int, 344 | execution_artifact_id: int): 345 | response_code, raw_test_case = rest_get_test_case(connection_params, db, test_case_id) 346 | 347 | if not raw_test_case: 348 | 500, "Error getting test case data" 349 | 350 | raw_test_case["redTeam"]["executionArtifactIds"].append(str(execution_artifact_id)) 351 | 352 | return rest_update_test_case(connection_params, db, raw_test_case) 353 | 354 | 355 | def rest_get_mitre_techniques(connection_params: VectrRESTConnParams): 356 | response = requests.get( 357 | connection_params.vectr_rest_url + "/mitre/getTechniqueIdMap?mitreFramework=ENTERPRISE", 358 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 359 | verify=False 360 | ) 361 | if not 'data' in response.json(): 362 | return 500, "Error getting MITRE techniques" 363 | 364 | data = response.json()['data'] 365 | 366 | techniques = [] 367 | for technique, mitre_ids in data.items(): 368 | for mitre_id in mitre_ids: 369 | techniques.append({ "name": technique, "mitreId": mitre_id }) 370 | 371 | return response.status_code, techniques 372 | 373 | def rest_get_mitre_tactics(connection_params: VectrRESTConnParams, db: str, assessment_id: int): 374 | # Assessment specific active phases 375 | response = requests.get( 376 | connection_params.vectr_rest_url + "/phases/getAssessmentActivePhases?databaseName=" + db + "&assessmentId=" + str(assessment_id), 377 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 378 | verify=False 379 | ) 380 | if not 'data' in response.json(): 381 | return 500, "Error getting MITRE techniques" 382 | 383 | active_phases = response.json()['data'] 384 | 385 | # General MITRE tactic metadata 386 | response = requests.get( 387 | connection_params.vectr_rest_url + "/mitre/tacticIdMap?mitreFramework=ENTERPRISE", 388 | headers={"Authorization": "VEC1 " + connection_params.api_key}, 389 | verify=False 390 | ) 391 | if not 'data' in response.json(): 392 | return 500, "Error getting MITRE techniques" 393 | 394 | data = response.json()['data'] 395 | 396 | tactics = [] 397 | for mitre_identifier, mitre_data in data.items(): 398 | id = "" 399 | for phase in active_phases: 400 | if phase.get('mitreTactics', []): 401 | if mitre_identifier in phase.get('mitreTactics', []): 402 | id = phase['id'] 403 | break 404 | 405 | tactic_id = mitre_data['externalId'] 406 | tactic_name = mitre_data['name'] 407 | description = mitre_data['description'] 408 | short_name = mitre_data['shortName'] 409 | 410 | tactics.append({ 411 | "id": id, 412 | "mitreId": tactic_id, 413 | "name": tactic_name, 414 | "description": description, 415 | "shortName": short_name, 416 | "mitreIdentifier": mitre_identifier 417 | }) 418 | 419 | return response.status_code, tactics 420 | 421 | 422 | def create_test_cases(connection_params: VectrGQLConnParams, 423 | db: str, 424 | campaign_id: str, 425 | test_cases: Dict[str, TestCase]) -> Dict[str, dict]: 426 | """Creates VECTR Test Cases in the target Campaign and Database 427 | 428 | Parameters 429 | ---------- 430 | connection_params : VectrGQLConnParams 431 | Connection parameters for the target VECTR instance including api key and url 432 | db : str 433 | The database target where the Campaigns will be created 434 | This only includes selectable databases, template operations are separate 435 | campaign_id : str 436 | The Campaign ID to which the Test Cases will belong 437 | test_cases: Dict[str, TestCase] 438 | TestCases to be created 439 | 440 | Returns 441 | ------- 442 | Dict[str, dict] 443 | A Test Case name-keyed dict of objects with the id and name of created Test Cases 444 | """ 445 | try: 446 | client = get_client(connection_params) 447 | test_case_mutation = gql( 448 | """ 449 | mutation ($input: CreateTestCaseAndTemplateMatchByNameInput!) { 450 | testCase { 451 | createWithTemplateMatchByName(input: $input) { 452 | testCases { 453 | id, name 454 | } 455 | } 456 | } 457 | } 458 | """ 459 | ) 460 | 461 | test_case_data = [] 462 | for test_case in test_cases: 463 | test_case_data.append({ 464 | "testCaseData": dict(test_case) 465 | }) 466 | 467 | test_case_vars = { 468 | "input": { 469 | "db": db, 470 | "campaignId": campaign_id, 471 | "createTestCaseInputs": test_case_data 472 | } 473 | } 474 | 475 | test_case_created_response = { "testcases": [] } 476 | 477 | result = client.execute(test_case_mutation, variable_values=test_case_vars) 478 | 479 | if "testCase" in result.keys(): 480 | test_case_type_res = result["testCase"] 481 | if "createWithTemplateMatchByName" in test_case_type_res: 482 | create_res = test_case_type_res["createWithTemplateMatchByName"] 483 | if "testCases" in create_res: 484 | test_cases_created = create_res["testCases"] 485 | 486 | for test_case in test_cases_created: 487 | test_case_created_response['testcases'].append({"id": test_case["id"], "name": test_case["name"]}) 488 | except Exception as e: 489 | return 500, f"Error creating test cases: {e}" 490 | 491 | return 200, test_case_created_response 492 | 493 | 494 | def get_org_id_for_campaign_and_assessment_data(connection_params: VectrGQLConnParams, org_name: str) -> str: 495 | client = get_client(connection_params) 496 | 497 | org_query = gql( 498 | """ 499 | query($nameVar: String) { 500 | organizations(filter: {name: {eq: $nameVar}}) { 501 | nodes { 502 | id, name 503 | } 504 | } 505 | } 506 | """ 507 | ) 508 | 509 | org_vars = {"nameVar": org_name} 510 | 511 | result = client.execute(org_query, variable_values=org_vars) 512 | 513 | if "organizations" in result.keys(): 514 | organizations_type_res = result["organizations"] 515 | if "nodes" in organizations_type_res: 516 | nodes_res = organizations_type_res["nodes"] 517 | if nodes_res: 518 | return nodes_res[0]["id"] 519 | 520 | raise RuntimeError("couldn't find org name. create in VECTR first") 521 | 522 | 523 | def get_assessment_by_name(connection_params: VectrGQLConnParams, db_name: str, assessment_name: str) -> str: 524 | client = get_client(connection_params) 525 | 526 | org_query = gql( 527 | """ 528 | query ($db: String!, $nameVar: String){ 529 | assessments(db:$db, filter: {name: {eq: $nameVar}}) { 530 | nodes { 531 | id, name 532 | } 533 | } 534 | } 535 | """ 536 | ) 537 | ass_vars = {"nameVar": assessment_name, "db": db_name} 538 | result = client.execute(org_query, variable_values=ass_vars) 539 | if "assessments" in result.keys(): 540 | assessments_type_res = result["assessments"] 541 | if "nodes" in assessments_type_res: 542 | nodes_res = assessments_type_res["nodes"] 543 | if nodes_res: 544 | return nodes_res[0]["id"] 545 | 546 | raise RuntimeError("couldn't find assessment name. create in VECTR first") 547 | 548 | def get_campaign_by_name(connection_params: VectrGQLConnParams, db_name: str, campaign_name: str) -> str: 549 | client = get_client(connection_params) 550 | 551 | org_query = gql( 552 | """ 553 | query ($db: String!, $nameVar: String){ 554 | campaigns(db:$db, filter: {name: {eq: $nameVar}}) { 555 | nodes { 556 | id, name 557 | } 558 | } 559 | } 560 | """ 561 | ) 562 | 563 | cpg_vars = {"nameVar": campaign_name, "db": db_name} 564 | result = client.execute(org_query, variable_values=cpg_vars) 565 | if "campaigns" in result.keys(): 566 | campaigns_type_res = result["campaigns"] 567 | if "nodes" in campaigns_type_res: 568 | nodes_res = campaigns_type_res["nodes"] 569 | if nodes_res: 570 | return 200, nodes_res[0]["id"] 571 | 572 | return 500, "Couldn't find campaign name. Create it in VECTR first." 573 | 574 | def get_testcases_for_campaign_by_id(connection_params: VectrGQLConnParams, db_name: str, campaign_id: str) -> str: 575 | client = get_client(connection_params) 576 | 577 | org_query = gql( 578 | """ 579 | query ($db: String!, $idVar: String!){ 580 | campaign(id:$idVar, db:$db) { 581 | id, 582 | name, 583 | testCases { 584 | id, 585 | name, 586 | description, 587 | method, 588 | mitreId, 589 | outcome { 590 | path, 591 | abbreviation 592 | }, 593 | operatorGuidance, 594 | outcomeNotes, 595 | tags { 596 | name 597 | }, 598 | executionArtifactIdInfo { 599 | id 600 | }, 601 | status, 602 | attackStart { 603 | id, 604 | createTime, 605 | updateTime, 606 | team 607 | }, 608 | attackStop { 609 | id, 610 | createTime, 611 | updateTime, 612 | team 613 | } 614 | } 615 | } 616 | } 617 | """ 618 | ) 619 | 620 | cpg_vars = {"idVar": campaign_id, "db": db_name} 621 | result = client.execute(org_query, variable_values=cpg_vars) 622 | if "campaign" in result.keys(): 623 | campaign_type_res = result["campaign"] 624 | if "testCases" in campaign_type_res: 625 | return 200, campaign_type_res["testCases"] 626 | 627 | return 500, "Couldn't find campaign name. Create it in VECTR first." 628 | 629 | 630 | def transform_mythic_task_to_testcase(vectr_con, task_data, provided_test_case_name, override_mitre_technique_id, override_mitre_tactic_name): 631 | task = task_data.get('task', {}) 632 | task_metadata = task_data.get('task_metadata', {}) 633 | callback = task_data.get('callback', {}) 634 | responses = task_data.get('responses', []) 635 | 636 | if provided_test_case_name: 637 | test_case_name = provided_test_case_name 638 | else: 639 | test_case_name = f"{task.get('command_name')}{' ' + task.get('original_params') if task.get('original_params') else ''}" 640 | 641 | description = f"""#### Executed via Mythic 642 | - **Command**: {f"{task.get('command_name')}{' ' + task.get('original_params') if task.get('original_params') else ''}"} 643 | - **Task Description**: {task_metadata.get('description')} 644 | - **Target User**: `{callback.get('user')}` 645 | - **Target Host**: `{callback.get('host')}` 646 | 647 | Metadata: 648 | ``` 649 | Callback ID: {callback.get('id')} 650 | Payload Type: {task.get('payload_type')} 651 | Task ID: {task.get('id')} 652 | ``` 653 | """ 654 | 655 | mitre_id = task_metadata.get('attack') if task_metadata.get('attack') else "T1204" 656 | if override_mitre_technique_id: 657 | mitre_id = override_mitre_technique_id.upper() 658 | 659 | mitre_tactic_name = override_mitre_tactic_name.title() if override_mitre_tactic_name else "Execution" 660 | 661 | outcome_notes = "#### Command Output\n```" 662 | for response in responses: 663 | outcome_notes += "\n" + response.get('response') 664 | outcome_notes += "\n```" 665 | 666 | tags = [] 667 | if task.get('payload_type'): 668 | tags.append(task.get('payload_type')) 669 | if task.get('status'): 670 | tags.append(f"task_status:{task.get('status')}") 671 | if task.get('operator_username'): 672 | tags.append(f"mythic_user:{task.get('operator_username')}") 673 | if callback.get('user'): 674 | tags.append(f"callback_user:{callback.get('user')}") 675 | if callback.get('host'): 676 | tags.append(f"callback_host:{callback.get('host')}") 677 | if callback.get('display_id'): 678 | tags.append(f"callback_host:{callback.get('display_id')}") 679 | 680 | outcome = "TBD" 681 | 682 | target_hosts = [] 683 | if callback.get('host'): 684 | target_hosts.append(callback.get('host')) 685 | 686 | # This appears to have duplicate timezone indicators, so using regex to get the content we want 687 | timestamp_parse = re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s\+\d{4}", task['timestamp']) 688 | 689 | if timestamp_parse: 690 | execution_epoch = int(datetime.strptime(timestamp_parse.group(0), "%Y-%m-%d %H:%M:%S.%f %z").timestamp()*1000) 691 | else: 692 | logger.info("Error parsing timestamp, using current time") 693 | execution_epoch = int(datetime.now().timestamp()*1000) 694 | 695 | return TestCase( 696 | Variant=f"{test_case_name}", 697 | Objective=description, 698 | Phase=mitre_tactic_name, 699 | MitreID=mitre_id, 700 | Tags=','.join(tags), 701 | Status="Completed", 702 | Outcome=outcome, 703 | OutcomeNotes=outcome_notes, 704 | TargetAssets=','.join(target_hosts), 705 | # ExpectedDetectionLayers="", 706 | # AlertTriggered="Yes" if was_detected else "No", 707 | # References=references, 708 | # DetectingTools=detecting_tools, 709 | # ActivityLogged=activity_logged, 710 | StartTimeEpoch=execution_epoch, 711 | StopTimeEpoch=execution_epoch, 712 | Organizations=vectr_con.org_name 713 | ) 714 | 715 | def encrypt_execution_artifact(artifact_data): 716 | # Generate a hash of the encoded data 717 | hash_hex = generichash(artifact_data, digest_size=32).decode() 718 | 719 | # Encrypt the data using SecretBox 720 | key = nacl.utils.random(SecretBox.KEY_SIZE) 721 | nonce = nacl.utils.random(SecretBox.NONCE_SIZE) 722 | box = SecretBox(key) 723 | encrypted = box.encrypt(artifact_data, nonce) 724 | 725 | # Encode the encrypted data and nonce in base64 726 | encrypted_base64 = Base64Encoder.encode(encrypted.ciphertext).decode() 727 | key_base64 = Base64Encoder.encode(key).decode() 728 | nonce_base64 = Base64Encoder.encode(nonce).decode() 729 | 730 | return encrypted_base64, key_base64, nonce_base64, hash_hex 731 | 732 | 733 | async def process_standard_response(response_code: int, response_data: any, 734 | taskData: PTTaskMessageAllData, response: PTTaskCreateTaskingMessageResponse, as_json=True) -> \ 735 | PTTaskCreateTaskingMessageResponse: 736 | if response_code == 200: 737 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 738 | TaskID=taskData.Task.ID, 739 | Response=json.dumps(response_data).encode("UTF8") if as_json else f"{response_data}".encode("UTF8"), 740 | )) 741 | response.Success = True 742 | else: 743 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 744 | TaskID=taskData.Task.ID, 745 | Response=f"{response_data}".encode("UTF8"), 746 | )) 747 | response.TaskStatus = "Error: VECTR API Error" 748 | response.Success = False 749 | return response 750 | --------------------------------------------------------------------------------