Skip to content
Snippets Groups Projects
Select Git revision
  • 4a0ccced8abcf7e36c81dfb54b6c5e162b633405
  • main default protected
2 results

diagramParser.py

Blame
  • 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 = {}