Skip to content

Commit 4d5baff

Browse files
argaudreauArsenull
authored andcommitted
Add plugin field to adversaries, abilities, and planners (#2345)
* Add plugin field to adversaries, abilities, sources, and planners
1 parent 24f098f commit 4d5baff

File tree

13 files changed

+49
-44
lines changed

13 files changed

+49
-44
lines changed

app/objects/c_ability.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import collections
2-
import os
32
import uuid
43

54
import marshmallow as ma
@@ -8,7 +7,6 @@
87
from app.objects.secondclass.c_executor import ExecutorSchema
98
from app.objects.secondclass.c_requirement import RequirementSchema
109
from app.utility.base_object import BaseObject
11-
from app.utility.base_service import BaseService
1210
from app.utility.base_world import AccessSchema
1311

1412

@@ -27,6 +25,7 @@ class AbilitySchema(ma.Schema):
2725
additional_info = ma.fields.Dict(keys=ma.fields.String(), values=ma.fields.String())
2826
access = ma.fields.Nested(AccessSchema, missing=None)
2927
singleton = ma.fields.Bool(missing=None)
28+
plugin = ma.fields.String(missing=None)
3029

3130
@ma.pre_load
3231
def fix_id(self, data, **_):
@@ -58,7 +57,7 @@ def executors(self):
5857

5958
def __init__(self, ability_id='', name=None, description=None, tactic=None, technique_id=None, technique_name=None,
6059
executors=(), requirements=None, privilege=None, repeatable=False, buckets=None, access=None,
61-
additional_info=None, tags=None, singleton=False, **kwargs):
60+
additional_info=None, tags=None, singleton=False, plugin='', **kwargs):
6261
super().__init__()
6362
self.ability_id = ability_id if ability_id else str(uuid.uuid4())
6463
self.tactic = tactic.lower() if tactic else None
@@ -80,6 +79,7 @@ def __init__(self, ability_id='', name=None, description=None, tactic=None, tech
8079
self.additional_info = additional_info or dict()
8180
self.additional_info.update(**kwargs)
8281
self.tags = set(tags) if tags else set()
82+
self.plugin = plugin
8383

8484
def __getattr__(self, item):
8585
try:
@@ -103,14 +103,11 @@ def store(self, ram):
103103
existing.update('buckets', self.buckets)
104104
existing.update('tags', self.tags)
105105
existing.update('singleton', self.singleton)
106+
existing.update('plugin', self.plugin)
106107
return existing
107108

108109
async def which_plugin(self):
109-
file_svc = BaseService.get_service('file_svc')
110-
for plugin in os.listdir('plugins'):
111-
if await file_svc.walk_file_path(os.path.join('plugins', plugin, 'data', ''), '%s.yml' % self.ability_id):
112-
return plugin
113-
return None
110+
return self.plugin
114111

115112
def find_executor(self, name, platform):
116113
return self._executor_map.get(self._make_executor_map_key(name, platform))

app/objects/c_adversary.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import os
21
import uuid
32

43
import marshmallow as ma
54

65
from app.objects.interfaces.i_object import FirstClassObjectInterface
76
from app.utility.base_object import BaseObject
8-
from app.utility.base_service import BaseService
97

108

119
DEFAULT_OBJECTIVE_ID = '495a9828-cab1-44dd-a0ca-66e58177d8cc'
@@ -20,6 +18,7 @@ class AdversarySchema(ma.Schema):
2018
objective = ma.fields.String()
2119
tags = ma.fields.List(ma.fields.String(), allow_none=True)
2220
has_repeatable_abilities = ma.fields.Boolean(dump_only=True)
21+
plugin = ma.fields.String(missing=None)
2322

2423
@ma.pre_load
2524
def fix_id(self, adversary, **_):
@@ -57,7 +56,7 @@ class Adversary(FirstClassObjectInterface, BaseObject):
5756
def unique(self):
5857
return self.hash('%s' % self.adversary_id)
5958

60-
def __init__(self, name='', adversary_id='', description='', atomic_ordering=(), objective='', tags=None):
59+
def __init__(self, name='', adversary_id='', description='', atomic_ordering=(), objective='', tags=None, plugin=''):
6160
super().__init__()
6261
self.adversary_id = adversary_id if adversary_id else str(uuid.uuid4())
6362
self.name = name
@@ -66,6 +65,7 @@ def __init__(self, name='', adversary_id='', description='', atomic_ordering=(),
6665
self.objective = objective or DEFAULT_OBJECTIVE_ID
6766
self.tags = set(tags) if tags else set()
6867
self.has_repeatable_abilities = False
68+
self.plugin = plugin
6969

7070
def store(self, ram):
7171
existing = self.retrieve(ram['adversaries'], self.unique)
@@ -78,6 +78,7 @@ def store(self, ram):
7878
existing.update('objective', self.objective)
7979
existing.update('tags', self.tags)
8080
existing.update('has_repeatable_abilities', self.check_repeatable_abilities(ram['abilities']))
81+
existing.update('plugin', self.plugin)
8182
return existing
8283

8384
def verify(self, log, abilities, objectives):
@@ -101,11 +102,7 @@ def has_ability(self, ability):
101102
return False
102103

103104
async def which_plugin(self):
104-
file_svc = BaseService.get_service('file_svc')
105-
for plugin in os.listdir('plugins'):
106-
if await file_svc.walk_file_path(os.path.join('plugins', plugin, 'data', ''), '%s.yml' % self.adversary_id):
107-
return plugin
108-
return None
105+
return self.plugin
109106

110107
def check_repeatable_abilities(self, ability_list):
111108
return any(ab.repeatable for ab_id in self.atomic_ordering for ab in ability_list if ab.ability_id == ab_id)

app/objects/c_planner.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import os
21
import uuid
32

43
import marshmallow as ma
54

65
from app.objects.interfaces.i_object import FirstClassObjectInterface
76
from app.utility.base_object import BaseObject
8-
from app.utility.base_service import BaseService
97
from app.objects.secondclass.c_fact import Fact, FactSchema
108

119

@@ -18,6 +16,7 @@ class PlannerSchema(ma.Schema):
1816
stopping_conditions = ma.fields.List(ma.fields.Nested(FactSchema()))
1917
ignore_enforcement_modules = ma.fields.List(ma.fields.String())
2018
allow_repeatable_abilities = ma.fields.Boolean()
19+
plugin = ma.fields.String(missing=None)
2120

2221
@ma.post_load()
2322
def build_planner(self, data, **kwargs):
@@ -34,7 +33,7 @@ def unique(self):
3433
return self.hash(self.name)
3534

3635
def __init__(self, name='', planner_id='', module='', params=None, stopping_conditions=None, description=None,
37-
ignore_enforcement_modules=(), allow_repeatable_abilities=False):
36+
ignore_enforcement_modules=(), allow_repeatable_abilities=False, plugin=''):
3837
super().__init__()
3938
self.name = name
4039
self.planner_id = planner_id if planner_id else str(uuid.uuid4())
@@ -44,6 +43,7 @@ def __init__(self, name='', planner_id='', module='', params=None, stopping_cond
4443
self.stopping_conditions = self._set_stopping_conditions(stopping_conditions)
4544
self.ignore_enforcement_modules = ignore_enforcement_modules
4645
self.allow_repeatable_abilities = allow_repeatable_abilities
46+
self.plugin = plugin
4747

4848
def store(self, ram):
4949
existing = self.retrieve(ram['planners'], self.unique)
@@ -53,14 +53,11 @@ def store(self, ram):
5353
else:
5454
existing.update('stopping_conditions', self.stopping_conditions)
5555
existing.update('params', self.params)
56+
existing.update('plugin', self.plugin)
5657
return existing
5758

5859
async def which_plugin(self):
59-
file_svc = BaseService.get_service('file_svc')
60-
for plugin in os.listdir('plugins'):
61-
if await file_svc.walk_file_path(os.path.join('plugins', plugin, 'data', ''), '%s.yml' % self.planner_id):
62-
return plugin
63-
return None
60+
return self.plugin
6461

6562
@staticmethod
6663
def _set_stopping_conditions(conditions):

app/objects/c_source.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SourceSchema(ma.Schema):
3333
rules = ma.fields.List(ma.fields.Nested(RuleSchema))
3434
adjustments = ma.fields.List(ma.fields.Nested(AdjustmentSchema))
3535
relationships = ma.fields.List(ma.fields.Nested(RelationshipSchema))
36+
plugin = ma.fields.String(missing=None)
3637

3738
@ma.pre_load
3839
def fix_adjustments(self, in_data, **_):
@@ -81,14 +82,15 @@ class Source(FirstClassObjectInterface, BaseObject):
8182
def unique(self):
8283
return self.hash('%s' % self.id)
8384

84-
def __init__(self, name='', id='', facts=(), relationships=(), rules=(), adjustments=()):
85+
def __init__(self, name='', id='', facts=(), relationships=(), rules=(), adjustments=(), plugin=''):
8586
super().__init__()
8687
self.id = id if id else str(uuid.uuid4())
8788
self.name = name
8889
self.facts = facts
8990
self.rules = rules
9091
self.adjustments = adjustments
9192
self.relationships = relationships
93+
self.plugin = plugin
9294

9395
def store(self, ram):
9496
existing = self.retrieve(ram['sources'], self.unique)
@@ -99,4 +101,5 @@ def store(self, ram):
99101
existing.update('facts', self.facts)
100102
existing.update('rules', self.rules)
101103
existing.update('relationships', self.relationships)
104+
existing.update('plugin', self.plugin)
102105
return existing

app/service/data_svc.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import tarfile
88
import shutil
99
import warnings
10+
import pathlib
1011
from importlib import import_module
1112

1213
from app.objects.c_ability import Ability
@@ -160,14 +161,16 @@ async def load_ability_file(self, filename, access):
160161
requirements = await self._load_ability_requirements(ab.pop('requirements', []))
161162
buckets = ab.pop('buckets', [tactic])
162163
ab.pop('access', None)
164+
plugin = self._get_plugin_name(filename)
165+
ab.pop('plugin', plugin)
163166

164167
if tactic and tactic not in filename:
165168
self.log.error('Ability=%s has wrong tactic' % id)
166169

167170
await self._create_ability(ability_id=ability_id, name=name, description=description, tactic=tactic,
168171
technique_id=technique_id, technique_name=technique_name,
169172
executors=executors, requirements=requirements, privilege=privilege,
170-
repeatable=repeatable, buckets=buckets, access=access, singleton=singleton,
173+
repeatable=repeatable, buckets=buckets, access=access, singleton=singleton, plugin=plugin,
171174
**ab)
172175

173176
async def convert_v0_ability_executor(self, ability_data: dict):
@@ -239,6 +242,7 @@ async def load_yaml_file(self, object_class, filename, access):
239242
for src in self.strip_yml(filename):
240243
obj = object_class.load(src)
241244
obj.access = access
245+
obj.plugin = self._get_plugin_name(filename)
242246
await self.store(obj)
243247

244248
async def _load(self, plugins=()):
@@ -336,11 +340,11 @@ async def _load_data_encoders(self, plugins):
336340

337341
async def _create_ability(self, ability_id, name=None, description=None, tactic=None, technique_id=None,
338342
technique_name=None, executors=None, requirements=None, privilege=None,
339-
repeatable=False, buckets=None, access=None, singleton=False, **kwargs):
343+
repeatable=False, buckets=None, access=None, singleton=False, plugin='', **kwargs):
340344
ability = Ability(ability_id=ability_id, name=name, description=description, tactic=tactic,
341345
technique_id=technique_id, technique_name=technique_name, executors=executors,
342346
requirements=requirements, privilege=privilege, repeatable=repeatable, buckets=buckets,
343-
access=access, singleton=singleton, **kwargs)
347+
access=access, singleton=singleton, plugin=plugin, **kwargs)
344348
return await self.store(ability)
345349

