適用対象: ✅Microsoft Fabric✅Azure データ エクスプローラー✅Azure Monitor✅Microsoft Sentinel
グラフのエッジとノードのデータに対して、度や間隔などのノードの中心性のメトリックを計算します。
graph_node_centrality_fl()
関数は UDF (ユーザー定義関数) であり、グラフ データに対するノードの一元性のさまざまなメトリックを計算できます。 グラフ データは、リソース、アプリケーション、ユーザーなどのノードと、既存のアクセス許可や接続などのエッジで構成されます。 ノードの中心性は、グラフ構造におけるその重要性を表し、いくつかの方法で定義および測定できます。 サイバーセキュリティでは、一元性は攻撃者に対するノードの価値を表します。適切に接続されたトークンなど、中央の高いノードを侵害すると、より多くの機会が提供されます。 Defender の場合、中央の高いノードも重要であり、それに応じて保護する必要があります。 中心性は、検出された最短パスと同様に、エッジを介して直接計算されます。 さまざまな一元性メトリックは、さまざまなセキュリティ コンテキストで役立ちます。
この関数の入力データには、 SourceId, EdgeId, TargetId
形式のエッジのテーブルと、オプションの関連ノード プロパティを持つノードの一覧が含まれている必要があります。 または、グラフ入力を他の種類のデータから抽出することもできます。 たとえば、 ユーザー A がリソース B にログインした 種類のエントリを含むトラフィック ログは、種類 のエッジ (ユーザー A)-[ログイン済み]->(リソース B) としてモデル化できます。 個別のユーザーとリソースの一覧は、ノードとしてモデル化できます。 関数の一部として、最短パスが計算され、中央値計算の入力として使用されます。
次の前提条件が行われます。
- すべてのエッジはパス検出に有効です。 関連性のないエッジは、中心性を計算する前に除外する必要があります。
- エッジは重みなし、独立、無条件です。つまり、すべてのエッジが同じ確率を持ち、B から C への移動は、A から B への以前の移動に依存しません。
- 中央値メトリックは、A->B->C 型のエッジに加え、サイクルのない単純な方向最短パスに対して計算されます。 関数内の graph-match 演算子の内部構文を変更することで、より複雑な定義を作成できます。
これらの前提条件は、関数の内部ロジックを変更することで、必要に応じて調整できます。
この関数は、パスの長さの制限、最大出力サイズなどのオプションの制約の下で、有効なソースから有効なターゲットまでの間の可能なすべての最短パスを検出します。 結果のパスと元のエッジに対して、さまざまな中央値メトリックが計算され、ノードの重要度のさまざまな側面が表されます。 出力は、 isValidConnectorColumnName 列を使用して関連としてフラグが設定されたノードの一覧であり、各ノードの一元性メトリックを含む列が追加されています。 この関数では、ノード ID やエッジ ID などの必須フィールドのみが使用されます。 その他の関連フィールド (型、プロパティ リスト、セキュリティ関連のスコア、外部シグナルなど) は、関数定義を変更することでロジックと出力に追加できます。
構文
graph_node_centrality_fl(
edgesTableName,, , nodesTableName, scopeColumnName, isValidPathStartColumnName, isValidPathEndColumnName, isValidConnectorColumnName, nodeIdColumnName, edgeIdColumnName, sourceIdColumnName, targetIdColumnName, [minPathLength], [maxPathLength], [resultCountLimit])
構文規則について詳しく知る。
パラメーター
名前 | タイプ | 必須 | 説明 |
---|---|---|---|
edgesTableName | string |
✔️ | グラフの端を含む入力テーブルの名前。 |
nodesTableName を |
string |
✔️ | グラフのノードを含む入力テーブルの名前。 |
scopeColumnName を |
string |
✔️ | パーティションまたはスコープ (サブスクリプションやアカウントなど) を含むノードおよびエッジ テーブルの列の名前。スコープごとに異なる異常モデルが構築されます。 |
isValidPathStartColumnName | string |
✔️ | ノードのブール値フラグを含むノード テーブル内の列の名前。 |
isValidPathEndColumnName | string |
✔️ | ノードのブール値フラグを含むノード テーブル内の列の名前。 |
isValidConnectorColumnName | string |
✔️ | ノードのブール値フラグを含むノード テーブル内の列の名前。 True は、ノードが出力に含まれる有効なコネクタであり、 False であることを意味します。これは有効なコネクタではありません。 |
nodeIdColumnName を |
string |
✔️ | ノード ID を含むノード テーブル内の列の名前。 |
edgeIdColumnName | string |
✔️ | エッジ ID を含むエッジ テーブル内の列の名前。 |
sourceIdColumnName を |
string |
✔️ | エッジのソース ノード ID を含むエッジ テーブル内の列の名前。 |
targetIdColumnName を |
string |
✔️ | エッジのターゲット ノード ID を含むエッジ テーブル内の列の名前。 |
minPathLength を |
long |
パス内のステップ (エッジ) の最小数。 既定値: 1。 | |
maxPathLength を |
long |
パス内のステップ (エッジ) の最大数。 既定値: 8。 | |
resultCountLimit を |
long |
出力に返されるパスの最大数。 既定値: 100000。 |
関数の定義
関数を定義するには、次のようにコードをクエリ定義関数として埋め込むか、データベースに格納された関数として作成します。
-
クエリ定義 の
- 保存されている
次の let ステートメント
Von Bedeutung
let ステートメント 単独では実行できません。 その後に、表形式の式ステートメント続ける必要があります。
graph_node_centrality_fl()
の作業例を実行するには、例を参照してください。
let graph_node_centrality_fl = ( edgesTableName:string, nodesTableName:string, scopeColumnName:string
, isValidPathStartColumnName:string, isValidPathEndColumnName:string, isValidConnectorColumnName:string
, nodeIdColumnName:string, edgeIdColumnName:string, sourceIdColumnName:string, targetIdColumnName:string
, minPathLength:long = 1, maxPathLength:long = 8, resultCountLimit:long = 100000)
{
let edges = (
table(edgesTableName)
| extend sourceId = column_ifexists(sourceIdColumnName, '')
| extend targetId = column_ifexists(targetIdColumnName, '')
| extend edgeId = column_ifexists(edgeIdColumnName, '')
| extend scope = column_ifexists(scopeColumnName, '')
);
let nodes = (
table(nodesTableName)
| extend nodeId = column_ifexists(nodeIdColumnName, '')
| extend isValidPathStart = column_ifexists(isValidPathStartColumnName, '')
| extend isValidPathEnd = column_ifexists(isValidPathEndColumnName, '')
| extend isNodeValidConnector = column_ifexists(isValidConnectorColumnName, '')
| extend scope = column_ifexists(scopeColumnName, '')
);
let potentialPairsOnScope = (
nodes
| summarize countSources = dcountif(nodeId, (isValidPathStart)), countTargets = dcountif(nodeId, (isValidPathEnd)) by scope
| project scope, countPotentialPairsOnScope = countSources * countTargets
);
let paths = (
edges
// Build graph object partitioned by scope, so that no connections are allowed between scopes.
// In case no scopes are relevant, partitioning should be removed for better performance.
| make-graph sourceId --> targetId with nodes on nodeId partitioned-by scope (
// Look for existing shortest paths between source nodes and target nodes with less than predefined number of hops.
// Current configurations looks for directed paths without any cycles; this can be changed if needed.
graph-shortest-paths output = all cycles = none (s)-[e*minPathLength..maxPathLength]->(t)
// Filter only by paths with that connect valid endpoints
where ((s.isValidPathStart) and (t.isValidPathEnd))
project sourceId = s.nodeId
, isSourceValidPathStart = s.isValidPathStart
, targetId = t.nodeId
, isTargetValidPathEnd = t.isValidPathEnd
, scope = s.scope
, edgeIds = e.edgeId
, innerNodeIds = map(inner_nodes(e), nodeId)
, innerNodeConnector = map(inner_nodes(e), isNodeValidConnector)
| limit resultCountLimit
)
| extend pathLength = array_length(edgeIds)
, pathEndpointsId = hash_md5(strcat(sourceId, targetId))
, pathId = hash_md5(strcat(sourceId, strcat(edgeIds), targetId))
);
let pathsProcessed = (
paths
| mv-expand with_itemindex = i innerNodeId = innerNodeIds to typeof(string), innerNodeConnector to typeof(bool)
| where (innerNodeConnector)
| summarize countShortestPathsThroughNode = count(), take_any(sourceId, targetId, pathLength) by scope, innerNodeId, pathEndpointsId
| join kind = leftouter (paths | summarize countShortestPaths = count() by scope, pathEndpointsId) on scope, pathEndpointsId
| project-away scope1, pathEndpointsId1
| extend betweennessForPair = (todouble(countShortestPathsThroughNode)/countShortestPaths)
| summarize betweenness = sum(betweennessForPair), countShortestPathsThroughNode = sum(countShortestPathsThroughNode)
, countPairsConnectedByNode = dcount(pathEndpointsId)
by scope, nodeId = innerNodeId
| join kind = leftouter (potentialPairsOnScope) on scope
| extend relativePrestige = round(todouble(countPairsConnectedByNode)/countPotentialPairsOnScope, 6)
| project scope, nodeId, betweenness, relativePrestige, countShortestPathsThroughNode, countPairsConnectedByNode
);
let centrality = (
nodes
| summarize take_any(*) by scope, nodeId
| where (isNodeValidConnector)
| join kind = leftouter (edges | summarize outDegree = dcount(targetId) by scope, sourceId) on scope, $left.nodeId == $right.sourceId
| join kind = leftouter (edges | summarize inDegree = dcount(sourceId) by scope, targetId) on scope, $left.nodeId == $right.targetId
| project-away scope1, scope2, sourceId, targetId
| extend inDegree = coalesce(inDegree, 0), outDegree = coalesce(outDegree, 0)
| extend totalDegree = inDegree * outDegree
| join kind = leftouter (paths | summarize sourceOutFlow = dcount(targetId) by scope, sourceId) on scope, $left.nodeId == $right.sourceId
| join kind = leftouter (paths | summarize sinkInFlow = dcount(sourceId) by scope, targetId) on scope, $left.nodeId == $right.targetId
| project-away scope1, scope2, sourceId, targetId
| extend sourceOutFlow = coalesce(sourceOutFlow, 0), sinkInFlow = coalesce(sinkInFlow, 0)
| join kind = leftouter (pathsProcessed) on scope, nodeId
| project-away scope1, nodeId1
| extend betweenness = coalesce(betweenness, 0.0), relativePrestige = coalesce(relativePrestige, 0.0)
, countShortestPathsThroughNode = coalesce(countShortestPathsThroughNode, 0), countPairsConnectedByNode = coalesce(countPairsConnectedByNode, 0)
);
centrality
};
// Write your query to use the function here.
例
次の例では、呼び出し演算子 を使用して関数を実行します。
-
クエリ定義 の
- 保存されている
クエリ定義関数を使用するには、埋め込み関数定義の後に呼び出します。
let edges = datatable (SourceNodeName:string, EdgeName:string, EdgeType:string, TargetNodeName:string, Region:string)[
'vm-work-1', 'e1', 'can use', 'webapp-prd', 'US',
'vm-custom', 'e2', 'can use', 'webapp-prd', 'US',
'webapp-prd', 'e3', 'can access', 'vm-custom', 'US',
'webapp-prd', 'e4', 'can access', 'test-machine', 'US',
'vm-custom', 'e5', 'can access', 'server-0126', 'US',
'vm-custom', 'e6', 'can access', 'hub_router', 'US',
'webapp-prd', 'e7', 'can access', 'hub_router', 'US',
'test-machine', 'e8', 'can access', 'vm-custom', 'US',
'test-machine', 'e9', 'can access', 'hub_router', 'US',
'hub_router', 'e10', 'routes traffic to', 'remote_DT', 'US',
'vm-work-1', 'e11', 'can access', 'storage_main_backup', 'US',
'hub_router', 'e12', 'routes traffic to', 'vm-work-2', 'US',
'vm-work-2', 'e13', 'can access', 'backup_prc', 'US',
'remote_DT', 'e14', 'can access', 'backup_prc', 'US',
'backup_prc', 'e15', 'moves data to', 'storage_main_backup', 'US',
'backup_prc', 'e16', 'moves data to', 'storage_DevBox', 'US',
'device_A1', 'e17', 'is connected to', 'device_B2', 'EU',
'device_B2', 'e18', 'is connected to', 'device_A1', 'EU'
];
let nodes = datatable (NodeName:string, NodeType:string, NodeEnvironment:string, Region:string) [
'vm-work-1', 'Virtual Machine', 'Production', 'US',
'vm-custom', 'Virtual Machine', 'Production', 'US',
'webapp-prd', 'Application', 'None', 'US',
'test-machine', 'Virtual Machine', 'Test', 'US',
'hub_router', 'Traffic Router', 'None', 'US',
'vm-work-2', 'Virtual Machine', 'Production', 'US',
'remote_DT', 'Virtual Machine', 'Production', 'US',
'backup_prc', 'Service', 'Production', 'US',
'server-0126', 'Server', 'Production', 'US',
'storage_main_backup', 'Cloud Storage', 'Production', 'US',
'storage_DevBox', 'Cloud Storage', 'Test', 'US',
'device_A1', 'Device', 'Backend', 'EU',
'device_B2', 'Device', 'Backend', 'EU'
];
let nodesEnriched = (
nodes
| extend IsValidStart = (NodeType in ('Virtual Machine'))
, IsValidEnd = (NodeType in ('Cloud Storage'))
| extend IsValidConnector = (NodeType in ('Application', 'Traffic Router', 'Service'))
);
let graph_node_centrality_fl = ( edgesTableName:string, nodesTableName:string, scopeColumnName:string
, isValidPathStartColumnName:string, isValidPathEndColumnName:string, isValidConnectorColumnName:string
, nodeIdColumnName:string, edgeIdColumnName:string, sourceIdColumnName:string, targetIdColumnName:string
, minPathLength:long = 1, maxPathLength:long = 8, resultCountLimit:long = 100000)
{
let edges = (
table(edgesTableName)
| extend sourceId = column_ifexists(sourceIdColumnName, '')
| extend targetId = column_ifexists(targetIdColumnName, '')
| extend edgeId = column_ifexists(edgeIdColumnName, '')
| extend scope = column_ifexists(scopeColumnName, '')
);
let nodes = (
table(nodesTableName)
| extend nodeId = column_ifexists(nodeIdColumnName, '')
| extend isValidPathStart = column_ifexists(isValidPathStartColumnName, '')
| extend isValidPathEnd = column_ifexists(isValidPathEndColumnName, '')
| extend isNodeValidConnector = column_ifexists(isValidConnectorColumnName, '')
| extend scope = column_ifexists(scopeColumnName, '')
);
let potentialPairsOnScope = (
nodes
| summarize countSources = dcountif(nodeId, (isValidPathStart)), countTargets = dcountif(nodeId, (isValidPathEnd)) by scope
| project scope, countPotentialPairsOnScope = countSources * countTargets
);
let paths = (
edges
// Build graph object partitioned by scope, so that no connections are allowed between scopes.
// In case no scopes are relevant, partitioning should be removed for better performance.
| make-graph sourceId --> targetId with nodes on nodeId partitioned-by scope (
// Look for existing shortest paths between source nodes and target nodes with less than predefined number of hops.
// Current configurations looks for directed paths without any cycles; this can be changed if needed.
graph-shortest-paths output = all cycles = none (s)-[e*minPathLength..maxPathLength]->(t)
// Filter only by paths with that connect valid endpoints
where ((s.isValidPathStart) and (t.isValidPathEnd))
project sourceId = s.nodeId
, isSourceValidPathStart = s.isValidPathStart
, targetId = t.nodeId
, isTargetValidPathEnd = t.isValidPathEnd
, scope = s.scope
, edgeIds = e.edgeId
, innerNodeIds = map(inner_nodes(e), nodeId)
, innerNodeConnector = map(inner_nodes(e), isNodeValidConnector)
| limit resultCountLimit
)
| extend pathLength = array_length(edgeIds)
, pathEndpointsId = hash_md5(strcat(sourceId, targetId))
, pathId = hash_md5(strcat(sourceId, strcat(edgeIds), targetId))
);
let pathsProcessed = (
paths
| mv-expand with_itemindex = i innerNodeId = innerNodeIds to typeof(string), innerNodeConnector to typeof(bool)
| where (innerNodeConnector)
| summarize countShortestPathsThroughNode = count(), take_any(sourceId, targetId, pathLength) by scope, innerNodeId, pathEndpointsId
| join kind = leftouter (paths | summarize countShortestPaths = count() by scope, pathEndpointsId) on scope, pathEndpointsId
| project-away scope1, pathEndpointsId1
| extend betweennessForPair = (todouble(countShortestPathsThroughNode)/countShortestPaths)
| summarize betweenness = sum(betweennessForPair), countShortestPathsThroughNode = sum(countShortestPathsThroughNode)
, countPairsConnectedByNode = dcount(pathEndpointsId)
by scope, nodeId = innerNodeId
| join kind = leftouter (potentialPairsOnScope) on scope
| extend relativePrestige = round(todouble(countPairsConnectedByNode)/countPotentialPairsOnScope, 6)
| project scope, nodeId, betweenness, relativePrestige, countShortestPathsThroughNode, countPairsConnectedByNode
);
let centrality = (
nodes
| summarize take_any(*) by scope, nodeId
| where (isNodeValidConnector)
| join kind = leftouter (edges | summarize outDegree = dcount(targetId) by scope, sourceId) on scope, $left.nodeId == $right.sourceId
| join kind = leftouter (edges | summarize inDegree = dcount(sourceId) by scope, targetId) on scope, $left.nodeId == $right.targetId
| project-away scope1, scope2, sourceId, targetId
| extend inDegree = coalesce(inDegree, 0), outDegree = coalesce(outDegree, 0)
| extend totalDegree = inDegree * outDegree
| join kind = leftouter (paths | summarize sourceOutFlow = dcount(targetId) by scope, sourceId) on scope, $left.nodeId == $right.sourceId
| join kind = leftouter (paths | summarize sinkInFlow = dcount(sourceId) by scope, targetId) on scope, $left.nodeId == $right.targetId
| project-away scope1, scope2, sourceId, targetId
| extend sourceOutFlow = coalesce(sourceOutFlow, 0), sinkInFlow = coalesce(sinkInFlow, 0)
| join kind = leftouter (pathsProcessed) on scope, nodeId
| project-away scope1, nodeId1
| extend betweenness = coalesce(betweenness, 0.0), relativePrestige = coalesce(relativePrestige, 0.0)
, countShortestPathsThroughNode = coalesce(countShortestPathsThroughNode, 0), countPairsConnectedByNode = coalesce(countPairsConnectedByNode, 0)
);
centrality
};
graph_node_centrality_fl(edgesTableName = 'edges'
, nodesTableName = 'nodesEnriched'
, scopeColumnName = 'Region'
, nodeIdColumnName = 'NodeName'
, edgeIdColumnName = 'EdgeName'
, sourceIdColumnName = 'SourceNodeName'
, targetIdColumnName = 'TargetNodeName'
, isValidPathStartColumnName = 'IsValidStart'
, isValidPathEndColumnName = 'IsValidEnd'
, isValidConnectorColumnName = 'IsValidConnector'
)
アウトプット
範囲 | ノードID | ノードネーム | NodeType (ノードタイプ) | NodeEnvironment | リージョン | IsValidStart | IsValidEnd | IsValidConnector | isValidPathStart | isValidPathEnd | isNodeValidConnector | outDegree | inDegree | totalDegree | sourceOutFlow | sinkInFlow | betweenness | relativePrestige | countShortestPathsThroughNode | countPairsConnectedByNode |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
アメリカ | backup_prc | backup_prc | サービス | 生産 | アメリカ | いいえ | いいえ | 正しい | いいえ | いいえ | 正しい | 2 | 2 | 4 | 0 | 0 | 9 | 0.9 | 14 | 9 |
この関数を実行すると、有効なエンドポイントとしてフラグが設定されているすべてのターゲット (isTargetValidPathEnd == True) に、有効な開始点 (isSourceValidPathStart == True) としてフラグが設定されたソース ノード間で接続するすべての最短パスが検索されます。 有効なコネクタとしてフラグが設定されているすべてのノードについて、これらのパスと元のエッジの上にさまざまな中央値メトリックが計算されます (isValidConnector == True)。 出力は、各行が有効なコネクタ ノードに対応するテーブルです。 各行には、次のフィールドが含まれています。
-
nodeId
: コネクタ ノードの ID。 -
isValidConnector
: 中心性を計算する有効なコネクタであるノードのブール値フラグ。は True に等しい必要があります。 -
isSourceValidPathStart
: 有効なパス開始であるノードのブール値フラグ。 -
isTargetValidPathEnd
: ノードが有効なパスの末尾であるブール値フラグ。 -
scope
: ノードとパスを含むスコープ。 -
outDegree
: ノードの OutDegree 。 これは、ノードに隣接する隣接するエッジ上の個別のターゲットの数です。 -
inDegree
: ノード の InDegree 。 これは、ノードの受信エッジ上の個別のソースの数です。 -
totalDegree
:inDegree
にoutDegree
を乗算します。 この値は、すべての入力エッジがすべての外部エッジに接続されるため、ノードが作成できるパスの潜在的な数を表します。 -
sourceOutFlow
: BlastRadius と同様に、ノードから始まるパスを介して到達できるターゲットの数。 -
sinkInFlow
: ExposurePerimeter と同様に、パスを介してノードに到達できるソースの数。 -
betweenness
: Betweenness の中心性。すべての最短パスからノードを通過する最短パスの比率。 -
relativePrestige
: プレステージの中心性 は、ノードを通過する最短パスで接続されたソース/ターゲット ペアの数です。 相対名声は、このカウントをすべての潜在的なソースとターゲットのペアの数で正規化します。 計算は、より長いパスのスコアを罰するように調整できます。 -
countShortestPathsThroughNode
: ノードを通過する最短のパスの数 (定期的なソースとターゲットのペアを含む)。 -
countPairsConnectedByNode
: ノードを通過するパスからの個別のソースとターゲットのペアの数。
この例では、仮想マシンをストレージ アカウントに接続するパスに基づいて、アプリケーション、トラフィック ルーター、またはサービスであるすべての資産の一元性メトリックを計算しました。 出力の最初の行で、降順で並べ替えられた場合は、サービス のbackup_prcを確認できます。 2 の入力と出力、9 の間などです。 中心性メトリックが異なると、重要度のさまざまな側面が表されるため、完全には一致しません。 たとえば、ノード backup_prc は高い betweenness
と relativePrestige
を持ちますが、度が低いため、多くの直接エッジを持たないノードとして強調表示されますが、戦略的に配置され、そのスコープのグローバル relativePrestige で重要な役割を果たします。
graph_node_centrality_fl()
関数は、サイバーセキュリティ ドメインで使用して、グラフとしてモデル化されたデータを介して、接続されたトークンやユーザーなどの重要なノードを検出できます。 使用可能なさまざまな中央値メトリックは、ノードの姿勢をより深く理解し、それに応じて行動できるようにします。 たとえば、関連する信号の優先順位を付けたり、ノードを強化したり、不要な接続を中断したりします。