Skip to content

Commit c62529f

Browse files
committed
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.
1 parent 27f9bde commit c62529f

5 files changed

Lines changed: 109 additions & 1 deletion

File tree

app/flags/autonomous/blue_2.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from app.utility.base_world import BaseWorld
2+
from plugins.training.app.c_flag import Flag
3+
4+
5+
class AutonomousBlue2(Flag):
6+
name = 'Malicious file on system'
7+
challenge = 'Write a file on the Windows machine under the C:\\Users\\Public directory. Get the SHA256 hash of ' \
8+
'this file, and write it to C:\\Users\\Public\\malicious_files.txt. The autonomous defender should ' \
9+
'automatically find and delete the file.'
10+
extra_info = """"""
11+
12+
async def verify(self, services):
13+
def is_file_deleted(operation):
14+
return operation.ran_ability_id('5ec7ae3b-c909-41bb-9b6b-dadec409cd40')
15+
16+
for op in await services.get('data_svc').locate('operations',
17+
match=dict(access=BaseWorld.Access.BLUE,
18+
name='Blue Autonomous')):
19+
if await self._is_file_found(op) and is_file_deleted(op):
20+
return True
21+
return False

data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ badges:
99
autonomous:
1010
- flags.autonomous.blue_0.AutonomousBlue0
1111
- flags.autonomous.blue_1.AutonomousBlue1
12-
#- flags.autonomous.blue_2.AutonomousBlue2
12+
- flags.autonomous.blue_2.AutonomousBlue2
1313
- flags.autonomous.blue_3.AutonomousBlue3
1414
#manual:
1515
#- flags.manual.blue_0.ManualBlue0

tests/app/flags/__init__.py

Whitespace-only changes.

tests/app/flags/autonomous/__init__.py

Whitespace-only changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import asyncio
2+
from unittest.mock import AsyncMock, MagicMock, patch
3+
4+
import pytest
5+
import yaml
6+
7+
from plugins.training.app.flags.autonomous.blue_2 import AutonomousBlue2
8+
9+
10+
class TestAutonomousBlue2:
11+
"""Tests for the 'Malicious file on system' blue training flag (#167)."""
12+
13+
def test_flag_importable_from_correct_path(self):
14+
"""The flag class must be importable from flags.autonomous.blue_2."""
15+
assert AutonomousBlue2 is not None
16+
assert AutonomousBlue2.name == 'Malicious file on system'
17+
18+
def test_flag_registered_in_blue_certificate_yaml(self):
19+
"""The flag must be uncommented and listed in the Blue Certificate YAML."""
20+
with open('plugins/training/data/certifications/8da8f0b3-194a-4eed-95b0-43c1f1b64091.yml') as f:
21+
cert = yaml.safe_load(f)
22+
autonomous_flags = cert['badges']['autonomous']
23+
assert 'flags.autonomous.blue_2.AutonomousBlue2' in autonomous_flags
24+
25+
def test_flag_has_no_visible_false(self):
26+
"""The flag should not have visible=False which would hide it from users."""
27+
assert not hasattr(AutonomousBlue2, 'visible') or AutonomousBlue2.visible is not False
28+
29+
@pytest.mark.asyncio
30+
async def test_verify_returns_true_when_file_found_and_deleted(self):
31+
"""Flag should be granted when the file is found AND deleted."""
32+
flag = AutonomousBlue2(number=1)
33+
34+
mock_op = MagicMock()
35+
mock_op.ran_ability_id = MagicMock(return_value=True)
36+
37+
mock_fact_hash = MagicMock()
38+
mock_fact_hash.trait = 'file.malicious.hash'
39+
mock_fact_file = MagicMock()
40+
mock_fact_file.trait = 'host.malicious.file'
41+
mock_op.all_facts = AsyncMock(return_value=[mock_fact_hash, mock_fact_file])
42+
43+
mock_data_svc = AsyncMock()
44+
mock_data_svc.locate = AsyncMock(return_value=[mock_op])
45+
services = {'data_svc': mock_data_svc}
46+
47+
result = await flag.verify(services)
48+
assert result is True
49+
50+
@pytest.mark.asyncio
51+
async def test_verify_returns_false_when_file_not_deleted(self):
52+
"""Flag should NOT be granted when file is found but not deleted."""
53+
flag = AutonomousBlue2(number=1)
54+
55+
mock_op = MagicMock()
56+
# ran_ability_id returns True for find, False for delete
57+
def selective_ran(ability_id):
58+
if ability_id == 'f9b3eff0-e11c-48de-9338-1578b351b14b':
59+
return True # file found ability
60+
return False # file delete ability not run
61+
62+
mock_op.ran_ability_id = MagicMock(side_effect=selective_ran)
63+
64+
mock_fact_hash = MagicMock()
65+
mock_fact_hash.trait = 'file.malicious.hash'
66+
mock_fact_file = MagicMock()
67+
mock_fact_file.trait = 'host.malicious.file'
68+
mock_op.all_facts = AsyncMock(return_value=[mock_fact_hash, mock_fact_file])
69+
70+
mock_data_svc = AsyncMock()
71+
mock_data_svc.locate = AsyncMock(return_value=[mock_op])
72+
services = {'data_svc': mock_data_svc}
73+
74+
result = await flag.verify(services)
75+
assert result is False
76+
77+
@pytest.mark.asyncio
78+
async def test_verify_returns_false_when_no_operations(self):
79+
"""Flag should NOT be granted when there are no Blue Autonomous operations."""
80+
flag = AutonomousBlue2(number=1)
81+
82+
mock_data_svc = AsyncMock()
83+
mock_data_svc.locate = AsyncMock(return_value=[])
84+
services = {'data_svc': mock_data_svc}
85+
86+
result = await flag.verify(services)
87+
assert result is False

0 commit comments

Comments
 (0)