diff --git a/INSTALL/mysql.sql b/INSTALL/mysql.sql index 9f888dc..0d52940 100644 --- a/INSTALL/mysql.sql +++ b/INSTALL/mysql.sql @@ -186,6 +186,21 @@ CREATE TABLE `individuals` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `local_tools` +-- + +DROP TABLE IF EXISTS `local_tools`; +CREATE TABLE `local_tools` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `connector` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `settings` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `connector` (`connector`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- -- Table structure for table `organisation_encryption_keys` -- @@ -326,12 +341,17 @@ CREATE TABLE `meta_fields` ( `field` varchar(191) NOT NULL, `value` varchar(191) NOT NULL, `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, + `meta_template_id` int(10) unsigned NOT NULL, + `meta_template_field_id` int(10) unsigned NOT NULL, + `is_default` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `scope` (`scope`), KEY `uuid` (`uuid`), KEY `parent_id` (`parent_id`), KEY `field` (`field`), - KEY `value` (`value`) + KEY `value` (`value`), + KEY `meta_template_id` (`meta_template_id`), + KEY `meta_template_field_id` (`meta_template_field_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `meta_templates` ( diff --git a/README.md b/README.md index f08efb8..a00ccc6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ The installation is documented at the following location [INSTALL/INSTALL.md](IN Hardware requirements: -A webserver with 4GB of memory and a single CPU core should be plenty for the current scope of Cerebrate. This might increase over the time with additional features being added, but the goal is to keep Cerebrate as lean as possible. +A webserver with 4GB of memory and a single CPU core should be plenty for the current scope of Cerebrate. This might increase over the time with additional features being added, but the goal is to keep Cerebrate as lean as possible. Expect to have at least 40GB of disk space, depending on your log rotation strategy you might want to go higher. + +For installation via docker, refer to the [cerebrate-docker](https://github.com/cerebrate-project/cerebrate-docker) repo. # License diff --git a/src/Application.php b/src/Application.php index 90e3865..8eb1704 100644 --- a/src/Application.php +++ b/src/Application.php @@ -135,7 +135,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid $service->loadAuthenticator('Authentication.Session'); $service->loadAuthenticator('Authentication.Form', [ 'fields' => $fields, - 'loginUrl' => '/users/login' + 'loginUrl' => \Cake\Routing\Router::url('/users/login') ]); // Load identifiers diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index a1ad233..70116d9 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -99,7 +99,7 @@ class AppController extends Controller if (!empty($user['disabled'])) { $this->Authentication->logout(); $this->Flash->error(__('The user account is disabled.')); - return $this->redirect(['controller' => 'Users', 'action' => 'login']); + return $this->redirect(\Cake\Routing\Router::url('/users/login')); } unset($user['password']); $this->ACL->setUser($user); @@ -118,11 +118,6 @@ class AppController extends Controller $this->set('ajax', $this->request->is('ajax')); $this->request->getParam('prefix'); $this->set('darkMode', !empty(Configure::read('Cerebrate.dark'))); - if (!empty(Configure::read('baseurl'))) { - Configure::write('App.fullBaseUrl', Configure::read('baseurl')); - } else if (!empty(env('CEREBRATE_BASEURL'))) { - Configure::write('App.fullBaseUrl', env('CEREBRATE_BASEURL')); - } $this->set('baseurl', Configure::read('App.fullBaseUrl')); } diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index dcd7f31..eec94ce 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -694,6 +694,34 @@ class ACLComponent extends Component 'popup' => 1 ] ] + ], + 'LocalTools' => [ + 'label' => __('Local Tools'), + 'url' => '/localTools/index', + 'children' => [ + 'index' => [ + 'url' => '/localTools/index', + 'label' => __('List Connectors') + ], + 'viewConnector' => [ + 'url' => '/localTools/viewConnector/{{connector}}', + 'label' => __('View Connector'), + 'actions' => ['view'], + 'skipTopMenu' => 1 + ], + 'add' => [ + 'url' => '/localTools/add/{{connector}}', + 'label' => __('Add connection'), + 'actions' => ['viewConnector'], + 'skipTopMenu' => 1 + ], + 'view' => [ + 'url' => '/localTools/view/{{id}}', + 'label' => __('View Connection'), + 'actions' => ['view'], + 'skipTopMenu' => 1 + ] + ] ] ], 'Cerebrate' => [ diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 5b08801..31de16c 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -38,17 +38,37 @@ class CRUDComponent extends Component } $params = $this->Controller->ParamHandler->harvestParams($optionFilters); $query = $this->Table->find(); + if (!empty($options['filterFunction'])) { + $query = $options['filterFunction']($query); + } $query = $this->setFilters($params, $query, $options); $query = $this->setQuickFilters($params, $query, empty($options['quickFilters']) ? [] : $options['quickFilters']); if (!empty($options['contain'])) { $query->contain($options['contain']); } + if (!empty($options['fields'])) { + $query->select($options['fields']); + } if ($this->Controller->ParamHandler->isRest()) { $data = $query->all(); + if (isset($options['afterFind'])) { + if (is_callable($options['afterFind'])) { + $data = $options['afterFind']($data); + } else { + $data = $this->Table->{$options['afterFind']}($data); + } + } $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); } else { $this->Controller->loadComponent('Paginator'); $data = $this->Controller->Paginator->paginate($query); + if (isset($options['afterFind'])) { + if (is_callable($options['afterFind'])) { + $data = $options['afterFind']($data); + } else { + $data = $this->Table->{$options['afterFind']}($data); + } + } if (!empty($options['contextFilters'])) { $this->setFilteringContext($options['contextFilters'], $params); } @@ -63,7 +83,7 @@ class CRUDComponent extends Component $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/filters'); } - + /** * getResponsePayload Returns the adaquate response payload based on the request context * @@ -309,6 +329,9 @@ class CRUDComponent extends Component $data = $this->Table->get($id, $params); $data = $this->attachMetaData($id, $data); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data); + } if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); } diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php new file mode 100644 index 0000000..44d9f4e --- /dev/null +++ b/src/Controller/LocalToolsController.php @@ -0,0 +1,214 @@ +LocalTools->extractMeta($this->LocalTools->getConnectors(), true); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($data, 'json'); + } + $data = $this->CustomPagination->paginate($data); + $this->set('data', $data); + if ($this->request->is('ajax')) { + $this->viewBuilder()->disableAutoLayout(); + } + $this->set('metaGroup', 'Administration'); + } + + public function connectorIndex() + { + $this->set('metaGroup', 'Admin'); + $this->CRUD->index([ + 'filters' => ['name', 'connector'], + 'quickFilters' => ['name', 'connector'], + 'afterFind' => function($data) { + foreach ($data as $connector) { + $connector['health'] = [$this->LocalTools->healthCheckIndividual($connector)]; + } + return $data; + } + ]); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('metaGroup', 'Administration'); + } + + public function action($connectionId, $actionName) + { + $connection = $this->LocalTools->query()->where(['id' => $connectionId])->first(); + if (empty($connection)) { + throw new NotFoundException(__('Invalid connector.')); + } + $params = $this->ParamHandler->harvestParams($this->LocalTools->getActionFilterOptions($connection->connector, $actionName)); + $actionDetails = $this->LocalTools->getActionDetails($actionName); + $params['connection'] = $connection; + $results = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request); + if (!empty($results['redirect'])) { + $this->redirect($results['redirect']); + } + if (!empty($results['restResponse'])) { + return $results['restResponse']; + } + if ($this->ParamHandler->isRest()) { + return $results['data']['data']; + } + $this->set('data', $results); + $this->set('metaGroup', 'Administration'); + if ($actionDetails['type'] === 'formAction') { + if ($this->request->is(['post', 'put'])) { + if ($this->ParamHandler->isAjax()) { + if (!empty($results['success'])) { + return $this->RestResponse->ajaxSuccessResponse( + 'LocalTools', + 'action', + $connection, + empty($results['message']) ? __('Success.') : $results['message'] + ); + } else { + return $this->RestResponse->ajaxSuccessResponse( + 'LocalTools', + 'action', + false, + empty($results['message']) ? __('Success.') : $results['message'] + //['displayOnSuccess' => $displayOnSuccess] + ); + } + } else { + if (!empty($results['success'])) { + $this->Flash->success(empty($results['message']) ? __('Success.') : $results['message']); + $this->redirect(['controller' => 'localTools', 'action' => 'action', $connectionId, $actionDetails['redirect']]); + } else { + $this->Flash->error(empty($results['message']) ? __('Could not execute the requested action.') : $results['message']); + $this->redirect(['controller' => 'localTools', 'action' => 'action', $connectionId, $actionDetails['redirect']]); + } + } + } else { + $this->render('/Common/getForm'); + } + } else { + $this->render('/Common/' . $actionDetails['type']); + } + } + + public function add($connector = false) + { + $this->CRUD->add(); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $dropdownData = ['connectors' => []]; + foreach ($connectors as $connector) { + $dropdownData['connectors'][$connector['connector']] = $connector['name']; + } + $this->set(compact('dropdownData')); + $this->set('metaGroup', 'Administration'); + } + + public function viewConnector($connector_name) + { + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $connector = false; + foreach ($connectors as $c) { + if ($connector === false || version_compare($c['version'], $connectors['version']) > 0) { + $connector = $c; + } + } + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($connector, 'json'); + } + $this->set('entity', $connector); + $this->set('metaGroup', 'Administration'); + } + + public function edit($id) + { + $this->CRUD->edit($id); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + if ($this->ParamHandler->isAjax() && !empty($this->ajaxResponsePayload)) { + return $this->ajaxResponsePayload; + } + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $dropdownData = ['connectors' => []]; + foreach ($connectors as $connector) { + $dropdownData['connectors'][$connector['connector']] = $connector['name']; + } + $this->set(compact('dropdownData')); + $this->set('metaGroup', 'Administration'); + $this->render('add'); + } + + public function delete($id) + { + $this->CRUD->delete($id); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('metaGroup', 'Administration'); + } + + public function view($id) + { + $localTools = $this->LocalTools; + $this->CRUD->view($id, [ + 'afterFind' => function ($data) use($id, $localTools) { + $data['children'] = $localTools->getChildParameters($id); + return $data; + } + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $this->set('metaGroup', 'Administration'); + } + + public function exposedTools() + { + $this->CRUD->index([ + 'filters' => ['name', 'connector'], + 'quickFilters' => ['name', 'connector'], + 'fields' => ['id', 'name', 'connector', 'description'], + 'filterFunction' => function($query) { + $query->where(['exposed' => 1]); + return $query; + }, + 'afterFind' => function($data) { + foreach ($data as $connector) { + $connector = [ + 'id' => $connector['id'], + 'name' => $connector['name'], + 'connector' => $connector['connector'] + ]; + } + return $data; + } + ]); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('metaGroup', 'Administration'); + } + + public function broodTools($id) + { + $this->loadModel('Broods'); + $tools = $this->Broods->queryLocalTools($id); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($tools, 'json'); + } + $this->set('data', $tools); + $this->set('metaGroup', 'Administration'); + } +} diff --git a/src/Controller/Open/IndividualsController.php b/src/Controller/Open/IndividualsController.php new file mode 100644 index 0000000..097489b --- /dev/null +++ b/src/Controller/Open/IndividualsController.php @@ -0,0 +1,35 @@ +Authentication->allowUnauthenticated(['index']); + } + + public function index() + { + $this->CRUD->index([ + 'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id'], + 'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'], + 'contain' => ['Alignments' => 'Organisations'] + ]); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('alignmentScope', 'organisations'); + $this->set('metaGroup', 'Public'); + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 03ed6ad..8a40151 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -132,7 +132,7 @@ class UsersController extends AppController if ($result->isValid()) { $this->Authentication->logout(); $this->Flash->success(__('Goodbye.')); - return $this->redirect(['controller' => 'Users', 'action' => 'login']); + return $this->redirect(\Cake\Routing\Router::url('/users/login')); } } diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php new file mode 100644 index 0000000..c4ab2ef --- /dev/null +++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php @@ -0,0 +1,53 @@ +exposedFunctions[] = $functionName; + } + + public function runAction($action, $params) { + if (!in_array($action, $exposedFunctions)) { + throw new MethodNotAllowedException(__('Invalid connector function called.')); + } + return $this->{$action}($params); + } + + public function health(Object $connection): array + { + return 0; + } + + public function captureOrganisation($input): bool + { + if (empty($input['uuid'])) { + return false; + } + $organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations'); + $organisations->captureOrg($input); + return true; + } + + public function captureSharingGroup($input): bool + { + if (empty($input['uuid'])) { + return false; + } + $sharing_groups = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups'); + $sharing_groups->captureSharingGroup($input); + return true; + } +} + +?> diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php new file mode 100644 index 0000000..1acd685 --- /dev/null +++ b/src/Lib/default/local_tool_connectors/MispConnector.php @@ -0,0 +1,548 @@ + [ + 'type' => 'index', + 'scope' => 'child', + 'params' => [ + 'quickFilter', + 'sort', + 'direction', + 'page', + 'limit' + ] + ], + 'organisationsAction' => [ + 'type' => 'index', + 'scope' => 'child', + 'params' => [ + 'quickFilter', + 'limit', + 'page', + 'sort', + 'direction' + ] + ], + 'sharingGroupsAction' => [ + 'type' => 'index', + 'scope' => 'child', + 'params' => [ + 'quickFilter', + 'limit', + 'page', + 'sort', + 'direction' + ] + ], + 'fetchOrganisationAction' => [ + 'type' => 'formAction', + 'scope' => 'childAction', + 'params' => [ + 'uuid' + ], + 'redirect' => 'organisationsAction' + ], + 'fetchSharingGroupAction' => [ + 'type' => 'formAction', + 'scope' => 'childAction', + 'params' => [ + 'uuid' + ], + 'redirect' => 'sharingGroupsAction' + ], + 'modifySettingAction' => [ + 'type' => 'formAction', + 'scope' => 'childAction', + 'params' => [ + 'setting', + 'value' + ], + 'redirect' => 'serverSettingsAction' + ] + ]; + public $version = '0.1'; + + public function addExposedFunction(string $functionName): void + { + $this->exposedFunctions[] = $functionName; + } + + public function getExposedFunction(string $functionName): array + { + if (!empty($this->exposedFunctions[$functionName])) { + return $exposedFunctions[$functionName]; + } else { + throw new NotFoundException(__('Invalid action requested.')); + } + } + + public function health(Object $connection): array + { + $settings = json_decode($connection->settings, true); + $http = new Client(); + $response = $http->post($settings['url'] . '/users/view/me.json', '{}', [ + 'headers' => [ + 'AUTHORIZATION' => $settings['authkey'], + 'Accept' => 'Application/json', + 'Content-type' => 'Application/json' + ] + ]); + $responseCode = $response->getStatusCode(); + if ($response->isOk()) { + $status = 1; + $message = __('OK'); + } else if ($responseCode == 403){ + $status = 3; + $message = __('Unauthorized'); + } else { + $status = 0; + $message = __('Something went wrong.'); + } + + return [ + 'status' => $status, + 'message' => $message + ]; + } + + private function getData(string $url, array $params): Response + { + if (empty($params['connection'])) { + throw new NotFoundException(__('No connection object received.')); + } + $settings = json_decode($params['connection']->settings, true); + $http = new Client(); + if (!empty($params['sort'])) { + $list = explode('.', $params['sort']); + $params['sort'] = end($list); + } + if (!isset($params['limit'])) { + $params['limit'] = 50; + } + $url = $this->urlAppendParams($url, $params); + $response = $http->get($settings['url'] . $url, false, [ + 'headers' => [ + 'AUTHORIZATION' => $settings['authkey'], + 'Accept' => 'application/json', + 'Content-type' => 'application/json' + ] + ]); + if ($response->isOk()) { + return $response; + } else { + throw new NotFoundException(__('Could not retrieve the requested resource.')); + } + } + + private function postData(string $url, array $params): Response + { + if (empty($params['connection'])) { + throw new NotFoundException(__('No connection object received.')); + } + $settings = json_decode($params['connection']->settings, true); + $http = new Client(); + $url = $this->urlAppendParams($url, $params); + $response = $http->post($settings['url'] . $url, json_encode($params['body']), [ + 'headers' => [ + 'AUTHORIZATION' => $settings['authkey'], + 'Accept' => 'application/json' + ], + 'type' => 'json' + ]); + if ($response->isOk()) { + return $response; + } else { + throw new NotFoundException(__('Could not post to the requested resource.')); + } + } + + public function urlAppendParams(string $url, array $params): string + { + if (!isset($params['validParams'])) { + $validParams = [ + 'quickFilter' => 'searchall', + 'sort' => 'sort', + 'page' => 'page', + 'direction' => 'direction', + 'limit' => 'limit' + ]; + } else { + $validParams = $params['validParams']; + } + foreach ($validParams as $param => $remoteParam) { + if (!empty($params[$param])) { + $url .= sprintf('/%s:%s', $remoteParam, $params[$param]); + } + } + return $url; + } + + + public function diagnosticsAction(array $params): array + { + + } + + public function serverSettingsAction(array $params): array + { + $params['validParams'] = [ + 'limit' => 'limit', + 'page' => 'page', + 'quickFilter' => 'searchall' + ]; + $urlParams = h($params['connection']['id']) . '/serverSettingsAction'; + $response = $this->getData('/servers/serverSettings', $params); + $data = $response->getJson(); + if (!empty($data['finalSettings'])) { + $finalSettings = [ + 'type' => 'index', + 'data' => [ + 'data' => $data['finalSettings'], + 'skip_pagination' => 1, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'additionalUrlParams' => $urlParams + ] + ] + ], + 'fields' => [ + [ + 'name' => 'Setting', + 'sort' => 'setting', + 'data_path' => 'setting', + ], + [ + 'name' => 'Criticality', + 'sort' => 'level', + 'data_path' => 'level', + 'arrayData' => [ + 0 => 'Critical', + 1 => 'Recommended', + 2 => 'Optional' + ], + 'element' => 'array_lookup_field' + ], + [ + 'name' => __('Value'), + 'sort' => 'value', + 'data_path' => 'value', + ], + [ + 'name' => __('Type'), + 'sort' => 'type', + 'data_path' => 'type', + ], + [ + 'name' => __('Error message'), + 'sort' => 'errorMessage', + 'data_path' => 'errorMessage', + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}', + 'modal_params_data_path' => ['setting'], + 'icon' => 'download', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction' + ] + ] + ] + ]; + if (!empty($params['quickFilter'])) { + $needle = strtolower($params['quickFilter']); + foreach ($finalSettings['data']['data'] as $k => $v) { + if (strpos(strtolower($v['setting']), $needle) === false) { + unset($finalSettings['data']['data'][$k]); + } + } + $finalSettings['data']['data'] = array_values($finalSettings['data']['data']); + } + return $finalSettings; + } else { + return []; + } + } + + public function organisationsAction(array $params): array + { + $params['validParams'] = [ + 'limit' => 'limit', + 'page' => 'page', + 'quickFilter' => 'searchall' + ]; + $urlParams = h($params['connection']['id']) . '/organisationsAction'; + $response = $this->getData('/organisations/index', $params); + $data = $response->getJson(); + if (!empty($data)) { + return [ + 'type' => 'index', + 'data' => [ + 'data' => $data, + 'skip_pagination' => 1, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'additionalUrlParams' => $urlParams + ] + ] + ], + 'fields' => [ + [ + 'name' => 'Name', + 'sort' => 'Organisation.name', + 'data_path' => 'Organisation.name', + ], + [ + 'name' => 'uuid', + 'sort' => 'Organisation.uuid', + 'data_path' => 'Organisation.uuid' + ], + [ + 'name' => 'nationality', + 'sort' => 'Organisation.nationality', + 'data_path' => 'Organisation.nationality' + ], + [ + 'name' => 'sector', + 'sort' => 'Organisation.sector', + 'data_path' => 'Organisation.sector' + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchOrganisationAction?uuid={{0}}', + 'modal_params_data_path' => ['Organisation.uuid'], + 'icon' => 'download', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction' + ] + ] + ] + ]; + } else { + return []; + } + } + + public function sharingGroupsAction(array $params): array + { + $params['validParams'] = [ + 'limit' => 'limit', + 'page' => 'page', + 'quickFilter' => 'searchall' + ]; + $urlParams = h($params['connection']['id']) . '/sharingGroupsAction'; + $response = $this->getData('/sharing_groups/index', $params); + $data = $response->getJson(); + if (!empty($data)) { + return [ + 'type' => 'index', + 'data' => [ + 'data' => $data['response'], + 'skip_pagination' => 1, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'additionalUrlParams' => $urlParams + ] + ] + ], + 'fields' => [ + [ + 'name' => 'Name', + 'sort' => 'SharingGroup.name', + 'data_path' => 'SharingGroup.name', + ], + [ + 'name' => 'uuid', + 'sort' => 'SharingGroup.uuid', + 'data_path' => 'SharingGroup.uuid' + ], + [ + 'name' => 'Organisations', + 'sort' => 'Organisation', + 'data_path' => 'Organisation', + 'element' => 'count_summary' + ], + [ + 'name' => 'Roaming', + 'sort' => 'SharingGroup.roaming', + 'data_path' => 'SharingGroup.roaming', + 'element' => 'boolean' + ], + [ + 'name' => 'External servers', + 'sort' => 'Server', + 'data_path' => 'Server', + 'element' => 'count_summary' + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSharingGroupAction?uuid={{0}}', + 'modal_params_data_path' => ['SharingGroup.uuid'], + 'icon' => 'download', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/SharingGroupsAction' + ] + ] + ] + ]; + } else { + return []; + } + } + + public function fetchOrganisationAction(array $params): array + { + if ($params['request']->is(['get'])) { + return [ + 'data' => [ + 'title' => __('Fetch organisation'), + 'description' => __('Fetch and create/update organisation ({0}) from MISP.', $params['uuid']), + 'submit' => [ + 'action' => $params['request']->getParam('action') + ], + 'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchOrganisationAction', $params['uuid']] + ] + ]; + } elseif ($params['request']->is(['post'])) { + $response = $this->getData('/organisations/view/' . $params['uuid'], $params); + if ($response->getStatusCode() == 200) { + $result = $this->captureOrganisation($response->getJson()['Organisation']); + if ($result) { + return ['success' => 1, 'message' => __('Organisation created/modified.')]; + } else { + return ['success' => 0, 'message' => __('Could not save the changes to the organisation.')]; + } + } else { + return ['success' => 0, 'message' => __('Could not fetch the remote organisation.')]; + } + } + throw new MethodNotAllowedException(__('Invalid http request type for the given action.')); + } + + public function fetchSharingGroupAction(array $params): array + { + if ($params['request']->is(['get'])) { + return [ + 'data' => [ + 'title' => __('Fetch sharing group'), + 'description' => __('Fetch and create/update sharing group ({0}) from MISP.', $params['uuid']), + 'submit' => [ + 'action' => $params['request']->getParam('action') + ], + 'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchSharingGroupAction', $params['uuid']] + ] + ]; + } elseif ($params['request']->is(['post'])) { + $response = $this->getData('/sharing_groups/view/' . $params['uuid'], $params); + if ($response->getStatusCode() == 200) { + $mispSG = $response->getJson(); + $sg = [ + 'uuid' => $mispSG['SharingGroup']['uuid'], + 'name' => $mispSG['SharingGroup']['name'], + 'releasability' => $mispSG['SharingGroup']['releasability'], + 'description' => $mispSG['SharingGroup']['description'], + 'organisation' => $mispSG['Organisation'], + 'sharing_group_orgs' => [] + ]; + foreach ($mispSG['SharingGroupOrg'] as $sgo) { + $sg['sharing_group_orgs'][] = $sgo['Organisation']; + } + $result = $this->captureSharingGroup($sg, $params['user_id']); + if ($result) { + return ['success' => 1, 'message' => __('Sharing group created/modified.')]; + } else { + return ['success' => 0, 'message' => __('Could not save the changes to the sharing group.')]; + } + } else { + return ['success' => 0, 'message' => __('Could not fetch the remote sharing group.')]; + } + } + throw new MethodNotAllowedException(__('Invalid http request type for the given action.')); + } + + public function modifySettingAction(array $params): array + { + if ($params['request']->is(['get'])) { + $response = $this->getData('/servers/getSetting/' . $params['setting'], $params); + if ($response->getStatusCode() != 200) { + throw new NotFoundException(__('Setting could not be fetched from the remote.')); + } + $response = $response->getJson(); + $types = [ + 'string' => 'text', + 'boolean' => 'checkbox', + 'numeric' => 'number' + ]; + $fields = [ + [ + 'field' => 'value', + 'label' => __('Value'), + 'default' => h($response['value']), + 'type' => $types[$response['type']] + ], + ]; + return [ + 'data' => [ + 'title' => __('Modify server setting'), + 'description' => __('Modify setting ({0}) on connected MISP instance.', $params['setting']), + 'fields' => $fields, + 'submit' => [ + 'action' => $params['request']->getParam('action') + ], + 'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'modifySettingAction', $params['setting']] + ] + ]; + } elseif ($params['request']->is(['post'])) { + $params['body'] = ['value' => $params['value']]; + $response = $this->postData('/servers/serverSettingsEdit/' . $params['setting'], $params); + if ($response->getStatusCode() == 200) { + return ['success' => 1, 'message' => __('Setting saved.')]; + } else { + return ['success' => 0, 'message' => __('Could not fetch the remote sharing group.')]; + } + } + throw new MethodNotAllowedException(__('Invalid http request type for the given action.')); + + } +} + + ?> diff --git a/src/Model/Behavior/SyncTool.php b/src/Model/Behavior/SyncTool.php new file mode 100644 index 0000000..ad3f756 --- /dev/null +++ b/src/Model/Behavior/SyncTool.php @@ -0,0 +1,172 @@ +createHttpSocket($params); + } + + public function setupHttpSocketFeed($feed = null) + { + return $this->setupHttpSocket(); + } + + /** + * @param array $params + * @return HttpSocket + * @throws Exception + */ + public function createHttpSocket($params = array()) + { + App::uses('HttpSocket', 'Network/Http'); + $HttpSocket = new HttpSocket($params); + $proxy = Configure::read('Proxy'); + if (empty($params['skip_proxy']) && isset($proxy['host']) && !empty($proxy['host'])) { + $HttpSocket->configProxy($proxy['host'], $proxy['port'], $proxy['method'], $proxy['user'], $proxy['password']); + } + return $HttpSocket; + } + + /** + * @param array $server + * @return array|void + * @throws Exception + */ + public static function getServerClientCertificateInfo(array $server) + { + if (!$server['client_cert_file']) { + return; + } + + $clientCertificate = new File(APP . "files" . DS . "certs" . DS . $server['id'] . '_client.pem'); + if (!$clientCertificate->exists()) { + throw new Exception("Certificate file '{$clientCertificate->pwd()}' doesn't exists."); + } + + $certificateContent = $clientCertificate->read(); + if ($certificateContent === false) { + throw new Exception("Could not read '{$clientCertificate->pwd()}' file with client certificate."); + } + + return self::getClientCertificateInfo($certificateContent); + } + + /** + * @param string $certificateContent PEM encoded certificate and private key. + * @return array + * @throws Exception + */ + private static function getClientCertificateInfo($certificateContent) + { + $certificate = openssl_x509_read($certificateContent); + if (!$certificate) { + throw new Exception("Could't parse certificate: " . openssl_error_string()); + } + $privateKey = openssl_pkey_get_private($certificateContent); + if (!$privateKey) { + throw new Exception("Could't get private key from certificate: " . openssl_error_string()); + } + $verify = openssl_x509_check_private_key($certificate, $privateKey); + if (!$verify) { + throw new Exception('Public and private key do not match.'); + } + return self::parseCertificate($certificate); + } + + /** + * @param mixed $certificate + * @return array + * @throws Exception + */ + private static function parseCertificate($certificate) + { + $parsed = openssl_x509_parse($certificate); + if (!$parsed) { + throw new Exception("Could't get parse X.509 certificate: " . openssl_error_string()); + } + $currentTime = new DateTime(); + $output = [ + 'serial_number' => $parsed['serialNumberHex'], + 'signature_type' => $parsed['signatureTypeSN'], + 'valid_from' => isset($parsed['validFrom_time_t']) ? new DateTime("@{$parsed['validFrom_time_t']}") : null, + 'valid_to' => isset($parsed['validTo_time_t']) ? new DateTime("@{$parsed['validTo_time_t']}") : null, + 'public_key_size' => null, + 'public_key_type' => null, + 'public_key_size_ok' => null, + ]; + + $output['valid_from_ok'] = $output['valid_from'] ? ($output['valid_from'] <= $currentTime) : null; + $output['valid_to_ok'] = $output['valid_to'] ? ($output['valid_to'] >= $currentTime) : null; + + $subject = []; + foreach ($parsed['subject'] as $type => $value) { + $subject[] = "$type=$value"; + } + $output['subject'] = implode(', ', $subject); + + $issuer = []; + foreach ($parsed['issuer'] as $type => $value) { + $issuer[] = "$type=$value"; + } + $output['issuer'] = implode(', ', $issuer); + + $publicKey = openssl_pkey_get_public($certificate); + if ($publicKey) { + $publicKeyDetails = openssl_pkey_get_details($publicKey); + if ($publicKeyDetails) { + $output['public_key_size'] = $publicKeyDetails['bits']; + switch ($publicKeyDetails['type']) { + case OPENSSL_KEYTYPE_RSA: + $output['public_key_type'] = 'RSA'; + $output['public_key_size_ok'] = $output['public_key_size'] >= 2048; + break; + case OPENSSL_KEYTYPE_DSA: + $output['public_key_type'] = 'DSA'; + $output['public_key_size_ok'] = $output['public_key_size'] >= 2048; + break; + case OPENSSL_KEYTYPE_DH: + $output['public_key_type'] = 'DH'; + break; + case OPENSSL_KEYTYPE_EC: + $output['public_key_type'] = "EC ({$publicKeyDetails['ec']['curve_name']})"; + $output['public_key_size_ok'] = $output['public_key_size'] >= 224; + break; + } + } + } + + return $output; + } +} diff --git a/src/Model/Entity/LocalTool.php b/src/Model/Entity/LocalTool.php new file mode 100644 index 0000000..f996d77 --- /dev/null +++ b/src/Model/Entity/LocalTool.php @@ -0,0 +1,11 @@ +find(); + $brood = $query->where(['id' => $brood_id])->first(); + if (empty($brood)) { + throw new NotFoundException(__('Brood not found')); + } + $http = new Client(); + $response = $http->get($brood['url'] . '/localTools/exposedTools' , [], [ + 'headers' => [ + 'Authorization' => $brood['authkey'] + ], + 'type' => 'json' + ]); + if ($response->isOk()) { + return $response->getJson(); + } else { + return false; + } + } } diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php new file mode 100644 index 0000000..5deec9d --- /dev/null +++ b/src/Model/Table/LocalToolsTable.php @@ -0,0 +1,169 @@ + 'UNKNOWN', + 1 => 'OK', + 2 => 'ISSUES', + 3 => 'ERROR', + ]; + + public $exposedFunctions = []; + + private $currentConnector = null; + + private $connectors = null; + + public function initialize(array $config): void + { + parent::initialize($config); + } + + public function validationDefault(Validator $validator): Validator + { + return $validator; + } + + public function loadConnector(string $connectorName): void + { + if (empty($currentConnector) || get_class($currentConnector) !== $connectorName) { + $connectors = $this->getConnectors($connectorName); + if (empty($connectors[$connectorName])) { + throw new NotFoundException(__('Invalid connector module requested.')); + } else { + $this->currentConnector = $connectors[$connectorName]; + } + } + } + + public function action(int $user_id, string $connectorName, string $actionName, array $params, \Cake\Http\ServerRequest $request): array + { + $this->loadConnector($connectorName); + $params['request'] = $request; + $params['user_id'] = $user_id; + return $this->currentConnector->{$actionName}($params); + } + + public function getActionDetails(string $actionName): array + { + if (!empty($this->currentConnector->exposedFunctions[$actionName])) { + return $this->currentConnector->exposedFunctions[$actionName]; + } + throw new NotFoundException(__('Invalid connector module action requested.')); + } + + public function getActionFilterOptions(string $connectorName, string $actionName): array + { + $this->loadConnector($connectorName); + if (!empty($this->currentConnector->exposedFunctions[$actionName])) { + return $this->currentConnector->exposedFunctions[$actionName]['params'] ?? []; + } + throw new NotFoundException(__('Invalid connector module action requested.')); + } + + public function getConnectors(string $name = null): array + { + $connectors = []; + $dirs = [ + ROOT . '/src/Lib/default/local_tool_connectors', + ROOT . '/src/Lib/custom/local_tool_connectors' + ]; + foreach ($dirs as $dir) { + $dir = new Folder($dir); + $files = $dir->find('.*Connector\.php'); + foreach ($files as $file) { + require_once($dir->pwd() . '/'. $file); + $className = substr($file, 0, -4); + $classNamespace = '\\' . $className . '\\' . $className; + if (empty($name) || $name === $className) { + $connectors[$className] = new $classNamespace; + } + } + } + return $connectors; + } + + public function extractMeta(array $connector_classes, bool $includeConnections = false): array + { + $connectors = []; + foreach ($connector_classes as $connector_type => $connector_class) { + $connector = [ + 'name' => $connector_class->name, + 'connector' => $connector_type, + 'connector_version' => $connector_class->version, + 'connector_description' => $connector_class->description + ]; + if ($includeConnections) { + $connector['connections'] = $this->healthCheck($connector_type, $connector_class); + } + $connectors[] = $connector; + } + return $connectors; + } + + public function healthCheck(string $connector_type, Object $connector_class): array + { + $query = $this->find(); + $query->where([ + 'connector' => $connector_type + ]); + $connections = $query->all()->toList(); + foreach ($connections as &$connection) { + $connection = $this->healthCheckIndividual($connection); + } + return $connections; + } + + public function healthCheckIndividual(Object $connection): array + { + $connector_class = $this->getConnectors($connection->connector); + if (empty($connector_class[$connection->connector])) { + return []; + } + $connector_class = $connector_class[$connection->connector]; + $health = $connector_class->health($connection); + return $connection = [ + 'name' => $connection->name, + 'health' => $health['status'], + 'message' => $health['message'], + 'url' => '/localTools/view/' . $connection['id'] + ]; + } + + public function getConnectorByConnectionId($id): array + { + $connection = $this->find()->where(['id' => $id])->first(); + if (empty($connection)) { + throw new NotFoundException(__('Invalid connection.')); + } + return $this->getConnectors($connection->connector); + } + + public function getChildParameters($id): array + { + $connectors = $this->getConnectorByConnectionId($id); + if (empty($connectors)) { + throw new NotFoundException(__('Invalid connector.')); + } + $connector = array_values($connectors)[0]; + $children = []; + foreach ($connector->exposedFunctions as $functionName => $function) { + if ($function['type'] === 'index') { + $children[] = $functionName; + } + } + return $children; + } +} diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php index 5b6fd66..41cf20c 100644 --- a/src/Model/Table/SharingGroupsTable.php +++ b/src/Model/Table/SharingGroupsTable.php @@ -43,4 +43,55 @@ class SharingGroupsTable extends AppTable { return $rules; } + + public function captureSharingGroup($input, int $user_id = 0): ?int + { + if (!empty($input['id'])) { + unset($input['id']); + } + if (!empty($input['uuid'])) { + $existingSG = $this->find()->where([ + 'uuid' => $input['uuid'] + ])->first(); + } else { + return null; + } + if (empty($existingSG)) { + $data = $this->newEmptyEntity(); + $input['organisation_id'] = $this->Organisations->captureOrg($input['organisation']); + $input['user_id'] = $user_id; + $data = $this->patchEntity($data, $input, ['associated' => []]); + if (!$this->save($data)) { + return null; + } + $savedSG = $data; + } else { + $reserved = ['id', 'uuid', 'metaFields']; + foreach ($input as $field => $value) { + if (in_array($field, $reserved)) { + continue; + } + $existingSG->$field = $value; + } + if (!$this->save($existingSG)) { + return null; + } + $savedSG = $existingSG; + } + $this->postCaptureActions($savedSG->id, $input); + return $savedSG->id; + } + + public function postCaptureActions($id, $input): void + { + $sharingGroup = $this->find()->where([ + 'id' => $id + ])->first(); + $orgs = []; + foreach ($input['sharing_group_orgs'] as $sgo) { + $organisation_id = $this->Organisations->captureOrg($sgo); + $orgs[] = $this->SharingGroupOrgs->get($organisation_id); + } + $this->SharingGroupOrgs->link($sharingGroup, $orgs); + } } diff --git a/src/View/AppView.php b/src/View/AppView.php index e4b82b6..87b775b 100644 --- a/src/View/AppView.php +++ b/src/View/AppView.php @@ -40,6 +40,7 @@ class AppView extends View { parent::initialize(); $this->loadHelper('Hash'); + $this->loadHelper('PrettyPrint'); $this->loadHelper('FormFieldMassage'); $this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']); } diff --git a/src/View/Helper/PrettyPrintHelper.php b/src/View/Helper/PrettyPrintHelper.php new file mode 100644 index 0000000..c288cb3 --- /dev/null +++ b/src/View/Helper/PrettyPrintHelper.php @@ -0,0 +1,29 @@ + $value) { + if (is_array($value)) { + $value = $this->ppArray($value, $depth+1); + } else { + $value = h($value); + } + $text .= sprintf( + '
%s: %s
', + h($key), + $value + ); + } + return $text; + } +} + + +?> diff --git a/templates/Broods/index.php b/templates/Broods/index.php index 8e873d4..43d454a 100644 --- a/templates/Broods/index.php +++ b/templates/Broods/index.php @@ -66,8 +66,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'url' => '/broods/view', 'url_params_data_paths' => ['id'], + 'title' => __('View details'), 'icon' => 'eye' ], + [ + 'url' => '/localTools/broodTools', + 'url_params_data_paths' => ['id'], + 'title' => __('List available local tools'), + 'icon' => 'wrench' + ], [ 'open_modal' => '/broods/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', diff --git a/templates/Common/getForm.php b/templates/Common/getForm.php new file mode 100644 index 0000000..e147f14 --- /dev/null +++ b/templates/Common/getForm.php @@ -0,0 +1,2 @@ +element('genericElements/Form/genericForm', $data); diff --git a/templates/Common/index.php b/templates/Common/index.php new file mode 100644 index 0000000..1f0a5f6 --- /dev/null +++ b/templates/Common/index.php @@ -0,0 +1,4 @@ +element('genericElements/IndexTable/index_table', $data); + echo ''; +?> diff --git a/templates/LocalTools/add.php b/templates/LocalTools/add.php new file mode 100644 index 0000000..702e659 --- /dev/null +++ b/templates/LocalTools/add.php @@ -0,0 +1,35 @@ +element('genericElements/Form/genericForm', [ + 'data' => [ + 'description' => __('Add connections to local tools via any of the available connectors below.'), + 'model' => 'LocalTools', + 'fields' => [ + [ + 'field' => 'name' + ], + [ + 'field' => 'connector', + 'options' => $dropdownData['connectors'], + 'type' => 'dropdown' + ], + [ + 'field' => 'exposed', + 'type' => 'checkbox' + ], + [ + 'field' => 'settings', + 'type' => 'textarea' + ], + [ + 'field' => 'description', + 'type' => 'textarea' + ], + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ], + 'url' => empty($redirect) ? null : $redirect + ] + ]); +?> + diff --git a/templates/LocalTools/brood_tools.php b/templates/LocalTools/brood_tools.php new file mode 100644 index 0000000..ae41159 --- /dev/null +++ b/templates/LocalTools/brood_tools.php @@ -0,0 +1,49 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => __('Id'), + 'data_path' => 'id', + ], + [ + 'name' => __('Name'), + 'data_path' => 'name', + ], + [ + 'name' => 'Connector', + 'data_path' => 'connector' + ], + [ + 'name' => __('Description'), + 'data_path' => 'description', + ] + ], + 'title' => __('Local tools made available by the remote Cerebrate'), + 'description' => __('Cerebrate can connect to local tools via individual connectors and administrators can choose to expose a subset of their tools to other members of their Cerebrate in order for their peers to be able to issue interconnection requests. '), + 'pull' => 'right', + 'skip_pagination' => 1, + 'actions' => [ + [ + 'url' => '/localTools/connectionRequest', + 'url_params_data_paths' => ['id'], + 'title' => 'Issue a connection request', + 'icon' => 'plug' + ] + ] + ] +]); +echo ''; +?> diff --git a/templates/LocalTools/connector_index.php b/templates/LocalTools/connector_index.php new file mode 100644 index 0000000..8a2bf4e --- /dev/null +++ b/templates/LocalTools/connector_index.php @@ -0,0 +1,77 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'id', + ], + [ + 'name' => __('Name'), + 'sort' => 'name', + 'data_path' => 'name', + ], + [ + 'name' => __('Connector'), + 'sort' => 'connector', + 'data_path' => 'connector', + ], + [ + 'name' => 'Exposed', + 'data_path' => 'exposed', + 'element' => 'boolean' + ], + [ + 'name' => 'settings', + 'data_path' => 'settings', + 'isJson' => 1, + 'element' => 'array' + ], + [ + 'name' => 'description', + 'data_path' => 'description' + ], + [ + 'name' => 'health', + 'data_path' => 'health', + 'element' => 'health', + 'class' => 'text-nowrap' + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'url' => '/localTools/view', + 'url_params_data_paths' => ['id'], + 'icon' => 'eye' + ], + [ + 'open_modal' => '/localTools/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'edit' + ], + [ + 'open_modal' => '/localTools/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'trash' + ], + ] + ] +]); +echo ''; +?> diff --git a/templates/LocalTools/index.php b/templates/LocalTools/index.php new file mode 100644 index 0000000..91903a2 --- /dev/null +++ b/templates/LocalTools/index.php @@ -0,0 +1,54 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => __('Name'), + 'sort' => 'name', + 'data_path' => 'name', + ], + [ + 'name' => 'Connector', + 'data_path' => 'connector' + ], + [ + 'name' => 'Version', + 'data_path' => 'connector_version' + ], + [ + 'name' => 'Description', + 'data_path' => 'connector_description' + ], + [ + 'name' => 'Connections', + 'data_path' => 'connections', + 'element' => 'health', + 'class' => 'text-nowrap' + ] + ], + 'title' => __('Local tool connector index'), + 'description' => __('Cerebrate can connect to local tools via individual connectors, built to expose the various functionalities of the given tool via Cerebrate. Simply view the connectors\' details and the accompanying instance list to manage the connections using the given connector. '), + 'pull' => 'right', + 'actions' => [ + [ + 'url' => '/localTools/viewConnector', + 'url_params_data_paths' => ['connector'], + 'icon' => 'eye' + ] + ] + ] +]); +echo ''; +?> diff --git a/templates/LocalTools/view.php b/templates/LocalTools/view.php new file mode 100644 index 0000000..7a523f9 --- /dev/null +++ b/templates/LocalTools/view.php @@ -0,0 +1,51 @@ + '/LocalTools/action/{{0}}/' . $child, + 'url_params' => ['id'], + 'title' => \Cake\Utility\Inflector::humanize(substr($child, 0, -6)) + ]; + } + } + echo $this->element( + '/genericElements/SingleViews/single_view', + [ + 'data' => $entity, + 'title' => sprintf( + '%s control panel using %s', + h($entity->name), + h($entity->connector) + ), + 'fields' => [ + [ + 'key' => __('ID'), + 'path' => 'id' + ], + [ + 'key' => __('Name'), + 'path' => 'name' + ], + [ + 'key' => __('Connector'), + 'path' => 'connector' + ], + [ + 'key' => __('Exposed'), + 'path' => 'exposed', + 'type' => 'boolean' + ], + [ + 'key' => __('settings'), + 'path' => 'settings', + 'type' => 'json' + ], + [ + 'key' => __('Description'), + 'path' => 'description' + ] + ], + 'children' => $children + ] + ); diff --git a/templates/LocalTools/view_connector.php b/templates/LocalTools/view_connector.php new file mode 100644 index 0000000..d6c4665 --- /dev/null +++ b/templates/LocalTools/view_connector.php @@ -0,0 +1,33 @@ +element( + '/genericElements/SingleViews/single_view', + [ + 'title' => __('{0} connector view', $entity['name']), + 'data' => $entity, + 'fields' => [ + [ + 'key' => __('Name'), + 'path' => 'name' + ], + [ + 'key' => __('Connector name'), + 'path' => 'connector' + ], + [ + 'key' => __('version'), + 'path' => 'connector_version' + ], + [ + 'key' => __('Description'), + 'path' => 'connector_description' + ] + ], + 'children' => [ + [ + 'url' => '/localTools/connectorIndex/', + 'url_params' => ['connector'], + 'title' => __('Connections') + ] + ] + ] +); diff --git a/templates/element/genericElements/Configuration/Fields.php/scaffold.php b/templates/element/genericElements/Configuration/Fields.php/scaffold.php new file mode 100644 index 0000000..6a9187d --- /dev/null +++ b/templates/element/genericElements/Configuration/Fields.php/scaffold.php @@ -0,0 +1,4 @@ +element('genericElements/Configuration/Fields/' . $type . 'Field.php', ['data' => $field]); diff --git a/templates/element/genericElements/Configuration/scaffold.php b/templates/element/genericElements/Configuration/scaffold.php new file mode 100644 index 0000000..ea424fc --- /dev/null +++ b/templates/element/genericElements/Configuration/scaffold.php @@ -0,0 +1,33 @@ +'; +} +$fields = ''; +if (!empty($data['fields'])) { + foreach ($data['fields'] as $field) { + $fields .= $this->element('genericElements/Configuration/Fields/scaffold.php', ['data' => $field]); + } +} +echo sprintf( + '

