Merge branch '2.4' of github.com:MISP/MISP into 2.4

pull/6535/head
iglocska 2020-10-31 08:49:21 +01:00
commit 9768fc9bcc
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
29 changed files with 1969 additions and 145 deletions

View File

@ -48,7 +48,7 @@ class AppController extends Controller
public $helpers = array('Utility', 'OrgImg', 'FontAwesome', 'UserName', 'DataPathCollector');
private $__queryVersion = '113';
private $__queryVersion = '114';
public $pyMispVersion = '2.4.133';
public $phpmin = '7.2';
public $phprec = '7.4';

View File

@ -159,6 +159,10 @@ class ACLComponent extends Component
'restore' => array('perm_add'),
'index' => array('*'),
'getProxyMISPElements' => array('*'),
'extractAllFromReport' => array('*'),
'extractFromReport' => array('*'),
'replaceSuggestionInReport' => array('*'),
'importReportFromUrl' => array('*'),
),
'events' => array(
'add' => array('perm_add'),

View File

@ -185,11 +185,138 @@ class EventReportsController extends AppController
$this->__injectIndexVariablesToViewContext($filters);
if (!empty($filters['index_for_event'])) {
$this->set('extendedEvent', !empty($filters['extended_event']));
$fetcherModule = $this->EventReport->isFetchURLModuleEnabled();
$this->set('importModuleEnabled', is_array($fetcherModule));
$this->render('ajax/indexForEvent');
}
}
}
public function extractAllFromReport($reportId)
{
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
} else {
if ($this->request->is('post')) {
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false);
$results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report);
$report['EventReport']['content'] = $results['replacementResult']['contentWithReplacements'];
$contextResults = $this->EventReport->extractWithReplacements($this->Auth->user(), $report, ['replace' => true]);
$suggestionResult = $this->EventReport->transformFreeTextIntoSuggestion($contextResults['contentWithReplacements'], $results['complexTypeToolResult']);
$errors = $this->EventReport->applySuggestions($this->Auth->user(), $report, $suggestionResult['contentWithSuggestions'], $suggestionResult['suggestionsMapping']);
if (empty($errors)) {
if (!empty($this->data['EventReport']['tag_event'])) {
$this->EventReport->attachTagsAfterReplacements($this->Auth->User(), $contextResults['replacedContext'], $report['EventReport']['event_id']);
}
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId);
$data = [ 'report' => $report ];
$successMessage = __('Automatic extraction applied to Event Report %s', $reportId);
return $this->__getSuccessResponseBasedOnContext($successMessage, $data, 'applySuggestions', $reportId);
} else {
$errorMessage = __('Automatic extraction could not be applied to Event Report %s.%sReasons: %s', $reportId, PHP_EOL, json_encode($errors));
return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'applySuggestions', $reportId);
}
}
$this->layout = 'ajax';
$this->set('reportId', $reportId);
$this->render('ajax/extractAllFromReport');
}
}
public function extractFromReport($reportId)
{
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
} else {
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'view', $throwErrors=true, $full=false);
$dataResults = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report);
$report['EventReport']['content'] = $dataResults['replacementResult']['contentWithReplacements'];
$contextResults = $this->EventReport->extractWithReplacements($this->Auth->user(), $report);
$typeToCategoryMapping = $this->EventReport->Event->Attribute->typeToCategoryMapping();
$data = [
'complexTypeToolResult' => $dataResults['complexTypeToolResult'],
'typeToCategoryMapping' => $typeToCategoryMapping,
'replacementValues' => $dataResults['replacementResult']['replacedValues'],
'replacementContext' => $contextResults['replacedContext']
];
return $this->RestResponse->viewData($data, $this->response->type());
}
}
public function replaceSuggestionInReport($reportId)
{
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
} else {
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false);
if ($this->request->is('post')) {
$errors = [];
$suggestions = $this->EventReport->jsonDecode($this->data['EventReport']['suggestions']);
if (!empty($suggestions['content']) && !empty($suggestions['mapping'])) {
$errors = $this->EventReport->applySuggestions($this->Auth->user(), $report, $suggestions['content'], $suggestions['mapping']);
} else {
$errors[] = __('`content` and `mapping` key cannot be empty');
}
if (empty($errors)) {
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId);
$results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report);
$contextResults = $this->EventReport->extractWithReplacements($this->Auth->user(), $report);
$data = [
'report' => $report,
'complexTypeToolResult' => $results['complexTypeToolResult'],
'replacementValues' => $results['replacementResult']['replacedValues'],
'replacementContext' => $contextResults['replacedContext']
];
$successMessage = __('Suggestions applied to Event Report %s', $reportId);
return $this->__getSuccessResponseBasedOnContext($successMessage, $data, 'applySuggestions', $reportId);
} else {
$errorMessage = __('Suggestions could not be applied to Event Report %s.%sReasons: %s', $reportId, PHP_EOL, json_encode($errors));
return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'applySuggestions', $reportId);
}
}
$this->layout = 'ajax';
$this->render('ajax/replaceSuggestionInReport');
}
}
public function importReportFromUrl($event_id)
{
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
}
$fetcherModule = $this->EventReport->isFetchURLModuleEnabled();
if ($this->request->is('post')) {
if (empty($this->data['EventReport']['url'])) {
throw new MethodNotAllowedException(__('An URL must be provided'));
}
$url = $this->data['EventReport']['url'];
$markdown = $this->EventReport->downloadMarkdownFromURL($event_id, $url);
$errors = [];
if (!empty($markdown)) {
$report = [
'name' => __('Report from - %s (%s)', $url, time()),
'distribution' => 5,
'content' => $markdown
];
$errors = $this->EventReport->addReport($this->Auth->user(), $report, $event_id);
} else {
$errors[] = __('Could not fetch report from URL. Fetcher module not enabled or could not download the page');
}
$redirectTarget = array('controller' => 'events', 'action' => 'view', $event_id);
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($errors, array(), 'addFromURL', $this->EventReport->id, $redirectTarget);
} else {
$successMessage = __('Report downloaded and created');
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $this->EventReport->id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $report, 'addFromURL', false, $redirectTarget);
}
}
$this->set('importModuleEnabled', is_array($fetcherModule));
$this->set('event_id', $event_id);
$this->layout = 'ajax';
$this->render('ajax/importReportFromUrl');
}
private function __generateIndexConditions($filters = [])
{
$aclConditions = $this->EventReport->buildACLConditions($this->Auth->user());

View File

@ -0,0 +1,34 @@
<?php
class ButtonWidget
{
public $title = 'Button Widget';
public $render = 'Button';
public $width = 3;
public $height = 2;
public $cacheLifetime = false;
public $autoRefreshDelay = false;
public $params = array(
'url' => 'URL (after base url) to redirect to',
'text' => 'Text to display on the button'
);
public $description = 'Simple button to allow shortcuts';
public $placeholder =
'{
"url": "/events/index",
"text": "Go to events"
}';
public function handler($user, $options = array())
{
$data = array();
if(isset($options['url'])) {
$data['url'] = $options['url'];
}
if(isset($options['text'])) {
$data['text'] = $options['text'];
}
return $data;
}
}

