Skip to content

Commit

Permalink
[backend] check playbook filters & add playbook nodes tests (#8721) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Archidoit authored Nov 14, 2024
1 parent 69d4229 commit 28da644
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { SseEvent, StreamDataEvent } from '../types/event';
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 initPlaybookManager = () => {
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) {
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 { AuthContext, AuthUser } from '../../types/user';
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 @@ export const getPlaybookDefinition = async (context: AuthContext, playbook: Basi
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) => {
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
validateFilterGroupForStixMatch(filterGroup);
stringifiedFilters = config.filters;
}
}
return JSON.stringify({ ...config, filters: stringifiedFilters });
};

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

const playbook = await findById(context, user, id);
const definition = JSON.parse(playbook.playbook_definition ?? '{}') as ComponentDefinition;
Expand All @@ -105,7 +117,7 @@ export const playbookAddNode = async (context: AuthContext, user: AuthUser, id:
name: input.name,
position: input.position,
component_id: input.component_id,
configuration: input.configuration ?? '{}' // TODO Check valid json
configuration, // TODO Check valid json
});
const patch: any = { playbook_definition: JSON.stringify(definition) };
if (relatedComponent.is_entry_point) {
Expand Down Expand Up @@ -164,14 +176,7 @@ export const playbookUpdatePositions = async (context: AuthContext, user: AuthUs
};

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);

const playbook = await findById(context, user, id);
const definition = JSON.parse(playbook.playbook_definition) as ComponentDefinition;
Expand Down Expand Up @@ -207,7 +212,7 @@ export const playbookReplaceNode = async (context: AuthContext, user: AuthUser,
name: input.name,
position: input.position,
component_id: input.component_id,
configuration: input.configuration ?? '{}' // TODO Check valid json
configuration, // TODO Check valid json
};
}
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

0 comments on commit 28da644

Please sign in to comment.