Última atividade 1 month ago

Revisão cd50a34dd0d83023239a502a1c0141a5ac4fea45

gistfile1.txt Bruto
1#!/usr/bin/env python3
2"""
3wgctl JSON helper — called by shell functions to read/write JSON files.
4Usage: json_helper.py <command> <file> [key] [value]
5"""
6
7import sys
8import json
9import os
10
11DATETIME_FMT = os.environ.get('WGCTL_DATETIME_FMT', '%Y-%m-%d %H:%M')
12
13def get(file, key):
14 try:
15 with open(file) as f:
16 data = json.load(f)
17 val = data.get(key, [])
18 if isinstance(val, bool):
19 print(str(val).lower()) # true/false not True/False
20 elif isinstance(val, list):
21 if val:
22 print('\n'.join(str(v) for v in val))
23 else:
24 if val:
25 print(val)
26 except:
27 sys.exit(0)
28
29def set_key(file, key, value):
30 try:
31 data = {}
32 if os.path.exists(file):
33 with open(file) as f:
34 data = json.load(f)
35 # Try to parse as JSON value first (for arrays/bools)
36 try:
37 data[key] = json.loads(value)
38 except:
39 data[key] = value
40 with open(file, 'w') as f:
41 json.dump(data, f, indent=2)
42 except Exception as e:
43 print(f"Error: {e}", file=sys.stderr)
44 sys.exit(1)
45
46def delete_key(file, key):
47 try:
48 with open(file) as f:
49 data = json.load(f)
50 data.pop(key, None)
51 with open(file, 'w') as f:
52 json.dump(data, f, indent=2)
53 except Exception as e:
54 print(f"Error: {e}", file=sys.stderr)
55 sys.exit(1)
56
57def append(file, key, value):
58 try:
59 data = {}
60 if os.path.exists(file):
61 with open(file) as f:
62 data = json.load(f)
63 if key not in data:
64 data[key] = []
65 if value not in data[key]:
66 data[key].append(value)
67 with open(file, 'w') as f:
68 json.dump(data, f, indent=2)
69 except Exception as e:
70 print(f"Error: {e}", file=sys.stderr)
71 sys.exit(1)
72
73def remove_value(file, key, value):
74 try:
75 with open(file) as f:
76 data = json.load(f)
77 if key in data and value in data[key]:
78 data[key].remove(value)
79 with open(file, 'w') as f:
80 json.dump(data, f, indent=2)
81 except Exception as e:
82 print(f"Error: {e}", file=sys.stderr)
83 sys.exit(1)
84
85def cat(file):
86 try:
87 with open(file) as f:
88 data = json.load(f)
89 print(json.dumps(data, indent=2))
90 except Exception as e:
91 sys.exit(1)
92
93def has_key(file, key):
94 try:
95 with open(file) as f:
96 data = json.load(f)
97 sys.exit(0 if key in data else 1)
98 except:
99 sys.exit(1)
100
101def filter_values(file, key, value):
102 """Remove all entries where value matches"""
103 try:
104 with open(file) as f:
105 data = json.load(f)
106 data = {k: v for k, v in data.items() if v != value}
107 with open(file, 'w') as f:
108 json.dump(data, f, indent=2)
109 except Exception as e:
110 print(f"Error: {e}", file=sys.stderr)
111 sys.exit(1)
112
113def last_event(file, key, field, client):
114 """Get last event field for a client"""
115 try:
116 last = None
117 with open(file) as f:
118 for line in f:
119 try:
120 e = json.loads(line.strip())
121 if e.get(key) == client:
122 last = e
123 except:
124 pass
125 if last:
126 print(last.get(field, ''))
127 except:
128 pass
129
130def events_for(file, ip, limit):
131 """Format events for a given IP"""
132 try:
133 from datetime import datetime
134 events = []
135 with open(file) as f:
136 for line in f:
137 try:
138 e = json.loads(line.strip())
139 if e.get('ip') == ip:
140 events.append(e)
141 except:
142 pass
143 for e in events[-int(limit):]:
144 ts = e.get('timestamp', '')
145 try:
146 dt = datetime.fromisoformat(ts)
147 ts = dt.strftime(DATETIME_FMT)
148 except:
149 pass
150 endpoint = e.get('endpoint', '—')
151 client = e.get('client', '—')
152 event = e.get('event', '—')
153 print(f' {ts} {client:<20} {endpoint:<20} {event}')
154 except:
155 pass
156
157def fw_events(file, filter_ip, filter_type, clients_dir, limit):
158 """Format firewall drop events with dedup and counts"""
159 import glob
160 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
161
162 ip_to_name = {}
163 for conf in glob.glob(f"{clients_dir}/*.conf"):
164 name = os.path.basename(conf).replace('.conf', '')
165 try:
166 with open(conf) as f:
167 for line in f:
168 if line.startswith('Address'):
169 ip = line.split('=')[1].strip().split('/')[0]
170 ip_to_name[ip] = name
171 except:
172 pass
173
174 events = []
175 last_seen = {}
176
177 try:
178 with open(file) as f:
179 for line in f:
180 try:
181 e = json.loads(line.strip())
182 src = e.get('src_ip', '')
183 if not src:
184 continue
185 if filter_ip and src != filter_ip:
186 continue
187 dst = e.get('dest_ip', '')
188 port = e.get('dest_port', '')
189 proto = e.get('ip.protocol', 0)
190 key = (src, dst, port, proto)
191 ts_str = e.get('timestamp', '')
192 try:
193 from datetime import datetime
194 ts = datetime.fromisoformat(ts_str).timestamp()
195 except:
196 ts = 0
197 windows = {1: 5, 6: 30, 17: 10}
198 window = windows.get(proto, 10)
199 if key in last_seen and (ts - last_seen[key]) < window:
200 continue
201 last_seen[key] = ts
202 events.append(e)
203 except:
204 pass
205 except:
206 pass
207
208 # Dedup consecutive same src+dst+port within 60s with count
209 deduped = []
210 counts = []
211 for e in events:
212 ts_str = e.get('timestamp', '')
213 try:
214 from datetime import datetime
215 ts = datetime.fromisoformat(ts_str).timestamp()
216 except:
217 ts = 0
218 src = e.get('src_ip', '')
219 dst = e.get('dest_ip', '')
220 port = e.get('dest_port', '')
221 proto = e.get('ip.protocol', 0)
222 key = (src, dst, port, proto)
223
224 if deduped and counts:
225 prev = deduped[-1]
226 try:
227 prev_ts = datetime.fromisoformat(prev.get('timestamp','')).timestamp()
228 except:
229 prev_ts = 0
230 prev_key = (prev.get('src_ip',''), prev.get('dest_ip',''),
231 prev.get('dest_port',''), prev.get('ip.protocol',0))
232 if key == prev_key and (ts - prev_ts) < 60:
233 counts[-1] += 1
234 continue
235
236 deduped.append(e)
237 counts.append(1)
238
239 grouped = []
240 group_counts = []
241 for e in deduped:
242 ts_str = e.get('timestamp', '')
243 try:
244 from datetime import datetime
245 dt = datetime.fromisoformat(ts_str)
246 # Truncate to minute for grouping
247 minute_key = dt.strftime('%Y-%m-%d %H:%M')
248 except:
249 minute_key = ts_str[:16]
250
251 src = e.get('src_ip', '')
252 dst = e.get('dest_ip', '')
253 port = e.get('dest_port', '')
254 proto = e.get('ip.protocol', 0)
255 key = (minute_key, src, dst, proto) # group within same minute
256
257 if grouped and group_counts:
258 prev = grouped[-1]
259 try:
260 prev_dt = datetime.fromisoformat(prev.get('timestamp',''))
261 prev_minute = prev_dt.strftime('%Y-%m-%d %H:%M')
262 except:
263 prev_minute = ''
264 prev_key = (prev_minute, prev.get('src_ip',''),
265 prev.get('dest_ip',''), prev.get('ip.protocol',0))
266 if key == prev_key:
267 group_counts[-1] += 1
268 continue
269
270 grouped.append(e)
271 group_counts.append(1)
272
273 for e, count in zip(deduped[-int(limit):], counts[-int(limit):]):
274 ts = e.get('timestamp', '')
275 try:
276 from datetime import datetime
277 dt = datetime.fromisoformat(ts)
278 ts = dt.strftime(DATETIME_FMT)
279 except:
280 pass
281 src = e.get('src_ip', '—')
282 dst = e.get('dest_ip', '—')
283 port = e.get('dest_port', '')
284 proto_num = e.get('ip.protocol', 0)
285 proto = proto_map.get(proto_num, str(proto_num))
286 dst_str = f"{dst}:{port}" if port else dst
287 client = ip_to_name.get(src, src)
288 if filter_type and not client.startswith(filter_type + '-'):
289 continue
290 count_str = f" (x{count})" if count > 1 else ""
291 print(f"{ts}|{client}|{dst_str}|{proto}{count_str}")
292
293def wg_events(file, filter_client, filter_type, limit):
294 """Format WireGuard events from events.log with dedup"""
295 events = []
296 try:
297 with open(file) as f:
298 for line in f:
299 try:
300 e = json.loads(line.strip())
301 client = e.get('client', '')
302 if not client:
303 continue
304 if filter_client and client != filter_client:
305 continue
306 if filter_type and not client.startswith(filter_type + '-'):
307 continue
308 events.append(e)
309 except:
310 pass
311 except:
312 pass
313
314 # Dedup consecutive same client+event+endpoint within 60s
315 deduped = []
316 counts = []
317 for e in events:
318 ts_str = e.get('timestamp', '')
319 try:
320 from datetime import datetime
321 ts = datetime.fromisoformat(ts_str).timestamp()
322 except:
323 ts = 0
324 client = e.get('client', '')
325 event = e.get('event', '')
326 endpoint = e.get('endpoint', '')
327 key = (client, event, endpoint[:15])
328
329 if deduped and counts:
330 prev = deduped[-1]
331 prev_ts_str = prev.get('timestamp', '')
332 try:
333 prev_ts = datetime.fromisoformat(prev_ts_str).timestamp()
334 except:
335 prev_ts = 0
336 prev_key = (prev.get('client',''), prev.get('event',''), prev.get('endpoint','')[:15])
337 if key == prev_key and (ts - prev_ts) < 60:
338 counts[-1] += 1
339 continue
340
341 deduped.append(e)
342 counts.append(1)
343
344 for e, count in zip(deduped[-int(limit):], counts[-int(limit):]):
345 ts = e.get('timestamp', '')
346 try:
347 from datetime import datetime
348 dt = datetime.fromisoformat(ts)
349 ts = dt.strftime(DATETIME_FMT)
350 except:
351 pass
352 client = e.get('client', '—')
353 endpoint = e.get('endpoint', '—')
354 event = e.get('event', '—')
355 count_str = f" (x{count})" if count > 1 else ""
356 print(f"{ts}|{client}|{endpoint}|{event}{count_str}")
357
358def format_fw_event(line, clients_dir):
359 """Format a single fw_event line"""
360 import glob
361 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
362
363 # Build ip->name map
364 ip_to_name = {}
365 for conf in glob.glob(f"{clients_dir}/*.conf"):
366 name = os.path.basename(conf).replace('.conf', '')
367 try:
368 with open(conf) as f:
369 for l in f:
370 if l.startswith('Address'):
371 ip = l.split('=')[1].strip().split('/')[0]
372 ip_to_name[ip] = name
373 except:
374 pass
375
376 try:
377 e = json.loads(line.strip())
378 src = e.get('src_ip', '')
379 if not src:
380 return None
381 ts = e.get('timestamp', '')
382 try:
383 from datetime import datetime
384 dt = datetime.fromisoformat(ts)
385 ts = dt.strftime(DATETIME_FMT)
386 except:
387 pass
388 dst = e.get('dest_ip', '—')
389 port = e.get('dest_port', '')
390 proto_num = e.get('ip.protocol', 0)
391 proto = proto_map.get(proto_num, str(proto_num))
392 dst_str = f"{dst}:{port}" if port else dst
393 client = ip_to_name.get(src, src)
394 return f"{ts}|{client}|{dst_str}|{proto}"
395 except:
396 return None
397
398def format_wg_event(line):
399 """Format a single wg_event line"""
400 try:
401 e = json.loads(line.strip())
402 client = e.get('client', '')
403 if not client:
404 return None
405 ts = e.get('timestamp', '')
406 try:
407 from datetime import datetime
408 dt = datetime.fromisoformat(ts)
409 ts = dt.strftime(DATETIME_FMT)
410 except:
411 pass
412 endpoint = e.get('endpoint', '—')
413 event = e.get('event', '—')
414 return f"{ts}|{client}|{endpoint}|{event}|wg"
415 except:
416 return None
417
418def remove_events(file, identifier):
419 """Remove all events for a client/ip from a JSONL file"""
420 try:
421 lines = []
422 with open(file) as f:
423 for line in f:
424 try:
425 e = json.loads(line.strip())
426 if e.get('client') == identifier or e.get('src_ip') == identifier:
427 continue
428 lines.append(line)
429 except:
430 lines.append(line)
431 with open(file, 'w') as f:
432 f.writelines(lines)
433 except Exception as e:
434 print(f"Error: {e}", file=sys.stderr)
435 sys.exit(1)
436
437def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
438 """Follow both log files and output formatted events"""
439 import glob, time, select
440
441 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
442 peer_filter = set(filter_peers.split(',')) if filter_peers else set()
443
444 # Build ip->name map
445 ip_to_name = {}
446 for conf in glob.glob(f"{clients_dir}/*.conf"):
447 name = os.path.basename(conf).replace('.conf', '')
448 try:
449 with open(conf) as f:
450 for l in f:
451 if l.startswith('Address'):
452 ip = l.split('=')[1].strip().split('/')[0]
453 ip_to_name[ip] = name
454 except:
455 pass
456
457 # Open files and seek to end
458 files = {}
459 for label, path in [('fw', fw_file), ('wg', wg_file)]:
460 if path and os.path.exists(path):
461 f = open(path)
462 f.seek(0, 2) # seek to end
463 files[label] = f
464
465 dedup = {}
466
467 try:
468 while True:
469 for label, f in files.items():
470 line = f.readline()
471 if not line:
472 continue
473 try:
474 e = json.loads(line.strip())
475 except:
476 continue
477
478 if label == 'fw':
479 src = e.get('src_ip', '')
480 if not src:
481 continue
482 if filter_ip and src != filter_ip:
483 continue
484
485 # Filter by peer names if specified
486 if peer_filter:
487 client_name = ip_to_name.get(src, '')
488 if client_name not in peer_filter:
489 continue
490 dst = e.get('dest_ip', '—')
491 port = e.get('dest_port', '')
492 proto_num = e.get('ip.protocol', 0)
493 proto = proto_map.get(proto_num, str(proto_num))
494
495 # Dedup
496 key = (src, dst, port, proto_num)
497 windows = {1: 5, 6: 30, 17: 10}
498 window = windows.get(proto_num, 10)
499 now = time.time()
500 if key in dedup and (now - dedup[key]) < window:
501 continue
502 dedup[key] = now
503
504 client = ip_to_name.get(src, src)
505 if filter_type and not client.startswith(filter_type + '-'):
506 continue
507 dst_str = f"{dst}:{port}" if port else dst
508 ts = e.get('timestamp', '')[:16].replace('T', ' ')
509 print(f"fw|{ts}|{client}|{dst_str}|{proto}", flush=True)
510
511 elif label == 'wg':
512 client = e.get('client', '')
513 if not client:
514 continue
515 if filter_ip:
516 ip = ip_to_name.get(filter_ip, '')
517 if client != ip and client != filter_ip:
518 continue
519
520 if peer_filter and client not in peer_filter:
521 continue
522 if filter_type and not client.startswith(filter_type + '-'):
523 continue
524 ts = e.get('timestamp', '')[:16].replace('T', ' ')
525 endpoint = e.get('endpoint', '—')
526 event = e.get('event', '—')
527 print(f"wg|{ts}|{client}|{endpoint}|{event}", flush=True)
528
529 time.sleep(0.1)
530 except KeyboardInterrupt:
531 pass
532
533def count(file, key):
534 try:
535 with open(file) as f:
536 data = json.load(f)
537 val = data.get(key, [])
538 print(len(val) if isinstance(val, list) else 0)
539 except:
540 print(0)
541
542def audit_fw_counts(clients_dir):
543 """Return peer_name:fw_count pairs from iptables and client configs"""
544 import glob, subprocess
545
546 # Get iptables output once
547 try:
548 result = subprocess.run(
549 ['iptables', '-L', 'FORWARD', '-n'],
550 capture_output=True, text=True
551 )
552 fw_output = result.stdout
553 except:
554 fw_output = ""
555
556 # Build ip->name and count rules
557 for conf in glob.glob(f"{clients_dir}/*.conf"):
558 name = os.path.basename(conf).replace('.conf', '')
559 try:
560 with open(conf) as f:
561 for line in f:
562 if line.startswith('Address'):
563 ip = line.split('=')[1].strip().split('/')[0]
564 count = fw_output.count(ip)
565 print(f"{name}:{count}")
566 break
567 except:
568 pass
569
570def peer_group_map(groups_dir):
571 """Return peer:group pairs for all groups"""
572 import glob
573 try:
574 for group_file in glob.glob(f"{groups_dir}/*.group"):
575 try:
576 with open(group_file) as f:
577 g = json.load(f)
578 name = g.get('name', '')
579 for peer in g.get('peers', []):
580 if peer:
581 print(f"{peer}:{name}")
582 except:
583 pass
584 except:
585 pass
586
587def peer_groups(groups_dir, peer_name):
588 """Find all groups containing a peer"""
589 import glob
590 try:
591 for group_file in glob.glob(f"{groups_dir}/*.group"):
592 try:
593 with open(group_file) as f:
594 g = json.load(f)
595 if peer_name in g.get('peers', []):
596 print(g.get('name', ''))
597 except:
598 pass
599 except:
600 pass
601
602def peer_data(clients_dir, meta_dir, events_log):
603 import glob
604
605 meta = {}
606 for f in glob.glob(f"{meta_dir}/*.meta"):
607 name = os.path.basename(f).replace('.meta', '')
608 try:
609 with open(f) as mf:
610 meta[name] = json.load(mf)
611 except:
612 meta[name] = {}
613
614 last_events = {}
615 try:
616 with open(events_log) as f:
617 for line in f:
618 try:
619 e = json.loads(line.strip())
620 client = e.get('client', '')
621 if client:
622 last_events[client] = e
623 except:
624 pass
625 except:
626 pass
627
628 for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
629 name = os.path.basename(conf).replace('.conf', '')
630 ip = ''
631 try:
632 with open(conf) as f:
633 for line in f:
634 if line.startswith('Address'):
635 ip = line.split('=')[1].strip().split('/')[0]
636 break
637 except:
638 pass
639
640 m = meta.get(name, {})
641 rule = m.get('rule', '')
642 subtype = m.get('subtype', '')
643 main_group = m.get('main_group', '')
644
645 last_event = last_events.get(name, {})
646 last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
647 last_evt = last_event.get('event', '') # fixed: was last_event
648
649 print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}|{main_group}")
650
651def iso_to_ts(iso_str):
652 """Convert ISO timestamp to unix timestamp"""
653 try:
654 from datetime import datetime, timezone
655 dt = datetime.fromisoformat(iso_str)
656 if dt.tzinfo is None:
657 dt = dt.replace(tzinfo=timezone.utc)
658 print(int(dt.timestamp()))
659 except:
660 print(0)
661
662def rule_list_data(rules_dir, meta_dir):
663 """Return all rule data including base rules and extends"""
664 import glob
665
666 rule_peer_counts = {}
667 for f in glob.glob(f"{meta_dir}/*.meta"):
668 try:
669 with open(f) as mf:
670 meta = json.load(mf)
671 rule = meta.get('rule', '')
672 if rule:
673 rule_peer_counts[rule] = rule_peer_counts.get(rule, 0) + 1
674 except:
675 pass
676
677 rule_files = (
678 sorted(glob.glob(f"{rules_dir}/*.rule")) +
679 sorted(glob.glob(f"{rules_dir}/base/*.rule"))
680 )
681
682 # Collect all data first
683 rules_data = []
684 for rule_file in rule_files:
685 is_base = '/base/' in rule_file
686 try:
687 with open(rule_file) as f:
688 r = json.load(f)
689 name = r.get('name', '')
690 desc = r.get('desc', '')
691 group = r.get('group', '')
692 extends = ','.join(r.get('extends', []))
693 resolved = _rule_resolve_internal(rules_dir, name)
694 n_allows = len(resolved.get('allow_ips', [])) + \
695 len(resolved.get('allow_ports', []))
696 n_blocks = len(resolved.get('block_ips', [])) + \
697 len(resolved.get('block_ports', []))
698 peer_count = rule_peer_counts.get(name, 0)
699 rules_data.append({
700 'name': name, 'desc': desc, 'n_allows': n_allows,
701 'n_blocks': n_blocks, 'peer_count': peer_count,
702 'extends': extends, 'is_base': is_base, 'group': group
703 })
704 except:
705 pass
706
707 # Sort: non-base first, then by group (empty group last within non-base),
708 # then by name within group
709 rules_data.sort(key=lambda x: (
710 x['is_base'],
711 x['group'] == '' and not x['is_base'],
712 x['group'],
713 x['name']
714 ))
715
716 for r in rules_data:
717 print(f"{r['name']}|{r['desc']}|{r['n_allows']}|{r['n_blocks']}|"
718 f"{r['peer_count']}|{r['extends']}|{r['is_base']}|{r['group']}")
719
720def group_list_data(groups_dir, blocks_dir):
721 """Return group summary data in one call"""
722 import glob
723
724 # Get all block files
725 blocked_peers = set()
726 for f in glob.glob(f"{blocks_dir}/*.block"):
727 name = os.path.basename(f).replace('.block', '')
728 blocked_peers.add(name)
729
730 for group_file in sorted(glob.glob(f"{groups_dir}/*.group")):
731 try:
732 with open(group_file) as f:
733 g = json.load(f)
734 name = g.get('name', '')
735 desc = g.get('desc', '')
736 peers = [p for p in g.get('peers', []) if p]
737 total = len(peers)
738 blocked = sum(1 for p in peers if p in blocked_peers)
739 print(f"{name}|{desc}|{total}|{blocked}")
740 except:
741 pass
742
743def fmt_datetime(iso_str, fmt):
744 """Format ISO timestamp with given strftime format"""
745 try:
746 from datetime import datetime
747 dt = datetime.fromisoformat(iso_str)
748 print(dt.strftime(fmt))
749 except:
750 print(iso_str)
751
752def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips,
753 block_ports, allow_ports='', extends='', group=''):
754 rule = {
755 'name': name,
756 'desc': desc,
757 'group': group,
758 'dns_redirect': dns_redirect == 'true',
759 'extends': [x for x in extends.split(',') if x] if extends else [],
760 'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
761 'allow_ports': [x for x in allow_ports.split(',') if x] if allow_ports else [],
762 'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
763 'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
764 }
765 with open(file, 'w') as f:
766 json.dump(rule, f, indent=2)
767
768def cleanup_config(config_file):
769 """Normalize blank lines in WireGuard config"""
770 import re
771 try:
772 with open(config_file) as f:
773 config = f.read()
774 config = re.sub(r'\n{3,}', '\n\n', config)
775 config = config.rstrip('\n') + '\n'
776 with open(config_file, 'w') as f:
777 f.write(config)
778 except Exception as e:
779 print(f"Error: {e}", file=sys.stderr)
780 sys.exit(1)
781
782def remove_peer_block(config_file, name):
783 """Remove a peer block from WireGuard config by name"""
784 import re
785 try:
786 with open(config_file) as f:
787 config = f.read()
788 pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
789 result = re.sub(pattern, '\n', config)
790 with open(config_file, 'w') as f:
791 f.write(result)
792 except Exception as e:
793 print(f"Error: {e}", file=sys.stderr)
794 sys.exit(1)
795
796def create_group(file, name, desc):
797 """Create a new group JSON file"""
798 try:
799 group = {'name': name, 'desc': desc, 'peers': []}
800 with open(file, 'w') as f:
801 json.dump(group, f, indent=2)
802 except Exception as e:
803 print(f"Error: {e}", file=sys.stderr)
804 sys.exit(1)
805
806def parse_event(line):
807 """Parse a single JSON event line"""
808 try:
809 e = json.loads(line)
810 print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
811 except:
812 pass
813
814def parse_fw_event(line):
815 """Parse a single fw_events.log JSON line"""
816 try:
817 e = json.loads(line)
818 ts = e.get('timestamp', '')
819 src = e.get('src_ip', '')
820 dst = e.get('dest_ip', '')
821 port = e.get('dest_port', '')
822 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
823 proto_num = e.get('ip.protocol', 0)
824 proto = proto_map.get(proto_num, str(proto_num))
825 print(f"{ts}|{src}|{dst}|{port}|{proto}")
826 except:
827 pass
828
829def peer_transfer(wg_interface):
830 """Get total transfer bytes per peer"""
831 import subprocess
832 low = int(os.environ.get('ACTIVITY_TOTAL_LOW_BYTES', '1000000'))
833 med = int(os.environ.get('ACTIVITY_TOTAL_MED_BYTES', '10000000'))
834 high = int(os.environ.get('ACTIVITY_TOTAL_HIGH_BYTES', '100000000'))
835 try:
836 result = subprocess.run(
837 ['wg', 'show', wg_interface, 'transfer'],
838 capture_output=True, text=True
839 )
840 for line in result.stdout.strip().split('\n'):
841 if not line:
842 continue
843 parts = line.split('\t')
844 if len(parts) == 3:
845 pubkey, rx, tx = parts
846 total = int(rx) + int(tx)
847 if total == 0: level = 'none'
848 elif total < low: level = 'low'
849 elif total < med: level = 'medium'
850 elif total < high: level = 'high'
851 else: level = 'very high'
852 print(f"{pubkey}|{rx}|{tx}|{level}")
853 except:
854 pass
855
856def peer_transfer_delta(wg_interface, cache_file):
857 """Calculate current transfer rate using delta from previous sample"""
858 import subprocess, time
859
860 low = int(os.environ.get('ACTIVITY_CURRENT_LOW_BYTES', '10000')) # 10KB/s
861 med = int(os.environ.get('ACTIVITY_CURRENT_MED_BYTES', '100000')) # 100KB/s
862 high = int(os.environ.get('ACTIVITY_CURRENT_HIGH_BYTES', '1000000')) # 1MB/s
863
864 current = {}
865 now = time.time()
866 try:
867 result = subprocess.run(
868 ['wg', 'show', wg_interface, 'transfer'],
869 capture_output=True, text=True
870 )
871 for line in result.stdout.strip().split('\n'):
872 if not line:
873 continue
874 parts = line.split('\t')
875 if len(parts) == 3:
876 pubkey, rx, tx = parts
877 current[pubkey] = {'rx': int(rx), 'tx': int(tx), 'ts': now}
878 except:
879 pass
880
881 prev = {}
882 if os.path.exists(cache_file):
883 try:
884 with open(cache_file) as f:
885 prev = json.load(f)
886 except:
887 pass
888
889 try:
890 with open(cache_file, 'w') as f:
891 json.dump(current, f)
892 except:
893 pass
894
895 for pubkey, data in current.items():
896 if pubkey in prev:
897 dt = data['ts'] - prev[pubkey].get('ts', data['ts'])
898 if dt > 0:
899 rx_rate = max(0, (data['rx'] - prev[pubkey]['rx']) / dt)
900 tx_rate = max(0, (data['tx'] - prev[pubkey]['tx']) / dt)
901 total = rx_rate + tx_rate
902 if total <= 0: level = 'idle'
903 elif total < low: level = 'low'
904 elif total < med: level = 'medium'
905 elif total < high: level = 'high'
906 else: level = 'very high'
907 print(f"{pubkey}|{int(rx_rate)}|{int(tx_rate)}|{level}")
908 else:
909 print(f"{pubkey}|0|0|idle")
910 else:
911 print(f"{pubkey}|0|0|unknown")
912
913def remove_events_filtered(wg_file, fw_file, filter_name, filter_ip,
914 filter_fw, filter_wg, before_days):
915 """Remove events with filters: by name/ip, source, or age"""
916 import time
917 from datetime import datetime, timezone
918
919 cutoff_ts = None
920 if before_days:
921 cutoff_ts = time.time() - (float(before_days) * 86400)
922
923 def should_remove_wg(e):
924 if filter_name and e.get('client') != filter_name:
925 return False
926 if cutoff_ts:
927 try:
928 ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
929 return ts < cutoff_ts
930 except:
931 return False
932 return True
933
934 def should_remove_fw(e):
935 if filter_ip and e.get('src_ip') != filter_ip:
936 return False
937 if cutoff_ts:
938 try:
939 ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
940 return ts < cutoff_ts
941 except:
942 return False
943 return True
944
945 removed_wg = removed_fw = 0
946
947 if not filter_fw and os.path.exists(wg_file):
948 lines = []
949 with open(wg_file) as f:
950 for line in f:
951 try:
952 e = json.loads(line.strip())
953 if should_remove_wg(e):
954 removed_wg += 1
955 continue
956 except:
957 pass
958 lines.append(line)
959 with open(wg_file, 'w') as f:
960 f.writelines(lines)
961
962 if not filter_wg and os.path.exists(fw_file):
963 lines = []
964 with open(fw_file) as f:
965 for line in f:
966 try:
967 e = json.loads(line.strip())
968 if should_remove_fw(e):
969 removed_fw += 1
970 continue
971 except:
972 pass
973 lines.append(line)
974 with open(fw_file, 'w') as f:
975 f.writelines(lines)
976
977 print(f"{removed_wg}|{removed_fw}")
978
979def _rule_resolve_internal(rules_dir, rule_name, visited=None):
980 """Internal recursive resolver — returns dict, does not print"""
981 if visited is None:
982 visited = set()
983 if rule_name in visited:
984 raise ValueError(f"Circular dependency detected: {rule_name}")
985 visited.add(rule_name)
986
987 rule_file = find_rule_file(rules_dir, rule_name)
988 with open(rule_file) as f:
989 rule = json.load(f)
990
991 merged = {
992 'allow_ips': [],
993 'allow_ports': [],
994 'block_ips': [],
995 'block_ports': [],
996 'dns_redirect': False
997 }
998
999 for base_name in rule.get('extends', []):
1000 base = _rule_resolve_internal(rules_dir, base_name, visited.copy())
1001 merged['allow_ips'] += base.get('allow_ips', [])
1002 merged['allow_ports'] += base.get('allow_ports', [])
1003 merged['block_ips'] += base.get('block_ips', [])
1004 merged['block_ports'] += base.get('block_ports', [])
1005 if base.get('dns_redirect'):
1006 merged['dns_redirect'] = True
1007
1008 # Merge own fields — use .get() with defaults for all fields
1009 merged['allow_ips'] = list(dict.fromkeys(
1010 merged['allow_ips'] + rule.get('allow_ips', [])))
1011 merged['allow_ports'] = list(dict.fromkeys(
1012 merged['allow_ports'] + rule.get('allow_ports', [])))
1013 merged['block_ips'] = list(dict.fromkeys(
1014 merged['block_ips'] + rule.get('block_ips', [])))
1015 merged['block_ports'] = list(dict.fromkeys(
1016 merged['block_ports'] + rule.get('block_ports', [])))
1017 if rule.get('dns_redirect', False):
1018 merged['dns_redirect'] = True
1019
1020 merged['name'] = rule.get('name', rule_name)
1021 merged['desc'] = rule.get('desc', '')
1022 merged['group'] = rule.get('group', '')
1023 merged['extends'] = rule.get('extends', [])
1024 return merged
1025
1026def rule_resolve(rules_dir, rule_name):
1027 """Resolve a rule with inheritance — prints JSON"""
1028 try:
1029 resolved = _rule_resolve_internal(rules_dir, rule_name)
1030 print(json.dumps(resolved))
1031 except Exception as e:
1032 print(f"Error: {e}", file=sys.stderr)
1033 sys.exit(1)
1034
1035def rule_resolve_field(rules_dir, rule_name, field):
1036 """Get a single field from resolved rule — prints values one per line"""
1037 try:
1038 resolved = _rule_resolve_internal(rules_dir, rule_name)
1039 val = resolved.get(field, [])
1040 if isinstance(val, list):
1041 for v in val:
1042 print(v)
1043 else:
1044 print(val)
1045 except Exception as e:
1046 print(f"Error: {e}", file=sys.stderr)
1047 sys.exit(1)
1048
1049def rule_inspect(rules_dir, rule_name):
1050 """Show inheritance tree for a rule"""
1051 try:
1052 rule_file = find_rule_file(rules_dir, rule_name)
1053 with open(rule_file) as f:
1054 rule = json.load(f)
1055 resolved = _rule_resolve_internal(rules_dir, rule_name)
1056 has_extends = bool(rule.get('extends', []))
1057
1058 # Own rules
1059 for ip in rule.get('allow_ips', []):
1060 print(f"own|allow_ip|{ip}")
1061 for p in rule.get('allow_ports', []):
1062 print(f"own|allow_port|{p}")
1063 for ip in rule.get('block_ips', []):
1064 print(f"own|block_ip|{ip}")
1065 for p in rule.get('block_ports', []):
1066 print(f"own|block_port|{p}")
1067
1068 # DNS redirect — separate section
1069 if rule.get('dns_redirect'):
1070 print(f"dns|dns_redirect|true")
1071
1072 if has_extends:
1073 # Inherited rules per base
1074 for base_name in rule.get('extends', []):
1075 base = _rule_resolve_internal(rules_dir, base_name)
1076 for ip in base.get('allow_ips', []):
1077 print(f"inherited:{base_name}|allow_ip|{ip}")
1078 for p in base.get('allow_ports', []):
1079 print(f"inherited:{base_name}|allow_port|{p}")
1080 for ip in base.get('block_ips', []):
1081 print(f"inherited:{base_name}|block_ip|{ip}")
1082 for p in base.get('block_ports', []):
1083 print(f"inherited:{base_name}|block_port|{p}")
1084 if base.get('dns_redirect'):
1085 print(f"inherited:{base_name}|dns_redirect|true")
1086
1087 # Resolved summary only when inheritance exists
1088 has_resolved = (
1089 resolved.get('allow_ips') or resolved.get('allow_ports') or
1090 resolved.get('block_ips') or resolved.get('block_ports') or
1091 resolved.get('dns_redirect')
1092 )
1093 if has_resolved:
1094 for ip in resolved.get('allow_ips', []):
1095 print(f"resolved|allow_ip|{ip}")
1096 for p in resolved.get('allow_ports', []):
1097 print(f"resolved|allow_port|{p}")
1098 for ip in resolved.get('block_ips', []):
1099 print(f"resolved|block_ip|{ip}")
1100 for p in resolved.get('block_ports', []):
1101 print(f"resolved|block_port|{p}")
1102 if resolved.get('dns_redirect'):
1103 print(f"resolved|dns_redirect|true")
1104
1105 except Exception as e:
1106 print(f"Error: {e}", file=sys.stderr)
1107 sys.exit(1)
1108
1109def find_rule_file(rules_dir, rule_name):
1110 """Find rule file in rules/ or rules/base/"""
1111 for path in [
1112 os.path.join(rules_dir, f"{rule_name}.rule"),
1113 os.path.join(rules_dir, "base", f"{rule_name}.rule"),
1114 ]:
1115 if os.path.exists(path):
1116 return path
1117 return ""
1118
1119def get_raw(file, key):
1120 try:
1121 with open(file) as f:
1122 data = json.load(f)
1123 val = data.get(key) # returns None if missing
1124 if val is None:
1125 pass # print nothing
1126 elif isinstance(val, bool):
1127 print(str(val).lower())
1128 elif isinstance(val, list):
1129 for v in val:
1130 print(v)
1131 else:
1132 print(val)
1133 except:
1134 pass
1135
1136def count_resolved(rules_dir, rule_name, key):
1137 """Count entries in resolved rule field"""
1138 try:
1139 resolved = _rule_resolve_internal(rules_dir, rule_name)
1140 print(len(resolved.get(key, [])))
1141 except:
1142 print(0)
1143
1144def _block_init(peer_ip):
1145 """Return empty block structure"""
1146 return {
1147 "peer_ip": peer_ip,
1148 "blocked_direct": False,
1149 "blocked_by_groups": [],
1150 "rules": []
1151 }
1152
1153def _block_read(file):
1154 try:
1155 with open(file) as f:
1156 content = f.read().strip()
1157 if not content:
1158 return None # empty file = no block data
1159 try:
1160 return json.loads(content)
1161 except json.JSONDecodeError:
1162 # Old format — migrate
1163 lines = content.split('\n')
1164 peer_ip = lines[0].split()[0] if lines else ''
1165 new_data = {
1166 "peer_ip": peer_ip,
1167 "blocked_direct": True,
1168 "blocked_by_groups": [],
1169 "rules": [{"name": "full block", "type": "full"}]
1170 }
1171 with open(file, 'w') as f:
1172 json.dump(new_data, f, indent=2)
1173 return new_data
1174 except FileNotFoundError:
1175 return None
1176 except Exception:
1177 return None
1178
1179def _block_write(file, data):
1180 """Write block file"""
1181 with open(file, 'w') as f:
1182 json.dump(data, f, indent=2)
1183
1184def block_get(file):
1185 """Read and print block file as JSON"""
1186 data = _block_read(file)
1187 if data:
1188 print(json.dumps(data))
1189
1190def block_is_blocked(file):
1191 """Return true if peer is effectively blocked"""
1192 data = _block_read(file)
1193 if not data:
1194 print("false")
1195 return
1196 blocked = data.get("blocked_direct", False) or \
1197 bool(data.get("blocked_by_groups", []))
1198 print("true" if blocked else "false")
1199
1200def block_set_direct(file, peer_ip, value):
1201 """Set blocked_direct"""
1202 try:
1203 data = _block_read(file) or _block_init(peer_ip)
1204 data["blocked_direct"] = value.lower() == "true"
1205 data["peer_ip"] = peer_ip
1206 _block_write(file, data)
1207 remaining = data["blocked_direct"] or bool(data.get("blocked_by_groups", []))
1208 pass
1209 # print("true" if remaining else "false")
1210 except Exception as e:
1211 print(f"Error: {e}", file=sys.stderr)
1212 sys.exit(1)
1213
1214def block_add_group(file, peer_ip, group):
1215 """Add group to blocked_by_groups"""
1216 try:
1217 data = _block_read(file) or _block_init(peer_ip)
1218 data["peer_ip"] = peer_ip
1219 groups = data.get("blocked_by_groups", [])
1220 if group not in groups:
1221 groups.append(group)
1222 data["blocked_by_groups"] = groups
1223 _block_write(file, data)
1224 except Exception as e:
1225 print(f"Error: {e}", file=sys.stderr)
1226 sys.exit(1)
1227
1228def block_remove_group(file, peer_ip, group):
1229 """Remove group from blocked_by_groups, return whether still blocked"""
1230 try:
1231 data = _block_read(file) or _block_init(peer_ip)
1232 groups = data.get("blocked_by_groups", [])
1233 if group in groups:
1234 groups.remove(group)
1235 data["blocked_by_groups"] = groups
1236 _block_write(file, data)
1237 except Exception as e:
1238 print(f"Error: {e}", file=sys.stderr)
1239 sys.exit(1)
1240 # print("true" if remaining else "false")
1241 pass
1242 except Exception as e:
1243 print(f"Error: {e}", file=sys.stderr)
1244 sys.exit(1)
1245
1246def block_add_rule(file, peer_ip, rule_type, name="", target="",
1247 port="", proto=""):
1248 """Add a block rule entry"""
1249 try:
1250 data = _block_read(file) or _block_init(peer_ip)
1251 data["peer_ip"] = peer_ip
1252 rule = {"type": rule_type}
1253 if name: rule["name"] = name
1254 if target: rule["target"] = target
1255 if port: rule["port"] = port
1256 if proto: rule["proto"] = proto
1257
1258 rules = data.get("rules", [])
1259 for existing in rules:
1260 if existing.get("type") == rule_type and \
1261 existing.get("target","") == target and \
1262 existing.get("port","") == port and \
1263 existing.get("proto","") == proto:
1264 return # already exists, skip
1265
1266 rules.append(rule)
1267 data["rules"] = rules
1268 _block_write(file, data)
1269 except Exception as e:
1270 print(f"Error: {e}", file=sys.stderr)
1271 sys.exit(1)
1272
1273def block_remove_rule(file, rule_type, target="", port="", proto=""):
1274 data = _block_read(file)
1275 if not data:
1276 return
1277 rules = data.get("rules", [])
1278 filtered = [r for r in rules if not (
1279 r.get("type") == rule_type and
1280 r.get("target", "") == target and
1281 r.get("port", "") == port and
1282 r.get("proto", "") == proto
1283 )]
1284 data["rules"] = filtered
1285 _block_write(file, data)
1286
1287def block_get_rules(file):
1288 """Print rules as pipe-separated lines: name|type|target|port|proto"""
1289 data = _block_read(file)
1290 if not data:
1291 return
1292 for r in data.get("rules", []):
1293 print(f"{r.get('name','')}|{r.get('type','')}|"
1294 f"{r.get('target','')}|{r.get('port','')}|{r.get('proto','')}")
1295
1296def block_get_groups(file):
1297 data = _block_read(file)
1298 if not data:
1299 return
1300 print(','.join(data.get('blocked_by_groups', [])))
1301
1302def block_get_direct(file):
1303 data = _block_read(file)
1304 if not data:
1305 print('false')
1306 return
1307 print('true' if data.get('blocked_direct', False) else 'false')
1308
1309# ============================================
1310# Net / Services
1311# ============================================
1312
1313def _net_read(file):
1314 """Read services.json, return dict or empty dict"""
1315 try:
1316 if not os.path.exists(file):
1317 return {}
1318 with open(file) as f:
1319 content = f.read().strip()
1320 if not content:
1321 return {}
1322 return json.loads(content)
1323 except Exception:
1324 return {}
1325
1326def _net_write(file, data):
1327 """Write services.json"""
1328 os.makedirs(os.path.dirname(file), exist_ok=True)
1329 with open(file, 'w') as f:
1330 json.dump(data, f, indent=2)
1331
1332def net_list(file):
1333 """List all service names with IP and port count"""
1334 data = _net_read(file)
1335 for name, svc in sorted(data.items()):
1336 ip = svc.get('ip', '')
1337 desc = svc.get('desc', '')
1338 tags = ','.join(svc.get('tags', []))
1339 ports = len(svc.get('ports', {}))
1340 print(f"{name}|{ip}|{desc}|{tags}|{ports}")
1341
1342def net_show(file, name):
1343 """Show full service details"""
1344 data = _net_read(file)
1345 if name not in data:
1346 print(f"Error: Service not found: {name}", file=sys.stderr)
1347 sys.exit(1)
1348 svc = data[name]
1349 print(f"name|{name}")
1350 print(f"ip|{svc.get('ip','')}")
1351 print(f"desc|{svc.get('desc','')}")
1352 print(f"tags|{','.join(svc.get('tags',[]))}")
1353 for port_name, port_def in svc.get('ports', {}).items():
1354 port = port_def.get('port', '')
1355 proto = port_def.get('proto', 'tcp')
1356 desc = port_def.get('desc', '')
1357 print(f"port|{port_name}|{port}|{proto}|{desc}")
1358
1359def net_exists(file, name):
1360 """Check if service exists"""
1361 data = _net_read(file)
1362 # Handle service:port syntax
1363 if ':' in name:
1364 svc_name, port_name = name.split(':', 1)
1365 if port_name == 'ports':
1366 print('true' if svc_name in data else 'false')
1367 else:
1368 svc = data.get(svc_name, {})
1369 print('true' if port_name in svc.get('ports', {}) else 'false')
1370 else:
1371 print('true' if name in data else 'false')
1372
1373def net_add_service(file, name, ip, desc='', tags=''):
1374 """Add or update a service"""
1375 data = _net_read(file)
1376 if name not in data:
1377 data[name] = {'ip': ip, 'ports': {}}
1378 else:
1379 data[name]['ip'] = ip
1380 if desc:
1381 data[name]['desc'] = desc
1382 if tags:
1383 data[name]['tags'] = [t.strip() for t in tags.split(',') if t.strip()]
1384 _net_write(file, data)
1385
1386def net_add_port(file, service, port_name, port, proto='tcp', desc=''):
1387 """Add or update a port on a service"""
1388 data = _net_read(file)
1389 if service not in data:
1390 print(f"Error: Service not found: {service}", file=sys.stderr)
1391 sys.exit(1)
1392 if 'ports' not in data[service]:
1393 data[service]['ports'] = {}
1394 entry = {'port': int(port), 'proto': proto}
1395 if desc:
1396 entry['desc'] = desc
1397 data[service]['ports'][port_name] = entry
1398 _net_write(file, data)
1399
1400def net_remove(file, name):
1401 """Remove service or port"""
1402 data = _net_read(file)
1403 if ':' in name:
1404 svc_name, port_name = name.split(':', 1)
1405 if svc_name not in data:
1406 print(f"Error: Service not found: {svc_name}", file=sys.stderr)
1407 sys.exit(1)
1408 if port_name == 'ports':
1409 # Remove all ports
1410 data[svc_name]['ports'] = {}
1411 else:
1412 if port_name not in data[svc_name].get('ports', {}):
1413 print(f"Error: Port not found: {port_name}", file=sys.stderr)
1414 sys.exit(1)
1415 del data[svc_name]['ports'][port_name]
1416 else:
1417 if name not in data:
1418 print(f"Error: Service not found: {name}", file=sys.stderr)
1419 sys.exit(1)
1420 del data[name]
1421 _net_write(file, data)
1422
1423def net_resolve(file, name):
1424 """Resolve service name to ip or ip:port:proto lines"""
1425 data = _net_read(file)
1426 if ':' in name:
1427 svc_name, port_name = name.split(':', 1)
1428 if svc_name not in data:
1429 print(f"Error: Service not found: {svc_name}", file=sys.stderr)
1430 sys.exit(1)
1431 svc = data[svc_name]
1432 ip = svc.get('ip', '')
1433 if port_name == 'ports':
1434 # All ports
1435 for pname, pdef in svc.get('ports', {}).items():
1436 print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
1437 else:
1438 if port_name not in svc.get('ports', {}):
1439 print(f"Error: Port not found: {port_name}", file=sys.stderr)
1440 sys.exit(1)
1441 pdef = svc['ports'][port_name]
1442 print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
1443 else:
1444 if name not in data:
1445 print(f"Error: Service not found: {name}", file=sys.stderr)
1446 sys.exit(1)
1447 print(data[name].get('ip', ''))
1448
1449def net_reverse_lookup(file, ip, port='', proto=''):
1450 """Reverse lookup IP/port to service name"""
1451 data = _net_read(file)
1452 for svc_name, svc in data.items():
1453 if svc.get('ip') != ip:
1454 continue
1455 if not port:
1456 print(svc_name)
1457 return
1458 for port_name, port_def in svc.get('ports', {}).items():
1459 if (str(port_def.get('port','')) == str(port) and
1460 port_def.get('proto','tcp') == proto):
1461 print(f"{svc_name}:{port_name}")
1462 return
1463 # IP matched but no port match — return service name
1464 print(svc_name)
1465 return
1466
1467def block_is_empty(file):
1468 data = _block_read(file)
1469 if not data:
1470 print("true")
1471 return
1472 empty = (
1473 not data.get("blocked_direct", False) and
1474 not data.get("blocked_by_groups", []) and
1475 not data.get("rules", []) and
1476 not data.get("services", [])
1477 )
1478 print("true" if empty else "false")
1479
1480def group_has_peer(file, peer_name):
1481 try:
1482 with open(file) as f:
1483 data = json.load(f)
1484 peers = data.get('peers', [])
1485 print('true' if peer_name in peers else 'false')
1486 except Exception:
1487 print('false')
1488
1489commands = {
1490 'get': lambda args: get(args[0], args[1]),
1491 'set': lambda args: set_key(args[0], args[1], args[2]),
1492 'delete': lambda args: delete_key(args[0], args[1]),
1493 'append': lambda args: append(args[0], args[1], args[2]),
1494 'remove': lambda args: remove_value(args[0], args[1], args[2]),
1495 'cat': lambda args: cat(args[0]),
1496 'has_key': lambda args: has_key(args[0], args[1]),
1497 'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
1498 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
1499 'events_for': lambda args: events_for(args[0], args[1], args[2]),
1500 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
1501 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
1502 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
1503 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
1504 'remove_events': lambda args: remove_events(args[0], args[1]),
1505 'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
1506 'count': lambda args: count(args[0], args[1]),
1507 'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
1508 'peer_group_map': lambda args: peer_group_map(args[0]),
1509 'peer_groups': lambda args: peer_groups(args[0], args[1]),
1510 'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
1511 'iso_to_ts': lambda args: iso_to_ts(args[0]),
1512 'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
1513 'group_list_data': lambda args: group_list_data(args[0], args[1]),
1514 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
1515 'create_rule': lambda args: create_rule(
1516 args[0], args[1], args[2], args[3], args[4], args[5], args[6],
1517 args[7] if len(args) > 7 else '',
1518 args[8] if len(args) > 8 else '',
1519 args[9] if len(args) > 9 else ''
1520 ),
1521 'cleanup_config': lambda args: cleanup_config(args[0]),
1522 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
1523 'create_group': lambda args: create_group(args[0], args[1], args[2]),
1524 'parse_event': lambda args: parse_event(args[0]),
1525 'parse_fw_event': lambda args: parse_fw_event(args[0]),
1526 'peer_transfer': lambda args: peer_transfer(args[0]),
1527 'remove_events_filtered': lambda args: remove_events_filtered(
1528 args[0], args[1], args[2], args[3], args[4]=='true', args[5]=='true', args[6] if len(args)>6 else ''),
1529 'peer_transfer': lambda args: peer_transfer(args[0]),
1530 'peer_transfer_delta': lambda args: peer_transfer_delta(args[0], args[1]),
1531 'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
1532 'rule_resolve_field': lambda args: rule_resolve_field(args[0], args[1], args[2]),
1533 'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
1534 'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])),
1535 'get_raw': lambda args: print(get_raw(args[0], args[1])),
1536 'count_resolved': lambda args: count_resolved(args[0], args[1], args[2]),
1537 'block_get': lambda args: block_get(args[0]),
1538 'block_is_blocked': lambda args: block_is_blocked(args[0]),
1539 'block_set_direct': lambda args: block_set_direct(args[0], args[1], args[2]),
1540 'block_add_group': lambda args: block_add_group(args[0], args[1], args[2]),
1541 'block_remove_group': lambda args: block_remove_group(args[0], args[1], args[2]),
1542 'block_add_rule': lambda args: block_add_rule(
1543 args[0], args[1], args[2],
1544 args[3] if len(args) > 3 else '',
1545 args[4] if len(args) > 4 else '',
1546 args[5] if len(args) > 5 else '',
1547 args[6] if len(args) > 6 else ''
1548 ),
1549 'block_remove_rule': lambda args: block_remove_rule(
1550 args[0], args[1],
1551 args[2] if len(args) > 2 else '',
1552 args[3] if len(args) > 3 else '',
1553 args[4] if len(args) > 4 else ''
1554 ),
1555 'block_get_rules': lambda args: block_get_rules(args[0]),
1556 'block_get_groups': lambda args: block_get_groups(args[0]),
1557 'block_get_direct': lambda args: block_get_direct(args[0]),
1558 'net_list': lambda args: net_list(args[0]),
1559 'net_show': lambda args: net_show(args[0], args[1]),
1560 'net_exists': lambda args: net_exists(args[0], args[1]),
1561 'net_add_service': lambda args: net_add_service(
1562 args[0], args[1], args[2],
1563 args[3] if len(args) > 3 else '',
1564 args[4] if len(args) > 4 else ''
1565 ),
1566 'net_add_port': lambda args: net_add_port(
1567 args[0], args[1], args[2], args[3],
1568 args[4] if len(args) > 4 else 'tcp',
1569 args[5] if len(args) > 5 else ''
1570 ),
1571 'net_remove': lambda args: net_remove(args[0], args[1]),
1572 'net_resolve': lambda args: net_resolve(args[0], args[1]),
1573 'net_reverse_lookup': lambda args: net_reverse_lookup(
1574 args[0], args[1],
1575 args[2] if len(args) > 2 else '',
1576 args[3] if len(args) > 3 else ''
1577 ),
1578 'block_is_empty': lambda args: block_is_empty(args[0]),
1579 'group_has_peer': lambda args: group_has_peer(args[0], args[1]),
1580}
1581
1582if __name__ == '__main__':
1583 if len(sys.argv) < 2:
1584 print("Usage: json_helper.py <command> <file> [key] [value]", file=sys.stderr)
1585 sys.exit(1)
1586
1587 cmd = sys.argv[1]
1588 args = sys.argv[2:]
1589
1590 if cmd not in commands:
1591 print(f"Unknown command: {cmd}", file=sys.stderr)
1592 sys.exit(1)
1593
1594 commands[cmd](args)