Esempio n. 1
0
class ADPayload(Payload):
    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)
    host_name = Column(String, nullable=False)
    user_name = Column(String, nullable=False)
    password = Column(String, nullable=False)
    ad_organizational_unit = Column(String, nullable=False)
    ad_mount_style = Column(DBEnum(ADMountStyle), nullable=False)
    ad_default_user_shell = Column(String)
    ad_map_uid_attribute = Column(String)
    ad_map_gid_attribute = Column(String)
    ad_map_ggid_attribute = Column(String)
    ad_preferred_dc_server = Column(String)
    ad_domain_admin_group_list = Column(String)  # JSON
    ad_namespace = Column(DBEnum(ADNamespace), default=ADNamespace.Domain)
    ad_packet_sign = Column(DBEnum(ADPacketSignPolicy),
                            default=ADPacketSignPolicy.Allow)
    ad_packet_encrypt = Column(DBEnum(ADPacketEncryptPolicy),
                               default=ADPacketEncryptPolicy.Allow)
    ad_restrict_ddns = Column(String)  # JSON
    ad_trust_change_pass_interval = Column(Integer)

    # We will take null to mean that the flag is not set
    ad_create_mobile_account_at_login = Column(Boolean)
    ad_warn_user_before_creating_ma = Column(Boolean)
    ad_force_home_local = Column(Boolean)

    __mapper_args__ = {
        'polymorphic_identity': 'com.apple.DirectoryService.managed',
    }
Esempio n. 2
0
class RamModule(JoinedComponentTableMixin, Component):
    """A stick of RAM."""
    size = Column(SmallInteger, check_range('size', min=128, max=17000))
    size.comment = """The capacity of the RAM stick."""
    speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
    interface = Column(DBEnum(RamInterface))
    format = Column(DBEnum(RamFormat))
Esempio n. 3
0
class Image(Thing):
    id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    name = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
    content = db.Column(db.LargeBinary, nullable=False)
    file_format = db.Column(DBEnum(ImageMimeTypes), nullable=False)
    orientation = db.Column(DBEnum(Orientation), nullable=False)
    image_list_id = Column(UUID(as_uuid=True), ForeignKey(ImageList.id), nullable=False)
    image_list = relationship(ImageList,
                              primaryjoin=ImageList.id == image_list_id,
                              backref=backref('images',
                                              cascade=CASCADE_OWN,
                                              order_by=lambda: Image.created,
                                              collection_class=OrderedSet))
Esempio n. 4
0
class WorkbenchRate(IndividualRate):
    id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
    processor = Column(Float(decimal_return_scale=2),
                       check_range('processor', *RATE_POSITIVE))
    ram = Column(Float(decimal_return_scale=2),
                 check_range('ram', *RATE_POSITIVE))
    data_storage = Column(Float(decimal_return_scale=2),
                          check_range('data_storage', *RATE_POSITIVE))
    graphic_card = Column(Float(decimal_return_scale=2),
                          check_range('graphic_card', *RATE_POSITIVE))
    labelling = Column(Boolean)
    bios = Column(DBEnum(Bios))
    appearance_range = Column(DBEnum(AppearanceRange))
    functionality_range = Column(DBEnum(FunctionalityRange))
