graph_blast_radius_fl()

Learn how to use the graph_blast_radius_fl() function to calculate the Blast Radius of source nodes over path or edge data.

Calculate the Blast Radius (list and score) of source nodes over path or edge data.

The function graph_blast_radius_fl() is a UDF (user-defined function) that allows you to calculate the Blast Radius of each of the source nodes based on paths or edges data. Each row of input data contains a source node and a target node, which can represent direct connections (edges) between nodes and targets, or longer multi-hop paths between them. If the paths aren’t available, we can first discover them using the graph-match operator or graph_path_discovery_fl() function. Then graph_blast_radius_fl() can be executed on top of the output of path discovery.

Blast Radius represents the connectivity of a specific source node to relevant targets. The more targets the source can access, the more effect it has if it’s compromised by the attacker - hence the name. Nodes with high Blast Radius are important in the cybersecurity domain due to the potential damage they might cause and to being highly valued by attackers. Thus, nodes with high Blast Radius should be protected accordingly - in terms of hardening and prioritizing security signals such as alerts.

The function outputs a list of connected targets for each source and also a score representing targets’ number. Optionally, in case there’s a meaningful ‘weight’ for each target (such as criticality or cost), a weighted score is calculated as a sum of targets’ weights. In addition, the limits for maximum total number of shown sources and maximum number of targets in each list are exposed as optional parameters for better control.

Syntax

graph_blast_radius_fl(sourceIdColumnName, targetIdColumnName, [targetWeightColumnName], [resultCountLimit], [listedIdsLimit])

Parameters

NameTypeRequiredDescription
sourceIdColumnNamestring✔️The name of the column containing the source node IDs (either for edges or paths).
targetIdColumnNamestring✔️The name of the column containing the target node IDs (either for edges or paths).
targetWeightColumnNamestringThe name of the column containing the target nodes’ weights (such as criticality). If no relevant weights are present, the weighted score is equal to 0. The default column name is noWeightsColumn.
resultCountLimitlongThe maximum number of returned rows (sorted by descending score). The default value is 100000.
listedIdsLimitlongThe maximum number of targets listed for each source. The default value is 50.

Function definition

You can define the function by either embedding its code as a query-defined function, or creating it as a stored function in your database, as follows:

Query-defined

Define the function using the following let statement. No permissions are required.

let graph_blast_radius_fl = (T:(*), sourceIdColumnName:string, targetIdColumnName:string, targetWeightColumnName:string = 'noWeightsColumn'
    , resultCountLimit:long = 100000, listedIdsLimit:long = 50)
{
let paths = (
    T
    | extend sourceId           = column_ifexists(sourceIdColumnName, '')
    | extend targetId           = column_ifexists(targetIdColumnName, '')
    | extend targetWeight       = tolong(column_ifexists(targetWeightColumnName, 0))
);
let aggregatedPaths = (
    paths
    | sort by sourceId, targetWeight desc
    | summarize blastRadiusList = array_slice(make_set_if(targetId, isnotempty(targetId)), 0, (listedIdsLimit - 1))
                , blastRadiusScore = dcountif(targetId, isnotempty(targetId))
                , blastRadiusScoreWeighted = sum(targetWeight)
        by sourceId
    | extend isBlastRadiusListCapped = (blastRadiusScore > listedIdsLimit)
);
aggregatedPaths
| top resultCountLimit by blastRadiusScore desc
};
// Write your query to use the function here.

Stored

Define the stored function once using the following .create function. Database User permissions are required.

