kubeagent 0.1.36 → 0.1.44
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 +32 -0
- package/dist/auth.js +15 -0
- package/dist/check/index.js +77 -0
- package/dist/check/run.js +114 -0
- package/dist/cli.js +25 -2
- package/dist/detector/sources.js +1 -1
- package/dist/diagnoser/index.js +37 -11
- package/dist/monitor/index.js +45 -14
- package/dist/notify/discord.js +6 -9
- package/dist/notify/ssrf-guard.js +150 -0
- package/dist/notify/teams.js +6 -9
- package/dist/notify/webhook.js +7 -9
- package/dist/orchestrator.js +2 -1
- package/dist/telemetry.js +27 -1
- package/package.json +10 -1
- package/dist/auth.d.ts +0 -29
- package/dist/cli.d.ts +0 -2
- package/dist/config.d.ts +0 -75
- package/dist/debug.d.ts +0 -10
- package/dist/detector/cache.d.ts +0 -6
- package/dist/detector/cache.test.d.ts +0 -1
- package/dist/detector/cache.test.js +0 -54
- package/dist/detector/catalog.d.ts +0 -17
- package/dist/detector/catalog.test.d.ts +0 -1
- package/dist/detector/catalog.test.js +0 -33
- package/dist/detector/eol.d.ts +0 -13
- package/dist/detector/eol.test.d.ts +0 -1
- package/dist/detector/eol.test.js +0 -78
- package/dist/detector/image-parser.d.ts +0 -11
- package/dist/detector/image-parser.test.d.ts +0 -1
- package/dist/detector/image-parser.test.js +0 -87
- package/dist/detector/index.d.ts +0 -33
- package/dist/detector/index.test.d.ts +0 -1
- package/dist/detector/index.test.js +0 -210
- package/dist/detector/osv.d.ts +0 -12
- package/dist/detector/osv.test.d.ts +0 -1
- package/dist/detector/osv.test.js +0 -86
- package/dist/detector/report.d.ts +0 -6
- package/dist/detector/sources.d.ts +0 -42
- package/dist/diagnoser/index.d.ts +0 -17
- package/dist/diagnoser/tools.d.ts +0 -119
- package/dist/kb/loader.d.ts +0 -1
- package/dist/kb/writer.d.ts +0 -12
- package/dist/kubectl-config.d.ts +0 -7
- package/dist/kubectl.d.ts +0 -13
- package/dist/monitor/checks.d.ts +0 -71
- package/dist/monitor/index.d.ts +0 -17
- package/dist/monitor/resolve.test.d.ts +0 -1
- package/dist/monitor/resolve.test.js +0 -21
- package/dist/monitor/types.d.ts +0 -11
- package/dist/notify/discord.d.ts +0 -4
- package/dist/notify/index.d.ts +0 -6
- package/dist/notify/pagerduty.d.ts +0 -9
- package/dist/notify/pagerduty.test.d.ts +0 -1
- package/dist/notify/pagerduty.test.js +0 -97
- package/dist/notify/setup.d.ts +0 -9
- package/dist/notify/slack.d.ts +0 -4
- package/dist/notify/teams.d.ts +0 -3
- package/dist/notify/telegram.d.ts +0 -9
- package/dist/notify/webhook.d.ts +0 -3
- package/dist/notify/webhook.test.d.ts +0 -1
- package/dist/notify/webhook.test.js +0 -53
- package/dist/onboard/cluster-scan.d.ts +0 -42
- package/dist/onboard/code-scan.d.ts +0 -9
- package/dist/onboard/index.d.ts +0 -3
- package/dist/onboard/interview.d.ts +0 -12
- package/dist/onboard/project-matcher.d.ts +0 -25
- package/dist/orchestrator.d.ts +0 -3
- package/dist/proxy-client.d.ts +0 -27
- package/dist/render.d.ts +0 -5
- package/dist/telemetry.d.ts +0 -1
- package/dist/telemetry.test.d.ts +0 -1
- package/dist/telemetry.test.js +0 -18
- package/dist/update-notifier.d.ts +0 -1
- package/dist/update-notifier.test.d.ts +0 -1
- package/dist/update-notifier.test.js +0 -50
- package/dist/verifier.d.ts +0 -9
- package/dist/version-check.d.ts +0 -14
- package/dist/version-check.test.d.ts +0 -1
- package/dist/version-check.test.js +0 -179
package/dist/notify/teams.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSafeWebhookUrl } from "./ssrf-guard.js";
|
|
1
2
|
function formatTeamsPayload(issues, clusterContext) {
|
|
2
3
|
const critical = issues.filter((i) => i.severity === "critical");
|
|
3
4
|
const warning = issues.filter((i) => i.severity === "warning");
|
|
@@ -25,15 +26,9 @@ function formatTeamsPayload(issues, clusterContext) {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
async function postToTeams(webhookUrl, payload) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.error("Teams: invalid URL protocol");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
console.error("Teams: invalid webhook URL");
|
|
29
|
+
// SSRF guard: reject invalid URLs and hosts in private/loopback/metadata ranges.
|
|
30
|
+
if (!(await isSafeWebhookUrl(webhookUrl))) {
|
|
31
|
+
console.error("Teams: invalid or unsafe webhook URL (must be a public host)");
|
|
37
32
|
return;
|
|
38
33
|
}
|
|
39
34
|
try {
|
|
@@ -41,6 +36,8 @@ async function postToTeams(webhookUrl, payload) {
|
|
|
41
36
|
method: "POST",
|
|
42
37
|
headers: { "Content-Type": "application/json" },
|
|
43
38
|
body: JSON.stringify(payload),
|
|
39
|
+
// Don't follow redirects to internal addresses (see ssrf-guard).
|
|
40
|
+
redirect: "error",
|
|
44
41
|
signal: AbortSignal.timeout(10_000),
|
|
45
42
|
});
|
|
46
43
|
if (!res.ok)
|
package/dist/notify/webhook.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { isSafeWebhookUrl } from "./ssrf-guard.js";
|
|
1
2
|
export async function sendWebhook(issues, channel, clusterContext) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
console.error("Webhook: invalid URL protocol");
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
console.error("Webhook: invalid URL");
|
|
3
|
+
// SSRF guard: reject invalid URLs and hosts in private/loopback/metadata ranges.
|
|
4
|
+
if (!(await isSafeWebhookUrl(channel.url))) {
|
|
5
|
+
console.error("Webhook: invalid or unsafe URL (must be a public https/http host)");
|
|
11
6
|
return;
|
|
12
7
|
}
|
|
13
8
|
const payload = {
|
|
@@ -31,6 +26,9 @@ export async function sendWebhook(issues, channel, clusterContext) {
|
|
|
31
26
|
method: "POST",
|
|
32
27
|
headers,
|
|
33
28
|
body: JSON.stringify(payload),
|
|
29
|
+
// Don't follow redirects: a public host could 30x to an internal address
|
|
30
|
+
// (metadata/localhost/RFC-1918) that the guard above never saw.
|
|
31
|
+
redirect: "error",
|
|
34
32
|
signal: AbortSignal.timeout(10_000),
|
|
35
33
|
});
|
|
36
34
|
if (!res.ok)
|
package/dist/orchestrator.js
CHANGED
|
@@ -140,7 +140,8 @@ export async function handleIssues(issues, config, clusterContext, noInteractive
|
|
|
140
140
|
autoFix: config.remediation.auto_fix,
|
|
141
141
|
safeActions: config.remediation.safe_actions,
|
|
142
142
|
noInteractive,
|
|
143
|
-
|
|
143
|
+
// Non-interactive mode must auto-deny every approval-gated (non-safe) action.
|
|
144
|
+
onApproval: noInteractive ? (async () => false) : askApproval,
|
|
144
145
|
onQuestion: (question, choices) => broadcastQuestion(question, choices, config, clusterContext),
|
|
145
146
|
});
|
|
146
147
|
// ── Analysis ──────────────────────────────────────────────
|
package/dist/telemetry.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir, platform, arch } from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
const TELEMETRY_DIR = join(homedir(), ".kubeagent");
|
|
5
6
|
const SENT_FILE = join(TELEMETRY_DIR, ".telemetry-sent");
|
|
7
|
+
const MACHINE_ID_FILE = join(TELEMETRY_DIR, ".machine-id");
|
|
6
8
|
const SERVER = "https://api.kubeagent.net";
|
|
9
|
+
/**
|
|
10
|
+
* Stable, anonymous per-machine ID. Used as the PostHog distinct_id for CLI
|
|
11
|
+
* events so install→auth→first-run funnels stitch together across invocations.
|
|
12
|
+
* Not tied to any user identity until the CLI logs in (server-side alias).
|
|
13
|
+
*/
|
|
14
|
+
function getOrCreateMachineId() {
|
|
15
|
+
try {
|
|
16
|
+
if (!existsSync(TELEMETRY_DIR))
|
|
17
|
+
mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
18
|
+
if (existsSync(MACHINE_ID_FILE)) {
|
|
19
|
+
const id = readFileSync(MACHINE_ID_FILE, "utf8").trim();
|
|
20
|
+
if (id)
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
const id = randomUUID();
|
|
24
|
+
writeFileSync(MACHINE_ID_FILE, id);
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// If we cannot persist, return an ephemeral id — better than nothing.
|
|
29
|
+
return randomUUID();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
7
32
|
export function sendTelemetry(cliVersion) {
|
|
8
33
|
// Only fire once per machine
|
|
9
34
|
if (existsSync(SENT_FILE))
|
|
@@ -24,6 +49,7 @@ export function sendTelemetry(cliVersion) {
|
|
|
24
49
|
os: platform(),
|
|
25
50
|
arch: arch(),
|
|
26
51
|
nodeVersion: process.version,
|
|
52
|
+
machineId: getOrCreateMachineId(),
|
|
27
53
|
});
|
|
28
54
|
fetch(`${SERVER}/telemetry`, {
|
|
29
55
|
method: "POST",
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kubeagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "AI-powered Kubernetes management CLI",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kubeagent-net/KubeAgent.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://kubeagent.net",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/kubeagent-net/KubeAgent/issues"
|
|
13
|
+
},
|
|
6
14
|
"type": "module",
|
|
7
15
|
"bin": {
|
|
8
16
|
"kubeagent": "./dist/cli.js"
|
|
@@ -24,6 +32,7 @@
|
|
|
24
32
|
"engines": {
|
|
25
33
|
"node": ">=22"
|
|
26
34
|
},
|
|
35
|
+
"packageManager": "npm@11.16.0",
|
|
27
36
|
"dependencies": {
|
|
28
37
|
"@anthropic-ai/sdk": "^0.81.0",
|
|
29
38
|
"chalk": "^5.4.0",
|
package/dist/auth.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export interface AuthState {
|
|
2
|
-
serverUrl: string;
|
|
3
|
-
appUrl: string;
|
|
4
|
-
token: string;
|
|
5
|
-
email?: string;
|
|
6
|
-
name?: string;
|
|
7
|
-
apiKey?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function loadAuth(): AuthState | null;
|
|
10
|
-
export declare function saveAuth(auth: AuthState): void;
|
|
11
|
-
export declare function clearAuth(): void;
|
|
12
|
-
export declare function loginBrowser(serverUrl: string, appUrl: string): Promise<AuthState>;
|
|
13
|
-
export declare function loginDevice(serverUrl: string, appUrl: string): Promise<AuthState>;
|
|
14
|
-
export declare function createApiKey(auth: AuthState, name: string): Promise<{
|
|
15
|
-
key: string;
|
|
16
|
-
prefix: string;
|
|
17
|
-
}>;
|
|
18
|
-
export interface BalanceInfo {
|
|
19
|
-
monthlyRemaining: number;
|
|
20
|
-
extraRemaining: number;
|
|
21
|
-
totalRemaining: number;
|
|
22
|
-
resetsAt: string;
|
|
23
|
-
plan: string | null;
|
|
24
|
-
planName: string | null;
|
|
25
|
-
monthlyTotal: number;
|
|
26
|
-
}
|
|
27
|
-
export declare function showAccount(auth: AuthState): Promise<{
|
|
28
|
-
balance: BalanceInfo;
|
|
29
|
-
}>;
|
package/dist/cli.d.ts
DELETED
package/dist/config.d.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
export interface ProjectMapping {
|
|
2
|
-
name: string;
|
|
3
|
-
dir: string;
|
|
4
|
-
deployment: string;
|
|
5
|
-
namespace: string;
|
|
6
|
-
kind: "Deployment" | "StatefulSet";
|
|
7
|
-
}
|
|
8
|
-
export interface ClusterConfig {
|
|
9
|
-
context: string;
|
|
10
|
-
interval: number;
|
|
11
|
-
codepaths: string[];
|
|
12
|
-
projects?: ProjectMapping[];
|
|
13
|
-
}
|
|
14
|
-
export type NotificationSeverity = "info" | "warning" | "critical";
|
|
15
|
-
export interface SlackChannel {
|
|
16
|
-
type: "slack";
|
|
17
|
-
webhook_url: string;
|
|
18
|
-
severity: NotificationSeverity;
|
|
19
|
-
label?: string;
|
|
20
|
-
}
|
|
21
|
-
export interface TelegramChannel {
|
|
22
|
-
type: "telegram";
|
|
23
|
-
bot_token: string;
|
|
24
|
-
chat_id: string;
|
|
25
|
-
severity: NotificationSeverity;
|
|
26
|
-
label?: string;
|
|
27
|
-
}
|
|
28
|
-
export interface WebhookChannel {
|
|
29
|
-
type: "webhook";
|
|
30
|
-
url: string;
|
|
31
|
-
secret?: string;
|
|
32
|
-
severity: NotificationSeverity;
|
|
33
|
-
label?: string;
|
|
34
|
-
}
|
|
35
|
-
export interface PagerDutyChannel {
|
|
36
|
-
type: "pagerduty";
|
|
37
|
-
routing_key: string;
|
|
38
|
-
severity: NotificationSeverity;
|
|
39
|
-
label?: string;
|
|
40
|
-
}
|
|
41
|
-
export interface DiscordChannel {
|
|
42
|
-
type: "discord";
|
|
43
|
-
webhook_url: string;
|
|
44
|
-
severity: NotificationSeverity;
|
|
45
|
-
label?: string;
|
|
46
|
-
}
|
|
47
|
-
export interface TeamsChannel {
|
|
48
|
-
type: "teams";
|
|
49
|
-
webhook_url: string;
|
|
50
|
-
severity: NotificationSeverity;
|
|
51
|
-
label?: string;
|
|
52
|
-
}
|
|
53
|
-
export type NotificationChannel = SlackChannel | TelegramChannel | WebhookChannel | PagerDutyChannel | DiscordChannel | TeamsChannel;
|
|
54
|
-
export declare const ALL_ACTIONS: readonly ["restart_pod", "rollout_restart", "scale_deployment", "set_resources"];
|
|
55
|
-
export type RemediationAction = (typeof ALL_ACTIONS)[number];
|
|
56
|
-
export interface KubeAgentConfig {
|
|
57
|
-
clusters: ClusterConfig[];
|
|
58
|
-
notifications: {
|
|
59
|
-
terminal: boolean;
|
|
60
|
-
channels: NotificationChannel[];
|
|
61
|
-
};
|
|
62
|
-
remediation: {
|
|
63
|
-
auto_fix: boolean;
|
|
64
|
-
max_retries: number;
|
|
65
|
-
cooldown: number;
|
|
66
|
-
safe_actions: RemediationAction[];
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export declare const DEFAULT_SAFE_ACTIONS: RemediationAction[];
|
|
70
|
-
export declare function defaultConfig(): KubeAgentConfig;
|
|
71
|
-
export declare function parseConfig(raw: Record<string, unknown>): KubeAgentConfig;
|
|
72
|
-
export declare function configDir(): string;
|
|
73
|
-
export declare function configPath(): string;
|
|
74
|
-
export declare function loadConfig(): KubeAgentConfig;
|
|
75
|
-
export declare function saveConfig(config: KubeAgentConfig): void;
|
package/dist/debug.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug logging — enabled with KUBEAGENT_DEBUG=1 (or any truthy value).
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* KUBEAGENT_DEBUG=1 kubeagent watch
|
|
6
|
-
*
|
|
7
|
-
* Logs to stderr so it doesn't interfere with piped output.
|
|
8
|
-
*/
|
|
9
|
-
export declare const DEBUG: boolean;
|
|
10
|
-
export declare function dbg(tag: string, message: string, data?: unknown): void;
|
package/dist/detector/cache.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare function readCache<T>(kind: string, key: string): T | null;
|
|
2
|
-
export declare function readStaleCache<T>(kind: string, key: string): {
|
|
3
|
-
data: T;
|
|
4
|
-
fetchedAt: number;
|
|
5
|
-
} | null;
|
|
6
|
-
export declare function writeCache<T>(kind: string, key: string, data: T): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
let tmpHome;
|
|
6
|
-
let originalHome;
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
originalHome = process.env.HOME;
|
|
9
|
-
tmpHome = mkdtempSync(join(tmpdir(), "kubeagent-cache-test-"));
|
|
10
|
-
process.env.HOME = tmpHome;
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
if (originalHome !== undefined)
|
|
14
|
-
process.env.HOME = originalHome;
|
|
15
|
-
else
|
|
16
|
-
delete process.env.HOME;
|
|
17
|
-
rmSync(tmpHome, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
describe("cache", () => {
|
|
20
|
-
it("round-trips data within the TTL", async () => {
|
|
21
|
-
const { readCache, writeCache } = await import("./cache.js");
|
|
22
|
-
writeCache("osv", "redis@7.0.5", [{ id: "CVE-1" }]);
|
|
23
|
-
const got = readCache("osv", "redis@7.0.5");
|
|
24
|
-
expect(got).toEqual([{ id: "CVE-1" }]);
|
|
25
|
-
});
|
|
26
|
-
it("returns null for a missing entry", async () => {
|
|
27
|
-
const { readCache } = await import("./cache.js");
|
|
28
|
-
expect(readCache("osv", "nope")).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
it("creates the cache directory on write", async () => {
|
|
31
|
-
const { writeCache } = await import("./cache.js");
|
|
32
|
-
writeCache("eol", "postgres", [{ cycle: "15" }]);
|
|
33
|
-
expect(existsSync(join(tmpHome, ".kubeagent", "cache", "eol", "postgres.json"))).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
it("sanitizes unsafe characters in cache keys", async () => {
|
|
36
|
-
const { writeCache } = await import("./cache.js");
|
|
37
|
-
writeCache("osv", "foo/../bar@1.0", { x: 1 });
|
|
38
|
-
expect(existsSync(join(tmpHome, ".kubeagent", "cache", "osv", "foo_.._bar@1.0.json"))).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
it("readStaleCache returns data and a fetchedAt timestamp", async () => {
|
|
41
|
-
const { writeCache, readStaleCache } = await import("./cache.js");
|
|
42
|
-
writeCache("osv", "k", "v");
|
|
43
|
-
const stale = readStaleCache("osv", "k");
|
|
44
|
-
expect(stale?.data).toBe("v");
|
|
45
|
-
expect(typeof stale?.fetchedAt).toBe("number");
|
|
46
|
-
});
|
|
47
|
-
it("treats a malformed cache file as a miss", async () => {
|
|
48
|
-
const { writeCache, readCache } = await import("./cache.js");
|
|
49
|
-
writeCache("osv", "k", "v");
|
|
50
|
-
const path = join(tmpHome, ".kubeagent", "cache", "osv", "k.json");
|
|
51
|
-
writeFileSync(path, "not json");
|
|
52
|
-
expect(readCache("osv", "k")).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface OsvLookup {
|
|
2
|
-
name: string;
|
|
3
|
-
ecosystem?: string;
|
|
4
|
-
}
|
|
5
|
-
export interface ProductEntry {
|
|
6
|
-
display: string;
|
|
7
|
-
eolProduct?: string;
|
|
8
|
-
osv?: OsvLookup;
|
|
9
|
-
}
|
|
10
|
-
export declare const PRODUCTS: Record<string, ProductEntry>;
|
|
11
|
-
export declare const ALIASES: Record<string, string>;
|
|
12
|
-
export interface CatalogMatch {
|
|
13
|
-
canonical: string;
|
|
14
|
-
entry: ProductEntry;
|
|
15
|
-
}
|
|
16
|
-
export declare function lookupCatalog(repository: string, name: string): CatalogMatch | null;
|
|
17
|
-
export declare function lookupChart(chartName: string): CatalogMatch | null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { lookupCatalog, lookupChart } from "./catalog.js";
|
|
3
|
-
describe("lookupCatalog", () => {
|
|
4
|
-
it("matches bare image names from the products map", () => {
|
|
5
|
-
const m = lookupCatalog("library/postgres", "postgres");
|
|
6
|
-
expect(m?.canonical).toBe("postgres");
|
|
7
|
-
expect(m?.entry.display).toBe("PostgreSQL");
|
|
8
|
-
});
|
|
9
|
-
it("resolves aliases for bitnami charts", () => {
|
|
10
|
-
const m = lookupCatalog("bitnami/postgresql", "postgresql");
|
|
11
|
-
expect(m?.canonical).toBe("postgres");
|
|
12
|
-
});
|
|
13
|
-
it("resolves quay.io prometheus", () => {
|
|
14
|
-
const m = lookupCatalog("prometheus/prometheus", "prometheus");
|
|
15
|
-
expect(m?.canonical).toBe("prometheus");
|
|
16
|
-
});
|
|
17
|
-
it("handles case-insensitive lookups", () => {
|
|
18
|
-
const m = lookupCatalog("Library/PostgreSQL", "PostgreSQL");
|
|
19
|
-
expect(m?.canonical).toBe("postgres");
|
|
20
|
-
});
|
|
21
|
-
it("returns null for unknown repositories", () => {
|
|
22
|
-
expect(lookupCatalog("acme/internal-app", "internal-app")).toBeNull();
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
describe("lookupChart", () => {
|
|
26
|
-
it("matches chart names to canonical products", () => {
|
|
27
|
-
expect(lookupChart("postgresql")?.canonical).toBe("postgres");
|
|
28
|
-
expect(lookupChart("redis")?.canonical).toBe("redis");
|
|
29
|
-
});
|
|
30
|
-
it("returns null for unknown charts", () => {
|
|
31
|
-
expect(lookupChart("unknown-chart")).toBeNull();
|
|
32
|
-
});
|
|
33
|
-
});
|
package/dist/detector/eol.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface EolStatus {
|
|
2
|
-
product: string;
|
|
3
|
-
cycle: string;
|
|
4
|
-
eol: boolean;
|
|
5
|
-
eolDate?: string;
|
|
6
|
-
latest?: string;
|
|
7
|
-
releaseDate?: string;
|
|
8
|
-
lts: boolean;
|
|
9
|
-
}
|
|
10
|
-
export declare function queryEol(product: string, version: string, opts?: {
|
|
11
|
-
noCache?: boolean;
|
|
12
|
-
timeoutMs?: number;
|
|
13
|
-
}): Promise<EolStatus | null>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
vi.mock("./cache.js", () => ({
|
|
3
|
-
readCache: vi.fn(),
|
|
4
|
-
readStaleCache: vi.fn(),
|
|
5
|
-
writeCache: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
import { readCache, readStaleCache, writeCache } from "./cache.js";
|
|
8
|
-
import { queryEol } from "./eol.js";
|
|
9
|
-
const mockFetch = vi.fn();
|
|
10
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
11
|
-
const cacheRead = vi.mocked(readCache);
|
|
12
|
-
const cacheStale = vi.mocked(readStaleCache);
|
|
13
|
-
const cacheWrite = vi.mocked(writeCache);
|
|
14
|
-
const POSTGRES_FIXTURE = [
|
|
15
|
-
{ cycle: "16", releaseDate: "2023-09-14", eol: "2028-11-09", latest: "16.2", lts: false },
|
|
16
|
-
{ cycle: "15", releaseDate: "2022-10-13", eol: "2027-11-11", latest: "15.6", lts: false },
|
|
17
|
-
{ cycle: "11", releaseDate: "2018-10-18", eol: "2023-11-09", latest: "11.22", lts: false },
|
|
18
|
-
];
|
|
19
|
-
describe("queryEol", () => {
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
mockFetch.mockReset();
|
|
22
|
-
cacheRead.mockReset();
|
|
23
|
-
cacheStale.mockReset();
|
|
24
|
-
cacheWrite.mockReset();
|
|
25
|
-
cacheRead.mockReturnValue(null);
|
|
26
|
-
cacheStale.mockReturnValue(null);
|
|
27
|
-
});
|
|
28
|
-
it("hits endoflife.date and picks the matching cycle by major version", async () => {
|
|
29
|
-
mockFetch.mockResolvedValue({ ok: true, json: async () => POSTGRES_FIXTURE });
|
|
30
|
-
const status = await queryEol("postgres", "15.2", { noCache: true });
|
|
31
|
-
expect(mockFetch.mock.calls[0][0]).toBe("https://endoflife.date/api/postgres.json");
|
|
32
|
-
expect(status).toMatchObject({
|
|
33
|
-
product: "postgres",
|
|
34
|
-
cycle: "15",
|
|
35
|
-
eol: false,
|
|
36
|
-
eolDate: "2027-11-11",
|
|
37
|
-
latest: "15.6",
|
|
38
|
-
});
|
|
39
|
-
expect(cacheWrite).toHaveBeenCalledOnce();
|
|
40
|
-
});
|
|
41
|
-
it("flags a cycle as EOL when its date is in the past", async () => {
|
|
42
|
-
mockFetch.mockResolvedValue({ ok: true, json: async () => POSTGRES_FIXTURE });
|
|
43
|
-
const status = await queryEol("postgres", "11.18", { noCache: true });
|
|
44
|
-
expect(status?.cycle).toBe("11");
|
|
45
|
-
expect(status?.eol).toBe(true);
|
|
46
|
-
expect(status?.eolDate).toBe("2023-11-09");
|
|
47
|
-
});
|
|
48
|
-
it("returns null when no cycle matches", async () => {
|
|
49
|
-
mockFetch.mockResolvedValue({ ok: true, json: async () => POSTGRES_FIXTURE });
|
|
50
|
-
const status = await queryEol("postgres", "99.0", { noCache: true });
|
|
51
|
-
expect(status).toBeNull();
|
|
52
|
-
});
|
|
53
|
-
it("returns null on network error with no cache", async () => {
|
|
54
|
-
mockFetch.mockRejectedValue(new Error("network down"));
|
|
55
|
-
const status = await queryEol("postgres", "15.2", { noCache: true });
|
|
56
|
-
expect(status).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
it("serves a fresh cached entry without hitting the network", async () => {
|
|
59
|
-
cacheRead.mockReturnValue(POSTGRES_FIXTURE);
|
|
60
|
-
const status = await queryEol("postgres", "15.2");
|
|
61
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
62
|
-
expect(status?.cycle).toBe("15");
|
|
63
|
-
});
|
|
64
|
-
it("falls back to stale cache on network error", async () => {
|
|
65
|
-
cacheStale.mockReturnValue({ data: POSTGRES_FIXTURE, fetchedAt: 1 });
|
|
66
|
-
mockFetch.mockRejectedValue(new Error("offline"));
|
|
67
|
-
const status = await queryEol("postgres", "11.18", { noCache: true });
|
|
68
|
-
expect(status?.eol).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
it("treats eol: true as EOL regardless of date", async () => {
|
|
71
|
-
mockFetch.mockResolvedValue({
|
|
72
|
-
ok: true,
|
|
73
|
-
json: async () => [{ cycle: "1.0", eol: true, latest: "1.0.5" }],
|
|
74
|
-
});
|
|
75
|
-
const status = await queryEol("oldthing", "1.0", { noCache: true });
|
|
76
|
-
expect(status?.eol).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface ParsedImage {
|
|
2
|
-
registry: string;
|
|
3
|
-
repository: string;
|
|
4
|
-
name: string;
|
|
5
|
-
tag: string;
|
|
6
|
-
digest?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function parseImage(image: string): ParsedImage;
|
|
9
|
-
export declare function normalizeVersion(tag: string): string;
|
|
10
|
-
export declare function majorMinor(version: string): string;
|
|
11
|
-
export declare function major(version: string): string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseImage, normalizeVersion, majorMinor, major } from "./image-parser.js";
|
|
3
|
-
describe("parseImage", () => {
|
|
4
|
-
it("parses a bare image name", () => {
|
|
5
|
-
expect(parseImage("nginx")).toEqual({
|
|
6
|
-
registry: "docker.io",
|
|
7
|
-
repository: "library/nginx",
|
|
8
|
-
name: "nginx",
|
|
9
|
-
tag: "latest",
|
|
10
|
-
digest: undefined,
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
it("parses a bare image with a tag", () => {
|
|
14
|
-
expect(parseImage("nginx:1.24.0")).toMatchObject({
|
|
15
|
-
registry: "docker.io",
|
|
16
|
-
repository: "library/nginx",
|
|
17
|
-
name: "nginx",
|
|
18
|
-
tag: "1.24.0",
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
it("parses a docker hub user/repo image", () => {
|
|
22
|
-
expect(parseImage("bitnami/postgresql:15")).toMatchObject({
|
|
23
|
-
registry: "docker.io",
|
|
24
|
-
repository: "bitnami/postgresql",
|
|
25
|
-
name: "postgresql",
|
|
26
|
-
tag: "15",
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
it("parses a custom registry", () => {
|
|
30
|
-
expect(parseImage("quay.io/prometheus/prometheus:v2.45.0")).toMatchObject({
|
|
31
|
-
registry: "quay.io",
|
|
32
|
-
repository: "prometheus/prometheus",
|
|
33
|
-
name: "prometheus",
|
|
34
|
-
tag: "v2.45.0",
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
it("parses registry.k8s.io single-segment paths without forcing library/", () => {
|
|
38
|
-
expect(parseImage("registry.k8s.io/etcd:3.5.9-0")).toMatchObject({
|
|
39
|
-
registry: "registry.k8s.io",
|
|
40
|
-
repository: "etcd",
|
|
41
|
-
name: "etcd",
|
|
42
|
-
tag: "3.5.9-0",
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
it("parses a registry with a port", () => {
|
|
46
|
-
expect(parseImage("localhost:5000/mything:1.0")).toMatchObject({
|
|
47
|
-
registry: "localhost:5000",
|
|
48
|
-
repository: "mything",
|
|
49
|
-
name: "mything",
|
|
50
|
-
tag: "1.0",
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
it("parses a digest-only reference", () => {
|
|
54
|
-
const p = parseImage("nginx@sha256:abcdef");
|
|
55
|
-
expect(p.repository).toBe("library/nginx");
|
|
56
|
-
expect(p.tag).toBe("");
|
|
57
|
-
expect(p.digest).toBe("sha256:abcdef");
|
|
58
|
-
});
|
|
59
|
-
it("parses tag + digest", () => {
|
|
60
|
-
const p = parseImage("nginx:1.24@sha256:abcdef");
|
|
61
|
-
expect(p.tag).toBe("1.24");
|
|
62
|
-
expect(p.digest).toBe("sha256:abcdef");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe("normalizeVersion", () => {
|
|
66
|
-
it("strips a leading v", () => {
|
|
67
|
-
expect(normalizeVersion("v1.2.3")).toBe("1.2.3");
|
|
68
|
-
});
|
|
69
|
-
it("strips suffixes like -alpine", () => {
|
|
70
|
-
expect(normalizeVersion("15.2-alpine")).toBe("15.2");
|
|
71
|
-
});
|
|
72
|
-
it("returns empty for placeholder tags", () => {
|
|
73
|
-
expect(normalizeVersion("latest")).toBe("");
|
|
74
|
-
expect(normalizeVersion("stable")).toBe("");
|
|
75
|
-
expect(normalizeVersion("")).toBe("");
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe("major/majorMinor", () => {
|
|
79
|
-
it("extracts major", () => {
|
|
80
|
-
expect(major("15.2.1")).toBe("15");
|
|
81
|
-
expect(major("11")).toBe("11");
|
|
82
|
-
});
|
|
83
|
-
it("extracts major.minor", () => {
|
|
84
|
-
expect(majorMinor("15.2.1")).toBe("15.2");
|
|
85
|
-
expect(majorMinor("11")).toBe("11");
|
|
86
|
-
});
|
|
87
|
-
});
|
package/dist/detector/index.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { KubectlOptions } from "../kubectl.js";
|
|
2
|
-
import { type Candidate, type WorkloadRef } from "./sources.js";
|
|
3
|
-
import { type CveFinding } from "./osv.js";
|
|
4
|
-
import { type EolStatus } from "./eol.js";
|
|
5
|
-
export interface DetectedApp {
|
|
6
|
-
canonical: string;
|
|
7
|
-
display: string;
|
|
8
|
-
version: string;
|
|
9
|
-
sources: Array<Candidate["source"]>;
|
|
10
|
-
workloads: WorkloadRef[];
|
|
11
|
-
cves: CveFinding[];
|
|
12
|
-
eol: EolStatus | null;
|
|
13
|
-
catalogMatch: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface UnknownApp {
|
|
16
|
-
name: string;
|
|
17
|
-
version: string;
|
|
18
|
-
workloads: WorkloadRef[];
|
|
19
|
-
}
|
|
20
|
-
export interface DetectionResult {
|
|
21
|
-
apps: DetectedApp[];
|
|
22
|
-
unknowns: UnknownApp[];
|
|
23
|
-
scanned: {
|
|
24
|
-
workloads: number;
|
|
25
|
-
helmReleases: number;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export interface DetectOptions extends KubectlOptions {
|
|
29
|
-
noCache?: boolean;
|
|
30
|
-
onProgress?: (step: string) => void;
|
|
31
|
-
}
|
|
32
|
-
export declare function detectApplications(opts?: DetectOptions): Promise<DetectionResult>;
|
|
33
|
-
export declare function formatApplicationsMarkdown(result: DetectionResult): string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|