Skip to content

Commit

Permalink
feat(server): initial implementation of scan over kubernetes-native r…
Browse files Browse the repository at this point in the history
…esources
  • Loading branch information
murarustefaan committed Feb 12, 2024
1 parent 0cd647b commit 3ff4f97
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 30 deletions.
23 changes: 13 additions & 10 deletions admission-controller/server/src/kubernetes/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import k8s from '@kubernetes/client-node';
export class ResourceService {
constructor(private readonly $client: ClientService) {}

listNamespaces() {
return this.$client
.api(k8s.CoreV1Api)
.listNamespace()
.then((res) => res.body.items);
async listNamespaces() {
const res = await this.$client.api(k8s.CoreV1Api).listNamespace();
return res.body.items;
}

getNamespace(name: string) {
return this.$client
.api(k8s.CoreV1Api)
.readNamespace(name)
.then((res) => res.body);
async getNamespace(name: string) {
const res = await this.$client.api(k8s.CoreV1Api).readNamespace(name);
return res.body;
}

async list(apiVersion: string, kind: string, namespace?: string) {
const res = await this.$client
.api(k8s.KubernetesObjectApi)
.list(apiVersion, kind, namespace);
return res.body.items;
}
}
34 changes: 16 additions & 18 deletions admission-controller/server/src/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { WatcherService } from '../kubernetes/watcher.service';
export class PoliciesService implements OnModuleInit {
private static readonly PLUGIN_BLOCKLIST = ['resource-links'];

private readonly _logger = new Logger(PoliciesService.name);
private readonly log = new Logger(PoliciesService.name);

private readonly policyStore = new Map<string, MonoklePolicy>(); // Map<policyName, policy>
private readonly bindingStore = new Map<string, MonoklePolicyBinding>(); // Map<bindingName, binding>
Expand Down Expand Up @@ -87,7 +87,7 @@ export class PoliciesService implements OnModuleInit {
if (!this.validatorStore.has(policy.binding.policyName)) {
// This should not happen and means there is a bug in other place in the code. Raise warning and skip.
// Do not create validator instance here to keep this function sync and to keep processing time low.
this._logger.warn(
this.log.warn(
`Validator not found for policy: ${policy.binding.policyName}`,
);
return null;
Expand All @@ -107,11 +107,9 @@ export class PoliciesService implements OnModuleInit {
resource: AdmissionRequestObject,
resourceNamespace?: V1Namespace,
): MonokleApplicablePolicy[] {
this._logger.debug({
policies: this.policyStore.size,
bindings: this.bindingStore.size,
});

this.log.debug(
`policies: ${this.policyStore.size}, bindings: ${this.bindingStore.size}`,
);
if (this.bindingStore.size === 0) {
return [];
}
Expand All @@ -121,7 +119,7 @@ export class PoliciesService implements OnModuleInit {
const policy = this.policyStore.get(binding.spec.policyName);

if (!policy) {
this._logger.error('Binding is pointing to missing policy', binding);
this.log.error('Binding is pointing to missing policy', binding);
return null;
}

Expand Down Expand Up @@ -150,8 +148,8 @@ export class PoliciesService implements OnModuleInit {
private async onPolicy(rawPolicy: MonoklePolicy) {
const policy = PoliciesService.postprocess(rawPolicy);

this._logger.log(`Policy change received: ${rawPolicy.metadata!.name}`);
this._logger.verbose({ rawPolicy, policy });
this.log.log(`Policy change received: ${rawPolicy.metadata!.name}`);
this.log.verbose({ rawPolicy, policy });

this.policyStore.set(rawPolicy.metadata!.name!, policy);

Expand All @@ -172,7 +170,7 @@ export class PoliciesService implements OnModuleInit {
// Run separately (instead of passing config to constructor) to make sure that validator
// is ready when 'setupValidator' function call fulfills.
await validator.preload(policy.spec);
this._logger.log(`Policy reconciled: ${rawPolicy.metadata!.name}`);
this.log.log(`Policy reconciled: ${rawPolicy.metadata!.name}`);

this.validatorStore.set(policy.metadata!.name!, validator);
}
Expand All @@ -184,8 +182,8 @@ export class PoliciesService implements OnModuleInit {
private onPolicyRemoval(rawPolicy: MonoklePolicy) {
const policy = PoliciesService.postprocess(rawPolicy);

this._logger.log(`Policy removed: ${rawPolicy.metadata!.name}`);
this._logger.verbose({ rawPolicy, policy });
this.log.log(`Policy removed: ${rawPolicy.metadata!.name}`);
this.log.verbose({ rawPolicy, policy });

this.policyStore.delete(rawPolicy.metadata!.name!);
this.validatorStore.delete(policy.metadata!.name!);
Expand All @@ -200,8 +198,8 @@ export class PoliciesService implements OnModuleInit {
':update',
)
private onBinding(rawBinding: MonoklePolicyBinding) {
this._logger.log(`Binding updated: ${rawBinding.metadata!.name}`);
this._logger.verbose({ rawBinding });
this.log.log(`Binding updated: ${rawBinding.metadata!.name}`);
this.log.verbose({ rawBinding });

this.bindingStore.set(rawBinding.metadata!.name!, rawBinding);
}
Expand All @@ -211,8 +209,8 @@ export class PoliciesService implements OnModuleInit {
':delete',
)
private onBindingRemoval(rawBinding: MonoklePolicyBinding) {
this._logger.log(`Binding removed: ${rawBinding.metadata!.name}`);
this._logger.verbose({ rawBinding });
this.log.log(`Binding removed: ${rawBinding.metadata!.name}`);
this.log.verbose({ rawBinding });

this.bindingStore.delete(rawBinding.metadata!.name!);
}
Expand All @@ -232,7 +230,7 @@ export class PoliciesService implements OnModuleInit {
((resource as any).namespace || resource.metadata.namespace) ===
undefined;

this._logger.verbose('Checking if resource matches binding', {
this.log.verbose('Checking if resource matches binding', {
namespaceMatchLabels,
namespaceMatchExpressions,
kind,
Expand Down
4 changes: 4 additions & 0 deletions admission-controller/server/src/reporting/reporting.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ResourceIdentifier = {
kind: string;
apiVersion: string;
};
10 changes: 10 additions & 0 deletions admission-controller/server/src/reporting/reporting.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { KubernetesModule } from '../kubernetes/kubernetes.module';
import { ReportingService } from './reporting.service';
import { PoliciesModule } from '../policies/policies.module';

@Module({
imports: [KubernetesModule, PoliciesModule],
providers: [ReportingService],
})
export class ReportingModule {}
119 changes: 119 additions & 0 deletions admission-controller/server/src/reporting/reporting.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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 { Resource } from '@monokle/validation';
import { V1Namespace } from '@kubernetes/client-node';

type ScannedResourceKind = ResourceIdentifier;

@Injectable()
export class ReportingService implements OnModuleInit {
private static readonly VALIDATOR_RESOURCE_DEFAULTS = {
id: '',
fileId: '',
filePath: '',
fileOffset: 0,
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 readonly log = new Logger(ReportingService.name);

constructor(
private readonly $client: ResourceService,
private readonly $policies: PoliciesService,
) {}

// todo: replace with correct trigger / entrypoint
async onModuleInit() {
this.log.log('Starting cluster report.');
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`,
);
}
}),
5000,
);
}

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

const validator = this.$policies
.getMatchingValidators(namespace as any)
.at(0);
if (!validator) {
this.log.log(
`No validator found for namespace ${namespace.metadata!.name}`,
);
return;
}

const resources = await this.buildInventory(namespace.metadata!.name!);
return await validator.validator.validate({ resources });
}

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

for (const { apiVersion, kind } of this.SCANNED_RESOURCES) {
const resources = await this.$client
.list(apiVersion, kind, namespace)
.catch((err) => {
// todo: sentry should handle this and report the available API versions for the given kind
// ie: HPA is not available in v2, but in v2beta2
this.log.warn(
`Failed to list resources for ${apiVersion}/${kind} in namespace ${namespace}`,
);
return [];
});
resources.forEach((resource) => {
resource.apiVersion ??= apiVersion;
resource.kind ??= kind;

inventory.add(
Object.assign(
{
name: resource.metadata!.name ?? '',
apiVersion: resource.apiVersion,
kind: resource.kind,
namespace: resource.metadata?.namespace,
content: resource,
},
ReportingService.VALIDATOR_RESOURCE_DEFAULTS,
),
);
});
}

return [...inventory];
}
}
3 changes: 2 additions & 1 deletion admission-controller/server/src/server.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { AdmissionModule } from './admission/admission.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ReportingModule } from './reporting/reporting.module';

@Module({
imports: [EventEmitterModule.forRoot(), AdmissionModule],
imports: [EventEmitterModule.forRoot(), AdmissionModule, ReportingModule],
controllers: [],
providers: [],
})
Expand Down
2 changes: 1 addition & 1 deletion admission-controller/synchronizer/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"experimentalDecorators": true
"experimentalDecorators": true,
"strict": true /* Enable all strict type-checking options. */
},
"exclude": ["node_modules", "dist"],
Expand Down

0 comments on commit 3ff4f97

Please sign in to comment.