├── 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 |
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 |
188 |
--------------------------------------------------------------------------------
/Payload_Type/vectr/vectr/agent_functions/vectr.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------