View File

@ -279,7 +279,7 @@ class SendEmail
*/
public function sendExternal(array $params)
{
foreach (array('body', 'reply-to', 'to', 'subject', 'text') as $requiredParam) {
foreach (array('body', 'reply-to', 'to', 'subject') as $requiredParam) {
if (!isset($params[$requiredParam])) {
throw new InvalidArgumentException("Param '$requiredParam' is required, but not provided.");
}

View File

@ -4779,4 +4779,18 @@ class Attribute extends AppModel
{
return (is_int($value) && $value >= 0) || ctype_digit($value);
}
public function typeToCategoryMapping()
{
$typeCategoryMapping = array();
foreach ($this->categoryDefinitions as $k => $cat) {
foreach ($cat['types'] as $type) {
$typeCategoryMapping[$type][$k] = $k;
}
}
foreach ($typeCategoryMapping as $k => $v) {
$typeCategoryMapping[$k] = array_values($v);
}
return $typeCategoryMapping;
}
}

View File

@ -13,6 +13,7 @@ class EventReport extends AppModel
'userKey' => 'user_id',
'change' => 'full'
),
'Regexp' => array('fields' => array('value')),
);
public $validate = array(
@ -519,4 +520,380 @@ class EventReport extends AppModel
}
return $errors;
}
public function applySuggestions($user, $report, $contentWithSuggestions, $suggestionsMapping) {
$errors = [];
$replacedContent = $contentWithSuggestions;
$success = 0;
foreach ($suggestionsMapping as $value => $suggestedAttribute) {
$suggestedAttribute['value'] = $value;
$savedAttribute = $this->createAttributeFromSuggestion($user, $report, $suggestedAttribute);
if (empty($savedAttribute['errors'])) {
$success++;
$replacedContent = $this->applySuggestionsInText($replacedContent, $savedAttribute['attribute'], $value);
} else {
$replacedContent = $this->revertToOriginalInText($replacedContent, $value);
$errors[] = $savedAttribute['errors'];
}
}
if ($success > 0 || count($suggestionsMapping) == 0) {
$report['EventReport']['content'] = $replacedContent;
$editErrors = $this->editReport($user, $report, $report['EventReport']['event_id']);
if (!empty($editErrors)) {
$errors[] = $editErrors;
}
}
return $errors;
}
public function applySuggestionsInText($contentWithSuggestions, $attribute, $value)
{
$textToBeReplaced = sprintf('@[suggestion](%s)', $value);
$textToInject = sprintf('@[attribute](%s)', $attribute['Attribute']['uuid']);
$replacedContent = str_replace($textToBeReplaced, $textToInject, $contentWithSuggestions);
return $replacedContent;
}
public function revertToOriginalInText($contentWithSuggestions, $value)
{
$textToBeReplaced = sprintf('@[suggestion](%s)', $value);
$textToInject = $value;
$replacedContent = str_replace($textToBeReplaced, $textToInject, $contentWithSuggestions);
return $replacedContent;
}
private function createAttributeFromSuggestion($user, $report, $suggestedAttribute)
{
$errors = [];
$attribute = [
'event_id' => $report['EventReport']['event_id'],
'distribution' => 5,
'category' => $suggestedAttribute['category'],
'type' => $suggestedAttribute['type'],
'value' => $suggestedAttribute['value'],
'to_ids' => $suggestedAttribute['to_ids'],
];
$validationErrors = array();
$this->Event->Attribute->captureAttribute($attribute, $report['EventReport']['event_id'], $user, false, false, false, $validationErrors);
$savedAttribute = false;
if (!empty($validationErrors)) {
$errors = $validationErrors;
} else {
$savedAttribute = $this->Event->Attribute->find('first', array(
'recursive' => -1,
'conditions' => array('Attribute.id' => $this->Event->Attribute->id),
));
}
return [
'errors' => $errors,
'attribute' => $savedAttribute
];
}
/**
* transformFreeTextIntoReplacement
*
* @param array $user
* @param array $report
* @param array $complexTypeToolResult Uses the complex type tool output to support import regex replacements.
* Another solution would be to run the regex replacement on each token of the report which is too heavy
* @return array
*/
public function transformFreeTextIntoReplacement(array $user, array $report, array $complexTypeToolResult)
{
$complexTypeToolResultWithImportRegex = $this->injectImportRegexOnComplexTypeToolResult($complexTypeToolResult);
$valueToValueWithRegex = Hash::combine($complexTypeToolResultWithImportRegex, '{n}.valueWithImportRegex', '{n}.value');
$proxyElements = $this->getProxyMISPElements($user, $report['EventReport']['event_id']);
$originalContent = $report['EventReport']['content'];
$content = $originalContent;
$replacedValues = [];
foreach ($proxyElements['attribute'] as $uuid => $attribute) {
$count = 0;
$textToInject = sprintf('@[attribute](%s)', $uuid);
$content = str_replace($attribute['value'], $textToInject, $content, $count);
if ($count > 0 || strpos($originalContent, $attribute['value'])) { // Check if the value has been replaced by the first match
if (!isset($replacedValues[$attribute['value']])) {
$replacedValues[$attribute['value']] = [
'attributeUUIDs' => [$uuid],
'valueInReport' => $attribute['value'],
];
} else {
$replacedValues[$attribute['value']]['attributeUUIDs'][] = $uuid;
}
$count = 0;
}
if (isset($valueToValueWithRegex[$attribute['value']]) && $valueToValueWithRegex[$attribute['value']] != $attribute['value']) {
$content = str_replace($valueToValueWithRegex[$attribute['value']], $textToInject, $content, $count);
if ($count > 0 || strpos($originalContent, $valueToValueWithRegex[$attribute['value']])) {
if (!isset($replacedValues[$attribute['value']])) {
$replacedValues[$attribute['value']] = [
'attributeUUIDs' => [$uuid],
'valueInReport' => $valueToValueWithRegex[$attribute['value']],
];
} else {
$replacedValues[$attribute['value']]['attributeUUIDs'][] = $uuid;
}
}
}
}
return [
'contentWithReplacements' => $content,
'replacedValues' => $replacedValues
];
}
public function transformFreeTextIntoSuggestion($content, $complexTypeToolResult)
{
$replacedContent = $content;
$suggestionsMapping = [];
$typeToCategoryMapping = $this->Event->Attribute->typeToCategoryMapping();
foreach ($complexTypeToolResult as $i => $complexTypeToolEntry) {
$textToBeReplaced = $complexTypeToolEntry['value'];
$textToInject = sprintf('@[suggestion](%s)', $textToBeReplaced);
$suggestionsMapping[$textToBeReplaced] = [
'category' => $typeToCategoryMapping[$complexTypeToolEntry['default_type']][0],
'type' => $complexTypeToolEntry['default_type'],
'value' => $textToBeReplaced,
'to_ids' => $complexTypeToolEntry['to_ids'],
];
$replacedContent = str_replace($textToBeReplaced, $textToInject, $replacedContent);
}
return [
'contentWithSuggestions' => $replacedContent,
'suggestionsMapping' => $suggestionsMapping
];
}
public function injectImportRegexOnComplexTypeToolResult($complexTypeToolResult) {
foreach ($complexTypeToolResult as $i => $complexTypeToolEntry) {
$transformedValue = $this->runRegexp($complexTypeToolEntry['default_type'], $complexTypeToolEntry['value']);
if ($transformedValue !== false) {
$complexTypeToolResult[$i]['valueWithImportRegex'] = $transformedValue;
}
}
return $complexTypeToolResult;
}
public function getComplexTypeToolResultFromReport($content)
{
App::uses('ComplexTypeTool', 'Tools');
$complexTypeTool = new ComplexTypeTool();
$this->Warninglist = ClassRegistry::init('Warninglist');
$complexTypeTool->setTLDs($this->Warninglist->fetchTLDLists());
$complexTypeToolResult = $complexTypeTool->checkComplexRouter($content, 'freetext');
return $complexTypeToolResult;
}
public function getComplexTypeToolResultWithReplacements($user, $report)
{
$complexTypeToolResult = $this->getComplexTypeToolResultFromReport($report['EventReport']['content']);
$replacementResult = $this->transformFreeTextIntoReplacement($user, $report, $complexTypeToolResult);
$complexTypeToolResult = $this->getComplexTypeToolResultFromReport($replacementResult['contentWithReplacements']);
return [
'complexTypeToolResult' => $complexTypeToolResult,
'replacementResult' => $replacementResult,
];
}
/**
* extractWithReplacements Extract context information from report with special care for ATT&CK
*
* @param array $user
* @param array $report
* @return array
*/
public function extractWithReplacements(array $user, array $report, array $options = [])
{
$baseOptions = [
'replace' => false,
'tags' => true,
'synonyms' => true,
'synonyms_min_characters' => 4,
'prune_deprecated' => true,
'attack' => true,
];
$options = array_merge($baseOptions, $options);
$originalContent = $report['EventReport']['content'];
$this->GalaxyCluster = ClassRegistry::init('GalaxyCluster');
$mitreAttackGalaxyId = $this->GalaxyCluster->Galaxy->getMitreAttackGalaxyId();
$clusterContain = ['Tag'];
$replacedContext = [];
if ($options['prune_deprecated']) {
$clusterContain['Galaxy'] = ['conditions' => ['Galaxy.namespace !=' => 'deprecated']];
}
if ($options['synonyms']) {
$clusterContain['GalaxyElement'] = ['conditions' => ['GalaxyElement.key' => 'synonyms']];
}
$clusterConditions = [];
if ($options['attack']) {
$clusterConditions = ['GalaxyCluster.galaxy_id !=' => $mitreAttackGalaxyId];
}
$clusters = $this->GalaxyCluster->find('all', [
'conditions' => $clusterConditions,
'contain' => $clusterContain
]);
if ($options['tags']) {
$this->Tag = ClassRegistry::init('Tag');
$tags = $this->Tag->fetchUsableTags($user);
foreach ($tags as $i => $tag) {
$tagName = $tag['Tag']['name'];
$found = $this->isValidReplacementTag($originalContent, $tagName);
if ($found) {
$replacedContext[$tagName][$tagName] = $tag['Tag'];
} else {
$tagNameUpper = strtoupper($tagName);
$found = $this->isValidReplacementTag($originalContent, $tagNameUpper);
if ($found) {
$replacedContext[$tagNameUpper][$tagName] = $tag['Tag'];
}
}
}
}
foreach ($clusters as $i => $cluster) {
$cluster['GalaxyCluster']['colour'] = '#0088cc';
$tagName = $cluster['GalaxyCluster']['tag_name'];
$found = $this->isValidReplacementTag($originalContent, $tagName);
if ($found) {
$replacedContext[$tagName][$tagName] = $cluster['GalaxyCluster'];
}
$toSearch = ' ' . $cluster['GalaxyCluster']['value'] . ' ';
$found = strpos($originalContent, $toSearch) !== false;
if ($found) {
$replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster'];
}
if ($options['synonyms']) {
foreach ($cluster['GalaxyElement'] as $j => $element) {
if (strlen($element['value']) >= $options['synonyms_min_characters']) {
$toSearch = ' ' . $element['value'] . ' ';
$found = strpos($originalContent, $toSearch) !== false;
if ($found) {
$replacedContext[$element['value']][$tagName] = $cluster['GalaxyCluster'];
}
}
}
}
}
if ($options['attack']) {
unset($clusterContain['Galaxy']);
$attackClusters = $this->GalaxyCluster->find('all', [
'conditions' => ['GalaxyCluster.galaxy_id' => $mitreAttackGalaxyId],
'contain' => $clusterContain
]);
foreach ($attackClusters as $i => $cluster) {
$cluster['GalaxyCluster']['colour'] = '#0088cc';
$tagName = $cluster['GalaxyCluster']['tag_name'];
$toSearch = ' ' . $cluster['GalaxyCluster']['value'] . ' ';
$found = strpos($originalContent, $toSearch) !== false;
if ($found) {
$replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster'];
} else {
$clusterParts = explode(' - ', $cluster['GalaxyCluster']['value'], 2);
$toSearch = ' ' . $clusterParts[0] . ' ';
$found = strpos($originalContent, $toSearch) !== false;
if ($found) {
$replacedContext[$clusterParts[0]][$tagName] = $cluster['GalaxyCluster'];
} else {
$toSearch = ' ' . $clusterParts[1] . ' ';
$found = strpos($originalContent, $toSearch) !== false;
if ($found) {
$replacedContext[$clusterParts[1]][$tagName] = $cluster['GalaxyCluster'];
}
}
}
}
}
$toReturn = [
'replacedContext' => $replacedContext
];
if ($options['replace']) {
$content = $originalContent;
foreach ($replacedContext as $rawText => $replacements) {
// Replace with first one until a better strategy is found
reset($replacements);
$replacement = key($replacements);
$textToInject = sprintf('@[tag](%s)', $replacement);
$content = str_replace($rawText, $textToInject, $content);
}
$toReturn['contentWithReplacements'] = $content;
}
return $toReturn;
}
public function downloadMarkdownFromURL($event_id, $url)
{
$this->Module = ClassRegistry::init('Module');
$module = $this->isFetchURLModuleEnabled();
if (!is_array($module)) {
return false;
}
$modulePayload = [
'module' => $module['name'],
'event_id' => $event_id,
'url' => $url
];
$module = $this->isFetchURLModuleEnabled();
if (!empty($module)) {
$result = $this->Module->queryModuleServer('/query', $modulePayload, false);
if (empty($result['results'][0]['values'][0])) {
return '';
}
return $result['results'][0]['values'][0];
}
return false;
}
public function isFetchURLModuleEnabled() {
$this->Module = ClassRegistry::init('Module');
$module = $this->Module->getEnabledModule('html_to_markdown', 'expansion');
return !empty($module) ? $module : false;
}
/**
* findValidReplacementTag Search if tagName is in content and is not wrapped in a tag reference
*
* @param string $content
* @param string $tagName
* @return bool
*/
private function isValidReplacementTag($content, $tagName)
{
$lastIndex = 0;
$allIndices = [];
$toSearch = strpos($tagName, ':') === false ? ' ' . $tagName . ' ' : $tagName;
while (($lastIndex = strpos($content, $toSearch, $lastIndex)) !== false) {
$allIndices[] = $lastIndex;
$lastIndex = $lastIndex + strlen($toSearch);
}
if (empty($allIndices)) {
return false;
} else {
$wrapper = '@[tag](';
foreach ($allIndices as $i => $index) {
$stringBeforeTag = substr($content, $index - strlen($wrapper), strlen($wrapper));
if ($stringBeforeTag != $wrapper) {
return true;
}
}
return false;
}
}
public function attachTagsAfterReplacements($user, $replacedContext, $eventId)
{
$this->EventTag = ClassRegistry::init('EventTag');
foreach ($replacedContext as $rawText => $tagNames) {
// Replace with first one until a better strategy is found
reset($tagNames);
$tagName = key($tagNames);
$tagId = $this->EventTag->Tag->lookupTagIdFromName($tagName);
if ($tagId === -1) {
$tagId = $this->EventTag->Tag->captureTag(['name' => $tagName], $user);
}
$this->EventTag->attachTagToEvent($eventId, $tagId);
}
}
}

