[Cloud Security] Aws credentials cleanup (#163528)

## Summary

The various unused credential methods supported by both the KSPM->EKS
and CSPM->AWS methods are not cleared out when a package is saved. As
there are currently two components on the frontend which allow the user
to specify their aws credential method, I've added hooks for both the
'packagePolicyCreate' and 'packagePolicyUpdate' methods in fleet to the
CSP serverside plugin. Both these hooks will pass the policy to a
cleanCredentials function which checks the 'aws.credentials.type' var to
determine which fields should be cleared out.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Karl Godard 2023-08-15 08:00:22 -07:00 committed by GitHub
parent eb3002eb65
commit 0651662159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 228 additions and 12 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PostureTypes, VulnSeverity } from './types';
import { PostureTypes, VulnSeverity, AwsCredentialsTypeFieldMap } from './types';
export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status';
export const STATUS_API_CURRENT_VERSION = '1';
@ -125,5 +125,14 @@ export const VULNERABILITIES_SEVERITY: Record<VulnSeverity, VulnSeverity> = {
};
export const VULNERABILITIES_ENUMERATION = 'CVE';
export const AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP: AwsCredentialsTypeFieldMap = {
assume_role: ['role_arn'],
direct_access_keys: ['access_key_id', 'secret_access_key'],
temporary_keys: ['access_key_id', 'secret_access_key', 'session_token'],
shared_credentials: ['shared_credential_file', 'credential_profile_name'],
cloud_formation: [],
};
export const SETUP_ACCESS_CLOUD_SHELL = 'google_cloud_shell';
export const SETUP_ACCESS_MANUAL = 'manual';

View file

@ -13,6 +13,17 @@ import { CspRuleTemplate } from './schemas';
import { findCspRuleTemplateRequest } from './schemas/csp_rule_template_api/get_csp_rule_template';
import { getComplianceDashboardSchema } from './schemas/stats';
export type AwsCredentialsType =
| 'assume_role'
| 'direct_access_keys'
| 'temporary_keys'
| 'shared_credentials'
| 'cloud_formation';
export type AwsCredentialsTypeFieldMap = {
[key in AwsCredentialsType]: string[];
};
export type Evaluation = 'passed' | 'failed' | 'NA';
export type PostureTypes = 'cspm' | 'kspm' | 'vuln_mgmt' | 'all';

View file

