diff --git a/ERFormatConstants.py b/ERFormatConstants.py index 83c37d1d1db41bcf4b8dee1db935fe7db8e42929..439a0be313af8e4d1b765ce37603ae79c9f5a441 100644 --- a/ERFormatConstants.py +++ b/ERFormatConstants.py @@ -1,9 +1,7 @@ #File contains constants from the CSV export format of the entity relationship tool +# Last updated: 06.05.2024 -#Generic fields for all rows in the CSV file -Id = "Id" # The id of the entity -Name = "Name" # ! Identifier, can be used to distinguish diagram components - +# Constants for text areas textArea1 = "Text Area 1" textArea2 = "Text Area 2" textArea3 = "Text Area 3" @@ -30,93 +28,62 @@ textArea23 = "Text Area 23" textArea24 = "Text Area 24" textArea25 = "Text Area 25" +#Generic fields for all rows in the CSV file +Id = "Id" # The id of the entity +Name = "Name" # ! Identifier, can be used to distinguish diagram components -#Mapped fields for Threat component -Threat = "Threat" # The threat -ThreatValue = textArea1 # The threat - -ThreatID = textArea2 # The id of the threat -ThreatIDValue = textArea3 # Value - -ThreatSource = textArea6 # The threat source -ThreatSourceValue = textArea7 # Value +# Lines +From = "Line Source" +To = "Line Destination" -Likelihood = textArea6 # The likelihood of the threat -LikelihoodValue = textArea7 # Value -ThreatDescription = textArea4 # The description of the threat -ThreatDescriptionValue = textArea5 # Value +#Mapped fields for Threat component +Threat = "Threat" # The threat +ThreatComponent = textArea1 # The threat -Vulnerability = textArea10 # The vulnerability -VulnerabilityValue = textArea11 # Value +ThreatMethodID = textArea3 # Value +ThreatName = textArea5 # The name of the threat +ThreatDescription = textArea7 # Value +ThreatSource = textArea9 # Value +Likelihood = textArea11 # Value #Consequence Consequence = "Consequence" # The consequence score of the area -> Text area 6 -ConsequenceValue = textArea1 # The consequence - -ConsequenceID = textArea2 # The id of the consequence -ConsequenceIDValue = textArea3 # Value - -ConsequenceDescription = textArea4 # The description of the consequence -ConsequenceDescriptionValue = textArea5 # Value +ConsequenceComponent = textArea1 # The consequence -ConsequenceScore = textArea6 # The consequence score of the area -ConsequenceScoreValue = textArea7 # Value +ConsequenceID = textArea3 # Value +ConsequenceName = textArea5 # The name of the consequence +ConsequenceDescription = textArea7 # The description of the consequence +ConsequenceScore = textArea9 # The consequence score of the area -AffectedComponent = textArea8 # The affected component -AffectedComponentValue = textArea9 # Value #Attack -Attack = "Attack" # The attack -AttackValue = textArea1 # The attack +Attack = "Cyber Attack" # The attack +AttackComponent = textArea1 # The attack +AttackType = textArea3 +AttackDescription = textArea5 -AttackType = textArea2 # The type of attack -AttackTypeValue = textArea3 # Value +# Relationship +relationship = "Relationship" +typeField = textArea3 +relationshipDescription = textArea5 -AttackedComponent = textArea4 # The attacked component -AttackedComponentValue = textArea5 # Value +# ER components +erName = textArea1 +erID = textArea3 +erDescription = textArea5 +erType = textArea7 -AttackDescription = textArea6 # The description of the attack -AttackDescriptionValue = textArea7 # Value -#Dynamics -Dynamics = "Dynamics" # The dynamics -ThreatDynamic = "Threat" # The threat dynamics -ConsequenceDynamic = "Consequence" # The consequence dynamics -AttackDynamic = "Attack" # The attack dynamics -ERDynamic = "ER" # The ER dynamics -BowtieDynamicType = textArea3 # The bowtie dynamics - -# ! Note all metric fields use ID and name as the fields for the metric -# ! The ID will be used to link them in the dynamic matrix - - -# Lines -From = "Line Source" -To = "Line Destination" # Table # ? Column names -Er_ID = "ER ID" -Bowtie_ID = "Bowtie ID" -Metric_ID = "Metric ID" -Metric_Name = "Metric Name" +indicatorID = "Indicator ID" +indicatorName = "Indicator name" Value = "Value" -Measure_date = "Measure Date" +Measure_date = "Measurement date" Frequency = "Frequency" Measurement_guide = "Measurement guide" - -# Column mapping -ErColumn = textArea1 -BowTieColumn = textArea2 -MetricColumn = textArea3 -MetricNameColumn = textArea10 -ValueColumn = textArea13 -MeasureDateColumn = textArea16 -FrequencyColumn = textArea19 -MeasurementGuideColumn = textArea22 - - -# Column locations -# ! Edge case: The first entries have irregular placement and needs specialized values +Scale = "Scale" +Interpretation = "Interpretation of measurement" diff --git a/components.py b/components.py index 44b0b95c78b68334af32b19e5fcadfb472adcf86..5a2a773cd6d9368d8fb7f3dab7e5475812f5a494 100644 --- a/components.py +++ b/components.py @@ -10,22 +10,68 @@ class Threat: Fields are parsed from the exported CSV file Threats have associated consequences, dynamic metrics and a related attack """ - def __init__(self, id, componentID, threatSource, description, likelihood, vulnerability) -> None: + 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.vulnerability = vulnerability # The vulnerability associated with the threat - self.linkedDynamics = [] # List of linked dynamics components - self.linkedConsequences = [] # List of linked consequences + self.linkedIndicators: dict[int, str] = {} # Dictironary of linked indicators ID, Reasonnig def __str__(self) -> str: - return f"Threat: {self.id}, {self.description}, {self.threatSource}, {self.likelihood}, {self.vulnerability}" + return f"Threat: {self.id}, {self.name}, {self.linkedIndicators}" + + def findIndicatorsThreat(df:pd.DataFrame, diagram: component.Diagram, self): + """ + 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 component + 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 + + if lineOne[const.From].item() == componentId: # If the component is the source + lineTwo = df.loc[df[const.Id] == lineOne[const.To].item()] # Find the component associated with the line + else: + lineTwo = df.loc[df[const.Id] == lineOne[const.From].item()] # Find the component associated with the line + + + sourceComponent = df.loc[df[const.Id]==lineTwo[const.From].item()] # Defines the source of the line connecting threats/consequences and attacks + destinationComponent = df.loc[df[const.Id]==lineTwo[const.To].item()] # Defines the destination of the line connecting threats/consequences and attacks + + + if componentType == const.ThreatDynamic: # * If the component is a threat + if sourceComponent[const.textArea1].item() == const.Threat: # Checks if source or destination is the threat + self.associatedThreat = sourceComponent + self.associatedAttack = destinationComponent + else: + self.associatedThreat = destinationComponent + self.associatedAttack = sourceComponent + + elif componentType == const.ConsequenceDynamic: # * If the component is a consequence + + if sourceComponent[const.textArea1].item() == const.Consequence: + self.associatedConsequence = sourceComponent + self.associatedAttack = destinationComponent + else: + self.associatedConsequence = destinationComponent + self.associatedAttack = sourceComponent + + # ! After parsing the different components related to the dynamics component, + # ! the metrics in the component are added to the global metric list and local metric list + self.linkMetric(df, metricsDict) class Consequence: """_summary_ @@ -33,16 +79,19 @@ class Consequence: Consequences are linked to threats and attacks and also contain their own dynamic metrics """ - def __init__(self, id, componentID, description, consequence, affectedComponents) -> None: + 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.affectedComponents = affectedComponents # List of affected components + + self.linkedIndicators: dict[int, str] = {} # Dictironary of linked indicators ID, Reasonnig + def __str__(self) -> str: - return f"Consequence: {self.id}, {self.description}, {self.consequence}, {self.affectedComponents}" + return f"Consequence: {self.id}, {self.name}, {self.consequence}, {self.linkedIndicators}" class Attack: @@ -51,31 +100,36 @@ class Attack: Attacks are linked to consequences and threats and contain their own dynamic metrics """ - def __init__(self, id, componentID, type, component, description) -> None: - self.id = id # The id field in the ER model + 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.component = component # The component related to the attack (linked by ID) self.description = description # The description of the component - self.associatedThreats = [] # List of associated threats linked by componentID - self.associatedConsequences = [] # List of associated consequences linked by componentID - self.linkedDynamics = [] # List of linked dynamics components + 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.id}, {self.description}, {self.attackType}" + 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] = {} + @dataclass class Diagram(): - threats: dict[Threat] - consequences : dict[Consequence] - attacks : dict[Attack] - dynamics : dict[DynamicComponent] - metrics : dict[Metric] - erComponents : dict[str] + # 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 = {} @@ -113,22 +167,5 @@ class Diagram(): attack = self.attacks[id] return attack - def getERDynamic(self, threatID) -> ERDynamic: - """_summary_ - The get ER dynamic is used to retrieve the ER dynamic component associated with a threat - This is done through the metric matrix and threat ID - 1. Threat ID is used to find a row in the metric matrix which contains the threat ID, - - Args: - threatID (_type_): _description_ - - Returns: - ERDynamic: _description_ - """ - pass - - def getBowtieDynamic(self, associatedId) -> BowtieDynamic: - pass - - def getMetric(self) -> Metric: + def getIndicator(self, indicaorID) -> ERDynamic: pass \ No newline at end of file diff --git a/diagramParser.py b/diagramParser.py index db122cacc2f7d9ccf6b296a68c46f6da55af70b9..4253712dc65c2a265e4362fd210b6875151603ec 100644 --- a/diagramParser.py +++ b/diagramParser.py @@ -13,110 +13,117 @@ def parseDiagramFile(csvFile) -> component.Diagram: diagram = component.Diagram() # Defines the diagram object - # List containing all threats and their descriptions - threats = diagram.threats - consequences = diagram.consequences - dynamics = diagram.dynamics - metrics = diagram.metrics + diagram = matrix.parseTable(df, diagram) #Parse the table and finds all indicators + diagram = parseArchitectureDiagram(df, diagram) #Parse the architecture diagram and finds all ER components along with indicators and reasoning - threats = parseThreats(df, threats) - consequences = parseConsequences(df, consequences) - - metrics, dynamics = parseDynamic(df, metrics, dynamics) - - metricsMatrix, metrics = matrix.parseTable(df, metrics, diagram) #Parse the table + diagram = parseThreats(df, diagram) # + diagram = parseConsequences(df, diagram) + diagram = parseAttacks(df, diagram) + return diagram - -def parseThreats(df, threatDict): +# Function will parse the threats and add them to the dictionary +def parseThreats(df,diagram: component.Diagram): for i in range(len(df)): - if df[const.ThreatValue][i] == const.Threat: #Creates threat object + if df[const.ThreatComponent][i] == const.Threat: #If the row in the dataframe is a threat threat = component.Threat( + df[const.Id][i], # Component ID df[const.ThreatIDValue][i], # ID from ER - df[const.Id][i], # LucidChart ID - df[const.ThreatSourceValue][i], - df[const.ThreatDescriptionValue][i], - df[const.LikelihoodValue][i], - df[const.VulnerabilityValue][i] + df[const.ThreatName][i], # Name of the threat + df[const.ThreatDescription][i], + df[const.ThreatSource][i], + df[const.Likelihood][i], ) - threatDict[df.Id[i]] = threat + diagram.threats[df[const.ThreatIDValue][i]] = threat - return threatDict + threat.findIndicatorsThreat(df, diagram, threat) # Find the indicators associated with the threat + + return diagram #Parses consequences and adds it to dictionary -def parseConsequences(df, consequenceDict): +def parseConsequences(df, diagram: component.Diagram): for i in range(len(df)): - if df[const.ConsequenceValue][i] == const.Consequence: + if df[const.ConsequenceComponent][i] == const.Consequence: consequence = component.Consequence( - df[const.ConsequenceIDValue][i], df[const.Id][i], - df[const.ConsequenceDescriptionValue][i], - df[const.ConsequenceScoreValue][i], - df[const.AffectedComponentValue][i] + df[const.ConsequenceID][i], + df[const.ConsequenceName][i], + df[const.ConsequenceDescription][i], + df[const.ConsequenceScore][i], ) - consequenceDict[df.Id[i]] = consequence + diagram.consequences[df[const.ConsequenceID][i]] = consequence - return consequenceDict + return diagram -def parseAttacks(df, attackDict): +def parseAttacks(df, diagram: component.Diagram): for i in range(len(df)): - if df[const.AttackValue][i] == const.Attack: + if df[const.AttackComponent][i] == const.Attack: attack = component.Attack( df[const.Id][i], - df[const.AttackedComponentValue][i], - df[const.AttackDescriptionValue][i], + df[const.AttackType][i], + df[const.AttackDescription][i], ) - attackDict[df.Id[i]] = attack + diagram.attacks[df.Id[i]] = attack #! Note that the ID is the component ID from LucidChart, not my ID - return attackDict - + return diagram + #Parses metrics components and adds it to list -def parseDynamic(df, metricDict, dynamicsDict): - for i in range(len(df)): # Iterates through the dataframe - if df[const.textArea1][i] == const.Dynamics: # If the component is a dynamic component +def parseArchitectureDiagram(df, diagram: component.Diagram): + for i in range(len(df)): + # Iterates through the dataframe + if df[const.textArea1][i] == const.relationship: # If the component is a dynamic component + if df[const.typeField][i] == "ER": # * If the component is an ER component # Find the line where the component is the source + diagram = parseRelationshipComponentER(df, diagram, i) - if df[const.textArea3][i] == const.ThreatDynamic: # If the dynamic component is a threat - threatDynamic = dynamic.BowtieDynamic( - df[const.Id][i], # Component ID LucidChart - df[const.textArea3][i] # Name of dynamic - ) - - threatDynamic.associateBowtie(df, threatDynamic.type, metricDict) # Associate the dynamic with the correct components - dynamicsDict[df.Id[i]] = threatDynamic - - elif df[const.textArea3][i] == const.ConsequenceDynamic: - consequenceDynamic = dynamic.BowtieDynamic( - df[const.Id][i], # Component ID LucidChart - df[const.textArea3][i] # Name of dynamic - ) - - consequenceDynamic.associateBowtie(df, consequenceDynamic.type, metricDict) - dynamicsDict[df.Id[i]] = consequenceDynamic - - elif df[const.textArea3][i] == const.AttackDynamic: - attackDynamic = dynamic.BowtieDynamic( - df[const.Id][i], # Component ID LucidChart - df[const.textArea3][i] # Name of dynamic - ) - - attackDynamic.associateBowtie(df,attackDynamic.type, metricDict) # Associate the dynamic with the correct components - dynamicsDict[df.Id[i]] = attackDynamic - - elif df[const.textArea3][i] == const.ERDynamic: - erDynamic = dynamic.ERDynamic( - df[const.Id][i], # Component ID LucidChart - df[const.textArea3][i], # Component type - df[const.textArea5][i] # Description - ) - - erDynamic.associatER(df, metricDict) - dynamicsDict[df.Id[i]] = erDynamic - return metricDict, dynamicsDict + return diagram + +def parseRelationshipComponentER(df: pd.DataFrame, diagram: component.Diagram, row) -> None: + + relationshipComponent = df.loc[row] # Define the relationship component + + print("Parsing ER component: ", relationshipComponent[const.erName]) + print(relationshipComponent[const.textArea5]) + + indicatorList = {} # Define a list over relevant indicators to individual relationship component + erComponents = {} # Define a list over ER components + + #* Iterate over the Relationship component to find all the indicators, and the reasoning for each + for i in range(len(6 ,relationshipComponent.columns, 2)): + indicatorID = relationshipComponent[f"Text Area {i}"].item() + indicatorReason = relationshipComponent[f"Text Area {i+1}"].item() + indicatorList[indicatorID] = indicatorReason + + # * Find all the lines associated with the component + linesFrom = df.loc[df[const.From] == relationshipComponent[const.Id].item()] # Find the component associated with the line + linesTo = df.loc[df[const.To] == relationshipComponent[const.Id].item()] # Find the component associated with the line + + linesFrom.append(linesTo) # Merge the two dataframes + + # * Iterate through both lines, and find the ER components related to the relationship component + for i in range(len(linesFrom)): + # Defines values + erName = linesFrom[const.Name][i].item() + erID = linesFrom[const.erID][i].item() + erDescription = linesFrom[const.erDescription][i].item() + erType = linesFrom[const.erType][i].item() + + # Create ER component + erComponent = component.ERComponent(erName,erID, erDescription, erType) + # Add indicators to the ER component + erComponent.linkedIndicators.update(indicatorList) + erComponents[erComponent.id]= erComponent + + for i in range (indicatorList): + indicatorID = i.key() + # We need to update the indicators with the ER components and the reasoning for the indicator annotation + diagram.indicators[indicatorID].erAssocaites.update(erComponents) + + return diagram def extractMetrics(df, index, startRange): diff --git a/dynamics.py b/dynamics.py index a287a8bce3fb5840b409e9894d48d3a577783229..a3320b6984cb77e16aefa58162a0fb07fa19ec80 100644 --- a/dynamics.py +++ b/dynamics.py @@ -1,30 +1,17 @@ import ERFormatConstants as const import logging as log +import components as component -class DynamicComponent: - """_summary_ - Dynamic component, parent class of bowtie dynamic and ER dynamic - It contains a metric list which contains the ID of the metrics associated with the dynamic component - """ - def __init__(self, componentID) -> None: - self.componentID = componentID - self.metrics = [] # List of metrics for the dynamic - - - #String only returns necessary info - def __str__(self) -> str: - return f"Dynamic Component: {self.componentID}, {len(self.metrics)}" - -#Metric class, will be used for all dynamics -class Metric: + #Metric class, will be used for all dynamics +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) -> None: + 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 @@ -33,151 +20,72 @@ class Metric: name (str): Unique name for the metric """ #Metrics found in the dynamics tables - self.erID = [] # ID of the metric used to locate in dynamics matrics - self.bowtieID = [] # ID of the metric used to locate in dynamics matrics - self.ID = ID # ID of the metric used to locate in dynamics matrics - self.name = name # Name of the metric - self.value = None # Value of the metric - self.date = None # Date of the metric - self.frequency = None # Frequency of the metric - self.measureGuide = None # Measure guide of the metric + self.erAssociates: dict[int, component.ERComponent] = {} # ID of the metric used to locate in dynamics matrics + self.threatAssoicates: dict[str, component.Threat] = {} # ID of the metric used to locate in dynamics matrics + self.consequenceAssociates: dict[str, component.Consequence] = {} # ID of the metric used to locate in dynamics matrics + self.attackAssociates: dict[int, component.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}" -class BowtieDynamic(DynamicComponent): + + +def linkMetric(self, df, metricsDict: dict): """ - The bowtie dynamic inherits the dynamic component and adds associated threats or consequences and attacks - Contains its own associate function and its own linkMetric function to link metrics + Function will extract all the metrics from the dynamics component, add it to the local metric list of the dynamic component, + the function also adds the metric to the global metric list (if its not already there) and and links the dynamic component to the metric, + through the associatedER and associatedAttack lists in the metric class + Args: - DynamicComponent (class): Parent class - """ - def __init__(self, componentID, type) -> None: - super().__init__(componentID) + df (pandas Dataframe): The original dataframe being parsed from lucidchart + metricsDict (dict): The metricsdict is the global metric list, it contains all the metrics in the diagram + """ - self.type = type # Type of dynamicM - self.associatedThreat = None # Threat associated with the dynamic - self.associatedAttack = None # Attack associated with the dynamic - self.associatedConsequence = None # Consequence associated with the dynamic + dynamicComponent = df.loc[df[const.Id] == self.componentID] # Finds the dynamic component we are extracting metrics from + for i in range (4, len(dynamicComponent.columns), 2): # Iterate through all the metrics + if(dynamicComponent["Text Area "+str(i)].isnull().values.any() == True): # If there are no metrics left + break # Break the loop + + metricID = dynamicComponent["Text Area "+str(i)].item() # Find the metric ID in the component + metricID = int(metricID) - def __str__(self) -> str: - """ - This string function is used to print the dynamic component - Based on the different types of dynamics the string function will return different information + metricName = dynamicComponent["Text Area "+str(i+1)].item() # Find the metric Name - Returns: - str: Print string - """ - if self.type == const.ThreatDynamic: - return super().__str__() + f"Associated threat ID: {self.associatedThreat[const.Id].item()} Associated attack ID {(self.associatedAttack[const.Id].item())}" - elif self.type == const.ConsequenceDynamic: - return super().__str__() + f"Associated consequence ID: {self.associatedConsequence[const.Id].item()} Assocaited attack ID:{(self.associatedAttack[const.Id].item())}" - elif self.type == const.AttackDynamic: - return super().__str__() + f"Associated attack ID: {self.associatedAttack[const.Id].item()}" + self.metrics.append(metricID) # ? Adds the metric to the metric list associated with the dynamic component specifically - def associateBowtie(self, df, componentType: str, metricsDict: dict): - """ - Handles the associated dynamics for the bowtie model as they are different from the ER model - ER model has one type of dynamic component with metrics however, the bowtie model has three dynamic types and needs more parsing - than the ER model du to the modeling annotation. - Abstracting this saves time - """ + if checkDuplicateMetrics(metricsDict, metricID) == False: # If the metric is in the global list + metric = Metric(metricID, metricName) # Create a new metric object + else: + metric = metricsDict[metricID] # Find the metric + if self.type == const.ThreatDynamic: # If the dynamic component is a threat + if checkDuplicate(metric.bowtieID, self.associatedThreat[const.Id].item()): #* If the threat component is associated with the metric + continue # * We wont add it to the metric threatList and attack list since its already there + metric.bowtieID.append(self.associatedThreat[const.Id].item()) # ? Adds the associated threat to the metric - componentId = self.componentID # Define the ID of the component - 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: - lineOne = lineSource - else: - lineOne = lineDestination - - if lineOne[const.From].item() == componentId: # If the component is the source - lineTwo = df.loc[df[const.Id] == lineOne[const.To].item()] # Find the component associated with the line - else: - lineTwo = df.loc[df[const.Id] == lineOne[const.From].item()] # Find the component associated with the line - - # ! Edge case, if the second line is in fact not a second line it is an attack meaning that the dynamic component is an attack - if componentType == const.Attack: # If the associated component is an attack - self.associatedAttack = lineTwo # ! The "line" is an attack, not a line, we add it to the associated attack field and move on - self.linkMetric(df, metricsDict) - return # The attack is the only component associated with the dynamic - - sourceComponent = df.loc[df[const.Id]==lineTwo[const.From].item()] # Defines the source of the line connecting threats/consequences and attacks - destinationComponent = df.loc[df[const.Id]==lineTwo[const.To].item()] # Defines the destination of the line connecting threats/consequences and attacks + elif self.type == const.ConsequenceDynamic: # Same procedure as if above + if checkDuplicate(metric.bowtieID, self.associatedConsequence[const.Id].item()): + continue + metric.bowtieID.append(self.associatedConsequence[const.Id].item()) - - if componentType == const.ThreatDynamic: # * If the component is a threat - if sourceComponent[const.textArea1].item() == const.Threat: # Checks if source or destination is the threat - self.associatedThreat = sourceComponent - self.associatedAttack = destinationComponent - else: - self.associatedThreat = destinationComponent - self.associatedAttack = sourceComponent - - elif componentType == const.ConsequenceDynamic: # * If the component is a consequence - - if sourceComponent[const.textArea1].item() == const.Consequence: - self.associatedConsequence = sourceComponent - self.associatedAttack = destinationComponent - else: - self.associatedConsequence = destinationComponent - self.associatedAttack = sourceComponent - - # ! After parsing the different components related to the dynamics component, - # ! the metrics in the component are added to the global metric list and local metric list - self.linkMetric(df, metricsDict) + if checkDuplicate(metric.bowtieID, self.associatedAttack[const.Id].item()): #* If the associated component is a duplicate + continue + metric.bowtieID.append(self.associatedAttack[const.Id].item()) - def linkMetric(self, df, metricsDict: dict): - """ - Function will extract all the metrics from the dynamics component, add it to the local metric list of the dynamic component, - the function also adds the metric to the global metric list (if its not already there) and and links the dynamic component to the metric, - through the associatedER and associatedAttack lists in the metric class - - Args: - df (pandas Dataframe): The original dataframe being parsed from lucidchart - metricsDict (dict): The metricsdict is the global metric list, it contains all the metrics in the diagram - """ + metricsDict[metricID] = metric # ! Adds the metric to the global metric list - dynamicComponent = df.loc[df[const.Id] == self.componentID] # Finds the dynamic component we are extracting metrics from - for i in range (4, len(dynamicComponent.columns), 2): # Iterate through all the metrics - - if(dynamicComponent["Text Area "+str(i)].isnull().values.any() == True): # If there are no metrics left - break # Break the loop - - metricID = dynamicComponent["Text Area "+str(i)].item() # Find the metric ID in the component - metricID = int(metricID) - - metricName = dynamicComponent["Text Area "+str(i+1)].item() # Find the metric Name - - self.metrics.append(metricID) # ? Adds the metric to the metric list associated with the dynamic component specifically - - if checkDuplicateMetrics(metricsDict, metricID) == False: # If the metric is in the global list - metric = Metric(metricID, metricName) # Create a new metric object - else: - metric = metricsDict[metricID] # Find the metric - - if self.type == const.ThreatDynamic: # If the dynamic component is a threat - if checkDuplicate(metric.bowtieID, self.associatedThreat[const.Id].item()): #* If the threat component is associated with the metric - continue # * We wont add it to the metric threatList and attack list since its already there - metric.bowtieID.append(self.associatedThreat[const.Id].item()) # ? Adds the associated threat to the metric - - elif self.type == const.ConsequenceDynamic: # Same procedure as if above - if checkDuplicate(metric.bowtieID, self.associatedConsequence[const.Id].item()): - continue - metric.bowtieID.append(self.associatedConsequence[const.Id].item()) - - if checkDuplicate(metric.bowtieID, self.associatedAttack[const.Id].item()): #* If the associated component is a duplicate - continue - metric.bowtieID.append(self.associatedAttack[const.Id].item()) - - metricsDict[metricID] = metric # ! Adds the metric to the global metric list - class ERDynamic(DynamicComponent): """ The ER dynamic inherits the dynamic component and adds associated ER components diff --git a/matrices.py b/matrices.py index d1343a031658dd9fa23148c4ad40437801013e20..e3007d98524034066cea337fc0a9f824f19efce9 100644 --- a/matrices.py +++ b/matrices.py @@ -1,121 +1,130 @@ import pandas as pd import ERFormatConstants as const - +import components as component +import dynamics as dynamic """_summary_ Parses csv table and returns a dataframe This is quite finicky as lucidchart has a very weird way of structuring tables as it is based on when different cells were created This is quite a big EDGE case and will only work for the specific table shape that I have created """ -def parseTable(df, metricsDict, diagram): +def parseTable(df, diagram: component.Diagram) -> component.Diagram: - cols = [const.Metric_ID, const.Metric_Name, const.Value, const.Measure_date, const.Frequency ,const.Measurement_guide] + cols = [const.Metric_ID, const.Metric_Name, const.Value, const.Measure_date, const.Frequency ,const.Measurement_guide, const.Scale, const.Interpretation] table = df.loc[df[const.Name]=='Table'] # Defines the table - - matrics = pd.DataFrame(columns=cols) - - stopLength = 226 - - # ! Parsing the first row of the table which has a fucked structure because lucidchart is garbage : ) - MetricID1 = table[const.textArea4].item() # Define the Metric ID - MetricName1 = table[const.textArea5].item() # Define the Metric Name + + # ! Prior to parsing the entire table, the first two rows need to be specifically created, + # ! This is due to the way the table is structured in LucidChart, the first 6 cells comes first, then the set of standard indicators are next + # ! Next is the top of the table again with the two rows and 5 columns before new indicators are parsed FCk this shit + # * Start table | 1 | 2 | 3 | 170 | 171 | 172 | 173 | 174 | + # * Row 1 | 4 | 5 | 6 | 175 | 176 | 177 | 178 | 179 | + # * Row 2 | 7 | 8 | 9 | 180 | 181 | 182 | 183 | 184 | + # * Row . . . . . . . . .. . . . . . . . . . . .. . . . . + # * Row 23 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | + # * Row 24 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 191 | + # * Etc + + + startStandardIndicators = 21-4 # Column 21-4 + stopStandardIndicators = 180-4 # First loop needs to stop at column 180-4, 4 columns are removed in diagramParser.py + interval = len(cols) + startSpecialIndicators = 196-4 # Text area 185 + stopSpecializedIndicators = len(table.columns)-2 + + + + indicatorID1 = table[const.textArea4].item() # Define the Metric ID + indicatorName1 = table[const.textArea5].item() # Define the Metric Name Value1 = table[const.textArea6].item() # Define the Value - MeasureDate1 = table[f"Text Area 229"].item() # Define the Measure Date - Frequency1 = table[f"Text Area 230"].item() # Define the Frequency - Guide1 = table[f"Text Area 231"].item() # Define the Measurement guide - - MetricID1 = int(MetricID1) - matrics.loc[0] = MetricID1, MetricName1, Value1, MeasureDate1, Frequency1, Guide1 # Fill inn dataframe - - # TODO Error handle/Communicate to end user here, "You seem to have a metric in your table which is not used etc" - fillMetricinfo(MetricID1, MetricName1, Value1, MeasureDate1, Frequency1, Guide1, metricsDict, diagram) - + MeasureDate1 = table[f"Text Area 175"].item() # Define the Measure Date + Frequency1 = table[f"Text Area 176"].item() # Define the Frequency + Guide1 = table[f"Text Area 177"].item() # Define the Measurement guide + Scale1 = table[f"Text Area 178"].item() + Interpretation1 = table[f"Text Area 179"].item() + + indicatorID1 = int(indicatorID1) # Convert indicator to int + + #* Create indicator object + indicator1 = dynamic.Indicator( + indicatorID1, + indicatorName1, + Value1, + MeasureDate1, + Frequency1, + Guide1, + Scale1, + Interpretation1 + ) + diagram.indicators[indicatorID1] = indicator1 # Insert indicator into diagram dictionary + - MetricID2 = table[const.textArea7].item() # Define the Metric ID - MetricName2 = table[const.textArea8].item() # Define the Metric Name + IndicatorID2 = table[const.textArea7].item() # Define the Metric ID + IndicatorName2 = table[const.textArea8].item() # Define the Metric Name Value2 = table[const.textArea9].item() # Define the Value - MeasureDate2 = table[f"Text Area 232"].item() # Define the Measure Date - Frequency2 = table[f"Text Area 233"].item() # Define the Frequency - Guide2 = table[f"Text Area 234"].item() # Define the Measurement guide - - MetricID2 = int(MetricID2) - matrics.loc[1] = MetricID2, MetricName2, Value2, MeasureDate2, Frequency2, Guide2 - - metric = metricsDict[MetricID2] # Access the appropriate metric in the dictionary - fillMetricinfo(MetricID2, MetricName2, Value2, MeasureDate2, Frequency2, Guide2, metricsDict, diagram) - - # TODO Error handle/Communicate to end user here, "You seem to have a metric in your table which is not used etc" - if metric == None: - handleMissingMetric() - - matricsIndex = 2 - - # Parsing from the third row and onwards is standard format - for i in range(10, stopLength, 6): - if i >= stopLength: + MeasureDate2 = table[f"Text Area 180"].item() # Define the Measure Date + Frequency2 = table[f"Text Area 181"].item() # Define the Frequency + Guide2 = table[f"Text Area 182"].item() # Define the Measurement guide + Scale2 = table[f"Text Area 183"].item() + Interpretation2 = table[f"Text Area 184"].item() + + IndicatorID2 = int(IndicatorID2) + + indicator2 = dynamic.Indicator( + IndicatorID2, + IndicatorName2, + Value2, + MeasureDate2, + Frequency2, + Guide2, + Scale2, + Interpretation2 + ) + diagram.indicators[IndicatorID2] = indicator2 # Insert indicator into diagram dictionary + + # ! Parsing from the third row and onwards is until row 23, the last row is the standard indicators + for i in range(startStandardIndicators, stopStandardIndicators, interval): + if i >= stopStandardIndicators: break - MetricID = table["Text Area "+str(i)].item() # Define the Metric ID - MetricName = table["Text Area "+str(i+1)].item() # Define the Metric Name + IndicatorID = table["Text Area "+str(i)].item() # Define the Metric ID + IndicatorName = table["Text Area "+str(i+1)].item() # Define the Metric Name Value = table["Text Area "+str(i+2)].item() # Define the Value MeasureDate = table["Text Area "+str(i+3)].item() # Define the Measure Date Frequency = table["Text Area "+str(i+4)].item() # Define the Frequency Guide = table["Text Area "+str(i+5)].item() # Define the Measurement guide + + indicator = dynamic.Indicator( + IndicatorID, + IndicatorName, + Value, + MeasureDate, + Frequency, + Guide + ) + diagram.indicators[IndicatorID] = indicator # Insert indicator into diagram dictionary - matrics.loc[matricsIndex] = MetricID, MetricName, Value, MeasureDate, Frequency, Guide - - print("Matrise:") - print(matrics) - - fillMetricinfo(MetricID, MetricName, Value, MeasureDate, Frequency, Guide,metricsDict, diagram) - + # ! Parsing will then continue from the last row, row 24, and onwards with usual text areas for specialized indicators + for i in range(startSpecialIndicators, stopSpecializedIndicators, interval): + if i >= stopSpecializedIndicators: + break + IndicatorID = table["Text Area "+str(i)].item() # Define the Metric ID + IndicatorName = table["Text Area "+str(i+1)].item() # Define the Metric Name + Value = table["Text Area "+str(i+2)].item() # Define the Value + MeasureDate = table["Text Area "+str(i+3)].item() # Define the Measure Date + Frequency = table["Text Area "+str(i+4)].item() # Define the Frequency + Guide = table["Text Area "+str(i+5)].item() # Define the Measurement guide - matricsIndex += 1 + indicator = dynamic.Indicator( + IndicatorID, + IndicatorName, + Value, + MeasureDate, + Frequency, + Guide + ) + diagram.indicators[IndicatorID] = indicator + - return matrics, metricsDict - -def handleMissingMetric(): - """Function will communicate to end user that a metric is in the matrics but is not used in the diagram - This will provide clarity to the end user and inform them of unlinked metrics - """ - pass - -def fillMetricinfo( - MetricID1: str, - MetricName1: str, - Value1: str, - MeasureDate1: str, - Frequency1: str, - Guide1: str, metricsDict: dict, diagram)-> dict: - """Function will fill in the metric information in the dataframe into the metric object and return the updated dictionary - """ - MetricID1 = int(MetricID1) - - if MetricID1 in metricsDict: - metric = metricsDict[MetricID1] # Access the appropriate metric in the dictionary - - else: - print("Metric is not used in the diagram!!!") - return - - metric.name = MetricName1 # Should be string - metric.value = Value1 # Should be string - metric.date = MeasureDate1 # TODO Parse this and change it to a date object maybe? Not sure if its necessary... - metric.frequency = Frequency1 # Should be string - metric.guide = Guide1 # Should be string - - metricsDict[MetricID1] = metric - - return metricsDict - -def findComponentIDBowtie(id, diagram): - - for threat in diagram.threats.values(): - if threat.id == str(id): - return threat.componentID - - for consequence in diagram.consequences.values(): - if consequence.id == str(id): - return consequence.componentID \ No newline at end of file + return diagram \ No newline at end of file