%s

%s
<%s/div>
', + empty($ajax) ? 'col-8' : '', + h($data['title']), + $diagnostics, + $fields +); +?> diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php index 52f02f9..e0d275b 100644 --- a/templates/element/genericElements/IndexTable/Fields/actions.php +++ b/templates/element/genericElements/IndexTable/Fields/actions.php @@ -35,7 +35,7 @@ } } $url_param_data_paths = ''; - $url = empty($action['url']) ? '#' : h($action['url']); + $url = empty($action['url']) ? '#' : $baseurl . h($action['url']); if (!empty($action['url_params_data_paths'])) { if (is_array($action['url_params_data_paths'])) { $temp = array(); @@ -81,17 +81,26 @@ ); } else if (!empty($action['open_modal']) && !empty($action['modal_params_data_path'])) { - $modal_url = str_replace( - '[onclick_params_data_path]', - h(Cake\Utility\Hash::extract($row, $action['modal_params_data_path'])[0]), - $action['open_modal'] - ); + if (is_array($action['modal_params_data_path'])) { + foreach ($action['modal_params_data_path'] as $k => $v) { + $modal_url = str_replace( + sprintf('{{%s}}', $k), + h(Cake\Utility\Hash::extract($row, $v)[0]), + $action['open_modal'] + ); + } + } else { + $modal_url = str_replace( + '[onclick_params_data_path]', + h(Cake\Utility\Hash::extract($row, $action['modal_params_data_path'])[0]), + $action['open_modal'] + ); + } $reload_url = !empty($action['reload_url']) ? $action['reload_url'] : $this->Url->build(['action' => 'index']); $action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue); } echo sprintf( - ' ', - $baseurl, + ' ', $url, empty($action['title']) ? '' : h($action['title']), empty($action['title']) ? '' : h($action['title']), diff --git a/templates/element/genericElements/IndexTable/Fields/array.php b/templates/element/genericElements/IndexTable/Fields/array.php new file mode 100644 index 0000000..b1cc9bf --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/array.php @@ -0,0 +1,7 @@ +Hash->extract($row, $field['data_path']);; + if (!empty($field['isJson'])) { + $data = json_decode($data[0], true); + } + echo $this->PrettyPrint->ppArray($data); +?> diff --git a/templates/element/genericElements/IndexTable/Fields/array_lookup_field.php b/templates/element/genericElements/IndexTable/Fields/array_lookup_field.php index 9f929f2..d361ef1 100644 --- a/templates/element/genericElements/IndexTable/Fields/array_lookup_field.php +++ b/templates/element/genericElements/IndexTable/Fields/array_lookup_field.php @@ -1,4 +1,6 @@ Hash->extract($row, $field['data_path'])[0]]); ?> diff --git a/templates/element/genericElements/IndexTable/Fields/generic_field.php b/templates/element/genericElements/IndexTable/Fields/generic_field.php index edff114..2ef0980 100644 --- a/templates/element/genericElements/IndexTable/Fields/generic_field.php +++ b/templates/element/genericElements/IndexTable/Fields/generic_field.php @@ -16,7 +16,6 @@ '', $data ? 'check' : 'times' ); - $data = ''; } else { $data = h($data); if (!empty($field['privacy'])) { diff --git a/templates/element/genericElements/IndexTable/Fields/health.php b/templates/element/genericElements/IndexTable/Fields/health.php new file mode 100644 index 0000000..ad14ac4 --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/health.php @@ -0,0 +1,28 @@ +Hash->extract($row, $field['data_path']); + $lines = ''; + $status_colours = [ + 0 => 'secondary', + 1 => 'success', + 2 => 'warning', + 3 => 'danger' + ]; + foreach ($data as $healthElement) { + $name = h($healthElement['name']); + if (!empty($healthElement['url'])) { + $name = sprintf( + '%s', + $baseurl, + $healthElement['url'], + $name + ); + } + $lines .= sprintf( + '

