Skip to content

Commit

Permalink
- Updated extension to use newer @kubescape/install (new version and …
Browse files Browse the repository at this point in the history
…report format)

- Cluster scan output is redirected to a file instead of stdout buffer, should fix a bug dealing with large reports
  • Loading branch information
amirmalka committed Nov 24, 2022
1 parent 5b32466 commit b7ae8a5
Show file tree
Hide file tree
Showing 10 changed files with 921 additions and 602 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This extension brings out the power of [Kubescape][kubescape] into [Lens][lens]
| 5.4 | 0.1.x |
| 5.5 | 0.1.x |
| 6.0 | 0.2.x |
| 6.1 | 0.2.x |

## Installation

Expand Down
1,258 changes: 766 additions & 492 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kubescape/lens-extension",
"version": "0.2.3",
"version": "0.2.4",
"author": {
"name": "Kubescape Team at Armo"
},
Expand All @@ -27,27 +27,27 @@
"clean": "rm -rf ./dist"
},
"dependencies": {
"@kubescape/install": "^0.3.11",
"@kubescape/install": "^0.4.0",
"abort-controller": "^3.0.0",
"node-fetch": "^2.6.7"
"node-fetch": "^3.3.0"
},
"devDependencies": {
"@k8slens/extensions": "^6.0.0",
"@types/node": "^18.6.2",
"@k8slens/extensions": "^6.1.9",
"@types/node": "^14.17.14",
"@types/node-fetch": "^2.6.2",
"@types/react": "^18.0.9",
"@types/react-dom": "^17.0.2",
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.16",
"css-loader": "^6.7.1",
"file-loader": "^6.2.0",
"mobx": "^6.6.1",
"mobx-react": "^7.5.2",
"mobx": "^6.5.0",
"mobx-react": "^7.6.0",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.54.0",
"sass-loader": "^13.0.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
"ts-loader": "^9.4.1",
"typescript": "^4.0.3",
"url-loader": "^4.1.1",
"webpack": "^5.74.0",
Expand Down
24 changes: 12 additions & 12 deletions src/components/KubescapeControlDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";

import { Renderer } from "@k8slens/extensions";
import { observable, makeObservable, computed, reaction, runInAction } from "mobx";
import { observable, makeObservable, computed, reaction, runInAction, toJS } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"
import { KubescapeControl } from "../kubescape/types";
import { docsUrl, getRelatedObjectsFromControl } from "../kubescape/controlUtils";
Expand Down Expand Up @@ -64,16 +64,16 @@ export class KubescapeControlDetails extends React.Component<{ control?: Kubesca
return null;
}

const relatedObjects = getRelatedObjectsFromControl(this.control.rawResult)
const relatedObjects = getRelatedObjectsFromControl(this.control, this.store.kubescapeResultMap, this.store.kubescapeRawResourceMap)
return await Promise.all(relatedObjects.map(async (entity) => {
if (entity.metadata?.uid) {
const kubeObject = await this.store.getKubeObject(entity.metadata.namespace, entity.kind, entity.apiVersion, entity.metadata.uid)
return new RelatedResource({
id: entity.metadata.uid,
name: kubeObject.getName(),
kind: kubeObject.kind,
namespace: kubeObject.metadata.namespace,
detailsUrl: Renderer.Navigation.getDetailsUrl(kubeObject.selfLink)
name: entity.metadata.name,
kind: entity.kind,
namespace: entity.metadata.namespace,
detailsUrl: kubeObject ? Renderer.Navigation.getDetailsUrl(kubeObject.selfLink) : null
});
}

Expand All @@ -87,13 +87,13 @@ export class KubescapeControlDetails extends React.Component<{ control?: Kubesca
const kubeObject = await this.store.getKubeObject(relatedObject.metadata.namespace, relatedObject.kind, relatedObject.apiVersion, relatedObject.metadata.uid)
if (relatedObject.kind.includes("RoleBinding")) {
roleBasedResource.roleBinding = new RelatedResource({
name: kubeObject.getName(),
detailsUrl: Renderer.Navigation.getDetailsUrl(kubeObject.selfLink)
name: kubeObject?.getName() ?? relatedObject.metadata.name,
detailsUrl: kubeObject ? Renderer.Navigation.getDetailsUrl(kubeObject.selfLink) : null
});
} else {
roleBasedResource.role = new RelatedResource({
name: kubeObject.getName(),
detailsUrl: Renderer.Navigation.getDetailsUrl(kubeObject.selfLink)
name: kubeObject?.getName() ?? relatedObject.metadata.name,
detailsUrl: kubeObject ? Renderer.Navigation.getDetailsUrl(kubeObject.selfLink) : null
});
}
}
Expand Down Expand Up @@ -123,7 +123,7 @@ export class KubescapeControlDetails extends React.Component<{ control?: Kubesca
</>);
}

navigationLink = (resource: RelatedResource) => <a href="#" onClick={(e) => {e.preventDefault(); Renderer.Navigation.navigate(resource.detailsUrl); this.props.onClose()}}>{resource.name}</a>
navigationLink = (resource: RelatedResource) => resource.detailsUrl ? <a href="#" onClick={(e) => {e.preventDefault(); Renderer.Navigation.navigate(resource.detailsUrl); this.props.onClose()}}>{resource.name}</a> : resource.name

@computed get failedResourcesTable() {
if (!this.failedResources || this.failedResources.length == 0) {
Expand All @@ -148,7 +148,7 @@ export class KubescapeControlDetails extends React.Component<{ control?: Kubesca
}

return (<><DrawerTitle>Failed Resources</DrawerTitle>
<Table>
<Table scrollable={false}>
<TableHead>{columns.map(col => <TableCell>{col.title}</TableCell>)}</TableHead>
{this.failedResources.map(resource => <TableRow>{columns.map(col => <TableCell>{col.value(resource)}</TableCell>)}</TableRow>)}
</Table>
Expand Down
23 changes: 18 additions & 5 deletions src/components/KubescapeWorkloadDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Renderer } from "@k8slens/extensions";
import { toJS } from "mobx";
import { computed, toJS } from "mobx";
import React from "react";
import { getFailedControlsById } from "../kubescape/controlUtils";
import { getResourceIdByUid, isFailedResult } from "../kubescape/controlUtils";
import { KubescapeReportStore } from "../stores";
import { KubescapeControlTable, controlTableColumn } from ".";

Expand All @@ -14,12 +14,25 @@ export class KubescapeWorkloadDetails<T extends Renderer.K8sApi.KubeObject> exte
constructor(props) {
super(props)
}

@computed get store() {
return KubescapeReportStore.getInstance();
}

render() {
const controls = toJS<any[]>(KubescapeReportStore.getInstance().activeClusterReportResult.controls)
const failedControls = getFailedControlsById(controls, this.props.object.getId())
const rawResourcesMap = toJS<any>(this.store.kubescapeRawResourceMap)
const resourceID = getResourceIdByUid(rawResourcesMap, this.props.object.getId())

let failedControls = []
if (resourceID == null) {
console.error("Failed to find resourceID for object", this.props.object.getId())
} else {
const failedControlIds = this.store.kubescapeResultMap[resourceID].filter(result => isFailedResult(result)).map(result => result.controlID)
failedControls = this.store.kubescapeControls.filter((control) => { return failedControlIds.includes(control.id)})
}

const columns = [controlTableColumn.severity, controlTableColumn.name, controlTableColumn.remediation];
const sortByDefault = { sortBy: controlTableColumn.id, orderBy: "desc" }
const sortByDefault = { sortBy: controlTableColumn.severity, orderBy: "desc" }
return (
failedControls.length > 0 ?
<div>
Expand Down
37 changes: 15 additions & 22 deletions src/kubescape/clusterScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,20 @@ import { KubernetesCluster } from "@k8slens/extensions/dist/src/common/catalog-e
import { Logger, SCAN_CLUSTER_EVENT_NAME } from "../utils";

function parseScanResult(scanResult: any) {
const controls = {};
const frameworks = [];

if (Object.keys(scanResult).length <= 0) {
Logger.debug({ msg: "Scan cluster ended with no results", result: scanResult })
return [[], []]
let resourceIdtoResult = {}
let resourceIdToResource = {}
for (const resource of scanResult.resources) {
resourceIdToResource[resource.resourceID] = resource.object
}

for (const framework of scanResult) {
const { controlReports, ...frameworkData } = framework
for (const control of controlReports) {
if (control.controlID in controls) {
continue;
}
controls[control.controlID] = control;
for (const result of scanResult.results) {
resourceIdtoResult[result.resourceID] = result.controls

for (const control of result.controls) {
scanResult.summaryDetails.controls[control.controlID].resourceIDs[result.resourceID] = result.resourceID
}

frameworks.push(frameworkData);
}
return [Object.values(controls), frameworks] as const;
return [Object.values(scanResult.summaryDetails.controls), resourceIdToResource, resourceIdtoResult] as const;
}

export async function scanClusterTask(preferenceStore, reportStore, ipc) {
Expand Down Expand Up @@ -62,17 +56,16 @@ export async function scanClusterTask(preferenceStore, reportStore, ipc) {
const kubeconfigPath = (<KubernetesCluster>activeEntity).spec.kubeconfigPath
const kubeconfigContext = (<KubernetesCluster>activeEntity).spec.kubeconfigContext
const scanClusterResult = await ipc.invoke(SCAN_CLUSTER_EVENT_NAME, kubeconfigContext, kubeconfigPath);
console.debug(scanClusterResult);

const [controls, frameworks] = parseScanResult(scanClusterResult);
Logger.debug(scanClusterResult);
const [controls, resourceIdToResource, resourceIdToResult] = parseScanResult(scanClusterResult);

scanResult = reportStore.scanResults.find(result => result.clusterId == clusterId);

if (scanResult) {
// Update Store
//scanResult.rawResult = ""; // commented out to reduce size of file
scanResult.controls = controls;
scanResult.frameworks = frameworks;
scanResult.resourceIdToResource = resourceIdToResource;
scanResult.resourceIdToResult = resourceIdToResult;

Logger.debug(`Saved scan result of cluster '${clusterName}'`);

Expand Down
113 changes: 65 additions & 48 deletions src/kubescape/controlUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { json } from "@k8slens/extensions/dist/src/common/utils"
import { toJS } from "mobx"
import { KubescapeControl, Severity, ControlStatus } from "./types"

const SeverityCritical = "Critical"
Expand All @@ -7,7 +9,7 @@ const SeverityLow = "Low"
const SeverityUnknown = "Unknown"

function calculateSeverity(control: any): Severity {
const baseScore = control.baseScore
const baseScore = control.scoreFactor
if (baseScore >= 9) {
return {
name: SeverityCritical,
Expand Down Expand Up @@ -43,79 +45,94 @@ function calculateSeverity(control: any): Severity {
}
}

Object.defineProperty(String.prototype, 'capitalize', {
value: function() {
return this.charAt(0).toUpperCase() + this.slice(1);
},
enumerable: false
});


function calculateStatus(control: any): ControlStatus {
if (control.failedResources == 0) {
if (control.totalResources > 0) {
return {
title: "Passed",
icon: "check_circle_outline",
value: 1,
color: "#23a71b"
}
if (control.statusInfo.status == "passed") {
return {
title: "Passed",
icon: "check_circle_outline",
value: 1,
color: "#23a71b"
}
}

if (control.statusInfo.status == "failed") {
return {
title: "Skipped/Irrelevant",
icon: "help_outline",
value: 0,
color: "gray"
title: "Failed",
icon: "highlight_off",
value: 10,
color: "#e1449f"
}
}

return {
title: "Failed",
icon: "highlight_off",
value: 10,
color: "#e1449f"
title: control.statusInfo.status.capitalize(),
icon: "help_outline",
value: 0,
color: "gray"
}
}


export function toKubescapeControl(control: any): KubescapeControl {
return {
id: control.id,
id: control.controlID,
name: control.name,
failedResources: control.failedResources,
allResources: control.totalResources,
failedResources: control.ResourceCounters.failedResources,
allResources: totalResources(control),
riskScore: control.score,
description: control.description,
remediation: control.remediation,
severity: calculateSeverity(control),
status: calculateStatus(control),
rawResult: control,
relatedResourceIds: Object.keys(control.resourceIDs)
}
}

export function getFailedControlsById(controls: any[], id: string): KubescapeControl[] {
return controls.filter(control =>
control.failedResources > 0 && control.ruleReports?.some(report =>
report.failedResources > 0 && report.ruleResponses?.some(response =>
response.alertObject.k8sApiObjects.some(k8sObj => {
if (k8sObj.apiVersion) {
return k8sObj.metadata.uid == id
}
return k8sObj.apiGroup ?
k8sObj.relatedObjects.some(relatedObj => relatedObj.metadata.uid == id) : false
}))
)
).map(control => toKubescapeControl(control));
export function totalResources(control: any): number {
let total = 0
for (const key in control.ResourceCounters) {
total += control.ResourceCounters[key]
}
return total
}

export function getRelatedObjectsFromControl(control: any): any[] {
const result: any[] = []

control.ruleReports?.forEach(report => {
if (report.failedResources > 0) {
report.ruleResponses?.forEach(response => {
if (response.ruleStatus == "failed") {
response.alertObject.k8sApiObjects.forEach(k8sObj => {
result.push(k8sObj)
})
}
})
export function getResourceIdByUid(rawResourcesMap: any, id: string): string {
for (let key of Object.keys(rawResourcesMap)) {
if (rawResourcesMap[key]?.metadata?.uid === id) {
return key
}
}

return null
}

export function getRelatedObjectsFromControl(control: KubescapeControl, resourceIdToResultMap: any, resourceIdToRawResourceMap: any): any[] {
const relatedObjects: any[] = []
control.relatedResourceIds.map((resourceId) => {
const results = resourceIdToResultMap[resourceId]
results.map((resultObject: any) => {
if (resultObject.controlID == control.id && isFailedResult(resultObject)) {
const rawResource = resourceIdToRawResourceMap[resourceId]
relatedObjects.push(toJS(rawResource))
}
})
})
return result

return relatedObjects
}

export function isFailedResult(resultObject: any): boolean {
return resultObject.rules.some(rule => rule.status == 'failed' && !rule.hasOwnProperty('exception'))
}

export function docsUrl(control: KubescapeControl): string {
return `https://hub.armo.cloud/docs/${control.id.toLocaleLowerCase()}`;
return `https://hub.armosec.io/docs/${control.id.replaceAll('.', '-').toLocaleLowerCase()}`;
}
5 changes: 3 additions & 2 deletions src/kubescape/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export type KubescapeClusterScanResult = {
frameworks: any;
isScanning: boolean;
time: number;
rawResult: any;
resourceIdToResult: any;
resourceIdToResource: any;
}

export type KubescapeReportStoreModel = {
Expand All @@ -35,5 +36,5 @@ export type KubescapeControl = {
remediation: string;
severity: Severity;
status: ControlStatus;
rawResult: any;
relatedResourceIds: string[];
}
Loading

0 comments on commit b7ae8a5

Please sign in to comment.