Skip to content

Commit

Permalink
feat(server): build native and custom resource api versions
Browse files Browse the repository at this point in the history
  • Loading branch information
murarustefaan committed Feb 13, 2024
1 parent 3ff4f97 commit e35a00c
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 31 deletions.
12 changes: 12 additions & 0 deletions admission-controller/server/src/kubernetes/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,16 @@ export class ResourceService {
.list(apiVersion, kind, namespace);
return res.body.items;
}

async listCRDs() {
const res = await this.$client
.api(k8s.ApiextensionsV1Api)
.listCustomResourceDefinition();
return res.body.items;
}

async listAPIs() {
const res = await this.$client.api(k8s.ApisApi).getAPIVersions();
return res.body;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type ResourceIdentifier = {
apiVersion?: string;
group?: string;
kind: string;
apiVersion: string;
version?: string;
};
147 changes: 117 additions & 30 deletions admission-controller/server/src/reporting/reporting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ResourceService } from '../kubernetes/resource.service';
import { PoliciesService } from '../policies/policies.service';
import { ResourceIdentifier } from './reporting.models';
import {
V1APIGroup,
V1CustomResourceDefinition,
V1Namespace,
} from '@kubernetes/client-node';
import { Resource } from '@monokle/validation';
import { V1Namespace } from '@kubernetes/client-node';

type ScannedResourceKind = ResourceIdentifier;

Expand All @@ -17,26 +21,24 @@ export class ReportingService implements OnModuleInit {
text: '',
};