Esempio n. 5
0
class DataStorage(JoinedComponentTableMixin, Component):
    """A device that stores information."""
    size = Column(Integer, check_range('size', min=1, max=10**8))
    size.comment = """
        The size of the data-storage in MB.
    """
    interface = Column(DBEnum(DataStorageInterface))

    @property
    def privacy(self):
        """Returns the privacy compliance state of the data storage.

        This is, the last erasure performed to the data storage.
        """
        from ereuse_devicehub.resources.event.models import EraseBasic
        try:
            ev = self.last_event_of(EraseBasic)
        except LookupError:
            ev = None
        return ev

    def __format__(self, format_spec):
        v = super().__format__(format_spec)
        if 's' in format_spec:
            v += ' – {} GB'.format(self.size // 1000 if self.size else '?')
        return v
Esempio n. 6
0
class WIFIPayload(Payload):
    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)
    ssid_str = Column(String, nullable=False)
    hidden_network = Column(Boolean, default=False)
    auto_join = Column(Boolean, nullable=True)
    encryption_type = Column(DBEnum(WIFIEncryptionType),
                             default=WIFIEncryptionType.Any)
    is_hotspot = Column(Boolean)
    domain_name = Column(String)
    service_provider_roaming_enabled = Column(Boolean)

    roaming_consortium_ois = Column(String)  # JSON
    nai_realm_names = Column(String)  # JSON
    mccs_and_mncs = Column(String)  # JSON
    displayed_operator_name = Column(String)
    captive_bypass = Column(Boolean)

    # If WEP, WPA or Any
    password = Column(String)
    #eap_client_configuration_id = Column(Integer, ForeignKey('eap_client_configurations.id'))
    tls_certificate_required = Column(Boolean)
    payload_certificate_uuid = Column(GUID)

    # Manual Proxy
    proxy_type = Column(String)
    proxy_server = Column(String)
    proxy_server_port = Column(Integer)
    proxy_username = Column(String)
    proxy_password = Column(String)
    proxy_pac_url = Column(String)
    proxy_pac_fallback_allowed = Column(Boolean)

    __mapper_args__ = {
        'polymorphic_identity': 'com.apple.wifi.managed',
    }
Esempio n. 7
0
class Profile(db.Model):
    """Top level profile.

    In Commandment, multiple profiles may have an association with the same payload.

    See Also:
          - `Configuration Profile Keys
            <https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW7>`_.

    Attributes:

    """
    __tablename__ = 'profiles'

    id = Column(Integer, primary_key=True)
    description = Column(Text)
    display_name = Column(String)
    expiration_date = Column(DateTime)  # Only for old style OTA
    identifier = Column(String, nullable=False)
    organization = Column(String)
    uuid = Column(GUID, index=True, default=uuid4())
    removal_disallowed = Column(Boolean)
    version = Column(Integer, default=1)
    scope = Column(DBEnum(PayloadScope), default=PayloadScope.User.value)
    removal_date = Column(DateTime)
    duration_until_removal = Column(BigInteger)
    consent_en = Column(Text)
    is_encrypted = Column(Boolean, default=False)

    payloads = relationship('Payload',
                            secondary=profile_payloads,
                            backref='profiles')
Esempio n. 8
0
class EmailPayload(Payload):
    """E-mail Payload"""
    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)
    email_account_description = Column(String)
    email_account_name = Column(String)
    email_account_type = Column(DBEnum(EmailAccountType), nullable=False)
    email_address = Column(String)
    incoming_auth = Column(DBEnum(EmailAuthenticationType), nullable=False)
    incoming_host = Column(String, nullable=False)
    incoming_port = Column(Integer)
    incoming_use_ssl = Column(Boolean, default=False)
    incoming_username = Column(String, nullable=False)
    incoming_password = Column(String)
    outgoing_password = Column(String)
    outgoing_incoming_same = Column(Boolean)
    outgoing_auth = Column(DBEnum(EmailAuthenticationType), nullable=False)

    __mapper_args__ = {'polymorphic_identity': 'com.apple.mail.managed'}
Esempio n. 9
0
class Agent(Thing):
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    type = Column(Unicode, nullable=False)
    name = Column(CIText())
    name.comment = """
        The name of the organization or person.
    """
    tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id'))
    tax_id.comment = """
        The Tax / Fiscal ID of the organization, 
        e.g. the TIN in the US or the CIF/NIF in Spain.
    """
    country = Column(DBEnum(enums.Country))
    country.comment = """
        Country issuing the tax_id number.
    """
    telephone = Column(PhoneNumberType())
    email = Column(EmailType, unique=True)

    __table_args__ = (UniqueConstraint(
        tax_id, country, name='Registration Number per country.'),
                      UniqueConstraint(tax_id,
                                       name,
                                       name='One tax ID with one name.'),
                      db.Index('agent_type', type, postgresql_using='hash'))

    @declared_attr
    def __mapper_args__(cls):
        """
        Defines inheritance.

        From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
        extensions/declarative/api.html
        #sqlalchemy.ext.declarative.declared_attr>`_
        """
        args = {POLYMORPHIC_ID: cls.t}
        if cls.t == 'Agent':
            args[POLYMORPHIC_ON] = cls.type
        if JoinedTableMixin in cls.mro():
            args[INHERIT_COND] = cls.id == Agent.id
        return args

    @property
    def events(self) -> list:
        # todo test
        return sorted(chain(self.events_agent, self.events_to),
                      key=attrgetter('created'))

    @validates('name')
    def does_not_contain_slash(self, _, value: str):
        if '/' in value:
            raise ValidationError('Name cannot contain slash \'')
        return value

    def __repr__(self) -> str:
        return '<{0.t} {0.name}>'.format(self)
