Skip to content

Commit

Permalink
[Security Solution][DQD] Persist new fields in results storage (#185025)
Browse files Browse the repository at this point in the history
Addresses #184751

## Summary

This PR addresses couple of issues:

### Main:
Persist revamped `resultsFieldMap` schema fields, namely
`incompatibleFieldMappingItems`, `incompatibleFieldValueItems` and
`sameFamilyFieldItems` in the `StorageResult` after index check, so that
after release user can start accumulating data in these fields, while we
prepare main UI changes.

### Additional:
Improve and narrow down existing in-house `EcsFlat` override type that
originally comes from `@elastic/ecs` npm package, because currently it
is too generic and too loose, resulting in an unnecessary conditional
checks and leads to perception of impossible states most of which are
refactored, cleaned and fixed in this PR.

### Screenshots

![image](https://github.com/elastic/kibana/assets/1625373/1cd13459-cf15-4026-84e8-3dea05eedf4d)

![image](https://github.com/elastic/kibana/assets/1625373/92593502-598a-439c-8c8e-fe3174ba963e)

![image](https://github.com/elastic/kibana/assets/1625373/67472930-5aee-4689-b748-44235bf4d9c0)

### How to test

1. Prepare index with invalid mapping and value fields + 1 same family
field
```graphql
DELETE test-field-items

PUT test-field-items
{
  "mappings": {
    "properties": {
      "event.category": { "type": "keyword"},
      "agent.type": {"type": "constant_keyword" },
      "source.ip": {"type": "text"}
    }
  }
}

PUT test-field-items/_doc/1
{
  "@timestamp": "2016-05-23T08:05:34.853Z",
  "event.category": "behavior"
}

PUT test-field-items/_doc/2
{
  "@timestamp": "2016-05-23T08:05:34.853Z",
  "event.category": "shmehavior"
}
```  
2. Open DQD dashboard in kibana
3. Create `test-*` data-view with `test-*` index pattern
4. Select it in the sourcerer
5. Click expand button near test-field-items index 
6. Verify that you have 1 mapping + 1 value incompatible field + 1 same
family field
7. Open kibana devtools 
8. Run
```graphql
GET .kibana-data-quality-dashboard-results-default/_search
{
  "size": 0,
  "query": { 
    "term": {
      "indexName": {
        "value": "test-field-items"
      }
    } 
  },
  "aggs": {
    "latest": {
      "terms": { "field": "indexName", "size": 10000 },
      "aggs": { 
        "latest_doc": { 
          "top_hits": { 
            "size": 1, 
            "sort": [{ "@timestamp": { "order": "desc" } }] 
          } 
        } 
      }
    }
  }
}
```
9. Verify that latest result contains `incompatibleFieldItems` and
`sameFamilyFieldItems` of expected shape:
```json5
//...
                     "incompatibleFieldValueItems": [
                      {
                        "fieldName": "event.category",
                        "expectedValues": [
                          "api",
                          "authentication",
                          "configuration",
                          "database",
                          "driver",
                          "email",
                          "file",
                          "host",
                          "iam",
                          "intrusion_detection",
                          "library",
                          "malware",
                          "network",
                          "package",
                          "process",
                          "registry",
                          "session",
                          "threat",
                          "vulnerability",
                          "web"
                        ],
                        "actualValues": [
                          { "name": "behavior",  count: 2 },
                          { "name": "shmehavior", count: 1}
                        ],
                        "description": """This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.
`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.
This field is an array. This will allow proper categorization of some events that fall in multiple categories."""
                      }
                     ],
                     "incompatibleFieldMappingItems": [
                      {
                        "fieldName": "source.ip",
                        "expectedValue": "ip",
                        "actualValue": "text",
                        "description": "IP address of the source (IPv4 or IPv6)."
                      }
                    ]
//...
"sameFamilyFieldItems": [
                      {
                        "fieldName": "agent.type",
                        "expectedValue": "keyword",
                        "actualValue": "constant_keyword",
                        "description": """Type of the agent.
The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine."""
                      }
                    ]
```
  • Loading branch information
kapral18 authored Jun 13, 2024
1 parent ed70d4c commit 4bc1227
Show file tree
Hide file tree
Showing 45 changed files with 568 additions and 624 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';
import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations';
import {
eventCategory,
someField,
eventCategoryWithUnallowedValues,
} from '../../mock/enriched_field_metadata/mock_enriched_field_metadata';
import { TestProviders } from '../../mock/test_providers/test_providers';
Expand Down Expand Up @@ -261,15 +262,9 @@ describe('getCommonTableColumns', () => {
const columns = getCommonTableColumns();
const descriptionolumnRender = columns[5].render;

const withDescription: EnrichedFieldMetadata = {
...eventCategory,
description: undefined,
};

render(
<TestProviders>
{descriptionolumnRender != null &&
descriptionolumnRender(withDescription.description, withDescription)}
{descriptionolumnRender != null && descriptionolumnRender(undefined, someField)}
</TestProviders>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,25 @@ export const getCommonTableColumns = (): Array<
{
field: 'indexFieldType',
name: i18n.INDEX_MAPPING_TYPE_ACTUAL,
render: (_, x) =>
x.type != null && x.indexFieldType !== x.type ? (
getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType }) ? (
render: (_, x) => {
// if custom field or ecs based field with mapping match
if (!x.hasEcsMetadata || x.indexFieldType === x.type) {
return <CodeSuccess data-test-subj="codeSuccess">{x.indexFieldType}</CodeSuccess>;
}

// mapping mismatch due to same family
if (getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType })) {
return (
<div>
<CodeSuccess data-test-subj="codeSuccess">{x.indexFieldType}</CodeSuccess>
<SameFamily />
</div>
) : (
<CodeDanger data-test-subj="codeDanger">{x.indexFieldType}</CodeDanger>
)
) : (
<CodeSuccess data-test-subj="codeSuccess">{x.indexFieldType}</CodeSuccess>
),
);
}

// mapping mismatch
return <CodeDanger data-test-subj="codeDanger">{x.indexFieldType}</CodeDanger>;
},
sortable: true,
truncateText: false,
width: '15%',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import React from 'react';
import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { eventCategory } from '../../mock/enriched_field_metadata/mock_enriched_field_metadata';
import { EnrichedFieldMetadata } from '../../types';
import { EMPTY_PLACEHOLDER, getIncompatibleMappingsTableColumns } from '.';
import { EcsBasedFieldMetadata } from '../../types';
import { getIncompatibleMappingsTableColumns } from '.';

describe('getIncompatibleMappingsTableColumns', () => {
test('it returns the expected column configuration', () => {
Expand Down Expand Up @@ -65,19 +65,6 @@ describe('getIncompatibleMappingsTableColumns', () => {

expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected);
});

test('it renders an empty placeholder when type is undefined', () => {
const columns = getIncompatibleMappingsTableColumns();
const typeColumnRender = columns[1].render;

render(
<TestProviders>
{typeColumnRender != null && typeColumnRender(undefined, eventCategory)}
</TestProviders>
);

expect(screen.getByTestId('codeSuccess')).toHaveTextContent(EMPTY_PLACEHOLDER);
});
});

describe('indexFieldType column render()', () => {
Expand All @@ -88,7 +75,7 @@ describe('getIncompatibleMappingsTableColumns', () => {
const columns = getIncompatibleMappingsTableColumns();
const indexFieldTypeColumnRender = columns[2].render;

const withTypeMismatchSameFamily: EnrichedFieldMetadata = {
const withTypeMismatchSameFamily: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType, // this index has a mapping of `wildcard` instead of `keyword`
isInSameFamily: true, // `wildcard` and `keyword` are in the same family
Expand Down Expand Up @@ -121,7 +108,7 @@ describe('getIncompatibleMappingsTableColumns', () => {
const columns = getIncompatibleMappingsTableColumns();
const indexFieldTypeColumnRender = columns[2].render;

const withTypeMismatchDifferentFamily: EnrichedFieldMetadata = {
const withTypeMismatchDifferentFamily: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType, // this index has a mapping of `text` instead of `keyword`
isInSameFamily: false, // `text` and `wildcard` are not in the same family
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import React from 'react';
import { SameFamily } from '../../data_quality_panel/same_family';
import { CodeDanger, CodeSuccess } from '../../styles';
import * as i18n from '../translations';
import type { EnrichedFieldMetadata } from '../../types';
import type { EcsBasedFieldMetadata } from '../../types';

export const EMPTY_PLACEHOLDER = '--';

export const getIncompatibleMappingsTableColumns = (): Array<
EuiTableFieldDataColumnType<EnrichedFieldMetadata>
EuiTableFieldDataColumnType<EcsBasedFieldMetadata>
> => [
{
field: 'indexFieldName',
Expand All @@ -28,11 +28,7 @@ export const getIncompatibleMappingsTableColumns = (): Array<
{
field: 'type',
name: i18n.ECS_MAPPING_TYPE_EXPECTED,
render: (type: string) => (
<CodeSuccess data-test-subj="codeSuccess">
{type != null ? type : EMPTY_PLACEHOLDER}
</CodeSuccess>
),
render: (type: string) => <CodeSuccess data-test-subj="codeSuccess">{type}</CodeSuccess>,
sortable: true,
truncateText: false,
width: '25%',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { omit } from 'lodash/fp';
import React from 'react';

import {
EMPTY_PLACEHOLDER,
getCustomTableColumns,
getEcsCompliantTableColumns,
getIncompatibleValuesTableColumns,
Expand Down Expand Up @@ -117,31 +116,6 @@ describe('helpers', () => {
expect(screen.queryByTestId('typePlaceholder')).not.toBeInTheDocument();
});
});

describe('when `type` is undefined', () => {
beforeEach(() => {
const withUndefinedType = {
...eventCategory,
type: undefined, // <--
};
const columns = getEcsCompliantTableColumns();
const typeRender = columns[1].render;

render(
<TestProviders>
<>{typeRender != null && typeRender(withUndefinedType.type, withUndefinedType)}</>
</TestProviders>
);
});

test('it does NOT render the `type`', () => {
expect(screen.queryByTestId('type')).not.toBeInTheDocument();
});

test('it renders the placeholder', () => {
expect(screen.getByTestId('typePlaceholder')).toHaveTextContent(EMPTY_PLACEHOLDER);
});
});
});

describe('allowed values render()', () => {
Expand Down Expand Up @@ -230,35 +204,6 @@ describe('helpers', () => {
expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument();
});
});

describe('when `description` is undefined', () => {
const withUndefinedDescription = {
...eventCategory,
description: undefined, // <--
};

beforeEach(() => {
const columns = getEcsCompliantTableColumns();
const descriptionRender = columns[3].render;

render(
<TestProviders>
<>
{descriptionRender != null &&
descriptionRender(withUndefinedDescription.description, withUndefinedDescription)}
</>
</TestProviders>
);
});

test('it does NOT render the `description`', () => {
expect(screen.queryByTestId('description')).not.toBeInTheDocument();
});

test('it renders the placeholder', () => {
expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument();
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ import { EcsAllowedValues } from './ecs_allowed_values';
import { IndexInvalidValues } from './index_invalid_values';
import { CodeSuccess } from '../styles';
import * as i18n from './translations';
import type { AllowedValue, EnrichedFieldMetadata, UnallowedValueCount } from '../types';
import type {
AllowedValue,
CustomFieldMetadata,
EcsBasedFieldMetadata,
UnallowedValueCount,
} from '../types';

export const EMPTY_PLACEHOLDER = '--';

export const getCustomTableColumns = (): Array<
EuiTableFieldDataColumnType<EnrichedFieldMetadata>
EuiTableFieldDataColumnType<CustomFieldMetadata>
> => [
{
field: 'indexFieldName',
Expand All @@ -40,7 +45,7 @@ export const getCustomTableColumns = (): Array<
];

export const getEcsCompliantTableColumns = (): Array<
EuiTableFieldDataColumnType<EnrichedFieldMetadata>
EuiTableFieldDataColumnType<EcsBasedFieldMetadata>
> => [
{
field: 'indexFieldName',
Expand All @@ -52,12 +57,7 @@ export const getEcsCompliantTableColumns = (): Array<
{
field: 'type',
name: i18n.ECS_MAPPING_TYPE,
render: (type: string | undefined) =>
type != null ? (
<CodeSuccess data-test-subj="type">{type}</CodeSuccess>
) : (
<EuiCode data-test-subj="typePlaceholder">{EMPTY_PLACEHOLDER}</EuiCode>
),
render: (type: string) => <CodeSuccess data-test-subj="type">{type}</CodeSuccess>,
sortable: true,
truncateText: false,
width: '25%',
Expand All @@ -75,20 +75,15 @@ export const getEcsCompliantTableColumns = (): Array<
{
field: 'description',
name: i18n.ECS_DESCRIPTION,
render: (description: string | undefined) =>
description != null ? (
<span data-test-subj="description">{description}</span>
) : (
<EuiCode data-test-subj="emptyPlaceholder">{EMPTY_PLACEHOLDER}</EuiCode>
),
render: (description: string) => <span data-test-subj="description">{description}</span>,
sortable: false,
truncateText: false,
width: '25%',
},
];

export const getIncompatibleValuesTableColumns = (): Array<
EuiTableFieldDataColumnType<EnrichedFieldMetadata>
EuiTableFieldDataColumnType<EcsBasedFieldMetadata>
> => [
{
field: 'indexFieldName',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ const search: Search = {
},
};

interface Props {
enrichedFieldMetadata: EnrichedFieldMetadata[];
getTableColumns: () => Array<EuiTableFieldDataColumnType<EnrichedFieldMetadata>>;
interface Props<T extends EnrichedFieldMetadata> {
enrichedFieldMetadata: T[];
getTableColumns: () => Array<EuiTableFieldDataColumnType<T>>;
title: string;
}

const CompareFieldsTableComponent: React.FC<Props> = ({
const CompareFieldsTableComponent = <T extends EnrichedFieldMetadata>({
enrichedFieldMetadata,
getTableColumns,
title,
}) => {
}: Props<T>): React.ReactElement => {
const columns = useMemo(() => getTableColumns(), [getTableColumns]);

return (
Expand All @@ -53,4 +53,8 @@ const CompareFieldsTableComponent: React.FC<Props> = ({

CompareFieldsTableComponent.displayName = 'CompareFieldsTableComponent';

export const CompareFieldsTable = React.memo(CompareFieldsTableComponent);
export const CompareFieldsTable = React.memo(
CompareFieldsTableComponent
// React.memo doesn't pass generics through so
// this is a cheap fix without sacrificing type safety
) as typeof CompareFieldsTableComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { EcsFlat } from '@elastic/ecs';
import { EcsFieldMetadata } from './types';

export const EcsFlatTyped = EcsFlat as unknown as Record<string, EcsFieldMetadata>;
export type EcsFlatTyped = typeof EcsFlatTyped;
Loading

0 comments on commit 4bc1227

Please sign in to comment.