@matthesketh/fleet 1.1.0 → 1.6.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/README.md +183 -251
- package/dist/adapters/detector/index.d.ts +8 -0
- package/dist/adapters/detector/index.js +54 -0
- package/dist/adapters/notifier/index.d.ts +2 -0
- package/dist/adapters/notifier/index.js +2 -0
- package/dist/adapters/notifier/stdout.d.ts +2 -0
- package/dist/adapters/notifier/stdout.js +8 -0
- package/dist/adapters/notifier/webhook.d.ts +9 -0
- package/dist/adapters/notifier/webhook.js +38 -0
- package/dist/adapters/runner/claude-cli.d.ts +7 -0
- package/dist/adapters/runner/claude-cli.js +231 -0
- package/dist/adapters/runner/mcp-call.d.ts +8 -0
- package/dist/adapters/runner/mcp-call.js +82 -0
- package/dist/adapters/runner/shell.d.ts +2 -0
- package/dist/adapters/runner/shell.js +103 -0
- package/dist/adapters/scheduler/systemd-timer.d.ts +17 -0
- package/dist/adapters/scheduler/systemd-timer.js +149 -0
- package/dist/adapters/signals/ci-status.d.ts +2 -0
- package/dist/adapters/signals/ci-status.js +79 -0
- package/dist/adapters/signals/container-up.d.ts +5 -0
- package/dist/adapters/signals/container-up.js +54 -0
- package/dist/adapters/signals/git-clean.d.ts +2 -0
- package/dist/adapters/signals/git-clean.js +55 -0
- package/dist/adapters/signals/index.d.ts +6 -0
- package/dist/adapters/signals/index.js +7 -0
- package/dist/adapters/types.d.ts +52 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli.js +43 -2
- package/dist/commands/add.js +0 -6
- package/dist/commands/boot-start.d.ts +1 -0
- package/dist/commands/boot-start.js +51 -0
- package/dist/commands/deploy.js +13 -0
- package/dist/commands/deps.js +5 -0
- package/dist/commands/egress.d.ts +1 -0
- package/dist/commands/egress.js +106 -0
- package/dist/commands/freeze.d.ts +4 -0
- package/dist/commands/freeze.js +64 -0
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.js +237 -8
- package/dist/commands/patch-systemd.d.ts +1 -0
- package/dist/commands/patch-systemd.js +126 -0
- package/dist/commands/rollback.d.ts +1 -0
- package/dist/commands/rollback.js +58 -0
- package/dist/commands/routine-run.d.ts +1 -0
- package/dist/commands/routine-run.js +122 -0
- package/dist/commands/routines.d.ts +1 -0
- package/dist/commands/routines.js +25 -0
- package/dist/commands/secrets.js +449 -16
- package/dist/commands/status.js +7 -3
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +16 -40
- package/dist/core/boot-refresh.d.ts +57 -0
- package/dist/core/boot-refresh.js +116 -0
- package/dist/core/deps/actors/pr-creator.js +11 -9
- package/dist/core/deps/collectors/docker-running.js +2 -2
- package/dist/core/deps/collectors/github-pr.js +5 -2
- package/dist/core/deps/collectors/npm.js +10 -5
- package/dist/core/deps/collectors/vulnerability.js +10 -6
- package/dist/core/deps/reporters/motd.js +1 -1
- package/dist/core/deps/reporters/telegram.js +2 -29
- package/dist/core/docker.js +45 -15
- package/dist/core/egress.d.ts +41 -0
- package/dist/core/egress.js +161 -0
- package/dist/core/exec.d.ts +7 -1
- package/dist/core/exec.js +25 -17
- package/dist/core/git.d.ts +1 -0
- package/dist/core/git.js +36 -23
- package/dist/core/github.js +27 -8
- package/dist/core/health.d.ts +3 -0
- package/dist/core/health.js +15 -3
- package/dist/core/logs-multi.d.ts +73 -0
- package/dist/core/logs-multi.js +163 -0
- package/dist/core/logs-policy.d.ts +55 -0
- package/dist/core/logs-policy.js +148 -0
- package/dist/core/nginx.js +8 -4
- package/dist/core/notify.d.ts +15 -0
- package/dist/core/notify.js +55 -0
- package/dist/core/registry.d.ts +25 -0
- package/dist/core/registry.js +57 -10
- package/dist/core/routines/cost-queries.d.ts +24 -0
- package/dist/core/routines/cost-queries.js +65 -0
- package/dist/core/routines/db.d.ts +9 -0
- package/dist/core/routines/db.js +126 -0
- package/dist/core/routines/defaults.d.ts +2 -0
- package/dist/core/routines/defaults.js +72 -0
- package/dist/core/routines/engine.d.ts +59 -0
- package/dist/core/routines/engine.js +175 -0
- package/dist/core/routines/incidents.d.ts +13 -0
- package/dist/core/routines/incidents.js +35 -0
- package/dist/core/routines/schema.d.ts +418 -0
- package/dist/core/routines/schema.js +113 -0
- package/dist/core/routines/signals-collector.d.ts +35 -0
- package/dist/core/routines/signals-collector.js +114 -0
- package/dist/core/routines/store.d.ts +316 -0
- package/dist/core/routines/store.js +99 -0
- package/dist/core/routines/test-utils.d.ts +2 -0
- package/dist/core/routines/test-utils.js +13 -0
- package/dist/core/secrets-audit.d.ts +21 -0
- package/dist/core/secrets-audit.js +60 -0
- package/dist/core/secrets-metadata.d.ts +39 -0
- package/dist/core/secrets-metadata.js +82 -0
- package/dist/core/secrets-motd.d.ts +20 -0
- package/dist/core/secrets-motd.js +72 -0
- package/dist/core/secrets-ops.d.ts +3 -1
- package/dist/core/secrets-ops.js +78 -13
- package/dist/core/secrets-providers.d.ts +50 -0
- package/dist/core/secrets-providers.js +291 -0
- package/dist/core/secrets-rotation.d.ts +52 -0
- package/dist/core/secrets-rotation.js +165 -0
- package/dist/core/secrets-snapshots.d.ts +26 -0
- package/dist/core/secrets-snapshots.js +95 -0
- package/dist/core/secrets-validate.js +2 -1
- package/dist/core/secrets.d.ts +12 -1
- package/dist/core/secrets.js +35 -24
- package/dist/core/self-update.d.ts +41 -0
- package/dist/core/self-update.js +73 -0
- package/dist/core/systemd.js +29 -12
- package/dist/core/telegram.d.ts +6 -0
- package/dist/core/telegram.js +32 -0
- package/dist/core/validate.d.ts +7 -0
- package/dist/core/validate.js +42 -0
- package/dist/index.js +0 -4
- package/dist/mcp/deps-tools.js +9 -1
- package/dist/mcp/git-tools.js +4 -4
- package/dist/mcp/server.js +193 -8
- package/dist/templates/systemd.js +3 -3
- package/dist/templates/unseal.js +5 -1
- package/dist/tui/components/Confirm.js +3 -4
- package/dist/tui/components/Header.js +37 -8
- package/dist/tui/components/KeyHint.js +14 -5
- package/dist/tui/exec-bridge.js +26 -12
- package/dist/tui/hooks/use-fleet-data.js +5 -2
- package/dist/tui/hooks/use-health.js +5 -2
- package/dist/tui/hooks/use-terminal-size.d.ts +1 -0
- package/dist/tui/hooks/use-terminal-size.js +1 -0
- package/dist/tui/router.js +133 -8
- package/dist/tui/routines/RoutinesApp.d.ts +8 -0
- package/dist/tui/routines/RoutinesApp.js +277 -0
- package/dist/tui/routines/components/AlertsPanel.d.ts +7 -0
- package/dist/tui/routines/components/AlertsPanel.js +22 -0
- package/dist/tui/routines/components/AlertsPanel.test.d.ts +1 -0
- package/dist/tui/routines/components/AlertsPanel.test.js +52 -0
- package/dist/tui/routines/components/CommandPalette.d.ts +12 -0
- package/dist/tui/routines/components/CommandPalette.js +21 -0
- package/dist/tui/routines/components/LiveRunPanel.d.ts +12 -0
- package/dist/tui/routines/components/LiveRunPanel.js +107 -0
- package/dist/tui/routines/components/RoutineForm.d.ts +8 -0
- package/dist/tui/routines/components/RoutineForm.js +254 -0
- package/dist/tui/routines/components/SignalsGrid.d.ts +13 -0
- package/dist/tui/routines/components/SignalsGrid.js +34 -0
- package/dist/tui/routines/components/SignalsGrid.test.d.ts +1 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +43 -0
- package/dist/tui/routines/format.d.ts +7 -0
- package/dist/tui/routines/format.js +51 -0
- package/dist/tui/routines/hooks/use-git-fleet.d.ts +33 -0
- package/dist/tui/routines/hooks/use-git-fleet.js +82 -0
- package/dist/tui/routines/hooks/use-logs-stream.d.ts +13 -0
- package/dist/tui/routines/hooks/use-logs-stream.js +64 -0
- package/dist/tui/routines/hooks/use-ops-fleet.d.ts +20 -0
- package/dist/tui/routines/hooks/use-ops-fleet.js +70 -0
- package/dist/tui/routines/hooks/use-repo-detail.d.ts +31 -0
- package/dist/tui/routines/hooks/use-repo-detail.js +104 -0
- package/dist/tui/routines/hooks/use-security.d.ts +33 -0
- package/dist/tui/routines/hooks/use-security.js +110 -0
- package/dist/tui/routines/hooks/use-signals.d.ts +9 -0
- package/dist/tui/routines/hooks/use-signals.js +60 -0
- package/dist/tui/routines/runtime.d.ts +20 -0
- package/dist/tui/routines/runtime.js +40 -0
- package/dist/tui/routines/tabs/CostTab.d.ts +7 -0
- package/dist/tui/routines/tabs/CostTab.js +24 -0
- package/dist/tui/routines/tabs/DashboardTab.d.ts +15 -0
- package/dist/tui/routines/tabs/DashboardTab.js +10 -0
- package/dist/tui/routines/tabs/GitTab.d.ts +6 -0
- package/dist/tui/routines/tabs/GitTab.js +39 -0
- package/dist/tui/routines/tabs/LogsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/LogsTab.js +58 -0
- package/dist/tui/routines/tabs/OpsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/OpsTab.js +34 -0
- package/dist/tui/routines/tabs/RepoDetailView.d.ts +6 -0
- package/dist/tui/routines/tabs/RepoDetailView.js +12 -0
- package/dist/tui/routines/tabs/RoutinesTab.d.ts +10 -0
- package/dist/tui/routines/tabs/RoutinesTab.js +58 -0
- package/dist/tui/routines/tabs/ScaffoldTab.d.ts +2 -0
- package/dist/tui/routines/tabs/ScaffoldTab.js +127 -0
- package/dist/tui/routines/tabs/SecurityTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SecurityTab.js +31 -0
- package/dist/tui/routines/tabs/SettingsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SettingsTab.js +61 -0
- package/dist/tui/routines/tabs/TimelineTab.d.ts +7 -0
- package/dist/tui/routines/tabs/TimelineTab.js +26 -0
- package/dist/tui/state.js +16 -1
- package/dist/tui/tests/flicker.test.d.ts +1 -0
- package/dist/tui/tests/flicker.test.js +105 -0
- package/dist/tui/tests/keyboard-integration.test.d.ts +1 -0
- package/dist/tui/tests/keyboard-integration.test.js +120 -0
- package/dist/tui/tests/test-app.d.ts +4 -0
- package/dist/tui/tests/test-app.js +79 -0
- package/dist/tui/types.d.ts +14 -1
- package/dist/tui/views/AppDetail.js +40 -26
- package/dist/tui/views/Dashboard.js +34 -9
- package/dist/tui/views/HealthView.js +42 -12
- package/dist/tui/views/LogsView.js +38 -10
- package/dist/tui/views/MultiLogsView.d.ts +2 -0
- package/dist/tui/views/MultiLogsView.js +165 -0
- package/dist/tui/views/SecretEdit.js +18 -7
- package/dist/tui/views/SecretsView.js +55 -39
- package/dist/ui/prompt.d.ts +52 -0
- package/dist/ui/prompt.js +169 -0
- package/package.json +33 -5
- package/dist/commands/motd.d.ts +0 -1
- package/dist/commands/motd.js +0 -10
- package/dist/templates/motd.d.ts +0 -1
- package/dist/templates/motd.js +0 -7
- package/dist/tui/components/AppList.d.ts +0 -12
- package/dist/tui/components/AppList.js +0 -32
- package/dist/tui/hooks/use-keyboard.d.ts +0 -1
- package/dist/tui/hooks/use-keyboard.js +0 -44
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AppEntry } from './registry.js';
|
|
2
|
+
export type PreflightResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
branch: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'not-a-git-repo' | 'no-remote' | 'detached-head' | 'dirty-tree';
|
|
8
|
+
};
|
|
9
|
+
export declare function preflight(projectRoot: string): PreflightResult;
|
|
10
|
+
export type FetchResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: 'fetch-failed';
|
|
15
|
+
detail: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function fetchOrigin(projectRoot: string, branch: string): FetchResult;
|
|
18
|
+
export type FastForwardResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
changed: boolean;
|
|
21
|
+
newHead: string;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: 'non-ff' | 'rev-parse-failed';
|
|
25
|
+
detail: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function fastForward(projectRoot: string, branch: string): FastForwardResult;
|
|
28
|
+
export type BuildResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
built: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: 'build-failed';
|
|
34
|
+
};
|
|
35
|
+
export declare function buildIfStale(app: AppEntry, currentHead: string): BuildResult;
|
|
36
|
+
export declare function recordBuiltCommit(appName: string, commit: string): void;
|
|
37
|
+
export declare const KILL_SWITCH = "/etc/fleet/no-auto-refresh";
|
|
38
|
+
export declare const DEFAULT_WALL_CLOCK_MS = 900000;
|
|
39
|
+
export type RefreshResult = {
|
|
40
|
+
kind: 'refreshed';
|
|
41
|
+
head: string;
|
|
42
|
+
built: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'no-change';
|
|
45
|
+
head: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'skipped';
|
|
48
|
+
reason: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'failed-safe';
|
|
51
|
+
step: string;
|
|
52
|
+
detail: string;
|
|
53
|
+
};
|
|
54
|
+
export interface RefreshOptions {
|
|
55
|
+
wallClockMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function refresh(app: AppEntry, opts?: RefreshOptions): Promise<RefreshResult>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isGitRepo, getGitStatus } from './git.js';
|
|
3
|
+
import { execGit } from './exec.js';
|
|
4
|
+
import { load, save } from './registry.js';
|
|
5
|
+
import { composeBuild } from './docker.js';
|
|
6
|
+
export function preflight(projectRoot) {
|
|
7
|
+
if (!isGitRepo(projectRoot))
|
|
8
|
+
return { ok: false, reason: 'not-a-git-repo' };
|
|
9
|
+
const s = getGitStatus(projectRoot);
|
|
10
|
+
if (!s.remoteName)
|
|
11
|
+
return { ok: false, reason: 'no-remote' };
|
|
12
|
+
if (!s.branch || s.branch === 'HEAD')
|
|
13
|
+
return { ok: false, reason: 'detached-head' };
|
|
14
|
+
if (!s.clean)
|
|
15
|
+
return { ok: false, reason: 'dirty-tree' };
|
|
16
|
+
return { ok: true, branch: s.branch };
|
|
17
|
+
}
|
|
18
|
+
export function fetchOrigin(projectRoot, branch) {
|
|
19
|
+
const r = execGit(['fetch', 'origin', branch], { cwd: projectRoot, timeout: 60_000 });
|
|
20
|
+
if (!r.ok)
|
|
21
|
+
return { ok: false, reason: 'fetch-failed', detail: r.stderr || `exit ${r.exitCode}` };
|
|
22
|
+
return { ok: true };
|
|
23
|
+
}
|
|
24
|
+
function revParse(projectRoot, ref) {
|
|
25
|
+
const r = execGit(['rev-parse', ref], { cwd: projectRoot, timeout: 10_000 });
|
|
26
|
+
return r.ok ? r.stdout.trim() : null;
|
|
27
|
+
}
|
|
28
|
+
export function fastForward(projectRoot, branch) {
|
|
29
|
+
const local = revParse(projectRoot, 'HEAD');
|
|
30
|
+
if (!local) {
|
|
31
|
+
return { ok: false, reason: 'rev-parse-failed', detail: 'rev-parse HEAD or origin/branch failed' };
|
|
32
|
+
}
|
|
33
|
+
const remote = revParse(projectRoot, `origin/${branch}`);
|
|
34
|
+
if (!remote) {
|
|
35
|
+
return { ok: false, reason: 'rev-parse-failed', detail: 'rev-parse HEAD or origin/branch failed' };
|
|
36
|
+
}
|
|
37
|
+
if (local === remote)
|
|
38
|
+
return { ok: true, changed: false, newHead: local };
|
|
39
|
+
const merge = execGit(['merge', '--ff-only', `origin/${branch}`], { cwd: projectRoot, timeout: 30_000 });
|
|
40
|
+
if (!merge.ok) {
|
|
41
|
+
execGit(['merge', '--abort'], { cwd: projectRoot, timeout: 10_000 });
|
|
42
|
+
return { ok: false, reason: 'non-ff', detail: merge.stderr || `exit ${merge.exitCode}` };
|
|
43
|
+
}
|
|
44
|
+
const newHead = revParse(projectRoot, 'HEAD');
|
|
45
|
+
return { ok: true, changed: true, newHead: newHead ?? remote };
|
|
46
|
+
}
|
|
47
|
+
export function buildIfStale(app, currentHead) {
|
|
48
|
+
if (app.lastBuiltCommit && app.lastBuiltCommit === currentHead) {
|
|
49
|
+
return { ok: true, built: false };
|
|
50
|
+
}
|
|
51
|
+
const ok = composeBuild(app.composePath, app.composeFile, app.name);
|
|
52
|
+
if (!ok)
|
|
53
|
+
return { ok: false, reason: 'build-failed' };
|
|
54
|
+
return { ok: true, built: true };
|
|
55
|
+
}
|
|
56
|
+
export function recordBuiltCommit(appName, commit) {
|
|
57
|
+
const reg = load();
|
|
58
|
+
const i = reg.apps.findIndex(a => a.name === appName);
|
|
59
|
+
if (i < 0)
|
|
60
|
+
return;
|
|
61
|
+
reg.apps[i] = { ...reg.apps[i], lastBuiltCommit: commit };
|
|
62
|
+
save(reg);
|
|
63
|
+
}
|
|
64
|
+
export const KILL_SWITCH = '/etc/fleet/no-auto-refresh';
|
|
65
|
+
function killSwitchPath() {
|
|
66
|
+
return process.env.FLEET_KILL_SWITCH ?? KILL_SWITCH;
|
|
67
|
+
}
|
|
68
|
+
export const DEFAULT_WALL_CLOCK_MS = 900_000;
|
|
69
|
+
async function doRefresh(app) {
|
|
70
|
+
const pre = preflight(app.composePath);
|
|
71
|
+
if (!pre.ok)
|
|
72
|
+
return { kind: 'skipped', reason: pre.reason };
|
|
73
|
+
const fetched = fetchOrigin(app.composePath, pre.branch);
|
|
74
|
+
if (!fetched.ok)
|
|
75
|
+
return { kind: 'failed-safe', step: 'fetch', detail: fetched.detail };
|
|
76
|
+
const ff = fastForward(app.composePath, pre.branch);
|
|
77
|
+
if (!ff.ok)
|
|
78
|
+
return { kind: 'failed-safe', step: 'merge', detail: ff.detail };
|
|
79
|
+
const build = buildIfStale(app, ff.newHead);
|
|
80
|
+
if (!build.ok)
|
|
81
|
+
return { kind: 'failed-safe', step: 'build', detail: build.reason };
|
|
82
|
+
if (build.built)
|
|
83
|
+
recordBuiltCommit(app.name, ff.newHead);
|
|
84
|
+
if (!ff.changed && !build.built)
|
|
85
|
+
return { kind: 'no-change', head: ff.newHead };
|
|
86
|
+
return { kind: 'refreshed', head: ff.newHead, built: build.built };
|
|
87
|
+
}
|
|
88
|
+
function isKillSwitchActive() {
|
|
89
|
+
try {
|
|
90
|
+
return existsSync(killSwitchPath());
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false; // permission error or similar — assume no kill switch, let refresh proceed
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function refresh(app, opts = {}) {
|
|
97
|
+
if (isKillSwitchActive())
|
|
98
|
+
return { kind: 'skipped', reason: 'kill-switch' };
|
|
99
|
+
const cap = opts.wallClockMs ?? DEFAULT_WALL_CLOCK_MS;
|
|
100
|
+
let timer;
|
|
101
|
+
try {
|
|
102
|
+
return await Promise.race([
|
|
103
|
+
doRefresh(app),
|
|
104
|
+
new Promise((resolve) => {
|
|
105
|
+
timer = setTimeout(() => resolve({ kind: 'failed-safe', step: 'wall-clock', detail: `exceeded ${cap}ms` }), cap);
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return { kind: 'failed-safe', step: 'exception', detail: err instanceof Error ? err.message : String(err) };
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (timer)
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { execSafe } from '../../exec.js';
|
|
4
4
|
export function generateVersionBump(finding) {
|
|
5
5
|
if (!finding.fixable || !finding.package || !finding.currentVersion || !finding.latestVersion) {
|
|
6
6
|
return null;
|
|
@@ -74,10 +74,10 @@ export function createDepsPr(app, findings, dryRun) {
|
|
|
74
74
|
if (dryRun) {
|
|
75
75
|
return { branch, bumps };
|
|
76
76
|
}
|
|
77
|
-
const sshEnv = { SSH_AUTH_SOCK:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const sshEnv = process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {};
|
|
78
|
+
execSafe('git', ['checkout', 'develop'], { cwd: app.composePath });
|
|
79
|
+
execSafe('git', ['pull'], { cwd: app.composePath, env: sshEnv });
|
|
80
|
+
execSafe('git', ['checkout', '-b', branch], { cwd: app.composePath });
|
|
81
81
|
for (const bump of bumps) {
|
|
82
82
|
const filePath = join(app.composePath, bump.file);
|
|
83
83
|
if (!existsSync(filePath))
|
|
@@ -87,17 +87,19 @@ export function createDepsPr(app, findings, dryRun) {
|
|
|
87
87
|
writeFileSync(filePath, content);
|
|
88
88
|
}
|
|
89
89
|
const files = [...new Set(bumps.map(b => b.file))];
|
|
90
|
-
|
|
90
|
+
execSafe('git', ['add', ...files], { cwd: app.composePath });
|
|
91
91
|
const commitMsg = bumps.length === 1
|
|
92
92
|
? `chore(deps): update ${fixable[0].package} from ${fixable[0].currentVersion} to ${fixable[0].latestVersion}`
|
|
93
93
|
: `chore(deps): update ${bumps.length} dependencies`;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
execSafe('git', ['commit', '-m', commitMsg], { cwd: app.composePath });
|
|
95
|
+
execSafe('git', ['push', '-u', 'origin', branch], { cwd: app.composePath, env: sshEnv });
|
|
96
96
|
if (!app.gitRepo)
|
|
97
97
|
return { branch, bumps };
|
|
98
98
|
const prBody = buildPrBody(fixable);
|
|
99
99
|
const prTitle = `chore(deps): update dependencies (${date})`;
|
|
100
|
-
const prResult =
|
|
100
|
+
const prResult = execSafe('gh', [
|
|
101
|
+
'pr', 'create', '--repo', app.gitRepo, '--title', prTitle, '--body', prBody, '--base', 'develop',
|
|
102
|
+
], { cwd: app.composePath, env: sshEnv });
|
|
101
103
|
const prUrl = prResult.ok ? prResult.stdout.trim() : undefined;
|
|
102
104
|
return { branch, bumps, prUrl };
|
|
103
105
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSafe } from '../../exec.js';
|
|
2
2
|
export class DockerRunningCollector {
|
|
3
3
|
_overrides;
|
|
4
4
|
type = 'docker-running';
|
|
@@ -11,7 +11,7 @@ export class DockerRunningCollector {
|
|
|
11
11
|
async collect(app) {
|
|
12
12
|
const findings = [];
|
|
13
13
|
for (const container of app.containers) {
|
|
14
|
-
const result =
|
|
14
|
+
const result = execSafe('docker', ['inspect', container], { timeout: 10_000 });
|
|
15
15
|
if (!result.ok)
|
|
16
16
|
continue;
|
|
17
17
|
const info = this.parseInspectOutput(result.stdout);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSafe } from '../../exec.js';
|
|
2
2
|
export class GitHubPrCollector {
|
|
3
3
|
type = 'github-pr';
|
|
4
4
|
detect(_appPath, app) {
|
|
@@ -7,7 +7,10 @@ export class GitHubPrCollector {
|
|
|
7
7
|
async collect(app) {
|
|
8
8
|
if (!app.gitRepo)
|
|
9
9
|
return [];
|
|
10
|
-
const result =
|
|
10
|
+
const result = execSafe('gh', [
|
|
11
|
+
'pr', 'list', '--repo', app.gitRepo, '--state', 'open',
|
|
12
|
+
'--json', 'number,title,url,labels', '--limit', '50',
|
|
13
|
+
], { timeout: 15_000 });
|
|
11
14
|
if (!result.ok)
|
|
12
15
|
return [];
|
|
13
16
|
try {
|
|
@@ -21,16 +21,21 @@ export class NpmCollector {
|
|
|
21
21
|
...pkg.devDependencies,
|
|
22
22
|
};
|
|
23
23
|
const findings = [];
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const entries = Object.entries(allDeps);
|
|
25
|
+
const BATCH_SIZE = 10;
|
|
26
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
27
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
28
|
+
const results = await Promise.allSettled(batch.map(([name, version]) => this.checkPackage(app.name, name, version)));
|
|
29
|
+
for (const result of results) {
|
|
30
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
31
|
+
findings.push(result.value);
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
return findings;
|
|
31
36
|
}
|
|
32
37
|
async checkPackage(appName, name, currentRaw) {
|
|
33
|
-
const current = currentRaw.replace(/^[
|
|
38
|
+
const current = currentRaw.replace(/^[^\d]*/, '');
|
|
34
39
|
try {
|
|
35
40
|
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`);
|
|
36
41
|
if (!res.ok)
|
|
@@ -13,10 +13,14 @@ export class VulnerabilityCollector {
|
|
|
13
13
|
if (packages.length === 0)
|
|
14
14
|
return [];
|
|
15
15
|
const findings = [];
|
|
16
|
-
const
|
|
17
|
-
for (
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const BATCH_SIZE = 10;
|
|
17
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
18
|
+
const batch = packages.slice(i, i + BATCH_SIZE);
|
|
19
|
+
const results = await Promise.allSettled(batch.map(pkg => this.queryOsv(app.name, pkg)));
|
|
20
|
+
for (const result of results) {
|
|
21
|
+
if (result.status === 'fulfilled') {
|
|
22
|
+
findings.push(...result.value);
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
return findings;
|
|
@@ -28,7 +32,7 @@ export class VulnerabilityCollector {
|
|
|
28
32
|
try {
|
|
29
33
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
30
34
|
for (const [name, versionRaw] of Object.entries(pkg.dependencies ?? {})) {
|
|
31
|
-
const version = versionRaw.replace(/^[
|
|
35
|
+
const version = versionRaw.replace(/^[^\d]*/, '');
|
|
32
36
|
packages.push({ name, version, ecosystem: 'npm' });
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -41,7 +45,7 @@ export class VulnerabilityCollector {
|
|
|
41
45
|
for (const [name, versionRaw] of Object.entries(composer.require ?? {})) {
|
|
42
46
|
if (name.startsWith('php') || name.startsWith('ext-'))
|
|
43
47
|
continue;
|
|
44
|
-
const version = versionRaw.replace(/^[
|
|
48
|
+
const version = versionRaw.replace(/^[^\d]*/, '');
|
|
45
49
|
packages.push({ name, version, ecosystem: 'Packagist' });
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { loadTelegramConfig, sendTelegram } from '../../telegram.js';
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
const NOTIFIED_PATH = join(__dirname, '..', '..', '..', '..', 'data', 'notified-findings.json');
|
|
6
|
-
const TELEGRAM_CONFIG_PATH = '/etc/fleet/telegram.json';
|
|
7
7
|
export function formatTelegramMessage(findings, appCount) {
|
|
8
8
|
if (findings.length === 0)
|
|
9
9
|
return '';
|
|
@@ -56,21 +56,7 @@ export async function sendTelegramNotification(findings, appCount, previousFindi
|
|
|
56
56
|
const message = formatTelegramMessage(filtered, appCount);
|
|
57
57
|
if (!message)
|
|
58
58
|
return false;
|
|
59
|
-
|
|
60
|
-
const res = await fetch(`https://api.telegram.org/bot${config.botToken}/sendMessage`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
-
body: JSON.stringify({
|
|
64
|
-
chat_id: config.chatId,
|
|
65
|
-
text: message,
|
|
66
|
-
parse_mode: 'HTML',
|
|
67
|
-
}),
|
|
68
|
-
});
|
|
69
|
-
return res.ok;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
59
|
+
return sendTelegram(config, message);
|
|
74
60
|
}
|
|
75
61
|
export function loadNotifiedFindings() {
|
|
76
62
|
if (!existsSync(NOTIFIED_PATH))
|
|
@@ -88,19 +74,6 @@ export function saveNotifiedFindings(findings) {
|
|
|
88
74
|
mkdirSync(dir, { recursive: true });
|
|
89
75
|
writeFileSync(NOTIFIED_PATH, JSON.stringify(findings, null, 2) + '\n');
|
|
90
76
|
}
|
|
91
|
-
function loadTelegramConfig() {
|
|
92
|
-
if (!existsSync(TELEGRAM_CONFIG_PATH))
|
|
93
|
-
return null;
|
|
94
|
-
try {
|
|
95
|
-
const raw = JSON.parse(readFileSync(TELEGRAM_CONFIG_PATH, 'utf-8'));
|
|
96
|
-
if (!raw.botToken || !raw.chatId)
|
|
97
|
-
return null;
|
|
98
|
-
return { botToken: raw.botToken, chatId: String(raw.chatId) };
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
77
|
function escapeHtml(text) {
|
|
105
78
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
106
79
|
}
|
package/dist/core/docker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execSafe } from './exec.js';
|
|
3
3
|
const SECRETS_BASE = '/run/fleet-secrets';
|
|
4
4
|
function loadEnvFile(path) {
|
|
5
5
|
if (!existsSync(path))
|
|
@@ -23,7 +23,9 @@ function loadEnvFile(path) {
|
|
|
23
23
|
return vars;
|
|
24
24
|
}
|
|
25
25
|
export function listContainers() {
|
|
26
|
-
const result =
|
|
26
|
+
const result = execSafe('docker', [
|
|
27
|
+
'ps', '--format', '{{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}',
|
|
28
|
+
], { timeout: 10_000 });
|
|
27
29
|
if (!result.ok || !result.stdout)
|
|
28
30
|
return [];
|
|
29
31
|
return result.stdout.split('\n').map(line => {
|
|
@@ -37,36 +39,64 @@ export function listContainers() {
|
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
export function getContainersByCompose(composePath, composeFile) {
|
|
40
|
-
const
|
|
41
|
-
const result =
|
|
42
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'ps', '--format', '{{.Names}}'];
|
|
43
|
+
const result = execSafe('docker', args, { cwd: composePath, timeout: 10_000 });
|
|
42
44
|
if (!result.ok || !result.stdout)
|
|
43
45
|
return [];
|
|
44
46
|
return result.stdout.split('\n').filter(Boolean);
|
|
45
47
|
}
|
|
46
48
|
export function getContainerLogs(container, lines = 100) {
|
|
47
|
-
const result =
|
|
48
|
-
return result.ok ? result.stdout : result.stderr || 'No logs available';
|
|
49
|
+
const result = execSafe('docker', ['logs', '--tail', String(lines), container], { timeout: 15_000 });
|
|
50
|
+
return result.ok ? (result.stdout || result.stderr) : result.stderr || 'No logs available';
|
|
51
|
+
}
|
|
52
|
+
function resolveImageName(composePath, composeFile) {
|
|
53
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'config', '--images'];
|
|
54
|
+
const r = execSafe('docker', args, { cwd: composePath, timeout: 15_000 });
|
|
55
|
+
if (!r.ok)
|
|
56
|
+
return null;
|
|
57
|
+
const first = r.stdout.split('\n').filter(Boolean)[0];
|
|
58
|
+
return first ?? null;
|
|
59
|
+
}
|
|
60
|
+
function imageExists(image) {
|
|
61
|
+
return execSafe('docker', ['image', 'inspect', image], { timeout: 10_000 }).ok;
|
|
49
62
|
}
|
|
50
63
|
export function composeBuild(composePath, composeFile, appName) {
|
|
51
|
-
const
|
|
64
|
+
const image = resolveImageName(composePath, composeFile);
|
|
65
|
+
if (image && imageExists(image)) {
|
|
66
|
+
const lastColon = image.lastIndexOf(':');
|
|
67
|
+
const base = lastColon > 0 ? image.slice(0, lastColon) : image;
|
|
68
|
+
const previous = `${base}:fleet-previous`;
|
|
69
|
+
execSafe('docker', ['tag', image, previous], { timeout: 10_000 });
|
|
70
|
+
// intentional: retag failure does not block build
|
|
71
|
+
}
|
|
72
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'build'];
|
|
52
73
|
const env = appName ? loadEnvFile(`${SECRETS_BASE}/${appName}/.env`) : {};
|
|
53
|
-
const result =
|
|
74
|
+
const result = execSafe('docker', args, {
|
|
75
|
+
cwd: composePath,
|
|
76
|
+
timeout: 300_000,
|
|
77
|
+
env: Object.keys(env).length > 0 ? env : undefined,
|
|
78
|
+
});
|
|
54
79
|
return result.ok;
|
|
55
80
|
}
|
|
56
81
|
export function composeUp(composePath, composeFile) {
|
|
57
|
-
const
|
|
58
|
-
const result =
|
|
82
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'up', '-d', '--force-recreate'];
|
|
83
|
+
const result = execSafe('docker', args, { cwd: composePath, timeout: 120_000 });
|
|
59
84
|
return result.ok;
|
|
60
85
|
}
|
|
61
86
|
export function composeDown(composePath, composeFile) {
|
|
62
|
-
const
|
|
63
|
-
const result =
|
|
87
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'down'];
|
|
88
|
+
const result = execSafe('docker', args, { cwd: composePath, timeout: 60_000 });
|
|
64
89
|
return result.ok;
|
|
65
90
|
}
|
|
66
91
|
export function inspectContainer(name) {
|
|
67
|
-
const result =
|
|
92
|
+
const result = execSafe('docker', ['inspect', name], { timeout: 10_000 });
|
|
68
93
|
if (!result.ok)
|
|
69
94
|
return null;
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(result.stdout);
|
|
97
|
+
return Array.isArray(parsed) ? parsed[0] : parsed;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
72
102
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress observation: see where each app's containers are talking to.
|
|
3
|
+
*
|
|
4
|
+
* v1 = snapshot mode. Reads conntrack via `ss -tn` filtered by container IPs,
|
|
5
|
+
* resolves remote IPs to hostnames best-effort, returns the deduplicated set.
|
|
6
|
+
*
|
|
7
|
+
* v2 (Phase E) = continuous shadow daemon (eBPF-based or nftables LOG target),
|
|
8
|
+
* with persistent observed-set storage and a real `enforce` mode that drops
|
|
9
|
+
* packets to non-allowlisted destinations. Design intentionally matches v1's
|
|
10
|
+
* data shape so the upgrade is non-breaking.
|
|
11
|
+
*/
|
|
12
|
+
import type { AppEntry } from './registry.js';
|
|
13
|
+
export interface EgressFlow {
|
|
14
|
+
/** App that owns the source container. */
|
|
15
|
+
app: string;
|
|
16
|
+
/** Container name. */
|
|
17
|
+
container: string;
|
|
18
|
+
/** Remote endpoint as host:port. Hostname resolved when possible. */
|
|
19
|
+
remote: string;
|
|
20
|
+
remoteIp: string;
|
|
21
|
+
remotePort: number;
|
|
22
|
+
/** True if remote matches the app's egress.allow list. */
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface EgressSnapshot {
|
|
26
|
+
takenAt: string;
|
|
27
|
+
app: string;
|
|
28
|
+
flows: EgressFlow[];
|
|
29
|
+
/** Distinct (host:port) destinations observed. Useful for seeding allow lists. */
|
|
30
|
+
uniqueRemotes: string[];
|
|
31
|
+
/** uniqueRemotes that aren't on the app's allow list. */
|
|
32
|
+
violations: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read all current outbound TCP/UDP flows from the host using `ss -tnp`,
|
|
36
|
+
* filter to those whose SOURCE matches one of the app's container IPs.
|
|
37
|
+
* Connections to private addresses are kept (they may indicate intra-host
|
|
38
|
+
* leaks) but flagged differently.
|
|
39
|
+
*/
|
|
40
|
+
export declare function snapshotEgress(app: AppEntry): EgressSnapshot;
|
|
41
|
+
export declare function addEgressAllow(app: AppEntry, host: string): string[];
|