Esempio n. 10
0
class Printer(Device):
    id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
    wireless = Column(Boolean, nullable=False, default=False)
    wireless.comment = """Whether it is a wireless printer."""
    scanning = Column(Boolean, nullable=False, default=False)
    scanning.comment = """Whether the printer has scanning capabilities."""
    technology = Column(DBEnum(PrinterTechnology))
    technology.comment = """Technology used to print."""
    monochrome = Column(Boolean, nullable=False, default=True)
    monochrome.comment = """Whether the printer is only monochrome."""
Esempio n. 11
0
class VPNPayload(Payload):
    """VPN Payload"""
    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)
    user_defined_name = Column(String)
    override_primary = Column(Boolean, default=False)
    vpn_type = Column(DBEnum(VPNType), nullable=False)
    vpn_sub_type = Column(String)
    provider_bundle_identifier = Column(String)
    on_demand_enabled = Column(Integer)

    __mapper_args__ = {
        'polymorphic_identity': 'com.apple.vpn.managed',
    }
Esempio n. 12
0
class Rate(JoinedTableMixin, EventWithOneDevice):
    rating = Column(Float(decimal_return_scale=2),
                    check_range('rating', *RATE_POSITIVE))
    algorithm_software = Column(DBEnum(RatingSoftware), nullable=False)
    algorithm_version = Column(StrictVersionType, nullable=False)
    appearance = Column(Float(decimal_return_scale=2),
                        check_range('appearance', *RATE_NEGATIVE))
    functionality = Column(Float(decimal_return_scale=2),
                           check_range('functionality', *RATE_NEGATIVE))

    @property
    def rating_range(self) -> RatingRange:
        return RatingRange.from_score(self.rating)
Esempio n. 13
0
class TestDataStorage(Test):
    id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
    length = Column(DBEnum(TestHardDriveLength),
                    nullable=False)  # todo from type
    status = Column(Unicode(STR_SIZE), nullable=False)
    lifetime = Column(Interval, nullable=False)
    first_error = Column(SmallInteger, nullable=False, default=0)
    passed_lifetime = Column(Interval)
    assessment = Column(Boolean)
    reallocated_sector_count = Column(SmallInteger)
    power_cycle_count = Column(SmallInteger)
    reported_uncorrectable_errors = Column(SmallInteger)
    command_timeout = Column(SmallInteger)
    current_pending_sector_count = Column(SmallInteger)
    offline_uncorrectable = Column(SmallInteger)
    remaining_lifetime_percentage = Column(SmallInteger)
Esempio n. 14
0
class ADCertPayload(Payload):
    id = Column(Integer, ForeignKey('payloads.id'), primary_key=True)
    certificate_description = Column(
        String)  # Description was reserved from the base Payload table
    allow_all_apps_access = Column(Boolean)
    cert_server = Column(String, nullable=False)
    cert_template = Column(String, nullable=False, default='User')
    acquisition_mechanism = Column(
        DBEnum(ADCertificateAcquisitionMechanism),
        default=ADCertificateAcquisitionMechanism.RPC)
    certificate_authority = Column(String, nullable=False)
    renewal_time_interval = Column(Integer)
    identity_description = Column(String, nullable=True)
    key_is_extractable = Column(Boolean, default=False)
    prompt_for_credentials = Column(Boolean)
    keysize = Column(Integer, default=2048)

    __mapper_args__ = {
        'polymorphic_identity': 'com.apple.ADCertificate.managed',
    }
