Merge branch 'main' into inbox

pull/54/head
iglocska 2021-06-01 14:37:29 +02:00
commit ef94ce147c
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
39 changed files with 1840 additions and 32 deletions

View File

@ -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` (

View File

@ -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

View File

@ -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

View File

@ -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'));
}

View File

@ -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' => [

View File

@ -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');
}

View File

@ -0,0 +1,214 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
class LocalToolsController extends AppController
{
public function index()
{
$data = $this->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');
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Controller\Open;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
use Cake\Event\EventInterface;
class IndividualsController extends AppController
{
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
$this->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');
}
}

View File

@ -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'));
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace CommonConnectorTools;
use Cake\ORM\Locator\LocatorAwareTrait;
class CommonConnectorTools
{
public $description = '';
public $name = '';
public $exposedFunctions = [
'diagnostics'
];
public $version = '???';
public function addExposedFunction(string $functionName): void
{
$this->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;
}
}
?>

View File

@ -0,0 +1,548 @@
<?php
namespace MispConnector;
require_once(ROOT . '/src/Lib/default/local_tool_connectors/CommonConnectorTools.php');
use CommonConnectorTools\CommonConnectorTools;
use Cake\Http\Client;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Client\Response;
class MispConnector extends CommonConnectorTools
{
public $description = 'MISP connector, handling diagnostics, organisation and sharing group management of your instance. Synchronisation requests can also be managed through the connector.';
public $name = 'MISP';
public $exposedFunctions = [
'serverSettingsAction' => [
'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.'));
}
}
?>

View File

@ -0,0 +1,172 @@
<?php
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\Event\EventInterface;
use Cake\Datasource\EntityInterface;
use ArrayObject;
use Cake\Utility\Text;
use Cake\Http\Client;
class SyncTool extends Behavior
{
// take a server as parameter and return a HttpSocket object using the ssl options defined in the server settings
public function setupHttpSocket($server = null, $timeout = false)
{
$params = array();
if (!empty($server)) {
if ($server['cert_file']) {
$params['ssl_cafile'] = APP . "files" . DS . "certs" . DS . $server['id'] . '.pem';
}
if ($server['client_cert_file']) {
$params['ssl_local_cert'] = APP . "files" . DS . "certs" . DS . $server['id'] . '_client.pem';
}
if ($server['self_signed']) {
$params['ssl_allow_self_signed'] = true;
$params['ssl_verify_peer_name'] = false;
if (!isset($server['cert_file'])) {
$params['ssl_verify_peer'] = false;
}
}
if (!empty($server['skip_proxy'])) {
$params['skip_proxy'] = 1;
}
if (!empty($timeout)) {
$params['timeout'] = $timeout;
}
}
return $this->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;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class LocalTool extends AppModel
{
}

View File

@ -171,4 +171,25 @@ class BroodsTable extends AppTable
return false;
}
}
public function queryLocalTools($brood_id)
{
$query = $this->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;
}
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Migrations\Migrations;
use Cake\Filesystem\Folder;
use Cake\Filesystem\File;
use Cake\Http\Exception\NotFoundException;
class LocalToolsTable extends AppTable
{
const HEALTH_CODES = [
0 => '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;
}
}

View File

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

View File

@ -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']);
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\View\Helper;
use Cake\View\Helper;
class PrettyPrintHelper extends Helper
{
public function ppArray($array, $depth = 0)
{
$text = '';
foreach ($array as $key => $value) {
if (is_array($value)) {
$value = $this->ppArray($value, $depth+1);
} else {
$value = h($value);
}
$text .= sprintf(
'<div><span class="text-primary">%s</span>: %s</div>',
h($key),
$value
);
}
return $text;
}
}
?>

View File

@ -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',

View File

@ -0,0 +1,2 @@
<?php
echo $this->element('genericElements/Form/genericForm', $data);

View File

@ -0,0 +1,4 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', $data);
echo '</div>';
?>

View File

@ -0,0 +1,35 @@
<?php
echo $this->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
]
]);
?>
</div>

View File

@ -0,0 +1,49 @@
<?php
echo $this->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 '</div>';
?>

View File

@ -0,0 +1,77 @@
<?php
echo $this->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 '</div>';
?>

View File

@ -0,0 +1,54 @@
<?php
echo $this->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 '</div>';
?>

View File

@ -0,0 +1,51 @@
<?php
$children = [];
if (!empty($entity['children'])) {
foreach ($entity['children'] as $child) {
$children[] = [
'url' => '/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
]
);

View File

@ -0,0 +1,33 @@
<?php
echo $this->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')
]
]
]
);

View File

@ -0,0 +1,4 @@
<?php
$random = Cake\Utility\Security::randomString(8);
$type = empty($data['type']) ? 'generic' : $data['type'];
echo $this->element('genericElements/Configuration/Fields/' . $type . 'Field.php', ['data' => $field]);

View File

@ -0,0 +1,33 @@
<?php
/*
* Generic configuration panel builder
*
* Simply pass a JSON with the following keys set:
* - title: The title for the configuration interface
* - diagnostic-view (optional): url of the diagnostic page to be shown and refreshed on any setting change
* - fields: an array with each element generating an input field
* - field: the unique field name for the context
* - description: a brief description of the field
* - type (optional): the type of form element to use
* - options (optional): for select style elements
* - validation (optional): regex to validate input
* - requirements (optional): boolean, if false is passed the field is skipped
*/
$diagnostics = '';
if (!empty($data['diagnostics'])) {
$diagnostics = '<div data-url="' . h($data['diagnostics']) . '"></div>';
}
$fields = '';
if (!empty($data['fields'])) {
foreach ($data['fields'] as $field) {
$fields .= $this->element('genericElements/Configuration/Fields/scaffold.php', ['data' => $field]);
}
}
echo sprintf(
'<div class="%s"><h2>%s</h2><div>%s</div><div><%s/div></div>',
empty($ajax) ? 'col-8' : '',
h($data['title']),
$diagnostics,
$fields
);
?>

View File

@ -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(
'<a href="%s%s" title="%s" aria-label="%s" %s %s class="link-unstyled"><i class="%s"></i></a> ',
$baseurl,
'<a href="%s" title="%s" aria-label="%s" %s %s class="link-unstyled"><i class="%s"></i></a> ',
$url,
empty($action['title']) ? '' : h($action['title']),
empty($action['title']) ? '' : h($action['title']),

View File

@ -0,0 +1,7 @@
<?php
$data = $this->Hash->extract($row, $field['data_path']);;
if (!empty($field['isJson'])) {
$data = json_decode($data[0], true);
}
echo $this->PrettyPrint->ppArray($data);
?>

View File

@ -1,4 +1,6 @@
<?php
if (!isset($arrayData) && isset($field['arrayData'])) {
$arrayData = $field['arrayData'];
}
echo h($arrayData[$this->Hash->extract($row, $field['data_path'])[0]]);
?>

View File

@ -16,7 +16,6 @@
'<i class="fa fa-%s"></i>',
$data ? 'check' : 'times'
);
$data = '';
} else {
$data = h($data);
if (!empty($field['privacy'])) {

View File

@ -0,0 +1,28 @@
<?php
$data = $this->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(
'<a href="%s/%s">%s</a>',
$baseurl,
$healthElement['url'],
$name
);
}
$lines .= sprintf(
'<p><span class="text-%s"><i class="fas fa-circle" ></i></span> %s: %s</p>',
$status_colours[$healthElement['health']],
$name,
h($healthElement['message'])
);
}
echo $lines;
?>

View File

@ -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];
}

View File

@ -1,3 +1,19 @@
<?php
$value = Cake\Utility\Hash::extract($data, $field['path']);
echo sprintf('<pre><code>%s</pre></code>', 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(
'<div class="json_container_%s"></div>',
h($randomId)
);
?>
<script type="text/javascript">
$(document).ready(function() {
$('.json_container_<?php echo h($randomId);?>').html(syntaxHighlightJson(<?php echo json_encode($string); ?>, 4));
});
</script>

View File

@ -53,7 +53,7 @@ foreach ($data['menu'] as $name => $menuElement) {
}
}
$logoutButton = sprintf(
'<span class="nav-item"><a href="%susers/logout" class="nav-link">%s</a></span>',
'<span class="nav-item"><a href="%s/users/logout" class="nav-link">%s</a></span>',
$baseurl,
__('Logout')
);

View File

@ -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<string>} 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<Object>} 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<HTMLFormElement>} 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<Object>} 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()
}
}
}

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/(?:\r\n|\r|\n)/g, '<br>').replace(/ /g, '&nbsp;');
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 '<span class="' + cls + '">' + match + '</span>';
});
}
var UI
$(document).ready(() => {
if (typeof UIFactory !== "undefined") {