├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── README.md
├── assets
├── Team
│ ├── Anna.png
│ ├── Cortland.png
│ ├── DAWG.png
│ ├── IMG_0293_jpg.jpg:Zone.Identifier
│ ├── Jimmy.jpg
│ ├── Owen2.png
│ └── Weston.png
└── brizo-high-resolution-color-logo.png
├── functions.ts
├── k6
└── script.js
├── package-lock.json
├── package.json
├── prometheusQueries.ts
├── react-dom-client.d.ts
├── src
├── client
│ ├── App.tsx
│ ├── components
│ │ ├── CISConfigResult.tsx
│ │ ├── CpuUsageChart.tsx
│ │ ├── DynamicPromComponent.tsx
│ │ ├── GrandCISResults.tsx
│ │ ├── Loading.tsx
│ │ ├── MainContainer.tsx
│ │ ├── MemoryUsageChart.tsx
│ │ ├── NavbarComponent.tsx
│ │ ├── NodeCard.tsx
│ │ ├── PodCard.tsx
│ │ ├── StaticPromComponent.tsx
│ │ ├── ViewCluster.tsx
│ │ ├── ViewNamespace.tsx
│ │ └── ViewStructure.tsx
│ ├── css
│ │ ├── main.scss
│ │ ├── normalize.css
│ │ └── skeleton.css
│ ├── index.html
│ └── index.tsx
└── server
│ ├── controllers
│ ├── clusterController.ts
│ ├── k6Controller.ts
│ ├── prometheusController.ts
│ └── securityController.ts
│ ├── routers
│ ├── apiRouter.ts
│ ├── clusterRouter.ts
│ ├── k6Router.ts
│ ├── prometheusRouter.ts
│ └── securityRouter.ts
│ └── server.ts
├── tests
└── cis
│ ├── job-eks.yaml
│ └── job.yaml
├── tsconfig.json
├── types.ts
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/*
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true,
6 | "node": true
7 | },
8 | "extends": [
9 | "plugin:react/recommended",
10 | "standard-with-typescript",
11 | "plugin:@typescript-eslint/eslint-recommended",
12 | "plugin:@typescript-eslint/recommended"
13 | ],
14 | "overrides": [
15 | ],
16 | "ignorePatterns": [
17 | "k6/script.js"
18 | ],
19 | "parserOptions": {
20 | "ecmaVersion": "latest",
21 | "project": ["tsconfig.json"]
22 | },
23 | "plugins": [
24 | "react",
25 | "@typescript-eslint"
26 | ],
27 | "rules": {
28 | "@typescript-eslint/explicit-function-return-type": "off",
29 | "semi": ["error", "always"],
30 | "@typescript-eslint/semi": ["off"],
31 | "@typescript-eslint/no-non-null-assertion": ["off"]
32 | },
33 | "settings": {
34 | "react": {
35 | "version": "detect"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | output.txt
5 | go.mod
6 | go.sum
7 | kubeBenchRunner.go
8 | output.json
9 | TODO
10 | eks-setup
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Brizo
2 |
3 | 
4 |
5 | Brizo is a lightweight developer tool built from scratch to help developers monitor their Kubernetes (K8) clusters. Prometheus and ChartJS work together to monitor and display important cluster health metrics for a K8s cluster. Brizo also runs your cluster through CIS security testing standards to ensure proper cluster security.
6 |
7 |
51 |
52 |
53 | ## Table of Contents
54 |
55 | 1. [Brizo](#Brizo)
56 | 2. [Features](#features)
57 | 3. [Benefits](#benefits)
58 | 4. [Privacy Statement](#privacy-statement)
59 | 5. [Installation](#installation)
60 | 6. [Scripts](#scripts)
61 | 7. [File Structure](#file-structure)
62 | 8. [Our Team](#our-team)
63 | 9. [License](#license)
64 |
65 | ## Features
66 |
67 | Brizo offers several key features that make it a valuable tool for kubernetes cluster management:
68 |
69 | 1. **K8s Cluster Structure Display**: Brizo automatically generates a visual display of your K8s cluster structure, saving you the effort of tracking your cluster manually in the terminal. Navigate between namespaces with ease to see your deployed Nodes and PODs.
70 |
71 | 2. **Compatibility with Multiple Environments**: Brizo is compatible with multiple developer environments, including macOS, Windows, and Linux. Instructions based on your dev environment can be found in the [Installation](#installation) section.
72 |
73 | 3. **Security Testing**: Brizo compares your cluster configuration to the CIS security standards, ensuring proper setup for developers. Brizo also offers remediations to address any of the test warnings/failures.
74 |
75 | 4. **Autoscale Testing**: Brizo works with Grafana Cloud k6 services to artificially create traffic spikes and monitor your cluster's responsiveness, which helps developers identify potential bottlenecks in their cluster configuration during the development phase.
76 |
77 |
78 |
79 | ## Benefits
80 |
81 | By using Brizo, developers can enjoy numerous benefits, such as:
82 |
83 | 1. **Streamlined Cluster Testing**: Brizo automates the security benchmarking of your cluster.
84 |
85 | 2. **Improved Cluster Structure for Autoscaling**: Brizo helps developers identify potential bottlenecks in their K8s cluster, which helps improve autoscaling capabilities.
86 |
87 | 3. **Intuitive Dashboards**: Brizo offers easy to read, live data charts to visually represent important cluster metrics scraped by Prometheus.
88 |
89 | ## Privacy Statement
90 |
91 | Brizo scrapes your K8s cluster for the purpose of displaying metrics and running CIS security protocol benchmark tests. The application does not extract or store any personal data from users. However, as a precaution, developers should avoid using sensitive information when generating clusters. This ensures that no sensitive data is inadvertently recorded or stored in the database.
92 |
93 | ## Installation
94 |
95 | 1. Ensure you have the required prerequisites installed:
96 | - [npm](https://www.npmjs.com/)
97 | - [kubectl](https://kubernetes.io/docs/tasks/tools/)
98 | - [EKS Cluster](#EKS) or [Minikube Cluster](#Minikube)
99 | - [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/)
100 | - [Grafana Cloud](https://grafana.com/products/cloud/)
101 |
102 | 2. Fork the Brizo repository to your own GitHub account.
103 | 3. Clone your forked repository to your local machine.
104 |
105 | ```bash
106 | git clone https://github.com//brizo.git
107 | ```
108 |
109 | 4. Navigate to the root project directory and install dependencies.
110 |
111 | ```bash
112 | cd brizo
113 | npm install
114 | ```
115 |
116 | ## EKS-Setup
117 | 1. Create an [EKS Cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html)
118 | - Make sure your instance matches cpu architecture
119 | 2. Set up [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
120 | 3. Connect [Prometheus](https://docs.aws.amazon.com/eks/latest/userguide/prometheus.html)
121 | 4. Expose the Prometheus service:
122 | - ```kubectl --namespace=prometheus port-forward deploy/prometheus-server 9090```
123 | 5. Connect [Grafana](https://aws.amazon.com/quickstart/architecture/eks-grafana/)
124 | 6. Expose the Grafana service:
125 | - ```kubectl port-forward -n grafana svc/grafana 30381:80```
126 | 7. Build the [Kube-Bench](#Kube-Bench-EKS) Job
127 |
128 | ## Minikube Setup
129 | 1. Create a [Minkube Cluster](https://kubernetes.io/docs/tutorials/kubernetes-basics/create-cluster/cluster-intro/)
130 | 2. Install [Helm](https://helm.sh/docs/intro/install/)
131 | 3. Create a Prometheus service with [Helm](https://helm.sh/docs/intro/install/)
132 | - ```helm repo add prometheus-community https://prometheus-community.github.io/helm-charts```
133 | - ```helm repo update```
134 | - ```helm install prometheus prometheus-community/prometheus```
135 | - ```kubectl expose service prometheus-server --type=NodePort --target-port=9090 --name=prometheus-server-ext```
136 | - ```minikube service prometheus-server-ext```
137 | 4. Create a Grafana service with [Helm](https://helm.sh/docs/intro/install/)
138 | - ```helm repo add grafana https://grafana.github.io/helm-charts```
139 | - ```helm repo update```
140 | - ```helm install grafana grafana/grafana```
141 | - ```kubectl expose service grafana --type=NodePort --target-port=3000 --name=grafana-ext```
142 | - ```minikube service grafana-ext```
143 | 5. Get username and password for Grafana login
144 | - ```kubectl get secret --namespace default grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo```
145 |
146 | ## Kube-Bench-EKS
147 | 1. ```aws ecr create-repository --repository-name k8s/kube-bench --image-tag-mutability MUTABLE```
148 | - ```git clone https://github.com/aquasecurity/kube-bench.git```
149 | - ```cd kube-bench```
150 | - ```aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com```
151 | - ```docker build -t kube-bench kube-bench```
152 | - ```docker tag kube-bench:latest .dkr.ecr..amazonaws.com/k8s/kube-bench:latest```
153 | - ```docker push .dkr.ecr..amazonaws.com/k8s/kube-bench:latest```
154 | - ```eksctl create iamidentitymapping --cluster --region= --arn arn:aws:iam:::user/ --group system:masters --username admin```
155 |
156 | ## Scripts
157 |
158 | Below are descriptions of each npm script:
159 |
160 | - `npm run dev`: Starts the development server using Nodemon
161 |
162 | ## File Structure
163 |
164 | ```
165 | |____assets
166 | | |____brizo-high-resolution-color-logo.png
167 | | |____Team
168 | | | |____Anna.png
169 | | | |____Cortland.png
170 | | | |____DAWG.png
171 | | | |____Owen.png
172 | | | |____Weston.png
173 | |____kbcommands.md
174 | |____output.txt
175 | |____.eslintrc.json
176 | |____functions.ts
177 | |____src
178 | | |____server
179 | | | |____routers
180 | | | | |____apiRouter.ts
181 | | | | |____securityRouter.ts
182 | | | | |____clusterRouter.ts
183 | | | | |____prometheusRouter.ts
184 | | | | |____k6Router.ts
185 | | | |____controllers
186 | | | | |____k6Controller.ts
187 | | | | |____securityController.ts
188 | | | | |____clusterController.ts
189 | | | | |____prometheusController.ts
190 | | | |____server.ts
191 | | |____client
192 | | | |____index.tsx
193 | | | |____App.tsx
194 | | | |____index.html
195 | | | |____css
196 | | | | |____main.scss
197 | | | | |____skeleton.css
198 | | | | |____normalize.css
199 | | | |____components
200 | | | | |____MemoryUsageChart.tsx
201 | | | | |____ViewNamespace.tsx
202 | | | | |____GrandCISResults.tsx
203 | | | | |____ViewStructure.tsx
204 | | | | |____PodCard.tsx
205 | | | | |____NavbarComponent.tsx
206 | | | | |____NodeCard.tsx
207 | | | | |____MainContainer.tsx
208 | | | | |____StaticPromComponent.tsx
209 | | | | |____CpuUsageChart.tsx
210 | | | | |____ViewCluster.tsx
211 | | | | |____DynamicPromComponent.tsx
212 | | | | |____Loading.tsx
213 | | | | |____CISConfigResult.tsx
214 | ```
215 |
216 | ## Our Team
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 | Cortland Young
225 | GitHub
226 | LinkedIn
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | Owen Hill
235 | GitHub
236 | LinkedIn
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 | Jimmy Tran
245 | GitHub
246 | LinkedIn
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 | Weston Schott
255 | GitHub
256 | LinkedIn
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | Anna Yu
265 | GitHub
266 | LinkedIn
267 |
268 |
269 |
270 |
271 |
272 | ## License
273 |
274 | This project is licensed under the terms of the [MIT LICENSE](./LICENSE).
275 |
--------------------------------------------------------------------------------
/assets/Team/Anna.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/Anna.png
--------------------------------------------------------------------------------
/assets/Team/Cortland.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/Cortland.png
--------------------------------------------------------------------------------
/assets/Team/DAWG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/DAWG.png
--------------------------------------------------------------------------------
/assets/Team/IMG_0293_jpg.jpg:Zone.Identifier:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/IMG_0293_jpg.jpg:Zone.Identifier
--------------------------------------------------------------------------------
/assets/Team/Jimmy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/Jimmy.jpg
--------------------------------------------------------------------------------
/assets/Team/Owen2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/Owen2.png
--------------------------------------------------------------------------------
/assets/Team/Weston.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/Team/Weston.png
--------------------------------------------------------------------------------
/assets/brizo-high-resolution-color-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Brizo/60e36a84750d3be792e8954237beb127e88b0021/assets/brizo-high-resolution-color-logo.png
--------------------------------------------------------------------------------
/functions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Takes a status string with possible inputs 'PASS', 'FAIL', 'WARN', or 'INFO' as input and returns the corresponding color code based on the status.
3 | * @param {string} status - The `status` parameter is a string that represents the status of the CIS test(s).
4 | */
5 | export const statusToColor = (status: string) => {
6 | let textColor;
7 | if (status === 'PASS') textColor = '#90ee90';
8 | else if (status === 'FAIL') textColor = 'red';
9 | else if (status === 'WARN') textColor = '#ffd500';
10 | else if (status === 'INFO') textColor = '#787878';
11 | return textColor;
12 | };
13 | /**
14 | * Takes a status string with possible inputs 'Running', 'Succeeded', 'Pending', or nothing ('Failed/Unknown') as input and returns the corresponding color code based on the status.
15 | * @param {string} status - The `status` parameter is a string that represents the status of the Pod phase.
16 | */
17 | export const podPhaseStatusToColor = (phase: string) => {
18 | let textColor;
19 | if (phase === 'Running' || phase === 'Succeeded') textColor = 'green';
20 | else if (phase === 'Pending') textColor = 'yellow';
21 | else textColor = 'red';
22 | return textColor;
23 | };
24 |
25 | /**
26 | * Takes a string as input and returns the parsed integer value of that string.
27 | * @param {string} resultText - A string that represents the numerical value(s) of the CIS test(s).
28 | */
29 | export const passedNumberFromTest = (resultText: string) => parseInt(resultText);
30 |
31 | /**
32 | * Extracts the first all uppercase word from the status. All statuses contain ['PASS'], ['FAIL'], ['WARN'], or ['INFO']
33 | * @param {string} resultStatus - The `resultStatus` parameter is a string that represents the status of a test.
34 | */
35 | export const checkStatusFromTest = (resultStatus: string) => resultStatus.match(/[A-Z]+/g)![0];
36 |
37 | /**
38 | * Converts a value in bytes to megabytes.
39 | * @param {number | undefined} bytesValue - The `bytesValue` parameter is a number representing the size in bytes.
40 | */
41 | export const convertBytesToMB = (bytesValue: number | undefined) => {
42 | if (bytesValue === undefined) return 0;
43 | return Math.floor(bytesValue * 0.000001);
44 | };
45 |
46 | /**
47 | * Converts a value in bytes to gigabytes.
48 | * @param {number | undefined} bytesValue - The `bytesValue` parameter is a number representing the size in bytes.
49 | */
50 | export const convertBytesToGB = (bytesValue: number | undefined) => {
51 | if (bytesValue === undefined) return 0;
52 | return Math.floor(bytesValue * 0.000000001);
53 | };
54 |
55 | /**
56 | * Converts a value in bytes to gigabytes.
57 | * @param {number | undefined} bytesValue - The `bytesValue` parameter is a number representing the size in bytes.
58 | * @param {number | undefined} decimalPlace - The `decimalPlace` parameter is a number representing the number of values after the decimal place that show.
59 | */
60 | export const convertBytesToGBDecimal = (bytesValue: number | undefined, decimalPlace: number | undefined) => {
61 | if (bytesValue === undefined) return 0;
62 | return (bytesValue * 0.000000001).toFixed(decimalPlace);
63 | };
64 |
--------------------------------------------------------------------------------
/k6/script.js:
--------------------------------------------------------------------------------
1 | import http from 'k6/http'
2 | import { sleep } from 'k6'
3 |
4 | export const options = {
5 | scenarios: {
6 | shared_iter_scenario: {
7 | executor: 'shared-iterations',
8 | vus: 10,
9 | iterations: 100,
10 | startTime: '0s'
11 | },
12 | per_vu_scenario: {
13 | executor: 'per-vu-iterations',
14 | vus: 10,
15 | iterations: 10,
16 | startTime: '10s'
17 | }
18 | },
19 | ext: {
20 | loadimpact: {
21 | // Project: Default project
22 | projectID: 3646357,
23 | // Test runs with the same name groups test runs together
24 | name: 'DU MA Test',
25 | apm: [
26 | {
27 | provider: 'prometheus',
28 | remoteWriteURL: 'http://10.100.45.157:19090/api/v1/write',
29 | includeDefaultMetrics: true,
30 | includeTestRunId: true,
31 | resampleRate: 3
32 | },
33 | {
34 | provider: 'prometheus',
35 | remoteWriteURL: 'https://prometheus-prod-36-prod-us-west-0.grafana.net/api/prom/push',
36 | // optional parameters
37 | credentials: {
38 | token: '',
39 | // insert your token here
40 | },
41 | includeDefaultMetrics: true,
42 | metrics: ['http_req_sending', 'my_rate', 'my_gauge'], //...other options,
43 | includeDefaultMetrics: true,
44 | includeTestRunId: true,
45 | resampleRate: 3,
46 | },
47 | ]
48 | }
49 | }
50 | }
51 |
52 | export default function () {
53 | http.get('http://test.k6.io/');
54 | sleep(5);
55 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brizo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.ts",
6 | "scripts": {
7 | "build": "npx tsc",
8 | "start": "node dist/src/server/server.js",
9 | "dev": "nodemon ./src/server/server.ts & webpack-dev-server"
10 | },
11 | "dependencies": {
12 | "@kubernetes/client-node": "^0.18.1",
13 | "@types/axios": "^0.14.0",
14 | "@types/react": "^18.2.12",
15 | "@types/react-dom": "^18.2.5",
16 | "axios": "^1.4.0",
17 | "chart.js": "^4.3.0",
18 | "child_process": "^1.0.2",
19 | "css-loader": "^6.8.1",
20 | "dotenv": "^16.1.4",
21 | "express": "^4.18.2",
22 | "express-prom-bundle": "^6.6.0",
23 | "fs": "^0.0.1-security",
24 | "html-webpack-plugin": "^5.5.3",
25 | "node": "^20.2.0",
26 | "nodemon": "^2.0.22",
27 | "path": "^0.12.7",
28 | "prom-client": "^14.2.0",
29 | "react": "^18.2.0",
30 | "react-chartjs-2": "^5.2.0",
31 | "react-dom": "^18.2.0",
32 | "react-router-dom": "^6.13.0",
33 | "sass": "^1.63.4",
34 | "sass-loader": "^13.3.2",
35 | "style-loader": "^3.3.3",
36 | "ts-loader": "^9.4.3",
37 | "ts-node": "^10.9.1",
38 | "use-async-effect": "^2.2.7",
39 | "webpack": "^5.87.0",
40 | "webpack-cli": "^5.1.4",
41 | "webpack-dev-server": "^4.15.1"
42 | },
43 | "devDependencies": {
44 | "@types/express": "^4.17.17",
45 | "@types/node": "^20.3.1",
46 | "@types/react-dom": "^18.2.5",
47 | "@typescript-eslint/eslint-plugin": "^5.60.0",
48 | "@typescript-eslint/parser": "^5.60.0",
49 | "eslint": "^8.43.0",
50 | "eslint-config-standard-with-typescript": "^35.0.0",
51 | "eslint-plugin-import": "^2.27.5",
52 | "eslint-plugin-n": "^15.7.0",
53 | "eslint-plugin-promise": "^6.1.1",
54 | "eslint-plugin-react": "^7.32.2",
55 | "typescript": "^4.9.5"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/prometheusQueries.ts:
--------------------------------------------------------------------------------
1 | export const staticPromQueries = [
2 | // 'machine_cpu_cores',
3 | 'machine_memory_bytes'
4 | ];
5 |
6 | export const dynamicPromQueries = [
7 | 'container_memory_usage_bytes'
8 | ];
9 |
--------------------------------------------------------------------------------
/react-dom-client.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-dom/client';
2 |
--------------------------------------------------------------------------------
/src/client/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createHashRouter, RouterProvider } from 'react-router-dom';
3 | import ViewStructure from './components/ViewStructure';
4 | import ViewNamespace from './components/ViewNamespace';
5 | import ViewCluster from './components/ViewCluster';
6 | import MainContainer from './components/MainContainer';
7 |
8 | /**
9 | * MainContainer: The root for the browser router.
10 | * Children: Components that will be displayed where the component is
11 | * provided.
12 | */
13 |
14 | const router = createHashRouter([
15 | {
16 | path: '/',
17 | element: ,
18 | children: [
19 | {
20 | path: '/',
21 | element:
22 | },
23 | {
24 | path: '/namespace',
25 | element:
26 | },
27 | {
28 | path: '/cluster',
29 | element:
30 | }
31 | ]
32 | }
33 | ]);
34 |
35 | const App = () => {
36 | return ;
37 | };
38 |
39 | export default App;
40 |
--------------------------------------------------------------------------------
/src/client/components/CISConfigResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import type { sectionResultsInfo } from '../../../types';
3 | import { statusToColor, passedNumberFromTest, checkStatusFromTest } from '../../../functions';
4 |
5 | interface benchResult {
6 | data: sectionResultsInfo
7 | testName: string
8 | }
9 |
10 | /**
11 | * Takes in a props object of type benchResult and renders a group of lists based on the keys of the object.
12 | */
13 | function CISConfigResult (props: benchResult) {
14 | const { data, testName } = props;
15 | const { remediations, summary, testResults } = data;
16 | const [showMore, setShowMore] = useState(false);
17 | const [showRemediations, setShowRemediations] = useState(false);
18 | const [showResults, setShowResults] = useState(true);
19 | if (summary[0].includes('== Summary')) summary.shift();
20 | /**
21 | * Takes a string and returns JSX elements that display the number of tests that fall under the status and the status which is based on severity.
22 | * @param {string} resultText - String that represents the result of the test.
23 | */
24 | const renderResult = (resultText: string) => (
25 | <>
26 | {passedNumberFromTest(resultText)}
27 |
28 | {checkStatusFromTest(resultText)}
29 | >
30 | );
31 |
32 | return (
33 |
34 |
{ setShowMore(!showMore); }}>
35 | {testName}
36 | {showMore && <>
37 |
38 | {summary.map((summaryText: string, index) => {
39 | return (
40 |
41 | {renderResult(summaryText)}
42 |
43 | );
44 | })}
45 |
46 |
47 |
{ setShowResults(!showResults); }}> results
48 |
49 | {showResults && <> {
50 | testResults.map((results, index) => {
51 | return (
52 |
53 | {results}
54 |
55 | );
56 | })
57 | } >}
58 |
59 |
60 |
61 | {remediations.length > 0 &&
62 | <>
63 |
{ setShowRemediations(!showRemediations); }}>
64 | remediations
65 | {showRemediations && <> { remediations.map((remedies, index) => (
66 |
67 | {remedies}
68 |
)
69 | )} >
70 | }
71 | >}
72 |
73 | >}
74 |
75 | );
76 | }
77 |
78 | export default CISConfigResult;
79 |
--------------------------------------------------------------------------------
/src/client/components/CpuUsageChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend
10 | } from 'chart.js';
11 | import { Bar } from 'react-chartjs-2';
12 | import useAsyncEffect from 'use-async-effect';
13 | import type { newDynamicPromObject } from '../../../types';
14 | import Loading from './Loading';
15 |
16 | ChartJS.register(
17 | CategoryScale,
18 | LinearScale,
19 | BarElement,
20 | Title,
21 | Tooltip,
22 | Legend
23 | );
24 |
25 | const options = {
26 | responsive: true,
27 | maintainAspectRatio: true,
28 | plugins: {
29 | legend: {
30 | display: false
31 | },
32 | title: {
33 | display: true,
34 | text: 'CPU Usage by Container'
35 | }
36 | }
37 | };
38 |
39 | const CpuUsageChart = () => {
40 | const [chartD, setChartD] = React.useState({
41 | labels: [] as string[],
42 | datasets: [
43 | {
44 | label: '',
45 | data: [] as string[],
46 | backgroundColor: '#eeeeee',
47 | color: 'eeeeee',
48 | barPercentage: 0.9
49 | }
50 | ]
51 | });
52 |
53 | const [haveData, setHaveData] = React.useState(false);
54 |
55 | useAsyncEffect(async () => { await fetchData(); }, []);
56 |
57 | const fetchData = async () => {
58 | const data = await fetch('/api/prom/metrics/dynamic',
59 | {
60 | method: 'POST',
61 | headers: {
62 | 'Content-Type': 'application/json'
63 | },
64 | body: JSON.stringify({ queries: ['container_cpu_usage_seconds_total'] })
65 | });
66 | const jsonData = await data.json();
67 | addData(jsonData);
68 | };
69 |
70 | const addData = (data: newDynamicPromObject[]) => {
71 | const labels: string[] = [];
72 | const datasets: Array<{ label: string, data: string[], backgroundColor: string, color: string, barPercentage: number, categoryPercentage: number }> = [];
73 |
74 | data.forEach((e) => {
75 | if (!labels.includes(e.container!)) {
76 | labels.push(e.container!);
77 | }
78 | // console.log(e.container);
79 | datasets.push({
80 | label: e.container!,
81 | data: [e.value!],
82 | backgroundColor: '#eeeeee',
83 | color: 'white',
84 | barPercentage: 0.5,
85 | categoryPercentage: 34
86 | });
87 | });
88 | // console.log(labels);
89 | const updatedChartD = {
90 | labels,
91 | datasets
92 | };
93 |
94 | setHaveData(true);
95 | setChartD(updatedChartD);
96 | // console.log(updatedChartD);
97 | };
98 |
99 | if (!haveData) {
100 | return loading
;
101 | } else {
102 | return (
103 |
104 |
109 |
110 | );
111 | }
112 | };
113 |
114 | export default CpuUsageChart;
115 |
--------------------------------------------------------------------------------
/src/client/components/DynamicPromComponent.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import type { responsePromArray } from '../../../types';
3 | // import { Line } from 'react-chartjs-2';
4 | // // interface promPropObject {
5 | // // data: responsePromArray
6 | // // }
7 |
8 | // const DynamicPromComponent = (props: promPropObject) => {
9 | // // const { data } = props;
10 | // // const [lineChartData, setLineChartData] = useState({
11 | // // labels: ,
12 | // // datasets: []
13 | // // })
14 | // // const renderedChart;
15 | // // console.log(data);
16 | // return (
17 | // DynamicPromComponent
18 | // );
19 | // };
20 |
21 | // export default DynamicPromComponent;
22 |
--------------------------------------------------------------------------------
/src/client/components/GrandCISResults.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { statusToColor, passedNumberFromTest, checkStatusFromTest } from '../../../functions';
3 | interface CISSummary {
4 | data: string[]
5 | }
6 |
7 | /**
8 | * Takes in a props object of type CISSummary and renders the summary based on the values.
9 | */
10 | function GrandCISResults (props: CISSummary) {
11 | const { data } = props;
12 | data.shift(); // remove '=== Summary Total ==='
13 |
14 | /**
15 | * The function `renderResult` takes a `resultText` parameter and returns JSX elements that display a number and a status based on the result text.
16 | * @param {string} resultText - The `resultText` parameter is a string that represents the result of a
17 | */
18 | const renderResult = (resultText: string) => {
19 | return (
20 | <>
21 | {passedNumberFromTest(resultText)}
22 |
23 | {checkStatusFromTest(resultText)}
24 | >
25 | );
26 | };
27 |
28 | return (
29 |
30 |
cis check summary
31 |
32 | {data.map((item, index) => {
33 | return (
34 |
35 | {
36 | renderResult(item)}
37 |
38 | );
39 | })}
40 |
41 |
42 | );
43 | }
44 |
45 | export default GrandCISResults;
46 |
--------------------------------------------------------------------------------
/src/client/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = () => ;
4 |
5 | export default Loading;
6 |
--------------------------------------------------------------------------------
/src/client/components/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet, useOutletContext } from 'react-router-dom';
3 | import NavbarComponent from './NavbarComponent';
4 |
5 | /**
6 | * The root component for the browser router. Components that will be displayed
7 | * where the component is placed.
8 | *
9 | * "namespaces" refers to the namespace(s) of the K8s cluster.
10 | */
11 |
12 | interface ContextType {
13 | namespaces: string[] | null
14 | setNamespaces: (namespaces: string[]) => void
15 | }
16 |
17 | export default function MainContainer () {
18 | const [namespaces, setNamespaces] = React.useState([]);
19 |
20 | /**
21 | * By providing Outlet with a context, we're able to share the values and
22 | * functions we pass into context with any component that uses the appropriate
23 | * functions. For this instance, the function necessary for retrieval of namespaces and setNamespaces is useNamespaces(), which returns a context provider with the type ContextType, which is a type that expects an array of strings and a setNamespaces function.
24 | */
25 |
26 | return (
27 | <>
28 |
29 |
30 | >
31 | );
32 | }
33 |
34 | export function useNamespaces () {
35 | return useOutletContext();
36 | }
37 |
--------------------------------------------------------------------------------
/src/client/components/MemoryUsageChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend
10 | } from 'chart.js';
11 | import { Bar } from 'react-chartjs-2';
12 | import useAsyncEffect from 'use-async-effect';
13 | import type { newDynamicPromObject } from '../../../types';
14 | import Loading from './Loading';
15 | import { convertBytesToGBDecimal } from '../../../functions';
16 |
17 | ChartJS.register(
18 | CategoryScale,
19 | LinearScale,
20 | BarElement,
21 | Title,
22 | Tooltip,
23 | Legend
24 | );
25 |
26 | const options = {
27 | responsive: true,
28 | maintainAspectRatio: true,
29 | redraw: false,
30 | color: '#ffffff',
31 | plugins: {
32 | legend: {
33 | display: false
34 | },
35 | title: {
36 | display: true,
37 | text: 'Memory Usage By Container'
38 | }
39 | }
40 | };
41 |
42 | const MemoryUsageChart = () => {
43 | const [chartD, setChartD] = React.useState({
44 | labels: [] as string[],
45 | datasets: [
46 | {
47 | label: '',
48 | data: [] as string[],
49 | backgroundColor: '#eeeeee'
50 | }
51 | ]
52 | });
53 | const [haveData, setHaveData] = React.useState(false);
54 |
55 | useAsyncEffect(async () => { await fetchData(); }, []);
56 |
57 | const fetchData = async () => {
58 | const data = await fetch('/api/prom/metrics/dynamic',
59 | {
60 | method: 'POST',
61 | headers: {
62 | 'Content-Type': 'application/json'
63 | },
64 | body: JSON.stringify({ queries: ['container_memory_usage_bytes'] })
65 | });
66 | const jsonData = await data.json();
67 | addData(jsonData);
68 | };
69 |
70 | const addData = (data: newDynamicPromObject[]) => {
71 | const labels: string[] = [];
72 | const datasets: Array<{ label: string, data: string[], backgroundColor: string, barPercentage: number, categoryPercentage: number }> = [];
73 |
74 | data.forEach((e) => {
75 | if (!labels.includes(e.container!)) {
76 | labels.push(e.container!);
77 | }
78 | const valueConvertedToGB = `${convertBytesToGBDecimal(parseFloat(e.value!), 2)}`;
79 | datasets.push({
80 | label: e.container!,
81 | data: [valueConvertedToGB],
82 | backgroundColor: '#eeeeee',
83 | barPercentage: 0.5,
84 | categoryPercentage: 34
85 | });
86 | });
87 | const updatedChartD = {
88 | labels,
89 | datasets
90 | };
91 |
92 | setHaveData(true);
93 | setChartD(updatedChartD);
94 | };
95 |
96 | if (!haveData) {
97 | return loading
;
98 | } else {
99 | return (
100 |
101 |
106 |
107 | );
108 | }
109 | };
110 |
111 | export default MemoryUsageChart;
112 |
--------------------------------------------------------------------------------
/src/client/components/NavbarComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | /**
5 | * NavbarComponent: Contains all of the buttons and navigation logic.
6 | * By utilizing the useNavigate hook, we're able to navigate throughout our
7 | * single page application while maintaining our "history".
8 | */
9 | export default function NavbarComponent () {
10 | const navigateTo = useNavigate();
11 |
12 | const refreshPage = () => {
13 | navigateTo('/');
14 | window.location.reload();
15 | };
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
{ navigateTo('/'); }} className='primary-button'>structure
23 |
{ navigateTo('/namespace'); }} className='primary-button'>namespace metrics
24 |
{ navigateTo('/cluster'); }} className='primary-button'>cluster testing
25 |
26 |
27 | { refreshPage(); }} className='refresh-button'>
28 |
29 |
30 |
31 |
32 |
33 | >
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/client/components/NodeCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { nodeCardProps } from '../../../types';
3 | import { convertBytesToMB } from '../../../functions';
4 |
5 | function NodeCard (props: nodeCardProps) {
6 | const convertKiToGB = (kiValue: string) =>
7 | Math.floor(parseInt(kiValue) / 976600);
8 |
9 | const { name, uid, addresses, allocatable, capacity, images, togglePods } =
10 | props;
11 |
12 | const addressList = addresses.map((address, index) => (
13 |
14 | {address.address} {' --> ' + address.type}
15 |
16 | ));
17 | const imageList = images.map((imageObject, index) => {
18 | return (
19 |
20 | name: {' ' + imageObject.names![1]}
21 |
22 | size: {' '} {convertBytesToMB(imageObject.sizeBytes)}MB
23 |
24 | );
25 | });
26 |
27 | return (
28 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
29 |
30 |
{name}
31 |
uid: {uid}
32 |
33 | addresses:
34 | {addressList}
35 |
36 |
37 |
38 |
allocatable resources / capacity:
39 |
40 | cpu cores: {allocatable!.cpu} / {capacity!.cpu}
41 |
42 |
43 | storage: {' '}
44 | {convertKiToGB(allocatable!['ephemeral-storage'])}GB /{' '}
45 | {convertKiToGB(capacity!['ephemeral-storage'])}GB
46 |
47 |
48 | memory: {convertKiToGB(allocatable!.memory)}GB /{' '}
49 | {convertKiToGB(capacity!.memory)}GB
50 |
51 |
52 | pods: {allocatable!.pods} /{' '}
53 | {capacity!.pods}
54 |
55 |
56 |
images:
57 |
{imageList}
58 |
59 | );
60 | }
61 |
62 | export default NodeCard;
63 |
--------------------------------------------------------------------------------
/src/client/components/PodCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type {
3 | podCardProps,
4 | volumeMount
5 | } from '../../../types';
6 | import type { V1Container } from '@kubernetes/client-node';
7 | import { podPhaseStatusToColor } from '../../../functions';
8 |
9 | /**
10 | * Takes in a props object of type podCardProp and renders a card representing a Kubernetes pod.
11 | */
12 |
13 | function PodCard (props: podCardProps) {
14 | const { containers, hostIP, nodeName, phase, podIPs, podName, uid, podsInNode } = props;
15 |
16 | React.useEffect(() => { console.log(podsInNode); }, [podsInNode]);
17 |
18 | /**
19 | * Takes in the `container` object of type `V1Container` and iterates over
20 | its properties using a `for...in` loop.
21 | * @param {V1Container} container - An object of type `V1Container` that closely represents the data inside a docker container.
22 | */
23 | const renderContainer = (container: V1Container) => {
24 | // initialize container array
25 | const contArr: JSX.Element[] = [];
26 |
27 | for (const key in container) {
28 | // skip over these properties as they contain extra or irrelevant data
29 | if (key === 'resources' || key === 'command' || key === 'args') continue;
30 |
31 | const value: any = container[key as keyof V1Container];
32 | let renderedValue;
33 | // if the value is a string or number, create the ul element, else
34 | // depending on the key, fire the correct render function, otherwise
35 | // the value is simply an array and so we map it to ul elements
36 | if (typeof value !== 'object') {
37 | renderedValue = ;
38 | } else {
39 | if (key === 'volumeMounts') renderedValue = renderVolumeMounts(value);
40 | if (key === 'volumeMounts') console.log(container[key]);
41 | else if (Array.isArray(value) && typeof value[0] !== 'object') {
42 | renderedValue = (<>
43 | {value.map((item: string, index) => (
44 |
45 | ))}>);
46 | }
47 | }
48 | // verify renderedValue isn't null before pushing to our container
49 | if (renderedValue !== null) contArr.push(<>{renderedValue}>);
50 | }
51 | return contArr;
52 | };
53 |
54 | /**
55 | * Takes in an aray of type `volumeMount` and maps them to ul elements
56 | * @param value - Array of objects representing volumes.
57 | */
58 | const renderVolumeMounts = (value: volumeMount[]) => {
59 | return (
60 |
61 |
Volume Mounts:
62 | {value.map((mountInfo, index) => (
63 |
64 | Name:
65 | {' ' + mountInfo.name}
66 |
67 | Mount Path:
68 | {' ' + mountInfo.mountPath}
69 |
70 | ))}
71 |
72 | );
73 | };
74 |
75 | /**
76 | * Iterates over the containers array that's passed down in props, calling the
77 | * `renderContainer` function for each container, and returns the flattened response.
78 | */
79 | const containerArrToText = () =>
80 | // iterate over flattened containers array
81 | containers.flatMap((container: V1Container) => {
82 | // invoke renderContainer with container passed in
83 | const renderedContainer = renderContainer(container);
84 | // store evaluated output in renderedContainer and return it
85 | return renderedContainer;
86 | });
87 |
88 | /**
89 | * Iterates over the podIPs array that's passed down in props and returns a list of IP addresses.
90 | */
91 | const renderPodIps = () =>
92 | podIPs.map((ipAddresses, index) => (
93 |
94 | ));
95 |
96 | return (
97 |
98 |
{podName}
99 |
uid: {uid}
100 |
101 |
102 |
103 |
Pod IP(s):
104 |
105 |
106 |
107 | Containers:
108 | {containerArrToText()}
109 |
110 |
111 |
112 | );
113 | }
114 |
115 | export default PodCard;
116 |
--------------------------------------------------------------------------------
/src/client/components/StaticPromComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { newStaticPromObject } from '../../../types';
3 | import { convertBytesToGB } from '../../../functions';
4 | import { Doughnut } from 'react-chartjs-2';
5 |
6 | import {
7 | Chart as ChartJS,
8 | ArcElement,
9 | Tooltip,
10 | Legend
11 | } from 'chart.js';
12 |
13 | ChartJS.register(ArcElement, Tooltip, Legend);
14 |
15 | const StaticPromComponent = (props: { data: newStaticPromObject, key: string }) => {
16 | const { data } = props;
17 | const options = {
18 | color: '#ffffff',
19 | plugins: {
20 | title: {
21 | display: true,
22 | text: data.instance
23 | }
24 | },
25 | layout: {
26 | padding: 5
27 | },
28 | maintainAspectRatio: false,
29 | responsive: true,
30 | aspectRatio: 1
31 | };
32 | let chartData: any;
33 |
34 | if (data.queryName === 'machine_memory_bytes') {
35 | const memoryInGB = convertBytesToGB(Number(data.value));
36 | chartData = {
37 | labels: ['Used Memory (GB)', 'Available Memory (GB)'],
38 | datasets: [
39 | {
40 | data: [memoryInGB, 16 - memoryInGB],
41 | backgroundColor: ['#FF6384', '#36A2EB'],
42 | hoverBackgroundColor: ['#FF6384', '#36A2EB']
43 | }
44 | ]
45 | };
46 | }
47 | return (
48 | <>
49 |
50 | >
51 | );
52 | };
53 |
54 | export default StaticPromComponent;
55 |
--------------------------------------------------------------------------------
/src/client/components/ViewCluster.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios';
3 | import GrandCISResults from './GrandCISResults';
4 | import CISConfigResult from './CISConfigResult';
5 | import Loading from './Loading';
6 |
7 | /**
8 | * ViewCluster: Responsible for the /cluster or cluster button.
9 | * Currently creates and displays the GrandCISResults and CISConfigResult components based on the returned data.
10 | * ViewCluster will also be responsible for creating and displaying the results of the load balancing K6 tests.
11 | */
12 |
13 | function ViewCluster () {
14 | const [totalCISResults, setTotalCISResults] = React.useState();
15 | const [controlPlaneConfiguration, setControlPlaneConfiguration] = React.useState();
16 | const [controlPlaneSecurityConfiguration, setControlPlaneSecurityConfiguration] = React.useState();
17 | const [etcdNodeConfiguration, setEtcdNodeConfiguration] = React.useState();
18 | const [kubernetesPolicies, setKubernetesPolicies] = React.useState();
19 | const [workerNodeSecurity, setWorkerNodeSecurity] = React.useState();
20 | const [displayLoadingGif, setDisplayLoadingGif] = React.useState(false);
21 |
22 | /**
23 | * Resets the display of various results and toggles the loading GIF off.
24 | */
25 | const resetResultDisplay = () => {
26 | setTotalCISResults(undefined);
27 | setControlPlaneConfiguration(undefined);
28 | setControlPlaneSecurityConfiguration(undefined);
29 | setEtcdNodeConfiguration(undefined);
30 | setKubernetesPolicies(undefined);
31 | setWorkerNodeSecurity(undefined);
32 | setDisplayLoadingGif(false);
33 | };
34 |
35 | /**
36 | * Fetches ! LOCAL ! CIS test data for a cluster and updates the state with the results for different components.
37 | */
38 | const fetchLocalCISTest = async () => {
39 | try {
40 | // remove previous results
41 | resetResultDisplay();
42 | // fetch cis test for local cluster and toggle loading gif
43 | setDisplayLoadingGif(true);
44 | const response = await axios.get('/api/security/local/cis');
45 | setDisplayLoadingGif(false);
46 | const data = response.data;
47 | // create grand summary component
48 | setTotalCISResults( );
49 | // parse data for each component, passing in the test name for the title
50 | setControlPlaneConfiguration( );
51 | setControlPlaneSecurityConfiguration( );
52 | setEtcdNodeConfiguration( );
53 | setKubernetesPolicies( );
54 | setWorkerNodeSecurity( );
55 | } catch (error) {
56 | setDisplayLoadingGif(false);
57 | console.error(error);
58 | }
59 | };
60 |
61 | /**
62 | * Fetches ! AMAZON EKS ! CIS test data for a cluster and updates the state with the results for different components.
63 | */
64 |
65 | const fetchEKSCISTest = async () => {
66 | try {
67 | // remove previous results
68 | resetResultDisplay();
69 | // fetch cis test for eks and toggle loading gif
70 | setDisplayLoadingGif(true);
71 | const response = await axios.get('/api/security/eks/cis');
72 | setDisplayLoadingGif(false);
73 | const data = response.data;
74 | // create grand summary component
75 | setTotalCISResults( );
76 | // parse data for worker node component
77 | setWorkerNodeSecurity( );
78 | } catch (error) {
79 | setDisplayLoadingGif(false);
80 | console.error(error);
81 | }
82 | };
83 |
84 | const fetchK6Test = async () => {
85 | setDisplayLoadingGif(true);
86 | const response = await axios.get('/api/k6/autoscale');
87 | if (response.status === 200) setDisplayLoadingGif(false);
88 | };
89 |
90 | return (
91 | <>
92 |
93 |
94 | { void fetchLocalCISTest(); }} style={{ backgroundColor: '#90ee90' }}>local cis test
95 | { void fetchEKSCISTest(); }} style={{ backgroundColor: '#78cc78' }}>eks cis test
96 | { void fetchK6Test(); }} style={{ backgroundColor: '#90ee90' }}>autoscaling test
97 |
98 |
99 |
100 |
101 | {displayLoadingGif &&
}
102 |
103 | {totalCISResults}
104 | {controlPlaneConfiguration}
105 | {controlPlaneSecurityConfiguration}
106 | {etcdNodeConfiguration}
107 | {kubernetesPolicies}
108 | {workerNodeSecurity}
109 |
110 |
111 | >
112 | );
113 | }
114 |
115 | export default ViewCluster;
116 |
--------------------------------------------------------------------------------
/src/client/components/ViewNamespace.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useAsyncEffect from 'use-async-effect';
3 | import axios from 'axios';
4 | import { staticPromQueries, dynamicPromQueries } from '../../../prometheusQueries';
5 | import type { newDynamicPromObject, newStaticPromObject } from '../../../types';
6 | import Loading from './Loading';
7 | import StaticPromComponent from './StaticPromComponent';
8 | import MemoryUsageChart from './MemoryUsageChart';
9 | import { Doughnut } from 'react-chartjs-2';
10 | import CpuUsageChart from './CpuUsageChart';
11 |
12 | /**
13 | * ViewNamespace: Responsible for /namespace or namespace button.
14 | * Displays the metrics for all of the pods and nodes in the namespace.
15 | */
16 |
17 | const ViewNamespace = () => {
18 | const [staticPromData, setStaticPromData] = React.useState([]);
19 | // const [dynamicPromData, setDynamicPromData] = React.useState([]);
20 |
21 | const [displayLoadingGif, setDisplayLoadingGif] = React.useState(false);
22 | useAsyncEffect(async () => {
23 | setDisplayLoadingGif(true);
24 | await fetchMetricsData();
25 | setDisplayLoadingGif(false);
26 | }, []);
27 |
28 | const fetchMetricsData = async () => {
29 | const staticResponseObject = await axios.post('/api/prom/metrics/static', { queries: staticPromQueries });
30 | console.log('Static response object: ', staticResponseObject);
31 | setStaticPromData([...staticResponseObject.data]);
32 | // console.log(staticResponseObject);
33 | // const dynamicResponseObject = await axios.post('/api/prom/metrics/default', { queries: dynamicPromQueries });
34 | // setDynamicPromData([...dynamicResponseObject.data]);
35 | };
36 |
37 | const createStaticPromComp = () => {
38 | return staticPromData.map((promQuery, index) => (
39 |
40 |
41 |
42 | ));
43 | };
44 |
45 | return (
46 | <>
47 |
48 |
49 |
50 | {createStaticPromComp()}
51 |
52 | {/* text area horizontal scrollbar containing static information */}
53 |
54 |
55 | {displayLoadingGif &&
}
56 |
57 | {/* {createDynamicPromComp()} */}
58 |
59 |
60 | >
61 | );
62 | };
63 |
64 | export default ViewNamespace;
65 |
--------------------------------------------------------------------------------
/src/client/components/ViewStructure.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NodeCard from './NodeCard';
3 | import PodCard from './PodCard';
4 | import { useNamespaces } from './MainContainer';
5 | import useAsyncEffect from 'use-async-effect';
6 | import type { namespaceObject, newNodeObject, newPodObject, nodeCardProps } from '../../../types';
7 | import axios from 'axios';
8 |
9 | /**
10 | * ViewStructure: Responsible for the "homepage" or structure button.
11 | * Creates and displays the Node and Pod card components based on the returned data from our GET requests.
12 | */
13 |
14 | const ViewStructure = () => {
15 | const { namespaces, setNamespaces } = useNamespaces();
16 | const [namespaceButtons, setNamespaceButtons] = React.useState([]);
17 | const [nodeCards, setNodeCards] = React.useState([]);
18 | const [podComponents, setPodComponents] = React.useState([]);
19 | const [podsInNode, setPodsInNode] = React.useState({});
20 |
21 | useAsyncEffect(async () => {
22 | await fetchNamespaces();
23 | }, []);
24 |
25 | /**
26 | * GET request to '/api/cluster/namespaces', retrieves the list of namespaces, sets namespaces state with retrieved data, and calls the createNamespaceComponents function with the namespaces data.
27 | * If an error occurs, it is logged to the console.
28 | */
29 | const fetchNamespaces = async () => {
30 | try {
31 | const response = await axios.get('/api/cluster/namespaces');
32 | const namespacesData = response.data;
33 | setNamespaces(namespacesData);
34 | createNamespaceComponents(namespacesData);
35 | } catch (error) {
36 | console.error(error);
37 | }
38 | };
39 |
40 | /**
41 | * GET request to '/api/cluster/node/${selectedNamespace}', passing in the name of the namespace button clicked on, retrieves the list of nodes that belong to the namespace, and calls the createNodeComponents function with the node data.
42 | * @param {string} selectedNamespace - The `selectedNamespace` parameter is a string that represents the namespace that is clicked on.
43 | */
44 | const fetchNode = async (selectedNamespace: string): Promise => {
45 | try {
46 | const response = await axios.get(`/api/cluster/node/${selectedNamespace}`);
47 | const nodesData: nodeCardProps[] = response.data;
48 | createNodeComponents(nodesData);
49 | await fetchPod(selectedNamespace);
50 | } catch (error) {
51 | console.error(error);
52 | }
53 | };
54 |
55 | /**
56 | * GET request to '/api/cluster/pod/${selectedNamespace}', passing in the the name of the namespace provided by the fetchNode function, and then creates pod components based on the retrieved data.
57 | * @param {string} selectedNamespace - The `selectedNamespace` parameter is a string that represents the namespace that is passed in from the fetchNode function.
58 | */
59 | const fetchPod = async (selectedNamespace: string) => {
60 | try {
61 | const response = await axios.get(`/api/cluster/pod/${selectedNamespace}`);
62 | const podsData = response.data;
63 | createPodComponents(podsData);
64 | } catch (error) {
65 | console.error(error);
66 | }
67 | };
68 |
69 | /**
70 | * Takes an array of `namespaceObject` and creates buttons for each object in the array. The button array is returned.
71 | * @param {namespaceObject[]} namespaceArray - An array of objects representing namespaces. Each object should have a "name" property.
72 | */
73 | const createNamespaceComponents = (namespaceArray: namespaceObject[]) => {
74 | const buttons = namespaceArray.map((namespaceObject: namespaceObject, index) => (
75 | {
80 | void fetchNode(namespaceObject.name);
81 | }}
82 | >
83 | {namespaceObject.name}
84 |
85 | ));
86 | setNamespaceButtons(buttons);
87 | };
88 |
89 | /** Takes an array of `newNodeObject` and creates NodeCard components based on the data in each object. The created NodeCard component array is returned.
90 | * @param {newNodeObject[]} nodeData - An array of objects representing nodes.
91 | */
92 | const createNodeComponents = (nodeData: newNodeObject[]) => {
93 | const mappedNodes = nodeData.map((node, index) => {
94 | return (
95 | );
107 | });
108 | setNodeCards(mappedNodes);
109 | };
110 |
111 | /** Takes an array of `newPodObject` and creates PodCard components based on the data in each object. The created PodCard component array is returned.
112 | * @param {newPodObject[]} podData - An array of objects representing pods.
113 | */
114 | const createPodComponents = (podData: newPodObject[]) => {
115 | const mappedPods: JSX.Element[] = podData.map((newPodObject, index) => {
116 | return (
117 |
128 | );
129 | });
130 | setPodComponents([]);
131 | setPodComponents(mappedPods);
132 | };
133 |
134 | const togglePods = async (event: React.MouseEvent) => {
135 | try {
136 | const nodeName = event.currentTarget.getAttribute('data-node-name');
137 | const response = await axios.get(`/api/cluster/pod/${nodeName!}`);
138 | const podsInNode = response.data;
139 | setPodsInNode(podsInNode);
140 | createPodComponents(podsInNode);
141 | } catch (error) {
142 | console.log(error);
143 | }
144 | };
145 |
146 | return (
147 | <>
148 |
149 | {namespaceButtons}
150 |
151 |
152 |
153 | {nodeCards.length > 0 && <>
Nodes >}
154 |
155 | {nodeCards}
156 |
157 | {podComponents.length > 0 && <>
Pods >}
158 |
159 | {podComponents}
160 |
161 |
162 | >
163 | );
164 | };
165 |
166 | export default ViewStructure;
167 |
--------------------------------------------------------------------------------
/src/client/css/main.scss:
--------------------------------------------------------------------------------
1 | $mainColor: #102E44;
2 | $lighterMainColor: #1A4260;
3 | $darkTextColor: #3C3C3C;
4 | $lightTextColor: #F1FEFA;
5 | $accentColor: #B2FCFB;
6 | $tintColor: #99F4DB;
7 | $standardBoxShadow: 5px 5px 10px 0px rgba(0, 0, 0, 0.5);
8 |
9 | html, body, #root {
10 | height: 100%;
11 | min-height: 100vh;
12 | }
13 |
14 | #root {
15 | width: 100%;
16 | min-height: 100vh;
17 | }
18 |
19 | body {
20 | padding: 20px;
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: center;
24 | background-color: $lighterMainColor;
25 | }
26 |
27 | button {
28 | margin: 0;
29 | border: none;
30 | transition: 100ms ease-in-out;
31 | border-bottom: 4px solid rgba(0, 0, 0, 0.1);
32 | border-right: 2px solid rgba(0, 0, 0, 0.1);
33 | &:hover {
34 | transform: translateY(-3px);
35 | box-shadow: $standardBoxShadow // animate
36 | }
37 | &:active {
38 | transform: translateZ(100px);
39 | }
40 | &.primary-button {
41 | background-color: $accentColor;
42 | margin-left: 10px;
43 | }
44 | &.secondary-button {
45 | background-color: $tintColor;
46 | }
47 | &.refresh-button {
48 | background-color: $accentColor;
49 | padding-left: 10px;
50 | padding-right: 10px;
51 | }
52 | }
53 |
54 | .navbar-container {
55 | padding-bottom: 10px;
56 | display: flex;
57 | width: 100%;
58 | justify-content: space-between;
59 | }
60 |
61 | hr {
62 | padding: 0;
63 | margin: 10px;
64 | border: 0.5px solid $mainColor;
65 | }
66 |
67 | .main-info-container, .cis-container {
68 | display: flex;
69 | flex-direction: column;
70 | text-align: center;
71 | padding-top: 10px;
72 | max-height: 600px;
73 | color: $lightTextColor;
74 | }
75 |
76 | .card-container {
77 | display: flex;
78 | justify-content: center;
79 | flex-direction: row;
80 | width: 100%;
81 | height: 95%;
82 | gap: 40px;
83 | padding-bottom: 25px;
84 | .card {
85 | display: flex;
86 | flex-direction: column;
87 | background-color: $lightTextColor;
88 | color: $darkTextColor;
89 | border-radius: 10px;
90 | height: auto;
91 | width: 90%;
92 | padding-bottom: 20px;
93 | box-shadow: $standardBoxShadow;
94 | h3 {
95 | margin-top: 10px;
96 | margin-bottom: 0px;
97 | }
98 | h4 {
99 | font-size: 90%;
100 | margin-bottom: 10px;
101 | }
102 | ul {
103 | margin: 0;
104 | // line-height: 5px;
105 | }
106 | }
107 | }
108 |
109 | @media screen and (min-width:1100px) {
110 | .pod-container {
111 | grid-template-columns: repeat(4, 1fr) !important;
112 | }
113 | }
114 |
115 | .pod-container {
116 | margin-top: 30px;
117 | display: grid;
118 | grid-template-columns: repeat(3, 1fr);
119 | justify-items: center;
120 | width: 100%;
121 | height: 95%;
122 | row-gap: 20px;
123 | padding-bottom: 60px;
124 | .pod-card {
125 | background-color: #e6eeec;
126 | color: $darkTextColor;
127 | border-radius: 10px;
128 | height: auto;
129 | width: 90%;
130 | box-shadow: $standardBoxShadow;
131 | padding-bottom: 10px;
132 | padding-left: 10px;
133 | padding-right: 10px;
134 | display: flex;
135 | flex-direction: column;
136 | align-items: center;
137 | h5 {
138 | margin-top: 5px;
139 | margin-bottom: 0px;
140 | align-self: center;
141 | }
142 | h4 {
143 | font-size: 90%;
144 | margin-bottom: 10px;
145 | }
146 | h6 {
147 | margin-bottom: 0px;
148 | font-size: 70%;
149 | }
150 | ul {
151 | margin: 0;
152 | }
153 | }
154 | }
155 |
156 | .namespace-button-container, .benchmark-buttons-container {
157 | display: flex;
158 | gap: 10px;
159 | justify-content: center;
160 | align-items: center;
161 | padding-top: 5px;
162 | padding-bottom: 5px;
163 | }
164 |
165 | .config-container {
166 | border-radius: 10px;
167 | background-color: $mainColor;
168 | padding: 15px;
169 | padding-bottom: 20px;
170 | box-shadow: $standardBoxShadow;
171 | button {
172 | color: $lightTextColor;
173 | border: none;
174 | &:hover {
175 | transform: translateY(2px);
176 | box-shadow: none;
177 | }
178 | &:active {
179 | transform: none;
180 | }
181 | }
182 | h3 {
183 | margin: 0;
184 | }
185 | h5 {
186 | margin: 0;
187 | }
188 | }
189 |
190 | .summary-container {
191 | background-color: $lightTextColor;
192 | border-radius: 10px;
193 | color: $darkTextColor;
194 | padding: 10px;
195 | padding-bottom: 20px;
196 | box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 0.2);
197 | h3 {
198 | margin: 0;
199 | }
200 | h5 {
201 | margin: 0;
202 | }
203 | p {
204 | margin: 0;
205 | }
206 | }
207 |
208 | .result-container {
209 | display: flex;
210 | justify-content: center;
211 | align-items: center;
212 | gap: 20px;
213 | line-height: 10px;
214 |
215 | }
216 |
217 | .config-result-container {
218 | margin: 0;
219 | text-align: center;
220 | p {
221 | margin: 0;
222 | text-align: left;
223 | }
224 | }
225 |
226 | .image-scrollbox {
227 | height: 150px;
228 | overflow-y: scroll;
229 | display: flex;
230 | flex-direction: column;
231 | padding-left: 10px;
232 | ul {
233 | text-align: left;
234 | margin-left: 5px;
235 | }
236 | }
237 |
238 | .display-more {
239 | display: none;
240 | }
241 |
242 | .light-hr {
243 | background: linear-gradient(90deg, rgb(241,254,250) 0%, rgb(219,230,227) 50%, rgb(241,254,250) 100%);
244 | height: 2px;
245 | border: none;
246 | margin: 5px;
247 | }
248 |
249 | .benchmark-buttons-container {
250 | display: flex;
251 | align-items: center;
252 | align-self: center;
253 | }
254 |
255 | .benchmark-buttons {
256 | background-color: $lightTextColor;
257 | padding: 10px;
258 | padding-left: 20px;
259 | padding-right: 20px;
260 | border-radius: 10px;
261 | display: flex;
262 | gap: 10px;
263 | }
264 |
265 | .cis-container {
266 | gap: 10px;
267 | }
268 |
269 | .result-list-container {
270 | padding-left: 5%;
271 | padding-right: 5%;
272 | }
273 |
274 | .navbar-logo {
275 | width: 40px;
276 | }
277 |
278 | .content-box {
279 | display: flex;
280 | flex-direction: column;
281 | justify-content: flex-start;
282 | align-items: flex-start;
283 | padding: 10px;
284 | .content-title {
285 | align-self: center;
286 | }
287 | ul {
288 | text-align: left;
289 | }
290 | }
291 |
292 | .content-box-2 {
293 | display: flex;
294 | flex-direction: column;
295 | justify-content: flex-start;
296 | align-items: flex-start;
297 | padding: 0px;
298 | .content-title {
299 | align-self: center;
300 | }
301 | ul {
302 | text-align: left;
303 | }
304 | }
305 |
306 | .doughnut-chart-container {
307 | max-width: 200px;
308 | }
--------------------------------------------------------------------------------
/src/client/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
--------------------------------------------------------------------------------
/src/client/css/skeleton.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Skeleton V2.0.4
3 | * Copyright 2014, Dave Gamache
4 | * www.getskeleton.com
5 | * Free to use under the MIT license.
6 | * http://www.opensource.org/licenses/mit-license.php
7 | * 12/29/2014
8 | */
9 |
10 |
11 | /* Table of contents
12 | ––––––––––––––––––––––––––––––––––––––––––––––––––
13 | - Grid
14 | - Base Styles
15 | - Typography
16 | - Links
17 | - Buttons
18 | - Forms
19 | - Lists
20 | - Code
21 | - Tables
22 | - Spacing
23 | - Utilities
24 | - Clearing
25 | - Media Queries
26 | */
27 |
28 |
29 | /* Grid
30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
31 | .container {
32 | position: relative;
33 | width: 100%;
34 | max-width: 960px;
35 | margin: 0 auto;
36 | padding: 0 20px;
37 | box-sizing: border-box; }
38 | .column,
39 | .columns {
40 | width: 100%;
41 | float: left;
42 | box-sizing: border-box; }
43 |
44 | /* For devices larger than 400px */
45 | @media (min-width: 400px) {
46 | .container {
47 | width: 85%;
48 | padding: 0; }
49 | }
50 |
51 | /* For devices larger than 550px */
52 | @media (min-width: 550px) {
53 | .container {
54 | width: 80%; }
55 | .column,
56 | .columns {
57 | margin-left: 4%; }
58 | .column:first-child,
59 | .columns:first-child {
60 | margin-left: 0; }
61 |
62 | .one.column,
63 | .one.columns { width: 4.66666666667%; }
64 | .two.columns { width: 13.3333333333%; }
65 | .three.columns { width: 22%; }
66 | .four.columns { width: 30.6666666667%; }
67 | .five.columns { width: 39.3333333333%; }
68 | .six.columns { width: 48%; }
69 | .seven.columns { width: 56.6666666667%; }
70 | .eight.columns { width: 65.3333333333%; }
71 | .nine.columns { width: 74.0%; }
72 | .ten.columns { width: 82.6666666667%; }
73 | .eleven.columns { width: 91.3333333333%; }
74 | .twelve.columns { width: 100%; margin-left: 0; }
75 |
76 | .one-third.column { width: 30.6666666667%; }
77 | .two-thirds.column { width: 65.3333333333%; }
78 |
79 | .one-half.column { width: 48%; }
80 |
81 | /* Offsets */
82 | .offset-by-one.column,
83 | .offset-by-one.columns { margin-left: 8.66666666667%; }
84 | .offset-by-two.column,
85 | .offset-by-two.columns { margin-left: 17.3333333333%; }
86 | .offset-by-three.column,
87 | .offset-by-three.columns { margin-left: 26%; }
88 | .offset-by-four.column,
89 | .offset-by-four.columns { margin-left: 34.6666666667%; }
90 | .offset-by-five.column,
91 | .offset-by-five.columns { margin-left: 43.3333333333%; }
92 | .offset-by-six.column,
93 | .offset-by-six.columns { margin-left: 52%; }
94 | .offset-by-seven.column,
95 | .offset-by-seven.columns { margin-left: 60.6666666667%; }
96 | .offset-by-eight.column,
97 | .offset-by-eight.columns { margin-left: 69.3333333333%; }
98 | .offset-by-nine.column,
99 | .offset-by-nine.columns { margin-left: 78.0%; }
100 | .offset-by-ten.column,
101 | .offset-by-ten.columns { margin-left: 86.6666666667%; }
102 | .offset-by-eleven.column,
103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; }
104 |
105 | .offset-by-one-third.column,
106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; }
107 | .offset-by-two-thirds.column,
108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
109 |
110 | .offset-by-one-half.column,
111 | .offset-by-one-half.columns { margin-left: 52%; }
112 |
113 | }
114 |
115 |
116 | /* Base Styles
117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
118 | /* NOTE
119 | html is set to 62.5% so that all the REM measurements throughout Skeleton
120 | are based on 10px sizing. So basically 1.5rem = 15px :) */
121 | html {
122 | font-size: 62.5%; }
123 | body {
124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
125 | line-height: 1.6;
126 | font-weight: 400;
127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
128 | color: #222; }
129 |
130 |
131 | /* Typography
132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
133 | h1, h2, h3, h4, h5, h6 {
134 | margin-top: 0;
135 | margin-bottom: 2rem;
136 | font-weight: 300; }
137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
143 |
144 | /* Larger than phablet */
145 | @media (min-width: 550px) {
146 | h1 { font-size: 5.0rem; }
147 | h2 { font-size: 4.2rem; }
148 | h3 { font-size: 3.6rem; }
149 | h4 { font-size: 3.0rem; }
150 | h5 { font-size: 2.4rem; }
151 | h6 { font-size: 1.5rem; }
152 | }
153 |
154 | p {
155 | margin-top: 0; }
156 |
157 |
158 | /* Links
159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
160 | a {
161 | color: #1EAEDB; }
162 | a:hover {
163 | color: #0FA0CE; }
164 |
165 |
166 | /* Buttons
167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
168 | .button,
169 | button,
170 | input[type="submit"],
171 | input[type="reset"],
172 | input[type="button"] {
173 | display: inline-block;
174 | height: 38px;
175 | padding: 0 30px;
176 | color: #555;
177 | text-align: center;
178 | font-size: 11px;
179 | font-weight: 600;
180 | line-height: 38px;
181 | letter-spacing: .1rem;
182 | text-transform: uppercase;
183 | text-decoration: none;
184 | white-space: nowrap;
185 | background-color: transparent;
186 | border-radius: 4px;
187 | border: 1px solid #bbb;
188 | cursor: pointer;
189 | box-sizing: border-box; }
190 | /* .button:hover,
191 | button:hover,
192 | input[type="submit"]:hover,
193 | input[type="reset"]:hover,
194 | input[type="button"]:hover,
195 | .button:focus,
196 | button:focus,
197 | input[type="submit"]:focus,
198 | input[type="reset"]:focus,
199 | input[type="button"]:focus {
200 | color: #333;
201 | border-color: #888;
202 | outline: 0; } -> messes with border on button*/
203 | .button.button-primary,
204 | button.button-primary,
205 | input[type="submit"].button-primary,
206 | input[type="reset"].button-primary,
207 | input[type="button"].button-primary {
208 | color: #FFF;
209 | background-color: #33C3F0;
210 | border-color: #33C3F0; }
211 | .button.button-primary:hover,
212 | button.button-primary:hover,
213 | input[type="submit"].button-primary:hover,
214 | input[type="reset"].button-primary:hover,
215 | input[type="button"].button-primary:hover,
216 | .button.button-primary:focus,
217 | button.button-primary:focus,
218 | input[type="submit"].button-primary:focus,
219 | input[type="reset"].button-primary:focus,
220 | input[type="button"].button-primary:focus {
221 | color: #FFF;
222 | background-color: #1EAEDB;
223 | border-color: #1EAEDB; }
224 |
225 |
226 | /* Forms
227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
228 | input[type="email"],
229 | input[type="number"],
230 | input[type="search"],
231 | input[type="text"],
232 | input[type="tel"],
233 | input[type="url"],
234 | input[type="password"],
235 | textarea,
236 | select {
237 | height: 38px;
238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
239 | background-color: #fff;
240 | border: 1px solid #D1D1D1;
241 | border-radius: 4px;
242 | box-shadow: none;
243 | box-sizing: border-box; }
244 | /* Removes awkward default styles on some inputs for iOS */
245 | input[type="email"],
246 | input[type="number"],
247 | input[type="search"],
248 | input[type="text"],
249 | input[type="tel"],
250 | input[type="url"],
251 | input[type="password"],
252 | textarea {
253 | -webkit-appearance: none;
254 | -moz-appearance: none;
255 | appearance: none; }
256 | textarea {
257 | min-height: 65px;
258 | padding-top: 6px;
259 | padding-bottom: 6px; }
260 | input[type="email"]:focus,
261 | input[type="number"]:focus,
262 | input[type="search"]:focus,
263 | input[type="text"]:focus,
264 | input[type="tel"]:focus,
265 | input[type="url"]:focus,
266 | input[type="password"]:focus,
267 | textarea:focus,
268 | select:focus {
269 | border: 1px solid #33C3F0;
270 | outline: 0; }
271 | label,
272 | legend {
273 | display: block;
274 | margin-bottom: .5rem;
275 | font-weight: 600; }
276 | fieldset {
277 | padding: 0;
278 | border-width: 0; }
279 | input[type="checkbox"],
280 | input[type="radio"] {
281 | display: inline; }
282 | label > .label-body {
283 | display: inline-block;
284 | margin-left: .5rem;
285 | font-weight: normal; }
286 |
287 |
288 | /* Lists
289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
290 | ul {
291 | list-style: circle inside; }
292 | ol {
293 | list-style: decimal inside; }
294 | ol, ul {
295 | padding-left: 0;
296 | margin-top: 0; }
297 | ul ul,
298 | ul ol,
299 | ol ol,
300 | ol ul {
301 | margin: 1.5rem 0 1.5rem 3rem;
302 | font-size: 90%; }
303 | li {
304 | margin-bottom: 1rem; }
305 |
306 |
307 | /* Code
308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
309 | code {
310 | padding: .2rem .5rem;
311 | margin: 0 .2rem;
312 | font-size: 90%;
313 | white-space: nowrap;
314 | background: #F1F1F1;
315 | border: 1px solid #E1E1E1;
316 | border-radius: 4px; }
317 | pre > code {
318 | display: block;
319 | padding: 1rem 1.5rem;
320 | white-space: pre; }
321 |
322 |
323 | /* Tables
324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
325 | th,
326 | td {
327 | padding: 12px 15px;
328 | text-align: left;
329 | border-bottom: 1px solid #E1E1E1; }
330 | th:first-child,
331 | td:first-child {
332 | padding-left: 0; }
333 | th:last-child,
334 | td:last-child {
335 | padding-right: 0; }
336 |
337 |
338 | /* Spacing
339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
340 | button,
341 | .button {
342 | margin-bottom: 1rem; }
343 | input,
344 | textarea,
345 | select,
346 | fieldset {
347 | margin-bottom: 1.5rem; }
348 | pre,
349 | blockquote,
350 | dl,
351 | figure,
352 | table,
353 | p,
354 | ul,
355 | ol,
356 | form {
357 | margin-bottom: 2.5rem; }
358 |
359 |
360 | /* Utilities
361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
362 | .u-full-width {
363 | width: 100%;
364 | box-sizing: border-box; }
365 | .u-max-full-width {
366 | max-width: 100%;
367 | box-sizing: border-box; }
368 | .u-pull-right {
369 | float: right; }
370 | .u-pull-left {
371 | float: left; }
372 |
373 |
374 | /* Misc
375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
376 | hr {
377 | margin-top: 3rem;
378 | margin-bottom: 3.5rem;
379 | border-width: 0;
380 | border-top: 1px solid #E1E1E1; }
381 |
382 |
383 | /* Clearing
384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
385 |
386 | /* Self Clearing Goodness */
387 | .container:after,
388 | .row:after,
389 | .u-cf {
390 | content: "";
391 | display: table;
392 | clear: both; }
393 |
394 |
395 | /* Media Queries
396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
397 | /*
398 | Note: The best way to structure the use of media queries is to create the queries
399 | near the relevant code. For example, if you wanted to change the styles for buttons
400 | on small devices, paste the mobile query code up in the buttons section and style it
401 | there.
402 | */
403 |
404 |
405 | /* Larger than mobile */
406 | @media (min-width: 400px) {}
407 |
408 | /* Larger than phablet (also point when grid becomes active) */
409 | @media (min-width: 550px) {}
410 |
411 | /* Larger than tablet */
412 | @media (min-width: 750px) {}
413 |
414 | /* Larger than desktop */
415 | @media (min-width: 1000px) {}
416 |
417 | /* Larger than Desktop HD */
418 | @media (min-width: 1200px) {}
419 |
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Brizo
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/client/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 | import './css/normalize.css';
5 | import './css/skeleton.css';
6 | import './css/main.scss';
7 | const domNode = document.getElementById('root')!;
8 | const root = createRoot(domNode);
9 |
10 | root.render( );
11 |
--------------------------------------------------------------------------------
/src/server/controllers/clusterController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction } from 'express';
2 | import type {
3 | clusterControllerType,
4 | newPodObject,
5 | newNodeObject
6 | } from '../../../types';
7 | import os from 'os';
8 | import * as k8s from '@kubernetes/client-node';
9 |
10 | // declare kube file path
11 | const KUBE_FILE_PATH = `${os.homedir()}/.kube/config`;
12 |
13 | // create new kubeconfig class
14 | const kc = new k8s.KubeConfig();
15 |
16 | // load from kube config file
17 | kc.loadFromFile(KUBE_FILE_PATH);
18 |
19 | // make api client
20 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
21 |
22 | const clusterController: clusterControllerType = {
23 | getPods: async (req: Request, res: Response, next: NextFunction) => {
24 | // destructure namespace of interest from request params
25 | const { nodeName } = req.params;
26 |
27 | try {
28 | // fetch pods from k8s api for given namespace
29 | const result = await k8sApi.listPodForAllNamespaces(undefined, undefined, `spec.nodeName=${nodeName}`);
30 |
31 | // extract list of pod objects from result
32 | const podList = result.body.items;
33 |
34 | // initialize filtered pod list as empty array
35 | const filteredPodList: newPodObject[] = [];
36 |
37 | // iterate over podList
38 | podList.forEach((pod) => {
39 | // declare pod object info variables
40 | const nodeName = pod.spec?.nodeName;
41 | const podName = pod.metadata?.name;
42 | const uid = pod.metadata?.uid;
43 | const containers = pod.spec?.containers;
44 | const hostIP = pod.status?.hostIP;
45 | const phase = pod.status?.phase;
46 | const podIPs = pod.status?.podIPs;
47 |
48 | // create pod object
49 | const podObject: newPodObject = {
50 | nodeName,
51 | podName,
52 | uid,
53 | containers,
54 | hostIP,
55 | phase,
56 | podIPs
57 | };
58 |
59 | // push pod object to filtered list
60 | filteredPodList.push(podObject);
61 | });
62 |
63 | // save podListByNode to res.locals
64 | res.locals.filteredPodList = filteredPodList;
65 |
66 | // move to next middleware
67 | next();
68 | } catch (error) {
69 | // error handling
70 | console.log('Error getting list of pods.');
71 | next(error);
72 | }
73 | },
74 |
75 | getNodes: async (req: Request, res: Response, next: NextFunction) => {
76 | // destructure namespace from request params
77 | const { namespace } = req.params;
78 |
79 | try {
80 | // fetch nodes from k8s api for given namespace
81 | const result = await k8sApi.listNode(namespace);
82 |
83 | // extract list of nodes from result body
84 | const nodeList = result.body.items;
85 |
86 | // initialize filtered node list as empty array
87 | const filteredNodeList: newNodeObject[] = [];
88 |
89 | // iterate over node list
90 | nodeList.forEach((node) => {
91 | // declare node object info variables
92 | const name = node.metadata?.name;
93 | const uid = node.metadata?.uid;
94 | const podCIDRs = node.spec?.podCIDRs;
95 | const addresses = node.status?.addresses;
96 | const allocatable = node.status?.allocatable;
97 | const capacity = node.status?.capacity;
98 | const images = node.status?.images;
99 |
100 | // create node object
101 | const nodeObject: newNodeObject = {
102 | name,
103 | uid,
104 | podCIDRs,
105 | addresses,
106 | allocatable,
107 | capacity,
108 | images
109 | };
110 |
111 | // push node object to filtered node list
112 | filteredNodeList.push(nodeObject);
113 | });
114 | // store node list on res.locals
115 | res.locals.filteredNodeList = filteredNodeList;
116 |
117 | // move to next middleware
118 | next();
119 | } catch (error) {
120 | // error handling
121 | console.log('Error getting nodes.');
122 | next(error);
123 | }
124 | },
125 |
126 | getNamespaces: async (req: Request, res: Response, next: NextFunction) => {
127 | try {
128 | // fetch list of namespaces from k8s api
129 | const namespaceData = await k8sApi.listNamespace();
130 |
131 | // iterate through data and isolate namespace name, uid, and status for each namespace
132 | const namespaceList = namespaceData.body.items.map(
133 | (namespace) => {
134 | return {
135 | name: namespace.metadata?.name,
136 | uid: namespace.metadata?.uid,
137 | status: namespace.status?.phase
138 | };
139 | }
140 | );
141 |
142 | // store namespace list on res.locals
143 | res.locals.namespaceList = namespaceList;
144 | // move to next middleware
145 | next();
146 | } catch (error) {
147 | // error handling
148 | console.log('Error getting namespaces.');
149 | next(error);
150 | }
151 | }
152 | };
153 |
154 | export default clusterController;
155 |
--------------------------------------------------------------------------------
/src/server/controllers/k6Controller.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction } from 'express';
2 | import { exec } from 'child_process';
3 |
4 | const k6Controller = {
5 | runScalingTest: async (req: Request, res: Response, next: NextFunction) => {
6 | try {
7 | // define command to run the k6 auto scaling test
8 | const command = 'k6 cloud k6/script.js';
9 |
10 | // run command with exec
11 | exec(command, (error) => {
12 | // error handling
13 | if (error !== null) {
14 | next(error);
15 | }
16 | });
17 | // move to next middleware
18 | next();
19 | } catch (error) {
20 | // error handling
21 | next(error);
22 | }
23 | }
24 | };
25 |
26 | export default k6Controller;
27 |
--------------------------------------------------------------------------------
/src/server/controllers/prometheusController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction } from 'express';
2 | import type { newDynamicPromObject, dynamicPromQueryObject, staticPromQueryObject, newStaticPromObject } from '../../../types';
3 |
4 | const promURL = 'http://localhost:9090/api/v1/query?query=';
5 |
6 | const prometheusController = {
7 | getDynamicMetrics: async (req: Request, res: Response, next: NextFunction) => {
8 | // destructure queries from request body
9 | const { queries } = req.body;
10 |
11 | // initialize empty array for dynamic metrics we are scraping
12 | const dynamicMetrics: newDynamicPromObject[] = [];
13 | try {
14 | // iterate over queries array
15 | for (const query of queries) {
16 | // query prometheus
17 | const response = await fetch(promURL + String(query));
18 |
19 | // parse response
20 | const data = await response.json();
21 | // iterate over data results
22 | data.data.result.forEach((element: dynamicPromQueryObject) => {
23 | // define new element to be pushed into array
24 | if (element.metric.name !== undefined && element.metric.container !== undefined) {
25 | const newElement: newDynamicPromObject = {
26 | queryName: element.metric.__name__,
27 | container: element.metric?.container,
28 | pod: element.metric.pod,
29 | name: element.metric.name,
30 | value: element.value![1]
31 | };
32 | // push new element to array
33 | dynamicMetrics.push(newElement);
34 | }
35 | });
36 | }
37 |
38 | // store dynamic metrics on res.locals
39 | res.locals.dynamicMetrics = dynamicMetrics;
40 | // move to next middleware
41 | next();
42 | } catch (error) {
43 | // error handling
44 | next(error);
45 | }
46 | },
47 |
48 | getStaticMetrics: async (req: Request, res: Response, next: NextFunction) => {
49 | // destructure queries from request body
50 | const { queries } = req.body;
51 |
52 | // initialize empty array for static metrics we are scraping
53 | const staticMetrics: newStaticPromObject[] = [];
54 | try {
55 | // iterate over queries array
56 | for (const query of queries) {
57 | // query prometheus
58 | const response = await fetch(promURL + String(query));
59 |
60 | // parse response
61 | const data = await response.json();
62 | console.log(data.data.result);
63 | // iterate over data results
64 | data.data.result.forEach((element: staticPromQueryObject) => {
65 | // define new element to be pushed into array
66 | if (element.metric.__name__ !== undefined) {
67 | const newElement: newStaticPromObject = {
68 | queryName: element.metric.__name__,
69 | value: element.value![1],
70 | instance: element.metric.instance
71 | };
72 | // push new element to array
73 | staticMetrics.push(newElement);
74 | }
75 | });
76 | }
77 |
78 | // store static metrics on res.locals
79 | res.locals.staticMetrics = staticMetrics;
80 | // move to next middleware
81 | next();
82 | } catch (error) {
83 | // error handling
84 | next(error);
85 | }
86 | }
87 | };
88 |
89 | export default prometheusController;
90 |
--------------------------------------------------------------------------------
/src/server/controllers/securityController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction } from 'express';
2 | import { exec } from 'child_process';
3 | import fs from 'fs';
4 | import type {
5 | allTestInfoType,
6 | allTestInfoEKSType,
7 | indexObjectType,
8 | sectionResultsInfo
9 | } from '../../../types';
10 |
11 | const securityController = {
12 | // this method runs kube bench tool for cis testing, writes the log to output.text, and sends the info to the front end
13 | runKubeBenchLocal: async (req: Request, res: Response, next: NextFunction) => {
14 | try {
15 | // create kube bench job
16 | await securityController.applyKubeBenchJob('tests/cis/job.yaml', next);
17 |
18 | // get pod name of kube bench job
19 | const podName = await securityController.getKubeBenchPodName();
20 |
21 | // grab the log of the kube bench pod
22 | const kubeBenchOutput = await securityController.getKubeBenchPodLog(podName);
23 |
24 | // write kube bench pod log to output.txt
25 | securityController.writeOutputData(kubeBenchOutput, next);
26 |
27 | // parse kube bench pod log data before sending to front end
28 | const allTestInfo = securityController.filterOutputData(kubeBenchOutput.split('\n'));
29 |
30 | // save parsed kube bench pod log data on res.locals
31 | res.locals.allTestInfo = allTestInfo;
32 |
33 | // move to next middleware
34 | next();
35 | } catch (error) {
36 | // error handling
37 | next(error);
38 | }
39 | },
40 |
41 | runKubeBenchEKS: async (req: Request, res: Response, next: NextFunction) => {
42 | try {
43 | // create kube bench job
44 | await securityController.applyKubeBenchJob('tests/cis/job-eks.yaml', next);
45 |
46 | // get pod name of kube bench job
47 | const podName = await securityController.getKubeBenchPodName();
48 |
49 | // grab the log of the kube bench pod
50 | const kubeBenchOutput = await securityController.getKubeBenchPodLog(podName);
51 |
52 | // write kube bench pod log to output.txt
53 | securityController.writeOutputData(kubeBenchOutput, next);
54 |
55 | // filter kube bench pod log data before sending to front end
56 | const allTestInfoEKS = securityController.filterOutputDataEKS(kubeBenchOutput.split('\n'));
57 |
58 | // save parsed kube bench pod log data on res.locals
59 | res.locals.allTestInfoEKS = allTestInfoEKS;
60 |
61 | // move to next middleware
62 | next();
63 | } catch (error) {
64 | // error handling
65 | next(error);
66 | }
67 | },
68 |
69 | // method to apply kube bench job
70 | applyKubeBenchJob: async (jobPath: string, next: NextFunction) => {
71 | // define command to create kube bench test as a job
72 | const command = `kubectl apply -f ${jobPath}`;
73 |
74 | // run command using exec
75 | exec(command, (error) => {
76 | if (error !== null) {
77 | // error handling
78 | console.log('Error applying kube bench job.');
79 | next(error);
80 | }
81 | });
82 | },
83 |
84 | // method to get pod name
85 | getKubeBenchPodName: async () => {
86 | // return a promise of type string for grabbing the pod log later
87 | return await new Promise((resolve, reject) => {
88 | // define command to find the kube bench pod name
89 | const command = 'kubectl get pods -l job-name=kube-bench -o=jsonpath="{.items[0].metadata.name}"';
90 |
91 | // run command with exec
92 | exec(command, (error, stdout) => {
93 | if (error !== null) {
94 | // error handling
95 | console.log('Error getting kube bench pod name.');
96 | // change state of promise to 'rejected' and pass in the reason (error)
97 | reject(error);
98 | }
99 |
100 | // trim whitespace from pod name string
101 | const podName = stdout.trim();
102 |
103 | // change state of promise to 'fulfilled' and pass in the fulfillment value (podName)
104 | resolve(podName);
105 | });
106 | });
107 | },
108 |
109 | // method to get pod log
110 | getKubeBenchPodLog: async (podName: string) => {
111 | // return a promise of type string which will be the log of the kube bench pod
112 | return await new Promise((resolve, reject) => {
113 | // define command to display kube bench pod log
114 | const command = `kubectl logs ${podName}`;
115 |
116 | // run command with exec
117 | exec(command, (error, stdout) => {
118 | if (error !== null) {
119 | // error handling
120 | console.log('Error getting kube bench pod log.');
121 | // change state of promise to 'rejected' and pass in reason (error)
122 | reject(error);
123 | }
124 |
125 | // trim whitespace from output
126 | const kubeBenchOutput = stdout.trim();
127 |
128 | // change state of promise to 'fulfilled' and pass in fulfillment value (kubeBenchOutput)
129 | resolve(kubeBenchOutput);
130 | });
131 | });
132 | },
133 |
134 | // method to write output.txt file
135 | writeOutputData: (content: string, next: NextFunction) => {
136 | // use fs to write kube bench output to output.txt
137 | fs.writeFile('output.txt', content, (error) => {
138 | if (error !== null) {
139 | // error handling
140 | console.log('Error writing output file.');
141 | next(error);
142 | }
143 | console.log('Output file written successfully.');
144 | });
145 | },
146 |
147 | filterOutputData: (outputData: string[]) => {
148 | // initialize test results array
149 | const allTestResults: string[] = [];
150 |
151 | // iterate through outputData and store test results in test results array
152 | outputData.forEach((line) => {
153 | if (
154 | line.includes('[PASS]') ||
155 | line.includes('[WARN]') ||
156 | line.includes('[FAIL]')
157 | ) {
158 | allTestResults.push(line);
159 | }
160 | });
161 |
162 | // initialize index variables to slice sections from testLines
163 | // SECTION 1 INDICES
164 | const cpsctStart: indexObjectType = {
165 | position: 0,
166 | assigned: false
167 | };
168 | const cpsctEnd: indexObjectType = {
169 | position: 0,
170 | assigned: false
171 | };
172 |
173 | // SECTION 2 INDICES
174 | const encStart: indexObjectType = {
175 | position: 0,
176 | assigned: false
177 | };
178 | const encEnd: indexObjectType = {
179 | position: 0,
180 | assigned: false
181 | };
182 |
183 | // SECTION 3 INDICES
184 | const cpcStart: indexObjectType = {
185 | position: 0,
186 | assigned: false
187 | };
188 | const cpcEnd: indexObjectType = {
189 | position: 0,
190 | assigned: false
191 | };
192 |
193 | // SECTION 4 INDICES
194 | const wnscStart: indexObjectType = {
195 | position: 0,
196 | assigned: false
197 | };
198 | const wnscEnd: indexObjectType = {
199 | position: 0,
200 | assigned: false
201 | };
202 |
203 | // SECTION 5 INDICES
204 | const kpStart: indexObjectType = {
205 | position: 0,
206 | assigned: false
207 | };
208 | const kpEnd: indexObjectType = {
209 | position: 0,
210 | assigned: false
211 | };
212 |
213 | // isolate test results by section -> find index positions to slice test results array
214 | for (let index = 0; index < allTestResults.length; index += 1) {
215 | // SECTION 1: Control Plane Security Configuration
216 | // find first index position to slice from testLines array
217 | if (
218 | allTestResults[index].includes('1.1.1') &&
219 | !cpsctStart.assigned
220 | ) {
221 | cpsctStart.position = index;
222 | cpsctStart.assigned = true;
223 | }
224 | // find last index position to slice from testLines array
225 | if (
226 | allTestResults[index].includes('1.4.2') &&
227 | !cpsctEnd.assigned
228 | ) {
229 | cpsctEnd.position = index;
230 | cpsctEnd.assigned = true;
231 | }
232 |
233 | // SECTION 2: Etcd Node Configuration
234 | // find first index position to slice from testLines array
235 | if (
236 | allTestResults[index].includes(
237 | '2.1 Ensure that the --cert-file and --key-file arguments'
238 | ) &&
239 | !encStart.assigned
240 | ) {
241 | encStart.position = index;
242 | encStart.assigned = true;
243 | }
244 | // find last index position to slice from testLines array
245 | if (
246 | allTestResults[index].includes(
247 | '2.7 Ensure that a unique Certificate Authority'
248 | ) &&
249 | !encEnd.assigned
250 | ) {
251 | encEnd.position = index;
252 | encEnd.assigned = true;
253 | }
254 |
255 | // SECTION 3: Control Plane Configuration
256 | // find first index position to slice from testLines array
257 | if (
258 | allTestResults[index].includes('3.1.1') &&
259 | !cpcStart.assigned
260 | ) {
261 | cpcStart.position = index;
262 | cpcStart.assigned = true;
263 | }
264 | // find last index position to slice from testLines array
265 | if (
266 | allTestResults[index].includes('3.2.2') &&
267 | !cpcEnd.assigned
268 | ) {
269 | cpcEnd.position = index;
270 | cpcEnd.assigned = true;
271 | }
272 |
273 | // SECTION 4: Worker Node Security Configuration
274 | // find first index position to slice from testLines array
275 | if (
276 | allTestResults[index].includes('4.1.1') &&
277 | !wnscStart.assigned
278 | ) {
279 | wnscStart.position = index;
280 | wnscStart.assigned = true;
281 | }
282 | // find last index position to slice from testLines array
283 | if (
284 | allTestResults[index].includes('4.2.13') &&
285 | !wnscEnd.assigned
286 | ) {
287 | wnscEnd.position = index;
288 | wnscEnd.assigned = true;
289 | }
290 |
291 | // SECTION 5: Kubernetes Policies
292 | // find first index position to slice from testLines array
293 | if (
294 | allTestResults[index].includes('5.1.1') &&
295 | !kpStart.assigned
296 | ) {
297 | kpStart.position = index;
298 | kpStart.assigned = true;
299 | }
300 | // find last index position to slice from testLines array
301 | if (
302 | allTestResults[index].includes('5.7.4') &&
303 | !kpEnd.assigned
304 | ) {
305 | kpEnd.position = index;
306 | kpEnd.assigned = true;
307 | }
308 | }
309 |
310 | // create object to store testResults array, remediations array, and summary array for the given section
311 | const controlPlaneSecurityConfiguration: sectionResultsInfo = {
312 | testResults: allTestResults.slice(
313 | cpsctStart.position,
314 | cpsctEnd.position + 1
315 | ),
316 | remediations: securityController.condenseRemediations(
317 | outputData.slice(
318 | outputData.indexOf('== Remediations master =='),
319 | outputData.indexOf('== Summary master ==')
320 | )
321 | ),
322 | summary: outputData.slice(
323 | outputData.indexOf('== Summary master =='),
324 | outputData.indexOf('== Summary master ==') + 5
325 | )
326 | };
327 |
328 | // create object to store testResults array, remediations array, and summary array for the given section
329 | const etcdNodeConfiguration: sectionResultsInfo = {
330 | testResults: allTestResults.slice(encStart.position, encEnd.position + 1),
331 | remediations: securityController.condenseRemediations(
332 | outputData.slice(
333 | outputData.indexOf('== Remediations etcd =='),
334 | outputData.indexOf('== Summary etcd ==')
335 | )
336 | ),
337 | summary: outputData.slice(
338 | outputData.indexOf('== Summary etcd =='),
339 | outputData.indexOf('== Summary etcd ==') + 5
340 | )
341 | };
342 |
343 | // create object to store testResults array, remediations array, and summary array for the given section
344 | const controlPlaneConfiguration: sectionResultsInfo = {
345 | testResults: allTestResults.slice(cpcStart.position, cpcEnd.position + 1),
346 | remediations: securityController.condenseRemediations(
347 | outputData.slice(
348 | outputData.indexOf('== Remediations controlplane =='),
349 | outputData.indexOf('== Summary controlplane ==')
350 | )
351 | ),
352 | summary: outputData.slice(
353 | outputData.indexOf('== Summary controlplane =='),
354 | outputData.indexOf('== Summary controlplane ==') + 5
355 | )
356 | };
357 |
358 | // create object to store testResults array, remediations array, and summary array for the given section
359 | const workerNodeSecurity: sectionResultsInfo = {
360 | testResults: allTestResults.slice(
361 | wnscStart.position,
362 | wnscEnd.position + 1
363 | ),
364 | remediations: securityController.condenseRemediations(
365 | outputData.slice(
366 | outputData.indexOf('== Remediations node =='),
367 | outputData.indexOf('== Summary node ==')
368 | )
369 | ),
370 | summary: outputData.slice(
371 | outputData.indexOf('== Summary node =='),
372 | outputData.indexOf('== Summary node ==') + 5
373 | )
374 | };
375 |
376 | // create object to store testResults array, remediations array, and summary array for the given section
377 | const kubernetesPolicies: sectionResultsInfo = {
378 | testResults: allTestResults.slice(kpStart.position, kpEnd.position + 1),
379 | remediations: securityController.condenseRemediations(
380 | outputData.slice(
381 | outputData.indexOf('== Remediations policies =='),
382 | outputData.indexOf('== Summary policies ==')
383 | )
384 | ),
385 | summary: outputData.slice(
386 | outputData.indexOf('== Summary policies =='),
387 | outputData.indexOf('== Summary policies ==') + 5
388 | )
389 | };
390 |
391 | // create array of total summary remediations
392 | const totalSummary: string[] = outputData.slice(
393 | outputData.indexOf('== Summary total =='),
394 | outputData.indexOf('== Summary total ==') + 5
395 | );
396 |
397 | // create all test info object to return to runKubeBench to be sent to front end
398 | // this object stores all the objects we created for individual sections
399 | const allTestInfo: allTestInfoType = {
400 | controlPlaneSecurityConfiguration,
401 | etcdNodeConfiguration,
402 | controlPlaneConfiguration,
403 | workerNodeSecurity,
404 | kubernetesPolicies,
405 | totalSummary
406 | };
407 |
408 | // return allTestInfo object to be sent to front end
409 | return allTestInfo;
410 | },
411 |
412 | filterOutputDataEKS: (outputData: string[]) => {
413 | // initialize test results array
414 | const allTestResults: string[] = [];
415 |
416 | // iterate through outputData and store test results in test results array
417 | outputData.forEach((line) => {
418 | if (
419 | line.includes('[PASS]') ||
420 | line.includes('[WARN]') ||
421 | line.includes('[FAIL]')
422 | ) {
423 | allTestResults.push(line);
424 | }
425 | });
426 |
427 | // initialize index variables to slice sections from testLines
428 | // SECTION INDICES
429 | const wnscStart: indexObjectType = {
430 | position: 0,
431 | assigned: false
432 | };
433 | const wnscEnd: indexObjectType = {
434 | position: 0,
435 | assigned: false
436 | };
437 |
438 | // isolate test results by section -> find index positions to slice test results array
439 | for (let index = 0; index < allTestResults.length; index += 1) {
440 | // SECTION 4: Worker Node Security Configuration
441 | // find first index position to slice from testLines array
442 | if (
443 | allTestResults[index].includes('3.1.1') &&
444 | !wnscStart.assigned
445 | ) {
446 | wnscStart.position = index;
447 | wnscStart.assigned = true;
448 | }
449 | // find last index position to slice from testLines array
450 | if (
451 | allTestResults[index].includes('3.3.1') &&
452 | !wnscEnd.assigned
453 | ) {
454 | wnscEnd.position = index;
455 | wnscEnd.assigned = true;
456 | }
457 | }
458 |
459 | // create object to store testResults array, remediations array, and summary array for the given section
460 | const workerNodeSecurity: sectionResultsInfo = {
461 | testResults: allTestResults.slice(
462 | wnscStart.position,
463 | wnscEnd.position + 1
464 | ),
465 | remediations: securityController.condenseRemediations(
466 | outputData.slice(
467 | outputData.indexOf('== Remediations node =='),
468 | outputData.indexOf('== Summary node ==')
469 | )
470 | ),
471 | summary: outputData.slice(
472 | outputData.indexOf('== Summary node =='),
473 | outputData.indexOf('== Summary node ==') + 5
474 | )
475 | };
476 |
477 | const totalSummary: string[] = outputData.slice(
478 | outputData.indexOf('== Summary total =='),
479 | outputData.indexOf('== Summary total ==') + 5
480 | );
481 |
482 | // create all test info object to return to runKubeBench to be sent to front end
483 | // this object stores all the objects we created for individual sections
484 | const allTestInfoEKS: allTestInfoEKSType = {
485 | workerNodeSecurity,
486 | totalSummary
487 | };
488 |
489 | // return allTestInfo object to be sent to front end
490 | return allTestInfoEKS;
491 | },
492 |
493 | condenseRemediations: (remediationsArr: string[]) => {
494 | // initialize condensed remediations array
495 | const condensedRemediations: string[] = [];
496 | // declare combined string variable
497 | let combinedString = '';
498 |
499 | // iterate over remediations array
500 | for (let index = 1; index < remediationsArr.length; index += 1) {
501 | // check if the element is empty string
502 | if (remediationsArr[index] === '') {
503 | // check if combined string has been reassigned
504 | if (combinedString !== '') {
505 | condensedRemediations.push(combinedString);
506 | combinedString = '';
507 | }
508 | } else {
509 | // add element to combined string variable
510 | combinedString += remediationsArr[index];
511 | }
512 | }
513 | // check if combined string has been reassigned and push to array if so
514 | if (combinedString !== '') condensedRemediations.push(combinedString);
515 | // return condensed remediations array
516 | return condensedRemediations;
517 | }
518 | };
519 |
520 | export default securityController;
521 |
--------------------------------------------------------------------------------
/src/server/routers/apiRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import clusterRouter from './clusterRouter';
3 | import securityRouter from './securityRouter';
4 | import prometheusRouter from './prometheusRouter';
5 | import k6Router from './k6Router';
6 |
7 | // declare express router
8 | const router = express.Router();
9 |
10 | // set up routes to sub-routers for specific use cases
11 | router.use('/cluster', clusterRouter);
12 | router.use('/security', securityRouter);
13 | router.use('/prom', prometheusRouter);
14 | router.use('/k6', k6Router);
15 |
16 | export default router;
17 |
--------------------------------------------------------------------------------
/src/server/routers/clusterRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import type { Request, Response } from 'express';
3 | import clusterController from '../controllers/clusterController';
4 |
5 | const router = express.Router();
6 |
7 | // get request for namespace list
8 | router.get(
9 | '/namespaces',
10 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
11 | clusterController.getNamespaces,
12 | (req: Request, res: Response) => {
13 | res.status(200).json(res.locals.namespaceList);
14 | }
15 | );
16 |
17 | // get request for node list
18 | router.get(
19 | '/node/:namespace',
20 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
21 | clusterController.getNodes,
22 | (req: Request, res: Response) => {
23 | res.status(200).json(res.locals.filteredNodeList);
24 | }
25 | );
26 |
27 | // get request for pod list
28 | router.get(
29 | '/pod/:nodeName',
30 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
31 | clusterController.getPods,
32 | (req: Request, res: Response) => {
33 | res.status(200).json(res.locals.filteredPodList);
34 | }
35 | );
36 |
37 | export default router;
38 |
--------------------------------------------------------------------------------
/src/server/routers/k6Router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import type { Request, Response } from 'express';
3 | import k6Controller from '../controllers/k6Controller';
4 |
5 | const router = express.Router();
6 |
7 | // get request to run autocaling test with k6
8 | router.get(
9 | '/autoscale',
10 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
11 | k6Controller.runScalingTest,
12 | (req: Request, res: Response) => {
13 | res.sendStatus(200);
14 | }
15 | );
16 |
17 | export default router;
18 |
--------------------------------------------------------------------------------
/src/server/routers/prometheusRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import type { Request, Response } from 'express';
3 | import prometheusController from '../controllers/prometheusController';
4 |
5 | const router = express.Router();
6 |
7 | // post request to grab static metrics
8 | // use post request so we can set request body to an array of strings to query prometheus with
9 | router.post(
10 | '/metrics/static',
11 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
12 | prometheusController.getStaticMetrics,
13 | (req: Request, res: Response) => {
14 | res.status(200).json(res.locals.staticMetrics);
15 | }
16 | );
17 |
18 | router.post(
19 | '/metrics/dynamic',
20 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
21 | prometheusController.getDynamicMetrics,
22 | (req: Request, res: Response) => {
23 | res.status(200).json(res.locals.dynamicMetrics);
24 | }
25 | );
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/src/server/routers/securityRouter.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from 'express';
2 | import express from 'express';
3 | import localSecurityController from '../controllers/securityController';
4 |
5 | const router = express.Router();
6 |
7 | router.get(
8 | '/local/cis',
9 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
10 | localSecurityController.runKubeBenchLocal,
11 | (req: Request, res: Response) => {
12 | res.status(200).json(res.locals.allTestInfo);
13 | }
14 | );
15 |
16 | router.get(
17 | '/eks/cis',
18 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
19 | localSecurityController.runKubeBenchEKS,
20 | (req: Request, res: Response) => {
21 | res.status(200).json(res.locals.allTestInfoEKS);
22 | }
23 | );
24 |
25 | export default router;
26 |
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | // ------------------IMPORTS------------------//
2 | import express from 'express';
3 | import type { Request, Response, NextFunction } from 'express';
4 | import type { ServerError } from '../../types';
5 | import apiRouter from '../server/routers/apiRouter';
6 | import path from 'path';
7 |
8 | // ------------------SET UP EXPRESS APP------------------//
9 | // declare port
10 | const PORT = 9000;
11 |
12 | // declare app with express invocation
13 | const app = express();
14 |
15 | // parse requests & serve static files
16 | app.use(express.json());
17 | app.use(express.static(path.resolve(__dirname, '../client')));
18 |
19 | // ------------------ROUTE HANDLING------------------//
20 | // set up api router
21 | app.use('/api', apiRouter);
22 |
23 | // ------------------REACT ROUTER------------------//
24 | app.use('*', (req: Request, res: Response, next: NextFunction) => {
25 | res
26 | .status(200)
27 | .sendFile(path.resolve(__dirname, '../client/index.html'));
28 | });
29 |
30 | // ------------------ERROR HANDLERS------------------//
31 | // Global catch-all
32 | app.use((err: ServerError, req: Request, res: Response, next: NextFunction) => {
33 | const defaultErr = {
34 | log: 'Error caught in global handler',
35 | status: 500,
36 | message: { err: 'An error occurred' }
37 | };
38 | const errorObj = Object.assign({}, defaultErr, err);
39 | console.log(errorObj.log);
40 | console.log(err);
41 | return res.status(errorObj.status).json(errorObj.message);
42 | });
43 |
44 | // ------------------SERVER LISTENER------------------//
45 | // declare port & listen
46 | app.listen(PORT, () => {
47 | console.log(`App listening on port ${PORT}`);
48 | });
49 |
50 | // ------------------APP EXPORT------------------//
51 | // export app
52 | module.exports = app;
53 |
--------------------------------------------------------------------------------
/tests/cis/job-eks.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: batch/v1
3 | kind: Job
4 | metadata:
5 | name: kube-bench
6 | spec:
7 | template:
8 | metadata:
9 | labels: {
10 | 'container-uid': 81a54e92-18b5-4a67-ab5a-781110f66e7e
11 | }
12 | spec:
13 | hostPID: true
14 | containers:
15 | - name: kube-bench
16 | # Push the image to your ECR and then refer to it here
17 | image: 979439831614.dkr.ecr.us-east-2.amazonaws.com/k8s/kube-bench:latest
18 | # image: docker.io/aquasec/kube-bench:latest
19 | # To send findings to AWS Security Hub, refer to `job-eks-asff.yaml` instead
20 | command:
21 | [
22 | "kube-bench",
23 | "run",
24 | "--targets",
25 | "node",
26 | "--benchmark",
27 | "eks-1.2.0",
28 | ]
29 | volumeMounts:
30 | - name: var-lib-kubelet
31 | mountPath: /var/lib/kubelet
32 | readOnly: true
33 | - name: etc-systemd
34 | mountPath: /etc/systemd
35 | readOnly: true
36 | - name: etc-kubernetes
37 | mountPath: /etc/kubernetes
38 | readOnly: true
39 | restartPolicy: Never
40 | volumes:
41 | - name: var-lib-kubelet
42 | hostPath:
43 | path: "/var/lib/kubelet"
44 | - name: etc-systemd
45 | hostPath:
46 | path: "/etc/systemd"
47 | - name: etc-kubernetes
48 | hostPath:
49 | path: "/etc/kubernetes"
--------------------------------------------------------------------------------
/tests/cis/job.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: batch/v1
3 | kind: Job
4 | metadata:
5 | name: kube-bench
6 | spec:
7 | template:
8 | metadata:
9 | labels:
10 | app: kube-bench
11 | spec:
12 | hostPID: true
13 | containers:
14 | - name: kube-bench
15 | image: docker.io/aquasec/kube-bench:v0.6.15
16 | command: ['kube-bench']
17 | volumeMounts:
18 | - name: var-lib-etcd
19 | mountPath: /var/lib/etcd
20 | readOnly: true
21 | - name: var-lib-kubelet
22 | mountPath: /var/lib/kubelet
23 | readOnly: true
24 | - name: var-lib-kube-scheduler
25 | mountPath: /var/lib/kube-scheduler
26 | readOnly: true
27 | - name: var-lib-kube-controller-manager
28 | mountPath: /var/lib/kube-controller-manager
29 | readOnly: true
30 | - name: etc-systemd
31 | mountPath: /etc/systemd
32 | readOnly: true
33 | - name: lib-systemd
34 | mountPath: /lib/systemd/
35 | readOnly: true
36 | - name: srv-kubernetes
37 | mountPath: /srv/kubernetes/
38 | readOnly: true
39 | - name: etc-kubernetes
40 | mountPath: /etc/kubernetes
41 | readOnly: true
42 | # /usr/local/mount-from-host/bin is mounted to access kubectl / kubelet, for auto-detecting the Kubernetes version.
43 | # You can omit this mount if you specify --version as part of the command.
44 | - name: usr-bin
45 | mountPath: /usr/local/mount-from-host/bin
46 | readOnly: true
47 | - name: etc-cni-netd
48 | mountPath: /etc/cni/net.d/
49 | readOnly: true
50 | - name: opt-cni-bin
51 | mountPath: /opt/cni/bin/
52 | readOnly: true
53 | restartPolicy: Never
54 | volumes:
55 | - name: var-lib-etcd
56 | hostPath:
57 | path: '/var/lib/etcd'
58 | - name: var-lib-kubelet
59 | hostPath:
60 | path: '/var/lib/kubelet'
61 | - name: var-lib-kube-scheduler
62 | hostPath:
63 | path: '/var/lib/kube-scheduler'
64 | - name: var-lib-kube-controller-manager
65 | hostPath:
66 | path: '/var/lib/kube-controller-manager'
67 | - name: etc-systemd
68 | hostPath:
69 | path: '/etc/systemd'
70 | - name: lib-systemd
71 | hostPath:
72 | path: '/lib/systemd'
73 | - name: srv-kubernetes
74 | hostPath:
75 | path: '/srv/kubernetes'
76 | - name: etc-kubernetes
77 | hostPath:
78 | path: '/etc/kubernetes'
79 | - name: usr-bin
80 | hostPath:
81 | path: '/usr/bin'
82 | - name: etc-cni-netd
83 | hostPath:
84 | path: '/etc/cni/net.d/'
85 | - name: opt-cni-bin
86 | hostPath:
87 | path: '/opt/cni/bin/'
88 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "ES5",
5 | "module": "commonjs",
6 | "outDir": "dist",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node"
11 | },
12 | "include": [
13 | "src",
14 | ],
15 | "exclude": [
16 | "dist",
17 | "node_modules"
18 | ]
19 | }
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import type { V1Container, V1ContainerImage, V1PodIP } from '@kubernetes/client-node';
2 | import type { Request, Response, NextFunction } from 'express';
3 | export interface ServerError {
4 | err: '400'
5 | }
6 |
7 | export interface clusterControllerType {
8 | getPods: (req: Request, res: Response, next: NextFunction) => Promise
9 | getNodes: (req: Request, res: Response, next: NextFunction) => Promise
10 | getNamespaces: (req: Request, res: Response, next: NextFunction) => Promise
11 | }
12 |
13 | export interface indexObjectType {
14 | position: number
15 | assigned: boolean
16 | }
17 |
18 | export interface sectionResultsInfo {
19 | testResults: string[]
20 | remediations: string[]
21 | summary: string[]
22 | }
23 |
24 | export interface allTestInfoType {
25 | controlPlaneSecurityConfiguration: sectionResultsInfo
26 | etcdNodeConfiguration: sectionResultsInfo
27 | controlPlaneConfiguration: sectionResultsInfo
28 | workerNodeSecurity: sectionResultsInfo
29 | kubernetesPolicies: sectionResultsInfo
30 | totalSummary: string[]
31 | }
32 |
33 | export interface allTestInfoEKSType {
34 | workerNodeSecurity: sectionResultsInfo
35 | totalSummary: string[]
36 | }
37 |
38 | export interface namespaceObject {
39 | name: string
40 | uid: string
41 | status: string
42 | }
43 |
44 | export interface containerObject {
45 | env?: Array<{
46 | name?: string
47 | value?: string
48 | valueFrom?: {
49 | fieldRef?: {
50 | apiVersion?: string
51 | fieldPath?: string
52 | }
53 | secretKeyRef?: {
54 | key?: string
55 | name?: string
56 | }
57 | }
58 | }>
59 | image?: string
60 | imagePullPolicy?: string
61 | livenessProbe?: {
62 | failureThreshold: number
63 | httpGet: {
64 | path: string
65 | port: number
66 | scheme: string
67 | }
68 | initialDelaySeconds: number
69 | periodSeconds: number
70 | successThreshold: number
71 | timeoutSeconds: number
72 | }
73 | name?: string
74 | ports?: Array<{
75 | containerPort?: number
76 | name?: string
77 | protocol?: string
78 | }>
79 | readinessProbe?: {
80 | failureThreshold?: number
81 | httpGet?: {
82 | path: string
83 | port: number
84 | scheme: string
85 | }
86 | periodSeconds?: number
87 | successThreshold?: number
88 | timeoutSeconds?: number
89 | }
90 | resources?: Record // -> ???
91 | securityContext?: {
92 | allowPrivilegeEscalation: boolean
93 | capabilities: {
94 | drop: string[]
95 | }
96 | seccompProfile: {
97 | type: string
98 | }
99 | }
100 | terminationMessagePath?: string
101 | terminationMessagePolicy?: string
102 | volumeMounts?: Array<{
103 | mountPath: string
104 | name: string
105 | subPath?: string
106 | readOnly?: boolean
107 | }>
108 | }
109 |
110 | export interface newPodObject {
111 | nodeName: string | undefined
112 | podName: string | undefined
113 | uid: string | undefined
114 | containers: V1Container[] | undefined
115 | hostIP: string | undefined
116 | phase: string | undefined
117 | podIPs: V1PodIP[] | undefined
118 | }
119 |
120 | export interface newNodeObject {
121 | name: string | undefined
122 | uid: string | undefined
123 | podCIDRs: string[] | undefined
124 | addresses: addressObject[] | undefined
125 | allocatable: Record | undefined
126 | // allocatable: allocatableObject
127 | capacity: Record | undefined
128 | // capacity: capacityObject
129 | images: V1ContainerImage[] | undefined
130 | }
131 |
132 | export type nodeObjectList = Record
133 |
134 | export interface addressObject {
135 | address: string
136 | type: string
137 | }
138 |
139 | // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
140 | export interface allocatableObject {
141 | cpu: string
142 | 'ephemeral-storage': string
143 | 'hugepages-1Gi': string
144 | 'hugepages-2Mi': string
145 | 'hugepages-32Mi': string
146 | 'hugepages-64Ki': string
147 | memory: string
148 | pods: string
149 | }
150 |
151 | export interface capacityObject {
152 | cpu: string
153 | 'ephemeral-storage': string
154 | 'hugepages-1Gi': string
155 | 'hugepages-2Mi': string
156 | 'hugepages-32Mi': string
157 | 'hugepages-64Ki': string
158 | memory: string
159 | pods: string
160 | }
161 |
162 | export interface nodeCardProps {
163 | key: string
164 | name: string
165 | uid: string
166 | podCIDRs: string[]
167 | addresses: addressObject[]
168 | allocatable: Record | undefined
169 | capacity: Record | undefined
170 | images: V1ContainerImage[]
171 | togglePods: (event: React.MouseEvent) => void
172 | }
173 |
174 | export interface podCardProps {
175 | nodeName?: string
176 | podName?: string
177 | uid?: string
178 | containers: V1Container[]
179 | hostIP?: string
180 | phase?: string
181 | podIPs: V1PodIP[]
182 | podsInNode?: any
183 | }
184 |
185 | export interface livenessProbeObject {
186 | failureThreshold?: number
187 | httpGet?: {
188 | path: string
189 | port: number
190 | scheme: string
191 | }
192 | initialDelaySeconds?: number
193 | periodSeconds?: number
194 | successThreshold?: number
195 | timeoutSeconds?: number
196 | }
197 |
198 | export interface volumeMount {
199 | mountPath: string
200 | name: string
201 | subPath?: string
202 | readOnly?: boolean
203 | }
204 |
205 | export interface newDynamicPromObject {
206 | container?: string
207 | pod?: string
208 | queryName?: string
209 | name?: string
210 | value?: string
211 | }
212 |
213 | export interface dynamicPromQueryObject {
214 | metric: {
215 | __name__?: string
216 | alpha_eksctl_io_cluster_name?: string
217 | alpha_eksctl_io_nodegroup_name?: string
218 | beta_kubernetes_io_arch?: string
219 | beta_kubernetes_io_instance_type?: string
220 | beta_kubernetes_io_os?: string
221 | container?: string
222 | eks_amazonaws_com_capacityType?: string
223 | eks_amazonaws_com_nodegroup?: string
224 | eks_amazonaws_com_nodegroup_image?: string
225 | eks_amazonaws_com_sourceLaunchTemplateId?: string
226 | eks_amazonaws_com_sourceLaunchTemplateVersion?: string
227 | failure_domain_beta_kubernetes_io_region?: string
228 | failure_domain_beta_kubernetes_io_zone?: string
229 | id?: string
230 | image?: string
231 | instance?: string
232 | job?: string
233 | k8s_io_cloud_provider_aws?: string
234 | kubernetes_io_arch?: string
235 | kubernetes_io_hostname?: string
236 | kubernetes_io_os?: string
237 | name?: string
238 | namespace?: string
239 | node_kubernetes_io_instance_type?: string
240 | pod?: string
241 | topology_ebs_csi_aws_com_zone?: string
242 | topology_kubernetes_io_region?: string
243 | topology_kubernetes_io_zone?: string
244 | }
245 | value?: [number, string]
246 | }
247 |
248 | export interface staticPromQueryObject {
249 | metric: {
250 | __name__?: string
251 | alpha_eksctl_io_cluster_name?: string
252 | alpha_eksctl_io_nodegroup_name?: string
253 | beta_kubernetes_io_arch?: string
254 | beta_kubernetes_io_instance_type?: string
255 | beta_kubernetes_io_os?: string
256 | boot_id?: string
257 | eks_amazonaws_com_capacityType?: string
258 | eks_amazonaws_com_nodegroup?: string
259 | eks_amazonaws_com_nodegroup_image?: string
260 | eks_amazonaws_com_sourceLaunchTemplateId?: string
261 | eks_amazonaws_com_sourceLaunchTemplateVersion?: string
262 | failure_domain_beta_kubernetes_io_region?: string
263 | failure_domain_beta_kubernetes_io_zone?: string
264 | instance?: string
265 | job?: string
266 | }
267 | value?: [number, string]
268 | }
269 |
270 | export interface newStaticPromObject {
271 | queryName?: string
272 | value?: string
273 | instance?: string
274 | }
275 |
276 | export interface datasetsType {
277 | label: string
278 | data: string[]
279 | backgroundColor: string
280 | }
281 |
282 | export interface chartType {
283 | labels: string[]
284 | datasets: datasetsType[]
285 | }
286 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/client/index.tsx',
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | filename: 'bundle.js'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.(ts|tsx)$/,
15 | exclude: /node_modules/,
16 | use: ['ts-loader']
17 | },
18 | {
19 | test: /\.(js|jsx)$/,
20 | exclude: /node_modules/,
21 | use: ['babel-loader']
22 | },
23 | {
24 | test: /\.s?css$/i,
25 | use: ['style-loader', 'css-loader', 'sass-loader']
26 | }
27 | ]
28 | },
29 | resolve: {
30 | extensions: ['.tsx', '.ts', '.js', 'jsx']
31 | },
32 | plugins: [
33 | new HtmlWebpackPlugin({
34 | template: './src/client/index.html',
35 | filename: './index.html'
36 | })
37 | ],
38 | devServer: {
39 | static: {
40 | directory: path.join(__dirname, './dist')
41 | },
42 | proxy: {
43 | '/api': 'http://localhost:9000',
44 | secure: false
45 | }
46 | }
47 | };
48 |
--------------------------------------------------------------------------------