@ -6,7 +6,11 @@
*/
import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks';
import { getBenchmarkFromPackagePolicy, getBenchmarkTypeFilter } from './helpers';
import {
getBenchmarkFromPackagePolicy,
getBenchmarkTypeFilter,
cleanupCredentials,
} from './helpers';
describe('test helper methods', () => {
it('get default integration type from inputs with multiple enabled types', () => {
@ -60,4 +64,126 @@ describe('test helper methods', () => {
const typeFilter = getBenchmarkTypeFilter('cis_eks');
expect(typeFilter).toMatch('csp-rule-template.attributes.metadata.benchmark.id: "cis_eks"');
});
describe('cleanupCredentials', () => {
it('cleans unused aws credential methods, except role_arn when using assume_role', () => {
const mockPackagePolicy = createPackagePolicyMock();
mockPackagePolicy.inputs = [
{
type: 'cloudbeat/cis_eks',
enabled: true,
streams: [
{
id: 'findings',
enabled: true,
data_stream: {
dataset: 'cloud_security_posture.findings',
type: 'logs',
},
vars: {
'aws.credentials.type': { value: 'assume_role' },
access_key_id: { value: 'unused', type: 'text' },
credential_profile_name: { value: 'unused', type: 'text' },
role_arn: { value: 'inuse' },
secret_access_key: { value: 'unused', type: 'text' },
session_token: { value: 'unused', type: 'text' },
shared_credential_file: { value: 'unused', type: 'text' },
},
},
],
},
];
const cleanedPackage = cleanupCredentials(mockPackagePolicy);
expect(cleanedPackage.inputs[0].streams[0].vars).toEqual({
'aws.credentials.type': { value: 'assume_role' },
access_key_id: { value: undefined, type: 'text' },
credential_profile_name: { value: undefined, type: 'text' },
role_arn: { value: 'inuse' },
secret_access_key: { value: undefined, type: 'text' },
session_token: { value: undefined, type: 'text' },
shared_credential_file: { value: undefined, type: 'text' },
});
});
it('cleans unused aws credential methods, when using cloud formation', () => {
const mockPackagePolicy = createPackagePolicyMock();
mockPackagePolicy.inputs = [
{
type: 'cloudbeat/cis_eks',
enabled: true,
streams: [
{
id: 'findings',
enabled: true,
data_stream: {
dataset: 'cloud_security_posture.findings',
type: 'logs',
},
vars: {
'aws.credentials.type': { value: 'cloud_formation' },
access_key_id: { value: 'unused', type: 'text' },
credential_profile_name: { value: 'unused', type: 'text' },
role_arn: { value: 'unused' },
secret_access_key: { value: 'unused', type: 'text' },
session_token: { value: 'unused', type: 'text' },
shared_credential_file: { value: 'unused', type: 'text' },
},
},
],
},
];
const cleanedPackage = cleanupCredentials(mockPackagePolicy);
expect(cleanedPackage.inputs[0].streams[0].vars).toEqual({
'aws.credentials.type': { value: 'cloud_formation' },
access_key_id: { value: undefined, type: 'text' },
credential_profile_name: { value: undefined, type: 'text' },
role_arn: { value: undefined },
secret_access_key: { value: undefined, type: 'text' },
session_token: { value: undefined, type: 'text' },
shared_credential_file: { value: undefined, type: 'text' },
});
});
it('cleans unused aws credential methods, when using direct_access_keys method ', () => {
const mockPackagePolicy = createPackagePolicyMock();
mockPackagePolicy.inputs = [
{
type: 'cloudbeat/cis_eks',
enabled: true,
streams: [
{
id: 'findings',
enabled: true,
data_stream: {
dataset: 'cloud_security_posture.findings',
type: 'logs',
},
vars: {
'aws.credentials.type': { value: 'direct_access_keys' },
access_key_id: { value: 'used', type: 'text' },
credential_profile_name: { value: 'unused', type: 'text' },
role_arn: { value: 'unused' },
secret_access_key: { value: 'used', type: 'text' },
session_token: { value: 'unused', type: 'text' },
shared_credential_file: { value: 'unused', type: 'text' },
},
},
],
},
];
const cleanedPackage = cleanupCredentials(mockPackagePolicy);
expect(cleanedPackage.inputs[0].streams[0].vars).toEqual({
'aws.credentials.type': { value: 'direct_access_keys' },
access_key_id: { value: 'used', type: 'text' },
credential_profile_name: { value: undefined, type: 'text' },
role_arn: { value: undefined },
secret_access_key: { value: 'used', type: 'text' },
session_token: { value: undefined, type: 'text' },
shared_credential_file: { value: undefined, type: 'text' },
});
});
});
});

View file

@ -12,13 +12,15 @@ import {
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
PackagePolicy,
PackagePolicyInput,
UpdatePackagePolicy,
} from '@kbn/fleet-plugin/common';
import {
CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
CLOUDBEAT_VANILLA,
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP,
} from '../constants';
import type { BenchmarkId, Score, BaseCspSetupStatus } from '../types';
import type { BenchmarkId, Score, BaseCspSetupStatus, AwsCredentialsType } from '../types';
/**
* @example
@ -98,3 +100,47 @@ export const getStatusForIndexName = (indexName: string, status?: BaseCspSetupSt
return 'unknown';
};
export const cleanupCredentials = (packagePolicy: NewPackagePolicy | UpdatePackagePolicy) => {
const enabledInput = packagePolicy.inputs.find((i) => i.enabled);
const credentialType: AwsCredentialsType | undefined =
enabledInput?.streams?.[0].vars?.['aws.credentials.type'].value;
if (credentialType) {
const credsToKeep = AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP[credentialType];
const credFields = Object.values(AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP).flat();
if (credsToKeep) {
// we need to return a copy of the policy with the unused
// credentials set to undefined
return {
...packagePolicy,
inputs: packagePolicy.inputs.map((input) => {
if (input.enabled) {
return {
...input,
streams: input.streams.map((stream) => {
const vars = stream.vars;
for (const field in vars) {
if (!credsToKeep.includes(field) && credFields.includes(field)) {
vars[field].value = undefined;
}
}
return {
...stream,
vars,
};
}),
};
}
return input;
}),
};
}
}
// nothing to do, return unmutated policy
return packagePolicy;
};

View file

@ -24,7 +24,6 @@ import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import {
getAwsCredentialsFormManualOptions,
AwsCredentialsType,
AwsOptions,
DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE,
} from './get_aws_credentials_form_options';
@ -35,6 +34,7 @@ import {
NewPackagePolicyPostureInput,
} from '../utils';
import { SetupFormat, useAwsCredentialsForm } from './hooks';
import { AwsCredentialsType } from '../../../../common/types';
interface AWSSetupInfoContentProps {
integrationLink: string;

View file

@ -10,6 +10,7 @@ import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { AwsCredentialsType } from '../../../../common/types';
const AssumeRoleDescription = (
<div>
@ -69,13 +70,6 @@ const AWS_FIELD_LABEL = {
}),
};
export type AwsCredentialsType =
| 'assume_role'
| 'direct_access_keys'
| 'temporary_keys'
| 'shared_credentials'
| 'cloud_formation';
export type AwsCredentialsFields = Record<string, { label: string; type?: 'password' | 'text' }>;
export interface AwsOptionValue {

View file

@ -14,12 +14,12 @@ import {
NewPackagePolicyPostureInput,
} from '../utils';
import {
AwsCredentialsType,
DEFAULT_MANUAL_AWS_CREDENTIALS_TYPE,
getAwsCredentialsFormOptions,
getInputVarsFields,
} from './get_aws_credentials_form_options';
import { CLOUDBEAT_AWS } from '../../../../common/constants';
import { AwsCredentialsType } from '../../../../common/types';
/**
* Update CloudFormation template and stack name in the Agent Policy
* based on the selected policy template

View file

@ -18,6 +18,7 @@ import type {
PostDeletePackagePoliciesResponse,
PackagePolicy,
NewPackagePolicy,
UpdatePackagePolicy,
} from '@kbn/fleet-plugin/common';
import type {
TaskManagerSetupContract,
@ -25,6 +26,7 @@ import type {
} from '@kbn/task-manager-plugin/server';
import { isCspPackage } from '../common/utils/helpers';
import { isSubscriptionAllowed } from '../common/utils/subscription';
import { cleanupCredentials } from '../common/utils/helpers';
import type {
CspServerPluginSetup,
CspServerPluginStart,
@ -124,6 +126,34 @@ export class CspPlugin
}
);
plugins.fleet.registerExternalCallback(
'packagePolicyCreate',
async (
packagePolicy: NewPackagePolicy,
soClient: SavedObjectsClientContract
): Promise<NewPackagePolicy> => {
if (isCspPackage(packagePolicy.package?.name)) {
return cleanupCredentials(packagePolicy);
}
return packagePolicy;
}
);
plugins.fleet.registerExternalCallback(
'packagePolicyUpdate',
async (
packagePolicy: UpdatePackagePolicy,
soClient: SavedObjectsClientContract
): Promise<UpdatePackagePolicy> => {
if (isCspPackage(packagePolicy.package?.name)) {
return cleanupCredentials(packagePolicy);
}
return packagePolicy;
}
);
plugins.fleet.registerExternalCallback(
'packagePolicyPostCreate',
async (