.create-or-alter function with (docstring = "Calculate the Blast Radius (list and score) of source nodes over path or edge data", skipvalidation = "true", folder = 'Cybersecurity') 
graph_blast_radius_fl (T:(*), sourceIdColumnName:string, targetIdColumnName:string, targetWeightColumnName:string = 'noWeightsColumn'
    , resultCountLimit:long = 100000, listedIdsLimit:long = 50)
{
let paths = (
    T
    | extend sourceId           = column_ifexists(sourceIdColumnName, '')
    | extend targetId           = column_ifexists(targetIdColumnName, '')
    | extend targetWeight       = tolong(column_ifexists(targetWeightColumnName, 0))
);
let aggregatedPaths = (
    paths
    | sort by sourceId, targetWeight desc
    | summarize blastRadiusList = array_slice(make_set_if(targetId, isnotempty(targetId)), 0, (listedIdsLimit - 1))
                , blastRadiusScore = dcountif(targetId, isnotempty(targetId))
                , blastRadiusScoreWeighted = sum(targetWeight)
        by sourceId
    | extend isBlastRadiusListCapped = (blastRadiusScore > listedIdsLimit)
);
aggregatedPaths
| top resultCountLimit by blastRadiusScore desc
}

Example

The following example uses the invoke operator to run the function.

Query-defined

To use a query-defined function, invoke it after the embedded function definition.

let connections = datatable (SourceNodeName:string, TargetNodeName:string, TargetNodeCriticality:int)[						
    'vm-work-1',            'webapp-prd', 	          3,
    'vm-custom',        	'webapp-prd', 	          3,
    'webapp-prd',           'vm-custom', 	          1,
    'webapp-prd',       	'test-machine', 	      1,
    'vm-custom',        	'server-0126', 	          1,
    'vm-custom',        	'hub_router', 	          2,
    'webapp-prd',       	'hub_router', 	          2,
    'test-machine',       	'vm-custom',              1,
    'test-machine',        	'hub_router', 	          2,
    'hub_router',           'remote_DT', 	          1,
    'vm-work-1',            'storage_main_backup', 	  5,
    'hub_router',           'vm-work-2', 	          1,
    'vm-work-2',        	'backup_prc', 	          3,
    'remote_DT',            'backup_prc', 	          3,
    'backup_prc',           'storage_main_backup', 	  5,
    'backup_prc',           'storage_DevBox', 	      1,
    'device_A1',            'sevice_B2', 	          2,
    'sevice_B2',            'device_A1', 	          2
];
let graph_blast_radius_fl = (T:(*), sourceIdColumnName:string, targetIdColumnName:string, targetWeightColumnName:string = 'noWeightsColumn'
    , resultCountLimit:long = 100000, listedIdsLimit:long = 50)
{
let paths = (
    T
    | extend sourceId           = column_ifexists(sourceIdColumnName, '')
    | extend targetId           = column_ifexists(targetIdColumnName, '')
    | extend targetWeight       = tolong(column_ifexists(targetWeightColumnName, 0))
);
let aggregatedPaths = (
    paths
    | sort by sourceId, targetWeight desc
    | summarize blastRadiusList = array_slice(make_set_if(targetId, isnotempty(targetId)), 0, (listedIdsLimit - 1))
                , blastRadiusScore = dcountif(targetId, isnotempty(targetId))
                , blastRadiusScoreWeighted = sum(targetWeight)
        by sourceId
    | extend isBlastRadiusListCapped = (blastRadiusScore > listedIdsLimit)
);
aggregatedPaths
| top resultCountLimit by blastRadiusScore desc
};
connections
| invoke graph_blast_radius_fl(sourceIdColumnName 		= 'SourceNodeName'
                            , targetIdColumnName 		= 'TargetNodeName'
                            , targetWeightColumnName 	= 'TargetNodeCriticality'
)

Stored

