diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index e596154594..bb8b385845 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -21,7 +21,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + submodules: recursive - name: Log in to the Container registry uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc diff --git a/CITATION.cff b/CITATION.cff index 6616bcaa03..4390b9c738 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,7 +2,7 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - name: "MITRE Corporation" -title: "CALDERA: A Scalable, Automated Adversary Emulation Platform" -version: 4.1.0 -date-released: 2022-09-17 +title: "MITRE Caldera: A Scalable, Automated Adversary Emulation Platform" +version: 4.2.0 +date-released: 2023-06-19 url: "https://github.com/mitre/caldera" diff --git a/Dockerfile b/Dockerfile index 3e0e7e6919..446957ddad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,40 +24,6 @@ ENV VIRTUAL_ENV=/opt/venv/caldera RUN python3 -m venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# haproxy is needed for the ssl plugin -RUN apt-get install haproxy -y - -# Generate self signed certificate -RUN openssl req -x509 -newkey rsa:4096 -out conf/certificate.pem -keyout conf/certificate.pem -subj "/C=US/ST=VA/L=McLean/O=Mitre/OU=IT/CN=mycaldera.caldera" -nodes -RUN mv conf/certificate.pem plugins/ssl/conf/certificate.pem - -# Enable Self Signed Certificate within haproxy -RUN cp plugins/ssl/templates/haproxy.conf conf/ -RUN sed -i 's#bind\ \*\:8443\ ssl\ crt\ plugins/ssl/conf/insecure_certificate.pem#bind\ \*\:8443\ ssl\ crt\ plugins/ssl/conf/certificate.pem#g' conf/haproxy.conf - -# Enable ssl plugin -RUN sed -i '/^\-\ manx/a \-\ ssl' conf/default.yml - -# Enable emu -#RUN sed -i '/^\-\ ssl/a \-\ emu' conf/default.yml - -# haproxy is needed for the ssl plugin -RUN apt-get install haproxy -y - -# Generate self signed certificate -RUN openssl req -x509 -newkey rsa:4096 -out conf/certificate.pem -keyout conf/certificate.pem -subj "/C=US/ST=VA/L=McLean/O=Mitre/OU=IT/CN=mycaldera.caldera" -nodes -RUN mv conf/certificate.pem plugins/ssl/conf/certificate.pem - -# Enable Self Signed Certificate within haproxy -RUN cp plugins/ssl/templates/haproxy.conf conf/ -RUN sed -i 's#bind\ \*\:8443\ ssl\ crt\ plugins/ssl/conf/insecure_certificate.pem#bind\ \*\:8443\ ssl\ crt\ plugins/ssl/conf/certificate.pem#g' conf/haproxy.conf - -# Enable ssl plugin -RUN sed -i '/^\-\ manx/a \-\ ssl' conf/default.yml - -# Enable emu -#RUN sed -i '/^\-\ ssl/a \-\ emu' conf/default.yml - # Install pip requirements RUN pip3 install --no-cache-dir -r requirements.txt @@ -65,15 +31,6 @@ RUN pip3 install --no-cache-dir -r requirements.txt RUN python3 -c "import app; import app.utility.config_generator; app.utility.config_generator.ensure_local_config();"; \ sed -i '/\- atomic/d' conf/local.yml; -# Copy default.yml to local.yml or none of these changes above will stick -RUN cp conf/default.yml conf/local.yml - -# Install golang -RUN curl -L https://go.dev/dl/go1.17.6.linux-amd64.tar.gz -o go1.17.6.linux-amd64.tar.gz -RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz; -ENV PATH="${PATH}:/usr/local/go/bin" -RUN go version; - # Compile default sandcat agent binaries, which will download basic golang dependencies. # Install Go dependencies diff --git a/app/contacts/contact_tcp.py b/app/contacts/contact_tcp.py index 35f942c8f4..d6ff0b7542 100644 --- a/app/contacts/contact_tcp.py +++ b/app/contacts/contact_tcp.py @@ -85,6 +85,7 @@ async def send(self, session_id: int, cmd: str, timeout: int = 60) -> Tuple[int, try: conn = next(i.connection for i in self.sessions if i.id == int(session_id)) conn.send(str.encode(' ')) + time.sleep(0.01) conn.send(str.encode('%s\n' % cmd)) response = await self._attempt_connection(session_id, conn, timeout=timeout) response = json.loads(response) diff --git a/plugins/debrief b/plugins/debrief index e4d4f9e954..cc5d3f52d3 160000 --- a/plugins/debrief +++ b/plugins/debrief @@ -1 +1 @@ -Subproject commit e4d4f9e9543e4e2866aef1ab540ced5004693ad7 +Subproject commit cc5d3f52d3d2f663295e8b3f7cd939dcdab504b9 diff --git a/plugins/fieldmanual b/plugins/fieldmanual index c286e77e05..856eee10a4 160000 --- a/plugins/fieldmanual +++ b/plugins/fieldmanual @@ -1 +1 @@ -Subproject commit c286e77e05214b055c8e20a8d60d99883f20f220 +Subproject commit 856eee10a4c4e023a6052dc52f6aa4f116f76c43 diff --git a/plugins/ssl b/plugins/ssl index ac5bfcb1b7..ca7706342d 160000 --- a/plugins/ssl +++ b/plugins/ssl @@ -1 +1 @@ -Subproject commit ac5bfcb1b74e80798b9fe07aad10f5990f9a2c2c +Subproject commit ca7706342d2a60e71b7206f2addd425dfa185ab4 diff --git a/plugins/stockpile b/plugins/stockpile index 960f9adfbf..01331a302c 160000 --- a/plugins/stockpile +++ b/plugins/stockpile @@ -1 +1 @@ -Subproject commit 960f9adfbfb952b76cbcbc0f6953387c60749d84 +Subproject commit 01331a302c277bf8e5dc3cd957917ef405282539 diff --git a/tests/objects/test_operation.py b/tests/objects/test_operation.py index 3a93d278fb..d3d4ecc8a4 100644 --- a/tests/objects/test_operation.py +++ b/tests/objects/test_operation.py @@ -497,3 +497,102 @@ def test_update_untrusted_agents_with_untrusted_no_operation_agents(self, operat op = Operation(name='test', agents=[], adversary=adversary) op.update_untrusted_agents(operation_agent) assert not op.untrusted_agents + + def test_check_reason_skipped_unknown_platform(self, test_agent, test_ability): + test_agent.platform = 'unknown' + op = Operation(name='test', agents=[test_agent], state='running') + reason = op._check_reason_skipped(agent=test_agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=test_agent.executors, agent_ran={}) + assert reason['reason'] == 'Platform not available' + assert reason['reason_id'] == Operation.Reason.PLATFORM.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_valid_executor(self, test_agent, test_ability): + test_agent.platform = 'darwin' + op = Operation(name='test', agents=[test_agent], state='running') + reason = op._check_reason_skipped(agent=test_agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=[], agent_ran={}) + assert reason['reason'] == 'Mismatched ability platform and executor' + assert reason['reason_id'] == Operation.Reason.EXECUTOR.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_privilege(self, custom_agent, test_ability, mocker, test_executor): + test_executor.name = 'psh' + agent = custom_agent() + test_ability.privilege = 'Elevated' + op = Operation(name='test', agents=[agent], state='running') + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Ability privilege not fulfilled' + assert reason['reason_id'] == Operation.Reason.PRIVILEGE.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_fact_dependency(self, custom_agent, test_ability, mocker, test_executor, fact): + test_executor.name = 'psh' + agent = custom_agent() + op = Operation(name='test', agents=[agent], state='running') + with mocker.patch('app.objects.c_ability.Ability.find_executors') as mock_find_executors: + mock_find_executors.return_value = [test_executor] + with mocker.patch('re.findall') as mock_findall: + mock_findall.return_value = [fact('test.fact.attribute')] + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Fact dependency not fulfilled' + assert reason['reason_id'] == Operation.Reason.FACT_DEPENDENCY.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_link_ignored(self, custom_agent, test_ability, mocker, active_link): + agent = custom_agent() + op = Operation(name='test', agents=[agent], state='running') + test_link = Link.load(active_link) + op.chain = [test_link] + op.ignored_links = [test_link.id] + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Link ignored - highly visible or discarded link' + assert reason['reason_id'] == Operation.Reason.LINK_IGNORED.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_untrusted(self, custom_agent, test_ability, mocker): + agent = custom_agent(trusted=False) + op = Operation(name='test', agents=[agent], state='running') + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Agent not trusted' + assert reason['reason_id'] == Operation.Reason.UNTRUSTED.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_op_running(self, custom_agent, test_ability, mocker): + agent = custom_agent() + op = Operation(name='test', agents=[agent], state='running') + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Operation not completed' + assert reason['reason_id'] == Operation.Reason.OP_RUNNING.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_check_reason_skipped_other(self, custom_agent, test_ability, mocker): + agent = custom_agent() + op = Operation(name='test', agents=[agent], state='finished') + reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state, + agent_executors=agent.executors, agent_ran={}) + assert reason['reason'] == 'Other' + assert reason['reason_id'] == Operation.Reason.OTHER.value + assert reason['ability_id'] == test_ability.ability_id + assert reason['ability_name'] == test_ability.name + + async def test_add_ignored_link(self, make_test_link, operation_agent): + test_agent = operation_agent + test_link = make_test_link(9876, test_agent.paw, Link().states['DISCARD']) + op = Operation(name='test', agents=[test_agent], state='running') + op.add_ignored_link(test_link.id) + assert op.ignored_links + assert test_link.id in op.ignored_links + assert len(op.ignored_links) == 1