csle-attack-profiler 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of csle-attack-profiler might be problematic. Click here for more details.

@@ -0,0 +1 @@
1
+ from . __version__ import __version__
@@ -0,0 +1 @@
1
+ __version__ = '0.5.2'
@@ -0,0 +1,204 @@
1
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action \
2
+ import EmulationAttackerAction
3
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action_id \
4
+ import EmulationAttackerActionId
5
+ from csle_attack_profiler.dao.tactics import Tactics
6
+ from csle_attack_profiler.dao.attack_mapping import EmulationAttackerMapping
7
+ from csle_attack_profiler.dao.attack_graph import AttackGraph
8
+ from mitreattack.stix20 import MitreAttackData
9
+ from typing import List, Dict, Union
10
+ import os
11
+
12
+
13
+ class AttackProfiler:
14
+ """
15
+ Class representing the attack profile based on the MITRE ATT&CK framework for Enterprise.
16
+ """
17
+
18
+ def __init__(self, techniques_tactics: Dict[str, List[str]], mitigations: Dict[str, List[str]],
19
+ data_sources: Dict[str, List[str]], subtechniques: Dict[str, str],
20
+ action_id: EmulationAttackerActionId) -> None:
21
+ """
22
+ Class constructor
23
+
24
+ :params techniques_tactics: the techniques and tactics used by the attacker action.
25
+ The key is the technique and the value is the tactics
26
+ :params mitigations: the mitigations used by the attacker action. The key is the technique and the
27
+ value is the mitigations
28
+ :params data_sources: the data sources used by the attacker action. The key is the technqiue and the value is
29
+ the data sources
30
+ :params sub_techniques: the sub-techniques used by the attacker action. The key is the technique and
31
+ the value is the sub-techniques
32
+ :params action_id: the id of the attacker action
33
+ """
34
+
35
+ self.techniques_tactics = techniques_tactics
36
+ self.mitigations = mitigations
37
+ self.data_sources = data_sources
38
+ self.subtechniques = subtechniques
39
+ self.action_id = action_id
40
+
41
+ @staticmethod
42
+ def get_attack_profile(attacker_action: EmulationAttackerAction) -> 'AttackProfiler':
43
+ """
44
+ Returns the attack profile of the actions
45
+
46
+ :params attacker_action: the attacker action
47
+ :return: the attack profile of the action
48
+ """
49
+ current_dir = os.path.dirname(__file__)
50
+ path = os.path.join(current_dir, "./dao/enterprise-attack.json")
51
+ mitre_attack_data = MitreAttackData(path)
52
+
53
+ # Retrieve the id from the attacker action
54
+ attacker_id = attacker_action.id
55
+ # Get the defined tactics and techniques for the attack
56
+ attack_mapping = EmulationAttackerMapping.get_attack_info(attacker_id)
57
+ if attack_mapping is None:
58
+ return AttackProfiler({}, {}, {}, {}, EmulationAttackerActionId.CONTINUE)
59
+
60
+ attack_techniques_vals = [technique.value for technique in attack_mapping['techniques']]
61
+
62
+ attacker_action_id = attacker_action.id
63
+ techniques_tactics = {}
64
+ mitigations = {}
65
+ data_sources = {}
66
+ sub_techniques = {}
67
+ # Loop over the techniques associated with the attack
68
+ for technique_name in attack_techniques_vals:
69
+ # Get technique from MitreAttackData, stix_id maps to technique in the library.
70
+ try:
71
+ obj = mitre_attack_data.get_objects_by_name(technique_name, "attack-pattern")
72
+ except Exception as e:
73
+ os.system("echo 'Error in getting technique: {}'".format(e))
74
+ continue
75
+ technique = obj[0]
76
+ stix_id = technique.id
77
+
78
+ # Collect the tactics and add it to the dictionary
79
+ tactics = [phase['phase_name'] for phase in technique.kill_chain_phases]
80
+ techniques_tactics[technique_name] = tactics
81
+
82
+ # Add the data sources to the dictionary
83
+ if hasattr(technique, 'x_mitre_data_sources'):
84
+ data_sources[technique_name] = technique.x_mitre_data_sources
85
+ # Fetch the mitigations from the technique and add it to the dictionary
86
+ try:
87
+ mitigations_object = mitre_attack_data.get_mitigations_mitigating_technique(stix_id)
88
+ mitigations_list = [mitig['object']['name'] for mitig in mitigations_object]
89
+ mitigations[technique_name] = mitigations_list
90
+ except Exception as e:
91
+ os.system("echo 'Error in getting mitigations: {}'".format(e))
92
+ continue
93
+
94
+ # Add the sub-technique to the dictionary
95
+ if 'subtechniques' in attack_mapping:
96
+ sub_techniques_mapping = [sub_technique.value for sub_technique in attack_mapping['subtechniques']]
97
+ for st in sub_techniques_mapping:
98
+ try:
99
+ sub_technique_obj = mitre_attack_data.get_objects_by_name(st, "attack-pattern")
100
+ parent_technique_obj = mitre_attack_data.get_parent_technique_of_subtechnique(
101
+ sub_technique_obj[0].id)
102
+ sub_techniques[parent_technique_obj[0]['object'].name] = st
103
+ except Exception as e:
104
+ os.system("echo 'Error in getting sub-techniques: {}'".format(e))
105
+ continue
106
+
107
+ return AttackProfiler(techniques_tactics, mitigations, data_sources, sub_techniques, attacker_action_id)
108
+
109
+ @staticmethod
110
+ def get_attack_profile_sequence(attacker_actions: List[EmulationAttackerAction],
111
+ attack_graph: Union[AttackGraph, None] = None) -> List['AttackProfiler']:
112
+ """
113
+ Returns the attack profile of the actions in a sequence
114
+
115
+ :params attacker_action: a list of attacker actions
116
+
117
+ :return: a list of attack profiles of the actions
118
+ """
119
+
120
+ attack_profiles = []
121
+ for action in attacker_actions:
122
+ attack_profiles.append(AttackProfiler.get_attack_profile(action))
123
+
124
+ # IF attack graph is provided
125
+ if attack_graph:
126
+
127
+ node = attack_graph.get_root_node()
128
+ for profile in attack_profiles:
129
+ # Get the mappings of the techniques and tactics
130
+ techniques_tactics = profile.techniques_tactics
131
+ techniques_to_keep = []
132
+ children = attack_graph.get_children(node[0], node[2])
133
+ possible_nodes = []
134
+ # First we check the techniques in node we are currently at
135
+ for technique in techniques_tactics:
136
+ # If the node.name is in the techniques_tactics, add it to the techniques_to_keep
137
+ if node[0].value in techniques_tactics[technique]:
138
+ techniques_to_keep.append(technique)
139
+ if node not in possible_nodes:
140
+ possible_nodes.append(node)
141
+ if children is None:
142
+ continue
143
+ for child in children:
144
+ # Child is a list of tuples, where the first element is the node name,
145
+ # and the second element is the node id
146
+ for technique in techniques_tactics:
147
+ if child[0].value in techniques_tactics[technique]:
148
+
149
+ techniques_to_keep.append(technique)
150
+ # If the child is not in the possible_children, add it to the list.
151
+ if attack_graph.get_node(child[0], child[1]) not in possible_nodes:
152
+ p_node = attack_graph.get_node(child[0], child[1])
153
+ if p_node is not None:
154
+ possible_nodes.append(p_node)
155
+
156
+ # If the possible node is just one node, move to that node
157
+ if len(possible_nodes) == 1:
158
+ node = possible_nodes[0]
159
+ if not techniques_to_keep:
160
+ continue
161
+ # Remove the techniques and associated tactics, data sources,
162
+ # mitigations and sub-techniques that are not in the techniques_to_keep
163
+ techniques_to_remove = set(profile.techniques_tactics.keys()) - set(techniques_to_keep)
164
+ for technique in techniques_to_remove:
165
+ try:
166
+ del profile.techniques_tactics[technique]
167
+ del profile.mitigations[technique]
168
+ del profile.data_sources[technique]
169
+ del profile.subtechniques[technique]
170
+ except Exception as e:
171
+ os.system("echo 'Error in removing techniques: {}'".format(e))
172
+ continue
173
+
174
+ # ELSE Baseline conditions
175
+ else:
176
+ initial_access = False
177
+ for profile in attack_profiles:
178
+ techniques_tactics = profile.techniques_tactics
179
+ techniques_to_remove = set()
180
+ # Loop over the mappings of the techniques to tactics
181
+ for technique in techniques_tactics:
182
+ if Tactics.DISCOVERY.value in techniques_tactics[technique] and not initial_access:
183
+ techniques_to_remove.add(technique)
184
+ elif Tactics.RECONNAISSANCE.value in techniques_tactics[technique] and initial_access:
185
+ techniques_to_remove.add(technique)
186
+ if Tactics.INITIAL_ACCESS.value in techniques_tactics[technique] and not initial_access:
187
+ initial_access = True
188
+ elif Tactics.INITIAL_ACCESS.value in techniques_tactics[technique] and initial_access:
189
+ techniques_to_remove.add(technique)
190
+ elif Tactics.LATERAL_MOVEMENT.value in techniques_tactics[technique] and not initial_access:
191
+ techniques_to_remove.add(technique)
192
+
193
+ # Remove the techniques and associated tactics, data sources, mitigations and sub-techniques
194
+ for technique in techniques_to_remove:
195
+ try:
196
+ del profile.techniques_tactics[technique]
197
+ del profile.mitigations[technique]
198
+ del profile.data_sources[technique]
199
+ del profile.subtechniques[technique]
200
+ except Exception as e:
201
+ os.system("echo 'Error in removing techniques: {}'".format(e))
202
+ continue
203
+
204
+ return attack_profiles
@@ -0,0 +1,449 @@
1
+ from csle_common.dao.system_identification.emulation_statistics import EmulationStatistics
2
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action_id import EmulationAttackerActionId
3
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action_type import EmulationAttackerActionType
4
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action_outcome import EmulationAttackerActionOutcome
5
+ from csle_common.dao.emulation_action.attacker.emulation_attacker_action import EmulationAttackerAction
6
+ from csle_attack_profiler.attack_profiler import AttackProfiler
7
+ from typing import List, Union, Tuple, Any
8
+ import numpy as np
9
+ import sys
10
+
11
+
12
+ class HMMProfiler:
13
+ """
14
+ The HMMProfiler class is used to profile a sequence of observations based on a Hidden Markov Model (HMM).
15
+ """
16
+
17
+ def __init__(self, statistics: List[EmulationStatistics], model_name: Union[str, None] = None) -> None:
18
+ """
19
+ Class constructor
20
+
21
+ :param statistics: The list of EmulationStatistics objects
22
+ :param model_name: The name of the model
23
+ """
24
+ self.statistics = statistics
25
+ self.transition_matrix: List[List[float]] = []
26
+ self.emission_matrix: List[List[float]] = []
27
+ self.hidden_states: List[str] = []
28
+ self.emission_matrix_observations: List[int] = []
29
+ self.start_state_probs: List[float] = []
30
+ self.model_name = None
31
+
32
+ def create_model(self, transition_matrix: List[List[float]],
33
+ hidden_states: List[str], metric: str,
34
+ save_model: bool = False, location: str = ".") -> None:
35
+ """
36
+ Creates the HMM model based on the given transition matrix, states and metrics.
37
+ If save = True, matrices are saved to given location
38
+
39
+ :param transition_matrix: The transition matrix
40
+ :param states: The list of states of the HMM (format: 'A:attack_name' or
41
+ 'no_intrusion' based on emulation statistics file)
42
+ :param metrics: The list of metrics to profile
43
+ :param save: Whether to save the matrices to a file
44
+ :param location: The location to save the matrices, if save = True, e.g "./resources",
45
+ default is current directory
46
+ """
47
+ emission_matrix, emission_matrix_observations = self.get_matrices_of_observation(self.statistics,
48
+ metric, hidden_states)
49
+ self.emission_matrix = emission_matrix
50
+ self.emission_matrix_observations = emission_matrix_observations
51
+ self.transition_matrix = transition_matrix
52
+ self.start_state_probs = self.calculate_initial_states(self.transition_matrix)
53
+ self.hidden_states = hidden_states
54
+ if save_model and location:
55
+ np.save(f'{location}/transition_matrix.npy', transition_matrix)
56
+ np.save(f'{location}/hidden_states.npy', hidden_states)
57
+ np.save(f'{location}/start_state_probs.npy', self.start_state_probs)
58
+ np.save(f'{location}/emission_matrix_{metric}.npy', emission_matrix)
59
+ np.save(f'{location}/emission_matrix_observations_{metric}.npy', emission_matrix_observations)
60
+
61
+ def load_model(self, location: str, metric: str) -> None:
62
+ """
63
+ Loads the HMM model from the given location.
64
+
65
+ :param location: The location of the model files, default is current directory
66
+ :param metric: The data metric to use
67
+ """
68
+ self.transition_matrix = np.load(f'{location}/transition_matrix.npy')
69
+ self.hidden_states = np.load(f'{location}/hidden_states.npy')
70
+ self.start_state_probs = np.load(f'{location}/start_state_probs.npy')
71
+ self.emission_matrix = np.load(f'{location}/emission_matrix_{metric}.npy')
72
+ self.emission_matrix_observations = np.load(f'{location}/emission_matrix_observations_{metric}.npy')
73
+
74
+ def profile_sequence(self, sequence: List[int]) -> List[str]:
75
+ """
76
+ Profiles a sequence of observations based on the HMM model.
77
+
78
+ :param sequence: The sequence of observations
79
+ :return: The most likely sequence of states
80
+ """
81
+
82
+ path = HMMProfiler.viterbi(self.hidden_states, self.start_state_probs,
83
+ self.transition_matrix, self.emission_matrix,
84
+ sequence, self.emission_matrix_observations)
85
+ profiled_sequence = []
86
+ for i in range(len(path)):
87
+ profiled_sequence.append(self.hidden_states[int(path[i])])
88
+
89
+ return profiled_sequence
90
+
91
+ def get_matrices_of_observation(self, statistics: List[EmulationStatistics],
92
+ metric: str, states: List[str]) -> Tuple[List[List[float]], List[int]]:
93
+ """
94
+ Creates the emission matrix for a given metric based on the statistics from the EmulationStatistics objects.
95
+
96
+ :param statistics: The list of EmulationStatistics objects
97
+ :param metric: The metric to get the emission matrix for
98
+ :return: The emission matrix, the list of observations, the list of states
99
+ """
100
+ emission_matrix = []
101
+ attack_observations = {}
102
+ attack_observations_total_counts = {}
103
+ all_keys = set()
104
+
105
+ for stats in statistics:
106
+ for condition, metric_distribution in stats.conditionals_counts.items():
107
+ action = condition.split('_')
108
+ if action[0] == 'no':
109
+ action[0] = 'no_intrusion'
110
+ if action[0] not in attack_observations:
111
+ # We are not intrested in the observations from 'intrusion' or 'A:Continue'
112
+ if action[0] == 'intrusion' or action[0] == 'A:Continue':
113
+ continue
114
+ else:
115
+ # Add the observations of the attack to the dictionary
116
+ if metric in metric_distribution:
117
+ attack_observations[action[0]] = metric_distribution[metric]
118
+ # Sum the total counts of the observations
119
+ attack_observations_total_counts[action[0]] = sum(attack_observations[action[0]].values())
120
+ # Aggregate the counts from the metric distribution
121
+ else:
122
+ counts_observation = metric_distribution[metric]
123
+ for element in counts_observation:
124
+ if element in attack_observations[action[0]]:
125
+ # Aggregate the counts if the element is already in the dictionary
126
+ attack_observations[action[0]][element] += counts_observation[element]
127
+ else:
128
+ attack_observations[action[0]][element] = counts_observation[element]
129
+ # Sum the total counts of the observations
130
+ attack_observations_total_counts[action[0]] += sum(attack_observations[action[0]].values())
131
+
132
+ # Store all possible values for the observation
133
+ if action[0] in attack_observations:
134
+ all_keys.update(attack_observations[action[0]])
135
+
136
+ # Normalize the counts
137
+ for attack, _ in attack_observations.items():
138
+ attack_observations_total_counts[attack] = sum(attack_observations[attack].values())
139
+ for key in all_keys:
140
+ int_key = int(key)
141
+ if key in attack_observations[attack]:
142
+ count = attack_observations[attack].pop(key, 0)
143
+ attack_observations[attack][int_key] = count / attack_observations_total_counts[attack]
144
+ else:
145
+ attack_observations[attack][int_key] = 0
146
+ # Sort the dictionary by key
147
+ attack_observations[attack] = dict(sorted(attack_observations[attack].items()))
148
+
149
+ # Take any attack as the reference to get the keys
150
+ emission_matrix_observations = []
151
+ emission_matrix_states = []
152
+ # Create the emission matrix
153
+ for state in states:
154
+ if state in attack_observations:
155
+ # Normalize the and then append
156
+ emission_matrix.append(list(attack_observations[state].values()))
157
+ # Get the keys of all observations
158
+ emission_matrix_observations = list(attack_observations[state].keys())
159
+ emission_matrix_states.append(state)
160
+ else:
161
+ # LaPlace smoothing for missing observations
162
+ num_keys = len(all_keys)
163
+ laplace_probability = 1 / (num_keys + 2)
164
+ laplace_sum = laplace_probability * num_keys
165
+ laplace_probability_adj = laplace_probability / laplace_sum
166
+ emission_matrix.append([laplace_probability_adj] * num_keys)
167
+ emission_matrix_states.append(state)
168
+
169
+ # Check if the sum of the probabilities is 1
170
+ for i in range(len(emission_matrix)):
171
+ sum_prob = round(sum(emission_matrix[i]), 10)
172
+ if sum_prob != 1:
173
+ print(f'Sum of probabilities for state {emission_matrix_states[i]} is {sum_prob}')
174
+
175
+ return (emission_matrix, emission_matrix_observations)
176
+
177
+ def convert_states_to_profiles(self, states: List[str]) -> List[Union[AttackProfiler, str]]:
178
+ """
179
+ Converts a list of states to a list of AttackProfiles.
180
+
181
+ :param states: The list of states to convert
182
+ :return: The list of EmulationAttackerActionId
183
+ """
184
+
185
+ new_states: List[Union[AttackProfiler, str]] = []
186
+ for state in states:
187
+ if state == 'A:Continue':
188
+ action = EmulationAttackerAction(id=EmulationAttackerActionId.CONTINUE, name="Continue", cmds=[],
189
+ type=EmulationAttackerActionType.CONTINUE, descr="CONTINUE", ips=[],
190
+ index=0, action_outcome=EmulationAttackerActionOutcome.CONTINUE)
191
+ p = AttackProfiler.get_attack_profile(action)
192
+ new_states.append(p)
193
+ elif state == 'A:CVE-2015-1427 exploit':
194
+ action = EmulationAttackerAction(
195
+ id=EmulationAttackerActionId.CVE_2015_1427_EXPLOIT, name="CVE-2015-1427 exploit", cmds=[],
196
+ type=EmulationAttackerActionType.EXPLOIT,
197
+ descr="Uses the CVE-2015-1427 vulnerability to "
198
+ "get remote code execution and then sets up a SSH backdoor"
199
+ "to upgrade the channel", index=-1, ips=[],
200
+ action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
201
+ p = AttackProfiler.get_attack_profile(action)
202
+ new_states.append(p)
203
+ elif state == 'A:DVWA SQL Injection Exploit':
204
+ action = EmulationAttackerAction(
205
+ id=EmulationAttackerActionId.DVWA_SQL_INJECTION, name="DVWA SQL Injection Exploit",
206
+ cmds=[], type=EmulationAttackerActionType.EXPLOIT,
207
+ descr="Uses the DVWA SQL Injection exploit to extract secret passwords",
208
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
209
+ p = AttackProfiler.get_attack_profile(action)
210
+ new_states.append(p)
211
+ elif state == 'A:Install tools':
212
+ action = EmulationAttackerAction(
213
+ id=EmulationAttackerActionId.INSTALL_TOOLS, name="Install tools", cmds=[],
214
+ type=EmulationAttackerActionType.POST_EXPLOIT,
215
+ descr="If taken root on remote machine, installs pentest tools, e.g. nmap",
216
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.PIVOTING)
217
+ p = AttackProfiler.get_attack_profile(action)
218
+ new_states.append(p)
219
+ elif state == 'A:Network service login':
220
+ action = EmulationAttackerAction(
221
+ id=EmulationAttackerActionId.NETWORK_SERVICE_LOGIN, name="Network service login",
222
+ cmds=[], type=EmulationAttackerActionType.POST_EXPLOIT,
223
+ descr="Uses known credentials to login to network services on a server",
224
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.LOGIN)
225
+ p = AttackProfiler.get_attack_profile(action)
226
+ new_states.append(p)
227
+ elif state == 'A:Ping Scan':
228
+ action = EmulationAttackerAction(
229
+ id=EmulationAttackerActionId.PING_SCAN_HOST, name="Ping Scan",
230
+ cmds=[], type=EmulationAttackerActionType.RECON,
231
+ descr="A host discovery scan, it is quick because it only checks of hosts "
232
+ "are up with Ping, without scanning the ports.", ips=[], index=-1,
233
+ action_outcome=EmulationAttackerActionOutcome.INFORMATION_GATHERING, backdoor=False)
234
+ p = AttackProfiler.get_attack_profile(action)
235
+ new_states.append(p)
236
+ elif state == 'A:Sambacry Explolit':
237
+ action = EmulationAttackerAction(
238
+ id=EmulationAttackerActionId.SAMBACRY_EXPLOIT, name="Sambacry Explolit", cmds=[],
239
+ type=EmulationAttackerActionType.EXPLOIT,
240
+ descr="Uses the sambacry shell to get remote code execution and then"
241
+ "sets up a SSH backdoor to upgrade the channel",
242
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
243
+ p = AttackProfiler.get_attack_profile(action)
244
+ new_states.append(p)
245
+ elif state == 'A:ShellShock Explolit':
246
+ action = EmulationAttackerAction(
247
+ id=EmulationAttackerActionId.SHELLSHOCK_EXPLOIT, name="ShellShock Exploit",
248
+ cmds=[], type=EmulationAttackerActionType.EXPLOIT,
249
+ descr="Uses the Shellshock exploit and curl to do remote code execution and create a backdoor",
250
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
251
+ p = AttackProfiler.get_attack_profile(action)
252
+ new_states.append(p)
253
+ elif state == 'A:SSH dictionary attack for username=pw':
254
+ action = EmulationAttackerAction(
255
+ id=EmulationAttackerActionId.SSH_SAME_USER_PASS_DICTIONARY_HOST,
256
+ name="SSH dictionary attack for username=pw", cmds=[],
257
+ type=EmulationAttackerActionType.EXPLOIT, index=-1,
258
+ descr="A dictionary attack that tries common passwords and usernames for SSH"
259
+ "where username=password", ips=[], action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
260
+ p = AttackProfiler.get_attack_profile(action)
261
+ new_states.append(p)
262
+ elif state == 'A:FTP dictionary attack for username=pw':
263
+ action = EmulationAttackerAction(
264
+ id=EmulationAttackerActionId.FTP_SAME_USER_PASS_DICTIONARY_HOST,
265
+ name="FTP dictionary attack for username=pw", cmds=[], type=EmulationAttackerActionType.EXPLOIT,
266
+ index=-1, descr="A dictionary attack that tries common passwords and"
267
+ "usernames for FTP where username=password", ips=[],
268
+ action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
269
+ p = AttackProfiler.get_attack_profile(action)
270
+ new_states.append(p)
271
+ elif state == 'A:Telnet dictionary attack for username=pw':
272
+ action = EmulationAttackerAction(
273
+ id=EmulationAttackerActionId.TELNET_SAME_USER_PASS_DICTIONARY_HOST,
274
+ name="Telnet dictionary attack for username=pw", cmds=[],
275
+ type=EmulationAttackerActionType.EXPLOIT, index=-1,
276
+ descr="A dictionary attack that tries common passwords and usernames for"
277
+ "Telnet where username=password", ips=[],
278
+ action_outcome=EmulationAttackerActionOutcome.SHELL_ACCESS)
279
+ p = AttackProfiler.get_attack_profile(action)
280
+ new_states.append(p)
281
+ elif state == 'A:CVE-2010-0426 exploit':
282
+ action = EmulationAttackerAction(
283
+ id=EmulationAttackerActionId.CVE_2010_0426_PRIV_ESC,
284
+ name="CVE-2010-0426 exploit", cmds=[], type=EmulationAttackerActionType.PRIVILEGE_ESCALATION,
285
+ descr="Uses the CVE-2010-0426 vulnerability to perform privilege escalation to get root access",
286
+ index=-1, ips=[], action_outcome=EmulationAttackerActionOutcome.PRIVILEGE_ESCALATION_ROOT)
287
+ p = AttackProfiler.get_attack_profile(action)
288
+ new_states.append(p)
289
+ elif state == 'A:TCP SYN (Stealth) Scan':
290
+ action = EmulationAttackerAction(
291
+ id=EmulationAttackerActionId.TCP_SYN_STEALTH_SCAN_HOST, name="TCP SYN (Stealth) Scan",
292
+ cmds=[], type=EmulationAttackerActionType.RECON,
293
+ descr="A stealthy and fast TCP SYN scan to detect open TCP ports on the subnet", ips=[],
294
+ index=-1, action_outcome=EmulationAttackerActionOutcome.INFORMATION_GATHERING, backdoor=False)
295
+ p = AttackProfiler.get_attack_profile(action)
296
+ new_states.append(p)
297
+ elif state == 'ssh backdoor':
298
+ action = EmulationAttackerAction(
299
+ id=EmulationAttackerActionId.SSH_BACKDOOR, name="Install SSH backdoor",
300
+ cmds=[], type=EmulationAttackerActionType.POST_EXPLOIT,
301
+ descr="If taken root on remote machine, installs a ssh backdoor useful for"
302
+ "upgrading telnetor weaker channels", index=-1, ips=[],
303
+ action_outcome=EmulationAttackerActionOutcome.PIVOTING, alt_cmds=None, backdoor=True)
304
+ p = AttackProfiler.get_attack_profile(action)
305
+ new_states.append(p)
306
+ else:
307
+ new_states.append(state)
308
+
309
+ return new_states
310
+
311
+ def calculate_initial_states(self, transition_matrix: List[List[float]]) -> List[float]:
312
+ """
313
+ Calculates the initial states probabilities based on the transition matrix.
314
+
315
+ 1 / (# of states)
316
+
317
+ :param transition_matrix: The transition matrix
318
+
319
+ :return: The start states probabilities
320
+ """
321
+ start_states = []
322
+ total_states = len(transition_matrix)
323
+ for _ in range(total_states):
324
+ start_states.append(1 / total_states)
325
+
326
+ return start_states
327
+
328
+ @staticmethod
329
+ def viterbi(hidden_states: List[EmulationAttackerActionId], init_probs: List[float],
330
+ trans_matrix: List[List[float]], emission_matrix: List[List[float]],
331
+ obs: List[int], emissions_list: List[int]) -> List[float]:
332
+ """
333
+ Viterbi algorithm for Hidden Markov Models (HMM).
334
+
335
+ :param hidden_states: The hidden states
336
+ :param init_probs: The initial probabilities of the hidden states
337
+ :param trans_matrix: The transition matrix
338
+ :param emission_matrix: The emission matrix
339
+ :param obs: The observation sequence
340
+ :param emissions_list: The list of possible observations
341
+
342
+ :return: The most likely sequence of hidden states
343
+ """
344
+ # Convert the emissions list to a numpy array, to use the where function
345
+ emissions_list_typed: np.ndarray[int, Any] = np.array(emissions_list)
346
+
347
+ # Check that the sum equals 1
348
+ for i in range(len(emission_matrix)):
349
+ if round(sum(emission_matrix[i]), 10) != 1:
350
+ print(f'Sum of probabilities for state {hidden_states[i]} is not 1')
351
+ print(f'Sum of probabilities: {sum(emission_matrix[i])}')
352
+
353
+ # The number of hidden states
354
+ S = len(hidden_states)
355
+ # The number of observations
356
+ T = len(obs)
357
+
358
+ # The Viterbi matrix (prob) T x S matrix of zeroes
359
+ prob = np.zeros((T, S))
360
+ # The backpointer matrix (prev)
361
+ prev = np.empty((T, S))
362
+ # Initialization
363
+ for i in range(S):
364
+ # Fetch the index of the observation in the emission_matrix
365
+ index, = np.where(emissions_list_typed == obs[0])
366
+ if index[0].size > 0:
367
+ prob[0][i] = init_probs[i] * emission_matrix[i][index[0]]
368
+ else:
369
+ print(f'Observation {obs[0]} not found in the emission matrix')
370
+ sys.exit(1)
371
+
372
+ # Recursion
373
+ for t in range(1, T):
374
+ index, = np.where(emissions_list_typed == obs[t])
375
+ for i in range(S):
376
+ max_prob = -1
377
+ max_state = -1
378
+ for j in range(S):
379
+ new_prob = prob[t - 1][j] * trans_matrix[j][i] * emission_matrix[i][index[0]]
380
+ if new_prob > max_prob:
381
+ max_prob = new_prob
382
+ max_state = j
383
+ prob[t][i] = max_prob
384
+ prev[t][i] = max_state
385
+
386
+ path = np.zeros(T)
387
+ path[T - 1] = np.argmax(prob[T - 1])
388
+ for t in range(T - 2, -1, -1):
389
+ path[t] = prev[t + 1][int(path[t + 1])]
390
+ # Convert the path to a list
391
+ typed_path: List[float] = path.tolist()
392
+
393
+ return typed_path
394
+
395
+ def generate_sequence(self, intrusion_length: int, initial_state_index: int,
396
+ seed: Union[int, None] = None) -> Tuple[List[str], List[int]]:
397
+ """
398
+ Generates a sequence of states and corresponding observations based on the given emission matrix,
399
+ and transition matrix. First, a sequence of observation from 'no intrusion' is generated
400
+ based on the geometric distribution of the initial state. Then, a sequence observations and states are
401
+ generated based on emission matrix and transition matrix. The length of this intrusion
402
+ sequence is given by the intrusion_length parameter.
403
+
404
+ :param P_obs: The emission matrix
405
+ :param P_states: The transition matrix
406
+ :param states: The list of states
407
+ :param observations: The list of observations
408
+ :param intrusion_length: The length of the intrusion
409
+ :param initial_state: The initial state
410
+ return: The sequence of states and observations
411
+ """
412
+ P_obs = self.emission_matrix
413
+ P_states = self.transition_matrix
414
+ states = self.hidden_states
415
+ observations = self.emission_matrix_observations
416
+ if seed:
417
+ np.random.seed(seed)
418
+ obs_len = len(observations)
419
+ states_len = len(states)
420
+
421
+ # Return the geometric distribution of the initial state
422
+ dist = np.random.geometric(p=P_states[initial_state_index][0], size=1000)
423
+ T_i = round(sum(dist) / len(dist))
424
+
425
+ state_seq = [states[initial_state_index]] * T_i
426
+ obs_seq = []
427
+ for i in range(T_i):
428
+
429
+ o_i = np.random.choice(obs_len, p=P_obs[initial_state_index])
430
+ obs_seq.append(observations[o_i])
431
+
432
+ recon_states_sum = np.sum(P_states[initial_state_index][1:])
433
+ recon_states = P_states[initial_state_index][1:] / recon_states_sum
434
+
435
+ intrusion_start_state = np.random.choice(states_len - 1, p=recon_states)
436
+ intrusion_start_observation = np.random.choice(obs_len, p=P_obs[intrusion_start_state])
437
+ state_seq.append(states[intrusion_start_state])
438
+ obs_seq.append(observations[intrusion_start_observation])
439
+
440
+ s_i = intrusion_start_state
441
+ for i in range(intrusion_length):
442
+ # si ~ Ps(si | si-1)
443
+ s_i = np.random.choice(states_len, p=P_states[s_i])
444
+ # oi ~ Po(oi | si)
445
+ o_i = np.random.choice(obs_len, p=P_obs[s_i])
446
+ state_seq.append(states[s_i])
447
+ obs_seq.append(observations[o_i])
448
+
449
+ return state_seq, obs_seq
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.1
2
+ Name: csle-attack-profiler
3
+ Version: 0.5.2
4
+ Summary: Library with MITRE attack profiler for CSLE
5
+ Author: Bength Pappila
6
+ Author-email: brpa@kth.se
7
+ License: Creative Commons Attribution-ShareAlike 4.0 International
8
+ Keywords: Reinforcement-Learning Cyber-Security Markov-Games Markov-Decision-Processes
9
+ Platform: unix
10
+ Platform: linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Intended Audience :: Science/Research
16
+ Requires-Python: >=3.8
17
+ Requires-Dist: mitreattack-python ==2.0.14
18
+ Requires-Dist: csle-base ==0.5.2
19
+ Requires-Dist: csle-common ==0.5.2
20
+ Provides-Extra: testing
21
+ Requires-Dist: pytest >=6.0 ; extra == 'testing'
22
+ Requires-Dist: pytest-cov >=2.0 ; extra == 'testing'
23
+ Requires-Dist: pytest-mock >=3.6.0 ; extra == 'testing'
24
+ Requires-Dist: grpcio >=1.57.0 ; extra == 'testing'
25
+ Requires-Dist: grpcio-tools >=1.57.0 ; extra == 'testing'
26
+ Requires-Dist: pytest-grpc >=0.8.0 ; extra == 'testing'
27
+ Requires-Dist: mypy >=1.4.1 ; extra == 'testing'
28
+ Requires-Dist: mypy-extensions >=1.0.0 ; extra == 'testing'
29
+ Requires-Dist: mypy-protobuf >=3.5.0 ; extra == 'testing'
30
+ Requires-Dist: types-PyYAML >=6.0.12.11 ; extra == 'testing'
31
+ Requires-Dist: types-paramiko >=3.2.0.0 ; extra == 'testing'
32
+ Requires-Dist: types-protobuf >=4.23.0.3 ; extra == 'testing'
33
+ Requires-Dist: types-requests >=2.31.0.1 ; extra == 'testing'
34
+ Requires-Dist: types-urllib3 >=1.26.25.13 ; extra == 'testing'
35
+ Requires-Dist: flake8 >=6.1.0 ; extra == 'testing'
36
+ Requires-Dist: flake8-rst-docstrings >=0.3.0 ; extra == 'testing'
37
+ Requires-Dist: tox >=3.24 ; extra == 'testing'
38
+ Requires-Dist: sphinx >=5.3.0 ; extra == 'testing'
39
+ Requires-Dist: sphinxcontrib-napoleon >=0.7 ; extra == 'testing'
40
+ Requires-Dist: sphinx-rtd-theme >=1.1.1 ; extra == 'testing'
41
+ Requires-Dist: twine >=4.0.2 ; extra == 'testing'
42
+ Requires-Dist: build >=0.10.0 ; extra == 'testing'
43
+
@@ -0,0 +1,8 @@
1
+ csle_attack_profiler/__init__.py,sha256=C7_gE0lIQJ8Wh2jgU8C8P_xyvq76bKTf0gm8bGYhMBk,37
2
+ csle_attack_profiler/__version__.py,sha256=pIxUWQG2brcif6SJXtn4CcMr6f5oY-6lwZ_FJEilJwQ,22
3
+ csle_attack_profiler/attack_profiler.py,sha256=bYqSeItJkhswronvIRFPyvf1rtvUKEWPPF4K2t1JVqE,10535
4
+ csle_attack_profiler/hmm_profiling.py,sha256=6xlvv3Q84E47AYEwH1c89-oAaJb89XJXcNnmS-aO9qg,24128
5
+ csle_attack_profiler-0.5.2.dist-info/METADATA,sha256=5IUQwyu9Ovtcb7o6P2-s5bu_n9NluBGWHgyHzF267wM,2006
6
+ csle_attack_profiler-0.5.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
+ csle_attack_profiler-0.5.2.dist-info/top_level.txt,sha256=OuI4zvPo3MQYLQ7Dqm32oM0V8rNdwHe9tjtngx2KVtA,21
8
+ csle_attack_profiler-0.5.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ csle_attack_profiler