nexus-fleet 1.0.0__tar.gz
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.
- nexus_fleet-1.0.0/PKG-INFO +80 -0
- nexus_fleet-1.0.0/README.md +63 -0
- nexus_fleet-1.0.0/nexus_agent/__init__.py +2 -0
- nexus_fleet-1.0.0/nexus_agent/__main__.py +61 -0
- nexus_fleet-1.0.0/nexus_agent/agent.py +351 -0
- nexus_fleet-1.0.0/nexus_agent/collectors.py +319 -0
- nexus_fleet-1.0.0/nexus_cli/__init__.py +1 -0
- nexus_fleet-1.0.0/nexus_cli/__main__.py +100 -0
- nexus_fleet-1.0.0/nexus_cli/admin.py +46 -0
- nexus_fleet-1.0.0/nexus_cli/menu.py +328 -0
- nexus_fleet-1.0.0/nexus_common/__init__.py +1 -0
- nexus_fleet-1.0.0/nexus_common/log.py +31 -0
- nexus_fleet-1.0.0/nexus_common/protocol.py +158 -0
- nexus_fleet-1.0.0/nexus_common/schema.py +185 -0
- nexus_fleet-1.0.0/nexus_dashboard/__init__.py +1 -0
- nexus_fleet-1.0.0/nexus_dashboard/app.js +162 -0
- nexus_fleet-1.0.0/nexus_dashboard/index.html +71 -0
- nexus_fleet-1.0.0/nexus_dashboard/server.py +35 -0
- nexus_fleet-1.0.0/nexus_dashboard/style.css +48 -0
- nexus_fleet-1.0.0/nexus_fleet.egg-info/PKG-INFO +80 -0
- nexus_fleet-1.0.0/nexus_fleet.egg-info/SOURCES.txt +29 -0
- nexus_fleet-1.0.0/nexus_fleet.egg-info/dependency_links.txt +1 -0
- nexus_fleet-1.0.0/nexus_fleet.egg-info/entry_points.txt +5 -0
- nexus_fleet-1.0.0/nexus_fleet.egg-info/top_level.txt +5 -0
- nexus_fleet-1.0.0/nexus_manager/__init__.py +2 -0
- nexus_fleet-1.0.0/nexus_manager/__main__.py +53 -0
- nexus_fleet-1.0.0/nexus_manager/rules.py +206 -0
- nexus_fleet-1.0.0/nexus_manager/server.py +837 -0
- nexus_fleet-1.0.0/nexus_manager/sigma.py +71 -0
- nexus_fleet-1.0.0/pyproject.toml +45 -0
- nexus_fleet-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexus-fleet
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Nexus Fleet — agent/manager/cli/dashboard keamanan endpoint (ala-Wazuh, ringan, stdlib-only)
|
|
5
|
+
Author: Nexus Security
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-org/nexus-fleet
|
|
8
|
+
Project-URL: Issues, https://github.com/your-org/nexus-fleet/issues
|
|
9
|
+
Keywords: security,soc,edr,agent,manager,monitoring,wazuh,fleet
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Nexus Fleet — Security Platform (agent · manager · cli · dashboard)
|
|
19
|
+
|
|
20
|
+
**1 platform, 4 komponen** ala-Wazuh, stdlib-only (Python 3.8+). Amankan **jaringan, server,
|
|
21
|
+
dan website** dari satu titik: agent ringan mengirim telemetri keamanan ke manager pusat yang
|
|
22
|
+
menjalankan **rule engine → alert** (level 0–15 + MITRE ATT&CK), lalu ditampilkan di dashboard/CLI.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
endpoint ──(HMAC/HTTP)──► nexus-manager ──► SQLite (events, alerts, audit)
|
|
26
|
+
nexus-agent FIM·SCA· rule+alert engine ▲ ▲
|
|
27
|
+
inventory·webaudit·ports policy·retention │ │
|
|
28
|
+
◄── policy/perintah ── nexus-dashboard nexus-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
**pip (disarankan):**
|
|
34
|
+
```bash
|
|
35
|
+
pip install . # dari folder ini; memasang 4 perintah:
|
|
36
|
+
# nexus-manager · nexus-agent · nexus-cli · nexus-dashboard
|
|
37
|
+
```
|
|
38
|
+
**npm (wrapper Node → Python):**
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g . # perintah sama; butuh Python 3.8+ di PATH
|
|
41
|
+
```
|
|
42
|
+
**tanpa install:** `cd python/fleet && python -m nexus_manager run` (dst.).
|
|
43
|
+
|
|
44
|
+
## Pakai
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1) Manager (server pusat + dashboard di http://host:8765/)
|
|
48
|
+
nexus-manager run --host 0.0.0.0 --port 8765
|
|
49
|
+
nexus-manager info # enrollment key + admin token
|
|
50
|
+
|
|
51
|
+
# 2) Agent di tiap endpoint
|
|
52
|
+
nexus-agent enroll --host <manager> --port 8765 --key <ENROLL_KEY> --labels prod,web
|
|
53
|
+
nexus-agent start # daemon: FIM/SCA/inventory/webaudit/ports/...
|
|
54
|
+
|
|
55
|
+
# 3) Admin / SOC
|
|
56
|
+
nexus-cli # console interaktif: menu Network & Website security
|
|
57
|
+
nexus-cli --token <ADMIN_TOKEN> alerts # alert (rule engine + MITRE + rekomendasi)
|
|
58
|
+
nexus-cli --token <ADMIN_TOKEN> ack --id <ALERT_ID> --status resolved
|
|
59
|
+
nexus-cli --token <ADMIN_TOKEN> report # report konsisten (schema nexus.report/v1)
|
|
60
|
+
nexus-dashboard --port 8080 # (opsional) host dashboard di port terpisah
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Service (jalan saat boot)
|
|
64
|
+
- **Linux:** `deploy/systemd/nexus-{manager,agent}.service` → `systemctl enable --now`.
|
|
65
|
+
- **Windows:** `deploy/windows/install-agent-service.ps1` (Scheduled Task, SYSTEM, AtStartup).
|
|
66
|
+
|
|
67
|
+
## Konsep kunci
|
|
68
|
+
- **Skema baku** (`nexus_common/schema.py`): event/alert/report seragam, condong OCSF, `origin: real|demo`.
|
|
69
|
+
- **Real findings only**: manager menolak event `demo` secara default (`accept_demo=0`).
|
|
70
|
+
- **Rule engine** (`nexus_manager/rules.py`): rule native + MITRE + rekomendasi + response; bisa di-push.
|
|
71
|
+
- **Alert engine**: dedup anti-fatigue, ack/resolve, retensi, audit log.
|
|
72
|
+
- **Keamanan**: HMAC per-agent, enrollment key, admin token; HTTP LAN (offline-first).
|
|
73
|
+
|
|
74
|
+
Uji end-to-end: `python ../tests/test_fleet.py` (21 seksi).
|
|
75
|
+
|
|
76
|
+
> Roadmap menuju standar industri penuh: agent Go/Rust, OpenSearch/Postgres, mTLS/gRPC,
|
|
77
|
+
> import Sigma, YARA, Active Response, OCSF penuh, AI remediation, RBAC multi-tenant.
|
|
78
|
+
> Pembeda: developer-first (audit Laravel/React/Next, parser log app, security posture score).
|
|
79
|
+
|
|
80
|
+
*For Personal / Ethical Hacking Study Only.*
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Nexus Fleet — Security Platform (agent · manager · cli · dashboard)
|
|
2
|
+
|
|
3
|
+
**1 platform, 4 komponen** ala-Wazuh, stdlib-only (Python 3.8+). Amankan **jaringan, server,
|
|
4
|
+
dan website** dari satu titik: agent ringan mengirim telemetri keamanan ke manager pusat yang
|
|
5
|
+
menjalankan **rule engine → alert** (level 0–15 + MITRE ATT&CK), lalu ditampilkan di dashboard/CLI.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
endpoint ──(HMAC/HTTP)──► nexus-manager ──► SQLite (events, alerts, audit)
|
|
9
|
+
nexus-agent FIM·SCA· rule+alert engine ▲ ▲
|
|
10
|
+
inventory·webaudit·ports policy·retention │ │
|
|
11
|
+
◄── policy/perintah ── nexus-dashboard nexus-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
**pip (disarankan):**
|
|
17
|
+
```bash
|
|
18
|
+
pip install . # dari folder ini; memasang 4 perintah:
|
|
19
|
+
# nexus-manager · nexus-agent · nexus-cli · nexus-dashboard
|
|
20
|
+
```
|
|
21
|
+
**npm (wrapper Node → Python):**
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g . # perintah sama; butuh Python 3.8+ di PATH
|
|
24
|
+
```
|
|
25
|
+
**tanpa install:** `cd python/fleet && python -m nexus_manager run` (dst.).
|
|
26
|
+
|
|
27
|
+
## Pakai
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1) Manager (server pusat + dashboard di http://host:8765/)
|
|
31
|
+
nexus-manager run --host 0.0.0.0 --port 8765
|
|
32
|
+
nexus-manager info # enrollment key + admin token
|
|
33
|
+
|
|
34
|
+
# 2) Agent di tiap endpoint
|
|
35
|
+
nexus-agent enroll --host <manager> --port 8765 --key <ENROLL_KEY> --labels prod,web
|
|
36
|
+
nexus-agent start # daemon: FIM/SCA/inventory/webaudit/ports/...
|
|
37
|
+
|
|
38
|
+
# 3) Admin / SOC
|
|
39
|
+
nexus-cli # console interaktif: menu Network & Website security
|
|
40
|
+
nexus-cli --token <ADMIN_TOKEN> alerts # alert (rule engine + MITRE + rekomendasi)
|
|
41
|
+
nexus-cli --token <ADMIN_TOKEN> ack --id <ALERT_ID> --status resolved
|
|
42
|
+
nexus-cli --token <ADMIN_TOKEN> report # report konsisten (schema nexus.report/v1)
|
|
43
|
+
nexus-dashboard --port 8080 # (opsional) host dashboard di port terpisah
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Service (jalan saat boot)
|
|
47
|
+
- **Linux:** `deploy/systemd/nexus-{manager,agent}.service` → `systemctl enable --now`.
|
|
48
|
+
- **Windows:** `deploy/windows/install-agent-service.ps1` (Scheduled Task, SYSTEM, AtStartup).
|
|
49
|
+
|
|
50
|
+
## Konsep kunci
|
|
51
|
+
- **Skema baku** (`nexus_common/schema.py`): event/alert/report seragam, condong OCSF, `origin: real|demo`.
|
|
52
|
+
- **Real findings only**: manager menolak event `demo` secara default (`accept_demo=0`).
|
|
53
|
+
- **Rule engine** (`nexus_manager/rules.py`): rule native + MITRE + rekomendasi + response; bisa di-push.
|
|
54
|
+
- **Alert engine**: dedup anti-fatigue, ack/resolve, retensi, audit log.
|
|
55
|
+
- **Keamanan**: HMAC per-agent, enrollment key, admin token; HTTP LAN (offline-first).
|
|
56
|
+
|
|
57
|
+
Uji end-to-end: `python ../tests/test_fleet.py` (21 seksi).
|
|
58
|
+
|
|
59
|
+
> Roadmap menuju standar industri penuh: agent Go/Rust, OpenSearch/Postgres, mTLS/gRPC,
|
|
60
|
+
> import Sigma, YARA, Active Response, OCSF penuh, AI remediation, RBAC multi-tenant.
|
|
61
|
+
> Pembeda: developer-first (audit Laravel/React/Next, parser log app, security posture score).
|
|
62
|
+
|
|
63
|
+
*For Personal / Ethical Hacking Study Only.*
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# nexus_agent/__main__.py
|
|
2
|
+
"""
|
|
3
|
+
nexus-agent — entrypoint standalone (daemon endpoint ringan).
|
|
4
|
+
|
|
5
|
+
python -m nexus_agent enroll --host <manager> --port 8765 --key <ENROLL_KEY> [--name myhost]
|
|
6
|
+
python -m nexus_agent start # jalankan daemon (blocking)
|
|
7
|
+
python -m nexus_agent status
|
|
8
|
+
python -m nexus_agent reset # lupakan enrollment
|
|
9
|
+
python -m nexus_agent collect # sekali jalan: cetak telemetri (tanpa kirim)
|
|
10
|
+
|
|
11
|
+
Env:
|
|
12
|
+
NEXUS_AGENT_DB path file state SQLite (default ./fleet_agent.db)
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
20
|
+
|
|
21
|
+
from nexus_agent import agent # noqa: E402
|
|
22
|
+
from nexus_common import protocol as fc # noqa: E402
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv=None):
|
|
26
|
+
p = argparse.ArgumentParser(prog="nexus-agent",
|
|
27
|
+
description="Daemon endpoint Nexus Fleet (ala-Wazuh agent).")
|
|
28
|
+
sub = p.add_subparsers(dest="action", required=True)
|
|
29
|
+
e = sub.add_parser("enroll", help="daftar ke manager")
|
|
30
|
+
e.add_argument("--host", required=True)
|
|
31
|
+
e.add_argument("--port", default=str(fc.DEFAULT_MANAGER_PORT))
|
|
32
|
+
e.add_argument("--key", required=True, help="enrollment key dari manager")
|
|
33
|
+
e.add_argument("--name", default="")
|
|
34
|
+
e.add_argument("--labels", default="", help="label/grup, dipisah koma (mis. prod,web)")
|
|
35
|
+
sub.add_parser("start", help="jalankan daemon (blocking)")
|
|
36
|
+
sub.add_parser("status")
|
|
37
|
+
sub.add_parser("reset")
|
|
38
|
+
sub.add_parser("collect", help="kumpulkan telemetri sekali & cetak")
|
|
39
|
+
args = p.parse_args(argv)
|
|
40
|
+
|
|
41
|
+
if args.action == "enroll":
|
|
42
|
+
print(json.dumps(agent.enroll(args.host, args.port, args.key, args.name,
|
|
43
|
+
args.labels), indent=2))
|
|
44
|
+
return 0
|
|
45
|
+
if args.action == "start":
|
|
46
|
+
agent.run_foreground()
|
|
47
|
+
return 0
|
|
48
|
+
if args.action == "status":
|
|
49
|
+
print(json.dumps(agent.status(), indent=2))
|
|
50
|
+
return 0
|
|
51
|
+
if args.action == "reset":
|
|
52
|
+
print(json.dumps(agent.reset(), indent=2))
|
|
53
|
+
return 0
|
|
54
|
+
if args.action == "collect":
|
|
55
|
+
print(json.dumps(agent.collect_all(), indent=2, ensure_ascii=False))
|
|
56
|
+
return 0
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# nexus_agent/agent.py
|
|
2
|
+
"""
|
|
3
|
+
Daemon endpoint Nexus Fleet: enroll, heartbeat, kumpulkan telemetri, kirim ke
|
|
4
|
+
manager dgn antrian store-and-forward, terapkan policy & perintah.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sqlite3
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from nexus_common import protocol as fc
|
|
13
|
+
from nexus_common.log import log
|
|
14
|
+
from nexus_agent import collectors
|
|
15
|
+
|
|
16
|
+
_RUN = True
|
|
17
|
+
_SEV_RANK = {s: i for i, s in enumerate(fc.SEVERITIES)}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------- state
|
|
21
|
+
def _conn():
|
|
22
|
+
c = sqlite3.connect(fc.agent_state_path(), timeout=10)
|
|
23
|
+
c.row_factory = sqlite3.Row
|
|
24
|
+
return c
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _init():
|
|
28
|
+
c = _conn()
|
|
29
|
+
c.executescript(
|
|
30
|
+
"""
|
|
31
|
+
CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS queue (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, payload TEXT
|
|
34
|
+
);
|
|
35
|
+
"""
|
|
36
|
+
)
|
|
37
|
+
# Migrasi dari skema lama (kolom terpisah) -> simpan event utuh sebagai JSON,
|
|
38
|
+
# agar field kaya (event_type/target/evidence/origin) tidak hilang saat dikirim.
|
|
39
|
+
cols = {r[1] for r in c.execute("PRAGMA table_info(queue)").fetchall()}
|
|
40
|
+
if "payload" not in cols:
|
|
41
|
+
c.execute("DROP TABLE IF EXISTS queue")
|
|
42
|
+
c.execute("CREATE TABLE queue (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
43
|
+
"ts INTEGER, payload TEXT)")
|
|
44
|
+
c.commit(); c.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get(key, default=None):
|
|
48
|
+
c = _conn()
|
|
49
|
+
row = c.execute("SELECT value FROM state WHERE key=?", (key,)).fetchone()
|
|
50
|
+
c.close()
|
|
51
|
+
return row["value"] if row else default
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _set(key, value):
|
|
55
|
+
c = _conn()
|
|
56
|
+
c.execute("INSERT INTO state(key,value) VALUES(?,?) "
|
|
57
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, str(value)))
|
|
58
|
+
c.commit(); c.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _policy():
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(_get("policy") or "{}") or {}
|
|
64
|
+
except Exception:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _enqueue(events):
|
|
69
|
+
if not events:
|
|
70
|
+
return
|
|
71
|
+
c = _conn()
|
|
72
|
+
for e in events:
|
|
73
|
+
# Simpan event UTUH (semua field schema) sebagai JSON.
|
|
74
|
+
c.execute("INSERT INTO queue(ts,payload) VALUES(?,?)",
|
|
75
|
+
(int(e.get("ts", fc.now())), json.dumps(e)))
|
|
76
|
+
c.commit(); c.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _queue_size():
|
|
80
|
+
c = _conn()
|
|
81
|
+
n = c.execute("SELECT COUNT(*) n FROM queue").fetchone()["n"]
|
|
82
|
+
c.close()
|
|
83
|
+
return n
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------------------- collect
|
|
87
|
+
def collect_all():
|
|
88
|
+
pol = _policy()
|
|
89
|
+
enabled = pol.get("collectors", collectors.NAMES)
|
|
90
|
+
min_sev = _SEV_RANK.get(pol.get("min_report_severity", "info"), 0)
|
|
91
|
+
out = []
|
|
92
|
+
for name in enabled:
|
|
93
|
+
try:
|
|
94
|
+
events = _fim_collect(pol) if name == "fim" else None
|
|
95
|
+
if events is None:
|
|
96
|
+
fn = collectors.REGISTRY.get(name)
|
|
97
|
+
if not fn:
|
|
98
|
+
continue
|
|
99
|
+
events = fn(pol)
|
|
100
|
+
for e in events:
|
|
101
|
+
if _SEV_RANK.get(e.get("severity", "info"), 0) >= min_sev:
|
|
102
|
+
e.setdefault("ts", fc.now())
|
|
103
|
+
e.setdefault("source", e.get("type", name))
|
|
104
|
+
e["origin"] = "real" # item #4: telemetri agent SELALU real
|
|
105
|
+
out.append(e)
|
|
106
|
+
except Exception as ex:
|
|
107
|
+
log(f"[AGENT] collector {name} error: {ex}")
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# --------------------------------------------------------------------------- FIM (item #1)
|
|
112
|
+
def _sha256(path):
|
|
113
|
+
h = hashlib.sha256()
|
|
114
|
+
with open(path, "rb") as f:
|
|
115
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
116
|
+
h.update(chunk)
|
|
117
|
+
return h.hexdigest()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _fim_files(paths):
|
|
121
|
+
files = []
|
|
122
|
+
for p in paths:
|
|
123
|
+
if os.path.isfile(p):
|
|
124
|
+
files.append(p)
|
|
125
|
+
elif os.path.isdir(p):
|
|
126
|
+
for root, _dirs, names in os.walk(p):
|
|
127
|
+
for n in names:
|
|
128
|
+
files.append(os.path.join(root, n))
|
|
129
|
+
if len(files) > 2000: # batasi beban
|
|
130
|
+
return files
|
|
131
|
+
return files
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _fim_collect(pol):
|
|
135
|
+
"""File Integrity Monitoring: baseline checksum -> alert saat dibuat/diubah/dihapus."""
|
|
136
|
+
paths = pol.get("fim_paths", []) or []
|
|
137
|
+
if not paths:
|
|
138
|
+
return []
|
|
139
|
+
try:
|
|
140
|
+
baseline = json.loads(_get("fim_baseline") or "{}")
|
|
141
|
+
except Exception:
|
|
142
|
+
baseline = {}
|
|
143
|
+
had_baseline = bool(baseline)
|
|
144
|
+
current, events = {}, []
|
|
145
|
+
for f in _fim_files(paths):
|
|
146
|
+
try:
|
|
147
|
+
current[f] = _sha256(f)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
for f, h in current.items():
|
|
151
|
+
old = baseline.get(f)
|
|
152
|
+
if old is None:
|
|
153
|
+
if had_baseline: # baru (hanya alert bila baseline sudah ada -> hindari noise run pertama)
|
|
154
|
+
events.append({"type": "fim_new", "severity": "medium", "event_type": "file_created",
|
|
155
|
+
"title": f"File baru dipantau: {os.path.basename(f)}", "detail": f,
|
|
156
|
+
"target": {"path": f}, "evidence": {"new_hash": h}})
|
|
157
|
+
elif old != h:
|
|
158
|
+
events.append({"type": "fim_change", "severity": "high", "event_type": "file_modified",
|
|
159
|
+
"title": f"File diubah: {os.path.basename(f)}", "detail": f,
|
|
160
|
+
"target": {"path": f}, "evidence": {"old_hash": old, "new_hash": h}})
|
|
161
|
+
for f, old in baseline.items():
|
|
162
|
+
if f not in current:
|
|
163
|
+
events.append({"type": "fim_deleted", "severity": "medium", "event_type": "file_deleted",
|
|
164
|
+
"title": f"File dihapus: {os.path.basename(f)}", "detail": f,
|
|
165
|
+
"target": {"path": f}, "evidence": {"old_hash": old}})
|
|
166
|
+
_set("fim_baseline", json.dumps(current))
|
|
167
|
+
return events
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --------------------------------------------------------------------------- networking
|
|
171
|
+
def _murl(path):
|
|
172
|
+
return fc.manager_url(_get("manager_host", fc.DEFAULT_MANAGER_HOST),
|
|
173
|
+
_get("manager_port", fc.DEFAULT_MANAGER_PORT), path)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def enroll(manager_host, manager_port, enroll_key, name="", labels=None):
|
|
177
|
+
_init()
|
|
178
|
+
fp = fc.host_fingerprint()
|
|
179
|
+
if isinstance(labels, str):
|
|
180
|
+
labels = [x.strip() for x in labels.split(",") if x.strip()]
|
|
181
|
+
body = {"name": name or fp["hostname"], "fingerprint": fp, "ip": fc.local_ip(),
|
|
182
|
+
"labels": labels or []}
|
|
183
|
+
try:
|
|
184
|
+
resp = fc.post_enroll(fc.manager_url(manager_host, manager_port, "/enroll"),
|
|
185
|
+
body, enroll_key)
|
|
186
|
+
except fc.HttpError as e:
|
|
187
|
+
return {"module": "fleet_agent", "ok": False, "error": f"{e.status}: {e.body}"}
|
|
188
|
+
except Exception as e:
|
|
189
|
+
return {"module": "fleet_agent", "ok": False, "error": str(e)}
|
|
190
|
+
_set("manager_host", manager_host)
|
|
191
|
+
_set("manager_port", str(manager_port))
|
|
192
|
+
_set("agent_id", resp["agent_id"])
|
|
193
|
+
_set("agent_key", resp["agent_key"])
|
|
194
|
+
_set("name", body["name"])
|
|
195
|
+
_set("policy", json.dumps(resp.get("policy", {})))
|
|
196
|
+
_set("policy_version", str(resp.get("policy_version", 1)))
|
|
197
|
+
log(f"[AGENT] Terdaftar sebagai {resp['agent_id']} di {manager_host}:{manager_port}")
|
|
198
|
+
return {"module": "fleet_agent", "ok": True, "agent_id": resp["agent_id"],
|
|
199
|
+
"manager": f"{manager_host}:{manager_port}"}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _flush_queue():
|
|
203
|
+
aid, akey = _get("agent_id"), _get("agent_key")
|
|
204
|
+
if not aid:
|
|
205
|
+
return 0
|
|
206
|
+
c = _conn()
|
|
207
|
+
rows = c.execute("SELECT * FROM queue ORDER BY id ASC LIMIT 200").fetchall()
|
|
208
|
+
c.close()
|
|
209
|
+
if not rows:
|
|
210
|
+
return 0
|
|
211
|
+
events = []
|
|
212
|
+
for r in rows:
|
|
213
|
+
try:
|
|
214
|
+
events.append(json.loads(r["payload"]))
|
|
215
|
+
except Exception:
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
fc.post_signed(_murl("/events"), {"events": events}, aid, akey)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
log(f"[AGENT] Manager tak terjangkau, {len(events)} event tetap diantri ({e})")
|
|
221
|
+
return 0
|
|
222
|
+
ids = [r["id"] for r in rows]
|
|
223
|
+
c = _conn()
|
|
224
|
+
c.execute(f"DELETE FROM queue WHERE id IN ({','.join('?' * len(ids))})", ids)
|
|
225
|
+
c.commit(); c.close()
|
|
226
|
+
return len(ids)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _active_response(args):
|
|
230
|
+
"""Active Response (item Active Response). DEFAULT DRY-RUN untuk keamanan/etika:
|
|
231
|
+
hanya jalankan blokir nyata bila policy `active_response` diaktifkan."""
|
|
232
|
+
import subprocess
|
|
233
|
+
action = args.get("action", "")
|
|
234
|
+
ip = args.get("ip", "") or args.get("target", "")
|
|
235
|
+
enabled = str(_policy().get("active_response", "")).lower() in ("1", "true", "yes")
|
|
236
|
+
executed = False
|
|
237
|
+
if action == "block_ip" and ip:
|
|
238
|
+
if enabled:
|
|
239
|
+
try:
|
|
240
|
+
if __import__("platform").system() == "Windows":
|
|
241
|
+
cmd = ["netsh", "advfirewall", "firewall", "add", "rule",
|
|
242
|
+
f"name=NexusBlock-{ip}", "dir=in", "action=block", f"remoteip={ip}"]
|
|
243
|
+
else:
|
|
244
|
+
cmd = ["iptables", "-A", "INPUT", "-s", ip, "-j", "DROP"]
|
|
245
|
+
subprocess.run(cmd, capture_output=True, timeout=10)
|
|
246
|
+
executed = True
|
|
247
|
+
log(f"[AGENT] Active Response: IP {ip} DIBLOKIR di firewall.")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
log(f"[AGENT] Active Response gagal: {e}")
|
|
250
|
+
else:
|
|
251
|
+
log(f"[AGENT] (DRY-RUN) block_ip {ip} — aktifkan policy.active_response utk eksekusi.")
|
|
252
|
+
else:
|
|
253
|
+
log(f"[AGENT] Active Response: aksi '{action}' tidak dikenal/lengkap.")
|
|
254
|
+
_enqueue([{"type": "response", "severity": "medium", "event_type": "active_response",
|
|
255
|
+
"title": f"Active Response: {action} {ip} ({'executed' if executed else 'dry-run'})",
|
|
256
|
+
"detail": f"action={action} ip={ip} executed={executed}",
|
|
257
|
+
"target": {"ip": ip}, "data": {"action": action, "executed": executed},
|
|
258
|
+
"origin": "real", "source": "response"}])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _handle_commands(cmds):
|
|
262
|
+
for cmd in cmds:
|
|
263
|
+
name = cmd.get("command")
|
|
264
|
+
if name == "collect_now":
|
|
265
|
+
_enqueue(collect_all())
|
|
266
|
+
log("[AGENT] Perintah collect_now dijalankan.")
|
|
267
|
+
elif name == "respond":
|
|
268
|
+
_active_response(cmd.get("args") or {})
|
|
269
|
+
elif name == "ping":
|
|
270
|
+
log("[AGENT] Perintah ping diterima dari manager.")
|
|
271
|
+
elif name == "set_name":
|
|
272
|
+
nm = (cmd.get("args") or {}).get("name")
|
|
273
|
+
if nm:
|
|
274
|
+
_set("name", nm)
|
|
275
|
+
log(f"[AGENT] Nama agent diubah -> {nm}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _heartbeat():
|
|
279
|
+
aid, akey = _get("agent_id"), _get("agent_key")
|
|
280
|
+
try:
|
|
281
|
+
resp = fc.post_signed(_murl("/heartbeat"), {"ip": fc.local_ip()}, aid, akey)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
log(f"[AGENT] Heartbeat gagal (manager offline?): {e}")
|
|
284
|
+
return
|
|
285
|
+
if resp.get("policy_version", 0) != int(_get("policy_version", "0") or 0):
|
|
286
|
+
try:
|
|
287
|
+
pol = fc.get_admin(_murl("/policy"))
|
|
288
|
+
_set("policy", json.dumps(pol.get("policy", {})))
|
|
289
|
+
_set("policy_version", str(pol.get("policy_version", 1)))
|
|
290
|
+
log(f"[AGENT] Policy diperbarui -> versi {pol.get('policy_version')}")
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
_handle_commands(resp.get("commands", []))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def run_foreground(**kwargs):
|
|
297
|
+
global _RUN
|
|
298
|
+
_init()
|
|
299
|
+
if not _get("agent_id"):
|
|
300
|
+
log("[AGENT] Belum ter-enroll. Jalankan enrollment dulu.")
|
|
301
|
+
return {"module": "fleet_agent", "status": "error", "error": "not_enrolled"}
|
|
302
|
+
_RUN = True
|
|
303
|
+
log(f"[AGENT] Daemon mulai: {_get('agent_id')} -> "
|
|
304
|
+
f"{_get('manager_host')}:{_get('manager_port')}")
|
|
305
|
+
last_hb = last_collect = 0
|
|
306
|
+
_enqueue(collect_all())
|
|
307
|
+
try:
|
|
308
|
+
while _RUN:
|
|
309
|
+
pol = _policy()
|
|
310
|
+
hb_iv = int(pol.get("heartbeat_interval", fc.HEARTBEAT_INTERVAL))
|
|
311
|
+
col_iv = int(pol.get("collect_interval", fc.COLLECT_INTERVAL))
|
|
312
|
+
t = fc.now()
|
|
313
|
+
if t - last_hb >= hb_iv:
|
|
314
|
+
_heartbeat(); last_hb = t
|
|
315
|
+
if t - last_collect >= col_iv:
|
|
316
|
+
evts = collect_all(); _enqueue(evts)
|
|
317
|
+
log(f"[AGENT] Telemetri terkumpul: {len(evts)} event (antrian={_queue_size()})")
|
|
318
|
+
last_collect = t
|
|
319
|
+
sent = _flush_queue()
|
|
320
|
+
if sent:
|
|
321
|
+
log(f"[AGENT] {sent} event terkirim ke manager.")
|
|
322
|
+
time.sleep(2)
|
|
323
|
+
except KeyboardInterrupt:
|
|
324
|
+
pass
|
|
325
|
+
log("[AGENT] Daemon berhenti.")
|
|
326
|
+
return {"module": "fleet_agent", "status": "stopped"}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def stop():
|
|
330
|
+
global _RUN
|
|
331
|
+
_RUN = False
|
|
332
|
+
return {"module": "fleet_agent", "status": "stopped"}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def status():
|
|
336
|
+
_init()
|
|
337
|
+
return {"module": "fleet_agent", "enrolled": bool(_get("agent_id")),
|
|
338
|
+
"agent_id": _get("agent_id", ""), "name": _get("name", ""),
|
|
339
|
+
"manager_host": _get("manager_host", ""), "manager_port": _get("manager_port", ""),
|
|
340
|
+
"policy_version": int(_get("policy_version", "0") or 0),
|
|
341
|
+
"queue_size": _queue_size(), "collectors": collectors.NAMES}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def reset():
|
|
345
|
+
import os
|
|
346
|
+
try:
|
|
347
|
+
if os.path.exists(fc.agent_state_path()):
|
|
348
|
+
os.remove(fc.agent_state_path())
|
|
349
|
+
except Exception as e:
|
|
350
|
+
return {"module": "fleet_agent", "ok": False, "error": str(e)}
|
|
351
|
+
return {"module": "fleet_agent", "ok": True}
|