Skip to content

Commit

Permalink
[Cloud Security] 3P findings page and flyout support (#187874)
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanSh authored Aug 4, 2024
1 parent 381ea54 commit a98a8ab
Show file tree
Hide file tree
Showing 28 changed files with 2,173 additions and 1,015 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

// TODO: this needs to be defined in a versioned schema
import type { EcsEvent } from '@elastic/ecs';
import type { EcsDataStream, EcsEvent } from '@elastic/ecs';
import { CspBenchmarkRuleMetadata } from '../types/latest';

export interface CspFinding {
Expand All @@ -19,6 +19,7 @@ export interface CspFinding {
rule: CspBenchmarkRuleMetadata;
host: CspFindingHost;
event: EcsEvent;
data_stream: EcsDataStream;
agent: CspFindingAgent;
ecs: {
version: string;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const convertRuleTagsToMatchAnyKQL = (tags: string[]): string => {
export const getFindingsDetectionRuleSearchTags = (
cspBenchmarkRule: CspBenchmarkRuleMetadata
): string[] => {
if (!cspBenchmarkRule.benchmark || !cspBenchmarkRule.benchmark.id) {
if (!cspBenchmarkRule?.benchmark || !cspBenchmarkRule?.benchmark?.id) {
// Return an empty array if benchmark ID is undefined
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ export const useFetchDetectionRulesAlertsStatus = (tags: string[]) => {
throw new Error('Kibana http service is not available');
}

return useQuery<AlertStatus, Error>([DETECTION_ENGINE_ALERTS_KEY, tags], () =>
http.get<AlertStatus>(GET_DETECTION_RULE_ALERTS_STATUS_PATH, {
version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION,
query: { tags },
})
);
return useQuery<AlertStatus, Error>({
queryKey: [DETECTION_ENGINE_ALERTS_KEY, tags],
queryFn: () =>
http.get<AlertStatus>(GET_DETECTION_RULE_ALERTS_STATUS_PATH, {
version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION,
query: { tags },
}),
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

type Dataset = 'wiz.cloud_configuration_finding' | 'cloud_security_posture.findings';

export const WIZ_DATASET = 'wiz.cloud_configuration_finding';
export const CSP_DATASET = 'cloud_security_posture.findings';

export const getDatasetDisplayName = (dataset?: Dataset | string) => {
if (dataset === WIZ_DATASET) return 'Wiz';
if (dataset === CSP_DATASET) return 'Elastic CSP';
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ interface Props {
size?: IconSize;
}

const getBenchmarkIdIconType = (props: Props): string => {
switch (props.type) {
const getBenchmarkIdIconType = (type: BenchmarkId): string | undefined => {
switch (type) {
case 'cis_eks':
return cisEksIcon;
case 'cis_azure':
Expand All @@ -30,13 +30,17 @@ const getBenchmarkIdIconType = (props: Props): string => {
case 'cis_gcp':
return googleCloudLogo;
case 'cis_k8s':
default:
return 'logoKubernetes';
}
};

export const CISBenchmarkIcon = (props: Props) => (
<EuiToolTip content={props.name}>
<EuiIcon type={getBenchmarkIdIconType(props)} size={props.size || 'xl'} css={props.style} />
</EuiToolTip>
);
export const CISBenchmarkIcon = (props: Props) => {
const iconType = getBenchmarkIdIconType(props.type);
if (!iconType) return <></>;

return (
<EuiToolTip content={props.name}>
<EuiIcon type={iconType} size={props.size || 'xl'} css={props.style} />
</EuiToolTip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { css } from '@emotion/react';
import { statusColors } from '../common/constants';

interface Props {
type: 'passed' | 'failed';
type?: 'passed' | 'failed';
}

// 'fail' / 'pass' are same chars length, but not same width size.
Expand All @@ -37,8 +37,10 @@ export const CspEvaluationBadge = ({ type }: Props) => (
>
{type === 'failed' ? (
<FormattedMessage id="xpack.csp.cspEvaluationBadge.failLabel" defaultMessage="Fail" />
) : (
) : type === 'passed' ? (
<FormattedMessage id="xpack.csp.cspEvaluationBadge.passLabel" defaultMessage="Pass" />
) : (
<FormattedMessage id="xpack.csp.cspEvaluationBadge.naLabel" defaultMessage="N/A" />
)}
</EuiBadge>
);
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ interface DetectionRuleCounterProps {

export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounterProps) => {
const { data: rulesData, isLoading: ruleIsLoading } = useFetchDetectionRulesByTags(tags);
const { data: alertsData, isLoading: alertsIsLoading } = useFetchDetectionRulesAlertsStatus(tags);
const {
data: alertsData,
isLoading: alertsIsLoading,
isError: alertsIsError,
} = useFetchDetectionRulesAlertsStatus(tags);

const [isCreateRuleLoading, setIsCreateRuleLoading] = useState(false);

Expand Down Expand Up @@ -68,6 +72,8 @@ export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounte
queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]);
}, [createRuleFn, http, analytics, notifications, i18n, theme, queryClient]);

if (alertsIsError) return <>{'-'}</>;

return (
<EuiSkeletonText
data-test-subj="csp:detection-rule-counter-loading"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,84 @@ export const mockFindingsHit: CspFinding = {
category: ['configuration'],
outcome: 'success',
},
data_stream: {
dataset: 'cloud_security_posture.findings',
},
};

export const mockWizFinding = {
agent: {
name: 'ip-172-31-29-186.eu-west-1.compute.internal',
id: 'd66400e6-6224-489a-aae5-0dd529e7b61a',
ephemeral_id: '3159ed3a-8517-4289-9c4c-ab15abc7f938',
type: 'filebeat',
version: '8.14.1',
},
resource: {
name: 'annam-instance-group-61wh',
id: '45860879-12db-5fce-838d-eb4deac2a544',
},
elastic_agent: {
id: 'd66400e6-6224-489a-aae5-0dd529e7b61a',
version: '8.14.1',
snapshot: false,
},
wiz: {
cloud_configuration_finding: {
rule: {
id: '02fde46d-ba1c-405e-b20f-a3742a8d2f41',
},
},
},
rule: {
name: 'Unattached volume for more than 7 days',
id: '02fde46d-ba1c-405e-b20f-a3742a8d2f41',
},
message:
"This rule checks if Compute Disks have been unattached for more than 7 days. \nThis rule fails if a disk's status is `READY`, it has no users attached, and the `lastDetachTimestamp` is more than 7 days ago. \nUnattached disks can incur costs without providing any benefits and may also pose a security risk if they contain sensitive data that is not being used. It is recommended to either delete unattached disks that are no longer needed or reattach them to a relevant instance.",
tags: ['preserve_original_event', 'forwarded', 'wiz-cloud_configuration_finding'],
cloud: {
availability_zone: 'eu-west-1b',
image: {
id: 'ami-0551ce4d67096d606',
},
instance: {
id: 'i-0d3beee17a99bf575',
},
provider: 'GCP',
service: {
name: 'EC2',
},
machine: {
type: 't2.micro',
},
region: 'us-central1',
account: {
id: '704479110758',
},
},
input: {
type: 'cel',
},
'@timestamp': '2024-07-15T10:00:16.283Z',
ecs: {
version: '8.11.0',
},
data_stream: {
namespace: 'default',
type: 'logs',
dataset: 'wiz.cloud_configuration_finding',
},
event: {
agent_id_status: 'auth_metadata_missing',
ingested: '2024-07-15T10:49:45Z',
original:
'{"analyzedAt":"2024-07-15T10:00:16.283504Z","firstSeenAt":"2024-07-15T10:00:22.271901Z","id":"fd5b53a4-d85c-5d3a-b0bf-2eb270582db5","ignoreRules":null,"remediation":null,"resource":{"id":"45860879-12db-5fce-838d-eb4deac2a544","name":"annam-instance-group-61wh","nativeType":"compute#disk","projects":[{"id":"0f19bcc4-c17b-57d0-a187-db3a6b1a5100","name":"Project 3","riskProfile":{"businessImpact":"MBI"}}],"providerId":"https://www.googleapis.com/compute/v1/projects/my-walla-website/zones/us-central1-c/disks/annam-instance-group-61wh","region":"us-central1","subscription":{"cloudProvider":"GCP","externalId":"my-walla-website","id":"64982819-64ed-5c02-8a73-93d25fef8d89","name":"Product Integration"},"tags":[],"type":"VOLUME"},"result":"PASS","rule":{"description":"This rule checks if Compute Disks have been unattached for more than 7 days. \\nThis rule fails if a disk\'s status is `READY`, it has no users attached, and the `lastDetachTimestamp` is more than 7 days ago. \\nUnattached disks can incur costs without providing any benefits and may also pose a security risk if they contain sensitive data that is not being used. It is recommended to either delete unattached disks that are no longer needed or reattach them to a relevant instance.","functionAsControl":false,"graphId":"60db4cc3-d5c8-5e76-8dc9-77dde142ba98","id":"02fde46d-ba1c-405e-b20f-a3742a8d2f41","name":"Unattached volume for more than 7 days","remediationInstructions":"Perform the following step in order to delete a disk via GCP CLI: \\n``` \\ngcloud compute disks delete {{DiskName}} --zone={{Zone}}\\n``` \\n\\u003e**Note** \\n\\u003eA disk can only be deleted if it is not attached to any virtual machine instances."},"securitySubCategories":[{"category":{"framework":{"id":"wf-id-120","name":"NIS2 Directive (Article 21)"},"id":"wct-id-2418","name":"Article 21 Cybersecurity risk-management measures"},"id":"wsct-id-18827","title":"21.2.1 The measures to protect network and information systems shall include policies on risk analysis and information system security"},{"category":{"framework":{"id":"wf-id-105","name":"Wiz (Legacy)"},"id":"wct-id-2136","name":"Operationalization"},"id":"wsct-id-5540","title":"Operationalization"},{"category":{"framework":{"id":"wf-id-1","name":"Wiz for Risk Assessment"},"id":"wct-id-940","name":"Operationalization"},"id":"wsct-id-6548","title":"Operationalization"},{"category":{"framework":{"id":"wf-id-78","name":"Wiz for Cost Optimization"},"id":"wct-id-1796","name":"Waste"},"id":"wsct-id-10216","title":"Storage"}],"severity":"NONE","status":"RESOLVED","targetExternalId":"1404039754344376914","targetObjectProviderUniqueId":"https://www.googleapis.com/compute/v1/projects/my-walla-website/zones/us-central1-c/disks/annam-instance-group-61wh"}',
created: '2024-07-15T10:00:22.271Z',
kind: 'event',
id: 'fd5b53a4-d85c-5d3a-b0bf-2eb270582db5',
category: ['configuration'],
type: ['info'],
dataset: 'wiz.cloud_configuration_finding',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ export const generateCspFinding = (
dataset: 'cloud_security_posture.findings',
outcome: 'success',
},
data_stream: {
dataset: 'cloud_security_posture.findings',
},
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import userEvent from '@testing-library/user-event';
import { FindingsRuleFlyout } from './findings_flyout';
import { render, screen } from '@testing-library/react';
import { TestProvider } from '../../../test/test_provider';
import { mockFindingsHit } from '../__mocks__/findings';
import { mockFindingsHit, mockWizFinding } from '../__mocks__/findings';
import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants';

const onPaginate = jest.fn();
Expand All @@ -21,7 +21,7 @@ const TestComponent = ({ ...overrideProps }) => (
flyoutIndex={0}
findingsCount={2}
onPaginate={onPaginate}
findings={mockFindingsHit}
finding={mockFindingsHit}
{...overrideProps}
/>
</TestProvider>
Expand All @@ -48,12 +48,22 @@ describe('<FindingsFlyout/>', () => {
getAllByText(tag);
});
});

it('displays missing info callout when data source is not CSP', () => {
const { getByText } = render(<TestComponent finding={mockWizFinding} />);
getByText('Some fields not provided by Wiz');
});

it('does not display missing info callout when data source is CSP', () => {
const { queryByText } = render(<TestComponent finding={mockFindingsHit} />);
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
expect(missingInfoCallout).toBeNull();
});
});

describe('Rule Tab', () => {
it('displays rule text details', () => {
const { getByText, getAllByText } = render(<TestComponent />);

userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));

getAllByText(mockFindingsHit.rule.name);
Expand All @@ -63,17 +73,49 @@ describe('<FindingsFlyout/>', () => {
getAllByText(tag);
});
});

it('displays missing info callout when data source is not CSP', () => {
const { getByText } = render(<TestComponent finding={mockWizFinding} />);
userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));

getByText('Some fields not provided by Wiz');
});

it('does not display missing info callout when data source is CSP', () => {
const { queryByText } = render(<TestComponent finding={mockFindingsHit} />);
userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));

const missingInfoCallout = queryByText('Some fields not provided by Wiz');
expect(missingInfoCallout).toBeNull();
});
});

describe('Table Tab', () => {
it('displays resource name and id', () => {
const { getAllByText } = render(<TestComponent />);

userEvent.click(screen.getByTestId('findings_flyout_tab_table'));

getAllByText(mockFindingsHit.resource.name);
getAllByText(mockFindingsHit.resource.id);
});

it('does not display missing info callout for 3Ps', () => {
const { queryByText } = render(<TestComponent finding={mockWizFinding} />);
userEvent.click(screen.getByTestId('findings_flyout_tab_table'));

const missingInfoCallout = queryByText('Some fields not provided by Wiz');
expect(missingInfoCallout).toBeNull();
});
});

describe('JSON Tab', () => {
it('does not display missing info callout for 3Ps', () => {
const { queryByText } = render(<TestComponent finding={mockWizFinding} />);
userEvent.click(screen.getByTestId('findings_flyout_tab_json'));

const missingInfoCallout = queryByText('Some fields not provided by Wiz');
expect(missingInfoCallout).toBeNull();
});
});

it('should allow pagination with next', async () => {
Expand Down
Loading

0 comments on commit a98a8ab

Please sign in to comment.