Select Git revision
autodb_dump.sql
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
components.py 11.78 KiB
from dataclasses import dataclass
import pandas as pd
import ERFormatConstants as const
class Threat:
"""_summary_
The class threat is used to classify a threat in the ER model.
Fields are parsed from the exported CSV file
Threats have associated consequences, dynamic metrics and a related attack
"""
def __init__(self,componentID ,id, name , description, threatSource, likelihood) -> None:
self.id = id # The id field in the ER model
self.componentID = componentID # The component ID field provided by LucidChart
self.name = name # The name of the component
self.description = description # The description of the component
self.threatSource = threatSource # The threat source, source of the threat ...
self.likelihood = likelihood # The likelihood of the threat to occur
self.linkedIndicators: dict[int, str] = {} # Dictironary of linked indicators ID, Reasonnig
def __str__(self) -> str:
return f"Threat: {self.id}, {self.name}, {self.linkedIndicators}"
def findIndicatorsThreat(self, df:pd.DataFrame, diagram):
"""
Links the indicators for the threat to the threat object and updates the metrics dictionary with the indicators
"""
componentId = self.componentID # Define the ID of the componen
lines = df.loc[df['Name']=='Line'] # Find all lines in the diagram
lineSource = lines.loc[lines[const.From] == componentId] # Find the line where the component is the source
lineDestination = lines.loc[lines[const.To] == componentId] # ! The component SHOULD be source however, to error handle we need to check for destination as well
if lineSource.isnull().values.any() == True: # Checks if the threat is the source or destination
lineOne = lineSource
else:
lineOne = lineDestination
# * LineOne is the line connecting the threat to the Attack, and the relationship line
# * First we find the attack component on the other end of theline
if lineOne[const.From].item() == componentId: # If the component is the source
# ? Since lineOne from is the threat component, lineOne to is the attack component
attackComponent = df.loc[df[const.Id] == lineOne[const.To].item()] # Find the component associated with the line
else:
attackComponent = df.loc[df[const.Id] == lineOne[const.From].item()] # Find the component associated with the line
# * Now we need to find the lines associated with the relationship component
lineOneComponentID = lineOne[const.Id].item()
lines = df.loc[df[const.To] == lineOneComponentID]
if lines.empty:
lines = df.loc[df[const.From] == lineOneComponentID]
# * Due to the presence of barriers we need to iterate over the result and find the one that is the relationship component
for cols, rows in lines.iterrows():
if rows[const.To] == lineOneComponentID:
potentialID = rows[const.From]
else:
potentialID = rows[const.To]
potentialComponent = df.loc[df[const.Id] == potentialID]
if potentialComponent[const.typeField].item() == const.Threat:
relationshipComponent = potentialComponent
break
# * We now have the relationship component, and the attack component
# * We can now extract the metrics from the relationship component and define the attack
attackComponentID = attackComponent[const.Id].item()
attackType = attackComponent[const.AttackType].item()
attackDescription = attackComponent[const.AttackDescription].item()
attack = Attack(attackComponentID, attackType, attackDescription)
diagram.attacks[attackComponentID] = attack
for i in range(4, len(relationshipComponent.columns), 2):
indicatorID = relationshipComponent[f"Text Area {i}"].item()
if indicatorID == "" or "NaN":
break
indicatorID = int(indicatorID)
indicatorReason = relationshipComponent[f"Text Area {i+1}"].item()
self.linkedIndicators[indicatorID] = indicatorReason
diagram.indicators[indicatorID].threatAssoicates[self.id] = self
return diagram
class Consequence:
"""_summary_
The class consequence should contain information about a consequence
Consequences are linked to threats and attacks and also contain their own dynamic metrics
"""
def __init__(self, componentID, id, name, description, consequence) -> None:
self.id = id # The id field in the ER model
self.componentID = componentID # The component ID field provided by LucidChart
self.name = name # The name of the component
self.description = description # The description of the component
self.consequence = consequence # The consequence of the component
self.linkedIndicators: dict[int, str] = {} # Dictironary of linked indicators ID, Reasonnig
def __str__(self) -> str:
return f"Consequence: {self.id}, {self.name}, {self.consequence}, {self.linkedIndicators}"
def findIndicatorsConsequence(self, df:pd.DataFrame, diagram):
"""
Links the indicators for the consequence to the consequence object and updates the metrics dictionary with the indicators
"""
componentId = self.componentID # Define the ID of the componen
lines = df.loc[df['Name']=='Line'] # Find all lines in the diagram
lineSource = lines.loc[lines[const.From] == componentId] # Find the line where the component is the source
lineDestination = lines.loc[lines[const.To] == componentId] # ! The component SHOULD be source however, to error handle we need to check for destination as well
if lineSource.isnull().values.any() == True: # Checks if the threat is the source or destination
lineOne = lineSource
else:
lineOne = lineDestination
# * Now we need to find the lines associated with the relationship component
lineOneComponentID = lineOne[const.Id].item()
lines = df.loc[df[const.To] == lineOneComponentID]
if lines.empty:
lines = df.loc[df[const.From] == lineOneComponentID]
# * Due to the presence of barriers we need to iterate over the result and find the one that is the relationship component
for cols, rows in lines.iterrows():
if rows[const.To] == lineOneComponentID:
potentialID = rows[const.From]
else:
potentialID = rows[const.To]
potentialComponent = df.loc[df[const.Id] == potentialID]
if potentialComponent[const.typeField].item() == const.Consequence:
relationshipComponent = potentialComponent
break
for i in range(4, len(relationshipComponent.columns), 2):
indicatorID = relationshipComponent[f"Text Area {i}"].item()
if indicatorID == "" or "NaN":
break
indicatorID = int(indicatorID)
indicatorReason = relationshipComponent[f"Text Area {i+1}"].item()
self.linkedIndicators[indicatorID] = indicatorReason
diagram.indicators[indicatorID].consequenceAssociates[self.id] = self
return diagram
class Attack:
"""_summary_
The class attack should contain information about an attack at the center of the bow tie model
Attacks are linked to consequences and threats and contain their own dynamic metrics
"""
def __init__(self, componentID, type, description) -> None:
self.componentID = componentID # The component ID field provided by LucidChart
self.type = type # The type of the attack
self.description = description # The description of the component
self.associatedThreats: list[int] = [] # List of associated threats linked by ID
self.associatedConsequences: list[int] = [] # List of associated consequences linked by ID
def __str__(self) -> str:
return f"Attack: {self.description}, {self.attackType}"
class ERComponent:
def __init__(self, name, id, description, type) -> None:
self.name = name
self.id = id
self.description = description
self.type = type
self.linkedIndicators:dict[int, str] = {}
class Indicator:
"""
The metric class is associated with all the dynamics components AND the matrix
The initial creation of metrics is done through the dynamic units, there the init function is used to create the metrics,
and fill them with the correct associated components (erID, bowtieID)
When the object is then accessed again for the matrix parse the metrics are filled with the correct values
"""
def __init__(self, ID, name, value, date, frequency, guide, scale, interpretation) -> None:
"""
Metrics are created when parsing dynamics and are there filled with args
When the matrix is parsed the metrics are filled with the correct values
Args:
ID (str): Unique ID for the metric
name (str): Unique name for the metric
"""
#Metrics found in the dynamics tables
self.erAssociates: dict[int, ERComponent] = {} # ID of the metric used to locate in dynamics matrics
self.threatAssoicates: dict[int, Threat] = {} # ID of the metric used to locate in dynamics matrics
self.consequenceAssociates: dict[int, Consequence] = {} # ID of the metric used to locate in dynamics matrics
self.attackAssociates: dict[int, Attack] = {} # ID of the metric used to locate in dynamics matrics
self.indicatorID = ID # ID of the metric used to locate in dynamics matrics
self.indicatorName = name # Name of the metric
self.value = value # Value of the metric
self.date = date # Date of the metric
self.frequency = frequency # Frequency of the metric
self.measureGuide =guide # Measure guide of the metric
self.scale = scale # Scale of the metric
self.interpretation = interpretation # Interpretation of the metric
def __str__(self) -> str:
return f"Metric: {self.name}, Value: {self.value} Last update: {self.date}"
@dataclass
class Diagram():
# Dicttionaries of threats, consequences, attacks, indicators and ER components
threats: dict[int,Threat]
consequences : dict[int,Consequence]
attacks : dict[int, Attack] # ! The ID of the attack is the componentID not ID!
indicators : dict[int, Indicator]
erComponents : dict[int, ERComponent]
def __init__(self) -> None:
self.threats = {}
self.consequences = {}
self.attacks = {}
self.dynamics = {}
self.indicators = {}