let connections = datatable (SourceNodeName:string, TargetNodeName:string, TargetNodeCriticality:int)[
    'vm-work-1',            'webapp-prd',           3,
    'vm-custom',            'webapp-prd',           3,
    'webapp-prd',           'vm-custom',            1,
    'webapp-prd',          'test-machine',          1,
    'vm-custom',           'server-0126',           1,
    'vm-custom',           'hub_router',            2,
    'webapp-prd',          'hub_router',            2,
    'test-machine',        'vm-custom',             1,
    'test-machine',        'hub_router',            2,
    'hub_router',           'remote_DT',            1,
    'vm-work-1',            'storage_main_backup',  5,
    'hub_router',           'vm-work-2',            1,
    'vm-work-2',            'backup_prc',           3,
    'remote_DT',            'backup_prc',           3,
    'backup_prc',           'storage_main_backup',  5,
    'backup_prc',           'storage_DevBox',       1,
    'device_A1',            'sevice_B2',            2,
    'sevice_B2',            'device_A1',            2
];
connections
| invoke graph_blast_radius_fl(sourceIdColumnName       = 'SourceNodeName'
                            , targetIdColumnName        = 'TargetNodeName'
                            , targetWeightColumnName    = 'TargetNodeCriticality'
)

Output

sourceIdblastRadiusListblastRadiusScoreblastRadiusScoreWeightedisBlastRadiusListCapped
webapp-prd[“vm-custom”,“test-machine”,“hub_router”]34FALSE
vm-custom[“webapp-prd”,“server-0126”,“hub_router”]36FALSE
test-machine[“vm-custom”,“hub_router”]23FALSE
vm-work-1[“webapp-prd”,“storage_main_backup”]28FALSE
backup_prc[“storage_main_backup”,“storage_DevBox”]26FALSE
hub_router[“remote_DT”,“vm-work-2”]22FALSE
vm-work-2[“backup_prc”]13FALSE
device_A1[“sevice_B2”]12FALSE
remote_DT[“backup_prc”]13FALSE
sevice_B2[“device_A1”]12FALSE

Running the function aggregates the connections or paths between sources and targets by source. For each source, Blast Radius represents the connected targets as score (regular and weighted) and list.

Each row in the output contains the following fields:

  • sourceId: ID of the source node taken from relevant column.
  • blastRadiusList: a list of target nodes IDs (taken from relevant column) that the source node is connected to. The list is capped to maximum length limit of listedIdsLimit parameter.
  • blastRadiusScore: the score is the count of target nodes that the source is connected to. High Blast Radius score indicates that the source node can potentially access lots of targets, and should be treated accordingly.
  • blastRadiusScoreWeighted: the weighted score is the sum of the optional target nodes’ weight column, representing their value - such as criticality or cost. If such weight exists, weighted Blast Radius score might be a more accurate metric of source node value due to potential access to high value targets.
  • isBlastRadiusListCapped: boolean flag whether the list of targets was capped by listedIdsLimit parameter. If it’s true, then other targets can be accessed from the source in addition to the listed one (up to the number of blastRadiusScore).

In the example above, we run the graph_blast_radius_fl() function on top of connections between sources and targets. In the first row of the output, we can see that source node ‘webapp-prd’ is connected to three targets (‘vm-custom’, ’test-machine’, ‘hub_router’). We use the input data TargetNodeCriticality column as target weights, and get a cumulative weight of 4. Also, since the number of targets is 3 and the default list limit is 50, all of the targets are shown - so the value of isBlastRadiusListCapped column is FALSE.

If the multi-hop paths aren’t available, we can build multi-hop paths between sources and targets (for example, by running ‘graph_path_discovery_fl()’) and run ‘graph_blast_radius_fl()’ on top of the results.

The output looks similar, but represents Blast Radius calculated over multi-hop paths, thus being a better indicator of source nodes true connectivity to relevant targets. In order to find the full paths between source and target scenarios (for example, for disruption), graph_path_discovery_fl() function can be used with filters on relevant source and target nodes.

The function graph_blast_radius_fl() can be used to calculate the Blast Radius of source nodes, calculated either over direct edges or longer paths. In the cybersecurity domain, it can provide several insights. Blast Radius scores, regular and weighted, represent a source node’s importance from both defenders’ and attackers’ perspectives. Nodes with a high Blast Radius should be protected accordingly, for example, in terms of access hardening and vulnerability management. Security signals such as alerts on such nodes should be prioritized. The Blast Radius list should be monitored for undesired connections between sources and targets and used in disruption scenarios. For example, if the source was compromised, connections between it and important targets should be broken.