Skip to content

Commit

Permalink
[backend/frontend] Add TAXII push endpoints security (#8932)
Browse files Browse the repository at this point in the history
  • Loading branch information
richard-julien committed Jan 6, 2025
1 parent 89882a9 commit 7fef3ef
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import TextField from '../../../../components/TextField';
import CreatorField from '../../common/form/CreatorField';
import { fieldSpacingContainerStyle } from '../../../../utils/field';
import { insertNode } from '../../../../utils/store';
import ObjectMembersField from '../../common/form/ObjectMembersField';
import SwitchField from '../../../../components/fields/SwitchField';

const styles = (theme) => ({
Expand All @@ -38,16 +39,22 @@ const ingestionTaxiiCollectionCreationValidation = (t) => Yup.object().shape({
description: Yup.string().nullable(),
user_id: Yup.object().nullable(),
confidence_to_score: Yup.bool().nullable(),
authorized_members: Yup.array().required().min(1),
});

const IngestionTaxiiCollectionCreation = (props) => {
const { t, classes } = props;
const onSubmit = (values, { setSubmitting, resetForm }) => {
const authorized_members = values.authorized_members.map(({ value }) => ({
id: value,
access_right: 'view',
}));
const input = {
name: values.name,
description: values.description,
confidence_to_score: values.confidence_to_score,
user_id: values.user_id?.value,
authorized_members,
};
commitMutation({
mutation: IngestionTaxiiCollectionCreationMutation,
Expand Down Expand Up @@ -84,7 +91,7 @@ const IngestionTaxiiCollectionCreation = (props) => {
onSubmit={onSubmit}
onReset={onClose}
>
{({ submitForm, handleReset, isSubmitting }) => (
{({ submitForm, handleReset, setFieldValue, isSubmitting }) => (
<Form>
<Field
component={TextField}
Expand All @@ -107,6 +114,13 @@ const IngestionTaxiiCollectionCreation = (props) => {
containerStyle={fieldSpacingContainerStyle}
showConfidence
/>
<ObjectMembersField
label={'Accessible for'}
style={fieldSpacingContainerStyle}
onChange={setFieldValue}
multiple={true}
name="authorized_members"
/>
<Field
component={SwitchField}
type="checkbox"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { createFragmentContainer, graphql } from 'react-relay';
import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup';
import * as R from 'ramda';
import ObjectMembersField from '../../common/form/ObjectMembersField';
import inject18n from '../../../../components/i18n';
import { commitMutation } from '../../../../relay/environment';
import TextField from '../../../../components/TextField';
import { fieldSpacingContainerStyle } from '../../../../utils/field';
import CreatorField from '../../common/form/CreatorField';
import { convertUser } from '../../../../utils/edition';
import { convertAuthorizedMembers, convertUser } from '../../../../utils/edition';
import Drawer from '../../common/drawer/Drawer';
import SwitchField from '../../../../components/fields/SwitchField';

Expand All @@ -29,6 +30,7 @@ const ingestionTaxiiCollectionValidation = (t) => Yup.object().shape({
description: Yup.string().nullable(),
user_id: Yup.mixed().nullable(),
confidence_to_score: Yup.bool().nullable(),
authorized_members: Yup.array().required().min(1),
});

const IngestionTaxiiCollectionEditionContainer = ({
Expand All @@ -55,15 +57,30 @@ const IngestionTaxiiCollectionEditionContainer = ({
})
.catch(() => false);
};

const handleSubmitFieldOptions = (name, value) => ingestionTaxiiCollectionValidation(t)
.validateAt(name, { [name]: value })
.then(() => {
commitMutation({
mutation: ingestionTaxiiCollectionMutationFieldPatch,
variables: {
id: ingestionTaxiiCollection?.id,
input: { key: name, value: value?.map(({ value: v }) => v) ?? '' },
},
});
}).catch(() => false);
const initialValues = R.pipe(
R.assoc('user_id', convertUser(ingestionTaxiiCollection, 'user')),
R.assoc('authorized_members', convertAuthorizedMembers(ingestionTaxiiCollection)),
R.pick([
'name',
'description',
'user_id',
'authorized_members',
'confidence_to_score',
]),
)(ingestionTaxiiCollection);

return (
<Drawer
title={t('Update a TAXII Push ingester')}
Expand Down Expand Up @@ -101,6 +118,13 @@ const IngestionTaxiiCollectionEditionContainer = ({
containerStyle={fieldSpacingContainerStyle}
showConfidence
/>
<ObjectMembersField
label={'Accessible for'}
style={fieldSpacingContainerStyle}
onChange={handleSubmitFieldOptions}
multiple={true}
name="authorized_members"
/>
<Field
component={SwitchField}
onChange={handleSubmitField}
Expand Down Expand Up @@ -138,6 +162,10 @@ const IngestionTaxiiCollectionEditionFragment = createFragmentContainer(
entity_type
name
}
authorized_members {
id
name
}
}
`,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11561,6 +11561,7 @@ type IngestionTaxiiCollection implements InternalObject & BasicObject {
description: String
user_id: String
user: Creator
authorized_members: [MemberAccess!]
}

enum IngestionTaxiiCollectionOrdering {
Expand All @@ -11585,6 +11586,7 @@ input IngestionTaxiiCollectionAddInput {
description: String
user_id: String
confidence_to_score: Boolean
authorized_members: [MemberAccessInput!]!
}

type IngestionCsv implements InternalObject & BasicObject {
Expand Down
1 change: 1 addition & 0 deletions opencti-platform/opencti-graphql/graphql-codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ generates:
ThreatActorIndividual: ../modules/threatActorIndividual/threatActorIndividual-types#BasicStoreEntityThreatActorIndividual
IngestionRss: ../modules/ingestion/ingestion-types#BasicStoreEntityIngestionRss
IngestionTaxii: ../modules/ingestion/ingestion-types#BasicStoreEntityIngestionTaxii
IngestionTaxiiCollection: ../modules/ingestion/ingestion-types#BasicStoreEntityIngestionTaxiiCollection
Indicator: ../modules/indicator/indicator-types#BasicStoreEntityIndicator
IngestionCsv: ../modules/ingestion/ingestion-types#BasicStoreEntityIngestionCsv
DecayRule: ../modules/decayRule/decayRule-types#BasicStoreEntityDecayRule
Expand Down
2 changes: 1 addition & 1 deletion opencti-platform/opencti-graphql/src/config/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GraphQLError } from 'graphql/index';
const CATEGORY_TECHNICAL = 'TECHNICAL';
const CATEGORY_BUSINESS = 'BUSINESS';

const error = (type, message, data) => {
export const error = (type, message, data) => {
return new GraphQLError(message, { extensions: { code: type, data } });
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ export const internalLoadById = async <T extends BasicStoreBase>(
return await elLoadById(context, user, id, opts) as unknown as T;
};

export const storeLoadById = async <T extends BasicStoreCommon>(context: AuthContext, user: AuthUser, id: string, type: string, opts = {}): Promise<T> => {
export const storeLoadById = async <T extends BasicStoreCommon>(context: AuthContext, user: AuthUser, id: string, type: string | string[], opts = {}): Promise<T> => {
if (R.isNil(type) || R.isEmpty(type)) {
throw FunctionalError('You need to specify a type when loading a element');
}
Expand Down
30 changes: 12 additions & 18 deletions opencti-platform/opencti-graphql/src/domain/taxii.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
import * as R from 'ramda';
import { Promise } from 'bluebird';
import { elIndex, elPaginate } from '../database/engine';
import { INDEX_INTERNAL_OBJECTS, isNotEmptyField, READ_INDEX_INTERNAL_OBJECTS, READ_STIX_DATA_WITH_INFERRED, READ_STIX_INDICES } from '../database/utils';
import { INDEX_INTERNAL_OBJECTS, isNotEmptyField, READ_STIX_DATA_WITH_INFERRED, READ_STIX_INDICES } from '../database/utils';
import { generateInternalId, generateStandardId } from '../schema/identifier';
import { ENTITY_TYPE_TAXII_COLLECTION } from '../schema/internalObject';
import { deleteElementById, stixLoadByIds, updateAttribute } from '../database/middleware';
import { listEntities, storeLoadById } from '../database/middleware-loader';
import { ForbiddenAccess, FunctionalError } from '../config/errors';
import { listAllEntities, listEntities, storeLoadById } from '../database/middleware-loader';
import { FunctionalError } from '../config/errors';
import { delEditContext, notify, setEditContext } from '../database/redis';
import conf, { BUS_TOPICS } from '../config/conf';
import { addFilter } from '../utils/filtering/filtering-utils';
import { convertFiltersToQueryOptions } from '../utils/filtering/filtering-resolution';
import { publishUserAction } from '../listener/UserActionListener';
import { MEMBER_ACCESS_RIGHT_VIEW, SYSTEM_USER, TAXIIAPI_SETCOLLECTIONS } from '../utils/access';
import { STIX_EXT_OCTI } from '../types/stix-extensions';
import { ENTITY_TYPE_INGESTION_TAXII_COLLECTION } from '../modules/ingestion/ingestion-types';

const MAX_TAXII_PAGINATION = conf.get('app:data_sharing:taxii:max_pagination_result') || 500;
const STIX_MEDIA_TYPE = 'application/stix+json;version=2.1';
Expand Down Expand Up @@ -42,7 +43,7 @@ export const createTaxiiCollection = async (context, user, input) => {
return data;
};
export const findById = async (context, user, collectionId) => {
return storeLoadById(context, user, collectionId, ENTITY_TYPE_TAXII_COLLECTION);
return storeLoadById(context, user, collectionId, [ENTITY_TYPE_TAXII_COLLECTION, ENTITY_TYPE_INGESTION_TAXII_COLLECTION]);
};
export const findAll = (context, user, args) => {
if (user) {
Expand Down Expand Up @@ -168,22 +169,15 @@ export const restBuildCollection = (collection) => {
id: collection.id,
title: collection.name,
description: collection.description,
can_read: true,
can_write: false,
can_read: collection.entity_type === ENTITY_TYPE_TAXII_COLLECTION,
can_write: collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION,
media_types: [STIX_MEDIA_TYPE],
};
};
export const getCollectionById = async (context, user, collectionId) => {
const collection = await storeLoadById(context, user, collectionId, ENTITY_TYPE_TAXII_COLLECTION);
if (!collection) {
throw ForbiddenAccess();
}
return collection;
};
export const restAllCollections = async (context, user) => {
const collections = await elPaginate(context, user, READ_INDEX_INTERNAL_OBJECTS, {
types: [ENTITY_TYPE_TAXII_COLLECTION],
connectionFormat: false,
});
return collections.map((c) => restBuildCollection(c));
const opts = { connectionFormat: false };
const collections = await listAllEntities(context, user, [ENTITY_TYPE_TAXII_COLLECTION, ENTITY_TYPE_INGESTION_TAXII_COLLECTION], opts);
return collections
.filter((c) => !(c.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION && c.ingestion_running === false))
.map((c) => restBuildCollection(c));
};
21 changes: 12 additions & 9 deletions opencti-platform/opencti-graphql/src/generated/graphql.ts

Large diffs are not rendered by default.

52 changes: 40 additions & 12 deletions opencti-platform/opencti-graphql/src/http/httpTaxii.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@

import * as R from 'ramda';
import { v4 as uuidv4 } from 'uuid';
import nconf from 'nconf';
import express from 'express';
import { authenticateUserFromRequest, TAXIIAPI } from '../domain/user';
import { findById as findWorkById } from '../domain/work';
import { basePath, getBaseUrl } from '../config/conf';
import { AuthRequired, ForbiddenAccess, UnsupportedError } from '../config/errors';
import { AuthRequired, error, ForbiddenAccess, UNSUPPORTED_ERROR, UnsupportedError } from '../config/errors';
import { STIX_EXT_OCTI } from '../types/stix-extensions';
import { findById, restAllCollections, restBuildCollection, restCollectionManifest, restCollectionStix, getCollectionById } from '../domain/taxii';
import { findById, restAllCollections, restBuildCollection, restCollectionManifest, restCollectionStix } from '../domain/taxii';
import { BYPASS, executionContext, SYSTEM_USER } from '../utils/access';
import { findById as findTaxiiCollection } from '../modules/ingestion/ingestion-taxii-collection-domain';
import { handleConfidenceToScoreTransformation, pushBundleToConnectorQueue } from '../manager/ingestionManager';
import { now } from '../utils/format';
import { computeWorkStatus } from '../domain/connector';
import { ENTITY_TYPE_INGESTION_TAXII_COLLECTION } from '../modules/ingestion/ingestion-types';

const TAXII_VERSION = 'application/taxii+json;version=2.1';

const TaxiiError = (message, code) => {
return error(UNSUPPORTED_ERROR, message, { http_status: code });
};
const sendJsonResponse = (res, data) => {
res.setHeader('content-type', TAXII_VERSION);
res.json(data);
};

const errorConverter = (e) => {
const details = R.pipe(R.dissoc('reason'), R.dissoc('http_status'))(e.data);
return {
title: e.message,
error_code: e.name,
description: e.data?.reason,
http_status: e.data?.http_status || 500,
details,
error_code: e.extensions.code,
http_status: e.extensions.data?.http_status || 500,
};
};
const userHaveAccess = (user) => {
Expand Down Expand Up @@ -66,10 +69,20 @@ const extractUserAndCollection = async (context, req, res, id) => {
return { user: SYSTEM_USER, collection: findCollection };
}
const authUser = await extractUserFromRequest(context, req, res);
const userCollection = await getCollectionById(context, authUser, id);
const userCollection = await findById(context, authUser, id);
if (!userCollection) {
throw TaxiiError('Collection not found', 404);
}
return { user: authUser, collection: userCollection };
};

const JsonTaxiiMiddleware = express.json({
type: (req) => {
return req.headers['content-type'] === TAXII_VERSION;
},
limit: nconf.get('app:max_payload_body_size') || '50mb'
});

const initTaxiiApi = (app) => {
// Discovery api
app.get(`${basePath}/taxii2`, async (req, res) => {
Expand Down Expand Up @@ -122,6 +135,9 @@ const initTaxiiApi = (app) => {
try {
const context = executionContext('taxii');
const { collection } = await extractUserAndCollection(context, req, res, id);
if (collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION && collection.ingestion_running !== true) {
throw TaxiiError('Collection not found', 404);
}
sendJsonResponse(res, restBuildCollection(collection));
} catch (e) {
const errorDetail = errorConverter(e);
Expand All @@ -133,6 +149,9 @@ const initTaxiiApi = (app) => {
try {
const context = executionContext('taxii');
const { user, collection } = await extractUserAndCollection(context, req, res, id);
if (collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION) {
throw TaxiiError('The client does not have access to this manifest resource', 403);
}
const manifest = await restCollectionManifest(context, user, collection, req.query);
if (manifest.objects.length > 0) {
res.set('X-TAXII-Date-Added-First', R.head(manifest.objects)?.version);
Expand All @@ -149,6 +168,9 @@ const initTaxiiApi = (app) => {
try {
const context = executionContext('taxii');
const { user, collection } = await extractUserAndCollection(context, req, res, id);
if (collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION) {
throw TaxiiError('The client does not have access to this objects resource', 403);
}
const stix = await restCollectionStix(context, user, collection, req.query);
if (stix.objects.length > 0) {
res.set('X-TAXII-Date-Added-First', getUpdatedAt(R.head(stix.objects)));
Expand All @@ -165,6 +187,9 @@ const initTaxiiApi = (app) => {
try {
const context = executionContext('taxii');
const { user, collection } = await extractUserAndCollection(context, req, res, id);
if (collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION) {
throw TaxiiError('The client does not have access to this objects resource', 403);
}
const args = rebuildParamsForObject(object_id, req);
const stix = await restCollectionStix(context, user, collection, args);
if (stix.objects.length > 0) {
Expand All @@ -182,6 +207,9 @@ const initTaxiiApi = (app) => {
try {
const context = executionContext('taxii');
const { user, collection } = await extractUserAndCollection(context, req, res, id);
if (collection.entity_type === ENTITY_TYPE_INGESTION_TAXII_COLLECTION) {
throw TaxiiError('The client does not have access to this objects resource', 403);
}
const args = rebuildParamsForObject(object_id, req);
const stix = await restCollectionStix(context, user, collection, args);
const data = R.head(stix.objects);
Expand All @@ -195,7 +223,7 @@ const initTaxiiApi = (app) => {
res.status(errorDetail.http_status).send(errorDetail);
}
});
app.post(`${basePath}/taxii2/root/collections/:id/objects`, async (req, res) => {
app.post(`${basePath}/taxii2/root/collections/:id/objects`, JsonTaxiiMiddleware, async (req, res) => {
const { id } = req.params;
const { objects = [] } = req.body;
try {
Expand All @@ -207,10 +235,10 @@ const initTaxiiApi = (app) => {
// Find and validate the collection
const ingestion = await findTaxiiCollection(context, user, id);
if (!ingestion) {
throw UnsupportedError('Ingestion not found');
throw TaxiiError('Collection not found', 404);
}
if (ingestion.ingestion_running !== true) {
throw UnsupportedError('Ingestion is not running');
throw TaxiiError('Collection not found', 404);
}
const stixObjects = handleConfidenceToScoreTransformation(ingestion, objects);
// Push the bundle in queue, return the job id
Expand Down Expand Up @@ -260,7 +288,7 @@ const initTaxiiApi = (app) => {
}
});
// Unsupported api (delete)
app.delete(`${basePath}/taxii2/root/collections/:id/objects/:object_id`, async (req, res) => {
app.delete(`${basePath}/taxii2/root/collections/:id/objects/:object_id`, async (_req, res) => {
const e = UnsupportedError('Unsupported operation');
const errorDetail = errorConverter(e);
res.status(errorDetail.http_status).send(errorDetail);
Expand Down
Loading

0 comments on commit 7fef3ef

Please sign in to comment.