TFDrift-Falco Graph UI Architecture¶
概要¶
TFDrift-Falcoの中核価値である**因果関係グラフ**を可視化するため、React + Cytoscape.jsベースのGraph UIを導入します。
なぜGraph UIが必要か¶
現状の問題(Grafanaの限界)¶
Grafanaは時系列データの可視化に優れていますが、**リソース間の因果関係**を表現できません:
❌ Grafanaの表示:
- Drift detected: +1
- Falco event count increased
- IAM change happened at 12:03
❓ 疑問:
- どのIAM?
- そのIAMはどのServiceAccount?
- なぜこのPodに影響?
- その結果、なぜこのFalco Ruleが発火?
TFDrift-Falcoの本質¶
TFDrift-Falcoは「Terraformの構成ドリフトが、どのリソース → どの権限 → どのRuntimeイベントにつながったか」を説明するツールです。
これは**時系列の話ではなく、因果関係の話**です → Graph問題
アーキテクチャ概要¶
┌─────────────────────────────────────────────────────────────┐
│ Data Sources │
├─────────────────────────────────────────────────────────────┤
│ Terraform State │ CloudTrail │ Falco Events │ K8s API │
└──────────┬──────────────────┬──────────────┬────────────────┘
│ │ │
v v v
┌─────────────────────────────────────────────────────────────┐
│ TFDrift-Falco Core Engine │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Resource Relationship Builder │ │
│ │ - IAM → ServiceAccount mapping │ │
│ │ - ServiceAccount → Pod mapping │ │
│ │ - Pod → Container mapping │ │
│ │ - Drift → IAM → SA → Pod → Falco Event path │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────┬──────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────┐
│ Graph API Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GET /api/graph/overview │ │
│ │ GET /api/graph/drift/:drift_id │ │
│ │ GET /api/graph/blast-radius/:resource_id │ │
│ │ GET /api/graph/path/:from/:to │ │
│ │ POST /api/graph/query │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────┬──────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────┐
│ Frontend: React + Cytoscape.js UI │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Graph Visualization │ │
│ │ - Node rendering (Drift, IAM, SA, Pod, Falco) │ │
│ │ - Edge rendering (cause → effect) │ │
│ │ - Interactive controls (zoom, pan, select) │ │
│ │ - Highlighting (blast radius, path) │ │
│ │ - Layer switching (attack view, ops view) │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────┬──────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────┐
│ Grafana (Entry Point) │
│ - Drift detection trend │
│ - Falco event counts │
│ - Drilldown link → Graph UI │
└─────────────────────────────────────────────────────────────┘
グラフデータモデル¶
ノードタイプ¶
enum NodeType {
TERRAFORM_CHANGE = 'terraform_change',
IAM_POLICY = 'iam_policy',
IAM_ROLE = 'iam_role',
SERVICE_ACCOUNT = 'service_account',
POD = 'pod',
CONTAINER = 'container',
FALCO_EVENT = 'falco_event',
SECURITY_GROUP = 'security_group',
NETWORK = 'network'
}
interface GraphNode {
id: string;
type: NodeType;
label: string;
data: {
resource_type: string;
resource_name: string;
timestamp?: string;
severity?: 'critical' | 'high' | 'medium' | 'low';
metadata: Record<string, any>;
};
style: {
color?: string;
size?: number;
shape?: string;
};
}
エッジタイプ¶
enum EdgeType {
CAUSED_BY = 'caused_by', // Drift → IAM change
GRANTS_ACCESS = 'grants_access', // IAM → ServiceAccount
USED_BY = 'used_by', // SA → Pod
CONTAINS = 'contains', // Pod → Container
TRIGGERED = 'triggered' // Container → Falco Event
}
interface GraphEdge {
id: string;
source: string;
target: string;
type: EdgeType;
label: string;
data: {
relationship: string;
metadata: Record<string, any>;
};
style: {
color?: string;
width?: number;
line_style?: 'solid' | 'dashed' | 'dotted';
};
}
グラフ全体¶
interface CausalGraph {
nodes: GraphNode[];
edges: GraphEdge[];
metadata: {
generated_at: string;
root_cause?: string;
blast_radius_size?: number;
};
}
API仕様¶
1. Overview Graph¶
全体の因果関係グラフを取得
GET /api/graph/overview?time_range=1h&severity=high,critical
Response:
{
"nodes": [...],
"edges": [...],
"metadata": {
"generated_at": "2025-12-19T17:50:00Z",
"total_nodes": 42,
"total_edges": 56
}
}
2. Drift-specific Graph¶
特定のDriftから始まる因果関係パスを取得
GET /api/graph/drift/:drift_id
Response:
{
"root_cause": {
"id": "drift-123",
"type": "terraform_change",
"label": "IAM Policy modification"
},
"path": [
{
"from": "drift-123",
"to": "iam-policy-456",
"relationship": "caused_by"
},
{
"from": "iam-policy-456",
"to": "sa-789",
"relationship": "grants_access"
},
...
],
"graph": {
"nodes": [...],
"edges": [...]
}
}
3. Blast Radius¶
特定リソースの影響範囲を取得
GET /api/graph/blast-radius/:resource_id
Response:
{
"center": {
"id": "iam-policy-456",
"type": "iam_policy"
},
"affected_resources": [
{
"id": "sa-789",
"type": "service_account",
"distance": 1
},
{
"id": "pod-101",
"type": "pod",
"distance": 2
},
...
],
"graph": {
"nodes": [...],
"edges": [...]
}
}
4. Path Query¶
2つのリソース間のパスを検索
GET /api/graph/path/:from/:to
Response:
{
"paths": [
{
"length": 4,
"nodes": ["drift-123", "iam-456", "sa-789", "pod-101", "falco-202"],
"edges": [...]
}
]
}
フロントエンド構造¶
ui/
├── src/
│ ├── components/
│ │ ├── Graph/
│ │ │ ├── CytoscapeGraph.tsx # メイングラフコンポーネント
│ │ │ ├── NodeRenderer.tsx # ノード描画
│ │ │ ├── EdgeRenderer.tsx # エッジ描画
│ │ │ ├── GraphControls.tsx # ズーム/パン/リセット
│ │ │ ├── GraphLegend.tsx # 凡例
│ │ │ └── GraphToolbar.tsx # ツールバー
│ │ ├── Filters/
│ │ │ ├── NodeTypeFilter.tsx # ノードタイプフィルタ
│ │ │ ├── SeverityFilter.tsx # 重要度フィルタ
│ │ │ ├── TimeRangeFilter.tsx # 時間範囲フィルタ
│ │ │ └── SearchFilter.tsx # 検索フィルタ
│ │ ├── Panels/
│ │ │ ├── NodeDetailPanel.tsx # ノード詳細パネル
│ │ │ ├── PathExplorerPanel.tsx # パス探索パネル
│ │ │ ├── BlastRadiusPanel.tsx # 影響範囲パネル
│ │ │ └── RecommendationPanel.tsx # 修正推奨パネル
│ │ └── Layouts/
│ │ ├── AttackViewLayout.tsx # 攻撃視点レイアウト
│ │ └── OpsViewLayout.tsx # 運用視点レイアウト
│ ├── hooks/
│ │ ├── useGraphData.ts # グラフデータ取得
│ │ ├── useCytoscape.ts # Cytoscape制御
│ │ ├── useGraphInteraction.ts # インタラクション管理
│ │ └── useGraphLayout.ts # レイアウト管理
│ ├── services/
│ │ ├── graphApi.ts # Graph API client
│ │ └── graphProcessor.ts # グラフデータ処理
│ ├── styles/
│ │ ├── cytoscapeStyles.ts # Cytoscapeスタイル定義
│ │ └── theme.ts # UIテーマ
│ ├── types/
│ │ └── graph.ts # 型定義
│ └── App.tsx
├── package.json
└── tsconfig.json
Cytoscapeスタイル定義¶
export const cytoscapeStylesheet = [
// Terraform Change nodes
{
selector: 'node[type="terraform_change"]',
style: {
'background-color': '#ff6b6b',
'label': 'data(label)',
'shape': 'hexagon',
'width': 80,
'height': 80,
'font-size': 12,
'text-valign': 'center',
'text-halign': 'center',
'border-width': 3,
'border-color': '#c92a2a'
}
},
// IAM Policy nodes
{
selector: 'node[type="iam_policy"]',
style: {
'background-color': '#4dabf7',
'label': 'data(label)',
'shape': 'round-rectangle',
'width': 70,
'height': 70,
'font-size': 11
}
},
// Service Account nodes
{
selector: 'node[type="service_account"]',
style: {
'background-color': '#51cf66',
'label': 'data(label)',
'shape': 'round-rectangle',
'width': 70,
'height': 70
}
},
// Pod nodes
{
selector: 'node[type="pod"]',
style: {
'background-color': '#ffd43b',
'label': 'data(label)',
'shape': 'round-rectangle',
'width': 60,
'height': 60
}
},
// Falco Event nodes
{
selector: 'node[type="falco_event"]',
style: {
'background-color': '#f06595',
'label': 'data(label)',
'shape': 'diamond',
'width': 70,
'height': 70,
'font-size': 10
}
},
// Critical severity highlight
{
selector: 'node[severity="critical"]',
style: {
'border-width': 5,
'border-color': '#c92a2a',
'background-color': '#ff6b6b'
}
},
// Edges - caused_by
{
selector: 'edge[type="caused_by"]',
style: {
'width': 3,
'line-color': '#ff6b6b',
'target-arrow-color': '#ff6b6b',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': 9,
'text-rotation': 'autorotate'
}
},
// Edges - grants_access
{
selector: 'edge[type="grants_access"]',
style: {
'width': 2,
'line-color': '#4dabf7',
'target-arrow-color': '#4dabf7',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
// Edges - used_by
{
selector: 'edge[type="used_by"]',
style: {
'width': 2,
'line-color': '#51cf66',
'target-arrow-color': '#51cf66',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
// Edges - triggered
{
selector: 'edge[type="triggered"]',
style: {
'width': 3,
'line-color': '#f06595',
'target-arrow-color': '#f06595',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
// Highlighted path
{
selector: '.highlighted',
style: {
'background-color': '#ffd43b',
'line-color': '#ffd43b',
'target-arrow-color': '#ffd43b',
'width': 5,
'z-index': 999
}
},
// Selected node
{
selector: ':selected',
style: {
'border-width': 6,
'border-color': '#228be6',
'z-index': 999
}
}
];
レイアウト戦略¶
1. Hierarchical Layout(階層レイアウト)¶
因果関係を上から下に表示(デフォルト)
const hierarchicalLayout = {
name: 'dagre',
rankDir: 'TB', // Top to Bottom
nodeSep: 100,
rankSep: 150,
animate: true,
animationDuration: 500
};
2. Radial Layout(放射状レイアウト)¶
Blast Radiusビュー用
const radialLayout = {
name: 'concentric',
concentric: (node) => node.data('distance'),
levelWidth: () => 2,
animate: true
};
3. Force-directed Layout(力学レイアウト)¶
複雑な関係性の探索用
インタラクティブ機能¶
1. ノードクリック¶
- クリック → 詳細パネル表示
- ダブルクリック → Blast Radius表示
- 右クリック → コンテキストメニュー
2. パスハイライト¶
function highlightPath(startNodeId: string, endNodeId: string) {
const path = cy.elements().aStar({
root: `#${startNodeId}`,
goal: `#${endNodeId}`
});
path.path.addClass('highlighted');
}
3. フィルタリング¶
function filterByNodeType(types: NodeType[]) {
cy.nodes().forEach(node => {
if (!types.includes(node.data('type'))) {
node.style('display', 'none');
} else {
node.style('display', 'element');
}
});
}
4. Blast Radius計算¶
function calculateBlastRadius(nodeId: string, maxDepth: number = 3) {
const startNode = cy.$id(nodeId);
const affected = startNode.successors().filter(node => {
const distance = getDistance(startNode, node);
return distance <= maxDepth;
});
return affected;
}
Grafana統合¶
Grafanaパネルからのドリルダウン¶
// Grafana Panel Link設定
{
"links": [
{
"title": "View Causality Graph",
"url": "http://localhost:3000/graph?drift_id=${__data.fields.drift_id}"
}
]
}
データソース連携¶
// Grafanaクエリパラメータを解析
const urlParams = new URLSearchParams(window.location.search);
const driftId = urlParams.get('drift_id');
const timeRange = urlParams.get('from');
// Graph APIを呼び出し
const graphData = await fetchDriftGraph(driftId, timeRange);
実装フェーズ¶
Phase 1: 基盤構築(Week 1)¶
- ✅ React + TypeScript + Vite セットアップ
- ✅ Cytoscape.js 統合
- ✅ 基本的なグラフ描画機能
- ✅ サンプルデータでの動作確認
Phase 2: バックエンド統合(Week 2)¶
- ⏳ Graph API実装
- ⏳ Resource Relationship Builder
- ⏳ Drift → Falco因果パス生成ロジック
- ⏳ APIとフロントエンドの統合
Phase 3: UI/UX強化(Week 3)¶
- ⏳ ノード詳細パネル
- ⏳ Blast Radiusビュー
- ⏳ パス探索機能
- ⏳ フィルタリング機能
- ⏳ レイアウト切り替え
Phase 4: Grafana統合(Week 4)¶
- ⏳ Grafanaドリルダウンリンク
- ⏳ 時系列データとの連携
- ⏳ 統合テスト
- ⏳ ドキュメント作成
技術スタック¶
フロントエンド¶
- React 18 - UIフレームワーク
- TypeScript - 型安全性
- Cytoscape.js - グラフビジュアライゼーション
- Vite - ビルドツール
- TanStack Query (React Query) - データフェッチング
- Zustand - 状態管理
- Tailwind CSS - スタイリング
バックエンド¶
- Go - 既存のTFDrift-Falcoコア
- Gin - HTTPフレームワーク(Graph API)
- GraphQL (オプション) - 柔軟なクエリ
インフラ¶
- Docker - コンテナ化
- Nginx - リバースプロキシ
- Grafana - 時系列ダッシュボード(入口)
成功指標¶
- 視認性: 因果関係が一目で理解できる
- パフォーマンス: 1000ノード以下のグラフを1秒以内に描画
- インタラクティブ性: クリック→詳細表示が0.5秒以内
- 統合性: GrafanaからシームレスにGraph UIへ遷移
まとめ¶
TFDrift-Falcoの真価は**「なぜそれが起きたか」を説明すること**にあります。
- Grafana = 「いつ・何回」(入口)
- Graph UI = 「なぜ・どこを直せば」(核心)
この2つの組み合わせで、完全なObservability(可観測性)と Actionability(行動可能性)を実現します。