k8s-av 1.0.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/.env.example +18 -0
- package/dist/cli/docker.d.ts +29 -0
- package/dist/cli/docker.js +241 -0
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +155 -0
- package/dist/cli/scan.d.ts +16 -0
- package/dist/cli/scan.js +151 -0
- package/dist/cli/start.d.ts +20 -0
- package/dist/cli/start.js +261 -0
- package/dist/core/attack-path.d.ts +26 -0
- package/dist/core/attack-path.js +191 -0
- package/dist/core/cve-enricher.d.ts +10 -0
- package/dist/core/cve-enricher.js +175 -0
- package/dist/core/fetcher.d.ts +26 -0
- package/dist/core/fetcher.js +130 -0
- package/dist/core/schema.d.ts +290 -0
- package/dist/core/schema.js +125 -0
- package/dist/core/transformer.d.ts +19 -0
- package/dist/core/transformer.js +510 -0
- package/dist/db/loader.d.ts +16 -0
- package/dist/db/loader.js +261 -0
- package/dist/db/neo4j-client.d.ts +35 -0
- package/dist/db/neo4j-client.js +218 -0
- package/dist/db/queries.d.ts +71 -0
- package/dist/db/queries.js +290 -0
- package/dist/db/test.d.ts +19 -0
- package/dist/db/test.js +268 -0
- package/dist/db/types.d.ts +137 -0
- package/dist/db/types.js +37 -0
- package/dist/schemas/index.d.ts +70 -0
- package/dist/schemas/index.js +43 -0
- package/dist/server/routes/blast.d.ts +3 -0
- package/dist/server/routes/blast.js +44 -0
- package/dist/server/routes/critical.d.ts +3 -0
- package/dist/server/routes/critical.js +80 -0
- package/dist/server/routes/cycles.d.ts +3 -0
- package/dist/server/routes/cycles.js +41 -0
- package/dist/server/routes/graph.d.ts +3 -0
- package/dist/server/routes/graph.js +57 -0
- package/dist/server/routes/ingest.d.ts +3 -0
- package/dist/server/routes/ingest.js +82 -0
- package/dist/server/routes/paths.d.ts +3 -0
- package/dist/server/routes/paths.js +66 -0
- package/dist/server/routes/report.d.ts +3 -0
- package/dist/server/routes/report.js +47 -0
- package/dist/server/routes/simulate.d.ts +3 -0
- package/dist/server/routes/simulate.js +75 -0
- package/dist/server/routes/vulnerabilities.d.ts +3 -0
- package/dist/server/routes/vulnerabilities.js +129 -0
- package/dist/server/server.d.ts +15 -0
- package/dist/server/server.js +136 -0
- package/dist/services/ingestion.service.d.ts +25 -0
- package/dist/services/ingestion.service.js +100 -0
- package/dist/services/report/formatter.d.ts +12 -0
- package/dist/services/report/formatter.js +138 -0
- package/dist/services/report/generator.d.ts +27 -0
- package/dist/services/report/generator.js +68 -0
- package/dist/services/risk-explainer.d.ts +67 -0
- package/dist/services/risk-explainer.js +285 -0
- package/docker/docker-compose.yml +66 -0
- package/package.json +75 -0
- package/ui/index.html +12 -0
- package/ui/package-lock.json +3150 -0
- package/ui/package.json +30 -0
- package/ui/postcss.config.cjs +6 -0
- package/ui/src/App.tsx +37 -0
- package/ui/src/components/Box.tsx +33 -0
- package/ui/src/components/DetailPanel.tsx +239 -0
- package/ui/src/components/RiskBadge.tsx +38 -0
- package/ui/src/components/Sidebar.tsx +107 -0
- package/ui/src/components/graph/CustomNode.tsx +102 -0
- package/ui/src/components/graph/GraphCanvas.tsx +174 -0
- package/ui/src/index.css +48 -0
- package/ui/src/lib/api.ts +161 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/store/useAppStore.ts +168 -0
- package/ui/src/views/CriticalNodeView.tsx +150 -0
- package/ui/src/views/OverviewView.tsx +76 -0
- package/ui/src/views/PathsView.tsx +280 -0
- package/ui/src/views/ReportView.tsx +367 -0
- package/ui/src/views/VulnerabilitiesView.tsx +135 -0
- package/ui/tailwind.config.ts +14 -0
- package/ui/tsconfig.json +20 -0
- package/ui/vite.config.ts +19 -0
package/.env.example
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# ── Neo4j ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Connection URL for the Neo4j Bolt protocol.
|
|
3
|
+
# Default matches the docker/docker-compose.yml service.
|
|
4
|
+
NEO4J_URI=bolt://localhost:7687
|
|
5
|
+
|
|
6
|
+
# Credentials set via NEO4J_AUTH in docker-compose.yml
|
|
7
|
+
NEO4J_USER=neo4j
|
|
8
|
+
NEO4J_PASSWORD=password
|
|
9
|
+
|
|
10
|
+
# ── Express server ─────────────────────────────────────────────────────────────
|
|
11
|
+
PORT=3001
|
|
12
|
+
|
|
13
|
+
# Allowed CORS origin (Vite dev server default)
|
|
14
|
+
CORS_ORIGIN=http://localhost:5173
|
|
15
|
+
|
|
16
|
+
# ── Optional ───────────────────────────────────────────────────────────────────
|
|
17
|
+
# Set to a real kubeconfig path when using --source live
|
|
18
|
+
# KUBECONFIG=/home/user/.kube/config
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docker.ts — Docker detection and Neo4j container lifecycle helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by the `start` and `ingest` CLI commands before they touch Neo4j.
|
|
5
|
+
* The `scan` command is Docker-free and never calls this module.
|
|
6
|
+
*/
|
|
7
|
+
declare const NEO4J_BOLT_PORT = 7687;
|
|
8
|
+
declare const NEO4J_HTTP_PORT = 7474;
|
|
9
|
+
export declare function checkDockerInstalled(): Promise<boolean>;
|
|
10
|
+
export declare function checkDockerRunning(): Promise<boolean>;
|
|
11
|
+
export declare function isNeo4jContainerRunning(): Promise<boolean>;
|
|
12
|
+
export declare function startNeo4j(): Promise<void>;
|
|
13
|
+
export declare function waitForNeo4j(timeoutMs?: number, pollMs?: number): Promise<void>;
|
|
14
|
+
export interface PreflightResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run the full Docker preflight:
|
|
19
|
+
* 1. Docker installed?
|
|
20
|
+
* 2. Docker daemon running?
|
|
21
|
+
* 3. Neo4j container up? (start it if not)
|
|
22
|
+
* 4. Neo4j accepting connections?
|
|
23
|
+
*
|
|
24
|
+
* Prints clear, actionable messages for every failure case.
|
|
25
|
+
* Never throws — returns { ok: false } so the caller can exit cleanly.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runPreflight(): Promise<PreflightResult>;
|
|
28
|
+
export { NEO4J_BOLT_PORT, NEO4J_HTTP_PORT };
|
|
29
|
+
//# sourceMappingURL=docker.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docker.ts — Docker detection and Neo4j container lifecycle helpers.
|
|
4
|
+
*
|
|
5
|
+
* Used by the `start` and `ingest` CLI commands before they touch Neo4j.
|
|
6
|
+
* The `scan` command is Docker-free and never calls this module.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.NEO4J_HTTP_PORT = exports.NEO4J_BOLT_PORT = void 0;
|
|
43
|
+
exports.checkDockerInstalled = checkDockerInstalled;
|
|
44
|
+
exports.checkDockerRunning = checkDockerRunning;
|
|
45
|
+
exports.isNeo4jContainerRunning = isNeo4jContainerRunning;
|
|
46
|
+
exports.startNeo4j = startNeo4j;
|
|
47
|
+
exports.waitForNeo4j = waitForNeo4j;
|
|
48
|
+
exports.runPreflight = runPreflight;
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const util = __importStar(require("util"));
|
|
52
|
+
const exec = util.promisify(child_process_1.exec);
|
|
53
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
54
|
+
const CONTAINER_NAME = 'k8s-attack-neo4j';
|
|
55
|
+
const COMPOSE_DIR = path.resolve(__dirname, '..', '..', 'docker');
|
|
56
|
+
const NEO4J_BOLT_PORT = 7687;
|
|
57
|
+
exports.NEO4J_BOLT_PORT = NEO4J_BOLT_PORT;
|
|
58
|
+
const NEO4J_HTTP_PORT = 7474;
|
|
59
|
+
exports.NEO4J_HTTP_PORT = NEO4J_HTTP_PORT;
|
|
60
|
+
// ─── Logging helpers ─────────────────────────────────────────────────────────
|
|
61
|
+
const ok = (msg) => console.log(` ✔ ${msg}`);
|
|
62
|
+
const warn = (msg) => console.log(` ⚠ ${msg}`);
|
|
63
|
+
const fail = (msg) => console.error(` ✖ ${msg}`);
|
|
64
|
+
const info = (msg) => console.log(` ${msg}`);
|
|
65
|
+
const line = () => console.log(' ' + '─'.repeat(58));
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// PART 1 — Is Docker installed?
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
async function checkDockerInstalled() {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await exec('docker --version');
|
|
72
|
+
const version = stdout.trim().split('\n')[0] ?? 'unknown';
|
|
73
|
+
ok(`Docker installed (${version})`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// PART 2 — Is the Docker daemon running?
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
async function checkDockerRunning() {
|
|
84
|
+
try {
|
|
85
|
+
await exec('docker ps');
|
|
86
|
+
ok('Docker daemon is running');
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// PART 3 — Is the Neo4j container already up?
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
async function isNeo4jContainerRunning() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec(`docker inspect --format "{{.State.Running}}" ${CONTAINER_NAME}`);
|
|
99
|
+
return stdout.trim() === 'true';
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
// PART 4 — Start Neo4j via docker-compose
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
async function startNeo4j() {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const child = (0, child_process_1.spawn)('docker', ['compose', 'up', '-d', '--remove-orphans'], { cwd: COMPOSE_DIR, stdio: 'pipe', shell: process.platform === 'win32' });
|
|
111
|
+
let stderr = '';
|
|
112
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
113
|
+
child.once('close', (code) => {
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
resolve();
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
reject(new Error(`docker compose up failed (exit ${code}):\n${stderr.slice(0, 400)}`));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
child.once('error', reject);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// PART 5 — Poll until Neo4j Bolt port is accepting connections
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
async function waitForNeo4j(timeoutMs = 120000, pollMs = 3000) {
|
|
128
|
+
const deadline = Date.now() + timeoutMs;
|
|
129
|
+
let attempt = 0;
|
|
130
|
+
while (Date.now() < deadline) {
|
|
131
|
+
attempt++;
|
|
132
|
+
try {
|
|
133
|
+
// Use `docker exec` to run a lightweight cypher-shell ping
|
|
134
|
+
await exec(`docker exec ${CONTAINER_NAME} cypher-shell -u neo4j -p password "RETURN 1" --format plain`);
|
|
135
|
+
return; // success
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
const remaining = Math.ceil((deadline - Date.now()) / 1000);
|
|
139
|
+
process.stdout.write(`\r ⏳ Waiting for Neo4j... attempt ${attempt} (${remaining}s left) `);
|
|
140
|
+
await sleep(pollMs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write('\n');
|
|
144
|
+
throw new Error(`Neo4j did not become ready within ${timeoutMs / 1000}s.\n` +
|
|
145
|
+
` Check container logs: docker logs ${CONTAINER_NAME}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Run the full Docker preflight:
|
|
149
|
+
* 1. Docker installed?
|
|
150
|
+
* 2. Docker daemon running?
|
|
151
|
+
* 3. Neo4j container up? (start it if not)
|
|
152
|
+
* 4. Neo4j accepting connections?
|
|
153
|
+
*
|
|
154
|
+
* Prints clear, actionable messages for every failure case.
|
|
155
|
+
* Never throws — returns { ok: false } so the caller can exit cleanly.
|
|
156
|
+
*/
|
|
157
|
+
async function runPreflight() {
|
|
158
|
+
console.log('\n' + '─'.repeat(62));
|
|
159
|
+
console.log(' Docker & Neo4j Preflight');
|
|
160
|
+
console.log('─'.repeat(62));
|
|
161
|
+
// ── 1. Docker installed ───────────────────────────────────────────────────
|
|
162
|
+
const installed = await checkDockerInstalled();
|
|
163
|
+
if (!installed) {
|
|
164
|
+
fail('Docker is not installed.\n');
|
|
165
|
+
info('K8s-AV requires Docker to run Neo4j locally.');
|
|
166
|
+
info('');
|
|
167
|
+
info(' 👉 Install Docker Desktop:');
|
|
168
|
+
info(' https://www.docker.com/products/docker-desktop/');
|
|
169
|
+
info('');
|
|
170
|
+
info(' After installing, start Docker Desktop and re-run:');
|
|
171
|
+
info(' npx k8s-av start');
|
|
172
|
+
info('');
|
|
173
|
+
info(' Or skip Neo4j and run in demo mode:');
|
|
174
|
+
info(' npx k8s-av start --mock');
|
|
175
|
+
line();
|
|
176
|
+
return { ok: false };
|
|
177
|
+
}
|
|
178
|
+
// ── 2. Docker daemon running ──────────────────────────────────────────────
|
|
179
|
+
const running = await checkDockerRunning();
|
|
180
|
+
if (!running) {
|
|
181
|
+
warn('Docker is installed but the daemon is not running.\n');
|
|
182
|
+
info(' Please start Docker Desktop and try again.');
|
|
183
|
+
info('');
|
|
184
|
+
info(' Once running, re-run:');
|
|
185
|
+
info(' npx k8s-av start');
|
|
186
|
+
info('');
|
|
187
|
+
info(' Or skip Neo4j and run in demo mode:');
|
|
188
|
+
info(' npx k8s-av start --mock');
|
|
189
|
+
line();
|
|
190
|
+
return { ok: false };
|
|
191
|
+
}
|
|
192
|
+
// ── 3. Neo4j container ───────────────────────────────────────────────────
|
|
193
|
+
const neo4jUp = await isNeo4jContainerRunning();
|
|
194
|
+
if (neo4jUp) {
|
|
195
|
+
ok(`Neo4j container running (${CONTAINER_NAME})`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
info(`Neo4j container not found — starting it now...`);
|
|
199
|
+
info(` Working directory: ${COMPOSE_DIR}`);
|
|
200
|
+
try {
|
|
201
|
+
await startNeo4j();
|
|
202
|
+
ok('Neo4j container started');
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
fail('Failed to start Neo4j container.\n');
|
|
206
|
+
info(` ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
+
info('');
|
|
208
|
+
info(' Try starting it manually:');
|
|
209
|
+
info(' cd docker && docker compose up -d');
|
|
210
|
+
info('');
|
|
211
|
+
info(' Or run in demo mode (no Neo4j required):');
|
|
212
|
+
info(' npx k8s-av start --mock');
|
|
213
|
+
line();
|
|
214
|
+
return { ok: false };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── 4. Wait for Neo4j to be ready ────────────────────────────────────────
|
|
218
|
+
info(`Neo4j ports: Bolt :${NEO4J_BOLT_PORT} Browser :${NEO4J_HTTP_PORT}`);
|
|
219
|
+
try {
|
|
220
|
+
await waitForNeo4j(120000);
|
|
221
|
+
process.stdout.write('\n'); // clear the spinner line
|
|
222
|
+
ok('Neo4j is ready');
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
process.stdout.write('\n');
|
|
226
|
+
fail('Neo4j did not become ready in time.\n');
|
|
227
|
+
info(` ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
info('');
|
|
229
|
+
info(' View logs: docker logs ' + CONTAINER_NAME);
|
|
230
|
+
info(' Try again: npx k8s-av start');
|
|
231
|
+
line();
|
|
232
|
+
return { ok: false };
|
|
233
|
+
}
|
|
234
|
+
line();
|
|
235
|
+
return { ok: true };
|
|
236
|
+
}
|
|
237
|
+
// ─── Utility ─────────────────────────────────────────────────────────────────
|
|
238
|
+
function sleep(ms) {
|
|
239
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kubernetes Attack Path Visualizer — CLI Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* scan — fetch + transform + enrich → write cluster-graph.json (no Neo4j)
|
|
7
|
+
* ingest — full pipeline: scan → load Neo4j → re-project GDS
|
|
8
|
+
* report — generate + print attack report from Neo4j
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx ts-node src/cli/index.ts scan --mock
|
|
12
|
+
* npx ts-node src/cli/index.ts ingest --source mock
|
|
13
|
+
* npx ts-node src/cli/index.ts report --format text
|
|
14
|
+
*/
|
|
15
|
+
import 'dotenv/config';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Kubernetes Attack Path Visualizer — CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* scan — fetch + transform + enrich → write cluster-graph.json (no Neo4j)
|
|
8
|
+
* ingest — full pipeline: scan → load Neo4j → re-project GDS
|
|
9
|
+
* report — generate + print attack report from Neo4j
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx ts-node src/cli/index.ts scan --mock
|
|
13
|
+
* npx ts-node src/cli/index.ts ingest --source mock
|
|
14
|
+
* npx ts-node src/cli/index.ts report --format text
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
require("dotenv/config");
|
|
18
|
+
const commander_1 = require("commander");
|
|
19
|
+
const scan_1 = require("./scan");
|
|
20
|
+
const start_1 = require("./start");
|
|
21
|
+
const ingestion_service_1 = require("../services/ingestion.service");
|
|
22
|
+
const loader_1 = require("../db/loader");
|
|
23
|
+
const queries_1 = require("../db/queries");
|
|
24
|
+
const neo4j_client_1 = require("../db/neo4j-client");
|
|
25
|
+
const generator_1 = require("../services/report/generator");
|
|
26
|
+
const formatter_1 = require("../services/report/formatter");
|
|
27
|
+
const docker_1 = require("./docker");
|
|
28
|
+
const program = new commander_1.Command();
|
|
29
|
+
program
|
|
30
|
+
.name('k8s-attack-viz')
|
|
31
|
+
.description('Kubernetes RBAC Attack Path Visualizer\n' +
|
|
32
|
+
'Ingests cluster data, builds an RBAC graph, enriches with CVEs,\n' +
|
|
33
|
+
'detects attack paths from entry points to crown jewels.')
|
|
34
|
+
.version('1.0.0', '-v, --version');
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// scan — local pipeline only (no Neo4j)
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
program
|
|
39
|
+
.command('scan')
|
|
40
|
+
.description('Scan a Kubernetes cluster and output an attack-path graph (no Neo4j)')
|
|
41
|
+
.option('--mock', 'Use bundled mock data instead of kubectl', false)
|
|
42
|
+
.option('--output <file>', 'Path for the output JSON file', 'cluster-graph.json')
|
|
43
|
+
.option('--skip-cve', 'Skip CVE enrichment', false)
|
|
44
|
+
.option('--verbose', 'Print all attack paths', false)
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
try {
|
|
47
|
+
await (0, scan_1.runScan)({ mock: opts.mock, output: opts.output, skipCve: opts.skipCve, verbose: opts.verbose });
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error(`\n❌ Scan failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
// ingest — full pipeline: scan → Neo4j → GDS
|
|
57
|
+
// Reuses same services as POST /api/ingest
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
program
|
|
60
|
+
.command('ingest')
|
|
61
|
+
.description('Full ingestion: fetch cluster data → load Neo4j → re-project GDS')
|
|
62
|
+
.option('--source <source>', 'Data source: mock | live', 'mock')
|
|
63
|
+
.option('--skip-cve', 'Skip CVE enrichment (faster)', false)
|
|
64
|
+
.option('--wipe', 'Wipe existing Neo4j graph before loading', false)
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
const source = opts.source === 'live' ? 'live' : 'mock';
|
|
67
|
+
try {
|
|
68
|
+
console.log('\n' + '═'.repeat(60));
|
|
69
|
+
console.log(' 🔷 K8s Attack Path Visualizer — Ingest');
|
|
70
|
+
console.log('═'.repeat(60));
|
|
71
|
+
// ── Docker + Neo4j preflight ──────────────────────────────────────────
|
|
72
|
+
const preflight = await (0, docker_1.runPreflight)();
|
|
73
|
+
if (!preflight.ok)
|
|
74
|
+
process.exit(1);
|
|
75
|
+
// Verify Neo4j driver connection
|
|
76
|
+
console.log('\n Connecting to Neo4j...');
|
|
77
|
+
await (0, neo4j_client_1.verifyConnection)();
|
|
78
|
+
// ── Step 1: ingestCluster (Teammate 1) ───────────────────────────────
|
|
79
|
+
console.log('\n [1/3] Ingesting cluster data...');
|
|
80
|
+
const ingestResult = await (0, ingestion_service_1.ingestCluster)({ source, skipCve: opts.skipCve });
|
|
81
|
+
console.log(` ✔ Graph JSON written: ${ingestResult.nodes} nodes, ${ingestResult.edges} edges`);
|
|
82
|
+
// ── Step 2: loadGraph (Teammate 2) ───────────────────────────────────
|
|
83
|
+
console.log('\n [2/3] Loading graph into Neo4j...');
|
|
84
|
+
const stats = await (0, loader_1.loadGraph)(ingestResult.graphPath, opts.wipe);
|
|
85
|
+
console.log(` ✔ Neo4j: ${stats.nodesLoaded} nodes, ${stats.edgesLoaded} edges (${stats.durationMs}ms)`);
|
|
86
|
+
// ── Step 3: Re-project GDS ────────────────────────────────────────────
|
|
87
|
+
console.log('\n [3/3] Projecting GDS graph...');
|
|
88
|
+
await (0, queries_1.ensureProjection)(true);
|
|
89
|
+
console.log(' ✔ GDS projection ready');
|
|
90
|
+
console.log('\n' + '─'.repeat(60));
|
|
91
|
+
console.log(' ✔ Ingestion complete. Run `report` to analyse.\n');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error(`\n❌ Ingest failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// report — generate + print attack report
|
|
101
|
+
// Reuses same generator + formatter as GET /api/report
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
program
|
|
104
|
+
.command('report')
|
|
105
|
+
.description('Generate a full attack-path report from Neo4j data')
|
|
106
|
+
.option('--format <format>', 'Output format: text | json', 'text')
|
|
107
|
+
.action(async (opts) => {
|
|
108
|
+
const format = opts.format === 'json' ? 'json' : 'text';
|
|
109
|
+
try {
|
|
110
|
+
console.log('\n Connecting to Neo4j...');
|
|
111
|
+
await (0, neo4j_client_1.verifyConnection)();
|
|
112
|
+
console.log(' Generating report...\n');
|
|
113
|
+
const data = await (0, generator_1.generateReport)();
|
|
114
|
+
// Same formatter used by the API — no duplication
|
|
115
|
+
const output = (0, formatter_1.formatReport)(data, format);
|
|
116
|
+
console.log(output);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`\n❌ Report failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// start — full system orchestrator: backend + ingest + UI + browser
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
program
|
|
128
|
+
.command('start')
|
|
129
|
+
.description('Start backend, load mock data, open the UI in your browser')
|
|
130
|
+
.option('--source <source>', 'Data source: mock | live', 'mock')
|
|
131
|
+
.option('--no-browser', 'Skip opening the browser automatically')
|
|
132
|
+
.action(async (opts) => {
|
|
133
|
+
try {
|
|
134
|
+
await (0, start_1.runStart)({
|
|
135
|
+
source: opts.source === 'live' ? 'live' : 'mock',
|
|
136
|
+
skipBrowser: !opts.browser,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error(`\n❌ Start failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
// Fallback
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
program.on('command:*', () => {
|
|
148
|
+
console.error(`Unknown command: ${program.args.join(' ')}`);
|
|
149
|
+
program.help();
|
|
150
|
+
});
|
|
151
|
+
if (process.argv.length <= 2) {
|
|
152
|
+
program.help();
|
|
153
|
+
}
|
|
154
|
+
program.parse(process.argv);
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ScanOptions {
|
|
2
|
+
/** Load data from mock JSON instead of running kubectl */
|
|
3
|
+
mock: boolean;
|
|
4
|
+
/** Destination file path for the cluster-graph.json output */
|
|
5
|
+
output: string;
|
|
6
|
+
/** Skip CVE enrichment (faster runs, no network required) */
|
|
7
|
+
skipCve?: boolean;
|
|
8
|
+
/** Print verbose attack-path report including alternate routes */
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrates the full ingestion → transformation → enrichment →
|
|
13
|
+
* validation → output pipeline.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runScan(options: ScanOptions): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=scan.d.ts.map
|
package/dist/cli/scan.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runScan = runScan;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const fetcher_1 = require("../core/fetcher");
|
|
40
|
+
const transformer_1 = require("../core/transformer");
|
|
41
|
+
const cve_enricher_1 = require("../core/cve-enricher");
|
|
42
|
+
const schema_1 = require("../core/schema");
|
|
43
|
+
const attack_path_1 = require("../core/attack-path");
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// LOGGING HELPERS
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
function step(label) {
|
|
48
|
+
console.log(`\n✔ ${label}`);
|
|
49
|
+
}
|
|
50
|
+
function divider() {
|
|
51
|
+
console.log(' ' + '─'.repeat(60));
|
|
52
|
+
}
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// MAIN SCAN PIPELINE
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Orchestrates the full ingestion → transformation → enrichment →
|
|
58
|
+
* validation → output pipeline.
|
|
59
|
+
*/
|
|
60
|
+
async function runScan(options) {
|
|
61
|
+
console.log('\n' + '═'.repeat(62));
|
|
62
|
+
console.log(' 🔐 Kubernetes Attack Path Visualizer');
|
|
63
|
+
console.log(' RBAC Graph Ingestion & CVE Enrichment Pipeline');
|
|
64
|
+
console.log('═'.repeat(62));
|
|
65
|
+
// ── Step 1: Fetch ──────────────────────────────────────────────────────────
|
|
66
|
+
step('Fetching cluster data...');
|
|
67
|
+
const rawData = await (0, fetcher_1.fetchClusterData)(options.mock);
|
|
68
|
+
const podCount = (rawData.pods?.items ?? []).length;
|
|
69
|
+
const saCount = (rawData.serviceAccounts?.items ?? []).length;
|
|
70
|
+
const roleCount = (rawData.roles?.items ?? []).length +
|
|
71
|
+
(rawData.clusterRoles?.items ?? []).length;
|
|
72
|
+
const bindingCount = (rawData.roleBindings?.items ?? []).length +
|
|
73
|
+
(rawData.clusterRoleBindings?.items ?? []).length;
|
|
74
|
+
console.log(` → Pods: ${podCount} | ServiceAccounts: ${saCount} | ` +
|
|
75
|
+
`Roles: ${roleCount} | Bindings: ${bindingCount}`);
|
|
76
|
+
// ── Step 2: Transform ─────────────────────────────────────────────────────
|
|
77
|
+
step('Transforming RBAC graph...');
|
|
78
|
+
let graph = (0, transformer_1.transformToGraph)(rawData);
|
|
79
|
+
console.log(` → Built ${graph.nodes.length} nodes and ${graph.edges.length} edges`);
|
|
80
|
+
const entryPts = graph.nodes.filter((n) => n.isEntryPoint).length;
|
|
81
|
+
const crownJs = graph.nodes.filter((n) => n.isCrownJewel).length;
|
|
82
|
+
console.log(` → Entry points: ${entryPts} | Crown jewels: ${crownJs}`);
|
|
83
|
+
// ── Step 3: CVE Enrichment ────────────────────────────────────────────────
|
|
84
|
+
if (options.skipCve) {
|
|
85
|
+
console.log('\n✔ CVE enrichment skipped (--skip-cve)');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
step('Enriching with CVE data...');
|
|
89
|
+
graph = await (0, cve_enricher_1.enrichWithCVE)(graph);
|
|
90
|
+
const enriched = graph.nodes.filter((n) => (n.cve?.length ?? 0) > 0).length;
|
|
91
|
+
console.log(` → ${enriched} pod(s) enriched with CVE data`);
|
|
92
|
+
}
|
|
93
|
+
// ── Step 4: Attack Path Detection ─────────────────────────────────────────
|
|
94
|
+
step('Detecting attack paths...');
|
|
95
|
+
let attackPaths;
|
|
96
|
+
if (options.verbose) {
|
|
97
|
+
const report = (0, attack_path_1.generateFullAttackReport)(graph);
|
|
98
|
+
attackPaths = report.paths;
|
|
99
|
+
console.log(` → Total paths : ${report.summary.totalPaths}`);
|
|
100
|
+
console.log(` → Critical (≥7) : ${report.summary.criticalPaths}`);
|
|
101
|
+
console.log(` → Avg hops : ${report.summary.avgHops}`);
|
|
102
|
+
(0, attack_path_1.printAttackPaths)(attackPaths, 10);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
attackPaths = (0, attack_path_1.detectAttackPaths)(graph);
|
|
106
|
+
console.log(` → ${attackPaths.length} attack path(s) detected`);
|
|
107
|
+
if (attackPaths.length > 0) {
|
|
108
|
+
const top = attackPaths[0];
|
|
109
|
+
console.log(` → Highest risk : ${top.entryPoint} → ${top.crownJewel}` +
|
|
110
|
+
` (${top.hops} hops, risk ${top.riskScore.toFixed(2)}/10)`);
|
|
111
|
+
(0, attack_path_1.printAttackPaths)(attackPaths, 6);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
graph = { ...graph, attackPaths };
|
|
115
|
+
// ── Step 5: Validation ────────────────────────────────────────────────────
|
|
116
|
+
step('Validating schema...');
|
|
117
|
+
const finalGraph = {
|
|
118
|
+
...graph,
|
|
119
|
+
metadata: {
|
|
120
|
+
generatedAt: new Date().toISOString(),
|
|
121
|
+
clusterContext: options.mock
|
|
122
|
+
? 'mock'
|
|
123
|
+
: process.env['KUBECONFIG'] ?? 'default',
|
|
124
|
+
totalNodes: graph.nodes.length,
|
|
125
|
+
totalEdges: graph.edges.length,
|
|
126
|
+
totalAttackPaths: attackPaths.length,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const validated = (0, schema_1.validateGraph)(finalGraph);
|
|
130
|
+
// ── Step 6: Persist ───────────────────────────────────────────────────────
|
|
131
|
+
step('Saving graph...');
|
|
132
|
+
const outputPath = path.resolve(options.output);
|
|
133
|
+
const outputDir = path.dirname(outputPath);
|
|
134
|
+
if (!fs.existsSync(outputDir)) {
|
|
135
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
fs.writeFileSync(outputPath, JSON.stringify(validated, null, 2), 'utf8');
|
|
138
|
+
console.log(` → Saved to: ${outputPath}`);
|
|
139
|
+
// ── Final Summary ─────────────────────────────────────────────────────────
|
|
140
|
+
divider();
|
|
141
|
+
console.log('\n 📊 Final Summary\n');
|
|
142
|
+
console.log(` Nodes : ${validated.nodes.length}`);
|
|
143
|
+
console.log(` Edges : ${validated.edges.length}`);
|
|
144
|
+
console.log(` Attack Paths : ${attackPaths.length}`);
|
|
145
|
+
console.log(` Entry Points : ${validated.nodes.filter((n) => n.isEntryPoint).length}`);
|
|
146
|
+
console.log(` Crown Jewels : ${validated.nodes.filter((n) => n.isCrownJewel).length}`);
|
|
147
|
+
console.log(` CVE-Enriched : ${validated.nodes.filter((n) => (n.cve?.length ?? 0) > 0).length}`);
|
|
148
|
+
console.log(`\n ✔ cluster-graph.json written to: ${outputPath}\n`);
|
|
149
|
+
divider();
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start.ts — CLI orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Sequence:
|
|
5
|
+
* 0. Docker + Neo4j preflight (skipped with --mock)
|
|
6
|
+
* 1. Spawn Express backend (ts-node or node dist/server/server.js)
|
|
7
|
+
* 2. Poll /health until ready (60s timeout)
|
|
8
|
+
* 3. POST /api/ingest (load cluster data)
|
|
9
|
+
* 4. Ensure UI deps installed (npm install inside ui/ if needed)
|
|
10
|
+
* 5. Spawn Vite frontend (npm run dev inside ui/)
|
|
11
|
+
* 6. Wait for Vite (poll localhost:5173, 30s timeout)
|
|
12
|
+
* 7. Open browser
|
|
13
|
+
* 8. Park — Ctrl+C kills both children cleanly
|
|
14
|
+
*/
|
|
15
|
+
export interface StartOptions {
|
|
16
|
+
skipBrowser: boolean;
|
|
17
|
+
source: 'mock' | 'live';
|
|
18
|
+
}
|
|
19
|
+
export declare function runStart(opts: StartOptions): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=start.d.ts.map
|