| |
| """ |
| C2Sentinel Training Script v2 - Improved training with proper normalization |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from torch.utils.data import Dataset, DataLoader |
| import numpy as np |
| import random |
| from tqdm import tqdm |
| import json |
|
|
| from c2sentinel import ( |
| C2Sentinel, C2SentinelConfig, LogBERTC2Sentinel, |
| FeatureExtractor |
| ) |
| from safetensors.torch import save_file |
|
|
|
|
| class C2TrafficDataset(Dataset): |
| """Dataset with normalized features.""" |
|
|
| def __init__(self, num_samples=10000, normalize=True): |
| self.samples = [] |
| self.labels = [] |
| self.c2_types = [] |
| self.feature_extractor = FeatureExtractor() |
|
|
| print(f"Generating {num_samples} training samples...") |
|
|
| num_c2 = num_samples // 2 |
| num_benign = num_samples - num_c2 |
|
|
| |
| for _ in tqdm(range(num_c2), desc="C2 samples"): |
| connections, c2_type = self._generate_c2_traffic() |
| features = self.feature_extractor.extract_features(connections) |
| self.samples.append(features) |
| self.labels.append(1) |
| self.c2_types.append(c2_type) |
|
|
| |
| for _ in tqdm(range(num_benign), desc="Benign samples"): |
| connections = self._generate_benign_traffic() |
| features = self.feature_extractor.extract_features(connections) |
| self.samples.append(features) |
| self.labels.append(0) |
| self.c2_types.append(0) |
|
|
| self.samples = np.array(self.samples, dtype=np.float32) |
| self.labels = np.array(self.labels, dtype=np.float32) |
| self.c2_types = np.array(self.c2_types, dtype=np.int64) |
|
|
| |
| if normalize: |
| self.mean = np.mean(self.samples, axis=0) |
| self.std = np.std(self.samples, axis=0) + 1e-8 |
| self.samples = (self.samples - self.mean) / self.std |
|
|
| |
| np.savez('normalization_params.npz', mean=self.mean, std=self.std) |
| print(f"Feature stats - mean range: [{self.mean.min():.2f}, {self.mean.max():.2f}], " |
| f"std range: [{self.std.min():.4f}, {self.std.max():.2f}]") |
|
|
| |
| indices = np.random.permutation(len(self.samples)) |
| self.samples = self.samples[indices] |
| self.labels = self.labels[indices] |
| self.c2_types = self.c2_types[indices] |
|
|
| print(f"C2 samples: {np.sum(self.labels)}, Benign: {len(self.labels) - np.sum(self.labels)}") |
|
|
| def _generate_c2_traffic(self): |
| """Generate C2 beacon traffic with clear patterns.""" |
| c2_type = random.randint(1, 10) |
|
|
| |
| if c2_type <= 3: |
| interval = random.uniform(2, 15) |
| jitter = random.uniform(0, 0.15) |
| port = random.choice([4444, 4445, 5555, 443]) |
| bytes_sent = random.randint(80, 200) |
| bytes_recv = random.randint(40, 150) |
| elif c2_type <= 6: |
| interval = random.uniform(30, 90) |
| jitter = random.uniform(0, 0.2) |
| port = 443 |
| bytes_sent = random.randint(60, 150) |
| bytes_recv = random.randint(40, 100) |
| else: |
| interval = random.uniform(120, 300) |
| jitter = random.uniform(0, 0.1) |
| port = 443 |
| bytes_sent = random.randint(50, 120) |
| bytes_recv = random.randint(40, 80) |
|
|
| |
| dst_ip = f"{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}" |
| num_connections = random.randint(10, 40) |
|
|
| connections = [] |
| timestamp = 1705600000 |
|
|
| for _ in range(num_connections): |
| actual_interval = interval * (1 + random.uniform(-jitter, jitter)) |
| timestamp += actual_interval |
|
|
| |
| size_var = random.uniform(0.95, 1.05) |
|
|
| connections.append({ |
| 'timestamp': timestamp, |
| 'dst_ip': dst_ip, |
| 'dst_port': port, |
| 'bytes_sent': int(bytes_sent * size_var), |
| 'bytes_recv': int(bytes_recv * size_var), |
| 'protocol': 'tcp' |
| }) |
|
|
| return connections, c2_type |
|
|
| def _generate_benign_traffic(self): |
| """Generate clearly benign traffic.""" |
| pattern = random.choice(['browsing', 'api', 'streaming', 'interactive']) |
|
|
| connections = [] |
| timestamp = 1705600000 |
|
|
| if pattern == 'browsing': |
| |
| for _ in range(random.randint(10, 40)): |
| timestamp += random.uniform(0.5, 45) |
| connections.append({ |
| 'timestamp': timestamp, |
| 'dst_ip': f"{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}", |
| 'dst_port': random.choice([80, 443]), |
| 'bytes_sent': random.randint(200, 5000), |
| 'bytes_recv': random.randint(5000, 500000), |
| 'protocol': 'tcp' |
| }) |
|
|
| elif pattern == 'api': |
| |
| dst_ip = f"{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}" |
| for _ in range(random.randint(15, 40)): |
| timestamp += random.uniform(0.1, 20) |
| connections.append({ |
| 'timestamp': timestamp, |
| 'dst_ip': dst_ip, |
| 'dst_port': 443, |
| 'bytes_sent': random.randint(100, 3000), |
| 'bytes_recv': random.randint(200, 100000), |
| 'protocol': 'tcp' |
| }) |
|
|
| elif pattern == 'streaming': |
| |
| dst_ip = f"{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}" |
| for _ in range(random.randint(20, 60)): |
| timestamp += random.uniform(0.05, 3) |
| connections.append({ |
| 'timestamp': timestamp, |
| 'dst_ip': dst_ip, |
| 'dst_port': 443, |
| 'bytes_sent': random.randint(30, 200), |
| 'bytes_recv': random.randint(5000, 150000), |
| 'protocol': 'tcp' |
| }) |
|
|
| else: |
| dst_ip = f"192.168.{random.randint(0,255)}.{random.randint(1,254)}" |
| for _ in range(random.randint(15, 50)): |
| if random.random() < 0.3: |
| timestamp += random.uniform(3, 45) |
| else: |
| timestamp += random.uniform(0.1, 2) |
| connections.append({ |
| 'timestamp': timestamp, |
| 'dst_ip': dst_ip, |
| 'dst_port': 22, |
| 'bytes_sent': random.randint(20, 800), |
| 'bytes_recv': random.randint(50, 20000), |
| 'protocol': 'tcp' |
| }) |
|
|
| return connections |
|
|
| def __len__(self): |
| return len(self.samples) |
|
|
| def __getitem__(self, idx): |
| return { |
| 'features': torch.tensor(self.samples[idx]), |
| 'label': torch.tensor(self.labels[idx]), |
| 'c2_type': torch.tensor(self.c2_types[idx]) |
| } |
|
|
|
|
| def train_model(num_epochs=100, batch_size=32, learning_rate=0.0001, num_samples=20000): |
| """Train with improved stability.""" |
|
|
| print("=" * 70) |
| print("C2Sentinel Model Training v2") |
| print("=" * 70) |
|
|
| config = C2SentinelConfig() |
| model = LogBERTC2Sentinel(config) |
|
|
| |
| def init_weights(m): |
| if isinstance(m, nn.Linear): |
| nn.init.xavier_uniform_(m.weight, gain=0.5) |
| if m.bias is not None: |
| nn.init.zeros_(m.bias) |
| model.apply(init_weights) |
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| print(f"Device: {device}") |
| model.to(device) |
|
|
| |
| total_params = sum(p.numel() for p in model.parameters()) |
| print(f"Model parameters: {total_params:,}") |
|
|
| dataset = C2TrafficDataset(num_samples=num_samples, normalize=True) |
|
|
| train_size = int(0.9 * len(dataset)) |
| val_size = len(dataset) - train_size |
| train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size]) |
|
|
| train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True) |
| val_loader = DataLoader(val_dataset, batch_size=batch_size) |
|
|
| print(f"Train: {train_size}, Val: {val_size}") |
|
|
| |
| criterion = nn.BCEWithLogitsLoss() |
|
|
| |
| optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=0.001) |
|
|
| |
| warmup_epochs = 5 |
| def lr_lambda(epoch): |
| if epoch < warmup_epochs: |
| return (epoch + 1) / warmup_epochs |
| return 0.5 * (1 + np.cos(np.pi * (epoch - warmup_epochs) / (num_epochs - warmup_epochs))) |
|
|
| scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda) |
|
|
| best_val_acc = 0 |
| patience = 15 |
| patience_counter = 0 |
|
|
| for epoch in range(num_epochs): |
| model.train() |
| train_loss = 0 |
| train_correct = 0 |
| train_total = 0 |
|
|
| for batch in tqdm(train_loader, desc=f"Epoch {epoch+1}/{num_epochs}", leave=False): |
| features = batch['features'].to(device) |
| labels = batch['label'].to(device) |
|
|
| optimizer.zero_grad() |
| outputs = model(features) |
|
|
| |
| loss = criterion(outputs['c2_logits'].squeeze(), labels) |
|
|
| loss.backward() |
|
|
| |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5) |
|
|
| optimizer.step() |
|
|
| train_loss += loss.item() |
| predictions = (torch.sigmoid(outputs['c2_logits'].squeeze()) > 0.5).float() |
| train_correct += (predictions == labels).sum().item() |
| train_total += labels.size(0) |
|
|
| scheduler.step() |
|
|
| |
| model.eval() |
| val_correct = 0 |
| val_total = 0 |
| val_loss = 0 |
|
|
| with torch.no_grad(): |
| for batch in val_loader: |
| features = batch['features'].to(device) |
| labels = batch['label'].to(device) |
|
|
| outputs = model(features) |
| loss = criterion(outputs['c2_logits'].squeeze(), labels) |
| val_loss += loss.item() |
|
|
| predictions = (torch.sigmoid(outputs['c2_logits'].squeeze()) > 0.5).float() |
| val_correct += (predictions == labels).sum().item() |
| val_total += labels.size(0) |
|
|
| train_acc = 100 * train_correct / train_total |
| val_acc = 100 * val_correct / val_total |
| lr = optimizer.param_groups[0]['lr'] |
|
|
| print(f"Epoch {epoch+1}: Loss={train_loss/len(train_loader):.4f}, " |
| f"Train={train_acc:.1f}%, Val={val_acc:.1f}%, LR={lr:.6f}") |
|
|
| if val_acc > best_val_acc: |
| best_val_acc = val_acc |
| patience_counter = 0 |
| save_file(model.state_dict(), 'c2_sentinel.safetensors') |
| print(f" -> Saved (Val: {val_acc:.1f}%)") |
| else: |
| patience_counter += 1 |
| if patience_counter >= patience: |
| print(f"Early stopping at epoch {epoch+1}") |
| break |
|
|
| print(f"\nBest validation accuracy: {best_val_acc:.1f}%") |
| return model, config |
|
|
|
|
| def test_model(): |
| """Test the trained model.""" |
| print("\n" + "=" * 70) |
| print("Testing Model") |
| print("=" * 70) |
|
|
| sentinel = C2Sentinel.load('c2_sentinel') |
|
|
| |
| print("\n[1] Cobalt Strike Beacon (60s interval)...") |
| cs = [{'timestamp': 1705600000 + i*60, 'dst_ip': '185.234.72.19', 'dst_port': 443, |
| 'bytes_sent': 92, 'bytes_recv': 48} for i in range(16)] |
| r = sentinel.analyze(cs) |
| print(f" {'β C2 DETECTED' if r.is_c2 else 'β No C2'} (prob={r.c2_probability:.2%})") |
|
|
| |
| print("\n[2] Metasploit Beacon (5s interval, port 4444)...") |
| msf = [{'timestamp': 1705600000 + i*5, 'dst_ip': '10.10.10.10', 'dst_port': 4444, |
| 'bytes_sent': 150, 'bytes_recv': 400} for i in range(20)] |
| r = sentinel.analyze(msf) |
| print(f" {'β C2 DETECTED' if r.is_c2 else 'β No C2'} (prob={r.c2_probability:.2%})") |
|
|
| |
| print("\n[3] APT Slow Beacon (120s interval)...") |
| apt = [{'timestamp': 1705600000 + i*120, 'dst_ip': '45.33.32.156', 'dst_port': 443, |
| 'bytes_sent': 80, 'bytes_recv': 60} for i in range(12)] |
| r = sentinel.analyze(apt) |
| print(f" {'β C2 DETECTED' if r.is_c2 else 'β No C2'} (prob={r.c2_probability:.2%})") |
|
|
| |
| print("\n[4] Web Browsing (should be clean)...") |
| browse = [{'timestamp': 1705600000 + i*random.uniform(2, 30), |
| 'dst_ip': f"{random.randint(1,200)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}", |
| 'dst_port': 443, 'bytes_sent': random.randint(500, 3000), |
| 'bytes_recv': random.randint(10000, 500000)} for i in range(20)] |
| r = sentinel.analyze(browse) |
| print(f" {'β C2 DETECTED (FP!)' if r.is_c2 else 'β Clean'} (prob={r.c2_probability:.2%})") |
|
|
| |
| print("\n[5] SSH Keepalive (should be clean)...") |
| ssh = [{'timestamp': 1705600000 + i*30, 'dst_ip': '192.168.1.50', 'dst_port': 22, |
| 'bytes_sent': 48, 'bytes_recv': 48} for i in range(15)] |
| r = sentinel.analyze(ssh) |
| print(f" {'β C2 DETECTED (FP!)' if r.is_c2 else 'β Clean'} (prob={r.c2_probability:.2%})") |
| print(f" Pattern: {r.matched_legitimate_pattern}") |
|
|
| |
| print("\n[6] API Calls (should be clean)...") |
| api = [{'timestamp': 1705600000 + i*random.uniform(0.5, 10), |
| 'dst_ip': '52.85.132.99', 'dst_port': 443, |
| 'bytes_sent': random.randint(100, 2000), |
| 'bytes_recv': random.randint(500, 80000)} for i in range(25)] |
| r = sentinel.analyze(api) |
| print(f" {'β C2 DETECTED (FP!)' if r.is_c2 else 'β Clean'} (prob={r.c2_probability:.2%})") |
|
|
|
|
| if __name__ == '__main__': |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--epochs', type=int, default=100) |
| parser.add_argument('--samples', type=int, default=20000) |
| parser.add_argument('--batch-size', type=int, default=32) |
| parser.add_argument('--lr', type=float, default=0.0001) |
| parser.add_argument('--test-only', action='store_true') |
| args = parser.parse_args() |
|
|
| if args.test_only: |
| test_model() |
| else: |
| train_model(num_epochs=args.epochs, batch_size=args.batch_size, |
| learning_rate=args.lr, num_samples=args.samples) |
| test_model() |
|
|