kubeagent 0.1.43 → 0.1.45
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/package.json +5 -2
- package/dist/auth.d.ts +0 -29
- package/dist/check/index.d.ts +0 -34
- package/dist/check/index.test.d.ts +0 -1
- package/dist/check/index.test.js +0 -202
- package/dist/check/run.d.ts +0 -10
- 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 -55
- package/dist/detector/sources.test.d.ts +0 -1
- package/dist/detector/sources.test.js +0 -36
- package/dist/diagnoser/approval.test.d.ts +0 -1
- package/dist/diagnoser/approval.test.js +0 -49
- package/dist/diagnoser/index.d.ts +0 -33
- 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 -27
- package/dist/monitor/resolve.test.d.ts +0 -1
- package/dist/monitor/resolve.test.js +0 -21
- package/dist/monitor/scope.test.d.ts +0 -1
- package/dist/monitor/scope.test.js +0 -87
- 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/ssrf-guard.d.ts +0 -5
- 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 -74
- 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
|
@@ -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 {};
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
vi.mock("./sources.js", () => ({
|
|
3
|
-
fetchWorkloadCandidates: vi.fn(),
|
|
4
|
-
fetchHelmCandidates: vi.fn(),
|
|
5
|
-
fetchIngressHostMap: vi.fn(),
|
|
6
|
-
workloadKey: (ref) => `${ref.kind}/${ref.namespace}/${ref.name}`,
|
|
7
|
-
}));
|
|
8
|
-
vi.mock("./osv.js", () => ({
|
|
9
|
-
queryOsv: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
vi.mock("./eol.js", () => ({
|
|
12
|
-
queryEol: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
import { fetchWorkloadCandidates, fetchHelmCandidates, fetchIngressHostMap } from "./sources.js";
|
|
15
|
-
import { queryOsv } from "./osv.js";
|
|
16
|
-
import { queryEol } from "./eol.js";
|
|
17
|
-
import { detectApplications, formatApplicationsMarkdown } from "./index.js";
|
|
18
|
-
const fetchWorkloads = vi.mocked(fetchWorkloadCandidates);
|
|
19
|
-
const fetchHelm = vi.mocked(fetchHelmCandidates);
|
|
20
|
-
const fetchIngress = vi.mocked(fetchIngressHostMap);
|
|
21
|
-
const osv = vi.mocked(queryOsv);
|
|
22
|
-
const eol = vi.mocked(queryEol);
|
|
23
|
-
describe("detectApplications", () => {
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
fetchWorkloads.mockReset();
|
|
26
|
-
fetchHelm.mockReset();
|
|
27
|
-
fetchIngress.mockReset();
|
|
28
|
-
osv.mockReset();
|
|
29
|
-
eol.mockReset();
|
|
30
|
-
fetchIngress.mockResolvedValue(new Map());
|
|
31
|
-
osv.mockResolvedValue([]);
|
|
32
|
-
eol.mockResolvedValue(null);
|
|
33
|
-
});
|
|
34
|
-
it("merges image and label candidates pointing at the same app/version", async () => {
|
|
35
|
-
fetchWorkloads.mockResolvedValue([
|
|
36
|
-
{
|
|
37
|
-
source: "image",
|
|
38
|
-
image: "postgres:15.2",
|
|
39
|
-
container: "db",
|
|
40
|
-
workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
source: "label",
|
|
44
|
-
appName: "postgres",
|
|
45
|
-
appVersion: "15.2",
|
|
46
|
-
workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
|
|
47
|
-
},
|
|
48
|
-
]);
|
|
49
|
-
fetchHelm.mockResolvedValue([]);
|
|
50
|
-
const result = await detectApplications({ noCache: true });
|
|
51
|
-
expect(result.apps).toHaveLength(1);
|
|
52
|
-
expect(result.apps[0].canonical).toBe("postgres");
|
|
53
|
-
expect(result.apps[0].version).toBe("15.2");
|
|
54
|
-
expect(result.apps[0].sources.sort()).toEqual(["image", "label"]);
|
|
55
|
-
expect(result.apps[0].workloads).toHaveLength(1);
|
|
56
|
-
});
|
|
57
|
-
it("tracks images not in the catalog as unknowns", async () => {
|
|
58
|
-
fetchWorkloads.mockResolvedValue([
|
|
59
|
-
{
|
|
60
|
-
source: "image",
|
|
61
|
-
image: "acme/internal-app:1.2.3",
|
|
62
|
-
container: "app",
|
|
63
|
-
workload: { kind: "Deployment", namespace: "team-x", name: "foo" },
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
66
|
-
fetchHelm.mockResolvedValue([]);
|
|
67
|
-
const result = await detectApplications({ noCache: true });
|
|
68
|
-
expect(result.apps).toHaveLength(0);
|
|
69
|
-
expect(result.unknowns).toHaveLength(1);
|
|
70
|
-
expect(result.unknowns[0]).toMatchObject({ name: "internal-app", version: "1.2.3" });
|
|
71
|
-
});
|
|
72
|
-
it("flags EOL apps via the eol lookup", async () => {
|
|
73
|
-
fetchWorkloads.mockResolvedValue([
|
|
74
|
-
{
|
|
75
|
-
source: "image",
|
|
76
|
-
image: "postgres:11.18",
|
|
77
|
-
container: "db",
|
|
78
|
-
workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
|
|
79
|
-
},
|
|
80
|
-
]);
|
|
81
|
-
fetchHelm.mockResolvedValue([]);
|
|
82
|
-
eol.mockResolvedValue({
|
|
83
|
-
product: "postgres",
|
|
84
|
-
cycle: "11",
|
|
85
|
-
eol: true,
|
|
86
|
-
eolDate: "2023-11-09",
|
|
87
|
-
latest: "11.22",
|
|
88
|
-
lts: false,
|
|
89
|
-
});
|
|
90
|
-
const result = await detectApplications({ noCache: true });
|
|
91
|
-
expect(result.apps[0].eol?.eol).toBe(true);
|
|
92
|
-
expect(result.apps[0].eol?.eolDate).toBe("2023-11-09");
|
|
93
|
-
});
|
|
94
|
-
it("attaches CVE findings from OSV", async () => {
|
|
95
|
-
fetchWorkloads.mockResolvedValue([
|
|
96
|
-
{
|
|
97
|
-
source: "image",
|
|
98
|
-
image: "redis:7.0.5",
|
|
99
|
-
container: "cache",
|
|
100
|
-
workload: { kind: "Deployment", namespace: "infra", name: "redis" },
|
|
101
|
-
},
|
|
102
|
-
]);
|
|
103
|
-
fetchHelm.mockResolvedValue([]);
|
|
104
|
-
osv.mockResolvedValue([
|
|
105
|
-
{ id: "CVE-2023-9999", summary: "Boom.", severity: "high", fixed: "7.0.12" },
|
|
106
|
-
]);
|
|
107
|
-
const result = await detectApplications({ noCache: true });
|
|
108
|
-
expect(result.apps[0].cves).toHaveLength(1);
|
|
109
|
-
expect(result.apps[0].cves[0].id).toBe("CVE-2023-9999");
|
|
110
|
-
});
|
|
111
|
-
it("orders apps by severity (EOL → CVEs → healthy)", async () => {
|
|
112
|
-
fetchWorkloads.mockResolvedValue([
|
|
113
|
-
{
|
|
114
|
-
source: "image",
|
|
115
|
-
image: "nginx:1.25.3",
|
|
116
|
-
container: "web",
|
|
117
|
-
workload: { kind: "Deployment", namespace: "edge", name: "nginx" },
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
source: "image",
|
|
121
|
-
image: "postgres:11.18",
|
|
122
|
-
container: "db",
|
|
123
|
-
workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
source: "image",
|
|
127
|
-
image: "redis:7.0.5",
|
|
128
|
-
container: "cache",
|
|
129
|
-
workload: { kind: "Deployment", namespace: "infra", name: "redis" },
|
|
130
|
-
},
|
|
131
|
-
]);
|
|
132
|
-
fetchHelm.mockResolvedValue([]);
|
|
133
|
-
eol.mockImplementation(async (product) => product === "postgres"
|
|
134
|
-
? { product: "postgres", cycle: "11", eol: true, eolDate: "2023-11-09", lts: false }
|
|
135
|
-
: null);
|
|
136
|
-
osv.mockImplementation(async (lookup) => lookup.name === "redis"
|
|
137
|
-
? [{ id: "CVE-2023-1", summary: "x", severity: "medium" }]
|
|
138
|
-
: []);
|
|
139
|
-
const result = await detectApplications({ noCache: true });
|
|
140
|
-
expect(result.apps.map((a) => a.canonical)).toEqual(["postgres", "redis", "nginx"]);
|
|
141
|
-
});
|
|
142
|
-
it("uses Helm chart appVersion when available", async () => {
|
|
143
|
-
fetchWorkloads.mockResolvedValue([]);
|
|
144
|
-
fetchHelm.mockResolvedValue([
|
|
145
|
-
{
|
|
146
|
-
source: "helm",
|
|
147
|
-
chart: "postgresql",
|
|
148
|
-
chartVersion: "12.0.0",
|
|
149
|
-
appVersion: "15.2.0",
|
|
150
|
-
namespace: "data",
|
|
151
|
-
releaseName: "primary",
|
|
152
|
-
},
|
|
153
|
-
]);
|
|
154
|
-
const result = await detectApplications({ noCache: true });
|
|
155
|
-
expect(result.apps).toHaveLength(1);
|
|
156
|
-
expect(result.apps[0].canonical).toBe("postgres");
|
|
157
|
-
expect(result.apps[0].version).toBe("15.2.0");
|
|
158
|
-
expect(result.apps[0].sources).toContain("helm");
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
describe("formatApplicationsMarkdown", () => {
|
|
162
|
-
it("produces sections for EOL, CVEs, healthy, and unknowns", () => {
|
|
163
|
-
const md = formatApplicationsMarkdown({
|
|
164
|
-
apps: [
|
|
165
|
-
{
|
|
166
|
-
canonical: "postgres",
|
|
167
|
-
display: "PostgreSQL",
|
|
168
|
-
version: "11.18",
|
|
169
|
-
sources: ["image"],
|
|
170
|
-
workloads: [{ kind: "StatefulSet", namespace: "prod", name: "db" }],
|
|
171
|
-
cves: [],
|
|
172
|
-
eol: { product: "postgres", cycle: "11", eol: true, eolDate: "2023-11-09", latest: "11.22", lts: false },
|
|
173
|
-
catalogMatch: true,
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
canonical: "redis",
|
|
177
|
-
display: "Redis",
|
|
178
|
-
version: "7.0.5",
|
|
179
|
-
sources: ["image"],
|
|
180
|
-
workloads: [{ kind: "Deployment", namespace: "infra", name: "redis" }],
|
|
181
|
-
cves: [{ id: "CVE-2023-1", summary: "boom", severity: "high", fixed: "7.0.12" }],
|
|
182
|
-
eol: null,
|
|
183
|
-
catalogMatch: true,
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
canonical: "nginx",
|
|
187
|
-
display: "nginx",
|
|
188
|
-
version: "1.25.3",
|
|
189
|
-
sources: ["image"],
|
|
190
|
-
workloads: [{ kind: "Deployment", namespace: "edge", name: "nginx" }],
|
|
191
|
-
cves: [],
|
|
192
|
-
eol: null,
|
|
193
|
-
catalogMatch: true,
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
unknowns: [
|
|
197
|
-
{ name: "internal-app", version: "1.2.3", workloads: [{ kind: "Deployment", namespace: "team-x", name: "foo" }] },
|
|
198
|
-
],
|
|
199
|
-
scanned: { workloads: 4, helmReleases: 0 },
|
|
200
|
-
});
|
|
201
|
-
expect(md).toContain("## End-of-Life");
|
|
202
|
-
expect(md).toContain("PostgreSQL");
|
|
203
|
-
expect(md).toContain("## Known Vulnerabilities");
|
|
204
|
-
expect(md).toContain("CVE-2023-1");
|
|
205
|
-
expect(md).toContain("## Up-to-date");
|
|
206
|
-
expect(md).toContain("nginx");
|
|
207
|
-
expect(md).toContain("## Other / not in catalog");
|
|
208
|
-
expect(md).toContain("internal-app");
|
|
209
|
-
});
|
|
210
|
-
});
|
package/dist/detector/osv.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { OsvLookup } from "./catalog.js";
|
|
2
|
-
export interface CveFinding {
|
|
3
|
-
id: string;
|
|
4
|
-
summary: string;
|
|
5
|
-
severity?: string;
|
|
6
|
-
fixed?: string;
|
|
7
|
-
reference?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function queryOsv(lookup: OsvLookup, version: string, opts?: {
|
|
10
|
-
noCache?: boolean;
|
|
11
|
-
timeoutMs?: number;
|
|
12
|
-
}): Promise<CveFinding[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,86 +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 { queryOsv } from "./osv.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
|
-
describe("queryOsv", () => {
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
mockFetch.mockReset();
|
|
17
|
-
cacheRead.mockReset();
|
|
18
|
-
cacheStale.mockReset();
|
|
19
|
-
cacheWrite.mockReset();
|
|
20
|
-
cacheRead.mockReturnValue(null);
|
|
21
|
-
cacheStale.mockReturnValue(null);
|
|
22
|
-
});
|
|
23
|
-
it("posts the package + version to api.osv.dev and returns mapped findings", async () => {
|
|
24
|
-
mockFetch.mockResolvedValue({
|
|
25
|
-
ok: true,
|
|
26
|
-
json: async () => ({
|
|
27
|
-
vulns: [
|
|
28
|
-
{
|
|
29
|
-
id: "CVE-2024-1",
|
|
30
|
-
summary: "Test vuln",
|
|
31
|
-
database_specific: { severity: "high" },
|
|
32
|
-
affected: [{ ranges: [{ events: [{ introduced: "0" }, { fixed: "7.0.12" }] }] }],
|
|
33
|
-
references: [{ type: "ADVISORY", url: "https://example.com/advisory" }],
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
}),
|
|
37
|
-
});
|
|
38
|
-
const findings = await queryOsv({ name: "redis" }, "7.0.5", { noCache: true });
|
|
39
|
-
expect(mockFetch).toHaveBeenCalledOnce();
|
|
40
|
-
expect(mockFetch.mock.calls[0][0]).toBe("https://api.osv.dev/v1/query");
|
|
41
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
42
|
-
expect(body).toEqual({ package: { name: "redis" }, version: "7.0.5" });
|
|
43
|
-
expect(findings).toHaveLength(1);
|
|
44
|
-
expect(findings[0]).toMatchObject({
|
|
45
|
-
id: "CVE-2024-1",
|
|
46
|
-
severity: "high",
|
|
47
|
-
fixed: "7.0.12",
|
|
48
|
-
reference: "https://example.com/advisory",
|
|
49
|
-
});
|
|
50
|
-
expect(cacheWrite).toHaveBeenCalledOnce();
|
|
51
|
-
});
|
|
52
|
-
it("includes ecosystem in the OSV body when provided", async () => {
|
|
53
|
-
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
|
|
54
|
-
await queryOsv({ name: "postgresql", ecosystem: "Debian" }, "15.2", { noCache: true });
|
|
55
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
56
|
-
expect(body.package).toEqual({ name: "postgresql", ecosystem: "Debian" });
|
|
57
|
-
});
|
|
58
|
-
it("returns empty when version is empty", async () => {
|
|
59
|
-
const findings = await queryOsv({ name: "redis" }, "", { noCache: true });
|
|
60
|
-
expect(findings).toEqual([]);
|
|
61
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
62
|
-
});
|
|
63
|
-
it("returns [] on network error when no stale cache is present", async () => {
|
|
64
|
-
mockFetch.mockRejectedValue(new Error("network down"));
|
|
65
|
-
const findings = await queryOsv({ name: "redis" }, "7.0.99", { noCache: true });
|
|
66
|
-
expect(findings).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
it("serves a fresh cached entry without hitting the network", async () => {
|
|
69
|
-
cacheRead.mockReturnValue([{ id: "CVE-cached", summary: "from cache" }]);
|
|
70
|
-
const findings = await queryOsv({ name: "redis" }, "7.0.5");
|
|
71
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
72
|
-
expect(findings[0].id).toBe("CVE-cached");
|
|
73
|
-
});
|
|
74
|
-
it("falls back to stale cache on network error", async () => {
|
|
75
|
-
cacheStale.mockReturnValue({ data: [{ id: "CVE-stale", summary: "from cache" }], fetchedAt: 1 });
|
|
76
|
-
mockFetch.mockRejectedValue(new Error("offline"));
|
|
77
|
-
const findings = await queryOsv({ name: "redis" }, "7.0.5", { noCache: true });
|
|
78
|
-
expect(findings[0].id).toBe("CVE-stale");
|
|
79
|
-
});
|
|
80
|
-
it("falls back to stale cache on non-2xx response", async () => {
|
|
81
|
-
cacheStale.mockReturnValue({ data: [{ id: "CVE-stale", summary: "x" }], fetchedAt: 1 });
|
|
82
|
-
mockFetch.mockResolvedValue({ ok: false, status: 500 });
|
|
83
|
-
const findings = await queryOsv({ name: "redis" }, "7.0.5", { noCache: true });
|
|
84
|
-
expect(findings[0].id).toBe("CVE-stale");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { AuthState } from "../auth.js";
|
|
2
|
-
import type { DetectionResult } from "./index.js";
|
|
3
|
-
export interface ReportOptions {
|
|
4
|
-
onProgress?: (step: string) => void;
|
|
5
|
-
}
|
|
6
|
-
export declare function generateReport(result: DetectionResult, auth: AuthState, opts?: ReportOptions): Promise<string>;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { type KubectlOptions } from "../kubectl.js";
|
|
2
|
-
export type WorkloadKind = "Deployment" | "StatefulSet" | "DaemonSet";
|
|
3
|
-
export interface WorkloadRef {
|
|
4
|
-
kind: WorkloadKind;
|
|
5
|
-
namespace: string;
|
|
6
|
-
name: string;
|
|
7
|
-
hosts?: string[];
|
|
8
|
-
}
|
|
9
|
-
export declare function workloadKey(ref: {
|
|
10
|
-
kind: string;
|
|
11
|
-
namespace: string;
|
|
12
|
-
name: string;
|
|
13
|
-
}): string;
|
|
14
|
-
export interface ImageCandidate {
|
|
15
|
-
source: "image";
|
|
16
|
-
image: string;
|
|
17
|
-
workload: WorkloadRef;
|
|
18
|
-
container: string;
|
|
19
|
-
}
|
|
20
|
-
export interface LabelCandidate {
|
|
21
|
-
source: "label";
|
|
22
|
-
appName: string;
|
|
23
|
-
appVersion: string;
|
|
24
|
-
workload: WorkloadRef;
|
|
25
|
-
}
|
|
26
|
-
export interface HelmCandidate {
|
|
27
|
-
source: "helm";
|
|
28
|
-
chart: string;
|
|
29
|
-
chartVersion: string;
|
|
30
|
-
appVersion?: string;
|
|
31
|
-
namespace: string;
|
|
32
|
-
releaseName: string;
|
|
33
|
-
}
|
|
34
|
-
export type Candidate = ImageCandidate | LabelCandidate | HelmCandidate;
|
|
35
|
-
export declare function fetchWorkloadCandidates(options: KubectlOptions): Promise<Candidate[]>;
|
|
36
|
-
interface HelmReleasePayload {
|
|
37
|
-
name: string;
|
|
38
|
-
namespace: string;
|
|
39
|
-
chart?: {
|
|
40
|
-
metadata?: {
|
|
41
|
-
name?: string;
|
|
42
|
-
version?: string;
|
|
43
|
-
appVersion?: string;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
export declare function decodeHelmRelease(b64: string): HelmReleasePayload | null;
|
|
48
|
-
export declare function fetchHelmCandidates(options: KubectlOptions): Promise<HelmCandidate[]>;
|
|
49
|
-
/**
|
|
50
|
-
* Build a map of workload key (Kind/namespace/name) → ingress hostnames.
|
|
51
|
-
* Joins Ingresses → Services → workload pod-template labels best-effort.
|
|
52
|
-
* Returns an empty map on failure (ingress visibility is non-essential).
|
|
53
|
-
*/
|
|
54
|
-
export declare function fetchIngressHostMap(options: KubectlOptions): Promise<Map<string, string[]>>;
|
|
55
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { gzipSync } from "node:zlib";
|
|
3
|
-
import { Buffer } from "node:buffer";
|
|
4
|
-
import { decodeHelmRelease } from "./sources.js";
|
|
5
|
-
// Reproduce the encoding chain Helm + the k8s API actually produce.
|
|
6
|
-
// Helm stores the release as `base64(gzip(json))` inside the Secret value,
|
|
7
|
-
// and the k8s API base64-encodes Secret `data.*` values on the wire — so the
|
|
8
|
-
// string kubectl returns is `base64(base64(gzip(json)))`. This test exists
|
|
9
|
-
// to lock in the decoder's behaviour against that chain so we can't
|
|
10
|
-
// regress to a single-base64 decode (which silently drops every Helm
|
|
11
|
-
// release at runtime).
|
|
12
|
-
function encodeAsKubectlReturnsIt(payload) {
|
|
13
|
-
const json = JSON.stringify(payload);
|
|
14
|
-
const gzipped = gzipSync(Buffer.from(json, "utf-8"));
|
|
15
|
-
const helmLayer = gzipped.toString("base64");
|
|
16
|
-
const apiLayer = Buffer.from(helmLayer, "utf-8").toString("base64");
|
|
17
|
-
return apiLayer;
|
|
18
|
-
}
|
|
19
|
-
describe("decodeHelmRelease", () => {
|
|
20
|
-
it("decodes a release blob in the format kubectl returns", () => {
|
|
21
|
-
const release = {
|
|
22
|
-
name: "postgres",
|
|
23
|
-
namespace: "data",
|
|
24
|
-
chart: { metadata: { name: "postgresql", version: "12.0.0", appVersion: "15.2.0" } },
|
|
25
|
-
};
|
|
26
|
-
const encoded = encodeAsKubectlReturnsIt(release);
|
|
27
|
-
expect(decodeHelmRelease(encoded)).toEqual(release);
|
|
28
|
-
});
|
|
29
|
-
it("returns null on garbage input rather than throwing", () => {
|
|
30
|
-
expect(decodeHelmRelease("not-base64-and-not-gzip")).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
it("returns null on valid base64 that isn't gzip", () => {
|
|
33
|
-
const apiLayer = Buffer.from("aGVsbG8gd29ybGQ=", "utf-8").toString("base64");
|
|
34
|
-
expect(decodeHelmRelease(apiLayer)).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { resolveApprovalDecision } from "./index.js";
|
|
3
|
-
const SAFE = new Set(["get_logs", "get_events", "describe_resource", "ask_user_question"]);
|
|
4
|
-
describe("resolveApprovalDecision", () => {
|
|
5
|
-
it("auto-executes a safe (read-only) action with no approver", async () => {
|
|
6
|
-
const decision = await resolveApprovalDecision("get_logs", {}, SAFE, undefined);
|
|
7
|
-
expect(decision.execute).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
it("DENIES a non-safe mutating action when no approver exists (non-interactive mode)", async () => {
|
|
10
|
-
const decision = await resolveApprovalDecision("scale_deployment", { deployment: "web", replicas: 0 }, SAFE, undefined);
|
|
11
|
-
expect(decision.execute).toBe(false);
|
|
12
|
-
if (!decision.execute) {
|
|
13
|
-
expect(decision.reason).toMatch(/no approver is available/i);
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
it("never invokes an executor implicitly — caller must gate on execute=false", async () => {
|
|
17
|
-
// Simulates the tool loop: a denied decision must not run the executor.
|
|
18
|
-
const executor = vi.fn();
|
|
19
|
-
const decision = await resolveApprovalDecision("restart_pod", { pod: "x" }, SAFE, undefined);
|
|
20
|
-
if (decision.execute)
|
|
21
|
-
executor();
|
|
22
|
-
expect(executor).not.toHaveBeenCalled();
|
|
23
|
-
});
|
|
24
|
-
it("calls onApproval for non-safe actions and executes when approved", async () => {
|
|
25
|
-
const onApproval = vi.fn().mockResolvedValue(true);
|
|
26
|
-
const decision = await resolveApprovalDecision("scale_deployment", { deployment: "web", replicas: 3 }, SAFE, onApproval);
|
|
27
|
-
expect(onApproval).toHaveBeenCalledWith("scale_deployment", { deployment: "web", replicas: 3 });
|
|
28
|
-
expect(decision.execute).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
it("denies with the user-denial message when onApproval returns false", async () => {
|
|
31
|
-
const onApproval = vi.fn().mockResolvedValue(false);
|
|
32
|
-
const decision = await resolveApprovalDecision("restart_pod", {}, SAFE, onApproval);
|
|
33
|
-
expect(decision.execute).toBe(false);
|
|
34
|
-
if (!decision.execute) {
|
|
35
|
-
expect(decision.reason).toMatch(/denied by user/i);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
it("does not call onApproval for safe actions", async () => {
|
|
39
|
-
const onApproval = vi.fn().mockResolvedValue(false);
|
|
40
|
-
const decision = await resolveApprovalDecision("get_logs", {}, SAFE, onApproval);
|
|
41
|
-
expect(onApproval).not.toHaveBeenCalled();
|
|
42
|
-
expect(decision.execute).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
it("treats user-configured safe_actions as auto-executable", async () => {
|
|
45
|
-
const safe = new Set([...SAFE, "rollout_restart"]);
|
|
46
|
-
const decision = await resolveApprovalDecision("rollout_restart", {}, safe, undefined);
|
|
47
|
-
expect(decision.execute).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Issue } from "../monitor/types.js";
|
|
2
|
-
export type ApprovalDecision = {
|
|
3
|
-
execute: true;
|
|
4
|
-
} | {
|
|
5
|
-
execute: false;
|
|
6
|
-
reason: string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Decide whether a tool call may execute, given the set of auto-safe actions and
|
|
10
|
-
* the (optional) approval callback.
|
|
11
|
-
*
|
|
12
|
-
* Security-critical: a missing/undefined `onApproval` MUST mean DENY for any
|
|
13
|
-
* action that is not in `safeActions`. In non-interactive mode the CLI passes no
|
|
14
|
-
* (or a deny-all) approver, and destructive remediation (e.g. scale-to-0) must
|
|
15
|
-
* never run unattended.
|
|
16
|
-
*/
|
|
17
|
-
export declare function resolveApprovalDecision(toolName: string, params: Record<string, unknown>, safeActions: Set<string>, onApproval?: (action: string, params: Record<string, unknown>) => Promise<boolean>): Promise<ApprovalDecision>;
|
|
18
|
-
export interface DiagnosisResult {
|
|
19
|
-
analysis: string;
|
|
20
|
-
action?: {
|
|
21
|
-
name: string;
|
|
22
|
-
safe: boolean;
|
|
23
|
-
params: Record<string, unknown>;
|
|
24
|
-
};
|
|
25
|
-
verificationContract?: string;
|
|
26
|
-
}
|
|
27
|
-
export declare function diagnose(issues: Issue[], kbDir: string, clusterContext?: string, options?: {
|
|
28
|
-
autoFix?: boolean;
|
|
29
|
-
safeActions?: string[];
|
|
30
|
-
noInteractive?: boolean;
|
|
31
|
-
onApproval?: (action: string, params: Record<string, unknown>) => Promise<boolean>;
|
|
32
|
-
onQuestion?: (question: string, choices: string[] | undefined) => Promise<void>;
|
|
33
|
-
}): Promise<DiagnosisResult>;
|