├── .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 | ![Brizo Banner](https://github.com/oslabs-beta/Brizo/blob/dev/assets/brizo-high-resolution-color-logo.png?raw=true) 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 |
8 | 9 | ReactJs 10 | 11 | 12 | Typescript 13 | 14 | 15 | ChartJS 16 | 17 | 18 | ExpressJS 19 | 20 | 21 | NPM 22 | 23 | 24 | React Router 25 | 26 | 27 | SASS 28 | 29 | 30 | Webpack 31 | 32 | 33 | AWS 34 | 35 | 36 | MacOS 37 | 38 | 39 | Windows 40 | 41 | 42 | Docker 43 | 44 | 45 | Eslint 46 | 47 | 48 | kubernetes 49 | 50 |
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 | 223 | 228 | 229 | 230 | 233 | 238 | 239 | 240 | 243 | 248 | 249 | 250 | 253 | 258 | 259 | 260 | 263 | 268 | 269 | 270 |
221 | 222 | 224 | Cortland Young
225 | GitHub
226 | LinkedIn 227 |
231 | 232 | 234 | Owen Hill
235 | GitHub
236 | LinkedIn 237 |
241 | 242 | 244 | Jimmy Tran
245 | GitHub
246 | LinkedIn 247 |
251 | 252 | 254 | Weston Schott
255 | GitHub
256 | LinkedIn 257 |
261 | 262 | 264 | Anna Yu
265 | GitHub
266 | LinkedIn 267 |
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 | 36 | {showMore && <> 37 |
38 | {summary.map((summaryText: string, index) => { 39 | return ( 40 |
41 | {renderResult(summaryText)} 42 |
43 | ); 44 | })} 45 |
46 |
47 | 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 | 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 | 23 | 24 | 25 |
26 |
27 | 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 =
    {key}: {value}
; 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 |
    {item}
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 |
    {ipAddresses.ip}
94 | )); 95 | 96 | return ( 97 |
98 |
{podName}
99 |
uid: {uid}
100 |
    Status: {phase}
101 |
    node: {nodeName}
102 |
103 | Pod IP(s): 104 |
    {renderPodIps()}
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 | 95 | 96 | 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 | 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 | --------------------------------------------------------------------------------