346350
async def _prune_non_critical_data(self):
@@ -410,3 +414,7 @@ async def _verify_default_objective_exists(self):
410414
async def _verify_adversary_profiles(self):
411415
for adv in await self.locate('adversaries'):
412416
adv.verify(log=self.log, abilities=self.ram['abilities'], objectives=self.ram['objectives'])
417+
418+
def _get_plugin_name(self, filename):
419+
plugin_path = pathlib.PurePath(filename).parts
420+
return plugin_path[1] if 'plugins' in plugin_path else ''

static/js/core.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ function alpineCore() {
2828
if (tabName === 'fieldmanual') {
2929
restRequest('GET', null, (data) => { this.setTabContent({ name: tabName, contentID: `tab-${tabName}`, address: address }, data); }, address);
3030
return;
31-
} else if (tabName === 'stockpile' || tabName === 'atomic') {
32-
return;
3331
}
3432

3533
// If tab is already open, jump to it

templates/BLUE.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<ul class="menu-list">
4747
{% for plugin in plugins | sort(attribute='name') %}
4848
<li>
49-
<a href="#home" x-on:click="addTab('{{ plugin.name }}', '{{ plugin.address }}')" class="nav-item" x-bind:class="{ 'disabled': '{{ plugin.name }}' === 'atomic' || '{{ plugin.name }}' === 'stockpile' }">
49+
<a href="#home" x-on:click="addTab('{{ plugin.name }}', '{{ plugin.address }}')" class="nav-item">
5050
{{ plugin.name }}
5151
<template x-if="`{{plugin.name | e}}` === 'fieldmanual'">
5252
<sup><i class="fas fa-external-link-alt pl-1 is-size-7"></i></sup>

templates/RED.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<ul class="menu-list">
4747
{% for plugin in plugins | sort(attribute='name') %}
4848
<li>
49-
<a href="#home" x-on:click="addTab('{{ plugin.name }}', '{{ plugin.address }}')" class="nav-item" x-bind:class="{ 'disabled': '{{ plugin.name }}' === 'atomic' || '{{ plugin.name }}' === 'stockpile' }">
49+
<a href="#home" x-on:click="addTab('{{ plugin.name }}', '{{ plugin.address }}')" class="nav-item">
5050
{{ plugin.name }}
5151
<template x-if="`{{plugin.name | e}}` === 'fieldmanual'">
5252
<sup><i class="fas fa-external-link-alt pl-1 is-size-7"></i></sup>

tests/api/v2/handlers/test_abilities_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ def new_ability_payload():
2323
'privilege': '',
2424
'repeatable': False,
2525
'requirements': [],
26-
'singleton': False
26+
'singleton': False,
27+
'plugin': ''
2728
}
2829

