Skip to content

Commit

Permalink
[Security Solution][Endpoint] Unit test for the ArtifactEntryCard (#…
Browse files Browse the repository at this point in the history
…111995) (#112137)

* Test file for ArtifactEntryCard
* Fix i18n pluralization
* refactor mapper of trusted apps for tags-to-effectScope from server code to common
* determine EffectScope for Exceptions based on tags
* Fix margin on artifact card divider
* Updated snapshot

Co-authored-by: Paul Tavares <[email protected]>
  • Loading branch information
kibanamachine and paul-tavares authored Sep 14, 2021
1 parent 8203a37 commit 0e8cb0c
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.
*/

import { EffectScope } from '../../types';

export const POLICY_REFERENCE_PREFIX = 'policy:';

/**
* Looks at an array of `tags` (attributed defined on the `ExceptionListItemSchema`) and returns back
* the `EffectScope` of based on the data in the array
* @param tags
*/
export const tagsToEffectScope = (tags: string[]): EffectScope => {
const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX));

if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) {
return {
type: 'global',
};
} else {
return {
type: 'policy',
policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)),
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* 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.
*/

import React from 'react';
import { cloneDeep } from 'lodash';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { ArtifactEntryCard, ArtifactEntryCardProps } from './artifact_entry_card';
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
import { act, fireEvent, getByTestId } from '@testing-library/react';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { AnyArtifact } from './types';
import { isTrustedApp } from './hooks/use_normalized_artifact';

const getCommonItemDataOverrides = () => {
return {
name: 'some internal app',
description: 'this app is trusted by the company',
created_at: new Date('2021-07-01').toISOString(),
};
};

const getTrustedAppProvider = () =>
new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides());

const getExceptionProvider = () => {
// cloneDeep needed because exception mock generator uses state across instances
return cloneDeep(
getExceptionListItemSchemaMock({
...getCommonItemDataOverrides(),
os_types: ['windows'],
updated_at: new Date().toISOString(),
created_by: 'Justa',
updated_by: 'Mara',
entries: [
{
field: 'process.hash.*',
operator: 'included',
type: 'match',
value: '1234234659af249ddf3e40864e9fb241',
},
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/one/two/three',
},
],
tags: ['policy:all'],
})
);
};