private SCANNED_RESOURCES: ScannedResourceKind[] = [
{ apiVersion: 'apps/v1', kind: 'Deployment' },
{ apiVersion: 'apps/v1', kind: 'StatefulSet' },
{ apiVersion: 'apps/v1', kind: 'DaemonSet' },
{ apiVersion: 'batch/v1', kind: 'CronJob' },
{ apiVersion: 'batch/v1', kind: 'Job' },
{ apiVersion: 'autoscaling/v2', kind: 'HorizontalPodAutoscaler' },
{ apiVersion: 'autoscaling/v1', kind: 'HorizontalPodAutoscaler' },
{ apiVersion: 'v1', kind: 'Pod' },
{ apiVersion: 'v1', kind: 'Service' },
{ apiVersion: 'v1', kind: 'ConfigMap' },
{ apiVersion: 'v1', kind: 'Secret' },
{ apiVersion: 'networking.k8s.io/v1', kind: 'Ingress' },
{ apiVersion: 'networking.k8s.io/v1', kind: 'NetworkPolicy' },
{ apiVersion: 'policy/v1beta1', kind: 'PodSecurityPolicy' },
{ apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'Role' },
{ apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'RoleBinding' },
{ apiVersion: 'v1', kind: 'ServiceAccount' },
// { apiVersion: 'apiextensions.k8s.io/v1', kind: 'customresourcedefinitions' },
private SCANNED_NATIVE_RESOURCES: ScannedResourceKind[] = [
{ version: 'v1', kind: 'Service' },
{ version: 'v1', kind: 'ConfigMap' },
{ version: 'v1', kind: 'Secret' },
{ version: 'v1', kind: 'ServiceAccount' },
{ version: 'v1', kind: 'PersistentVolumeClaim' },
{ group: 'apps', kind: 'Deployment' },
{ group: 'apps', kind: 'StatefulSet' },
{ group: 'apps', kind: 'DaemonSet' },
{ group: 'batch', kind: 'CronJob' },
{ group: 'batch', kind: 'Job' },
{ group: 'autoscaling', kind: 'HorizontalPodAutoscaler' },
{ group: 'networking.k8s.io', kind: 'Ingress' },
{ group: 'networking.k8s.io', kind: 'NetworkPolicy' },
{ group: 'rbac.authorization.k8s.io', kind: 'Role' },
{ group: 'rbac.authorization.k8s.io', kind: 'RoleBinding' },
];
private SCANNED_CUSTOM_RESOURCES: Array<ScannedResourceKind> = [];

private readonly log = new Logger(ReportingService.name);

Expand All @@ -45,33 +47,53 @@ export class ReportingService implements OnModuleInit {
private readonly $policies: PoliciesService,
) {}

private static getApiVersion(group: string | undefined, version: string) {
if (group) {
return `${group}/${version}`;
}
return version;
}

// todo: replace with correct trigger / entrypoint
async onModuleInit() {
this.log.log('Starting cluster report.');
this.log.log('Starting cluster reporting module');

this.SCANNED_NATIVE_RESOURCES = this.buildNativeApiVersions(
await this.$client.listAPIs().then((apis) => apis.groups),
);
this.SCANNED_CUSTOM_RESOURCES = this.buildCustomApiVersions(
await this.$client.listCRDs(),
);

// todo: replace with correct trigger / entrypoint
// running in setTimeout so the PoliciesService has time to preload the validators.
// when running in k8s, this should be removed as will be called trough apicalls / events
setTimeout(
() =>
this.$client.listNamespaces().then(async (namespaces) => {
for (const namespace of namespaces) {
const response = await this.validate(namespace);
this.log.log(
`Namespace ${namespace.metadata!.name} has ${
response!.runs[0].results.length ?? 'no'
} violations`,
);
if (response?.runs) {
this.log.log(
`Namespace ${namespace.metadata!.name} has ${
response.runs[0].results.length ?? 'no'
} violations`,
);
}
}
}),
5000,
);
}

public async validate(namespace: V1Namespace) {
this.log.debug(`Running scan on namespace ${namespace.metadata!.name}`);
this.log.log(`Running scan on namespace ${namespace.metadata!.name}`);

const validator = this.$policies
.getMatchingValidators(namespace as any)
.at(0);
if (!validator) {
this.log.log(
this.log.debug(
`No validator found for namespace ${namespace.metadata!.name}`,
);
return;
Expand All @@ -81,10 +103,75 @@ export class ReportingService implements OnModuleInit {
return await validator.validator.validate({ resources });
}

private buildNativeApiVersions(groups: V1APIGroup[]) {
return this.SCANNED_NATIVE_RESOURCES.map((resource) => {
const group = groups.find((group) => group.name === resource.group);
if (!group) {
const apiVersion = ReportingService.getApiVersion(
resource.group,
resource.version!,
);

if (resource.group) {
// resource had group defined yet could not be inferred from the API
this.log.warn(
`Could not find API group for resource ${apiVersion}/${resource.kind}`,
);
} else {
this.log.debug(
`API Discovery using default ${apiVersion}/${resource.kind}`,
);
}
return {
...resource,
apiVersion,
};
}

return group.versions.map((version) => {
const res = {
...resource,
version: version.version,
apiVersion: ReportingService.getApiVersion(
resource.group!,
version.version,
),
};
this.log.debug(
`API Discovery using found ${res.apiVersion}/${res.kind}`,
);
return res;
});
}).flat();
}

private buildCustomApiVersions(crds: V1CustomResourceDefinition[]) {
const namespaced = crds.filter((crd) => crd.spec.scope === 'Namespaced');

return namespaced
.map((crd) =>
crd.spec.versions.map((version) => {
const res = {
group: crd.spec.group,
kind: crd.spec.names.kind,
version: version.name,
apiVersion: ReportingService.getApiVersion(
crd.spec.group,
version.name,
),
};
this.log.debug(`CRD Discovery found ${res.apiVersion}/${res.kind}`);
return res;
}),
)
.flat();
}

private async buildInventory(namespace: string) {
const inventory = new Set<Resource>();

for (const { apiVersion, kind } of this.SCANNED_RESOURCES) {
for (const { group, version, kind } of this.SCANNED_NATIVE_RESOURCES) {
const apiVersion = ReportingService.getApiVersion(group!, version!);
const resources = await this.$client
.list(apiVersion, kind, namespace)
.catch((err) => {
Expand All @@ -103,7 +190,7 @@ export class ReportingService implements OnModuleInit {
Object.assign(
{
name: resource.metadata!.name ?? '',
apiVersion: resource.apiVersion,
apiVersion: resource.apiVersion!,
kind: resource.kind,
namespace: resource.metadata?.namespace,
content: resource,
Expand Down

0 comments on commit e35a00c

Please sign in to comment.