[Cases] Align cases lint rules with security solution (#117177)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2021-11-04 17:51:02 +02:00 committed by GitHub
parent 4c5e59db56
commit d38fb03820
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 164 additions and 74 deletions

View file

@ -902,17 +902,6 @@ module.exports = {
},
},
/**
* Cases overrides
*/
{
files: ['x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}'],
rules: {
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-imports': ['error'],
},
},
/**
* Security Solution overrides. These rules below are maintained and owned by
* the people within the security-solution-platform team. Please see ping them
@ -928,6 +917,8 @@ module.exports = {
'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/cases/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/cases/common/**/*.{js,mjs,ts,tsx}',
],
rules: {
'import/no-nodejs-modules': 'error',
@ -949,10 +940,12 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/**/*.{ts,tsx}',
'x-pack/plugins/timelines/**/*.{ts,tsx}',
'x-pack/plugins/cases/**/*.{ts,tsx}',
],
excludedFiles: [
'x-pack/plugins/security_solution/**/*.{test,mock,test_helper}.{ts,tsx}',
'x-pack/plugins/timelines/**/*.{test,mock,test_helper}.{ts,tsx}',
'x-pack/plugins/cases/**/*.{test,mock,test_helper}.{ts,tsx}',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 'error',
@ -963,6 +956,7 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/**/*.{ts,tsx}',
'x-pack/plugins/timelines/**/*.{ts,tsx}',
'x-pack/plugins/cases/**/*.{ts,tsx}',
],
rules: {
'@typescript-eslint/no-this-alias': 'error',
@ -985,6 +979,7 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}',
],
plugins: ['eslint-plugin-node', 'react'],
env: {

View file

@ -60,7 +60,7 @@ export const decodeOrThrow =
const getExcessProps = (props: rt.Props, r: Record<string, unknown>): string[] => {
const ex: string[] = [];
for (const k of Object.keys(r)) {
if (!props.hasOwnProperty(k)) {
if (!Object.prototype.hasOwnProperty.call(props, k)) {
ex.push(k);
}
}
@ -89,5 +89,5 @@ export function excess<C extends rt.InterfaceType<rt.Props> | rt.PartialType<rt.
codec.encode,
codec.props
);
return r as any;
return r as C;
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
/* eslint-disable react/display-name */
import React from 'react';
import { RecursivePartial } from '@elastic/eui/src/components/common';

View file

@ -36,7 +36,7 @@ export const mockFormHook = {
__readFieldConfigFromSchema: jest.fn(),
};
export const getFormMock = (sampleData: any) => ({
export const getFormMock = (sampleData: unknown) => ({
...mockFormHook,
submit: () =>
Promise.resolve({

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui';
import { EuiBasicTable } from '@elastic/eui';
import styled from 'styled-components';
import { Case, SubCase } from '../../containers/types';
import { CasesColumns } from './columns';
@ -14,7 +14,7 @@ import { AssociationType } from '../../../common';
type ExpandedRowMap = Record<string, Element> | {};
const EuiBasicTable: any = _EuiBasicTable;
// @ts-expect-error TS2769
const BasicTable = styled(EuiBasicTable)`
thead {
display: none;

View file

@ -303,7 +303,10 @@ describe('AllCasesGeneric', () => {
await waitFor(() => {
result.current.map(
(i, key) => i.name != null && !i.hasOwnProperty('actions') && checkIt(`${i.name}`, key)
(i, key) =>
i.name != null &&
!Object.prototype.hasOwnProperty.call(i, 'actions') &&
checkIt(`${i.name}`, key)
);
});
});
@ -378,7 +381,9 @@ describe('AllCasesGeneric', () => {
})
);
await waitFor(() => {
result.current.map((i) => i.name != null && !i.hasOwnProperty('actions'));
result.current.map(
(i) => i.name != null && !Object.prototype.hasOwnProperty.call(i, 'actions')
);
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy();
});
});

View file

@ -87,5 +87,8 @@ export const AllCasesSelectorModal: React.FC<AllCasesSelectorModalProps> = React
</OwnerProvider>
);
});
AllCasesSelectorModal.displayName = 'AllCasesSelectorModal';
// eslint-disable-next-line import/no-default-export
export { AllCasesSelectorModal as default };

View file

@ -10,7 +10,7 @@ import {
EuiEmptyPrompt,
EuiLoadingContent,
EuiTableSelectionType,
EuiBasicTable as _EuiBasicTable,
EuiBasicTable,
EuiBasicTableProps,
} from '@elastic/eui';
import classnames from 'classnames';
@ -40,12 +40,12 @@ interface CasesTableProps {
selection: EuiTableSelectionType<Case>;
showActions: boolean;
sorting: EuiBasicTableProps<Case>['sorting'];
tableRef: MutableRefObject<_EuiBasicTable | undefined>;
tableRef: MutableRefObject<EuiBasicTable | undefined>;
tableRowProps: EuiBasicTableProps<Case>['rowProps'];
userCanCrud: boolean;
}
const EuiBasicTable: any = _EuiBasicTable;
// @ts-expect-error TS2769
const BasicTable = styled(EuiBasicTable)`
${({ theme }) => `
.euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent {

View file

@ -242,5 +242,7 @@ export const ConfigureCases: React.FC<ConfigureCasesProps> = React.memo((props)
);
});
ConfigureCases.displayName = 'ConfigureCases';
// eslint-disable-next-line import/no-default-export
export default ConfigureCases;

View file

@ -9,8 +9,25 @@ import { i18n } from '@kbn/i18n';
import { CaseConnector, CaseConnectorsRegistry } from './types';
export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const connectors: Map<string, CaseConnector<any>> = new Map();
function assertConnectorExists(
connector: CaseConnector | undefined | null,
id: string
): asserts connector {
if (!connector) {
throw new Error(
i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', {
defaultMessage: 'Object type "{id}" is not registered.',
values: {
id,
},
})
);
}
}
const registry: CaseConnectorsRegistry = {
has: (id: string) => connectors.has(id),
register: <UIProps>(connector: CaseConnector<UIProps>) => {
@ -28,17 +45,9 @@ export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => {
connectors.set(connector.id, connector);
},
get: <UIProps>(id: string): CaseConnector<UIProps> => {
if (!connectors.has(id)) {
throw new Error(
i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', {
defaultMessage: 'Object type "{id}" is not registered.',
values: {
id,
},
})
);
}
return connectors.get(id)!;
const connector = connectors.get(id);
assertConnectorExists(connector, id);
return connector;
},
list: () => {
return Array.from(connectors).map(([id, connector]) => connector);

View file

@ -21,7 +21,7 @@ const DescriptionComponent: React.FC<Props> = ({ isLoading }) => {
useLensDraftComment();
const { setFieldValue } = useFormContext();
const [{ title, tags }] = useFormData({ watch: ['title', 'tags'] });
const editorRef = useRef<Record<string, any>>();
const editorRef = useRef<Record<string, unknown>>();
useEffect(() => {
if (draftComment?.commentId === fieldName && editorRef.current) {

View file

@ -98,5 +98,8 @@ export const CreateCase: React.FC<CreateCaseProps> = React.memo((props) => (
<CreateCaseComponent {...props} />
</OwnerProvider>
));
CreateCase.displayName = 'CreateCase';
// eslint-disable-next-line import/no-default-export
export { CreateCase as default };

View file

@ -96,4 +96,6 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
}
);
MarkdownEditorComponent.displayName = 'MarkdownEditorComponent';
export const MarkdownEditor = memo(MarkdownEditorComponent);

View file

@ -75,3 +75,5 @@ export const MarkdownEditorForm = React.memo(
}
)
);
MarkdownEditorForm.displayName = 'MarkdownEditorForm';

View file

@ -140,7 +140,7 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({
});
lens?.navigateToPrefilledEditor(undefined, {
originatingApp: currentAppId!,
originatingApp: currentAppId,
originatingPath,
});
}, [
@ -174,7 +174,7 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({
}
: undefined,
{
originatingApp: currentAppId!,
originatingApp: currentAppId,
originatingPath,
}
);
@ -310,7 +310,6 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({
if (draftComment) {
handleAdd(incomingEmbeddablePackage?.input.attributes, newTimeRange);
return;
}
}
}, [embeddable, storage, timefilter, currentAppId, handleAdd, handleUpdate, draftComment]);

View file

@ -201,10 +201,10 @@ export class SavedObjectFinderUi extends React.Component<
public render() {
return (
<React.Fragment>
<>
{this.renderSearchBar()}
{this.renderListing()}
</React.Fragment>
</>
);
}
@ -481,16 +481,23 @@ export class SavedObjectFinderUi extends React.Component<
{items.map((item) => {
const currentSavedObjectMetaData = savedObjectMetaData.find(
(metaData) => metaData.type === item.type
)!;
);
if (currentSavedObjectMetaData == null) {
return null;
}
const fullName = currentSavedObjectMetaData.getTooltipForSavedObject
? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject)
: `${item.title} (${currentSavedObjectMetaData!.name})`;
: `${item.title} (${currentSavedObjectMetaData.name})`;
const iconType = (
currentSavedObjectMetaData ||
({
getIconForSavedObject: () => 'document',
} as Pick<SavedObjectMetaData<{ title: string }>, 'getIconForSavedObject'>)
).getIconForSavedObject(item.savedObject);
return (
<EuiListGroupItem
key={item.id}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { some } from 'lodash';
import useDebounce from 'react-use/lib/useDebounce';
import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context';
@ -112,6 +114,7 @@ export const useLensButtonToggle = ({
) {
if (child.type === 'text') break outer; // don't dive into `text` nodes
node = child;
// eslint-disable-next-line no-continue
continue outer;
}
}

View file

@ -16,16 +16,25 @@ interface Props {
disableLinks?: boolean;
}
const withDisabledLinks = (disableLinks?: boolean): React.FC<EuiLinkAnchorProps> => {
const MarkdownLinkProcessingComponent: React.FC<EuiLinkAnchorProps> = memo((props) => (
<MarkdownLink {...props} disableLinks={disableLinks} />
));
MarkdownLinkProcessingComponent.displayName = 'MarkdownLinkProcessingComponent';
return MarkdownLinkProcessingComponent;
};
const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks }) => {
const { processingPlugins, parsingPlugins } = usePlugins();
const MarkdownLinkProcessingComponent: React.FC<EuiLinkAnchorProps> = useMemo(
() => (props) => <MarkdownLink {...props} disableLinks={disableLinks} />,
[disableLinks]
);
// Deep clone of the processing plugins to prevent affecting the markdown editor.
const processingPluginList = cloneDeep(processingPlugins);
// This line of code is TS-compatible and it will break if [1][1] change in the future.
processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent;
processingPluginList[1][1].components.a = useMemo(
() => withDisabledLinks(disableLinks),
[disableLinks]
);
return (
<EuiMarkdownFormat

View file

@ -100,5 +100,7 @@ export const RecentCases: React.FC<RecentCasesProps> = React.memo((props) => {
);
});
RecentCases.displayName = 'RecentCases';
// eslint-disable-next-line import/no-default-export
export { RecentCases as default };

View file

@ -396,6 +396,8 @@ const ActionIcon = React.memo<{
);
});
ActionIcon.displayName = 'ActionIcon';
export const getActionAttachment = ({
comment,
userCanCrud,

View file

@ -25,7 +25,7 @@ import * as i18n from './translations';
import { useUpdateComment } from '../../containers/use_update_comment';
import { useCurrentUser } from '../../common/lib/kibana';
import { AddComment } from '../add_comment';
import { AddComment, AddCommentRefObject } from '../add_comment';
import {
ActionConnector,
ActionsCommentRequestRt,
@ -52,7 +52,7 @@ import {
getActionAttachment,
} from './helpers';
import { UserActionAvatar } from './user_action_avatar';
import { UserActionMarkdown } from './user_action_markdown';
import { UserActionMarkdown, UserActionMarkdownRefObject } from './user_action_markdown';
import { UserActionTimestamp } from './user_action_timestamp';
import { UserActionUsername } from './user_action_username';
import { UserActionContentToolbar } from './user_action_content_toolbar';
@ -131,6 +131,17 @@ const MyEuiCommentList = styled(EuiCommentList)`
const DESCRIPTION_ID = 'description';
const NEW_ID = 'newComment';
const isAddCommentRef = (
ref: AddCommentRefObject | UserActionMarkdownRefObject | null | undefined
): ref is AddCommentRefObject => {
const commentRef = ref as AddCommentRefObject;
if (commentRef?.addQuote != null) {
return true;
}
return false;
};
export const UserActionTree = React.memo(
({
caseServices,
@ -167,7 +178,9 @@ export const UserActionTree = React.memo(
const { isLoadingIds, patchComment } = useUpdateComment();
const currentUser = useCurrentUser();
const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState<string[]>([]);
const commentRefs = useRef<Record<string, any>>({});
const commentRefs = useRef<
Record<string, AddCommentRefObject | UserActionMarkdownRefObject | undefined | null>
>({});
const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } =
useLensDraftComment();
@ -228,8 +241,9 @@ export const UserActionTree = React.memo(
const handleManageQuote = useCallback(
(quote: string) => {
if (commentRefs.current[NEW_ID]) {
commentRefs.current[NEW_ID].addQuote(quote);
const ref = commentRefs?.current[NEW_ID];
if (isAddCommentRef(ref)) {
ref.addQuote(quote);
}
handleOutlineComment('add-comment');
@ -337,6 +351,8 @@ export const UserActionTree = React.memo(
const userActions: EuiCommentProps[] = useMemo(
() =>
caseUserActions.reduce<EuiCommentProps[]>(
// TODO: Decrease complexity. https://github.com/elastic/kibana/issues/115730
// eslint-disable-next-line complexity
(comments, action, index) => {
// Comment creation
if (action.commentId != null && action.action === 'create') {
@ -664,15 +680,12 @@ export const UserActionTree = React.memo(
return prevManageMarkdownEditIds;
});
if (
commentRefs.current &&
commentRefs.current[draftComment.commentId] &&
commentRefs.current[draftComment.commentId].editor?.textarea &&
commentRefs.current[draftComment.commentId].editor?.toolbar
) {
commentRefs.current[draftComment.commentId].setComment(draftComment.comment);
const ref = commentRefs?.current?.[draftComment.commentId];
if (isAddCommentRef(ref) && ref.editor?.textarea) {
ref.setComment(draftComment.comment);
if (hasIncomingLensState) {
openLensModal({ editorRef: commentRefs.current[draftComment.commentId].editor });
openLensModal({ editorRef: ref.editor });
} else {
clearDraftComment();
}

View file

@ -26,7 +26,7 @@ interface UserActionMarkdownProps {
onSaveContent: (content: string) => void;
}
interface UserActionMarkdownRefObject {
export interface UserActionMarkdownRefObject {
setComment: (newComment: string) => void;
}

View file

@ -87,7 +87,7 @@ export const resolveCase = async (
signal: AbortSignal
): Promise<ResolvedCase> => {
const response = await KibanaServices.get().http.fetch<CaseResolveResponse>(
getCaseDetailsUrl(caseId) + '/resolve',
`${getCaseDetailsUrl(caseId)}/resolve`,
{
method: 'GET',
query: {

View file

@ -5,6 +5,8 @@
* 2.0.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable-next-line import/no-extraneous-dependencies
import { mount } from 'enzyme';

View file

@ -231,8 +231,10 @@ export class Authorization {
? Array.from(featureCaseOwners)
: privileges.kibana.reduce<string[]>((authorizedOwners, { authorized, privilege }) => {
if (authorized && requiredPrivileges.has(privilege)) {
const owner = requiredPrivileges.get(privilege)!;
authorizedOwners.push(owner);
const owner = requiredPrivileges.get(privilege);
if (owner) {
authorizedOwners.push(owner);
}
}
return authorizedOwners;

View file

@ -263,7 +263,7 @@ async function getCombinedCase({
id,
}),
]
: [Promise.reject('case connector feature is disabled')]),
: [Promise.reject(new Error('case connector feature is disabled'))]),
]);
if (subCasePromise.status === 'fulfilled') {

View file

@ -8,10 +8,14 @@
import { Boom, isBoom } from '@hapi/boom';
import { Logger } from 'src/core/server';
export interface HTTPError extends Error {
statusCode: number;
}
/**
* Helper class for wrapping errors while preserving the original thrown error.
*/
class CaseError extends Error {
export class CaseError extends Error {
public readonly wrappedError?: Error;
constructor(message?: string, originalError?: Error) {
super(message);
@ -51,6 +55,13 @@ export function isCaseError(error: unknown): error is CaseError {
return error instanceof CaseError;
}
/**
* Type guard for determining if an error is an HTTPError
*/
export function isHTTPError(error: unknown): error is HTTPError {
return (error as HTTPError)?.statusCode != null;
}
/**
* Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError
* if the logger was defined.

View file

@ -83,6 +83,7 @@ const SwimlaneFieldsSchema = schema.object({
const NoneFieldsSchema = schema.nullable(schema.object({}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ReducedConnectorFieldsSchema: { [x: string]: any } = {
[ConnectorTypes.jira]: JiraFieldsSchema,
[ConnectorTypes.resilient]: ResilientFieldsSchema,

View file

@ -8,7 +8,7 @@
import { CaseResponse } from '../../../common';
import { format } from './sir_format';
describe('ITSM formatter', () => {
describe('SIR formatter', () => {
const theCase = {
id: 'case-id',
connector: {

View file

@ -45,12 +45,16 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => {
if (fieldsToAdd.length > 0) {
sirFields = alerts.reduce<Record<SirFieldKey, string[]>>((acc, alert) => {
let temp = {};
fieldsToAdd.forEach((alertField) => {
const field = get(alertFieldMapping[alertField].alertPath, alert);
if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) {
manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field);
acc = {
temp = {
...acc,
...temp,
[alertFieldMapping[alertField].sirFieldKey]: [
...acc[alertFieldMapping[alertField].sirFieldKey],
field,
@ -58,7 +62,8 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => {
};
}
});
return acc;
return { ...acc, ...temp };
}, sirFields);
}

View file

@ -126,6 +126,11 @@ export class CasePlugin {
},
featuresPluginStart: plugins.features,
actionsPluginStart: plugins.actions,
/**
* Lens will be always defined as
* it is declared as required plugin in kibana.json
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
lensEmbeddableFactory: this.lensEmbeddableFactory!,
});

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { wrapError } from './utils';
import { isBoom, boomify } from '@hapi/boom';
import { HTTPError } from '../../common';
import { wrapError } from './utils';
describe('Utils', () => {
describe('wrapError', () => {
@ -25,7 +26,7 @@ describe('Utils', () => {
});
it('it set statusCode to errors status code', () => {
const error = new Error('Something happened') as any;
const error = new Error('Something happened') as HTTPError;
error.statusCode = 404;
const res = wrapError(error);

View file

@ -9,18 +9,21 @@ import { Boom, boomify, isBoom } from '@hapi/boom';
import { schema } from '@kbn/config-schema';
import { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
import { isCaseError } from '../../common';
import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common';
/**
* Transforms an error into the correct format for a kibana response.
*/
export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> {
export function wrapError(
error: CaseError | Boom | HTTPError | Error
): CustomHttpResponseOptions<ResponseError> {
let boom: Boom;
if (isCaseError(error)) {
boom = error.boomify();
} else {
const options = { statusCode: error.statusCode ?? 500 };
const options = { statusCode: isHTTPError(error) ? error.statusCode : 500 };
boom = isBoom(error) ? error : boomify(error, options);
}

View file

@ -4,7 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
/* eslint-disable no-process-exit */
import yargs from 'yargs';
import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient } from '@kbn/test';

View file

@ -48,8 +48,6 @@ function isEmptyAlert(alert: AlertInfo): boolean {
}
export class AlertService {
constructor() {}
public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) {
try {
const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger);