mirror of https://github.com/aliasrobotics/cai.git
477 lines
18 KiB
Python
477 lines
18 KiB
Python
"""
|
|
This script creates a hardcoded graph structure with 3 vulnerable nodes and computes edge probabilities
|
|
using simulated data. It then generates and saves a visual attack graph for demonstration purposes.
|
|
It also runs the CTR baseline analysis to get the security game equilibrium results.
|
|
|
|
Usage:
|
|
python tools/cut_the_rope/benchmark/graph_sample.py
|
|
|
|
The script performs the following steps:
|
|
1. Creates a hardcoded graph structure with 3 vulnerable nodes.
|
|
2. Simulates log data with token and cost statistics.
|
|
3. Computes edge probabilities using a heuristic based on simulated tokens, cost, and message count.
|
|
4. Extracts node vulnerability and name information.
|
|
5. Creates an attack graph using the computed probabilities.
|
|
6. Runs CTR baseline analysis to compute security game equilibrium.
|
|
7. Plots and saves the attack graph visualization and CTR results.
|
|
|
|
Example:
|
|
python3 tools/cut_the_rope/benchmark/graph_sample.py
|
|
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import argparse
|
|
import numpy as np
|
|
import sys
|
|
import io
|
|
from typing import List, Dict, Any
|
|
from pydantic import BaseModel
|
|
|
|
from tools.cut_the_rope.ctr_cai.experiment_cai import NodeInfo, EdgeInfo, GraphStructure
|
|
from tools.cut_the_rope.ctr_cai.probability_computation import compute_edge_probabilities_offline
|
|
from tools.cut_the_rope.ctr_cai.attack_graph_cai import create_graph_from_agent_output, plot_attack_graph
|
|
|
|
# Import CTR core functions for baseline analysis
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from core.ctr_core import main as ctr_core_main
|
|
from core.ctr_core import find_and_add_entry_node, generate_game_elements
|
|
from tools.cut_the_rope.ctr_cai.ctr_baseline_visualization import visualize_baseline_results
|
|
|
|
def random_steps(route, attack_rate=None, defense_rate=None, graph=None):
|
|
"""
|
|
Geometric distribution for randomly moving defender.
|
|
|
|
Args:
|
|
route: Attack path
|
|
attack_rate: Attacker movement rate parameter
|
|
defense_rate: Defender check rate parameter
|
|
graph: Attack graph (not used in this implementation)
|
|
|
|
Returns:
|
|
Array of probabilities for each position in the path
|
|
"""
|
|
# What is the prob that defender checks before attacker can make the next move?
|
|
if attack_rate is None:
|
|
attack_rate = 2 # Default attack rate
|
|
if defense_rate is None:
|
|
defense_rate = 2 # Default defense rate
|
|
|
|
if (attack_rate + defense_rate) == 0:
|
|
# Handle edge case
|
|
p = 0.5
|
|
else:
|
|
p = defense_rate / (attack_rate + defense_rate)
|
|
|
|
x = np.arange(len(route))
|
|
pmf = p * np.power(1-p, x)
|
|
|
|
if pmf.sum() > 0:
|
|
pmf = pmf / pmf.sum()
|
|
else:
|
|
# Fallback if all probabilities are zero
|
|
pmf = np.ones(len(route)) / len(route)
|
|
|
|
return pmf
|
|
|
|
def create_hardcoded_graph() -> GraphStructure:
|
|
"""
|
|
Load graph structure from the sample_graph_structure.json file.
|
|
|
|
Returns:
|
|
GraphStructure: The graph structure loaded from JSON file.
|
|
"""
|
|
# Path to the sample graph structure JSON file
|
|
json_file_path = "tools/cut_the_rope/benchmark/data/sample_graph_2_vulns/sample_graph_structure.json"
|
|
|
|
try:
|
|
with open(json_file_path, 'r') as f:
|
|
graph_data = json.load(f)
|
|
|
|
# Convert JSON data to NodeInfo and EdgeInfo objects
|
|
nodes = []
|
|
for node_data in graph_data["nodes"]:
|
|
nodes.append(NodeInfo(
|
|
id=node_data["id"],
|
|
name=node_data["name"],
|
|
info=node_data["info"],
|
|
vulnerability=node_data["vulnerability"],
|
|
message_id=node_data["message_id"]
|
|
))
|
|
|
|
edges = []
|
|
for edge_data in graph_data["edges"]:
|
|
edges.append(EdgeInfo(
|
|
source=edge_data["source"],
|
|
target=edge_data["target"]
|
|
))
|
|
|
|
print(f"Successfully loaded graph structure from: {json_file_path}")
|
|
print(f"Loaded {len(nodes)} nodes and {len(edges)} edges")
|
|
|
|
return GraphStructure(nodes=nodes, edges=edges)
|
|
|
|
except FileNotFoundError:
|
|
print(f"Error: Could not find {json_file_path}")
|
|
print("Falling back to original hardcoded graph structure...")
|
|
|
|
# Fallback to original hardcoded structure if file not found
|
|
nodes = [
|
|
NodeInfo(
|
|
id="1",
|
|
name="Entry",
|
|
info="Initial access point",
|
|
vulnerability=False,
|
|
message_id=0
|
|
),
|
|
NodeInfo(
|
|
id="2",
|
|
name="Recon",
|
|
info="Network reconnaissance",
|
|
vulnerability=False,
|
|
message_id=1
|
|
),
|
|
NodeInfo(
|
|
id="3",
|
|
name="Web Service",
|
|
info="Web application discovery",
|
|
vulnerability=False,
|
|
message_id=2
|
|
),
|
|
NodeInfo(
|
|
id="4",
|
|
name="DB Service",
|
|
info="Database service discovery",
|
|
vulnerability=False,
|
|
message_id=3
|
|
),
|
|
NodeInfo(
|
|
id="5",
|
|
name="Web Vuln",
|
|
info="Web application vulnerability",
|
|
vulnerability=True,
|
|
message_id=4
|
|
),
|
|
NodeInfo(
|
|
id="6",
|
|
name="DB Vuln",
|
|
info="Database vulnerability",
|
|
vulnerability=True,
|
|
message_id=5
|
|
),
|
|
NodeInfo(
|
|
id="7",
|
|
name="Pivot Node",
|
|
info="Strategic pivot point",
|
|
vulnerability=False,
|
|
message_id=6
|
|
),
|
|
NodeInfo(
|
|
id="8",
|
|
name="Crown Jewels",
|
|
info="Ultimate target system",
|
|
vulnerability=True,
|
|
message_id=7
|
|
)
|
|
]
|
|
|
|
edges = [
|
|
EdgeInfo(source="1", target="2"),
|
|
EdgeInfo(source="2", target="3"),
|
|
EdgeInfo(source="2", target="4"),
|
|
EdgeInfo(source="3", target="5"),
|
|
EdgeInfo(source="4", target="6"),
|
|
EdgeInfo(source="5", target="7"),
|
|
EdgeInfo(source="7", target="8"),
|
|
EdgeInfo(source="6", target="8"),
|
|
EdgeInfo(source="6", target="7"),
|
|
]
|
|
|
|
return GraphStructure(nodes=nodes, edges=edges)
|
|
|
|
except Exception as e:
|
|
print(f"Error loading graph structure: {str(e)}")
|
|
raise
|
|
|
|
def create_simulated_log() -> List[Dict[str, Any]]:
|
|
"""
|
|
Create log data that matches the message IDs from the loaded graph structure.
|
|
Designed to create balanced edge probabilities for interesting CTR results.
|
|
|
|
Returns:
|
|
List[Dict[str, Any]]: Simulated filtered log data with balanced cost patterns.
|
|
"""
|
|
return [
|
|
# Message ID 0 - User prompt (low cost)
|
|
{"message_id": 0, "content": "User ask to do a pentest for a domain", "role": "user", "content_tokens": 25},
|
|
|
|
# Message ID 3 - Reconnaissance (medium cost)
|
|
{"message_id": 3, "content": "Scanning internal network for live hosts and services", "role": "assistant", "content_tokens": 80},
|
|
|
|
# Message ID 10 - Web Server (medium-high cost)
|
|
{"message_id": 10, "content": "Identifying web technologies and potential entry points", "role": "assistant", "content_tokens": 120},
|
|
|
|
# Message ID 20 - Database (medium cost)
|
|
{"message_id": 20, "content": "Scanning for database services and metadata exposure", "role": "assistant", "content_tokens": 85},
|
|
|
|
# Message ID 70 - Brute Force Failed (high cost - failure is expensive)
|
|
{"message_id": 70, "content": "Brute force attempt failed due to patch or WAF detection", "role": "assistant", "content_tokens": 180},
|
|
|
|
# Message ID 120 - File Upload Vulnerability (medium cost - moderate complexity)
|
|
{"message_id": 120, "content": "File Upload Vulnerability (Remote Code Execution) discovered and exploited", "role": "assistant", "content_tokens": 140},
|
|
|
|
# Message ID 160 - SQL Injection (medium cost - similar complexity)
|
|
{"message_id": 160, "content": "Exploiting SQL injection to access database backend", "role": "assistant", "content_tokens": 130},
|
|
|
|
# Message ID 200 - Lateral Movement (high cost - complex operation)
|
|
{"message_id": 200, "content": "Using compromised systems to pivot deeper into the network", "role": "assistant", "content_tokens": 200},
|
|
|
|
# Message ID 300 - Privilege Escalation (very high cost - final goal)
|
|
{"message_id": 300, "content": "Escalating privileges to gain higher access", "role": "assistant", "content_tokens": 250},
|
|
]
|
|
|
|
def run_ctr_baseline_analysis(graph_structure, edge_probabilities, save_path, attack_rate_list=None, defense_rate_list=None):
|
|
"""
|
|
Run CTR baseline analysis on the hardcoded graph structure.
|
|
|
|
Args:
|
|
graph_structure: The hardcoded graph structure
|
|
edge_probabilities: Edge probabilities dictionary
|
|
save_path: Path to save CTR results
|
|
attack_rate_list: List of attack rates (default: [2])
|
|
defense_rate_list: List of defense rates (default: [2])
|
|
|
|
Returns:
|
|
baseline_result: CTR analysis results dictionary
|
|
"""
|
|
if attack_rate_list is None:
|
|
attack_rate_list = [2]
|
|
if defense_rate_list is None:
|
|
defense_rate_list = [2]
|
|
|
|
# Create attack graph
|
|
full_attack_graph = create_graph_from_agent_output(graph_structure, edge_probabilities)
|
|
|
|
# Extract attack paths (as2) - replicating the preprocessing steps from ctr_core
|
|
attacker_graph = full_attack_graph.copy()
|
|
atk_virtual_entry_node, attacker_graph, atk_original_roots = find_and_add_entry_node(attacker_graph)
|
|
# Do NOT merge targets - we want to preserve individual vulnerable nodes
|
|
_, V, _, as2, target_list, node_order, adv_list, theta, m = generate_game_elements(
|
|
attacker_graph, atk_virtual_entry_node, atk_original_roots)
|
|
|
|
# Check if generate_game_elements returned empty values (indicating no targets)
|
|
if not target_list or not V or not as2:
|
|
print("Warning: No target nodes or attack paths found after graph preprocessing. Skipping CTR analysis.")
|
|
return {
|
|
'optimal_defense': {},
|
|
'attacker_strategy': [],
|
|
'defender_success': 0.0,
|
|
'attacker_success': 0.0,
|
|
'error': 'No valid targets found after preprocessing'
|
|
}
|
|
|
|
# Capture CTR core output
|
|
captured_output = ""
|
|
baseline_result = None
|
|
|
|
try:
|
|
# Redirect stdout to capture CTR output
|
|
stdout = sys.stdout
|
|
output = io.StringIO()
|
|
sys.stdout = output
|
|
|
|
# Run CTR baseline analysis
|
|
baseline_result = ctr_core_main(
|
|
full_attack_graph=full_attack_graph,
|
|
defender_subgraphs_list=None,
|
|
attack_rate_list=attack_rate_list,
|
|
defense_rate_list=defense_rate_list,
|
|
random_steps_fn=random_steps,
|
|
run_baseline_only=True
|
|
)
|
|
|
|
captured_output = output.getvalue()
|
|
except Exception as e:
|
|
print(f"Error during CTR analysis: {str(e)}")
|
|
baseline_result = {
|
|
'optimal_defense': {},
|
|
'attacker_strategy': [],
|
|
'defender_success': 0.0,
|
|
'attacker_success': 0.0,
|
|
'error': f'CTR analysis failed: {str(e)}'
|
|
}
|
|
finally:
|
|
# Restore stdout
|
|
sys.stdout = stdout
|
|
|
|
# Handle case where baseline_result is None
|
|
if baseline_result is None:
|
|
print("Warning: CTR baseline analysis returned None. Using default values.")
|
|
baseline_result = {
|
|
'optimal_defense': {},
|
|
'attacker_strategy': [],
|
|
'defender_success': 0.0,
|
|
'attacker_success': 0.0,
|
|
'error': 'CTR analysis returned None'
|
|
}
|
|
|
|
# Save CTR baseline results
|
|
ctr_baseline_file_path = os.path.join(save_path, 'ctr_baseline.txt')
|
|
with open(ctr_baseline_file_path, 'w') as f:
|
|
f.write("CTR Baseline Analysis Results\n")
|
|
f.write("=" * 50 + "\n\n")
|
|
f.write("CAPTURED OUTPUT:\n")
|
|
f.write("-" * 20 + "\n")
|
|
f.write(captured_output)
|
|
f.write("\n\n")
|
|
f.write("BASELINE RESULT DICTIONARY:\n")
|
|
f.write("-" * 30 + "\n")
|
|
f.write(json.dumps(baseline_result, indent=2, default=str))
|
|
f.write("\n\n")
|
|
|
|
# Create formatted baseline tables with path information
|
|
try:
|
|
if baseline_result and 'error' not in baseline_result:
|
|
with open(ctr_baseline_file_path, 'a') as f:
|
|
f.write("FORMATTED TABLES:\n")
|
|
f.write("-" * 20 + "\n")
|
|
visualize_baseline_results(baseline_result, ctr_baseline_file_path, paths=as2, print_to_console=False)
|
|
except Exception as e:
|
|
print(f"Note: Could not create baseline visualization: {str(e)}")
|
|
|
|
print(f"CTR baseline results saved to: {ctr_baseline_file_path}")
|
|
return baseline_result
|
|
|
|
def main():
|
|
"""
|
|
Main function to create hardcoded graph, compute edge probabilities, and plot the attack graph.
|
|
"""
|
|
# Create hardcoded graph structure
|
|
graph = create_hardcoded_graph()
|
|
|
|
# Create simulated log data
|
|
filtered_log = create_simulated_log()
|
|
|
|
# Calculate total tokens from the simulated log
|
|
total_tokens_heuristic = sum(msg["content_tokens"] for msg in filtered_log)
|
|
total_cost_real = 0.80 # Lower cost to create more moderate probabilities
|
|
total_number_messages = len(filtered_log)
|
|
|
|
# Calculate euro per token with some variation
|
|
if total_tokens_heuristic > 0:
|
|
euro_per_token_heuristic = total_cost_real / total_tokens_heuristic
|
|
else:
|
|
euro_per_token_heuristic = 0.0
|
|
|
|
# Compute edge probabilities with balanced weights to avoid extreme values
|
|
edge_probabilities = compute_edge_probabilities_offline(
|
|
filtered_log=filtered_log,
|
|
graph_structure=graph,
|
|
total_tokens=total_tokens_heuristic,
|
|
total_cost=total_tokens_heuristic * euro_per_token_heuristic,
|
|
total_number_messages=total_number_messages,
|
|
w_cost=0.4, # Balanced cost weight
|
|
w_msg=0.3, # Higher message weight for distance consideration
|
|
w_tokens=0.3, # Balanced token weight
|
|
)
|
|
|
|
# Fix the starting edge probability (from node 1 to node 2) if it's 0%
|
|
starting_edge_key = "1->2"
|
|
if edge_probabilities.get(starting_edge_key, 0.0) == 0.0:
|
|
# Set a reasonable minimum probability for the starting edge
|
|
edge_probabilities[starting_edge_key] = 0.1 # 10% minimum for starting edge
|
|
|
|
# Extract node information for visualization
|
|
node_vulnerabilities = {node.id: node.vulnerability for node in graph.nodes}
|
|
node_info_dict = {node.id: node.name for node in graph.nodes}
|
|
|
|
# Create attack graph
|
|
attack_graph = create_graph_from_agent_output(graph, edge_probabilities)
|
|
|
|
# Set output path and graph name - fix the name
|
|
type_graph_name = "clean_sample_graph"
|
|
save_path = "tools/cut_the_rope/benchmark/data/" + type_graph_name
|
|
|
|
# Create directory if it doesn't exist
|
|
import os
|
|
os.makedirs(save_path, exist_ok=True)
|
|
|
|
# Plot and save the attack graph
|
|
plot_attack_graph(
|
|
attack_graph,
|
|
save_path=save_path,
|
|
node_info_dict=node_info_dict,
|
|
node_vulnerabilities=node_vulnerabilities,
|
|
type_graph=type_graph_name
|
|
)
|
|
|
|
# Print summary information
|
|
print(f"Graph created with {len(graph.nodes)} nodes and {len(graph.edges)} edges")
|
|
print(f"Vulnerable nodes: {sum(1 for node in graph.nodes if node.vulnerability)}")
|
|
print(f"Total simulated tokens: {total_tokens_heuristic}")
|
|
print(f"Total simulated cost: €{total_cost_real:.4f}")
|
|
print(f"Edge probabilities computed and graph saved to: {save_path}")
|
|
|
|
# Print edge probabilities for reference
|
|
print("\nEdge Probabilities:")
|
|
for edge in graph.edges:
|
|
edge_key = f"{edge.source}->{edge.target}"
|
|
prob = edge_probabilities.get(edge_key, 0.0)
|
|
source_name = next((node.name for node in graph.nodes if node.id == edge.source), "Unknown")
|
|
target_name = next((node.name for node in graph.nodes if node.id == edge.target), "Unknown")
|
|
print(f" {source_name} -> {target_name}: {prob:.2%}")
|
|
|
|
# Save the graph structure as JSON for reference
|
|
graph_json = {
|
|
"nodes": [node.model_dump() for node in graph.nodes],
|
|
"edges": [edge.model_dump() for edge in graph.edges]
|
|
}
|
|
|
|
json_path = save_path + "/clean_sample_graph_structure.json"
|
|
with open(json_path, "w") as f:
|
|
json.dump(graph_json, f, indent=2)
|
|
|
|
print(f"\nGraph structure also saved as JSON to: {json_path}")
|
|
|
|
# Run CTR baseline analysis
|
|
print(f"\n{'='*60}")
|
|
print("RUNNING CTR BASELINE ANALYSIS")
|
|
print(f"{'='*60}")
|
|
|
|
# Try different attack/defense rate combinations for more interesting results
|
|
baseline_result = run_ctr_baseline_analysis(
|
|
graph, edge_probabilities, save_path,
|
|
attack_rate_list=[1.5], defense_rate_list=[1.5] # More balanced rates
|
|
)
|
|
|
|
# Print CTR summary
|
|
print(f"\n{'='*60}")
|
|
print("CTR BASELINE ANALYSIS SUMMARY")
|
|
print(f"{'='*60}")
|
|
|
|
if baseline_result and 'error' not in baseline_result:
|
|
print(f"Defender Success Probability: {baseline_result.get('defender_success', 0.0):.6f}")
|
|
print(f"Attacker Success Probability: {baseline_result.get('attacker_success', 0.0):.6f}")
|
|
|
|
optimal_defense = baseline_result.get('optimal_defense', {})
|
|
if optimal_defense:
|
|
print(f"\nOptimal Defense Strategy:")
|
|
for location, probability in optimal_defense.items():
|
|
location_name = next((node.name for node in graph.nodes if node.id == str(location)), f"Node {location}")
|
|
print(f" {location_name}: {probability:.4f}")
|
|
|
|
attacker_strategy = baseline_result.get('attacker_strategy', [])
|
|
if len(attacker_strategy) > 0:
|
|
print(f"\nAttacker Strategy (probabilities for each path):")
|
|
for i, prob in enumerate(attacker_strategy):
|
|
print(f" Path {i+1}: {prob:.4f}")
|
|
else:
|
|
print("CTR analysis encountered an error or returned no results.")
|
|
if 'error' in baseline_result:
|
|
print(f"Error: {baseline_result['error']}")
|
|
|
|
print(f"\nAll results saved to: {save_path}")
|
|
|
|
if __name__ == "__main__":
|
|
main() |