View File

@ -160,7 +160,7 @@ class Module extends AppModel
return 'The requested module is not enabled.';
}
if (is_array($modules)) {
foreach ($modules['modules'] as $module) {
foreach ($modules as $module) {
if ($module['name'] == $name) {
if ($type && in_array(strtolower($type), $module['meta']['module-type'])) {
return $module;

View File

@ -4,6 +4,10 @@ App::uses('GpgTool', 'Tools');
class Server extends AppModel
{
const SETTING_CRITICAL = 0,
SETTING_RECOMMENDED = 1,
SETTING_OPTIONAL = 2;
public $name = 'Server';
public $actsAs = array('SysLogLogable.SysLogLogable' => array(
@ -1100,7 +1104,33 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
'null' => true
)
),
'attachment_scan_module' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('Name of enrichment module that will be used for attachment malware scanning. This module must return av-signature or sb-signature object.'),
'value' => '',
'errorMessage' => '',
'type' => 'string',
'null' => true,
],
'attachment_scan_hash_only' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('Send to attachment scan module just file hash. This can be useful if module sends attachment to remote service and you don\'t want to leak real data.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
'null' => true,
],
'attachment_scan_timeout' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('How long to wait for scan results in seconds.'),
'value' => 30,
'errorMessage' => '',
'test' => 'testForPositiveInteger',
'type' => 'numeric',
'null' => true,
]
),
'GnuPG' => array(
'branch' => 1,
@ -3443,6 +3473,14 @@ class Server extends AppModel
return true;
}
public function testForPositiveInteger($value)
{
if ((is_int($value) && $value >= 0) || ctype_digit($value)) {
return true;
}
return __('The value has to be a whole number greater or equal 0.');
}
public function testForCookieTimeout($value)
{
$numeric = $this->testForNumeric($value);

View File

@ -5,6 +5,72 @@ This plugin enables CakePHP applications to use Single Sign-On to authenticate i
## Usage
### Prerequisites - Shibboleth Service Provider
The MISP plugin takes care of the mapping of your shibboleth session attributes to MISP, but you will still need to install the service provider (SP) and configure it yourself. The documentation for Shibboleth Service Provider 3 can be found at https://wiki.shibboleth.net/confluence/display/SP3/Home.
To install Shibboleth SP3 on Ubuntu, you can use the instructions provided by SWITCH at https://www.switch.ch/aai/guides/sp/installation/ and then follow the below steps. If you already installed and configured Shibboleth you can skip this section.
Create signing and encryption certificate. The value following -e should be your entity ID, for example https://&lt;host&gt;/shibboleth.
```bash
sudo shib-keygen -f -u _shibd -h <host> -y 5 -e https://<host>/shibboleth -o /etc/shibboleth
```
Edit /etc/shibboleth/shibboleth2.xml to use the created certificate for both signing and encryption (change the values for key and certificate).
```xml
<CredentialResolver type="File" use="signing"
key="sp-key.pem" certificate="sp-cert.pem"/>
<CredentialResolver type="File" use="encryption"
key="sp-key.pem" certificate="sp-cert.pem"/>
```
Edit /etc/shibboleth/shibboleth2.xml to set secure cookie properties (cookieProps) if you want to.
```xml
<Sessions lifetime="28800" timeout="3600" relayState="ss:mem"
checkAddress="false" handlerSSL="false" cookieProps="https"
redirectLimit="exact">
```
At this point, you should already be able to test your configuration. The last line of the output should be "overall configuration is loadable, check console for non-fatal problems".
```bash
sudo shibd -t
```
Set entityID in /etc/shibboleth/shibboleth2.xml.
```xml
<ApplicationDefaults entityID="https://<host>/shibboleth"
REMOTE_USER="eppn subject-id pairwise-id persistent-id"
cipherSuites="DEFAULT:!EXP:!LOW:!aNULL:!eNULL:!DES:!IDEA:!SEED:!RC4:!3DES:!kRSA:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1">
```
Copy your identity provider metadata to /etc/shibboleth, for example to /etc/shibboleth/idp-metadata.xml and refer to it in /etc/shibboleth/shibboleth2.xml. Uncomment and edit the relevant line.
```xml
<MetadataProvider type="XML" validate="true" path="idp-metadata.xml"/>
```
Optionally, you can make sure the service provider does not create a session if some attributes, like OrgTag and GroupTag are missing. If users attempt to login an this happens, they will receive a pre-configured reply (default at /etc/shibboleth/attrChecker.html).
In /etc/shibboleth/shibboleth2.xml, edit ApplicationDefaults by adding the sessionHook:
```xml
<ApplicationDefaults entityID="https://<HOST>/shibboleth"
REMOTE_USER="eppn persistent-id targeted-id"
signing="front" encryption="false"
sessionHook="/Shibboleth.sso/AttrChecker"
```
Optional for attribute checking: add your checks (note that the incoming attribute names can be different for you, for more info on possible checks refer to https://wiki.shibboleth.net/confluence/display/SP3/Attribute+Checker+Handler):
```xml
<Handler type="AttributeChecker" Location="/AttrChecker" template="attrChecker.html" attributes="OrgTag GroupTag" flushSession="true"/>
```
At this point you will have to send your metadata to your identity provider. You can get template metadata based on your configuration from https://&lt;host&gt;/Shibboleth.sso/Metadata.
### MISP plugin configuration
Edit your MISP apache configuration by adding the below (location depends on your handler path, /Shibboleth.sso by default).
```Apache
<Location /Shibboleth.sso>
SetHandler shib
</Locations>
```
Enable the plugin at bootstrap.php:
```php
@ -18,43 +84,68 @@ Uncomment the following line to enable SSO authorization
'auth'=>array('ShibbAuth.ApacheShibb'),
```
And configure it. MailTag, OrgTag and GroupTag are the string that represent the key for the values needed by the plugin.
For example if you are using ADFS OrgTag will be ADFS_FEDERATION, GroupTag will be ADFS_GROUP, etc. meaning the key for the values needed.
DefaultOrg are values that come by default just in case they are not defined or obtained from the environment variables.
The GroupRoleMatching is an array that allows the definition and correlation between groups and roles in MISP, being them updated
If the line does not exist, add it to 'Security' array, for example like below. Note that you should just add the line to your own existing config.
```php
'Security' =>
array (
'level' => 'medium',
'salt' => '',
'cipherSeed' => '',
'password_policy_length' => 12,
'password_policy_complexity' => '/^((?=.*\\d)|(?=.*\\W+))(?![\\n])(?=.*[A-Z])(?=.*[a-z]).*$|.{16,}/',
'self_registration_message' => 'If you would like to send us a registration request, please fill out the form below. Make sure you fill out as much information as possible in order to ease the task of the administrators.',
'auth'=>array('ShibbAuth.ApacheShibb'),
)
```
And configure it. MailTag, OrgTag and GroupTag are the keys for the values needed by the plugin.
For example if you are using ADFS you should replace IDP_FEDERATION_TAG by ADFS_FEDERATION, IDP_GROUP_TAG by ADFS_GROUP, etc.
Replace MISP_DEFAULT_ORG by the organization you want users to be assigned to in case no organization value is given by the identity provider.
The GroupRoleMatching is an array that allows the definition and correlation between groups and roles in MISP. These get updated
if the groups are updated (i.e. a user that was admin and their groups changed inside the organization will have his role changed in MISP
upon the next login being now user or org admin respectively). The GroupSeparator is the character used to separate the different groups
in the list given by apache.
in the list given by apache. By default, you can leave it at ';'.
```php
'ApacheShibbAuth' => // Configuration for shibboleth authentication
array(
'MailTag' => 'EMAIL_TAG',
'OrgTag' => 'FEDERATION_TAG',
'GroupTag' => 'GROUP_TAG',
'MailTag' => 'IDP_EMAIL_TAG',
'OrgTag' => 'IDP_FEDERATION_TAG',
'GroupTag' => 'IDP_GROUP_TAG',
'GroupSeparator' => ';',
'GroupRoleMatching' => array( // 3:User, 1:admin. May be good to set "1" for the first user
'group_three' => '3',
'group_two' => 2,
'group_one' => 1,
'possible_group_attribute_value_3' => '3',
'possible_group_attribute_value_2' => 2,
'possible_group_attribute_value_1' => 1,
),
'DefaultOrg' => 'DEFAULT_ORG',
'DefaultOrg' => 'MISP_DEFAULT_ORG',
),
```
If used with Apache as webserver it might be useful to make a distinction to filter out API/Syncs from SSO login. It can be added to the vhost as follows:
If used with Apache as webserver it might be useful to make a distinction to filter out API/Syncs from SSO login. It can be added to the vhost as follows (Added lines are the If/Else clauses):
```Apache
<If "-T req('Authorization')">
Require all granted
AuthType None
</If>
<Else>
Require valid-user
AuthType shibboleth
ShibRequestSetting requiresession On
ShibRequestSetting shibexportassertion Off
ShibUseHeaders On
</Else>
<Directory /var/www/MISP/app/webroot>
Options -Indexes
AllowOverride all
<If "-T req('Authorization')">
Require all granted
AuthType None
</If>
<Else>
Require valid-user
AuthType shibboleth
ShibRequestSetting requiresession On
ShibRequestSetting shibexportassertion Off
ShibUseHeaders On
</Else>
</Directory>
```
If you want the logout button to work for killing your session, you can use the CustomAuth plugin to configure a custom logout url, by default the url should be https://&lt;host&gt;/Shibboleth.sso/Logout. This leads to a local logout. If you want to also trigger a logout at the identity provider, you can use the return mechanism. In this case you will need to change the allowed redirects. Your logout url will look like https://&lt;host&gt;/Shibboleth.sso/Logout?return=https://<idp_host>/Logout. Edit your shibboleth configuration (often at /etc/shibboleth/shibboleth2.xml) as necessary. Relevant shibboleth documentation can be found at https://wiki.shibboleth.net/confluence/display/SP3/Logout and https://wiki.shibboleth.net/confluence/display/SP3/Sessions.
```xml
<Sessions lifetime="28800" timeout="3600" relayState="ss:mem"
checkAddress="false" handlerSSL="false" cookieProps="https"
redirectLimit="exact+whitelist" redirectWhitelist="https://<idp_host>">
```

View File

@ -206,40 +206,38 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) {
<td class="shortish">
<ul class="inline" style="margin:0">
<?php
if (!empty($object['Feed'])) {
if (isset($object['Feed'])) {
foreach ($object['Feed'] as $feed) {
$popover = '';
foreach ($feed as $k => $v) {
if ($k == 'id') continue;
if (is_array($v)) {
foreach ($v as $k2 => $v2) {
$v[$k2] = h($v2);
}
$v = implode('<br>', $v);
} else {
$v = h($v);
}
$popover .= '<span class="bold black">' . Inflector::humanize(h($k)) . '</span>: <span class="blue">' . $v . '</span><br>';
$relatedData = array(
__('Name') => $feed['name'],
__('URL') => $feed['url'],
__('Provider') => $feed['provider'],
);
if (isset($feed['event_uuids'])) {
$relatedData[__('Event UUIDs')] = implode('<br>', $feed['event_uuids']);
}
$popover = '';
foreach ($relatedData as $k => $v) {
$popover .= '<span class="bold black">' . h($k) . '</span>: <span class="blue">' . h($v) . '</span><br>';
}
$liContents = '';
if ($isSiteAdmin || $hostOrgUser) {
if ($feed['source_format'] === 'misp') {
$liContents .= sprintf(
$liContents = sprintf(
'<form action="%s/feeds/previewIndex/%s" method="post" style="margin:0;line-height:auto;">%s%s</form>',
$baseurl,
h($feed['id']),
sprintf(
'<input type="hidden" name="data[Feed][eventid]" value="%s">',
h(json_encode($feed['event_uuids'], true))
h(json_encode($feed['event_uuids']))
),
sprintf(
'<input type="submit" class="linkButton useCursorPointer" value="%s" data-toggle="popover" data-content="%s" data-trigger="hover" style="margin-right:3px;line-height:normal;vertical-align: text-top;" />',
'<input type="submit" class="linkButton useCursorPointer" value="%s" data-toggle="popover" data-content="%s" data-trigger="hover" style="margin-right:3px;line-height:normal;vertical-align: text-top;">',
h($feed['id']),
h($popover)
)
);
} else {
$liContents .= sprintf(
$liContents = sprintf(
'<a href="%s/feeds/previewIndex/%s" style="margin-right:3px;" data-toggle="popover" data-content="%s" data-trigger="hover">%s</a>',
$baseurl,
h($feed['id']),
@ -248,7 +246,7 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) {
);
}
} else {
$liContents .= sprintf(
$liContents = sprintf(
'<span style="margin-right:3px;">%s</span>',
h($feed['id'])
);
@ -259,7 +257,7 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) {
);
}
}
if (!empty($object['Server'])) {
if (isset($object['Server'])) {
foreach ($object['Server'] as $server) {
$popover = '';
foreach ($server as $k => $v) {

View File

@ -0,0 +1,27 @@
<div>
<?php
/*
* A simple button to add a link to a specific section
*
* Expected input:
* { url: <relative url>, text: <text to be displayed on the button>}
*
* Example:
* {url: "/events/index", text: "To the list of events"}
*
*/
echo '<a href="'.$baseurl.h($data['url']).'">';
echo '<button class="btn btn-primary widget-button">';
echo h($data['text']);
echo '</button></a>';
?>
</div>
<style widget-scoped>
.widget-button {
height: 100%;
width: 100%;
text-align: center;
font-size: large;
}
</style>

View File

@ -130,6 +130,11 @@
<span class="<?= $this->FontAwesome->getClass('quote-left') ?> useCursorPointer icon" onclick="replacementAction('quote')" title="<?= __('Quote') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('code') ?> useCursorPointer icon" onclick="replacementAction('code')" title="<?= __('Code') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('table') ?> useCursorPointer icon" onclick="replacementAction('table')" title="<?= __('Table') ?>"></span>
<span style="position: absolute;right: 10px;">
<button id="cancelMarkdownButton" type="button" class="btn btn-mini" onclick="cancelEdit()">
<?= __('Cancel') ?>
</button>
</span>
</div>
<textarea id="editor"></textarea>
<div id="bottom-bar" class="editor-action-bar">

View File

@ -0,0 +1,28 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'data' => array(
'title' => __('Automatic entities extraction'),
'description' => __('Automatically extracting entities from a report will use the freetext import tools to extract and create attributes from the report.'),
'model' => 'EventReport',
'fields' => array(
sprintf('<h5>%s</h5>', __('Post extraction actions:')),
array(
'label' => __('Tag the event with contextual elements found in the report'),
'field' => 'tag_event',
'type' => 'checkbox',
'div' => array('class' => 'checkbox')
),
array(
'field' => 'id',
'default' => $reportId,
'type' => 'hidden'
)
),
'submit' => array(
'action' => $this->request->params['action'],
'ajaxSubmit' => sprintf('submitPopoverForm(\'%s\', \'extractAllFromReport\', 0, 1)', h($reportId))
),
)
));

View File

@ -0,0 +1,21 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'data' => array(
'title' => __('Import from URL (event %s)', h($event_id)),
'model' => 'EventReport',
'fields' => array(
array(
'type' => 'textarea',
'field' => 'url',
'class' => 'input span6',
'div' => 'text',
'label' => sprintf('<b>%s:</b> ', __('URL')) . __('Content for this URL will be downloaded and converted to Mardown')
),
),
'submit' => array(
'action' => $this->request->params['action'],
'ajaxSubmit' => sprintf('submitPopoverForm(\'%s\', \'addEventReport\', 0, 1)', h($event_id))
)
)
));

View File

@ -6,6 +6,11 @@
<button class="btn btn-small btn-primary" onclick="openGenericModal(baseurl + '/eventReports/add/<?= h($event_id) ?>')">
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add Event Report') ?>
</button>
<?php if ($importModuleEnabled): ?>
<button class="btn btn-small btn-primary" onclick="openGenericModal(baseurl + '/eventReports/importReportFromUrl/<?= h($event_id) ?>')" title="<?= __('Content for this URL will be downloaded and converted to Mardown') ?>">
<i class="<?= $this->FontAwesome->getClass('link') ?>"></i> <?= __('Import from URL') ?>
</button>
<?php endif; ?>
</div>
<?php
echo $this->element('/genericElements/IndexTable/index_table', array(
@ -85,13 +90,6 @@
'icon' => 'eye',
'dbclickAction' => true
),
array(
'url' => '/eventReports/edit',
'url_params_data_paths' => array(
'EventReport.id'
),
'icon' => 'edit'
),
array(
'title' => __('Delete'),
'icon' => 'trash',

View File

@ -0,0 +1,19 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'data' => array(
'title' => __('Replace suggestions in report'),
'model' => 'EventReport',
'fields' => array(
array(
'field' => 'suggestions',
'class' => 'textarea'
),
),
'submit' => array(
'action' => $this->request->params['action'],
'ajaxSubmit' => ''
),
)
));
?>

View File

@ -144,7 +144,7 @@
?></pre>
<code>&lt;request&gt;&lt;type&gt;ip&lt;/type&gt;&lt;eventid&gt;!51&lt;/eventid&gt;&lt;eventid&gt;!62&lt;/eventid&gt;&lt;withAttachment&gt;false&lt;/withAttachment&gt;&lt;tags&gt;APT1&lt;/tags&gt;&lt;tags&gt;!OSINT&lt;/tags&gt;&lt;from&gt;false&lt;/from&gt;&lt;to&gt;2015-02-15&lt;/to&gt;&lt;/request&gt;</code><br /><br />
<p><?php echo __('Alternatively, it is also possible to pass the filters via the parameters in the URL, though it is highly advised to use POST requests with JSON objects instead. The format is as described below');?>:</p>
<pre><?php echo $baseurl.'/attributes/bro/download/[type]/[tags]/[event_id]/[allowNonIDS]/[from]/[to]/[last]'; ?></pre>
<pre><?php echo $baseurl.'/attributes/bro/download/[type]/[tags]/[event_id]/[from]/[to]/[last]'; ?></pre>
<b>type</b>: <?php echo __('The Bro type, any valid Bro type is accepted. The mapping between Bro and MISP types is as follows');?>:<br />
<pre><?php
foreach ($broTypes as $key => $value) {

View File

@ -374,7 +374,7 @@
endif;
if (!empty($event['Feed']) || !empty($event['Event']['FeedCount'])):
?>
<h3>Related Feeds</h3>
<h3><?= __('Related Feeds') ?> <a href="#attributeList" title="<?= __('Show just attributes that has feed hits') ?>" onclick="toggleBoolFilter('<?= $baseurl ?>/events/view/<?= h($event['Event']['id']) ?>', 'feed')"><?= __('(show)') ?></a></h3>
<?php
if (!empty($event['Feed'])):
?>
@ -382,10 +382,9 @@
<?php
foreach ($event['Feed'] as $relatedFeed):
$relatedData = array(
'Name' => $relatedFeed['name'],
'URL' => $relatedFeed['url'],
'Provider' => $relatedFeed['provider'],
'Source Format' => $relatedFeed['source_format'] === 'misp' ? 'MISP' : $relatedFeed['source_format'],
__('Name') => $relatedFeed['name'],
__('URL') => $relatedFeed['url'],
__('Provider') => $relatedFeed['provider'],
);
$popover = '';
foreach ($relatedData as $k => $v) {
@ -397,13 +396,13 @@
if ($relatedFeed ['source_format'] === 'misp'):
?>
<form action="<?php echo $baseurl; ?>/feeds/previewIndex/<?php echo h($relatedFeed['id']); ?>" method="post" style="margin:0px;">
<input type="hidden" name="data[Feed][eventid]" value="<?php echo h(json_encode($relatedFeed['event_uuids'], true)); ?>">
<input type="submit" class="linkButton useCursorPointer" value="<?php echo h($relatedFeed['name']) . ' (' . $relatedFeed['id'] . ')'; ?>" data-toggle="popover" data-content="<?php echo h($popover); ?>" data-trigger="hover" />
<input type="hidden" name="data[Feed][eventid]" value="<?php echo h(json_encode($relatedFeed['event_uuids'])); ?>">
<input type="submit" class="linkButton useCursorPointer" value="<?php echo h($relatedFeed['name']) . ' (' . $relatedFeed['id'] . ')'; ?>" data-toggle="popover" data-content="<?php echo h($popover); ?>" data-trigger="hover">
</form>
<?php
else:
?>
<a href="<?php echo $baseurl; ?>/feeds/previewIndex/<?php echo h($relatedFeed['id']); ?>" data-toggle="popover" data-content="<?php echo h($popover); ?>" data-trigger="hover"><?php echo h($relatedFeed['name']) . ' (' . $relatedFeed['id'] . ')'; ?></a><br />
<a href="<?php echo $baseurl; ?>/feeds/previewIndex/<?php echo h($relatedFeed['id']); ?>" data-toggle="popover" data-content="<?php echo h($popover); ?>" data-trigger="hover"><?php echo h($relatedFeed['name']) . ' (' . $relatedFeed['id'] . ')'; ?></a><br>
<?php
endif;
?>
@ -458,7 +457,10 @@
?>
<?php if (!empty($event['warnings'])): ?>
<div class="warning_container">
<h4 class="red"><?= __('Warning: Potential false positives') ?> <a href="#attributeList" onclick="toggleBoolFilter('<?= $baseurl ?>/events/view/<?= h($event['Event']['id']) ?>', 'warning')"><?= __('(show)') ?></a></h4>
<h4 class="red">
<?= __('Warning: Potential false positives') ?>
<a href="#attributeList" title="<?= __('Show just attributes that has warning') ?>" onclick="toggleBoolFilter('<?= $baseurl ?>/events/view/<?= h($event['Event']['id']) ?>', 'warning')"><?= __('(show)') ?></a>
</h4>
<?php
$links = [];
foreach ($event['warnings'] as $id => $name) {

View File

@ -1,6 +1,6 @@
<?php
$table_data = array();
$table_data[] = array('key' => __('Id'), 'value' => $feed['Feed']['id']);
$table_data[] = array('key' => __('ID'), 'value' => $feed['Feed']['id']);
$table_data[] = array('key' => __('Name'), 'value' => $feed['Feed']['name']);
$table_data[] = array('key' => __('URL'), 'value' => $feed['Feed']['url']);
$table_data[] = array(
@ -19,7 +19,7 @@
$this->element(
'ajaxTags',
array(
'event' => false,
'scope' => 'feed',
'tags' => array(array('Tag' => $feed['Tag'])),
'tagAccess' => false,
'static_tags_only' => true
@ -74,13 +74,12 @@
// $table_data[] = array('key' => __('Role'), 'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id'])));
echo sprintf(
'<div class="feeds view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s<hr /><div class="feed_overlap_tool">%s</div></div></div></div>%s',
'<div class="feeds view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s<hr /><div class="feed_overlap_tool">%s</div></div></div></div>',
sprintf(
'<h2>%s</h2>%s',
__('Feed'),
$this->element('genericElements/viewMetaTable', array('table_data' => $table_data))
),
$this->element('Feeds/View/feed_overlap_tool', array('other_feeds' => $other_feeds, 'feed' => $feed)),
$this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'feeds', 'menuItem' => 'view'))
$this->element('Feeds/View/feed_overlap_tool', array('other_feeds' => $other_feeds, 'feed' => $feed))
);
?>
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'feeds', 'menuItem' => 'view'));

@ -1 +1 @@
Subproject commit b41e3d4f5037d0d325d38b2d5cf4f4bc94274cbc
Subproject commit b56a4d9e5c84e5f3aa10bd852c83e199f327391d

@ -1 +1 @@
Subproject commit 27a554ab12acbc1242f801b5682364b2047cf9e0
Subproject commit abf42cc8fb71c003c40dc0767f89804b45eb5303

@ -1 +1 @@
Subproject commit 278f726f1dbc7fab2607dd6618453559e6b5983f
Subproject commit 8d60a4f5052c4e303ac3f7f90b3ee14f655f88fd

View File

@ -6,6 +6,19 @@
.misp-element-wrapper.object .obj-value {
color: black !important;
}
.misp-element-wrapper.attribute .attr-value > span {
max-width: unset !important;
white-space: normal !important;
}
.misp-element-wrapper.object .obj-value > span {
max-width: unset !important;
white-space: normal !important;
}
span.misp-element-wrapper.attribute {
max-width: unset !important;
white-space: normal !important;
}
}
span.misp-element-wrapper {
@ -21,6 +34,25 @@ span.misp-element-wrapper {
cursor: help;
}
.misp-element-wrapper.suggestion {
cursor: pointer;
user-select: none;
border-color: #333 !important;
}
.misp-element-wrapper.suggestion input[type="checkbox"] {
margin: 0 3px;
}
.misp-element-wrapper.attribute.suggestion.picked {
box-shadow: #2f96b4 0px 0px 5px 0px;
}
.misp-element-wrapper.attribute.suggestion.picked .attr-type {
background-color: #2f96b4 !important;
color: white;
}
.misp-element-wrapper.invalid {
cursor: not-allowed;
}
@ -35,6 +67,12 @@ span.misp-element-wrapper {
display: inline-block;
}
.misp-element-wrapper.attribute.suggestion .attr-type {
color: white;
background-color: #333 !important;
border-right: 1px solid #333 !important;
}
.misp-element-wrapper.attribute .attr-type > span {
margin: 2px 3px;
}
@ -93,6 +131,18 @@ span.misp-element-wrapper.object {
max-width: 90%;
}
#suggestion-container ul.nav-tabs {
margin-bottom: 0;
}
#suggestion-container div.tab-content {
height: calc(100vh - 120px - 38px) !important;
}
.markdownEditor-full-container:fullscreen #suggestion-container div.tab-content {
height: calc(100vh - 50px - 38px) !important;
}
.CodeMirror-hint-active .blue {
color: white !important;
}

View File

@ -13,6 +13,10 @@
left: 0;
top: 0;
}
div.markdownEditor-full-container {
position: unset !important;
}
}
.markdownEditor-full-container {

File diff suppressed because it is too large Load Diff

View File

@ -155,6 +155,13 @@ function initCodeMirror() {
}
if (typeof cmCustomHints === 'function') {
cmOptions['hintOptions']['hint'] = cmCustomHints
// cmOptions['hintOptions']['hint'] = function (cm, options) {
// var result = cmCustomHints(cm, options);
// if (result) {
// CodeMirror.on(result, 'shown', function() {})
// }
// return result;
// }
}
cm = CodeMirror.fromTextArea($editor[0], cmOptions);
cm.on('changes', function(cm, event) {
@ -365,12 +372,13 @@ function setEditorData(data) {
cm.setValue(data)
}
function saveMarkdown() {
function saveMarkdown(confirmSave, callback) {
confirmSave = confirmSave === undefined ? true : confirmSave
if (modelNameForSave === undefined || markdownModelFieldNameForSave === undefined) {
console.log('Model or field not defined. Save not possible')
return
}
if (!confirm(saveConfirmMessage)) {
if (confirmSave && !confirm(saveConfirmMessage)) {
return
}
var url = baseurl + "/eventReports/edit/" + reportid
@ -404,6 +412,9 @@ function saveMarkdown() {
$('#temp').remove();
toggleLoadingInSaveButton(false)
$editor.prop('disabled', false);
if (callback !== undefined) {
callback()
}
},
type:"post",
url: formUrl
@ -411,6 +422,11 @@ function saveMarkdown() {
})
}
function cancelEdit() {
setEditorData(originalRaw);
setMode('viewer')
}
function downloadMarkdown(type) {
var content, fileType, baseName, extension
if (type == 'pdf') {
@ -575,6 +591,10 @@ function baseReplacementAction(action) {
cm.focus()
}
function setCMReadOnly(readonly) {
cm.setOption('readOnly', readonly)
}
function insertTopToolbarSection() {
$topBar.append($('<i />').addClass('top-bar-separator'))
}

View File

@ -118,7 +118,7 @@ function genericPopup(url, popupTarget, callback) {
if (callback !== undefined) {
callback();
}
});
}).fail(xhrFailCallback)
}
function screenshotPopup(url, title) {
@ -2523,33 +2523,42 @@ function serverSettingsPostActivationScripts(name, setting, id) {
}
function serverSettingSubmitForm(name, setting, id) {
subGroup = getSubGroupFromSetting(setting);
var subGroup = getSubGroupFromSetting(setting);
var formData = $(name + '_field').closest("form").serialize();
$.ajax({
data: formData,
cache: false,
beforeSend: function (XMLHttpRequest) {
beforeSend: function () {
$(".loading").show();
},
success:function (data, textStatus) {
success: function (data) {
if (!data.saved) {
$(".loading").hide();
showMessage('fail', data.errors);
resetForms();
$('.inline-field-placeholder').hide();
return;
}
$.ajax({
type:"get",
type: "get",
url: baseurl + "/servers/serverSettingsReloadSetting/" + setting + "/" + id,
success:function (data2, textStatus2) {
success: function (data2) {
$('#' + subGroup + "_" + id + '_row').replaceWith(data2);
$(".loading").hide();
},
error:function() {
error: function() {
showMessage('fail', 'Could not refresh the table.');
}
});
},
error:function() {
error: function() {
$(".loading").hide();
showMessage('fail', 'Request failed for an unknown reason.');
resetForms();
$('.inline-field-placeholder').hide();
},
type:"post",
type: "post",
url: baseurl + "/servers/serverSettingsEdit/" + setting + "/" + id + "/" + 1
});
$(name + '_field').unbind("keyup");

@ -1 +1 @@
Subproject commit 0d6db44c80afd81976f54f58c8cb02e4d33acc16
Subproject commit 52d806b349333d40c5dd75b62e8e64d6a18fcdf4