Esempio n. 15
0
class Snapshot(JoinedTableMixin, EventWithOneDevice):
    uuid = Column(UUID(as_uuid=True), nullable=False, unique=True)
    version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
    software = Column(DBEnum(SnapshotSoftware), nullable=False)
    elapsed = Column(Interval, nullable=False)
    expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
Esempio n. 16
0
class DisplayMixin:
    """Base class for the Display Component and the Monitor Device."""
    size = Column(Float(decimal_return_scale=1),
                  check_range('size', 2, 150),
                  nullable=False)
    size.comment = """
        The size of the monitor in inches.
    """
    technology = Column(DBEnum(DisplayTech))
    technology.comment = """
        The technology the monitor uses to display the image.
    """
    resolution_width = Column(SmallInteger,
                              check_range('resolution_width', 10, 20000),
                              nullable=False)
    resolution_width.comment = """
        The maximum horizontal resolution the monitor can natively support
        in pixels.
    """
    resolution_height = Column(SmallInteger,
                               check_range('resolution_height', 10, 20000),
                               nullable=False)
    resolution_height.comment = """
        The maximum vertical resolution the monitor can natively support
        in pixels.
    """
    refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000))
    contrast_ratio = Column(SmallInteger,
                            check_range('contrast_ratio', 100, 100000))
    touchable = Column(Boolean)
    touchable.comment = """Whether it is a touchscreen."""

    @hybrid_property
    def aspect_ratio(self):
        """The aspect ratio of the display, as a fraction: ``X/Y``.

        Regular values are ``4/3``, ``5/4``, ``16/9``, ``21/9``,
        ``14/10``, ``19/10``, ``16/10``.
        """
        return Fraction(self.resolution_width, self.resolution_height)

    # noinspection PyUnresolvedReferences
    @aspect_ratio.expression
    def aspect_ratio(cls):
        # The aspect ratio to use as SQL in the DB
        # This allows comparing resolutions
        return db.func.round(cls.resolution_width / cls.resolution_height, 2)

    @hybrid_property
    def widescreen(self):
        """Whether the monitor is considered to be widescreen.

        Widescreen monitors are those having a higher aspect ratio
        greater than 4/3.
        """
        # We add a tiny extra to 4/3 to avoid precision errors
        return self.aspect_ratio > 4.001 / 3

    def __str__(self) -> str:
        return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
            self)

    def __format__(self, format_spec: str) -> str:
        v = ''
        if 't' in format_spec:
            v += '{0.t} {0.model}'.format(self)
        if 's' in format_spec:
            v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
            v += '– {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
        return v