2930

3031
@pytest.fixture
3132
def updated_ability_payload(test_ability):
3233
ability_data = test_ability.schema.dump(test_ability)
33-
ability_data.update(dict(name='an updated test ability', tactic='defense-evasion'))
34+
ability_data.update(dict(name='an updated test ability', tactic='defense-evasion', plugin=''))
3435
return ability_data
3536

3637

@@ -39,15 +40,15 @@ def replaced_ability_payload(test_ability):
3940
ability_data = test_ability.schema.dump(test_ability)
4041
test_executor_linux = Executor(name='sh', platform='linux', command='whoami')
4142
ability_data.update(dict(name='replaced test ability', tactic='collection', technique_name='discovery',
42-
technique_id='2', executors=[ExecutorSchema().dump(test_executor_linux)]))
43+
technique_id='2', executors=[ExecutorSchema().dump(test_executor_linux)], plugin=''))
4344
return ability_data
4445

4546

4647
@pytest.fixture
4748
def test_ability(loop, api_v2_client, executor):
4849
executor_linux = executor(name='sh', platform='linux')
4950
ability = Ability(ability_id='123', name='Test Ability', executors=[executor_linux],
50-
technique_name='collection', technique_id='1', description='', privilege='', tactic='discovery')
51+
technique_name='collection', technique_id='1', description='', privilege='', tactic='discovery', plugin='testplugin')
5152
loop.run_until_complete(BaseService.get_service('data_svc').store(ability))
5253
return ability
5354

