mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[D4C] Further cloud_defend policy validation work (#154616)
## Summary Adds some additional validation to the yaml editor for both string byte length checks as well as combined maximum allowed selectors and responses by type. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Screenshots   
This commit is contained in:
parent
6431787de1
commit
db5ad71637
16 changed files with 307 additions and 98 deletions
|
@ -7,9 +7,9 @@
|
|||
export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 10; // generic default # of table rows to show (currently we only have a list of policies)
|
||||
export const LOCAL_STORAGE_PAGE_SIZE = 'cloudDefend:userPageSize';
|
||||
export const VALID_SELECTOR_NAME_REGEX = /^[a-z0-9][a-z0-9_\-]*$/i; // alphanumberic (no - or _ allowed on first char)
|
||||
export const MAX_SELECTORS_AND_RESPONSES_PER_TYPE = 64;
|
||||
export const MAX_SELECTOR_NAME_LENGTH = 128; // chars
|
||||
export const MAX_CONDITION_VALUE_LENGTH_BYTES = 511;
|
||||
export const MAX_FILE_PATH_VALUE_LENGTH_BYTES = 255;
|
||||
export const MAX_CONDITION_VALUE_LENGTH_BYTES = 511; // max length for all condition values. some props override this in cloud_defend/public/types.ts
|
||||
|
||||
// TODO: temporary until I change condition value length checks in the yaml editor view to be byte based.
|
||||
export const MAX_CONDITION_VALUE_LENGTH = 64;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import yaml from 'js-yaml';
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Selector,
|
||||
Response,
|
||||
|
@ -17,6 +18,10 @@ import {
|
|||
SelectorConditionsMap,
|
||||
SelectorCondition,
|
||||
} from '../types';
|
||||
import {
|
||||
MAX_CONDITION_VALUE_LENGTH_BYTES,
|
||||
MAX_SELECTORS_AND_RESPONSES_PER_TYPE,
|
||||
} from './constants';
|
||||
|
||||
export function getInputFromPolicy(policy: NewPackagePolicy, inputId: string) {
|
||||
return policy.inputs.find((input) => input.type === inputId);
|
||||
|
@ -49,6 +54,79 @@ export function conditionCombinationInvalid(
|
|||
return !!invalid;
|
||||
}
|
||||
|
||||
type TotalByType = {
|
||||
[key in SelectorType]: number;
|
||||
};
|
||||
|
||||
export function getTotalsByType(selectors: Selector[], responses: Response[]) {
|
||||
const totalsByType: TotalByType = { process: 0, file: 0 };
|
||||
|
||||
selectors.forEach((selector) => {
|
||||
totalsByType[selector.type]++;
|
||||
});
|
||||
|
||||
responses.forEach((response) => {
|
||||
totalsByType[response.type]++;
|
||||
});
|
||||
|
||||
return totalsByType;
|
||||
}
|
||||
|
||||
export function validateMaxSelectorsAndResponses(selectors: Selector[], responses: Response[]) {
|
||||
const errors: string[] = [];
|
||||
const totalsByType = getTotalsByType(selectors, responses);
|
||||
|
||||
// check selectors + responses doesn't exceed MAX_SELECTORS_AND_RESPONSES_PER_TYPE
|
||||
Object.values(totalsByType).forEach((count) => {
|
||||
if (count > MAX_SELECTORS_AND_RESPONSES_PER_TYPE) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.cloudDefend.errorMaxSelectorsResponsesExceeded', {
|
||||
defaultMessage:
|
||||
'You cannot exceed {max} selectors + responses for a given type e.g file, process',
|
||||
values: { max: MAX_SELECTORS_AND_RESPONSES_PER_TYPE },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateStringValuesForCondition(condition: SelectorCondition, values: string[]) {
|
||||
const errors: string[] = [];
|
||||
const maxValueBytes =
|
||||
SelectorConditionsMap[condition].maxValueBytes || MAX_CONDITION_VALUE_LENGTH_BYTES;
|
||||
|
||||
const { pattern, patternError } = SelectorConditionsMap[condition];
|
||||
|
||||
values.forEach((value) => {
|
||||
if (pattern && !new RegExp(pattern).test(value)) {
|
||||
if (patternError) {
|
||||
errors.push(patternError);
|
||||
} else {
|
||||
errors.push(
|
||||
i18n.translate('xpack.cloudDefend.errorGenericRegexFailure', {
|
||||
defaultMessage: '"{condition}" values must match the pattern: /{pattern}/',
|
||||
values: { condition, pattern },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = new Blob([value]).size;
|
||||
if (bytes > maxValueBytes) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.cloudDefend.errorMaxValueBytesExceeded', {
|
||||
defaultMessage: '"{condition}" values cannot exceed {maxValueBytes} bytes',
|
||||
values: { condition, maxValueBytes },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function getRestrictedValuesForCondition(
|
||||
type: SelectorType,
|
||||
condition: SelectorCondition
|
||||
|
|
|
@ -10,7 +10,11 @@ import { render, waitFor } from '@testing-library/react';
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TestProvider } from '../../test/test_provider';
|
||||
import { getCloudDefendNewPolicyMock, MOCK_YAML_INVALID_CONFIGURATION } from '../../test/mocks';
|
||||
import {
|
||||
getCloudDefendNewPolicyMock,
|
||||
MOCK_YAML_INVALID_CONFIGURATION,
|
||||
MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES,
|
||||
} from '../../test/mocks';
|
||||
import { ControlGeneralView } from '.';
|
||||
import { getInputFromPolicy } from '../../common/utils';
|
||||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
|
@ -156,4 +160,15 @@ describe('<ControlGeneralView />', () => {
|
|||
expect(queryAllByTestId('cloud-defend-selector')).toHaveLength(0);
|
||||
expect(queryAllByTestId('cloud-defend-response')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('prevents the user from adding more than MAX_SELECTORS_AND_RESPONSES_PER_TYPE', async () => {
|
||||
const { getByTestId } = render(
|
||||
<WrappedComponent
|
||||
policy={getCloudDefendNewPolicyMock(MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES)}
|
||||
/>
|
||||
);
|
||||
|
||||
userEvent.click(getByTestId('cloud-defend-btnAddSelector'));
|
||||
expect(getByTestId('cloud-defend-btnAddFileSelector')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,22 +24,26 @@ import {
|
|||
getSelectorsAndResponsesFromYaml,
|
||||
getDefaultSelectorByType,
|
||||
getDefaultResponseByType,
|
||||
getTotalsByType,
|
||||
} from '../../common/utils';
|
||||
import { SelectorType, Selector, Response, ViewDeps } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { ControlGeneralViewSelector } from '../control_general_view_selector';
|
||||
import { ControlGeneralViewResponse } from '../control_general_view_response';
|
||||
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../../common/constants';
|
||||
|
||||
interface AddSelectorButtonProps {
|
||||
type: 'Selector' | 'Response';
|
||||
onSelectType(type: SelectorType): void;
|
||||
selectors: Selector[];
|
||||
responses: Response[];
|
||||
}
|
||||
|
||||
/**
|
||||
* dual purpose button for adding selectors and responses by type
|
||||
*/
|
||||
const AddButton = ({ type, onSelectType, selectors }: AddSelectorButtonProps) => {
|
||||
const AddButton = ({ type, onSelectType, selectors, responses }: AddSelectorButtonProps) => {
|
||||
const totalsByType = useMemo(() => getTotalsByType(selectors, responses), [responses, selectors]);
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
|
@ -84,7 +88,10 @@ const AddButton = ({ type, onSelectType, selectors }: AddSelectorButtonProps) =>
|
|||
key={`addFile${type}`}
|
||||
icon="document"
|
||||
onClick={addFile}
|
||||
disabled={type === 'Response' && selectorCounts.file === 0}
|
||||
disabled={
|
||||
(type === 'Response' && selectorCounts.file === 0) ||
|
||||
totalsByType.file >= MAX_SELECTORS_AND_RESPONSES_PER_TYPE
|
||||
}
|
||||
data-test-subj={`cloud-defend-btnAddFile${type}`}
|
||||
>
|
||||
{isSelector ? i18n.fileSelector : i18n.fileResponse}
|
||||
|
@ -93,7 +100,10 @@ const AddButton = ({ type, onSelectType, selectors }: AddSelectorButtonProps) =>
|
|||
key={`addProcess${type}`}
|
||||
icon="gear"
|
||||
onClick={addProcess}
|
||||
disabled={type === 'Response' && selectorCounts.process === 0}
|
||||
disabled={
|
||||
(type === 'Response' && selectorCounts.process === 0) ||
|
||||
totalsByType.process >= MAX_SELECTORS_AND_RESPONSES_PER_TYPE
|
||||
}
|
||||
data-test-subj={`cloud-defend-btnAddProcess${type}`}
|
||||
>
|
||||
{isSelector ? i18n.processSelector : i18n.processResponse}
|
||||
|
@ -343,7 +353,12 @@ export const ControlGeneralView = ({ policy, onChange, show }: ViewDeps) => {
|
|||
);
|
||||
})}
|
||||
|
||||
<AddButton type="Selector" onSelectType={onAddSelector} selectors={selectors} />
|
||||
<AddButton
|
||||
type="Selector"
|
||||
onSelectType={onAddSelector}
|
||||
selectors={selectors}
|
||||
responses={responses}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
|
@ -371,7 +386,12 @@ export const ControlGeneralView = ({ policy, onChange, show }: ViewDeps) => {
|
|||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
<AddButton type="Response" onSelectType={onAddResponse} selectors={selectors} />
|
||||
<AddButton
|
||||
type="Response"
|
||||
onSelectType={onAddResponse}
|
||||
selectors={selectors}
|
||||
responses={responses}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -148,13 +148,6 @@ export const errorValueRequired = i18n.translate('xpack.cloudDefend.errorValueRe
|
|||
defaultMessage: 'At least one value is required.',
|
||||
});
|
||||
|
||||
export const errorValueLengthExceeded = i18n.translate(
|
||||
'xpack.cloudDefend.errorValueLengthExceeded',
|
||||
{
|
||||
defaultMessage: 'Values must not exceed 32 characters.',
|
||||
}
|
||||
);
|
||||
|
||||
export const getSelectorIconTooltip = (type: SelectorType) => {
|
||||
switch (type) {
|
||||
case 'process':
|
||||
|
|
|
@ -189,7 +189,7 @@ describe('<ControlGeneralViewSelector />', () => {
|
|||
throw new Error("Can't find input");
|
||||
}
|
||||
|
||||
expect(getByText(i18n.errorValueLengthExceeded)).toBeTruthy();
|
||||
expect(getByText('"containerImageName" values cannot exceed 511 bytes')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('prevents targetFilePath conditions from having values that exceed MAX_FILE_PATH_VALUE_LENGTH_BYTES', async () => {
|
||||
|
@ -212,7 +212,7 @@ describe('<ControlGeneralViewSelector />', () => {
|
|||
throw new Error("Can't find input");
|
||||
}
|
||||
|
||||
expect(getByText(i18n.errorValueLengthExceeded)).toBeTruthy();
|
||||
expect(getByText('"targetFilePath" values cannot exceed 255 bytes')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows an error if condition values fail their pattern regex', async () => {
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
EuiText,
|
||||
EuiCheckbox,
|
||||
} from '@elastic/eui';
|
||||
import { i18n as i18nLib } from '@kbn/i18n';
|
||||
import { useStyles } from './styles';
|
||||
import {
|
||||
ControlGeneralViewSelectorDeps,
|
||||
|
@ -40,16 +39,10 @@ import {
|
|||
getSelectorTypeIcon,
|
||||
conditionCombinationInvalid,
|
||||
getRestrictedValuesForCondition,
|
||||
validateStringValuesForCondition,
|
||||
} from '../../common/utils';
|
||||
import * as i18n from '../control_general_view/translations';
|
||||
import {
|
||||
VALID_SELECTOR_NAME_REGEX,
|
||||
MAX_SELECTOR_NAME_LENGTH,
|
||||
MAX_CONDITION_VALUE_LENGTH_BYTES,
|
||||
MAX_FILE_PATH_VALUE_LENGTH_BYTES,
|
||||
} from '../../common/constants';
|
||||
|
||||
const { translate } = i18nLib;
|
||||
import { VALID_SELECTOR_NAME_REGEX, MAX_SELECTOR_NAME_LENGTH } from '../../common/constants';
|
||||
|
||||
interface ConditionProps {
|
||||
label: string;
|
||||
|
@ -290,30 +283,11 @@ export const ControlGeneralViewSelector = ({
|
|||
errors.push(i18n.errorValueRequired);
|
||||
}
|
||||
|
||||
const { pattern, patternError } = SelectorConditionsMap[prop];
|
||||
const stringValueErrors = validateStringValuesForCondition(prop, values);
|
||||
|
||||
values.forEach((value) => {
|
||||
const bytes = new Blob([value]).size;
|
||||
|
||||
if (pattern && !new RegExp(pattern).test(value)) {
|
||||
if (patternError) {
|
||||
errors.push(patternError);
|
||||
} else {
|
||||
errors.push(
|
||||
translate('xpack.cloudDefend.errorGenericRegexFailure', {
|
||||
defaultMessage: '"{prop}" values must match the pattern: /{pattern}/',
|
||||
values: { prop, pattern },
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (prop === 'targetFilePath') {
|
||||
if (bytes > MAX_FILE_PATH_VALUE_LENGTH_BYTES) {
|
||||
errors.push(i18n.errorValueLengthExceeded);
|
||||
}
|
||||
} else if (bytes > MAX_CONDITION_VALUE_LENGTH_BYTES) {
|
||||
errors.push(i18n.errorValueLengthExceeded);
|
||||
}
|
||||
});
|
||||
if (stringValueErrors.length > 0) {
|
||||
errors.push(...stringValueErrors);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
errorMap[prop] = errors;
|
||||
|
|
|
@ -74,7 +74,6 @@ export const ControlSettings = ({ policy, onChange }: SettingsDeps) => {
|
|||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{/** general view removed from DOM for performance and to avoid errors when invalid yaml is passed to it**/}
|
||||
{isGeneralViewSelected && (
|
||||
<ControlGeneralView
|
||||
show={isGeneralViewSelected}
|
||||
|
@ -82,9 +81,9 @@ export const ControlSettings = ({ policy, onChange }: SettingsDeps) => {
|
|||
onChange={onGeneralChanges}
|
||||
/>
|
||||
)}
|
||||
{/** Yaml view is kept in the dom at all times to prevent some sizing/rendering issues.
|
||||
Also only listening for changes if yaml view visible to avoid isValid race condition **/}
|
||||
<ControlYamlView show={isYamlViewSelected} policy={policy} onChange={onYamlChanges} />
|
||||
{isYamlViewSelected && (
|
||||
<ControlYamlView show={isYamlViewSelected} policy={policy} onChange={onYamlChanges} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -8,8 +8,16 @@ import React from 'react';
|
|||
import { render } from '@testing-library/react';
|
||||
import '@kbn/kibana-react-plugin/public/code_editor/code_editor.test.helpers';
|
||||
import { TestProvider } from '../../test/test_provider';
|
||||
import { getCloudDefendNewPolicyMock, MOCK_YAML_INVALID_CONFIGURATION } from '../../test/mocks';
|
||||
import {
|
||||
getCloudDefendNewPolicyMock,
|
||||
MOCK_YAML_INVALID_CONFIGURATION,
|
||||
MOCK_YAML_INVALID_ACTIONS,
|
||||
MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES,
|
||||
MOCK_YAML_INVALID_STRING_ARRAY_CONDITION,
|
||||
} from '../../test/mocks';
|
||||
import { ControlYamlView } from '.';
|
||||
import * as i18n from './translations';
|
||||
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../../common/constants';
|
||||
|
||||
describe('<ControlYamlView />', () => {
|
||||
const onChange = jest.fn();
|
||||
|
@ -31,4 +39,39 @@ describe('<ControlYamlView />', () => {
|
|||
<WrappedComponent policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_CONFIGURATION)} />
|
||||
);
|
||||
});
|
||||
|
||||
it('handles additionalErrors: max selectors+responses exceeded ', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<WrappedComponent
|
||||
policy={getCloudDefendNewPolicyMock(MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES)}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
|
||||
expect(
|
||||
getByText(
|
||||
`You cannot exceed ${MAX_SELECTORS_AND_RESPONSES_PER_TYPE} selectors + responses for a given type e.g file, process`
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles additionalErrors: block action error', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<WrappedComponent policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_ACTIONS)} />
|
||||
);
|
||||
|
||||
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
|
||||
expect(getByText(i18n.errorAlertActionRequired)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles additionalErrors: selector condition value byte length', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<WrappedComponent
|
||||
policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_STRING_ARRAY_CONDITION)}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
|
||||
expect(getByText('"sessionLeaderName" values cannot exceed 16 bytes')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,36 +8,78 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||
import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiForm } from '@elastic/eui';
|
||||
import { CodeEditor, YamlLang } from '@kbn/kibana-react-plugin/public';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import yaml from 'js-yaml';
|
||||
import { uniq } from 'lodash';
|
||||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
import { useStyles } from './styles';
|
||||
import { useConfigModel } from './hooks/use_config_model';
|
||||
import { getInputFromPolicy } from '../../common/utils';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
validateStringValuesForCondition,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
validateMaxSelectorsAndResponses,
|
||||
} from '../../common/utils';
|
||||
import * as i18n from './translations';
|
||||
import { ViewDeps } from '../../types';
|
||||
import { ViewDeps, SelectorConditionsMap, SelectorCondition } from '../../types';
|
||||
|
||||
const { editor } = monaco;
|
||||
|
||||
const TEXT_EDITOR_PADDING = 10;
|
||||
|
||||
interface ConfigError {
|
||||
interface EditorError {
|
||||
line: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const ControlYamlView = ({ policy, onChange, show }: ViewDeps) => {
|
||||
const styles = useStyles();
|
||||
const [errors, setErrors] = useState<ConfigError[]>([]);
|
||||
const [actionsValid, setActionsValid] = useState(true);
|
||||
const [editorErrors, setEditorErrors] = useState<EditorError[]>([]);
|
||||
const [additionalErrors, setAdditionalErrors] = useState<string[]>([]);
|
||||
const input = getInputFromPolicy(policy, INPUT_CONTROL);
|
||||
const configuration = input?.vars?.configuration?.value || '';
|
||||
const currentModel = useConfigModel(configuration);
|
||||
|
||||
// not all validations can be done via json-schema
|
||||
const validateAdditional = useCallback((value) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(value);
|
||||
|
||||
errors.push(...validateMaxSelectorsAndResponses(selectors, responses));
|
||||
|
||||
// validate selectors
|
||||
selectors.forEach((selector) => {
|
||||
Object.keys(selector).map((prop) => {
|
||||
const condition = prop as SelectorCondition;
|
||||
|
||||
if (SelectorConditionsMap[condition]?.type === 'stringArray') {
|
||||
const values = selector[condition] as string[];
|
||||
errors.push(...validateStringValuesForCondition(condition, values));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// validate responses
|
||||
responses.forEach((response) => {
|
||||
// for now we force 'alert' action if 'block' action added.
|
||||
if (response.actions.includes('block') && !response.actions.includes('alert')) {
|
||||
errors.push(i18n.errorAlertActionRequired);
|
||||
}
|
||||
});
|
||||
|
||||
return uniq(errors);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// for on mount
|
||||
const otherErrors = validateAdditional(configuration);
|
||||
if (otherErrors.length !== additionalErrors.length) {
|
||||
setAdditionalErrors(otherErrors);
|
||||
}
|
||||
|
||||
const listener = editor.onDidChangeMarkers(([resource]) => {
|
||||
const markers = editor.getModelMarkers({ resource });
|
||||
const errs = markers.map((marker) => {
|
||||
const error: ConfigError = {
|
||||
const error: EditorError = {
|
||||
line: marker.startLineNumber,
|
||||
message: marker.message,
|
||||
};
|
||||
|
@ -46,49 +88,38 @@ export const ControlYamlView = ({ policy, onChange, show }: ViewDeps) => {
|
|||
});
|
||||
|
||||
// prevents infinite loop
|
||||
if (JSON.stringify(errs) !== JSON.stringify(errors)) {
|
||||
onChange({ isValid: actionsValid && errs.length === 0, updatedPolicy: policy });
|
||||
setErrors(errs);
|
||||
if (
|
||||
otherErrors.length !== additionalErrors.length ||
|
||||
JSON.stringify(errs) !== JSON.stringify(editorErrors)
|
||||
) {
|
||||
onChange({
|
||||
isValid: otherErrors.length === 0 && errs.length === 0,
|
||||
updatedPolicy: policy,
|
||||
});
|
||||
setEditorErrors(errs);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.dispose();
|
||||
};
|
||||
}, [actionsValid, errors, onChange, policy]);
|
||||
|
||||
// for now we force 'alert' action on all responses. This restriction may be removed in future when we have a plan to record all responses. e.g. audit
|
||||
const validateActions = useCallback((value) => {
|
||||
try {
|
||||
const json = yaml.load(value);
|
||||
|
||||
for (let i = 0; i < json.responses.length; i++) {
|
||||
const response = json.responses[i];
|
||||
|
||||
if (!response.actions.includes('alert')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
}, [editorErrors, onChange, policy, additionalErrors.length, validateAdditional, configuration]);
|
||||
|
||||
const onYamlChange = useCallback(
|
||||
(value) => {
|
||||
if (input?.vars) {
|
||||
input.vars.configuration.value = value;
|
||||
|
||||
const areActionsValid = validateActions(value);
|
||||
const errs = validateAdditional(value);
|
||||
setAdditionalErrors(errs);
|
||||
|
||||
setActionsValid(areActionsValid);
|
||||
|
||||
onChange({ isValid: areActionsValid && errors.length === 0, updatedPolicy: policy });
|
||||
onChange({
|
||||
isValid: errs.length === 0 && editorErrors.length === 0,
|
||||
updatedPolicy: policy,
|
||||
});
|
||||
}
|
||||
},
|
||||
[errors.length, input?.vars, onChange, policy, validateActions]
|
||||
[editorErrors.length, input?.vars, onChange, policy, validateAdditional]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -98,7 +129,13 @@ export const ControlYamlView = ({ policy, onChange, show }: ViewDeps) => {
|
|||
{i18n.controlYamlHelp}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{!actionsValid && <EuiForm isInvalid={true} error={i18n.errorAlertActionRequired} />}
|
||||
{additionalErrors.length > 0 && (
|
||||
<EuiForm
|
||||
data-test-subj="cloudDefendAdditionalErrors"
|
||||
isInvalid={true}
|
||||
error={additionalErrors}
|
||||
/>
|
||||
)}
|
||||
<div css={styles.yamlEditor}>
|
||||
<CodeEditor
|
||||
width="100%"
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const errorAlertActionRequired = i18n.translate('xpack.cloudDefend.alertActionRequired', {
|
||||
defaultMessage:
|
||||
'[Technical Preview] The "alert" action is required on all responses. This restriction will be removed once all responses become auditable.',
|
||||
defaultMessage: 'The alert action is required when "block" action used.',
|
||||
});
|
||||
|
||||
export const controlYamlHelp = i18n.translate('xpack.cloudDefend.controlYamlHelp', {
|
||||
defaultMessage: 'Configure BPF/LSM controls by creating selectors, and responses below.',
|
||||
defaultMessage:
|
||||
'Configure your policy by creating "file" or "process" selectors and responses below.',
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { INTEGRATION_PACKAGE_NAME, INPUT_CONTROL, ALERTS_DATASET } from '../../common/constants';
|
||||
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../common/constants';
|
||||
|
||||
export const MOCK_YAML_CONFIGURATION = `file:
|
||||
selectors:
|
||||
|
@ -34,10 +35,56 @@ export const MOCK_YAML_CONFIGURATION = `file:
|
|||
- alert
|
||||
`;
|
||||
|
||||
// block on it's own should be prevented
|
||||
export const MOCK_YAML_INVALID_ACTIONS = `file:
|
||||
selectors:
|
||||
- name: default
|
||||
operation:
|
||||
- createExecutable
|
||||
- modifyExecutable
|
||||
responses:
|
||||
- match:
|
||||
- default
|
||||
actions:
|
||||
- block
|
||||
`;
|
||||
|
||||
export const MOCK_YAML_INVALID_STRING_ARRAY_CONDITION = `file:
|
||||
selectors:
|
||||
- name: default
|
||||
operation:
|
||||
- createExecutable
|
||||
- modifyExecutable
|
||||
sessionLeaderName:
|
||||
- reallylongsessionleadernamethatshouldnotbeallowed
|
||||
responses:
|
||||
- match:
|
||||
- default
|
||||
actions:
|
||||
- log
|
||||
`;
|
||||
|
||||
export const MOCK_YAML_INVALID_CONFIGURATION = `
|
||||
s
|
||||
`;
|
||||
|
||||
export const MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES = `file:
|
||||
selectors:
|
||||
- name: default
|
||||
operation:
|
||||
- createExecutable
|
||||
- modifyExecutable
|
||||
responses:
|
||||
${new Array(MAX_SELECTORS_AND_RESPONSES_PER_TYPE + 1)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return ` - match: [default]
|
||||
actions: [alert]
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
`;
|
||||
|
||||
export const getCloudDefendNewPolicyMock = (yaml = MOCK_YAML_CONFIGURATION): NewPackagePolicy => ({
|
||||
name: 'some-cloud_defend-policy',
|
||||
description: '',
|
||||
|
|
|
@ -87,6 +87,7 @@ export interface SelectorConditionOptions {
|
|||
pattern?: string;
|
||||
patternError?: string;
|
||||
selectorType?: SelectorType;
|
||||
maxValueBytes?: number; // defaults to const MAX_FILE_PATH_VALUE_LENGTH_BYTES
|
||||
not?: SelectorCondition[];
|
||||
values?:
|
||||
| {
|
||||
|
@ -131,14 +132,18 @@ export const SelectorConditionsMap: SelectorConditionsMapProps = {
|
|||
process: ['fork', 'exec'],
|
||||
},
|
||||
},
|
||||
targetFilePath: { selectorType: 'file', type: 'stringArray' },
|
||||
targetFilePath: {
|
||||
selectorType: 'file',
|
||||
type: 'stringArray',
|
||||
maxValueBytes: 255,
|
||||
},
|
||||
ignoreVolumeFiles: { selectorType: 'file', type: 'flag', not: ['ignoreVolumeMounts'] },
|
||||
ignoreVolumeMounts: { selectorType: 'file', type: 'flag', not: ['ignoreVolumeFiles'] },
|
||||
processExecutable: { selectorType: 'process', type: 'stringArray', not: ['processName'] },
|
||||
processName: { selectorType: 'process', type: 'stringArray', not: ['processExecutable'] },
|
||||
processUserId: { selectorType: 'process', type: 'stringArray' },
|
||||
sessionLeaderInteractive: { selectorType: 'process', type: 'boolean' },
|
||||
sessionLeaderName: { selectorType: 'process', type: 'stringArray' },
|
||||
sessionLeaderName: { selectorType: 'process', type: 'stringArray', maxValueBytes: 16 },
|
||||
};
|
||||
|
||||
export type ResponseAction = 'log' | 'alert' | 'block';
|
||||
|
@ -146,6 +151,7 @@ export type ResponseAction = 'log' | 'alert' | 'block';
|
|||
export interface Selector {
|
||||
name: string;
|
||||
operation?: string[];
|
||||
containerImageFullName?: string[];
|
||||
containerImageName?: string[];
|
||||
containerImageTag?: string[];
|
||||
kubernetesClusterId?: string[];
|
||||
|
|
|
@ -38070,7 +38070,6 @@
|
|||
"xpack.cloudDefend.errorConditionRequired": "Au moins une condition par sélecteur est requise.",
|
||||
"xpack.cloudDefend.errorDuplicateName": "Ce nom est déjà utilisé par un autre sélecteur.",
|
||||
"xpack.cloudDefend.errorInvalidName": "Les noms des sélecteurs doivent être alphanumériques et ne doivent pas inclure d'espaces.",
|
||||
"xpack.cloudDefend.errorValueLengthExceeded": "Les valeurs ne doivent pas dépasser 32 caractères.",
|
||||
"xpack.cloudDefend.errorValueRequired": "Au moins une valeur est requise.",
|
||||
"xpack.cloudDefend.name": "Nom",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "Gérer ce déploiement",
|
||||
|
|
|
@ -38038,7 +38038,6 @@
|
|||
"xpack.cloudDefend.errorConditionRequired": "セレクターにつき1つ以上の条件が必要です。",
|
||||
"xpack.cloudDefend.errorDuplicateName": "この名前はすでに別のセレクターで使用されています。",
|
||||
"xpack.cloudDefend.errorInvalidName": "セレクター名は英数字でなければならず、スペースは使用できません。",
|
||||
"xpack.cloudDefend.errorValueLengthExceeded": "値は32文字以下でなければなりません。",
|
||||
"xpack.cloudDefend.errorValueRequired": "1つ以上の値が必要です。",
|
||||
"xpack.cloudDefend.name": "名前",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "このデプロイの管理",
|
||||
|
|
|
@ -38065,7 +38065,6 @@
|
|||
"xpack.cloudDefend.errorConditionRequired": "每个选择器至少需要一个条件。",
|
||||
"xpack.cloudDefend.errorDuplicateName": "此名称已由其他选择器使用。",
|
||||
"xpack.cloudDefend.errorInvalidName": "选择器名称必须为字母数字,并且不包含空格。",
|
||||
"xpack.cloudDefend.errorValueLengthExceeded": "值不能超过 32 个字符。",
|
||||
"xpack.cloudDefend.errorValueRequired": "至少需要一个值。",
|
||||
"xpack.cloudDefend.name": "名称",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "管理此部署",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue