Quick Start
BooLEVARD Tutorials
[Tutorial 1] Quick Start: Finding vulnerabilities in a Cybersecurity model
In this tutorial, we will explore a small Boolean model designed to simulate different cybersecurity scenarios. The model consists of nodes representing key components of a cybersecurity system, and the goal of this tutorial is to evaluate how different conditions and guidelines affect the overall security state of the system.
The model includes the following nodes:
Inputs: Security_Policies, User_Education, Network_Monitoring, and Security_Patches.
Internal nodes: Backups, Malware, Firewall, Unauthorized_Access, Exploits, Antivirus, Strong_Password, TwoFactAut, and Phishing_Attack.
Output nodes: Security_State.
[1]:
import boolevard as blv
import pandas as pd
from boolevard import CountPaths
/home/marco/.local/lib/python3.11/site-packages/pandas/core/arrays/masked.py:60: UserWarning: Pandas requires version '1.3.6' or newer of 'bottleneck' (version '1.3.5' currently installed).
from pandas.core import (
[ ]:
model = blv.Load("resources/security.bnet") # Load the model (BoolNet format)
check = ["Security_State"] # Set node(s) to check
[3]:
model.Info
[3]:
0 | 3 | 2 | 1 | 6 | 7 | 4 | 5 | 8 | 15 | 12 | 11 | 9 | 14 | 13 | 10 | DNF | NDNF | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
User_Education | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | User_Education | ~User_Education |
Security_Policies | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | Security_Policies | ~Security_Policies |
Security_Patches | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | Security_Patches | ~Security_Patches |
Network_Monitoring | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | Network_Monitoring | ~Network_Monitoring |
Backups | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | And(~Malware, Security_Policies) | Or(Malware, ~Security_Policies) |
Malware | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | And(~Antivirus, Exploits, ~Firewall, ~Strong_P... | Or(Antivirus, ~Exploits, Firewall, Strong_Pass... |
Security_State | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | And(Backups, ~Unauthorized_Access) | Or(~Backups, Unauthorized_Access) |
Firewall | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | Or(Network_Monitoring, Security_Patches) | And(~Network_Monitoring, ~Security_Patches) |
Unauthorized_Access | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Or(And(~Firewall, Malware, ~Strong_Password, ~... | Or(Firewall, Strong_Password, TwoFactAut, And(... |
Exploits | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Or(~Strong_Password, ~TwoFactAut) | And(Strong_Password, TwoFactAut) |
Antivirus | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | Or(Security_Patches, User_Education) | And(~Security_Patches, ~User_Education) |
Strong_Password | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | User_Education | ~User_Education |
TwoFactAut | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | Or(Security_Policies, Strong_Password, User_Ed... | And(~Security_Policies, ~Strong_Password, ~Use... |
Phishing_Attack | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Or(~TwoFactAut, ~User_Education) | And(TwoFactAut, User_Education) |
A total of 16 stable states are reached upon different input combinations. We will check the how secure our system is based on the number of paths leading to the activation or inactivation of the Security_State node.
[4]:
sec_status = model.CountPaths(check, ss_wise = True) # Average signed path counts leading to the local state of Security_State across all the stable states of the model
Evaluating Stable State: 0
Security_State: -35, 4.168351491292318e-06 minutes.
Evaluating Stable State: 3
Security_State: -1, 8.424123128255208e-07 minutes.
Evaluating Stable State: 2
Security_State: -1, 8.821487426757813e-07 minutes.
Evaluating Stable State: 1
Security_State: -1, 6.914138793945312e-07 minutes.
Evaluating Stable State: 6
Security_State: -6, 2.173582712809245e-06 minutes.
Evaluating Stable State: 7
Security_State: 4, 1.5695889790852864e-06 minutes.
Evaluating Stable State: 4
Security_State: 5, 1.9272168477376303e-06 minutes.
Evaluating Stable State: 5
Security_State: 7, 2.276897430419922e-06 minutes.
Evaluating Stable State: 8
Security_State: -1, 4.569689432779948e-07 minutes.
Evaluating Stable State: 15
Security_State: -1, 8.781750996907552e-07 minutes.
Evaluating Stable State: 12
Security_State: -1, 5.125999450683594e-07 minutes.
Evaluating Stable State: 11
Security_State: -1, 4.172325134277344e-07 minutes.
Evaluating Stable State: 9
Security_State: 21, 3.3338864644368488e-06 minutes.
Evaluating Stable State: 14
Security_State: 24, 2.0265579223632812e-06 minutes.
Evaluating Stable State: 13
Security_State: 26, 2.193450927734375e-06 minutes.
Evaluating Stable State: 10
Security_State: 29, 3.83456548055013e-06 minutes.
[6]:
# Sort the security outcomes in ascending order
security_outcomes = pd.DataFrame(sec_status, columns = ["Security_State"], index = model.Info.columns[:-2]).transpose().sort_values(by = "Security_State", axis = 1)
security_outcomes
[6]:
0 | 6 | 3 | 2 | 1 | 8 | 15 | 12 | 11 | 7 | 4 | 5 | 9 | 14 | 13 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Security_State | -35 | -6 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | 4 | 5 | 7 | 21 | 24 | 26 | 29 |
[8]:
# Sort the stable states based on security outcomes and check the input configurations triggering them
inputs = ["User_Education", "Security_Policies", "Security_Patches", "Network_Monitoring"]
model.Info.loc[inputs, security_outcomes.columns]
[8]:
0 | 6 | 3 | 2 | 1 | 8 | 15 | 12 | 11 | 7 | 4 | 5 | 9 | 14 | 13 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
User_Education | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Security_Policies | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Security_Patches | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
Network_Monitoring | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
In the table above, we can see that the absence of all inputs simultaneously results in the worst security outcome (SS = 0, Path count = 35). Activating any single input is not sufficient to change the inactive state of Security_States, although it reduces the number of paths leading to inactivation to one, except for Security_Policies, which leads to a slightly improved outcome (SS = 6, Path count = -6). Most input combinations result in a positive security state, except for the combination of Security_Patches and Network_Monitoring. The strongest security state occurs when all inputs are active, but the combination of User_Education, Security_Policies, and any additional input is enough to trigger almost the maximum observed secure paths.
Lastly, we will examine how common scenarios impact security, either by reducing the number of activating paths or shifting the system toward insecurity. To do so, we will perturb the model by simulating virus triggers and system bugs affecting the defenders, introducing additive activations or inactivations of specific nodes.
BooLEVARD allows for two types of perturbations: non-additive, where the perturbation completely overrides the target node’s regulation, and additive, where its regulatory effect is combined with the existing one. We will analyze the effects of these perturbations when all inputs are ON.
[9]:
perturbations = ["Firewall%INH", "Unauthorized_Access%ACT", "Exploits%ACT", "Antivirus%INH", "Strong_Password%INH", "TwoFactAut%INH", "Phishing_Attack%ACT"] # Define the perturbations
res = []
for p in perturbations:
pmodel = model.Pert(p, additive = True) # Perturb the model
pmodel.Info = pmodel.Info.loc[:, (pmodel.Info.loc[inputs] == 1).all(axis = 0) | pmodel.Info.columns.str.contains("DNF")] # Retrieve only stable states reached when all inputs are on
if len(pmodel.Info.columns) <= 2:
res.append("NA")
else:
sec_status = pmodel.CountPaths(check, ss_wise = True)
res.append(sec_status)
res = [item[0] for item in res]
res = pd.DataFrame(res, index = perturbations, columns = ["Security_State"]).transpose().sort_values(by = "Security_State", axis = 1) # Store results in a dataframe
res
Evaluating Stable State: 29
Security_State: 23, 2.5908152262369793e-06 minutes.
Evaluating Stable State: 30
Security_State: -1, 9.258588155110677e-07 minutes.
Evaluating Stable State: 28
Security_State: 21, 3.949801127115885e-06 minutes.
Evaluating Stable State: 23
Security_State: 25, 4.315376281738282e-06 minutes.
Evaluating Stable State: 9
Security_State: 16, 2.3404757181803385e-06 minutes.
Evaluating Stable State: 27
Security_State: 9, 2.642472585042318e-06 minutes.
Evaluating Stable State: 28
Security_State: 16, 2.3484230041503905e-06 minutes.
[9]:
Unauthorized_Access%ACT | TwoFactAut%INH | Strong_Password%INH | Phishing_Attack%ACT | Exploits%ACT | Firewall%INH | Antivirus%INH | |
---|---|---|---|---|---|---|---|
Security_State | -1 | 9 | 16 | 16 | 21 | 23 | 25 |
We observe that, except for the activation of Unauthorized_Access, all perturbations still allow for security, albeit with varying strengths. In this system, the loss of security strategies is predicted to have a greater impact than losing the antivirus or deactivating the firewall.