tests/api/v2/handlers/test_adversaries_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def new_adversary_payload():
3232
'adversary_id': '456',
3333
'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc',
3434
'tags': [],
35-
'atomic_ordering': []
35+
'atomic_ordering': [],
36+
'plugin': ''
3637
}
3738

3839

@@ -49,7 +50,8 @@ def test_adversary(loop):
4950
'adversary_id': '123',
5051
'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc',
5152
'tags': [],
52-
'atomic_ordering': []}
53+
'atomic_ordering': [],
54+
'plugin': ''}
5355
test_adversary = AdversarySchema().load(expected_adversary)
5456
loop.run_until_complete(BaseService.get_service('data_svc').store(test_adversary))
5557
return test_adversary

tests/api/v2/handlers/test_planners_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
@pytest.fixture
1010
def test_planner(loop, api_v2_client):
11-
planner = Planner(name="test planner", planner_id="123", description="a test planner")
11+
planner = Planner(name="test planner", planner_id="123", description="a test planner", plugin="planner")
1212
loop.run_until_complete(BaseService.get_service('data_svc').store(planner))
1313
return planner
1414

tests/api/v2/handlers/test_sources_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def new_source_payload():
3434
'name': 'new test source',
3535
'facts': [fact],
3636
'rules': [rule.schema.dump(rule)],
37-
'relationships': [relationship]
37+
'relationships': [relationship],
38+
'plugin': ''
3839
}
3940
return source
4041

