From c62529f0437fa9fe23c6a26554bf34c5fae1e11d Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 09:33:28 -0400 Subject: [PATCH 1/2] fix: grant blue training flag when malicious file is removed (#167) The AutonomousBlue2 ("Malicious file on system") flag was never granted because of two issues: 1. The flag class file existed at app/blue_2.py but was missing from app/flags/autonomous/blue_2.py where the certification YAML expected to import it from. 2. The flag entry was commented out in the Blue Certificate YAML (8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml), so it was never loaded into the training system. Also removed the spurious visible=False attribute that no other flag has. --- app/flags/autonomous/blue_2.py | 21 +++++ .../8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml | 2 +- tests/app/flags/__init__.py | 0 tests/app/flags/autonomous/__init__.py | 0 tests/app/flags/autonomous/test_blue_2.py | 87 +++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 app/flags/autonomous/blue_2.py create mode 100644 tests/app/flags/__init__.py create mode 100644 tests/app/flags/autonomous/__init__.py create mode 100644 tests/app/flags/autonomous/test_blue_2.py diff --git a/app/flags/autonomous/blue_2.py b/app/flags/autonomous/blue_2.py new file mode 100644 index 00000000..c764e5c9 --- /dev/null +++ b/app/flags/autonomous/blue_2.py @@ -0,0 +1,21 @@ +from app.utility.base_world import BaseWorld +from plugins.training.app.c_flag import Flag + + +class AutonomousBlue2(Flag): + name = 'Malicious file on system' + challenge = 'Write a file on the Windows machine under the C:\\Users\\Public directory. Get the SHA256 hash of ' \ + 'this file, and write it to C:\\Users\\Public\\malicious_files.txt. The autonomous defender should ' \ + 'automatically find and delete the file.' + extra_info = """""" + + async def verify(self, services): + def is_file_deleted(operation): + return operation.ran_ability_id('5ec7ae3b-c909-41bb-9b6b-dadec409cd40') + + for op in await services.get('data_svc').locate('operations', + match=dict(access=BaseWorld.Access.BLUE, + name='Blue Autonomous')): + if await self._is_file_found(op) and is_file_deleted(op): + return True + return False diff --git a/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml b/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml index c1a0d385..2191a37d 100644 --- a/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml +++ b/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml @@ -9,7 +9,7 @@ badges: autonomous: - flags.autonomous.blue_0.AutonomousBlue0 - flags.autonomous.blue_1.AutonomousBlue1 - #- flags.autonomous.blue_2.AutonomousBlue2 + - flags.autonomous.blue_2.AutonomousBlue2 - flags.autonomous.blue_3.AutonomousBlue3 #manual: #- flags.manual.blue_0.ManualBlue0 diff --git a/tests/app/flags/__init__.py b/tests/app/flags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/flags/autonomous/__init__.py b/tests/app/flags/autonomous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/flags/autonomous/test_blue_2.py b/tests/app/flags/autonomous/test_blue_2.py new file mode 100644 index 00000000..d3fbea43 --- /dev/null +++ b/tests/app/flags/autonomous/test_blue_2.py @@ -0,0 +1,87 @@ +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +import yaml + +from plugins.training.app.flags.autonomous.blue_2 import AutonomousBlue2 + + +class TestAutonomousBlue2: + """Tests for the 'Malicious file on system' blue training flag (#167).""" + + def test_flag_importable_from_correct_path(self): + """The flag class must be importable from flags.autonomous.blue_2.""" + assert AutonomousBlue2 is not None + assert AutonomousBlue2.name == 'Malicious file on system' + + def test_flag_registered_in_blue_certificate_yaml(self): + """The flag must be uncommented and listed in the Blue Certificate YAML.""" + with open('plugins/training/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml') as f: + cert = yaml.safe_load(f) + autonomous_flags = cert['badges']['autonomous'] + assert 'flags.autonomous.blue_2.AutonomousBlue2' in autonomous_flags + + def test_flag_has_no_visible_false(self): + """The flag should not have visible=False which would hide it from users.""" + assert not hasattr(AutonomousBlue2, 'visible') or AutonomousBlue2.visible is not False + + @pytest.mark.asyncio + async def test_verify_returns_true_when_file_found_and_deleted(self): + """Flag should be granted when the file is found AND deleted.""" + flag = AutonomousBlue2(number=1) + + mock_op = MagicMock() + mock_op.ran_ability_id = MagicMock(return_value=True) + + mock_fact_hash = MagicMock() + mock_fact_hash.trait = 'file.malicious.hash' + mock_fact_file = MagicMock() + mock_fact_file.trait = 'host.malicious.file' + mock_op.all_facts = AsyncMock(return_value=[mock_fact_hash, mock_fact_file]) + + mock_data_svc = AsyncMock() + mock_data_svc.locate = AsyncMock(return_value=[mock_op]) + services = {'data_svc': mock_data_svc} + + result = await flag.verify(services) + assert result is True + + @pytest.mark.asyncio + async def test_verify_returns_false_when_file_not_deleted(self): + """Flag should NOT be granted when file is found but not deleted.""" + flag = AutonomousBlue2(number=1) + + mock_op = MagicMock() + # ran_ability_id returns True for find, False for delete + def selective_ran(ability_id): + if ability_id == 'f9b3eff0-e11c-48de-9338-1578b351b14b': + return True # file found ability + return False # file delete ability not run + + mock_op.ran_ability_id = MagicMock(side_effect=selective_ran) + + mock_fact_hash = MagicMock() + mock_fact_hash.trait = 'file.malicious.hash' + mock_fact_file = MagicMock() + mock_fact_file.trait = 'host.malicious.file' + mock_op.all_facts = AsyncMock(return_value=[mock_fact_hash, mock_fact_file]) + + mock_data_svc = AsyncMock() + mock_data_svc.locate = AsyncMock(return_value=[mock_op]) + services = {'data_svc': mock_data_svc} + + result = await flag.verify(services) + assert result is False + + @pytest.mark.asyncio + async def test_verify_returns_false_when_no_operations(self): + """Flag should NOT be granted when there are no Blue Autonomous operations.""" + flag = AutonomousBlue2(number=1) + + mock_data_svc = AsyncMock() + mock_data_svc.locate = AsyncMock(return_value=[]) + services = {'data_svc': mock_data_svc} + + result = await flag.verify(services) + assert result is False From f7bf31e13bcefc5dfb077de56e00b84f5e9b46f1 Mon Sep 17 00:00:00 2001 From: deacon Date: Wed, 18 Mar 2026 09:12:42 -0400 Subject: [PATCH 2/2] fix: address Copilot review feedback - test_blue_2.py: replace hard-coded path with Path(__file__)-derived path, remove unused asyncio and patch imports - app/blue_2.py: replace dead-code duplicate with a stub that re-exports the canonical class from app/flags/autonomous/blue_2.py --- app/blue_2.py | 25 +++-------------------- tests/app/flags/autonomous/test_blue_2.py | 9 +++++--- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/app/blue_2.py b/app/blue_2.py index 455fe960..21549079 100644 --- a/app/blue_2.py +++ b/app/blue_2.py @@ -1,22 +1,3 @@ -from app.utility.base_world import BaseWorld -from plugins.training.app.c_flag import Flag - - -class AutonomousBlue2(Flag): - name = 'Malicious file on system' - challenge = 'Write a file on the Windows machine under the C:\\Users\\Public directory. Get the SHA256 hash of ' \ - 'this file, and write it to C:\\Users\\Public\\malicious_files.txt. The autonomous defender should ' \ - 'automatically find and delete the file.' - extra_info = """""" - visible = False - - async def verify(self, services): - def is_file_deleted(operation): - return operation.ran_ability_id('5ec7ae3b-c909-41bb-9b6b-dadec409cd40') - - for op in await services.get('data_svc').locate('operations', - match=dict(access=BaseWorld.Access.BLUE, - name='Blue Autonomous')): - if await self._is_file_found(op) and is_file_deleted(op): - return True - return False +# This module is kept for backwards compatibility. +# The canonical implementation is at app/flags/autonomous/blue_2.py. +from plugins.training.app.flags.autonomous.blue_2 import AutonomousBlue2 # noqa: F401 diff --git a/tests/app/flags/autonomous/test_blue_2.py b/tests/app/flags/autonomous/test_blue_2.py index d3fbea43..edd1dc31 100644 --- a/tests/app/flags/autonomous/test_blue_2.py +++ b/tests/app/flags/autonomous/test_blue_2.py @@ -1,11 +1,13 @@ -import asyncio -from unittest.mock import AsyncMock, MagicMock, patch +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock import pytest import yaml from plugins.training.app.flags.autonomous.blue_2 import AutonomousBlue2 +_PLUGIN_ROOT = Path(__file__).resolve().parents[4] + class TestAutonomousBlue2: """Tests for the 'Malicious file on system' blue training flag (#167).""" @@ -17,7 +19,8 @@ def test_flag_importable_from_correct_path(self): def test_flag_registered_in_blue_certificate_yaml(self): """The flag must be uncommented and listed in the Blue Certificate YAML.""" - with open('plugins/training/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml') as f: + yaml_path = _PLUGIN_ROOT / 'data' / 'certifications' / '8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml' + with open(yaml_path) as f: cert = yaml.safe_load(f) autonomous_flags = cert['badges']['autonomous'] assert 'flags.autonomous.blue_2.AutonomousBlue2' in autonomous_flags