%s: %s

', + $status_colours[$healthElement['health']], + $name, + h($healthElement['message']) + ); + } + echo $lines; +?> diff --git a/templates/element/genericElements/SingleViews/Fields/genericField.php b/templates/element/genericElements/SingleViews/Fields/genericField.php index 3c54be3..cba9ff6 100644 --- a/templates/element/genericElements/SingleViews/Fields/genericField.php +++ b/templates/element/genericElements/SingleViews/Fields/genericField.php @@ -2,6 +2,7 @@ if (isset($field['raw'])) { $string = $field['raw']; } else { + $value = 1; $value = Cake\Utility\Hash::extract($data, $field['path']); $string = empty($value[0]) ? '' : $value[0]; } diff --git a/templates/element/genericElements/SingleViews/Fields/jsonField.php b/templates/element/genericElements/SingleViews/Fields/jsonField.php index 9e6a687..9c8b16b 100644 --- a/templates/element/genericElements/SingleViews/Fields/jsonField.php +++ b/templates/element/genericElements/SingleViews/Fields/jsonField.php @@ -1,3 +1,19 @@ %s', h(json_encode($value, JSON_PRETTY_PRINT))); + $randomId = Cake\Utility\Security::randomString(8); + if (isset($field['raw'])) { + $string = $field['raw']; + } else { + $value = Cake\Utility\Hash::extract($data, $field['path']); + $string = empty($value[0]) ? '' : $value[0]; + } + echo sprintf( + '
', + h($randomId) + ); +?> + + diff --git a/templates/element/genericElements/header_scaffold.php b/templates/element/genericElements/header_scaffold.php index 1fe6a2f..ac999d8 100644 --- a/templates/element/genericElements/header_scaffold.php +++ b/templates/element/genericElements/header_scaffold.php @@ -53,7 +53,7 @@ foreach ($data['menu'] as $name => $menuElement) { } } $logoutButton = sprintf( - '%s', + '%s', $baseurl, __('Logout') ); diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 5d6e552..5b47bae 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -77,7 +77,7 @@ class AJAXApi { } /** - * + * * @param {FormData} formData - The data of a form * @param {Object} dataToMerge - Data to be merge into formData * @return {FormData} The form data merged with the additional dataToMerge data @@ -91,7 +91,7 @@ class AJAXApi { /** * @param {string} url - The URL to fetch - * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions * @return {Promise} Promise object resolving to the fetched HTML */ static async quickFetchURL(url, options={}) { @@ -102,7 +102,7 @@ class AJAXApi { /** * @param {string} url - The URL to fetch - * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions * @return {Promise} Promise object resolving to the fetched HTML */ static async quickFetchJSON(url, options={}) { @@ -113,7 +113,7 @@ class AJAXApi { /** * @param {string} url - The URL to fetch - * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions * @return {Promise} Promise object resolving to the fetched form */ static async quickFetchForm(url, options={}) { @@ -125,7 +125,7 @@ class AJAXApi { /** * @param {HTMLFormElement} form - The form to be posted * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form - * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions * @return {Promise} Promise object resolving to the result of the POST operation */ static async quickPostForm(form, dataToMerge={}, options={}) { @@ -395,7 +395,7 @@ class AJAXApi { } return toReturn } - + /** * @param {string} url - The URL from which to fetch the form * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form @@ -452,8 +452,7 @@ class AJAXApi { this.loadingOverlay.show() } else { this.loadingOverlay.hide() - + } } } - diff --git a/webroot/js/main.js b/webroot/js/main.js index 3e05d3a..ea39c37 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -66,6 +66,32 @@ function attachTestConnectionResultHtml(result, $container) { return $testResultDiv } +function syntaxHighlightJson(json, indent) { + if (indent === undefined) { + indent = 2; + } + if (typeof json == 'string') { + json = JSON.parse(json); + } + json = JSON.stringify(json, undefined, indent); + json = json.replace(/&/g, '&').replace(//g, '>').replace(/(?:\r\n|\r|\n)/g, '
').replace(/ /g, ' '); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'text-info'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'text-primary'; + } else { + cls = ''; + } + } else if (/true|false/.test(match)) { + cls = 'text-info'; + } else if (/null/.test(match)) { + cls = 'text-danger'; + } + return '' + match + ''; + }); +} + var UI $(document).ready(() => { if (typeof UIFactory !== "undefined") {