Esempio n. 17
0
class Computer(Device):
    """A chassis with components inside that can be processed
    automatically with Workbench Computer.

    Computer is broadly extended by ``Desktop``, ``Laptop``, and
    ``Server``. The property ``chassis`` defines it more granularly.
    """
    id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
    chassis = Column(DBEnum(ComputerChassis), nullable=False)
    chassis.comment = """The physical form of the computer.
    
    It is a subset of the Linux definition of DMI / DMI decode.
    """

    def __init__(self, chassis, **kwargs) -> None:
        chassis = ComputerChassis(chassis)
        super().__init__(chassis=chassis, **kwargs)

    @property
    def events(self) -> list:
        return sorted(chain(super().events, self.events_parent),
                      key=self.EVENT_SORT_KEY)

    @property
    def ram_size(self) -> int:
        """The total of RAM memory the computer has."""
        return sum(ram.size or 0 for ram in self.components
                   if isinstance(ram, RamModule))

    @property
    def data_storage_size(self) -> int:
        """The total of data storage the computer has."""
        return sum(ds.size or 0 for ds in self.components
                   if isinstance(ds, DataStorage))

    @property
    def processor_model(self) -> str:
        """The model of one of the processors of the computer."""
        return next(
            (p.model for p in self.components if isinstance(p, Processor)),
            None)

    @property
    def graphic_card_model(self) -> str:
        """The model of one of the graphic cards of the computer."""
        return next(
            (p.model for p in self.components if isinstance(p, GraphicCard)),
            None)

    @property
    def network_speeds(self) -> List[int]:
        """Returns two values representing the speeds of the network
        adapters of the device.

        1. The max Ethernet speed of the computer, 0 if ethernet
           adaptor exists but its speed is unknown, None if no eth
           adaptor exists.
        2. The max WiFi speed of the computer, 0 if computer has
           WiFi but its speed is unknown, None if no WiFi adaptor
           exists.
        """
        speeds = [None, None]
        for net in (c for c in self.components
                    if isinstance(c, NetworkAdapter)):
            speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless]
                                       or 0)
        return speeds

    @property
    def privacy(self):
        """Returns the privacy of all ``DataStorage`` components when
        it is not None.
        """
        return set(privacy
                   for privacy in (hdd.privacy for hdd in self.components
                                   if isinstance(hdd, DataStorage)) if privacy)

    def __format__(self, format_spec):
        if not format_spec:
            return super().__format__(format_spec)
        v = ''
        if 't' in format_spec:
            v += '{0.chassis} {0.model}'.format(self)
        elif 's' in format_spec:
            v += '({0.manufacturer})'.format(self)
            if self.serial_number:
                v += ' S/N ' + self.serial_number.upper()
        return v
