wimotek-edge-agent 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,9 @@ Commercial-grade edge access control, service bridge, and tunnel agent featuring
14
14
  - [Programmatic usage](#programmatic-usage)
15
15
  - [Reverse tunnel / NAT4](#reverse-tunnel--nat4)
16
16
  - [Audit log](#audit-log)
17
+ - [Storage Mechanics & Format](#1-storage-mechanics--format)
18
+ - [Risks of Unmanaged Log Growth](#2-risks-of-unmanaged-log-growth)
19
+ - [Log Rotation & Cleanup Solutions](#3-log-rotation--cleanup-solutions)
17
20
  - [Security model](#security-model)
18
21
  - [Commercial deployment checklist](#commercial-deployment-checklist)
19
22
  - [License](#license)
@@ -241,6 +244,8 @@ npx wimotek-edge-agent --help
241
244
  | `EDGE_AGENT_SESSION_TICKET_TTL_SECONDS` | no | `300` | Session ticket TTL (seconds) |
242
245
  | `EDGE_AGENT_TOFU_HEARTBEAT_SECONDS` | no | `10` | TOFU quarantine heartbeat interval |
243
246
  | `EDGE_AGENT_REQUIRE_LOCAL_BYPASS_TOKEN` | no | `true` | Require local bypass token for loopback |
247
+ | `EDGE_AGENT_AUDIT_MAX_BYTES` | no | `10485760` (10MB) | Max audit log size before auto-rotation |
248
+ | `EDGE_AGENT_AUDIT_BACKUP_COUNT` | no | `5` | Number of rotated audit log files to keep |
244
249
  | `EDGE_AGENT_REVERSE_TUNNEL` | no | auto | Force reverse tunnel (`true`/`false`) |
245
250
  | `EDGE_AGENT_NAT_TYPE` | no | `unknown` | Network NAT type (`nat4`, `symmetric`, etc.) |
246
251
  | `EDGE_AGENT_NODE_UUID` | reverse only | - | Agent node UUID |
@@ -310,6 +315,16 @@ Read or generate the local bypass token:
310
315
  wimotek-edge-agent bypass-token
311
316
  ```
312
317
 
318
+ Clean or truncate the audit log (keep recent N lines or 0 for complete truncate):
319
+
320
+ ```bash
321
+ # Keep latest 500 lines:
322
+ wimotek-edge-agent clean-audit --keep 500
323
+
324
+ # Completely wipe audit log:
325
+ wimotek-edge-agent clean-audit --keep 0
326
+ ```
327
+
313
328
  ## Programmatic usage
314
329
 
315
330
  ```python
@@ -364,13 +379,76 @@ await agent.run_reverse_tunnel(
364
379
 
365
380
  ## Audit log
366
381
 
367
- Every `AccessDecision` is written to `EDGE_AGENT_DATA_DIR/audit.jsonl`:
382
+ ### 1. Storage Mechanics & Format
383
+
384
+ Every `AccessDecision` and administrative action is written to `$EDGE_AGENT_DATA_DIR/audit.jsonl` (default: `./edge_agent_data/audit.jsonl`).
385
+ Lines are formatted as standard **JSON Lines (JSONL)** and protected by a thread-safe mutex lock:
368
386
 
369
387
  ```json
370
388
  {"ts":"2026-11-06T12:00:00+00:00","event":"access_decision","node_uuid":"node-xxx","source_ip":"203.0.113.10","is_loopback":false,"decision":"allow_token","reason":"edge_agent_token_verified","network_profile":"direct","nat_type":"unknown","fingerprint":"sha256:...","node_status":"trusted"}
371
389
  ```
372
390
 
373
- Ship this structured log file to your central logging pipeline (Loki, ELK, CloudWatch, OpenTelemetry). Raw join tokens are never logged.
391
+ Raw join tokens are never logged.
392
+
393
+ ---
394
+
395
+ ### 2. Risks of Unmanaged Log Growth
396
+
397
+ In high-throughput edge environments (hundreds of handshakes/sec):
398
+ 1. **Disk Exhaustion (ENOSPC)**: Without rotation, `audit.jsonl` can grow to tens of gigabytes, exhausting device storage.
399
+ 2. **Database Failure**: When disk fills up, SQLite (`trust_store.sqlite3`) fails during WAL commits, causing trust state and replay tracking to crash.
400
+ 3. **Memory & I/O Spikes**: Reading massive un-rotated log files causes severe memory consumption and potential OOM kills.
401
+ 4. **Compliance & SIEM Delay**: Retaining only raw local logs without shipping violates enterprise zero-trust audit policies.
402
+
403
+ ---
404
+
405
+ ### 3. Log Rotation & Cleanup Solutions
406
+
407
+ #### A. Built-in Automatic Size Rotation (Recommended)
408
+ `AuditLogger` features built-in auto-rotation when file size reaches the threshold:
409
+ - Automatically rolls into `audit.jsonl.1` through `audit.jsonl.5`, purging the oldest file.
410
+ - Configurable via environment variables:
411
+ ```bash
412
+ # Maximum file size before rotation (default: 10485760 = 10MB)
413
+ export EDGE_AGENT_AUDIT_MAX_BYTES=10485760
414
+ # Number of rotated archives to retain (default: 5)
415
+ export EDGE_AGENT_AUDIT_BACKUP_COUNT=5
416
+ ```
417
+
418
+ #### B. CLI Manual Truncate & Clear
419
+ ```bash
420
+ # Keep latest 500 lines and safely truncate history:
421
+ wimotek-edge-agent clean-audit --keep 500
422
+
423
+ # Completely wipe audit log:
424
+ wimotek-edge-agent clean-audit --keep 0
425
+ ```
426
+
427
+ #### C. Web Console 1-Click Clean
428
+ In the Edge Agent Web Console:
429
+ - Navigate to **"安全审计 (Audit Log)"** tab.
430
+ - Click the red **"清理日志 (Clear Logs)"** button to truncate safely with confirmation.
431
+
432
+ #### D. Production Linux Logrotate Configuration
433
+ For systemd/daemon deployments, configure `/etc/logrotate.d/wimotek-edge-agent`:
434
+
435
+ ```text
436
+ /path/to/edge_agent_data/audit.jsonl {
437
+ daily
438
+ missingok
439
+ rotate 7
440
+ compress
441
+ delaycompress
442
+ notifempty
443
+ copytruncate
444
+ }
445
+ ```
446
+ *(The `copytruncate` directive allows safe truncation without interrupting running agent processes).*
447
+
448
+ #### E. SIEM Central Log Shipping
449
+ Stream JSONL lines via Vector, FluentBit, or Filebeat directly to Loki, Elasticsearch, or AWS CloudWatch.
450
+
451
+ ---
374
452
 
375
453
  ## Security model
376
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wimotek-edge-agent",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Universal Edge Reverse Proxy & Service Bridge Agent CLI Runner",
5
5
  "bin": {
6
6
  "wimotek-edge-agent": "./bin/cli.js",
package/pyproject.toml CHANGED
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "wimotek-edge-agent"
9
- version = "1.0.13"
9
+ version = "1.0.15"
10
10
  description = "Universal Edge Reverse Proxy & Service Bridge Agent"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
@@ -40,7 +40,11 @@ class EdgeAgent:
40
40
  self.config.ensure_dirs()
41
41
  self.data_handler = data_handler
42
42
 
43
- self.audit = AuditLogger(config.audit_log_path)
43
+ self.audit = AuditLogger(
44
+ config.audit_log_path,
45
+ max_bytes=config.audit_max_bytes,
46
+ backup_count=config.audit_backup_count,
47
+ )
44
48
  self.trust_store = TrustStore(config.trust_db_path)
45
49
  self.local_bypass = LocalBypassManager(
46
50
  config.local_bypass_token_path,
@@ -451,6 +455,15 @@ def build_parser() -> argparse.ArgumentParser:
451
455
  p_sim.add_argument("--reverse-tunnel", action="store_true")
452
456
  p_sim.add_argument("--nat-type", default="unknown")
453
457
 
458
+ # 13. clean-audit
459
+ p_clean = subparsers.add_parser("clean-audit", help="Truncate or clear audit log")
460
+ p_clean.add_argument(
461
+ "--keep",
462
+ type=int,
463
+ default=0,
464
+ help="Keep last N records (default 0, clears all records)",
465
+ )
466
+
454
467
  return parser
455
468
 
456
469
 
@@ -491,6 +504,16 @@ def main(argv: Optional[list[str]] = None) -> int:
491
504
  }, indent=2))
492
505
  return 0
493
506
 
507
+ if args.command == "clean-audit":
508
+ retained = agent.audit.clear(keep_last_lines=args.keep)
509
+ agent.audit.emit("audit_log_cleaned", retained_lines=retained, keep_requested=args.keep)
510
+ print(json.dumps({
511
+ "status": "ok",
512
+ "message": f"Audit log cleaned (retained {retained} lines)",
513
+ "retained_lines": retained,
514
+ }, indent=2))
515
+ return 0
516
+
494
517
  if args.command == "stats":
495
518
  stats = agent.trust_store.get_stats()
496
519
  print(json.dumps(stats, indent=2))
@@ -9,14 +9,45 @@ from typing import Any
9
9
 
10
10
  class AuditLogger:
11
11
  """
12
- 结构化审计日志。商业部署建议再接入中心端日志平台。
12
+ 结构化审计日志管理器。
13
+ 支持自动按大小轮转 (Auto-Rotation) 与安全截断清理,防止未清理导致磁盘耗尽。
13
14
  """
14
15
 
15
- def __init__(self, path: Path) -> None:
16
+ def __init__(
17
+ self,
18
+ path: Path,
19
+ max_bytes: int = 10 * 1024 * 1024,
20
+ backup_count: int = 5,
21
+ ) -> None:
16
22
  self.path = path
17
23
  self.path.parent.mkdir(parents=True, exist_ok=True)
24
+ self.max_bytes = max_bytes
25
+ self.backup_count = backup_count
18
26
  self._lock = threading.Lock()
19
27
 
28
+ def _rotate_if_needed(self) -> None:
29
+ if self.max_bytes <= 0 or self.backup_count <= 0:
30
+ return
31
+ try:
32
+ if not self.path.exists() or self.path.stat().st_size < self.max_bytes:
33
+ return
34
+
35
+ for i in range(self.backup_count - 1, 0, -1):
36
+ sfn = self.path.with_name(f"{self.path.name}.{i}")
37
+ dfn = self.path.with_name(f"{self.path.name}.{i + 1}")
38
+ if sfn.exists():
39
+ if dfn.exists():
40
+ dfn.unlink()
41
+ sfn.rename(dfn)
42
+
43
+ dfn = self.path.with_name(f"{self.path.name}.1")
44
+ if dfn.exists():
45
+ dfn.unlink()
46
+ self.path.rename(dfn)
47
+ except Exception:
48
+ # 保证主线程写日志不因归档异常而崩溃
49
+ pass
50
+
20
51
  def emit(self, event: str, **fields: Any) -> None:
21
52
  record = {
22
53
  "ts": datetime.now(timezone.utc).isoformat(),
@@ -25,5 +56,31 @@ class AuditLogger:
25
56
  }
26
57
  line = json.dumps(record, ensure_ascii=False, default=str, sort_keys=True)
27
58
  with self._lock:
59
+ self._rotate_if_needed()
28
60
  with self.path.open("a", encoding="utf-8") as f:
29
- f.write(line + "\n")
61
+ f.write(line + "\n")
62
+
63
+ def clear(self, keep_last_lines: int = 0) -> int:
64
+ """
65
+ 清理或截断审计日志。
66
+ - keep_last_lines = 0: 完全清空
67
+ - keep_last_lines > 0: 保留最近 N 行
68
+ 返回清理后保留的行数。
69
+ """
70
+ with self._lock:
71
+ if not self.path.exists():
72
+ return 0
73
+ if keep_last_lines <= 0:
74
+ self.path.write_text("", encoding="utf-8")
75
+ return 0
76
+ try:
77
+ content = self.path.read_text(encoding="utf-8")
78
+ lines = [l for l in content.splitlines() if l.strip()]
79
+ remaining = lines[-keep_last_lines:]
80
+ self.path.write_text(
81
+ "\n".join(remaining) + ("\n" if remaining else ""),
82
+ encoding="utf-8",
83
+ )
84
+ return len(remaining)
85
+ except Exception:
86
+ return 0
@@ -44,6 +44,10 @@ class Config:
44
44
  session_ticket_ttl_seconds: int = 300
45
45
  tofu_heartbeat_seconds: int = 10
46
46
 
47
+ # 审计日志自动轮转配置
48
+ audit_max_bytes: int = 10 * 1024 * 1024 # 默认 10MB
49
+ audit_backup_count: int = 5 # 默认保留 5 个归档
50
+
47
51
  # None 表示自动判断;True 表示强制反向隧道;False 表示强制直连。
48
52
  reverse_tunnel: Optional[bool] = None
49
53
  nat_type: Optional[str] = None
@@ -99,6 +103,12 @@ class Config:
99
103
  tofu_heartbeat_seconds=int(
100
104
  os.getenv("EDGE_AGENT_TOFU_HEARTBEAT_SECONDS", "10")
101
105
  ),
106
+ audit_max_bytes=int(
107
+ os.getenv("EDGE_AGENT_AUDIT_MAX_BYTES", str(10 * 1024 * 1024))
108
+ ),
109
+ audit_backup_count=int(
110
+ os.getenv("EDGE_AGENT_AUDIT_BACKUP_COUNT", "5")
111
+ ),
102
112
  reverse_tunnel=_env_opt_bool("EDGE_AGENT_REVERSE_TUNNEL"),
103
113
  nat_type=os.getenv("EDGE_AGENT_NAT_TYPE") or None,
104
114
  tls_server_name=os.getenv("EDGE_AGENT_TLS_SERVER_NAME") or None,