In this tutorial, you will learn how to build a simple MAL language, create a model from it and run simulations.
Create a directory for the tutorial and set it as your working directory: mkdir mal-tutorial1 && cd mal-tutorial1
Create a Python virtual environment and activate it.
- On Linux-based operating systems:
python -m venv .venv
source .venv/bin/activate
- On Windows:
python -m venv .venv
.\.venv\Scripts\activate
Install the requirements:
pip install mal-toolbox
pip install mal-simulator
To define a mal-lang, create a file in the same directory called exampleLang.mal and copy the following code into it:
#id: "exampleLang"
#version: "2.0.0"
category System {
asset Machine {
| connect
-> authCompromise
| authenticate
-> authCompromise
& authCompromise
-> compromise
| compromise
-> storesCreds.access,
networks.communicate
}
asset Credentials {
| access
-> useUnencrypted,
crack
& useUnencrypted
-> use
| crack [HardAndCertain]
-> use
| use
-> authenticates.authenticate
# encrypted
-> useUnencrypted
}
asset Network {
| communicate
-> parties.connect
}
}
associations {
Machine [parties] * <-- Communication --> * [networks] Network
Machine [storedOn] 0..1 <-- Storage --> * [storesCreds] Credentials
Machine [authenticates] 0..1 <-- Access --> * [authCreds] Credentials
}
This piece of code defines a simple example of MAL-Language. More specifically, it is exampleLang, a basic MAL language intended to demonstrate the standard structure and essential components of a MAL language. Here is an explanation of the language:
We define a category called System that holds three assets:
- Machine
- If steps
connectandauthenticatehappen, thenauthCompromisewould be triggered, which at the same time would triggercompromise, which would also triggeraccessin theCredentialsasset andcommunicatein theNetworkasset.
- If steps
- Credentials
- If
accessis given, it would triggeruseUnencryptedandcrack.useUnencryptedwould triggeruse, which then would triggerauthenticatein theMachineasset. This would mean that the attack has succedeed and the attacker has access to the machine.
- The
crackstep has an ordinal distribution (HardAndCertain). This means that probability for this step to happen is hard and certain. HardAndCertain is defined by the distribution function Exponential(0.1). You can read more about this topic here. - The
encryptedstep is a defense step. That is, if the defender activates it,useEncryptedwill be blocked and, therefore, that path will be as well.
- If
- Network
- If the
communicatestep is activated,connectof theMachineasset will be given. This step allows jumping from machine to machine.
- If the
In the associations section we define the relationship assets have. In this case, we have three relationships:
MachineandNetworkhave an N to M relationship, represented by the*.MachineandCredentialshave two relationships:- In the
Storagesense, they have a 1 to N relationship. A machine can store many credentials (storesCredsand*) and credentials can only be stored in one machine (storedOnand0..1). - In the
Accesssense, they also have a 1 to N relationship. A machine can be authenticated with many credentials (authCredsand*) and credentials can only authenticate one machine (authenticatesand0..1).
- In the
Once we have the MAL-Lang file, we can create a python script to automate the creation of Language Graphs and Models based on this MAL-language. Get deeper insight into MAL syntax, visit the MAL specification repository.
Create a python file in the same directory called tutorial1_model.py.
Copy this code into tutorial1_model.py:
from maltoolbox.model import Model
from maltoolbox.language import LanguageGraph
def create_model(lang_graph: LanguageGraph) -> Model:
"""Create a model with two machines, one network and one set of credentials"""
model = Model("my-model", lang_graph)
office_net = model.add_asset('Network', 'OfficeNet')
machine_1 = model.add_asset('Machine', 'Machine1')
machine_2 = model.add_asset('Machine', 'Machine2')
credentials_for_machine_2 = model.add_asset('Credentials', 'CredentialsForM2')
office_net.add_associated_assets('parties', {machine_1, machine_2})
machine_1.add_associated_assets('storesCreds', {credentials_for_machine_2})
machine_2.add_associated_assets('authCreds', {credentials_for_machine_2})
credentials_for_machine_2.defenses = {'encrypted': 1.0}
return modelIn this simple function, we create:
- Two instances of our
Machineasset (Machine1andMachine2), one instance of theNetworkasset (OfficeNet), and one of instance of theCredentialsasset (CredentialsForM2). - A connection between
Machine1,Machine2andOfficeNet. The string"parties"comes from theCommunicationassociation in the MAL language we created. - A connection between
Machine1andCredentialsForM2. The string"storesCreds"comes from theStorageassociation. - A connection between
Machine2andCredentialsForM2. The string"authCreds"comes from theAccessassociation. - The
credentials_for_machine_2.defenses = {'encrypted': 1.0}activate the defenseencryptedstep in theCredentialsasset. The1.0means the defense step is activated. If you don't wish to activate the defense, you can comment this line. Beware that if you activate the defense in the model (by NOT commenting this line), the defender will not perform any action in the simulations, for there is only one possible defensive action in our exampleLang and if the defense is already activated in the model, there are no actions left for the defender to carry out during the simulations.
To instantiate the model, we will create another file called tutorial1_simulation.py. This model will work as our main file and we will later learn about mal-simulator in it. Add this to the tutorial1_simulation.py file:
def main():
lang_file = "/path/to/exampleLang.mal"
current_dir = os.path.dirname(os.path.abspath(__file__))
lang_file_path = os.path.join(current_dir, lang_file)
example_lang = LanguageGraph.load_from_file(lang_file_path)
# Create our example model
model = create_model(example_lang)
print("Model created successfully!")
print(model)
if __name__ == "__main__":
main()And add these imports to the beginning of the file:
import os
from maltoolbox.language import LanguageGraph
from tutorial1_model import create_modelBy executing this code, we create a model using a language graph, which in turn has been defined using our MAL-Lang (exampleLang.mal --> LanguageGraph --> Model) . To do so, run the script with python tutorial1_simulation.py. The expected result should lool like this, regardless of the OS used to execute our script:
Model created successfully!
Model(name: "my-model", language: LanguageGraph(id: "exampleLang", version: "2.0.0"))You can see the final version of tutorial1_model.py here.
To create an attack graph, we use the model and exampleLang. Add this import to the top of the tutorial1_simulation.py file:
from maltoolbox.attackgraph import AttackGraphPut this line after model = create_model(example_lang):
# Generate an attack graph from the model
graph = AttackGraph(example_lang, model)The attack graph is a representation of the model that folds out all of the attack steps defined in the MAL language, in this case, exampleLang. This can be used to run analysis or simulations. We will learn how to visualize models and attack graphs in tutorial 2.
If you would like to know more about the concepts LanguageGraph, Model or AttackGraph, visit this Wiki.
To run simulations, add these imports to the top of the tutorial1_simulation.py file:
from malsim import MalSimulator, run_simulation, AttackerSettings, DefenderSettings
from malsim.policies import RandomAgentNow we can create a MalSimulator object from the attack graph graph and run simulations.
Add this to the end of the main function:
simulator = MalSimulator(graph, [])
path = run_simulation(simulator)When we run python tutorial1_simulation.py we will just see "Simulation over after 0 steps.". This is because we don't have any agents. Let us add an attacker agent.
To do so, replace the previous code with:
# Create agent settings
agent_settings = [
AttackerSettings(
"Attacker1",
entry_points={"Machine1:compromise"},
goals={"Machine2:compromise"},
policy=RandomAgent
)
]
simulator = MalSimulator(graph, agents=agent_settings)
run_simulation(simulator)
import pprint
pprint.pprint(simulator.recording)In this section, we define the AttackerSettings object:
Attacker1: the name we give to the attacker agent.entry_points: the node where we make the attacker start. An attacker can have more than oneentry_point.goals: the node or nodes that we want the attacker to reach. This is an optional parameter. The simulation is done when the goals are reached, or when all possible nodes are reached if no goal is set.policy: tells therun_simulationfunction which policy (policies can be found in malsim.policies).
This registers an attacker in the simulator, gives a dict of agents to run_simulation which will use the policy set in the AttackerSettings object. We then print the recording of the simulation.
You should see something like the following code box in your terminal after running python tutorial1_simulation.py. You won't see an exact copy of the expected results because the attack path, in this case, is not deterministic. Simulations can be deterministic if we gave a seed to the simulator.
Iteration 0
---
Iteration 1
---
...
Iteration 9
---
Simulation over after 10 steps.
Total reward "Attacker1" 0.0
defaultdict(<class 'dict'>,
Total reward "Attacker1" 0.0
defaultdict(<class 'dict'>,
{1: {'Attacker1': [AttackGraphNode(name: "OfficeNet:communicate", id: 0, type: or)]},
2: {'Attacker1': [AttackGraphNode(name: "Machine1:connect", id: 1, type: or)]},
3: {'Attacker1': [AttackGraphNode(name: "Machine2:connect", id: 5, type: or)]},
4: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:access", id: 9, type: or)]},
5: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:crack", id: 11, type: or)]},
6: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:use", id: 12, type: or)]},
7: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:useUnencrypted", id: 10, type: and)]},
8: {'Attacker1': [AttackGraphNode(name: "Machine2:authenticate", id: 6, type: or)]},
9: {'Attacker1': [AttackGraphNode(name: "Machine2:authCompromise", id: 7, type: and)]},
10: {'Attacker1': [AttackGraphNode(name: "Machine2:compromise", id: 8, type: or)]}})
(.venv) C:\\mal-tutorials\tutorials\tutorial1>
Total reward "Attacker1" 0.0
defaultdict(<class 'dict'>,
{1: {'Attacker1': [AttackGraphNode(name: "OfficeNet:communicate", id: 0, type: or)]},
2: {'Attacker1': [AttackGraphNode(name: "Machine1:connect", id: 1, type: or)]},
3: {'Attacker1': [AttackGraphNode(name: "Machine2:connect", id: 5, type: or)]},
4: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:access", id: 9, type: or)]},
5: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:crack", id: 11, type: or)]},
6: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:use", id: 12, type: or)]},
7: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:useUnencrypted", id: 10, type: and)]},
8: {'Attacker1': [AttackGraphNode(name: "Machine2:authenticate", id: 6, type: or)]},
9: {'Attacker1': [AttackGraphNode(name: "Machine2:authCompromise", id: 7, type: and)]},
10: {'Attacker1': [AttackGraphNode(name: "Machine2:compromise", id: 8, type: or)]}})
Total reward "Attacker1" 0.0
defaultdict(<class 'dict'>,
{1: {'Attacker1': [AttackGraphNode(name: "OfficeNet:communicate", id: 0, type: or)]},
2: {'Attacker1': [AttackGraphNode(name: "Machine1:connect", id: 1, type: or)]},
3: {'Attacker1': [AttackGraphNode(name: "Machine2:connect", id: 5, type: or)]},
4: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:access", id: 9, type: or)]},
Total reward "Attacker1" 0.0
defaultdict(<class 'dict'>,
{1: {'Attacker1': [AttackGraphNode(name: "OfficeNet:communicate", id: 0, type: or)]},
2: {'Attacker1': [AttackGraphNode(name: "Machine1:connect", id: 1, type: or)]},
3: {'Attacker1': [AttackGraphNode(name: "Machine2:connect", id: 5, type: or)]},
4: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:access", id: 9, type: or)]},
5: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:crack", id: 11, type: or)]},
6: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:use", id: 12, type: or)]},
7: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:useUnencrypted", id: 10, type: and)]},
8: {'Attacker1': [AttackGraphNode(name: "Machine2:authenticate", id: 6, type: or)]},
9: {'Attacker1': [AttackGraphNode(name: "Machine2:authCompromise", id: 7, type: and)]},
10: {'Attacker1': [AttackGraphNode(name: "Machine2:compromise", id: 8, type: or)]}})The first step will be to comment line 17 # credentials_for_machine_2.defenses = {'encrypted': 1.0} in the tutorial1_model.py file. We do this so that the defender can use this defense step. If it is already activated, the defender won't be able to activate it during the simulation.
Now, we add a defender to the current agent_settings:
# Create agent settings
agent_settings = [
AttackerSettings(
"Attacker1",
entry_points={"Machine1:compromise"},
goals={"Machine2:compromise"},
policy=RandomAgent
),
DefenderSettings(
"Defender1",
policy=RandomAgent
)
]In this section, we define the DefenderSettings object:
Defender1: the name we give to the defender agent.policy: The same policies are shared between attackers and defenders (policies can be found in malsim.policies).
Run the simulation again python tutorial1_simulation.py and you will see something similar to the codebox below.
Iteration 0
---
Iteration 1
---
...
Iteration 9
---
Total reward "Attacker1" 0.0
Total reward "Defender1" 0.0
defaultdict(<class 'dict'>,
{1: {'Attacker1': [AttackGraphNode(name: "OfficeNet:communicate", id: 0, type: or)],
'Defender1': [AttackGraphNode(name: "CredentialsForM2:encrypted", id: 13, type: defense)]},
2: {'Attacker1': [AttackGraphNode(name: "Machine2:connect", id: 5, type: or)],
'Defender1': []},
3: {'Attacker1': [AttackGraphNode(name: "Machine1:connect", id: 1, type: or)],
'Defender1': []},
4: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:access", id: 9, type: or)],
'Defender1': []},
5: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:crack", id: 11, type: or)],
'Defender1': []},
6: {'Attacker1': [AttackGraphNode(name: "CredentialsForM2:use", id: 12, type: or)],
'Defender1': []},
7: {'Attacker1': [AttackGraphNode(name: "Machine2:authenticate", id: 6, type: or)],
'Defender1': []},
8: {'Attacker1': [AttackGraphNode(name: "Machine2:authCompromise", id: 7, type: and)],
'Defender1': []},
9: {'Attacker1': [AttackGraphNode(name: "Machine2:compromise", id: 8, type: or)],
'Defender1': []}})As we can see, the defender uses the encrypted defense step, but the attacker still reaches its goal.
You can see the final version of tutorial1_simulation.py here.
This tutorial has shown how to build a mal-lang, a model, an language graph, an attack graph and run a simulation. Tutorial 2 will cover model-building from a given mal-lang, visualize an attack graph and give further insights on simulations.