Esempio n. 18
0
class Laptop(Computer):
    layout = Column(DBEnum(Layouts))
    layout.comment = """Layout of a built-in keyboard of the computer,
Esempio n. 19
0
class Keyboard(ComputerAccessory):
    layout = Column(DBEnum(Layouts))  # If we want to do it not null
Esempio n. 20
0
class MinesweeperPattern(ModelBase):

    success_rate: int = Column(Integer)
    failure_rate: int = Column(Integer)
    click_event: MinesweeperClickEvent = Column(DBEnum(MinesweeperClickEvent))
    complexity: int = Column(Integer)
    _hash: int = None
    hashes: List[MinesweeperPatternHash]

    def __init__(self,
                 click_event: MinesweeperClickEvent,
                 success_rate: int = 0,
                 failure_rate: int = 0):
        self.click_event = click_event
        self.success_rate = success_rate
        self.failure_rate = failure_rate

    def _get_main_value_list(self, sibling_index: int,
                             from_tile: BoardTile) -> Tuple[any, List[any]]:
        value_list = self._get_tile_hash_values([], sibling_index, from_tile)
        return value_list.pop(0), value_list

    def calculate_main_hash(self, sibling_index: int,
                            from_tile: BoardTile) -> MinesweeperPatternHash:
        main_value, formatted_siblings = self._get_main_value_list(
            sibling_index, from_tile)

        other_sibls = [
            s for i, s in enumerate(formatted_siblings) if i != sibling_index
        ]
        hidden_sibls = len(
            [s for s in other_sibls if s == MinesweeperTileState.HIDDEN])
        self.complexity = round(hidden_sibls / len(other_sibls), 2)

        main_hash = MinesweeperPatternHash(
            self._calculate_hash_from_value_list(sibling_index, main_value,
                                                 formatted_siblings),
            sibling_index, self)

        return main_hash

    def calculate_other_hashes(
            self, from_tile: BoardTile) -> List[MinesweeperPatternHash]:
        if not self.hashes:
            raise RuntimeError('No existing main hash exists.')

        main_hash = self.hashes[0]
        sibling_index = main_hash.sibling_index

        main_value, formatted_siblings = self._get_main_value_list(
            sibling_index, from_tile)

        self._calculate_hashes(
            sibling_index, main_value,
            {k: v
             for k, v in enumerate(formatted_siblings)})
        hash(self)
        return [h for h in self.hashes if h != main_hash]

    def _calculate_hash_from_value_list(self, sibling_index: int,
                                        main_value: any,
                                        value_list: List[any]) -> str:
        raw_hash = [
            sibling_index, self.click_event.value, main_value,
            '-'.join([str(v) for v in value_list])
        ]

        hash_object = hashlib.sha1('-'.join([str(h) for h in raw_hash
                                             ]).encode('utf-8'))
        return hash_object.hexdigest()

    def _calculate_hashes(self, sibling_index: int, main_value: any,
                          sibling_values: Dict[int, any]):
        all_vls = [sibling_values, self._mirror_value_list(sibling_values)]
        for vl in all_vls.copy():
            rot_1 = self._rotate_value_list_left(vl)
            rot_2 = self._rotate_value_list_left(rot_1)
            rot_3 = self._rotate_value_list_left(rot_2)
            all_vls.extend([rot_1, rot_2, rot_3])
        all_vls.pop(0)

        for p in all_vls:
            for m_i, i in enumerate(p.keys()):
                if i == sibling_index:
                    break
            else:
                raise RuntimeError('Cannot calculate mirrored index')
            raw_hash = self._calculate_hash_from_value_list(
                m_i, main_value, list(p.values()))
            for existing in self.hashes:
                if existing.hash_ == raw_hash:
                    break
            else:
                MinesweeperPatternHash(raw_hash, m_i, self)

    def _mirror_value_list(self, sibling_values: Dict[int,
                                                      any]) -> Dict[int, any]:
        sv = [(k, v) for k, v in sibling_values.items()]
        return dict([sv[2], sv[1], sv[0], sv[4], sv[3], sv[7], sv[6], sv[5]])

    def _rotate_value_list_left(
            self, sibling_values: Dict[int, any]) -> Dict[int, any]:
        sv = [(k, v) for k, v in sibling_values.items()]
        return dict([sv[5], sv[3], sv[0], sv[6], sv[1], sv[7], sv[4], sv[2]])

    def _get_tile_hash_values(self, value_list: List[any], sibling_index: int,
                              tile: BoardTile) -> List[any]:

        self.__class__._add_tile_hash_value(value_list, tile)

        open_siblings = {s for s in tile.siblings if s and not s.is_displayed}

        for index, sibling in enumerate(tile.siblings):
            if not sibling:
                value_list.append(MinesweeperTileState.NO_TILE)
            elif index == sibling_index and self.click_event == MinesweeperClickEvent.UNFLAG:
                value_list.append(MinesweeperTileState.HIDDEN)
            elif ((sibling.is_displayed and sibling.value
                   and sibling.value > tile.value)
                  or not ({s
                           for s in sibling.siblings}.union({sibling})
                          & open_siblings)):
                value_list.append(MinesweeperTileState.ANY_VALUE)
            else:
                self.__class__._add_tile_hash_value(value_list, sibling)

        return value_list

    @classmethod
    def _add_tile_hash_value(cls, value_list: List[any],
                             tile: BoardTile) -> List[any]:
        if tile.is_flagged:
            value_list.append(MinesweeperTileState.FLAGGED)
        elif not tile.is_displayed:
            value_list.append(MinesweeperTileState.HIDDEN)
        else:
            value_list.append(tile.value)
        return value_list

    @classmethod
    def get_unclicked_sibling_indexes(cls, from_tile: BoardTile) -> List[int]:
        indexes: List[int] = []
        for index, s in enumerate(from_tile.siblings):
            if s and not s.is_displayed:
                indexes.append(index)
        if not indexes:
            raise RuntimeError('No unclicked sibling could be found.')
        return indexes

    @property
    def confidence(self) -> float:
        success_rate = self.success_rate**2
        failure_rate = self.failure_rate**2
        total = success_rate + failure_rate
        if total == 1:
            return success_rate - failure_rate
        elif total > 0:
            co = (1 - (1 / total)) * (1 - self.complexity)
            confidence = (success_rate / total) - (failure_rate / total)
            return round(confidence * 100 * co)
        else:
            return 0

    def __eq__(self, other) -> bool:
        if not self.__class__ is other.__class__:
            return False

        return hash(self) == hash(other)

    def __hash__(self) -> int:
        return int(''.join(sorted([h.hash_ for h in self.hashes])), 16)