@matthesketh/fleet 1.7.0 → 1.8.0
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/dist/commands/guard.js
CHANGED
|
@@ -17,6 +17,7 @@ const STATE_DIR = '/var/lib/fleet-guard';
|
|
|
17
17
|
const LOG_DIR = '/var/log/fleet-guard';
|
|
18
18
|
const SNAP_DIR = '/var/lib/cf-snapshots';
|
|
19
19
|
const CRON_TARGET = '/etc/cron.d/cf-protect';
|
|
20
|
+
const LOGROTATE_TARGET = '/etc/logrotate.d/fleet-guard';
|
|
20
21
|
function scriptsDir() {
|
|
21
22
|
// dist/commands/guard.js -> ../../scripts/guard relative to compiled file
|
|
22
23
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -77,6 +78,16 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
|
77
78
|
writeFileSync(CRON_TARGET, cron, { mode: 0o644 });
|
|
78
79
|
info(`installed cron at ${CRON_TARGET}`);
|
|
79
80
|
}
|
|
81
|
+
function installLogrotate() {
|
|
82
|
+
const src = join(scriptsDir(), 'logrotate.d-fleet-guard');
|
|
83
|
+
if (!existsSync(src)) {
|
|
84
|
+
info('logrotate template missing, skipping');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
copyFileSync(src, LOGROTATE_TARGET);
|
|
88
|
+
chmodSync(LOGROTATE_TARGET, 0o644);
|
|
89
|
+
info(`installed logrotate at ${LOGROTATE_TARGET}`);
|
|
90
|
+
}
|
|
80
91
|
function installCommand() {
|
|
81
92
|
requireRoot();
|
|
82
93
|
ensureUser();
|
|
@@ -88,6 +99,7 @@ function installCommand() {
|
|
|
88
99
|
ensureDir(SNAP_DIR, 0o700);
|
|
89
100
|
installScripts();
|
|
90
101
|
installCron();
|
|
102
|
+
installLogrotate();
|
|
91
103
|
success('fleet guard installed.');
|
|
92
104
|
info('next steps:');
|
|
93
105
|
info(' 1. seed creds at /etc/fleet/guard.cf.json (cloudflare api key + email + accountId)');
|
|
@@ -113,6 +125,9 @@ function helpText() {
|
|
|
113
125
|
' reject <token> reject a pending action',
|
|
114
126
|
' show <token> dump one record',
|
|
115
127
|
' execute run all approved actions',
|
|
128
|
+
' policy show [zone] print policy (or effective hold list for zone)',
|
|
129
|
+
' policy set <zone|default> <list> override hold list (comma-separated)',
|
|
130
|
+
' policy reset [zone] reset zone (or all) to defaults',
|
|
116
131
|
].join('\n');
|
|
117
132
|
}
|
|
118
133
|
export function guardCommand(args) {
|
|
@@ -131,7 +146,7 @@ export function guardCommand(args) {
|
|
|
131
146
|
}
|
|
132
147
|
return;
|
|
133
148
|
}
|
|
134
|
-
const passthrough = new Set(['status', 'list', 'hold', 'approve', 'reject', 'show', 'execute']);
|
|
149
|
+
const passthrough = new Set(['status', 'list', 'hold', 'approve', 'reject', 'show', 'execute', 'policy']);
|
|
135
150
|
if (passthrough.has(sub)) {
|
|
136
151
|
const code = delegate(sub, rest);
|
|
137
152
|
if (code !== 0)
|
package/package.json
CHANGED
|
@@ -34,15 +34,9 @@ NOISY_ACTIONS = {
|
|
|
34
34
|
"search",
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
"zone_delete", "zone_create", "zone_settings_change",
|
|
41
|
-
"ns_change", "transfer_in", "transfer_out",
|
|
42
|
-
"member_add", "member_remove",
|
|
43
|
-
"api_token_create", "api_token_delete",
|
|
44
|
-
"ssl_change",
|
|
45
|
-
}
|
|
37
|
+
# hold/notify decision is delegated to `fleet-guard policy effective` so
|
|
38
|
+
# admins can override per-zone via /etc/fleet/guard.policy.json. defaults
|
|
39
|
+
# match the destructive / hijack-shaped action types and live in fleet-guard.
|
|
46
40
|
|
|
47
41
|
|
|
48
42
|
def read_creds():
|
|
@@ -97,6 +91,20 @@ def classify(action_type):
|
|
|
97
91
|
return "alert"
|
|
98
92
|
|
|
99
93
|
|
|
94
|
+
def policy_decision(action, zone):
|
|
95
|
+
"""ask fleet-guard which bucket this action falls in for the given zone.
|
|
96
|
+
on any error fall back to 'notify' so we don't drop signals."""
|
|
97
|
+
try:
|
|
98
|
+
cmd = [GUARD, "policy", "effective", action]
|
|
99
|
+
if zone:
|
|
100
|
+
cmd.extend(["--zone", zone])
|
|
101
|
+
res = run(cmd, capture_output=True, text=True, timeout=10)
|
|
102
|
+
decision = (res.stdout or "").strip()
|
|
103
|
+
return decision if decision in ("hold", "notify") else "notify"
|
|
104
|
+
except Exception:
|
|
105
|
+
return "notify"
|
|
106
|
+
|
|
107
|
+
|
|
100
108
|
def notify(title, body):
|
|
101
109
|
run([NOTIFIER, title, body], check=False)
|
|
102
110
|
|
|
@@ -141,7 +149,8 @@ def main():
|
|
|
141
149
|
alerts = [ev for ev in events if classify((ev.get("action") or {}).get("type")) == "alert"]
|
|
142
150
|
for ev in alerts:
|
|
143
151
|
action = (ev.get("action") or {}).get("type") or ""
|
|
144
|
-
if
|
|
152
|
+
zone = (ev.get("metadata") or {}).get("zone") or (ev.get("resource") or {}).get("id", "") if (ev.get("resource") or {}).get("type") == "zone" else ""
|
|
153
|
+
if policy_decision(action, zone) == "hold":
|
|
145
154
|
# destructive — create a fleet-guard hold instead of just notifying.
|
|
146
155
|
# the cli itself fires the notification with approval token.
|
|
147
156
|
payload = {
|
|
@@ -38,12 +38,24 @@ ROOT = Path("/var/lib/fleet-guard")
|
|
|
38
38
|
LOG = Path("/var/log/fleet-guard/audit.jsonl")
|
|
39
39
|
NOTIFY = "/usr/local/sbin/notify"
|
|
40
40
|
GUARD_CFG = Path("/etc/fleet/guard.json")
|
|
41
|
+
POLICY_CFG = Path("/etc/fleet/guard.policy.json")
|
|
41
42
|
DEFAULT_TTL = 600
|
|
42
43
|
|
|
43
44
|
PENDING = ROOT / "pending"
|
|
44
45
|
APPROVED = ROOT / "approved"
|
|
45
46
|
PROCESSED = ROOT / "processed"
|
|
46
47
|
|
|
48
|
+
# baseline policy: which audit-log action types create a hold (require
|
|
49
|
+
# approval) vs just notify. cf-audit-monitor consults `policy effective`
|
|
50
|
+
# per-event so admins can override per-zone via /etc/fleet/guard.policy.json.
|
|
51
|
+
DEFAULT_HOLD_ACTIONS = [
|
|
52
|
+
"zone_delete", "zone_create", "zone_settings_change",
|
|
53
|
+
"ns_change", "transfer_in", "transfer_out",
|
|
54
|
+
"member_add", "member_remove",
|
|
55
|
+
"api_token_create", "api_token_delete",
|
|
56
|
+
"ssl_change",
|
|
57
|
+
]
|
|
58
|
+
|
|
47
59
|
|
|
48
60
|
def utcnow():
|
|
49
61
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -211,6 +223,78 @@ def cmd_status(args):
|
|
|
211
223
|
return 0
|
|
212
224
|
|
|
213
225
|
|
|
226
|
+
def load_policy():
|
|
227
|
+
"""returns {'default': {'hold': [...]}, 'zones': {<zone>: {'hold': [...]}}}.
|
|
228
|
+
missing file or invalid json → defaults only."""
|
|
229
|
+
if not POLICY_CFG.exists():
|
|
230
|
+
return {"default": {"hold": list(DEFAULT_HOLD_ACTIONS)}, "zones": {}}
|
|
231
|
+
try:
|
|
232
|
+
raw = json.loads(POLICY_CFG.read_text())
|
|
233
|
+
except Exception:
|
|
234
|
+
return {"default": {"hold": list(DEFAULT_HOLD_ACTIONS)}, "zones": {}}
|
|
235
|
+
raw.setdefault("default", {})
|
|
236
|
+
raw["default"].setdefault("hold", list(DEFAULT_HOLD_ACTIONS))
|
|
237
|
+
raw.setdefault("zones", {})
|
|
238
|
+
return raw
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def save_policy(policy):
|
|
242
|
+
POLICY_CFG.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
tmp = POLICY_CFG.with_suffix(".json.tmp")
|
|
244
|
+
tmp.write_text(json.dumps(policy, indent=2, sort_keys=True))
|
|
245
|
+
tmp.rename(POLICY_CFG)
|
|
246
|
+
os.chmod(POLICY_CFG, 0o640)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def effective_hold_list(policy, zone):
|
|
250
|
+
"""zone-level override wins; falls back to default."""
|
|
251
|
+
if zone:
|
|
252
|
+
z = policy.get("zones", {}).get(zone)
|
|
253
|
+
if z and "hold" in z:
|
|
254
|
+
return z["hold"]
|
|
255
|
+
return policy.get("default", {}).get("hold", DEFAULT_HOLD_ACTIONS)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def cmd_policy(args):
|
|
259
|
+
sub = args.policy_cmd
|
|
260
|
+
policy = load_policy()
|
|
261
|
+
if sub == "show":
|
|
262
|
+
if args.zone:
|
|
263
|
+
hold = effective_hold_list(policy, args.zone)
|
|
264
|
+
print(json.dumps({"zone": args.zone, "hold": hold}, indent=2))
|
|
265
|
+
else:
|
|
266
|
+
print(json.dumps(policy, indent=2, sort_keys=True))
|
|
267
|
+
return 0
|
|
268
|
+
if sub == "set":
|
|
269
|
+
actions = [a.strip() for a in args.actions.split(",") if a.strip()]
|
|
270
|
+
if args.zone == "default":
|
|
271
|
+
policy.setdefault("default", {})["hold"] = actions
|
|
272
|
+
else:
|
|
273
|
+
policy.setdefault("zones", {}).setdefault(args.zone, {})["hold"] = actions
|
|
274
|
+
save_policy(policy)
|
|
275
|
+
log_event({"event": "policy_set", "zone": args.zone, "actions": actions})
|
|
276
|
+
print(f"set {args.zone} hold={actions}")
|
|
277
|
+
return 0
|
|
278
|
+
if sub == "reset":
|
|
279
|
+
if args.zone == "default" or args.zone == "":
|
|
280
|
+
policy["default"] = {"hold": list(DEFAULT_HOLD_ACTIONS)}
|
|
281
|
+
policy["zones"] = {}
|
|
282
|
+
else:
|
|
283
|
+
policy.get("zones", {}).pop(args.zone, None)
|
|
284
|
+
save_policy(policy)
|
|
285
|
+
log_event({"event": "policy_reset", "zone": args.zone or "all"})
|
|
286
|
+
print(f"reset {args.zone or 'all'}")
|
|
287
|
+
return 0
|
|
288
|
+
if sub == "effective":
|
|
289
|
+
# used by cf-audit-monitor. prints "hold" or "notify" then exits 0.
|
|
290
|
+
hold = effective_hold_list(policy, args.zone)
|
|
291
|
+
decision = "hold" if args.action in hold else "notify"
|
|
292
|
+
print(decision)
|
|
293
|
+
return 0
|
|
294
|
+
print(f"unknown policy subcommand: {sub}", file=sys.stderr)
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
|
|
214
298
|
def cmd_execute(args):
|
|
215
299
|
"""invoke executor for each approved record. external script handles each kind."""
|
|
216
300
|
executor = "/usr/local/sbin/fleet-guard-execute"
|
|
@@ -274,6 +358,20 @@ def main():
|
|
|
274
358
|
sp = sub.add_parser("execute")
|
|
275
359
|
sp.set_defaults(fn=cmd_execute)
|
|
276
360
|
|
|
361
|
+
sp = sub.add_parser("policy", help="view/edit hold-action policy")
|
|
362
|
+
psub = sp.add_subparsers(dest="policy_cmd", required=True)
|
|
363
|
+
pshow = psub.add_parser("show", help="print policy (or effective hold list for zone)")
|
|
364
|
+
pshow.add_argument("zone", nargs="?", default="")
|
|
365
|
+
pset = psub.add_parser("set", help="set hold-action list for default or zone")
|
|
366
|
+
pset.add_argument("zone")
|
|
367
|
+
pset.add_argument("actions", help="comma-separated action types")
|
|
368
|
+
preset = psub.add_parser("reset", help="reset policy: omit zone to wipe all overrides")
|
|
369
|
+
preset.add_argument("zone", nargs="?", default="")
|
|
370
|
+
peff = psub.add_parser("effective", help="print 'hold' or 'notify' for an action+zone")
|
|
371
|
+
peff.add_argument("action")
|
|
372
|
+
peff.add_argument("--zone", default="")
|
|
373
|
+
sp.set_defaults(fn=cmd_policy)
|
|
374
|
+
|
|
277
375
|
args = p.parse_args()
|
|
278
376
|
sys.exit(args.fn(args))
|
|
279
377
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/var/log/fleet-guard/*.log
|
|
2
|
+
/var/log/cf-audit-monitor.log
|
|
3
|
+
/var/log/cf-snapshot.log
|
|
4
|
+
/var/log/dns-drift-watch.log
|
|
5
|
+
/var/log/cert-expiry-watch.log
|
|
6
|
+
/var/log/refresh-cf-firewall.log
|
|
7
|
+
{
|
|
8
|
+
daily
|
|
9
|
+
rotate 30
|
|
10
|
+
compress
|
|
11
|
+
delaycompress
|
|
12
|
+
missingok
|
|
13
|
+
notifempty
|
|
14
|
+
create 600 fleet-guard fleet-guard
|
|
15
|
+
sharedscripts
|
|
16
|
+
su root root
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/var/log/fleet-guard/audit.jsonl
|
|
20
|
+
/var/log/mcp-audit.jsonl
|
|
21
|
+
{
|
|
22
|
+
monthly
|
|
23
|
+
rotate 12
|
|
24
|
+
compress
|
|
25
|
+
delaycompress
|
|
26
|
+
missingok
|
|
27
|
+
notifempty
|
|
28
|
+
copytruncate
|
|
29
|
+
create 600 root root
|
|
30
|
+
su root root
|
|
31
|
+
}
|