@@ -80,6 +81,7 @@ def expected_updated_source_dump(updated_source_payload, mocker, mock_time):
8081
source = SourceSchema().load(updated_source_payload)
8182
dumped_obj = source.display_schema.dump(source)
8283
dumped_obj['relationships'][0]['unique'] = mock.ANY
84+
dumped_obj['plugin'] = ''
8385
return dumped_obj
8486

8587

tests/services/test_rest_svc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ def test_delete_operation(self, loop, rest_svc, data_svc):
7575
'adversary': {'description': 'an empty adversary profile', 'name': 'ad-hoc',
7676
'adversary_id': 'ad-hoc', 'atomic_ordering': [],
7777
'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc',
78-
'tags': [], 'has_repeatable_abilities': False}, 'state': 'finished',
78+
'tags': [], 'has_repeatable_abilities': False, 'plugin': None}, 'state': 'finished',
7979
'planner': {'name': 'test', 'description': None, 'module': 'test',
8080
'stopping_conditions': [], 'params': {}, 'allow_repeatable_abilities': False,
81-
'ignore_enforcement_modules': [], 'id': '123'}, 'jitter': '2/8',
81+
'ignore_enforcement_modules': [], 'id': '123', 'plugin': ''}, 'jitter': '2/8',
8282
'host_group': [{'trusted': True, 'architecture': 'unknown', 'watchdog': 0,
8383
'contact': 'unknown', 'username': 'unknown', 'links': [], 'sleep_max': 8,
8484
'exe_name': 'unknown', 'executors': ['pwsh', 'psh'], 'ppid': 0,
@@ -147,11 +147,11 @@ def test_create_operation(self, loop, rest_svc, data_svc):
147147
want = {'name': 'Test',
148148
'adversary': {'description': 'an empty adversary profile', 'name': 'ad-hoc', 'adversary_id': 'ad-hoc',
149149
'atomic_ordering': [], 'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc', 'tags': [],
150-
'has_repeatable_abilities': False},
150+
'has_repeatable_abilities': False, 'plugin': None},
151151
'state': 'finished',
152152
'planner': {'name': 'test', 'description': None, 'module': 'test', 'stopping_conditions': [],
153153
'params': {},
154-
'ignore_enforcement_modules': [], 'id': '123', 'allow_repeatable_abilities': False},
154+
'ignore_enforcement_modules': [], 'id': '123', 'allow_repeatable_abilities': False, 'plugin': ''},
155155
'jitter': '2/8',
156156
'group': '',
157157
'source': '',

0 commit comments

Comments
 (0)