describe.each([
['trusted apps', getTrustedAppProvider],
['exceptions/event filters', getExceptionProvider],
])('when using the ArtifactEntryCard component with %s', (_, generateItem) => {
let item: AnyArtifact;
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (
props?: Partial<ArtifactEntryCardProps>
) => ReturnType<AppContextTestRender['render']>;

beforeEach(() => {
item = generateItem();
appTestContext = createAppRootMockRenderer();
render = (props = {}) => {
renderResult = appTestContext.render(
<ArtifactEntryCard
{...{
item,
'data-test-subj': 'testCard',
...props,
}}
/>
);
return renderResult;
};
});

it('should display title and who has created and updated it last', async () => {
render();

expect(renderResult.getByTestId('testCard-header-title').textContent).toEqual(
'some internal app'
);
expect(renderResult.getByTestId('testCard-subHeader-touchedBy-createdBy').textContent).toEqual(
'Created byJJusta'
);
expect(renderResult.getByTestId('testCard-subHeader-touchedBy-updatedBy').textContent).toEqual(
'Updated byMMara'
);
});

it('should display Global effected scope', async () => {
render();

expect(renderResult.getByTestId('testCard-subHeader-effectScope-value').textContent).toEqual(
'Applied globally'
);
});

it('should display dates in expected format', () => {
render();

expect(renderResult.getByTestId('testCard-header-updated').textContent).toEqual(
expect.stringMatching(/Last updated(\s seconds? ago|now)/)
);
});

it('should display description if one exists', async () => {
render();

expect(renderResult.getByTestId('testCard-description').textContent).toEqual(item.description);
});

it('should display default empty value if description does not exist', async () => {
item.description = undefined;
render();

expect(renderResult.getByTestId('testCard-description').textContent).toEqual('—');
});

it('should display OS and criteria conditions', () => {
render();

expect(renderResult.getByTestId('testCard-criteriaConditions').textContent).toEqual(
' OSIS WindowsAND process.hash.*IS 1234234659af249ddf3e40864e9fb241AND process.executable.caselessIS /one/two/three'
);
});

it('should NOT show the action menu button if no actions were provided', async () => {
render();
const menuButton = await renderResult.queryByTestId('testCard-header-actions-button');

expect(menuButton).toBeNull();
});

describe('and actions were defined', () => {
let actions: ArtifactEntryCardProps['actions'];

beforeEach(() => {
actions = [
{
'data-test-subj': 'test-action',
children: 'action one',
},
];
});

it('should show the actions icon when actions were defined', () => {
render({ actions });

expect(renderResult.getByTestId('testCard-header-actions-button')).not.toBeNull();
});

it('should show popup with defined actions', async () => {
render({ actions });
await act(async () => {
await fireEvent.click(renderResult.getByTestId('testCard-header-actions-button'));
});

const bodyHtmlElement = renderResult.baseElement as HTMLElement;

expect(getByTestId(bodyHtmlElement, 'testCard-header-actions-popoverPanel')).not.toBeNull();
expect(getByTestId(bodyHtmlElement, 'test-action')).not.toBeNull();
});
});

describe('and artifact is defined per policy', () => {
let policies: ArtifactEntryCardProps['policies'];

beforeEach(() => {
if (isTrustedApp(item)) {
item.effectScope = {
type: 'policy',
policies: ['policy-1'],
};
} else {
item.tags = ['policy:policy-1'];
}

policies = {
'policy-1': {
children: 'Policy one',
'data-test-subj': 'policyMenuItem',
},
};
});

it('should display correct label with count of policies', () => {
render({ policies });

expect(renderResult.getByTestId('testCard-subHeader-effectScope-value').textContent).toEqual(
'Applied to 1 policy'
);
});

it('should display effected scope as a button', () => {
render({ policies });

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
).not.toBeNull();
});

it('should show popup menu with list of associated policies when clicked', async () => {
render({ policies });
await act(async () => {
await fireEvent.click(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
);
});

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
).not.toBeNull();

expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one');
});

it('should display policy ID if no policy menu item found in `policies` prop', async () => {
render();
await act(async () => {
await fireEvent.click(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
);
});

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
).not.toBeNull();

expect(renderResult.getByText('policy-1').textContent).not.toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const ArtifactEntryCard = memo(
</EuiText>
</EuiPanel>

<EuiHorizontalRule margin="xs" />
<EuiHorizontalRule margin="none" />

<EuiPanel hasBorder={false} hasShadow={false} paddingSize="l">
<CriteriaConditions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const GLOBAL_EFFECT_SCOPE = i18n.translate(

export const POLICY_EFFECT_SCOPE = (policyCount = 0) => {
return i18n.translate('xpack.securitySolution.artifactCard.policyEffectScope', {
defaultMessage: 'Applied to {count} policies',
defaultMessage: 'Applied to {count} {count, plural, one {policy} other {policies}}',
values: {
count: policyCount,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import { useMemo } from 'react';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { AnyArtifact, ArtifactInfo } from '../types';
import { TrustedApp } from '../../../../../common/endpoint/types';
import { EffectScope, TrustedApp } from '../../../../../common/endpoint/types';
import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping';

/**
* Takes in any artifact and return back a new data structure used internally with by the card's components
Expand All @@ -37,16 +38,20 @@ export const useNormalizedArtifact = (item: AnyArtifact): ArtifactInfo => {
description,
entries: (entries as unknown) as ArtifactInfo['entries'],
os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item),
effectScope: isTrustedApp(item) ? item.effectScope : { type: 'global' },
effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item),
};
}, [item]);
};

const isTrustedApp = (item: AnyArtifact): item is TrustedApp => {
export const isTrustedApp = (item: AnyArtifact): item is TrustedApp => {
return 'effectScope' in item;
};

const getOsFromExceptionItem = (item: ExceptionListItemSchema): string => {
// FYI: Exceptions seem to allow for items to be assigned to more than one OS, unlike Event Filters and Trusted Apps
return item.os_types.join(', ');
};

const getEffectScopeFromExceptionItem = (item: ExceptionListItemSchema): EffectScope => {
return tagsToEffectScope(item.tags);
};
Loading

0 comments on commit 0e8cb0c

Please sign in to comment.