danielostrow commited on
Commit
4ab39cb
·
verified ·
1 Parent(s): fb62c6c

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +253 -203
app.py CHANGED
@@ -1,277 +1,327 @@
1
  #!/usr/bin/env python3
2
- """
3
- C2Sentinel Demo - HuggingFace Space
4
- Interactive demo for testing C2 beacon detection.
5
- """
6
 
7
  import gradio as gr
8
  import json
 
 
9
  from huggingface_hub import hf_hub_download
10
 
11
- # Download model files
12
- model_dir = "."
13
- hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2sentinel.py", local_dir=model_dir)
14
- hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2_sentinel.safetensors", local_dir=model_dir)
15
- hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2_sentinel.json", local_dir=model_dir)
16
-
17
  from c2sentinel import C2Sentinel
18
-
19
- # Load model
20
  sentinel = C2Sentinel.load('c2_sentinel')
21
 
22
- # Example connection data
23
  EXAMPLES = {
24
- "-- Select Example --": "",
25
- "C2 Beacon (60s intervals)": json.dumps([
26
- {"timestamp": 1705600000, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
27
- {"timestamp": 1705600060, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
28
- {"timestamp": 1705600120, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
29
- {"timestamp": 1705600180, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
30
- {"timestamp": 1705600240, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
31
- {"timestamp": 1705600300, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
32
- {"timestamp": 1705600360, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
33
- {"timestamp": 1705600420, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500},
34
- ], indent=2),
35
- "Metasploit Port 4444": json.dumps([
36
- {"timestamp": 1705600000, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300},
37
- {"timestamp": 1705600030, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300},
38
- {"timestamp": 1705600060, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300},
39
- {"timestamp": 1705600090, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300},
40
- {"timestamp": 1705600120, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300},
41
- ], indent=2),
42
- "SSH Keepalive (Legitimate)": json.dumps([
43
- {"timestamp": 1705600000, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
44
- {"timestamp": 1705600030, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
45
- {"timestamp": 1705600060, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
46
- {"timestamp": 1705600090, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
47
- {"timestamp": 1705600120, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
48
- {"timestamp": 1705600150, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48},
49
- ], indent=2),
50
- "Web Browsing (Legitimate)": json.dumps([
51
  {"timestamp": 1705600000, "dst_ip": "93.184.216.34", "dst_port": 443, "bytes_sent": 500, "bytes_recv": 15000},
52
- {"timestamp": 1705600002, "dst_ip": "93.184.216.34", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 8000},
53
- {"timestamp": 1705600010, "dst_ip": "151.101.1.140", "dst_port": 443, "bytes_sent": 800, "bytes_recv": 45000},
54
- {"timestamp": 1705600015, "dst_ip": "172.217.14.206", "dst_port": 443, "bytes_sent": 300, "bytes_recv": 12000},
55
  {"timestamp": 1705600025, "dst_ip": "151.101.1.140", "dst_port": 443, "bytes_sent": 150, "bytes_recv": 5000},
56
- ], indent=2),
57
- "Slow Beacon (5 min)": json.dumps([
58
- {"timestamp": 1705600000, "dst_ip": "203.0.113.50", "dst_port": 8080, "bytes_sent": 256, "bytes_recv": 512},
59
- {"timestamp": 1705600300, "dst_ip": "203.0.113.50", "dst_port": 8080, "bytes_sent": 256, "bytes_recv": 512},
60
- {"timestamp": 1705600600, "dst_ip": "203.0.113.50", "dst_port": 8080, "bytes_sent": 256, "bytes_recv": 512},
61
- {"timestamp": 1705600900, "dst_ip": "203.0.113.50", "dst_port": 8080, "bytes_sent": 256, "bytes_recv": 512},
62
- {"timestamp": 1705601200, "dst_ip": "203.0.113.50", "dst_port": 8080, "bytes_sent": 256, "bytes_recv": 512},
63
- ], indent=2),
64
  }
65
 
66
 
67
- def parse_log_file(file_content: str) -> list:
68
- """Parse various log file formats into connection records."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  connections = []
70
- lines = file_content.strip().split('\n')
71
 
72
  for line in lines:
73
  line = line.strip()
74
  if not line or line.startswith('#'):
75
  continue
76
 
 
 
 
77
  try:
78
- record = json.loads(line)
79
- if 'dst_ip' in record or 'id.resp_h' in record:
80
- conn = {
81
- 'timestamp': record.get('timestamp', record.get('ts', 0)),
82
- 'dst_ip': record.get('dst_ip', record.get('id.resp_h', '')),
83
- 'dst_port': int(record.get('dst_port', record.get('id.resp_p', 0))),
84
- 'bytes_sent': int(record.get('bytes_sent', record.get('orig_bytes', 0) or 0)),
85
- 'bytes_recv': int(record.get('bytes_recv', record.get('resp_bytes', 0) or 0)),
86
- }
87
- if conn['dst_ip']:
88
- connections.append(conn)
89
- continue
90
- except (json.JSONDecodeError, ValueError):
91
  pass
92
 
93
- parts = line.split('\t')
94
- if len(parts) >= 10:
95
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  conn = {
97
- 'timestamp': float(parts[0]),
98
- 'dst_ip': parts[4],
99
- 'dst_port': int(parts[5]),
100
- 'bytes_sent': int(parts[9] if parts[9] != '-' else 0),
101
- 'bytes_recv': int(parts[10] if len(parts) > 10 and parts[10] != '-' else 0),
102
  }
103
- connections.append(conn)
104
- except (ValueError, IndexError):
105
- pass
 
 
 
 
106
 
107
  return connections
108
 
109
 
110
- def analyze_connections(
111
- connection_json: str,
112
- uploaded_file,
113
- threshold: float,
114
- strict_mode: bool,
115
- whitelist_ips: str,
116
- blacklist_ips: str
117
- ) -> str:
118
- """Analyze connection data and return results."""
119
  try:
120
- # Reset whitelist/blacklist
121
  sentinel.whitelist_ips = set()
122
- sentinel.whitelist_domains = set()
123
  sentinel.blacklist_ips = set()
124
- sentinel.blacklist_domains = set()
125
-
126
- # Apply whitelist
127
- if whitelist_ips and whitelist_ips.strip():
128
- ips = [ip.strip() for ip in whitelist_ips.split(',') if ip.strip()]
129
- sentinel.add_whitelist(ips=ips)
130
 
131
- # Apply blacklist
132
- if blacklist_ips and blacklist_ips.strip():
133
- ips = [ip.strip() for ip in blacklist_ips.split(',') if ip.strip()]
134
- sentinel.add_blacklist(ips=ips)
135
 
136
  # Get connections
137
  connections = []
138
 
139
- if uploaded_file is not None:
140
- file_content = uploaded_file.decode('utf-8') if isinstance(uploaded_file, bytes) else open(uploaded_file, 'r').read()
141
- connections = parse_log_file(file_content)
142
  if not connections:
143
  try:
144
- connections = json.loads(file_content)
145
  except:
146
  pass
147
 
148
- if not connections and connection_json and connection_json.strip():
149
- connections = json.loads(connection_json)
 
 
 
 
 
 
 
 
150
 
151
- if not isinstance(connections, list):
152
- return "## Error\nInput must be a JSON array of connection objects"
153
 
154
  if len(connections) < 3:
155
- return "## Error\nNeed at least 3 connections for analysis"
156
 
157
- # Run analysis
158
- result = sentinel.analyze(connections, threshold=threshold, strict_mode=strict_mode)
159
 
160
- # Format result
161
  if result.is_c2:
162
- output = f"## 🚨 C2 DETECTED: {result.c2_type}\n\n"
163
  else:
164
- output = "## ✅ No C2 Detected\n\n"
165
 
166
- output += f"| Metric | Value |\n|--------|-------|\n"
167
- output += f"| Probability | {result.c2_probability:.1%} |\n"
168
- output += f"| Confidence | {result.confidence:.1%} |\n"
169
- output += f"| Detection Method | {result.detection_method} |\n"
170
- output += f"| Connections | {len(connections)} |\n"
171
 
172
  if result.matched_legitimate_pattern:
173
- output += f"| Matched Pattern | {result.matched_legitimate_pattern} |\n"
174
 
175
  if result.risk_factors:
176
- output += "\n### Risk Factors\n"
177
- for f in result.risk_factors[:5]:
178
- output += f"- {f}\n"
179
-
180
- if result.mitigating_factors:
181
- output += "\n### Mitigating Factors\n"
182
- for f in result.mitigating_factors[:5]:
183
- output += f"- {f}\n"
184
 
185
- if result.recommendations:
186
- output += "\n### Recommendations\n"
187
- for r in result.recommendations[:3]:
188
- output += f"- {r}\n"
189
 
190
- return output
191
 
192
- except json.JSONDecodeError as e:
193
- return f"## Error\nInvalid JSON: {str(e)}"
194
  except Exception as e:
195
- return f"## Error\n{str(e)}"
196
 
197
 
198
- def load_example(example_name: str) -> str:
199
- """Load example connection data."""
200
- return EXAMPLES.get(example_name, "")
 
 
201
 
 
202
 
203
- # Build interface
204
- with gr.Blocks(title="C2Sentinel", theme=gr.themes.Soft()) as demo:
205
- gr.Markdown("""
206
- # C2Sentinel - C2 Beacon Detection
207
-
208
- Analyze network connections for Command and Control beacon activity.
209
 
210
- [Model](https://huggingface.co/danielostrow/c2sentinel) | [Docs](https://huggingface.co/danielostrow/c2sentinel/blob/main/API_REFERENCE.md) | [neuralintellect.com](https://neuralintellect.com)
211
- """)
 
 
 
212
 
213
- with gr.Row():
214
- with gr.Column():
215
- example_dropdown = gr.Dropdown(
216
- choices=list(EXAMPLES.keys()),
217
- value="-- Select Example --",
218
- label="Load Example"
219
- )
220
-
221
- connection_input = gr.Textbox(
222
- label="Connection Data (JSON)",
223
- placeholder='[{"timestamp": 1000000, "dst_ip": "10.0.0.1", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500}, ...]',
224
- lines=10
225
- )
226
-
227
- file_upload = gr.File(
228
- label="Or Upload Log File",
229
- file_types=[".json", ".log", ".txt"],
230
- type="binary"
231
- )
232
-
233
- with gr.Row():
234
- threshold = gr.Slider(
235
- minimum=0.1, maximum=0.9, value=0.5, step=0.1,
236
- label="Threshold (lower=sensitive)"
237
- )
238
- strict_mode = gr.Checkbox(label="Strict Mode", value=False)
239
-
240
- with gr.Accordion("Whitelist / Blacklist", open=False):
241
- whitelist_ips = gr.Textbox(
242
- label="Whitelist IPs (comma-separated)",
243
- placeholder="8.8.8.8, 1.1.1.1"
244
- )
245
- blacklist_ips = gr.Textbox(
246
- label="Blacklist IPs (comma-separated)",
247
- placeholder="10.10.10.10"
248
- )
249
-
250
- analyze_btn = gr.Button("Analyze", variant="primary")
251
-
252
- with gr.Column():
253
- result_output = gr.Markdown(label="Results")
254
-
255
- # Event handlers
256
- example_dropdown.change(
257
- fn=load_example,
258
- inputs=[example_dropdown],
259
- outputs=[connection_input]
260
- )
261
-
262
- analyze_btn.click(
263
- fn=analyze_connections,
264
- inputs=[connection_input, file_upload, threshold, strict_mode, whitelist_ips, blacklist_ips],
265
- outputs=[result_output]
266
- )
267
 
268
  gr.Markdown("""
269
  ---
270
- **Format:** `[{"timestamp": float, "dst_ip": "x.x.x.x", "dst_port": int, "bytes_sent": int, "bytes_recv": int}, ...]`
271
-
272
- Built on [LogBERT](https://arxiv.org/abs/2103.04475) | Author: Daniel Ostrow
273
  """)
274
 
 
 
275
 
276
  if __name__ == "__main__":
277
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  #!/usr/bin/env python3
2
+ """C2Sentinel - HuggingFace Space Demo"""
 
 
 
3
 
4
  import gradio as gr
5
  import json
6
+ import re
7
+ from datetime import datetime
8
  from huggingface_hub import hf_hub_download
9
 
10
+ # Download and load model
11
+ hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2sentinel.py", local_dir=".")
12
+ hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2_sentinel.safetensors", local_dir=".")
13
+ hf_hub_download(repo_id="danielostrow/c2sentinel", filename="c2_sentinel.json", local_dir=".")
 
 
14
  from c2sentinel import C2Sentinel
 
 
15
  sentinel = C2Sentinel.load('c2_sentinel')
16
 
17
+ # Examples
18
  EXAMPLES = {
19
+ "C2 Beacon (60s)": [
20
+ {"timestamp": 1705600000+i*60, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500}
21
+ for i in range(8)
22
+ ],
23
+ "Metasploit 4444": [
24
+ {"timestamp": 1705600000+i*30, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300}
25
+ for i in range(6)
26
+ ],
27
+ "SSH Keepalive": [
28
+ {"timestamp": 1705600000+i*30, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48}
29
+ for i in range(6)
30
+ ],
31
+ "Web Traffic": [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  {"timestamp": 1705600000, "dst_ip": "93.184.216.34", "dst_port": 443, "bytes_sent": 500, "bytes_recv": 15000},
33
+ {"timestamp": 1705600002, "dst_ip": "151.101.1.140", "dst_port": 443, "bytes_sent": 800, "bytes_recv": 45000},
34
+ {"timestamp": 1705600010, "dst_ip": "172.217.14.206", "dst_port": 443, "bytes_sent": 300, "bytes_recv": 12000},
35
+ {"timestamp": 1705600015, "dst_ip": "93.184.216.34", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 8000},
36
  {"timestamp": 1705600025, "dst_ip": "151.101.1.140", "dst_port": 443, "bytes_sent": 150, "bytes_recv": 5000},
37
+ ],
 
 
 
 
 
 
 
38
  }
39
 
40
 
41
+ def parse_timestamp(ts_str):
42
+ """Parse various timestamp formats to unix timestamp."""
43
+ if not ts_str:
44
+ return None
45
+
46
+ # Already numeric
47
+ try:
48
+ ts = float(ts_str)
49
+ if ts > 1e12: # milliseconds
50
+ return ts / 1000
51
+ return ts
52
+ except:
53
+ pass
54
+
55
+ # Common date formats
56
+ formats = [
57
+ "%Y-%m-%dT%H:%M:%S.%fZ",
58
+ "%Y-%m-%dT%H:%M:%SZ",
59
+ "%Y-%m-%dT%H:%M:%S.%f",
60
+ "%Y-%m-%dT%H:%M:%S",
61
+ "%Y-%m-%d %H:%M:%S.%f",
62
+ "%Y-%m-%d %H:%M:%S",
63
+ "%b %d %H:%M:%S",
64
+ "%b %d %H:%M:%S",
65
+ "%d/%b/%Y:%H:%M:%S",
66
+ ]
67
+
68
+ for fmt in formats:
69
+ try:
70
+ dt = datetime.strptime(ts_str.strip(), fmt)
71
+ if dt.year == 1900:
72
+ dt = dt.replace(year=datetime.now().year)
73
+ return dt.timestamp()
74
+ except:
75
+ continue
76
+ return None
77
+
78
+
79
+ def extract_ip_port(text):
80
+ """Extract IP and port from various formats."""
81
+ # IP:port format
82
+ m = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)', text)
83
+ if m:
84
+ return m.group(1), int(m.group(2))
85
+
86
+ # Just IP
87
+ m = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', text)
88
+ if m:
89
+ return m.group(1), None
90
+
91
+ return None, None
92
+
93
+
94
+ def parse_logs(content):
95
+ """Parse multiple log formats into connection records."""
96
  connections = []
97
+ lines = content.strip().split('\n')
98
 
99
  for line in lines:
100
  line = line.strip()
101
  if not line or line.startswith('#'):
102
  continue
103
 
104
+ conn = None
105
+
106
+ # 1. JSON format
107
  try:
108
+ obj = json.loads(line)
109
+ conn = {
110
+ 'timestamp': parse_timestamp(obj.get('timestamp') or obj.get('ts') or obj.get('@timestamp') or obj.get('EventTime') or obj.get('time')),
111
+ 'dst_ip': obj.get('dst_ip') or obj.get('dest_ip') or obj.get('id.resp_h') or obj.get('DestinationIP') or obj.get('dst') or obj.get('destination_ip'),
112
+ 'dst_port': obj.get('dst_port') or obj.get('dest_port') or obj.get('id.resp_p') or obj.get('DestinationPort') or obj.get('dport'),
113
+ 'bytes_sent': obj.get('bytes_sent') or obj.get('orig_bytes') or obj.get('SentBytes') or obj.get('out_bytes') or 0,
114
+ 'bytes_recv': obj.get('bytes_recv') or obj.get('resp_bytes') or obj.get('ReceivedBytes') or obj.get('in_bytes') or 0,
115
+ }
116
+ except json.JSONDecodeError:
 
 
 
 
117
  pass
118
 
119
+ # 2. Zeek/Bro conn.log (tab-separated)
120
+ if not conn:
121
+ parts = line.split('\t')
122
+ if len(parts) >= 10:
123
+ try:
124
+ conn = {
125
+ 'timestamp': parse_timestamp(parts[0]),
126
+ 'dst_ip': parts[4] if parts[4] != '-' else None,
127
+ 'dst_port': int(parts[5]) if parts[5] != '-' else None,
128
+ 'bytes_sent': int(parts[9]) if len(parts) > 9 and parts[9] != '-' else 0,
129
+ 'bytes_recv': int(parts[10]) if len(parts) > 10 and parts[10] != '-' else 0,
130
+ }
131
+ except:
132
+ pass
133
+
134
+ # 3. Syslog format (various)
135
+ if not conn:
136
+ # Standard syslog: "Mon DD HH:MM:SS host process: message"
137
+ syslog_match = re.match(r'^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+\S+:\s*(.*)$', line)
138
+ if syslog_match:
139
+ ts = parse_timestamp(syslog_match.group(1))
140
+ msg = syslog_match.group(2)
141
+ dst_ip, dst_port = extract_ip_port(msg)
142
+
143
+ bytes_match = re.search(r'(\d+)\s*bytes?', msg, re.I)
144
+ bytes_val = int(bytes_match.group(1)) if bytes_match else 100
145
+
146
+ if dst_ip:
147
+ conn = {
148
+ 'timestamp': ts,
149
+ 'dst_ip': dst_ip,
150
+ 'dst_port': dst_port or 443,
151
+ 'bytes_sent': bytes_val // 2,
152
+ 'bytes_recv': bytes_val // 2,
153
+ }
154
+
155
+ # 4. Windows Event Log (XML-ish or key=value)
156
+ if not conn:
157
+ # Key=value format
158
+ if '=' in line:
159
+ kv = dict(re.findall(r'(\w+)=("[^"]*"|\S+)', line))
160
+ for k in list(kv.keys()):
161
+ kv[k] = kv[k].strip('"')
162
+
163
+ dst_ip = kv.get('DestAddress') or kv.get('DestinationIp') or kv.get('dst') or kv.get('RemoteAddress')
164
+ dst_port = kv.get('DestPort') or kv.get('DestinationPort') or kv.get('dport') or kv.get('RemotePort')
165
+ ts = kv.get('TimeGenerated') or kv.get('EventTime') or kv.get('timestamp')
166
+
167
+ if dst_ip:
168
+ conn = {
169
+ 'timestamp': parse_timestamp(ts) if ts else datetime.now().timestamp(),
170
+ 'dst_ip': dst_ip,
171
+ 'dst_port': int(dst_port) if dst_port and dst_port.isdigit() else 443,
172
+ 'bytes_sent': int(kv.get('SentBytes', 100)),
173
+ 'bytes_recv': int(kv.get('ReceivedBytes', 100)),
174
+ }
175
+
176
+ # 5. CSV format
177
+ if not conn and ',' in line and not line.startswith('{'):
178
+ parts = [p.strip().strip('"') for p in line.split(',')]
179
+ if len(parts) >= 4:
180
+ # Try to identify columns
181
+ for i, p in enumerate(parts):
182
+ ip, port = extract_ip_port(p)
183
+ if ip:
184
+ ts = None
185
+ for j, q in enumerate(parts):
186
+ ts = parse_timestamp(q)
187
+ if ts:
188
+ break
189
+
190
+ conn = {
191
+ 'timestamp': ts or datetime.now().timestamp(),
192
+ 'dst_ip': ip,
193
+ 'dst_port': port or 443,
194
+ 'bytes_sent': 100,
195
+ 'bytes_recv': 100,
196
+ }
197
+
198
+ # Look for bytes
199
+ for p in parts:
200
+ if p.isdigit() and int(p) > 0:
201
+ conn['bytes_sent'] = int(p)
202
+ break
203
+ break
204
+
205
+ # 6. Generic IP extraction as fallback
206
+ if not conn:
207
+ dst_ip, dst_port = extract_ip_port(line)
208
+ if dst_ip:
209
+ ts = parse_timestamp(re.search(r'[\d\-T:\.Z]+', line).group() if re.search(r'[\d\-T:\.Z]+', line) else None)
210
  conn = {
211
+ 'timestamp': ts or datetime.now().timestamp(),
212
+ 'dst_ip': dst_ip,
213
+ 'dst_port': dst_port or 443,
214
+ 'bytes_sent': 100,
215
+ 'bytes_recv': 100,
216
  }
217
+
218
+ # Validate and add
219
+ if conn and conn.get('dst_ip') and conn.get('timestamp'):
220
+ conn['dst_port'] = int(conn['dst_port'] or 443)
221
+ conn['bytes_sent'] = int(conn['bytes_sent'] or 0)
222
+ conn['bytes_recv'] = int(conn['bytes_recv'] or 0)
223
+ connections.append(conn)
224
 
225
  return connections
226
 
227
 
228
+ def analyze(input_text, file_obj, example, threshold, whitelist, blacklist):
229
+ """Main analysis function."""
 
 
 
 
 
 
 
230
  try:
231
+ # Reset lists
232
  sentinel.whitelist_ips = set()
 
233
  sentinel.blacklist_ips = set()
 
 
 
 
 
 
234
 
235
+ if whitelist:
236
+ sentinel.add_whitelist(ips=[ip.strip() for ip in whitelist.split(',') if ip.strip()])
237
+ if blacklist:
238
+ sentinel.add_blacklist(ips=[ip.strip() for ip in blacklist.split(',') if ip.strip()])
239
 
240
  # Get connections
241
  connections = []
242
 
243
+ if file_obj:
244
+ content = file_obj.decode('utf-8') if isinstance(file_obj, bytes) else open(file_obj, 'r').read()
245
+ connections = parse_logs(content)
246
  if not connections:
247
  try:
248
+ connections = json.loads(content)
249
  except:
250
  pass
251
 
252
+ if not connections and example and example in EXAMPLES:
253
+ connections = EXAMPLES[example]
254
+
255
+ if not connections and input_text:
256
+ connections = parse_logs(input_text)
257
+ if not connections:
258
+ try:
259
+ connections = json.loads(input_text)
260
+ except:
261
+ pass
262
 
263
+ if not connections:
264
+ return "⚠️ No valid connections found. Check input format."
265
 
266
  if len(connections) < 3:
267
+ return f"⚠️ Need 3+ connections (found {len(connections)})"
268
 
269
+ # Analyze
270
+ result = sentinel.analyze(connections, threshold=threshold)
271
 
272
+ # Format output
273
  if result.is_c2:
274
+ out = f"### 🚨 C2 DETECTED\n**Type:** {result.c2_type}\n\n"
275
  else:
276
+ out = "### ✅ No C2 Detected\n\n"
277
 
278
+ out += f"**Probability:** {result.c2_probability:.0%} · **Confidence:** {result.confidence:.0%} · **Connections:** {len(connections)}\n\n"
 
 
 
 
279
 
280
  if result.matched_legitimate_pattern:
281
+ out += f"**Pattern:** {result.matched_legitimate_pattern}\n\n"
282
 
283
  if result.risk_factors:
284
+ out += "**Risk:** " + " · ".join(result.risk_factors[:3]) + "\n\n"
 
 
 
 
 
 
 
285
 
286
+ if result.recommendations and result.is_c2:
287
+ out += "**Action:** " + result.recommendations[0] + "\n"
 
 
288
 
289
+ return out
290
 
 
 
291
  except Exception as e:
292
+ return f"⚠️ Error: {str(e)}"
293
 
294
 
295
+ # UI
296
+ with gr.Blocks(title="C2Sentinel", theme=gr.themes.Soft(), css="""
297
+ .container { max-width: 900px; margin: auto; }
298
+ footer { display: none !important; }
299
+ """) as demo:
300
 
301
+ gr.Markdown("# 🛡️ C2Sentinel\nDetect C2 beacon patterns in network logs")
302
 
303
+ with gr.Row():
304
+ with gr.Column(scale=3):
305
+ file_input = gr.File(label="Upload Log File", file_types=[".json", ".log", ".txt", ".csv", ".evtx"], type="binary")
306
+ text_input = gr.Textbox(label="Or Paste Log Data", lines=6, placeholder="JSON, syslog, Zeek, CSV, Windows events...")
307
+ example_select = gr.Radio(choices=list(EXAMPLES.keys()), label="Or Use Example", value=None)
 
308
 
309
+ with gr.Column(scale=2):
310
+ threshold = gr.Slider(0.3, 0.8, 0.5, step=0.1, label="Sensitivity", info="Lower = more alerts")
311
+ whitelist = gr.Textbox(label="Whitelist IPs", placeholder="8.8.8.8, 1.1.1.1", lines=1)
312
+ blacklist = gr.Textbox(label="Blacklist IPs", placeholder="10.10.10.10", lines=1)
313
+ btn = gr.Button("Analyze", variant="primary", size="lg")
314
 
315
+ output = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
  gr.Markdown("""
318
  ---
319
+ **Formats:** JSON, Zeek conn.log, syslog, Graylog, Windows Event, CSV
320
+ **Docs:** [Model](https://huggingface.co/danielostrow/c2sentinel) · [API](https://huggingface.co/danielostrow/c2sentinel/blob/main/API_REFERENCE.md) · [neuralintellect.com](https://neuralintellect.com)
 
321
  """)
322
 
323
+ btn.click(analyze, [text_input, file_input, example_select, threshold, whitelist, blacklist], output)
324
+ example_select.change(lambda x: "", [example_select], [text_input])
325
 
326
  if __name__ == "__main__":
327
  demo.launch(server_name="0.0.0.0", server_port=7860)