Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backend] check playbook filters & add playbook nodes tests (#8721) #8955

Merged
merged 5 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import type { StixBundle } from '../types/stix-common';
import { utcDate } from '../utils/format';
import { findById } from '../modules/playbook/playbook-domain';
import type { CronConfiguration, StreamConfiguration } from '../modules/playbook/playbook-components';
import { type CronConfiguration, PLAYBOOK_INTERNAL_DATA_CRON, type StreamConfiguration } from '../modules/playbook/playbook-components';
import { PLAYBOOK_COMPONENTS } from '../modules/playbook/playbook-components';
import type { BasicStoreEntityPlaybook, ComponentDefinition, PlaybookExecution, PlaybookExecutionStep } from '../modules/playbook/playbook-types';
import { ENTITY_TYPE_PLAYBOOK } from '../modules/playbook/playbook-types';
Expand Down Expand Up @@ -391,7 +391,7 @@
const def = JSON.parse(playbook.playbook_definition) as ComponentDefinition;
// 01. Find the starting point of the playbook
const instance = def.nodes.find((n) => n.id === playbook.playbook_start);
if (instance && instance.component_id === 'PLAYBOOK_INTERNAL_DATA_CRON') {
if (instance && instance.component_id === PLAYBOOK_INTERNAL_DATA_CRON.id) {

Check warning on line 394 in opencti-platform/opencti-graphql/src/manager/playbookManager.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/manager/playbookManager.ts#L394

Added line #L394 was not covered by tests
const connector = PLAYBOOK_COMPONENTS[instance.component_id];
const cronConfiguration = (JSON.parse(instance.configuration ?? '{}') as CronConfiguration);
if (shouldTriggerNow(cronConfiguration, baseDate) && cronConfiguration.filters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const PLAYBOOK_INTERNAL_DATA_CRON_SCHEMA: JSONSchemaType<CronConfiguration> = {
},
required: ['period', 'triggerTime', 'onlyLast', 'filters'],
};
const PLAYBOOK_INTERNAL_DATA_CRON: PlaybookComponent<CronConfiguration> = {
export const PLAYBOOK_INTERNAL_DATA_CRON: PlaybookComponent<CronConfiguration> = {
id: 'PLAYBOOK_INTERNAL_DATA_CRON',
name: 'Query knowledge on a regular basis',
description: 'Query knowledge on the platform',
Expand Down Expand Up @@ -227,7 +227,7 @@ const PLAYBOOK_MATCHING_COMPONENT_SCHEMA: JSONSchemaType<MatchConfiguration> = {
},
required: ['filters'],
};
const PLAYBOOK_MATCHING_COMPONENT: PlaybookComponent<MatchConfiguration> = {
export const PLAYBOOK_MATCHING_COMPONENT: PlaybookComponent<MatchConfiguration> = {
id: 'PLAYBOOK_FILTERING_COMPONENT',
name: 'Match knowledge',
description: 'Match STIX data according to filter (pass if match)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
import type { EditInput, FilterGroup, PlaybookAddInput, PlaybookAddLinkInput, PlaybookAddNodeInput, PositionInput } from '../../generated/graphql';
import type { BasicStoreEntityPlaybook, ComponentDefinition, LinkDefinition, NodeDefinition } from './playbook-types';
import { ENTITY_TYPE_PLAYBOOK } from './playbook-types';
import { PLAYBOOK_COMPONENTS, type SharingConfiguration } from './playbook-components';
import { PLAYBOOK_COMPONENTS, PLAYBOOK_INTERNAL_DATA_CRON, type SharingConfiguration } from './playbook-components';
import { UnsupportedError } from '../../config/errors';
import { type BasicStoreEntityOrganization, ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../organization/organization-types';
import { SYSTEM_USER } from '../../utils/access';
import { validateFilterGroupForStixMatch } from '../../utils/filtering/filtering-stix/stix-filtering';
import { registerConnectorQueues, unregisterConnector } from '../../database/rabbitmq';
import { checkAndConvertFilters } from '../../utils/filtering/filtering-utils';

export const findById: DomainFindById<BasicStoreEntityPlaybook> = (context: AuthContext, user: AuthUser, playbookId: string) => {
return storeLoadById(context, user, playbookId, ENTITY_TYPE_PLAYBOOK);
Expand Down Expand Up @@ -79,15 +80,26 @@
return playbook.playbook_definition;
};

export const playbookAddNode = async (context: AuthContext, user: AuthUser, id: string, input: PlaybookAddNodeInput) => {
// our stix matching is currently limited, we need to validate the input filters
if (input.configuration) {
const config = JSON.parse(input.configuration);
if (config.filters) {
const filterGroup = JSON.parse(config.filters) as FilterGroup;
const checkPlaybookFiltersAndBuildConfigWithCorrectFilters = (input: PlaybookAddNodeInput) => {
Archidoit marked this conversation as resolved.
Show resolved Hide resolved
if (!input.configuration) {
return '{}';
}
let stringifiedFilters;
const config = JSON.parse(input.configuration);
if (config.filters) {
const filterGroup = JSON.parse(config.filters) as FilterGroup;
if (input.component_id === PLAYBOOK_INTERNAL_DATA_CRON.id) {
stringifiedFilters = JSON.stringify(checkAndConvertFilters(filterGroup));
} else { // our stix matching is currently limited, we need to validate the input filters

Check warning on line 93 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L84-L93

Added lines #L84 - L93 were not covered by tests
validateFilterGroupForStixMatch(filterGroup);
stringifiedFilters = config.filters;

Check warning on line 95 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L95

Added line #L95 was not covered by tests
}
}
return JSON.stringify({ ...config, filters: stringifiedFilters });
};

Check warning on line 99 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L98-L99

Added lines #L98 - L99 were not covered by tests

export const playbookAddNode = async (context: AuthContext, user: AuthUser, id: string, input: PlaybookAddNodeInput) => {
const configuration = checkPlaybookFiltersAndBuildConfigWithCorrectFilters(input);

Check warning on line 102 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L102

Added line #L102 was not covered by tests

const playbook = await findById(context, user, id);
const definition = JSON.parse(playbook.playbook_definition ?? '{}') as ComponentDefinition;
Expand All @@ -105,7 +117,7 @@
name: input.name,
position: input.position,
component_id: input.component_id,
configuration: input.configuration ?? '{}' // TODO Check valid json
configuration, // TODO Check valid json

Check warning on line 120 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L120

Added line #L120 was not covered by tests
});
const patch: any = { playbook_definition: JSON.stringify(definition) };
if (relatedComponent.is_entry_point) {
Expand Down Expand Up @@ -164,14 +176,7 @@
};

export const playbookReplaceNode = async (context: AuthContext, user: AuthUser, id: string, nodeId: string, input: PlaybookAddNodeInput) => {
// our stix matching is currently limited, we need to validate the input filters
if (input.configuration) {
const config = JSON.parse(input.configuration);
if (config.filters) {
const filterGroup = JSON.parse(config.filters) as FilterGroup;
validateFilterGroupForStixMatch(filterGroup);
}
}
const configuration = checkPlaybookFiltersAndBuildConfigWithCorrectFilters(input);

Check warning on line 179 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L179

Added line #L179 was not covered by tests

const playbook = await findById(context, user, id);
const definition = JSON.parse(playbook.playbook_definition) as ComponentDefinition;
Expand Down Expand Up @@ -207,7 +212,7 @@
name: input.name,
position: input.position,
component_id: input.component_id,
configuration: input.configuration ?? '{}' // TODO Check valid json
configuration, // TODO Check valid json

Check warning on line 215 in opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/modules/playbook/playbook-domain.ts#L215

Added line #L215 was not covered by tests
};
}
return n;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
import gql from 'graphql-tag';
import { adminQueryWithSuccess } from '../../utils/testQueryHelper';
import { adminQueryWithError, adminQueryWithSuccess } from '../../utils/testQueryHelper';
import type { PlaybookAddNodeInput } from '../../../src/generated/graphql';
import { PLAYBOOK_INTERNAL_DATA_CRON, PLAYBOOK_MATCHING_COMPONENT } from '../../../src/modules/playbook/playbook-components';
import { UNSUPPORTED_ERROR } from '../../../src/config/errors';

const LIST_PLAYBOOKS = gql`
query playbooks(
Expand Down Expand Up @@ -59,6 +62,12 @@ const UPDATE_PLAYBOOK = gql`
}
`;

const ADD_NODE_PLAYBOOK = gql`
mutation playbookAddNode($id: ID!, $input: PlaybookAddNodeInput!) {
playbookAddNode(id: $id, input: $input)
}
`;

const DELETE_PLAYBOOK = gql`
mutation playbookDelete($id: ID!) {
playbookDelete(id:$id)
Expand All @@ -68,6 +77,13 @@ const DELETE_PLAYBOOK = gql`
describe('Playbook resolver standard behavior', () => {
let playbookId = '';
const playbookName = 'Playbook1';
const emptyStringFilters = JSON.stringify({
mode: 'and',
filters: [
{ key: ['entity_type'], values: ['Report'], operator: 'eq' },
],
filterGroups: [],
});
it('should list playbooks', async () => {
const queryResult = await adminQueryWithSuccess({ query: LIST_PLAYBOOKS, variables: { first: 10 } });
expect(queryResult.data?.playbooks.edges.length).toEqual(0);
Expand Down Expand Up @@ -106,6 +122,142 @@ describe('Playbook resolver standard behavior', () => {
});
expect(queryResult.data?.playbookFieldPatch.name).toEqual('Playbook1 - updated');
});
it('should add entry node to a playbook', async () => {
const configuration = {
filters: emptyStringFilters,
};
const addNodeInput: PlaybookAddNodeInput = {
component_id: PLAYBOOK_INTERNAL_DATA_CRON.id,
configuration: JSON.stringify(configuration),
name: 'node1',
position: {
x: 1,
y: 1,
},
};
await adminQueryWithSuccess({
query: ADD_NODE_PLAYBOOK,
variables: {
id: playbookId,
input: addNodeInput,
}
});
const queryResult = await adminQueryWithSuccess({ query: READ_PLAYBOOK, variables: { id: playbookId } });
const playbookNodes = JSON.parse(queryResult.data?.playbook.playbook_definition).nodes;
expect(playbookNodes.length).toEqual(1);
const node1 = playbookNodes[0];
expect(node1.name).toEqual('node1');
expect(node1.position.x).toEqual(1);
expect(JSON.parse(node1.configuration).filters).toEqual(emptyStringFilters);
});
it('should not add several entry nodes to a playbook', async () => {
const configuration = {
filters: emptyStringFilters,
};
const addNodeInput: PlaybookAddNodeInput = {
component_id: PLAYBOOK_INTERNAL_DATA_CRON.id,
configuration: JSON.stringify(configuration),
name: 'node1',
position: {
x: 1,
y: 2,
},
};
await adminQueryWithError(
{
query: ADD_NODE_PLAYBOOK,
variables: {
id: playbookId,
input: addNodeInput,
}
},
'Playbook multiple entrypoint is not supported',
UNSUPPORTED_ERROR
);
});
it('should not add unknown component to a playbook', async () => {
const configuration = {
filters: emptyStringFilters,
};
const addNodeInput: PlaybookAddNodeInput = {
component_id: 'fake_component_id',
configuration: JSON.stringify(configuration),
name: 'node1',
position: {
x: 3,
y: 12,
},
};
await adminQueryWithError(
{
query: ADD_NODE_PLAYBOOK,
variables: {
id: playbookId,
input: addNodeInput,
}
},
'Playbook related component not found',
UNSUPPORTED_ERROR
);
});
it('should not add node with incorrect filters for PLAYBOOK_INTERNAL_DATA_CRON component', async () => {
const incorrectStringFilters = JSON.stringify({
mode: 'and',
filters: [
{ key: ['fake_key'], values: [], operator: 'nil' },
],
filterGroups: [],
});
const configuration = {
filters: incorrectStringFilters,
};
const addNodeInput: PlaybookAddNodeInput = {
component_id: PLAYBOOK_INTERNAL_DATA_CRON.id,
configuration: JSON.stringify(configuration),
name: 'incorrectNode',
position: { x: 1, y: 1 },
};
await adminQueryWithError(
{
query: ADD_NODE_PLAYBOOK,
variables: {
id: playbookId,
input: addNodeInput,
}
},
'incorrect filter keys not existing in any schema definition',
UNSUPPORTED_ERROR
);
});
it('should not add node with incorrect filters for components with stix filtering', async () => {
const incorrectStringFilters = JSON.stringify({
mode: 'and',
filters: [
{ key: ['published'], values: [], operator: 'nil' },
],
filterGroups: [],
});
const configuration = {
filters: incorrectStringFilters,
};
const addNodeInput: PlaybookAddNodeInput = {
component_id: PLAYBOOK_MATCHING_COMPONENT.id,
configuration: JSON.stringify(configuration),
name: 'incorrectNode',
position: { x: 1, y: 1 },
};
await adminQueryWithError(
{
query: ADD_NODE_PLAYBOOK,
variables: {
id: playbookId,
input: addNodeInput,
}
},
'Stix filtering is not compatible with the provided filter key',
UNSUPPORTED_ERROR
);
});
it('should remove playbook', async () => {
const queryResult = await adminQueryWithSuccess({
query: DELETE_PLAYBOOK,
Expand Down
20 changes: 20 additions & 0 deletions opencti-platform/opencti-graphql/tests/utils/testQueryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ export const adminQueryWithSuccess = async (request: { query: any, variables: an
return requestResult;
};

export const adminQueryWithError = async (
request: { query: any, variables: any },
errorMessage?: string,
errorName?: string
) => {
const requestResult = await adminQuery({
query: request.query,
variables: request.variables,
});
expect(requestResult, `Something is wrong with this query: ${request.query}`).toBeDefined();
expect(requestResult.errors.length).toEqual(1);
if (errorMessage) {
expect(requestResult.errors[0].message, `error message: ${errorMessage} is expected, but got ${requestResult.errors[0].message}`).toBe(errorMessage);
}
if (errorName) {
expect(requestResult.errors[0].extensions.code, `error is expected but got ${requestResult.errors[0].name}`).toBe(errorName);
}
return requestResult;
};

/**
* Execute the query as some User, and verify success and return query result.
* @param client
Expand Down