| import uuid |
| |
| import marshmallow as ma |
| |
| from app.objects.interfaces.i_object import FirstClassObjectInterface |
| from app.utility.base_object import BaseObject |
| |
| |
| DEFAULT_OBJECTIVE_ID = '495a9828-cab1-44dd-a0ca-66e58177d8cc' |
| |
| |
| class AdversarySchema(ma.Schema): |
| |
| class Meta: |
| unknown = ma.EXCLUDE |
| |
| adversary_id = ma.fields.String() |
| name = ma.fields.String() |
| description = ma.fields.String() |
| atomic_ordering = ma.fields.List(ma.fields.String()) |
| objective = ma.fields.String() |
| tags = ma.fields.List(ma.fields.String(), allow_none=True) |
| has_repeatable_abilities = ma.fields.Boolean(dump_only=True) |
| plugin = ma.fields.String(load_default=None) |
| |
| @ma.pre_load |
| def fix_id(self, adversary, **_): |
| if 'id' in adversary: |
| adversary['adversary_id'] = adversary.pop('id') |
| return adversary |
| |
| @ma.pre_load |
| def phase_to_atomic_ordering(self, adversary, **_): |
| """ |
| Convert legacy adversary phases to atomic ordering |
| """ |
| if 'phases' in adversary and 'atomic_ordering' in adversary: |
| raise ma.ValidationError('atomic_ordering and phases cannot be used at the same time', 'phases', adversary) |
| elif 'phases' in adversary: |
| adversary['atomic_ordering'] = [ab_id for phase in adversary.get('phases', {}).values() for ab_id in phase] |
| del adversary['phases'] |
| return adversary |
| |
| @ma.pre_load |
| def remove_properties(self, data, **_): |
| data.pop('has_repeatable_abilities', None) |
| return data |
| |
| @ma.post_load |
| def build_adversary(self, data, **kwargs): |
| return None if kwargs.get('partial') is True else Adversary(**data) |
| |
| |
| class Adversary(FirstClassObjectInterface, BaseObject): |
| |
| schema = AdversarySchema() |
| |
| @property |
| def unique(self): |
| return self.hash('%s' % self.adversary_id) |
| |
| def __init__(self, name='', adversary_id='', description='', atomic_ordering=(), objective='', tags=None, plugin=''): |
| super().__init__() |
| self.adversary_id = adversary_id if adversary_id else str(uuid.uuid4()) |
| self.name = name |
| self.description = description |
| self.atomic_ordering = atomic_ordering |
| self.objective = objective or DEFAULT_OBJECTIVE_ID |
| self.tags = set(tags) if tags else set() |
| self.has_repeatable_abilities = False |
| self.plugin = plugin |
| |
| def store(self, ram): |
| existing = self.retrieve(ram['adversaries'], self.unique) |
| if not existing: |
| ram['adversaries'].append(self) |
| return self.retrieve(ram['adversaries'], self.unique) |
| existing.update('name', self.name) |
| existing.update('description', self.description) |
| existing.update('atomic_ordering', self.atomic_ordering) |
| existing.update('objective', self.objective) |
| existing.update('tags', self.tags) |
| existing.update('has_repeatable_abilities', self.check_repeatable_abilities(ram['abilities'])) |
| existing.update('plugin', self.plugin) |
| return existing |
| |
| def verify(self, log, abilities, objectives): |
| for ability_id in self.atomic_ordering: |
| if not next((ability for ability in abilities if ability.ability_id == ability_id), None): |
| log.warning('Ability referenced in adversary %s but not found: %s', self.adversary_id, ability_id) |
| |
| if not self.objective: |
| self.objective = DEFAULT_OBJECTIVE_ID |
| elif not next((objective for objective in objectives if objective.id == self.objective), None): |
| log.warning('Objective referenced in adversary %s but not found: %s. Setting default objective.', |
| self.adversary_id, self.objective) |
| self.objective = DEFAULT_OBJECTIVE_ID |
| |
| self.has_repeatable_abilities = self.check_repeatable_abilities(abilities) |
| |
| def has_ability(self, ability): |
| for a in self.atomic_ordering: |
| if ability == a: |
| return True |
| return False |
| |
| async def which_plugin(self): |
| return self.plugin |
| |
| def check_repeatable_abilities(self, ability_list): |
| return any(ab.repeatable for ab_id in self.atomic_ordering for ab in ability_list if ab.ability_id == ab_id) |