Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""C2Sentinel
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
import json
|
|
@@ -7,321 +7,153 @@ import re
|
|
| 7 |
from datetime import datetime
|
| 8 |
from huggingface_hub import hf_hub_download
|
| 9 |
|
| 10 |
-
#
|
| 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
|
| 20 |
-
|
| 21 |
-
|
| 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
|
| 42 |
-
|
| 43 |
-
if not ts_str:
|
| 44 |
-
return None
|
| 45 |
-
|
| 46 |
-
# Already numeric
|
| 47 |
try:
|
| 48 |
-
|
| 49 |
-
if
|
| 50 |
-
|
| 51 |
-
|
| 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(
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
lines = content.strip().split('\n')
|
| 98 |
-
|
| 99 |
-
for line in lines:
|
| 100 |
line = line.strip()
|
| 101 |
-
if not line or line.startswith('#'):
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
conn = None
|
| 105 |
|
| 106 |
-
#
|
| 107 |
try:
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
'timestamp':
|
| 111 |
-
'dst_ip':
|
| 112 |
-
'dst_port':
|
| 113 |
-
'bytes_sent':
|
| 114 |
-
'bytes_recv':
|
| 115 |
}
|
| 116 |
-
except
|
| 117 |
-
pass
|
| 118 |
|
| 119 |
-
#
|
| 120 |
-
if not
|
| 121 |
-
|
| 122 |
-
if len(
|
| 123 |
try:
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 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
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
if not
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 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"
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 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)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""C2Sentinel Demo"""
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
import json
|
|
|
|
| 7 |
from datetime import datetime
|
| 8 |
from huggingface_hub import hf_hub_download
|
| 9 |
|
| 10 |
+
# 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 |
+
"C2 Beacon": [{"timestamp": 1705600000+i*60, "dst_ip": "45.33.32.156", "dst_port": 443, "bytes_sent": 200, "bytes_recv": 500} for i in range(8)],
|
| 19 |
+
"Metasploit": [{"timestamp": 1705600000+i*30, "dst_ip": "10.10.10.10", "dst_port": 4444, "bytes_sent": 150, "bytes_recv": 300} for i in range(6)],
|
| 20 |
+
"SSH Keepalive": [{"timestamp": 1705600000+i*30, "dst_ip": "192.168.1.10", "dst_port": 22, "bytes_sent": 48, "bytes_recv": 48} for i in range(6)],
|
| 21 |
+
"Web Traffic": [{"timestamp": 1705600000+i*5, "dst_ip": f"93.184.{i}.34", "dst_port": 443, "bytes_sent": 500+i*100, "bytes_recv": 15000+i*5000} for i in range(5)],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
|
| 25 |
+
def parse_ts(s):
|
| 26 |
+
if not s: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
try:
|
| 28 |
+
t = float(s)
|
| 29 |
+
return t/1000 if t > 1e12 else t
|
| 30 |
+
except: pass
|
| 31 |
+
for f in ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%b %d %H:%M:%S"]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
try:
|
| 33 |
+
dt = datetime.strptime(str(s).strip(), f)
|
| 34 |
+
if dt.year == 1900: dt = dt.replace(year=2026)
|
|
|
|
| 35 |
return dt.timestamp()
|
| 36 |
+
except: pass
|
|
|
|
| 37 |
return None
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
def parse_logs(content):
|
| 41 |
+
conns = []
|
| 42 |
+
for line in content.strip().split('\n'):
|
|
|
|
|
|
|
|
|
|
| 43 |
line = line.strip()
|
| 44 |
+
if not line or line.startswith('#'): continue
|
| 45 |
+
c = None
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
# JSON
|
| 48 |
try:
|
| 49 |
+
o = json.loads(line)
|
| 50 |
+
c = {
|
| 51 |
+
'timestamp': parse_ts(o.get('timestamp') or o.get('ts') or o.get('@timestamp') or o.get('time')),
|
| 52 |
+
'dst_ip': o.get('dst_ip') or o.get('dest_ip') or o.get('id.resp_h') or o.get('DestinationIP') or o.get('dst'),
|
| 53 |
+
'dst_port': o.get('dst_port') or o.get('dest_port') or o.get('id.resp_p') or o.get('DestinationPort') or o.get('dport'),
|
| 54 |
+
'bytes_sent': o.get('bytes_sent') or o.get('orig_bytes') or o.get('SentBytes') or 100,
|
| 55 |
+
'bytes_recv': o.get('bytes_recv') or o.get('resp_bytes') or o.get('ReceivedBytes') or 100,
|
| 56 |
}
|
| 57 |
+
except: pass
|
|
|
|
| 58 |
|
| 59 |
+
# Zeek
|
| 60 |
+
if not c:
|
| 61 |
+
p = line.split('\t')
|
| 62 |
+
if len(p) >= 10:
|
| 63 |
try:
|
| 64 |
+
c = {'timestamp': parse_ts(p[0]), 'dst_ip': p[4] if p[4]!='-' else None, 'dst_port': int(p[5]) if p[5]!='-' else 443,
|
| 65 |
+
'bytes_sent': int(p[9]) if p[9]!='-' else 100, 'bytes_recv': int(p[10]) if len(p)>10 and p[10]!='-' else 100}
|
| 66 |
+
except: pass
|
| 67 |
+
|
| 68 |
+
# Syslog
|
| 69 |
+
if not c:
|
| 70 |
+
m = re.match(r'^(\w{3}\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+\S+:\s*(.*)$', line)
|
| 71 |
+
if m:
|
| 72 |
+
ip = re.search(r'(\d+\.\d+\.\d+\.\d+)', m.group(2))
|
| 73 |
+
if ip:
|
| 74 |
+
c = {'timestamp': parse_ts(m.group(1)), 'dst_ip': ip.group(1), 'dst_port': 443, 'bytes_sent': 100, 'bytes_recv': 100}
|
| 75 |
+
|
| 76 |
+
# Key=value
|
| 77 |
+
if not c and '=' in line:
|
| 78 |
+
kv = dict(re.findall(r'(\w+)=(\S+)', line))
|
| 79 |
+
ip = kv.get('DestAddress') or kv.get('DestinationIp') or kv.get('dst') or kv.get('RemoteAddress')
|
| 80 |
+
if ip:
|
| 81 |
+
c = {'timestamp': parse_ts(kv.get('TimeGenerated') or kv.get('EventTime')) or datetime.now().timestamp(),
|
| 82 |
+
'dst_ip': ip, 'dst_port': int(kv.get('DestPort', 443)), 'bytes_sent': 100, 'bytes_recv': 100}
|
| 83 |
+
|
| 84 |
+
# CSV
|
| 85 |
+
if not c and ',' in line:
|
| 86 |
+
for p in line.split(','):
|
| 87 |
+
ip = re.search(r'(\d+\.\d+\.\d+\.\d+)', p)
|
| 88 |
+
if ip:
|
| 89 |
+
c = {'timestamp': datetime.now().timestamp(), 'dst_ip': ip.group(1), 'dst_port': 443, 'bytes_sent': 100, 'bytes_recv': 100}
|
| 90 |
+
break
|
| 91 |
+
|
| 92 |
+
# Fallback
|
| 93 |
+
if not c:
|
| 94 |
+
ip = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
|
| 95 |
+
if ip:
|
| 96 |
+
c = {'timestamp': datetime.now().timestamp(), 'dst_ip': ip.group(1), 'dst_port': 443, 'bytes_sent': 100, 'bytes_recv': 100}
|
| 97 |
+
|
| 98 |
+
if c and c.get('dst_ip') and c.get('timestamp'):
|
| 99 |
+
c['dst_port'] = int(c.get('dst_port') or 443)
|
| 100 |
+
c['bytes_sent'] = int(c.get('bytes_sent') or 100)
|
| 101 |
+
c['bytes_recv'] = int(c.get('bytes_recv') or 100)
|
| 102 |
+
conns.append(c)
|
| 103 |
+
return conns
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def analyze(text, file, example, thresh, wl, bl):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
try:
|
|
|
|
| 108 |
sentinel.whitelist_ips = set()
|
| 109 |
sentinel.blacklist_ips = set()
|
| 110 |
+
if wl: sentinel.add_whitelist(ips=[x.strip() for x in wl.split(',') if x.strip()])
|
| 111 |
+
if bl: sentinel.add_blacklist(ips=[x.strip() for x in bl.split(',') if x.strip()])
|
| 112 |
+
|
| 113 |
+
conns = []
|
| 114 |
+
if file:
|
| 115 |
+
content = file.decode('utf-8') if isinstance(file, bytes) else open(file).read()
|
| 116 |
+
conns = parse_logs(content)
|
| 117 |
+
if not conns:
|
| 118 |
+
try: conns = json.loads(content)
|
| 119 |
+
except: pass
|
| 120 |
+
if not conns and example in EXAMPLES:
|
| 121 |
+
conns = EXAMPLES[example]
|
| 122 |
+
if not conns and text:
|
| 123 |
+
conns = parse_logs(text)
|
| 124 |
+
if not conns:
|
| 125 |
+
try: conns = json.loads(text)
|
| 126 |
+
except: pass
|
| 127 |
+
|
| 128 |
+
if not conns: return "No valid connections found"
|
| 129 |
+
if len(conns) < 3: return f"Need 3+ connections (found {len(conns)})"
|
| 130 |
+
|
| 131 |
+
r = sentinel.analyze(conns, threshold=thresh)
|
| 132 |
+
|
| 133 |
+
out = f"### {'C2 DETECTED: '+r.c2_type if r.is_c2 else 'No C2 Detected'}\n\n"
|
| 134 |
+
out += f"**Prob:** {r.c2_probability:.0%} | **Conf:** {r.confidence:.0%} | **N:** {len(conns)}\n\n"
|
| 135 |
+
if r.matched_legitimate_pattern: out += f"**Pattern:** {r.matched_legitimate_pattern}\n"
|
| 136 |
+
if r.risk_factors: out += "**Risk:** " + ", ".join(r.risk_factors[:3]) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
return out
|
|
|
|
| 138 |
except Exception as e:
|
| 139 |
+
return f"Error: {e}"
|
|
|
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
with gr.Blocks(title="C2Sentinel") as demo:
|
| 143 |
+
gr.Markdown("## C2Sentinel - Beacon Detection\n[Docs](https://huggingface.co/danielostrow/c2sentinel)")
|
| 144 |
|
| 145 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
with gr.Column(scale=2):
|
| 147 |
+
file_in = gr.File(label="Log File", file_types=[".json",".log",".txt",".csv"], type="binary")
|
| 148 |
+
text_in = gr.Textbox(label="Or Paste Logs", lines=5)
|
| 149 |
+
ex_in = gr.Dropdown(choices=[""] + list(EXAMPLES.keys()), value="", label="Example")
|
| 150 |
+
with gr.Column(scale=1):
|
| 151 |
+
thresh = gr.Slider(0.3, 0.8, 0.5, label="Threshold")
|
| 152 |
+
wl = gr.Textbox(label="Whitelist", placeholder="8.8.8.8")
|
| 153 |
+
bl = gr.Textbox(label="Blacklist", placeholder="10.10.10.10")
|
| 154 |
+
btn = gr.Button("Analyze", variant="primary")
|
| 155 |
+
|
| 156 |
+
out = gr.Markdown()
|
| 157 |
+
btn.click(analyze, [text_in, file_in, ex_in, thresh, wl, bl], out)
|
| 158 |
+
|
| 159 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|