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

pull/7125/head
mokaddem 2021-03-01 15:59:13 +01:00
commit 9fa273ed1f
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
27 changed files with 226 additions and 92 deletions

View File

@ -671,4 +671,11 @@ class AdminShell extends AppShell
$this->Job->saveField('message', 'Job done.');
$this->Job->saveField('status', 4);
}
public function updatesDone()
{
$blocking = !empty($this->args[0]);
$done = $this->AdminSetting->updatesDone($blocking);
$this->out($done ? 'True' : 'False');
}
}

View File

@ -123,6 +123,9 @@ class AppController extends Controller
if (Configure::read('Security.disable_browser_cache')) {
$this->response->disableCache();
}
if (!$this->_isRest()) {
$this->__contentSecurityPolicy();
}
$this->response->header('X-XSS-Protection', '1; mode=block');
if (!empty($this->params['named']['sql'])) {
@ -314,11 +317,11 @@ class AppController extends Controller
$this->__accessMonitor($user);
} else {
$pre_auth_actions = array('login', 'register', 'getGpgPublicKey');
$preAuthActions = array('login', 'register', 'getGpgPublicKey');
if (!empty(Configure::read('Security.email_otp_enabled'))) {
$pre_auth_actions[] = 'email_otp';
$preAuthActions[] = 'email_otp';
}
if (!$this->_isControllerAction(['users' => $pre_auth_actions])) {
if (!$this->_isControllerAction(['users' => $preAuthActions, 'servers' => ['cspReport']])) {
if (!$this->request->is('ajax')) {
$this->Session->write('pre_login_requested_url', $this->here);
}
@ -685,6 +688,50 @@ class AppController extends Controller
}
}
/**
* Generate Content-Security-Policy HTTP header
*/
private function __contentSecurityPolicy()
{
$default = [
'default-src' => "'self' data: 'unsafe-inline' 'unsafe-eval'",
'style-src' => "'self' 'unsafe-inline'",
'object-src' => "'none'",
'frame-ancestors' => "'none'",
'worker-src' => "'none'",
'child-src' => "'none'",
'frame-src' => "'none'",
'base-uri' => "'self'",
'img-src' => "'self' data:",
'font-src' => "'self'",
'form-action' => "'self'",
'connect-src' => "'self'",
'manifest-src' => "'none'",
'report-uri' => '/servers/cspReport',
];
if (env('HTTPS')) {
$default['upgrade-insecure-requests'] = null;
}
$custom = Configure::read('Security.csp');
if ($custom === false) {
return;
}
if (is_array($custom)) {
$default = $default + $custom;
}
$header = [];
foreach ($default as $key => $value) {
if ($value !== false) {
if ($value === null) {
$header[] = $key;
} else {
$header[] = "$key $value";
}
}
}
$this->response->header('Content-Security-Policy', implode('; ', $header));
}
private function __rateLimitCheck()
{
$info = array();

View File

@ -533,6 +533,7 @@ class ACLComponent extends Component
'uploadFile' => array(),
'viewDeprecatedFunctionUse' => array(),
'killAllWorkers' => ['perm_site_admin'],
'cspReport' => ['*'],
),
'shadowAttributes' => array(
'accept' => array('perm_add'),

View File

@ -767,7 +767,7 @@ class EventsController extends AppController
}
}
}
$events = $this->GalaxyCluster->attachClustersToEventIndex($this->Auth->user(), $events, false, false);
$events = $this->GalaxyCluster->attachClustersToEventIndex($this->Auth->user(), $events, false);
}
foreach ($events as $key => $event) {
if (empty($event['SharingGroup']['name'])) {
@ -830,7 +830,7 @@ class EventsController extends AppController
if (Configure::read('MISP.showDiscussionsCountOnIndex')) {
$events = $this->Event->attachDiscussionsCountToEvents($this->Auth->user(), $events);
}
$events = $this->GalaxyCluster->attachClustersToEventIndex($this->Auth->user(), $events, true, false);
$events = $this->GalaxyCluster->attachClustersToEventIndex($this->Auth->user(), $events, true);
if ($this->params['ext'] === 'csv') {
App::uses('CsvExport', 'Export');

View File

@ -35,8 +35,11 @@ class ServersController extends AppController
public function beforeFilter()
{
$this->Auth->allow(['cspReport']); // cspReport must work without authentication
parent::beforeFilter();
$this->Security->unlockedActions[] = 'getApiInfo';
$this->Security->unlockedActions[] = 'cspReport';
// permit reuse of CSRF tokens on some pages.
switch ($this->request->params['action']) {
case 'push':
@ -2419,6 +2422,27 @@ misp.direct_call(relative_path, body)
}
}
public function cspReport()
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException('This action expects a POST request.');
}
$report = $this->Server->jsonDecode($this->request->input());
if (!isset($report['csp-report'])) {
throw new RuntimeException("Invalid report");
}
$message = 'CSP reported violation';
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR';
if (isset($_SERVER[$ipHeader])) {
$message .= ' from IP ' . $_SERVER[$ipHeader];
}
$this->log("$message: " . json_encode($report['csp-report'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return new CakeResponse(['statusCodes' => 204]);
}
public function viewDeprecatedFunctionUse()
{
$data = $this->Deprecation->getDeprecatedAccessList($this->Server);

View File

@ -1137,7 +1137,7 @@ class UsersController extends AppController
}
if ($this->request->is('post') && Configure::read('Security.email_otp_enabled')) {
$user = $this->Auth->identify($this->request, $this->response);
if ($user) {
if ($user && !$user['disabled']) {
$this->Session->write('email_otp_user', $user);
return $this->redirect('email_otp');
}

View File

@ -43,4 +43,21 @@ class AdminSetting extends AppModel
return false;
}
}
public function updatesDone($blocking = false)
{
if ($blocking) {
$continue = false;
while ($continue == false) {
$db_version = $this->find('first', array('conditions' => array('setting' => 'db_version')));
$continue = empty($this->findUpgrades($db_version['AdminSetting']['value']));
}
return true;
} else {
$db_version = $this->find('first', array('conditions' => array('setting' => 'db_version')));
return empty($this->findUpgrades($db_version['AdminSetting']['value']));
}
}
}

View File

@ -2004,7 +2004,7 @@ class AppModel extends Model
}
}
$db_version = $db_version[0];
$updates = $this->__findUpgrades($db_version['AdminSetting']['value']);
$updates = $this->findUpgrades($db_version['AdminSetting']['value']);
if ($processId) {
$job = $this->Job->find('first', array(
'conditions' => array('Job.id' => $processId)
@ -2349,7 +2349,7 @@ class AppModel extends Model
}
}
private function __findUpgrades($db_version)
public function findUpgrades($db_version)
{
$updates = array();
if (strpos($db_version, '.')) {

View File

@ -463,15 +463,17 @@ class EventReport extends AppModel
$objects = [];
$templateConditions = [];
foreach ($event['Object'] as $k => $object) {
foreach ($object['Attribute'] as &$objectAttribute) {
unset($objectAttribute['ShadowAttribute']);
$objectAttribute['object_uuid'] = $object['uuid'];
$attributes[$objectAttribute['uuid']] = $objectAttribute;
if (isset($object['Attribute'])) {
foreach ($object['Attribute'] as &$objectAttribute) {
unset($objectAttribute['ShadowAttribute']);
$objectAttribute['object_uuid'] = $object['uuid'];
$attributes[$objectAttribute['uuid']] = $objectAttribute;
foreach ($objectAttribute['AttributeTag'] as $at) {
$allTagNames[$at['Tag']['name']] = $at['Tag'];
foreach ($objectAttribute['AttributeTag'] as $at) {
$allTagNames[$at['Tag']['name']] = $at['Tag'];
}
$this->Event->Attribute->removeGalaxyClusterTags($objectAttribute);
}
$this->Event->Attribute->removeGalaxyClusterTags($objectAttribute);
}
$objects[$object['uuid']] = $object;

View File

@ -1442,17 +1442,16 @@ class GalaxyCluster extends AppModel
/**
* @param array $user
* @param array $events
* @param bool $replace
* @param bool $fetchFullCluster
* @param bool $replace Remove galaxy cluster tags
* @return array
*/
public function attachClustersToEventIndex(array $user, array $events, $replace = false, $fetchFullCluster = true)
public function attachClustersToEventIndex(array $user, array $events, $replace = false)
{
$clusterTagNames = [];
foreach ($events as $event) {
foreach ($event['EventTag'] as $eventTag) {
if ($eventTag['Tag']['is_galaxy']) {
$clusterTagNames[strtolower($eventTag['Tag']['name'])] = true;
$clusterTagNames[$eventTag['Tag']['id']] = strtolower($eventTag['Tag']['name']);
}
}
}
@ -1462,12 +1461,10 @@ class GalaxyCluster extends AppModel
}
$options = [
'conditions' => ['LOWER(GalaxyCluster.tag_name)' => array_keys($clusterTagNames)],
'conditions' => ['LOWER(GalaxyCluster.tag_name)' => $clusterTagNames],
'contain' => ['Galaxy', 'GalaxyElement'],
];
if (!$fetchFullCluster) {
$options['contain'] = ['Galaxy'];
}
$clusters = $this->fetchGalaxyClusters($user, $options, $fetchFullCluster);
$clusters = $this->fetchGalaxyClusters($user, $options);
$clustersByTagName = [];
foreach ($clusters as $cluster) {
@ -1482,7 +1479,6 @@ class GalaxyCluster extends AppModel
$tagName = strtolower($eventTag['Tag']['name']);
if (isset($clustersByTagName[$tagName])) {
$cluster = $this->postprocess($clustersByTagName[$tagName], $eventTag['Tag']['id']);
$cluster['GalaxyCluster']['tag_id'] = $eventTag['Tag']['id'];
$cluster['GalaxyCluster']['local'] = $eventTag['local'];
$events[$k]['GalaxyCluster'][] = $cluster['GalaxyCluster'];
if ($replace) {

View File

@ -153,7 +153,6 @@ class SharingGroup extends AppModel
} else {
return array();
}
if ($scope === 'full') {
$sgs = $this->find('all', array(
'contain' => array('SharingGroupServer' => array('Server'), 'SharingGroupOrg' => array('Organisation'), 'Organisation'),
@ -164,7 +163,9 @@ class SharingGroup extends AppModel
} elseif ($scope === 'simplified') {
$fieldsOrg = array('id', 'name', 'uuid');
$fieldsServer = array('id', 'url', 'name');
$permissionTree = ($user['Role']['perm_site_admin'] || $user['Role']['perm_sync']) ? 1 : 0;
//$permissionTree = ($user['Role']['perm_site_admin'] || $user['Role']['perm_sync']) ? 1 : 0;
//Temporary fix: read only users used for pulling were stripping organisation data from sharing groups
$permissionTree = 1;
$fieldsSharingGroup = array(
array(
'fields' => array(
@ -604,18 +605,18 @@ class SharingGroup extends AppModel
$authorisedToSave = $this->checkIfAuthorisedToSave($user, $sg);
}
if (!$user['Role']['perm_site_admin'] &&
!($user['Role']['perm_sync'] && $syncLocal ) &&
!($user['Role']['perm_sync'] && $syncLocal) &&
!$authorisedToSave
) {
$this->Log->create();
$entry = array(
'org' => $user['Organisation']['name'],
'model' => 'SharingGroup',
'model_id' => $sg['SharingGroup']['uuid'],
'email' => $user['email'],
'action' => 'error',
'user_id' => $user['id'],
'title' => 'Tried to save a sharing group but the user does not belong to it.'
'org' => $user['Organisation']['name'],
'model' => 'SharingGroup',
'model_id' => 0,
'email' => $user['email'],
'action' => 'error',
'user_id' => $user['id'],
'title' => "Tried to save a sharing group with UUID '{$sg['SharingGroup']['uuid']}' but the user does not belong to it."
);
$this->Log->save($entry);
return false;

View File

@ -692,7 +692,7 @@ class Taxonomy extends AppModel
*/
public function splitTagToComponents($tag)
{
preg_match('/^([^:="]+):([^:="]+)(="([^:="]+"))?$/i', $tag, $matches);
preg_match('/^([^:="]+):([^:="]+)(="([^:="]+)")?$/i', $tag, $matches);
if (empty($matches)) {
return null; // tag is not in taxonomy format
}

View File

@ -63,18 +63,18 @@ class OidcAuthenticate extends BaseAuthenticate
}
if ($user) {
$this->log($mispUsername, 'Found in database.');
$this->log($mispUsername, "Found in database with ID {$user['id']}.");
if ($user['org_id'] != $organisationId) {
$user['org_id'] = $organisationId;
$this->userModel()->updateField($user, 'org_id', $organisationId);
$this->log($mispUsername, "User organisation changed from ${user['org_id']} to $organisationId.");
$this->log($mispUsername, "User organisation changed from {$user['org_id']} to $organisationId.");
}
if ($user['role_id'] != $roleId) {
$user['role_id'] = $roleId;
$this->userModel()->updateField($user, 'role_id', $roleId);
$this->log($mispUsername, "User role changed from ${user['role_id']} to $roleId.");
$this->log($mispUsername, "User role changed from {$user['role_id']} to $roleId.");
}
$this->log($mispUsername, 'Logged in.');
@ -171,6 +171,7 @@ class OidcAuthenticate extends BaseAuthenticate
*/
private function getUserRole(array $roles, $mispUsername)
{
$this->log($mispUsername, 'Provided roles: ' . implode(', ', $roles));
$roleMapper = $this->getConfig('role_mapper');
if (!is_array($roleMapper)) {
throw new RuntimeException("Config option `OidcAuth.role_mapper` must be array.");
@ -194,13 +195,11 @@ class OidcAuthenticate extends BaseAuthenticate
continue;
}
}
if ($userRole === null || $roleId < $userRole) { // role with lower ID wins
$userRole = $roleId;
}
return $roleId; // first match wins
}
}
return $userRole;
return null;
}
/**

View File

@ -0,0 +1,49 @@
# MISP OpenID Connect Authentication
This plugin provides ability to use OpenID as Single sign-on for login users to MISP.
When plugin is enabled, users are direcly redirected to SSO provider and it is not possible
to login with passwords stored in MISP.
## Usage
1. Install required library using composer
```
cd app
php composer.phar require jumbojett/openid-connect-php
```
2. Enable in `app/Config/config.php`
```php
$config = array(
...
'Security' => array(
...
'auth' => 'array('OidcAuth.Oidc')',
),
...
```
3. Configure in `app/Config/config.php` (replace variables in `{{ }}` with your values)
```php
$config = array(
...
'OidcAuth' = [
'provider_url' => '{{ OIDC_PROVIDER }}',
'client_id' => '{{ OIDC_CLIENT_ID }}',
'client_secret' => '{{ OIDC_CLIENT_SECRET }}',
'role_mapper' => [ // if user has multiple roles, first role that match will be assigned to user
'misp-user' => 3, // User
'misp-admin' => 1, // Admin
],
'default_org' => '{{ MISP_ORG }}',
],
...
```
## Caveats
* When user is blocked in SSO (IdM), he/she will be not blocked in MISP. He could not log in, but users authentication keys will still work and also he/she will still receive all emails.

View File

@ -35,13 +35,15 @@
}
}
}
$paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : [];
if ($ajax && isset($containerId)) {
$paginationData['data-paginator'] = "#{$containerId}_content";
}
$this->Paginator->options($paginationData);
$skipPagination = isset($data['skip_pagination']) ? $data['skip_pagination'] : 0;
if (!$skipPagination) {
$paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : array();
if ($ajax && isset($containerId)) {
$paginationData['data-paginator'] = "#{$containerId}_content";
}
$this->Paginator->options($paginationData);
$paginatonLinks = $this->element('/genericElements/IndexTable/pagination_links');
echo $paginatonLinks;
}

View File

@ -170,10 +170,11 @@
'text' => __('List Noticelists'),
'url' => $baseurl . '/noticelists/index'
),
[
array(
'text' => __('List Correlation Exclusions'),
'url' => $baseurl . '/correlation_exclusions/index'
]
'url' => $baseurl . '/correlation_exclusions/index',
'requirement' => $canAccess('correlation_exclusions', 'index'),
)
)
),
array(

View File

@ -4,9 +4,7 @@
<?php endif; ?>
<?php
echo $this->element('/genericElements/IndexTable/index_table', array(
'paginatorOptions' => array(
'update' => '#eventreport_index_div',
),
'containerId' => 'eventreport',
'data' => array(
'data' => $reports,
'top_bar' => array(
@ -165,7 +163,7 @@
$('#eventReportSelectors a.btn').click(function(e) {
e.preventDefault()
$("#eventreport_index_div").empty()
$("#eventreport_content").empty()
.append(
$('<div></div>')
.css({'text-align': 'center', 'font-size': 'large', 'margin': '5px 0'})
@ -173,7 +171,7 @@
)
var url = $(this).attr('href')
$.get(url, function(data) {
$("#eventreport_index_div").html(data);
$("#eventreport_content").html(data);
});
});
})
@ -183,7 +181,7 @@
$.ajax({
dataType: "html",
beforeSend: function() {
$("#eventreport_index_div").empty()
$("#eventreport_content").empty()
.append(
$('<div></div>')
.css({'text-align': 'center', 'font-size': 'large', 'margin': '5px 0'})
@ -191,10 +189,10 @@
)
},
success:function (data) {
$("#eventreport_index_div").html(data);
$("#eventreport_content").html(data);
},
error: function (jqXHR, textStatus, errorThrown) {
$("#eventreport_index_div").empty().text('<?= __('Failed to load Event report table')?>')
$("#eventreport_content").empty().text('<?= __('Failed to load Event report table')?>')
showMessage('fail', textStatus + ": " + errorThrown);
},
url:url

View File

@ -527,7 +527,7 @@
</div>
<div id="eventreport_div" style="display: none;">
<span class="report-title-section"><?php echo __('Event Reports');?></span>
<div id="eventreport_index_div"></div>
<div id="eventreport_content"></div>
</div>
<div id="clusterrelation_div" class="info_container_eventgraph_network" style="display: none;" data-fullscreen="false">
</div>
@ -554,8 +554,8 @@ $(function () {
});
$.get("<?php echo $baseurl; ?>/eventReports/index/event_id:<?= h($event['Event']['id']); ?>/index_for_event:1<?= $extended ? '/extended_event:1' : ''?>", function(data) {
$("#eventreport_index_div").html(data);
if ($('#eventreport_index_div table tbody > tr').length) { // open if contain a report
$("#eventreport_content").html(data);
if ($('#eventreport_content table tbody > tr').length) { // open if contain a report
$('#eventreport_toggle').click()
}
});

View File

@ -96,14 +96,12 @@
<div class="row-fuild">
<div id="relations_container"></div>
</div>
<div class="">
<div id="elements_div"></div>
</div>
<div id="elements_content"></div>
</div>
<script type="text/javascript">
$(function () {
$.get("<?= $baseurl ?>/galaxy_elements/index/<?php echo $cluster['GalaxyCluster']['id']; ?>", function(data) {
$("#elements_div").html(data);
$("#elements_content").html(data);
});
$.get("<?= $baseurl ?>/galaxy_clusters/viewGalaxyMatrix/<?php echo $cluster['GalaxyCluster']['id']; ?>", function(data) {
$("#matrix_container").html(data);

View File

@ -1,9 +1,7 @@
<?php
$indexOptions = array(
'containerId' => 'elements',
'data' => array(
'paginatorOptions' => array(
'update' => '#elements_div',
),
'data' => $elements,
'top_bar' => array(
'children' => array(
@ -16,7 +14,7 @@ $indexOptions = array(
'onClickParams' => [
h($clusterId) . '/context:all',
$baseurl . '/galaxy_elements/index',
'#elements_div'
'#elements_content'
],
),
array(
@ -26,7 +24,7 @@ $indexOptions = array(
'onClickParams' => [
h($clusterId) . '/context:JSONView',
$baseurl . '/galaxy_elements/index',
'#elements_div'
'#elements_content'
],
),
)
@ -87,8 +85,6 @@ echo $this->element('/genericElements/IndexTable/index_table', $indexOptions);
if ($context == 'JSONView') {
echo sprintf('<div id="elementJSONDiv" class="well well-small">%s</div>', json_encode($JSONElements));
}
echo $this->Js->writeBuffer();
?>
<script>
@ -96,4 +92,4 @@ echo $this->Js->writeBuffer();
if ($jsondiv.length > 0) {
$jsondiv.html(syntaxHighlightJson($jsondiv.text(), 8));
}
</script>
</script>

View File

@ -2,7 +2,8 @@
<?php echo $this->Form->create('OrgBlocklist');?>
<fieldset>
<legend><?php echo __('Add Organisation Blocklist Entries');?></legend>
<p><?php echo __('Simply paste a list of all the organisation UUIDs that you wish to block from being entered.');?></p>
<p><?php echo __('Blocklisting an organisation prevents the creation of any event by that organisation on this instance as well as syncing of that organisation\'s events to this instance. It does not prevent a local user of the blocklisted organisation from logging in and editing or viewing data.');?></p>
<p><?php echo __('Paste a list of all the organisation UUIDs that you want to add to the blocklist below (one per line).');?></p>
<?php
echo $this->Form->input('uuids', array(
'type' => 'textarea',

View File

@ -1,8 +1,7 @@
<div class="orgBlocklist form">
<?php echo $this->Form->create('OrgBlocklist');?>
<fieldset>
<legend><?php echo __('Edit Event Blocklist Entries');?></legend>
<p><?php echo __('List of all the event UUIDs that you wish to block from being entered.');?></p>
<legend><?php echo __('Edit Organisation Blocklist Entries');?></legend>
<?php
echo $this->Form->input('uuids', array(
'type' => 'textarea',

View File

@ -29,7 +29,7 @@
$class = 'warning';
}
$versionText = __('Outdated version');
$versionText .= sprintf(_(' (%s days, %s hours older than super project)'), $status['timeDiff']->format('%a'), $status['timeDiff']->format('%h'));
$versionText .= sprintf(__(' (%s days, %s hours older than super project)'), $status['timeDiff']->format('%a'), $status['timeDiff']->format('%h'));
break;
case 'younger':
$class = 'warning';
@ -65,7 +65,7 @@
</tbody>
</table>
<div id="submoduleGitResultDiv" class="hidden">
<strong><?php echo _('Update result:'); ?></strong>
<strong><?php echo __('Update result:'); ?></strong>
<div class="apply_css_arrow">
<pre id="submoduleGitResult" class="green bold" style="margin-left: 10px;"></pre>
</div>

View File

@ -29,7 +29,8 @@
"ext-bcmath": "For faster validating IBAN numbers",
"ext-rdkafka": "Required for publishing events to Kafka broker",
"elasticsearch/elasticsearch": "For logging to elasticsearch",
"aws/aws-sdk-php": "To upload samples to S3"
"aws/aws-sdk-php": "To upload samples to S3",
"jumbojett/openid-connect-php": "For OIDC authentication"
},
"config": {
"vendor-dir": "Vendor",

@ -1 +1 @@
Subproject commit 36994fda1ef423e6141d2a3582fed3e6219dbf59
Subproject commit e764ed6983bac3a3171fe1a649176224d1abbf0a

@ -1 +1 @@
Subproject commit 82fbe9b0a8ac0e8f52c447fb5e49ccb31d4e6374
Subproject commit 75a9cdca81fff723c519948dcdd4fdfc912fc135

View File

@ -740,9 +740,6 @@ function quickSubmitTagCollectionTagForm(selected_tag_ids, addData) {
$('#temp #TagCollectionTag').val(JSON.stringify(selected_tag_ids));
xhr({
data: $('#TagCollectionAddTagForm').serialize(),
beforeSend: function (XMLHttpRequest) {
$(".loading").show();
},
success:function (data, textStatus) {
handleGenericAjaxResponse(data);
refreshTagCollectionRow(tag_collection_id);
@ -5038,10 +5035,9 @@ function submit_feed_overlap_tool(feedId) {
}
function fetchFormDataAjax(url, callback, errorCallback) {
var formData = false;
$.ajax({
data: '[]',
success:function (data) {
success: function (data) {
callback(data);
},
error:function() {
@ -5050,8 +5046,7 @@ function fetchFormDataAjax(url, callback, errorCallback) {
errorCallback();
}
},
async: false,
type:"get",
type: "get",
cache: false,
url: url
});
@ -5406,19 +5401,19 @@ $('body').on('click', '.hex-value-convert', function() {
if (tagData.TaxonomyPredicate.description) {
$predicate.append($('<p/>').css("margin-bottom", "5px").append(
$('<strong/>').text('Description: '),
$('<span/>').text(tagData.TaxonomyPredicate.description),
$('<span/>').text(tagData.TaxonomyPredicate.description)
));
}
if (tagData.TaxonomyPredicate.TaxonomyEntry && tagData.TaxonomyPredicate.TaxonomyEntry[0].numerical_value) {
$predicate.append($('<p/>').css("margin-bottom", "5px").append(
$('<strong/>').text('Numerical value: '),
$('<span/>').text(tagData.TaxonomyPredicate.TaxonomyEntry[0].numerical_value),
$('<span/>').text(tagData.TaxonomyPredicate.TaxonomyEntry[0].numerical_value)
));
}
var $meta = $('<div/>').append(
$('<h3/>').text('Taxonomy: ' + tagData.Taxonomy.namespace.toUpperCase()),
$('<p/>').css("margin-bottom", "5px").append(
$('<span/>').text(tagData.Taxonomy.description),
$('<span/>').text(tagData.Taxonomy.description)
)
)
return $('<div/>').append($predicate, $meta)