Merge branch '2.4' into guides

pull/3664/head
Steve Clement 2018-09-11 15:43:06 +02:00
commit 93cb31493c
21 changed files with 134 additions and 85 deletions

View File

@ -177,10 +177,9 @@ sudo python3 setup.py install
-----------
# CakePHP is included as a submodule of MISP, execute the following commands to let git fetch it:
cd /usr/local/www/MISP
sudo -u www git submodule init
sudo -u www git submodule update
sudo -u www git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
sudo -u www git submodule foreach git config core.filemode false
sudo -u www git submodule foreach --recursive git config core.filemode false
# Once done, install CakeResque along with its dependencies if you intend to use the built in background jobs:
cd /usr/local/www/MISP/app

View File

@ -272,10 +272,9 @@ doas pip3.6 install stix2
```
# CakePHP is included as a submodule of MISP, execute the following commands to let git fetch it:
cd /var/www/htdocs/MISP
doas -u www git submodule init
doas -u www git submodule update
doas -u www git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
doas -u www git submodule foreach git config core.filemode false
doas -u www git submodule foreach --recursive git config core.filemode false
# Once done, install CakeResque along with its dependencies if you intend to use the built in background jobs:
cd /var/www/htdocs/MISP/app

View File

@ -162,10 +162,9 @@ cd $PATH_TO_MISP/app/files/scripts/mixbox
sudo pip3 install .
cd $PATH_TO_MISP
sudo -u www-data git submodule init
sudo -u www-data git submodule update
sudo -u www-data git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
sudo -u www-data git submodule foreach git config core.filemode false
sudo -u www-data git submodule foreach --recursive git config core.filemode false
# install PyMISP
cd $PATH_TO_MISP/PyMISP
@ -678,8 +677,7 @@ sudo apt-get install -y libssl-dev swig python3-ssdeep p7zip-full unrar-free sql
sudo pip3 install SQLAlchemy PrettyTable python-magic
sudo git clone https://github.com/viper-framework/viper.git
cd viper
sudo git submodule init
sudo git submodule update
sudo git submodule update --init --recursive
sudo pip3 install -r requirements.txt
sudo pip3 uninstall yara -y
/usr/local/src/viper/viper-cli -h

View File

@ -174,10 +174,9 @@ cd $PATH_TO_MISP/app/files/scripts/mixbox
sudo pip3 install .
cd $PATH_TO_MISP
sudo -u www-data git submodule init
sudo -u www-data git submodule update
sudo -u www-data git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
sudo -u www-data git submodule foreach git config core.filemode false
sudo -u www-data git submodule foreach --recursive git config core.filemode false
# install PyMISP
cd $PATH_TO_MISP/PyMISP
@ -701,8 +700,7 @@ sudo apt-get install -y libssl-dev swig python3-ssdeep p7zip-full unrar-free sql
sudo pip3 install SQLAlchemy PrettyTable python-magic
sudo git clone https://github.com/viper-framework/viper.git
cd viper
sudo git submodule init
sudo git submodule update
sudo git git submodule update --init --recursive
sudo pip3 install -r requirements.txt
sudo pip3 uninstall yara -y
/usr/local/src/viper/viper-cli -h

View File

@ -149,10 +149,9 @@ function installMISPonKali() {
pip3 install .
cd $PATH_TO_MISP
$SUDO_WWW git submodule init
$SUDO_WWW git submodule update
$SUDO_WWW git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
$SUDO_WWW git submodule foreach git config core.filemode false
$SUDO_WWW git submodule foreach --recursive git config core.filemode false
# install PyMISP
cd $PATH_TO_MISP/PyMISP
@ -515,8 +514,7 @@ function installMISPonKali() {
git clone https://github.com/viper-framework/viper.git
chown -R $MISP_USER:$MISP_USER viper
cd viper
$SUDO git submodule init
$SUDO git submodule update
$SUDO git submodule update --init --recursive
pip3 install -r requirements.txt
pip3 uninstall yara -y
$SUDO /usr/local/src/viper/viper-cli -h > /dev/null

View File

@ -152,10 +152,9 @@ systemctl restart rh-php71-php-fpm.service
4.01/ CakePHP is now included as a submodule of MISP, execute the following commands to let git fetch it ignore this
message: No submodule mapping found in .gitmodules for path 'app/Plugin/CakeResque'
cd /var/www/MISP
git submodule init
git submodule update
git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
git submodule foreach git config core.filemode false
git submodule foreach --recursive git config core.filemode false
4.02/ Install CakeResque along with its dependencies if you intend to use the built in background jobs
cd /var/www/MISP/app

View File

@ -56,10 +56,9 @@ sudo mkdir /var/www/MISP
sudo chown www-data:www-data /var/www/MISP
cd /var/www/MISP
sudo -u www-data git clone https://github.com/MISP/MISP.git /var/www/MISP
sudo -u www-data git submodule init
sudo -u www-data git submodule update
sudo -u www-data git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
sudo -u www-data git submodule foreach git config core.filemode false
sudo -u www-data git submodule foreach --recursive git config core.filemode false
# Make git ignore filesystem permission differences
sudo -u www-data git config core.filemode false

View File

@ -80,11 +80,10 @@ git checkout tags/$(git describe --tags `git rev-list --tags --max-count=1`)
# the message regarding a "detached HEAD state" is expected behaviour
# (you only have to create a new branch, if you want to change stuff and do a pull request for example)
git submodule init
git submodule update
git submodule update --init --recursive
# Make git ignore filesystem permission differences
git submodule foreach git config core.filemode false
git submodule foreach --recursive git config core.filemode false
# install Mitre's STIX and its dependencies by running the following commands:
sudo apt-get install python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev python-setuptools python-pip

View File

@ -115,10 +115,9 @@ umask $UMASK
# No submodule mapping found in .gitmodules for path 'app/Plugin/CakeResque'
cd /var/www/MISP
git submodule init
git submodule update
git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
git submodule foreach git config core.filemode false
git submodule foreach --recursive git config core.filemode false
# Once done, install CakeResque along with its dependencies if you intend to use the built in background jobs:
cd /var/www/MISP/app

View File

@ -131,10 +131,9 @@ sudo git config core.filemode false
# Fetch submodules
cd /var/www/MISP
sudo git submodule init
sudo git submodule update
sudo git submodule update --init --recursive
# Make git ignore filesystem permission differences for submodules
sudo git submodule foreach git config core.filemode false
sudo git submodule foreach --recursive git config core.filemode false
# install Mitre's STIX and its dependencies by running the following commands:
sudo yum install python-importlib python-lxml python-dateutil python-six -y

View File

@ -2083,7 +2083,7 @@ class AttributesController extends AppController
}
public function restSearch($returnFormat = 'json', $value = false, $type = false, $category = false, $org = false, $tags = false, $from = false, $to = false, $last = false, $eventid = false, $withAttachments = false, $uuid = false, $publish_timestamp = false, $published = false, $timestamp = false, $enforceWarninglist = false, $to_ids = false, $deleted = false, $includeEventUuid = false, $event_timestamp = false, $threat_level_id = false) {
$paramArray = array('value' , 'type', 'category', 'org', 'tags', 'from', 'to', 'last', 'eventid', 'withAttachments', 'uuid', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'to_ids', 'deleted', 'includeEventUuid', 'event_timestamp', 'threat_level_id');
$paramArray = array('value' , 'type', 'category', 'org', 'tags', 'from', 'to', 'last', 'eventid', 'withAttachments', 'uuid', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'to_ids', 'deleted', 'includeEventUuid', 'event_timestamp', 'threat_level_id', 'includeEventTags');
$filterData = array(
'request' => $this->request,
'named_params' => $this->params['named'],
@ -2134,7 +2134,8 @@ class AttributesController extends AppController
'enforceWarninglist' => !empty($filters['enforceWarninglist']) ? $filters['enforceWarninglist'] : 0,
'includeAllTags' => true,
'flatten' => 1,
'includeEventUuid' => !empty($filters['includeEventUuid']) ? $filters['includeEventUuid'] : 0
'includeEventUuid' => !empty($filters['includeEventUuid']) ? $filters['includeEventUuid'] : 0,
'includeEventTags' => !empty($filters['includeEventTags']) ? $filters['includeEventTags'] : 0
);
if (!empty($filtes['deleted'])) {
$params['deleted'] = 1;

View File

@ -4015,21 +4015,21 @@ class EventsController extends AppController
// #TODO i18n
$exports = array(
'xml' => array(
'url' => '/events/restSearch/download/false/false/false/false/false/false/false/false/false/' . $id . '/false.xml',
'url' => '/events/restSearch/xml/false/false/false/false/false/false/false/false/false/' . $id . '/false.xml',
'text' => 'MISP XML (metadata + all attributes)',
'requiresPublished' => false,
'checkbox' => true,
'checkbox_text' => 'Encode Attachments',
'checkbox_set' => '/events/restSearch/download/false/false/false/false/false/false/false/false/false/' . $id . '/true.xml',
'checkbox_set' => '/events/restSearch/xml/false/false/false/false/false/false/false/false/false/' . $id . '/true.xml',
'checkbox_default' => true
),
'json' => array(
'url' => '/events/restSearch/download/false/false/false/false/false/false/false/false/false/' . $id . '/false.json',
'url' => '/events/restSearch/json/false/false/false/false/false/false/false/false/false/' . $id . '/false.json',
'text' => 'MISP JSON (metadata + all attributes)',
'requiresPublished' => false,
'checkbox' => true,
'checkbox_text' => 'Encode Attachments',
'checkbox_set' => '/events/restSearch/download/false/false/false/false/false/false/false/false/false/' . $id . '/true.json',
'checkbox_set' => '/events/restSearch/json/false/false/false/false/false/false/false/false/false/' . $id . '/true.json',
'checkbox_default' => true
),
'openIOC' => array(

View File

@ -624,7 +624,6 @@ class ServersController extends AppController
$this->set('successes', $result[0]);
$this->set('fails', $result[1]);
$this->set('pulledProposals', $result[2]);
$this->set('lastpulledid', $result[3]);
} else {
$this->loadModel('Job');
$this->Job->create();

View File

@ -29,15 +29,21 @@ class JsonExport
if (isset($attribute['Object']) && empty($attribute['Object']['id'])) {
unset($attribute['Object']);
}
if (isset($attribute['AttributeTag'])) {
$attributeTags = array();
foreach ($attribute['AttributeTag'] as $tk => $tag) {
$attribute['Tag'][$tk] = $attribute['AttributeTag'][$tk]['Tag'];
$tagTypes = array('AttributeTag', 'EventTag');
foreach($tagTypes as $tagType) {
if (isset($attribute[$tagType])) {
$attributeTags = array();
foreach ($attribute[$tagType] as $tk => $tag) {
if ($tagType === 'EventTag') {
$attribute[$tagType][$tk]['Tag']['inherited'] = 1;
}
$attribute['Tag'][] = $attribute[$tagType][$tk]['Tag'];
}
unset($attribute[$tagType]);
}
unset($attribute['AttributeTag']);
unset($attribute['value1']);
unset($attribute['value2']);
}
unset($attribute['value1']);
unset($attribute['value2']);
return json_encode($attribute);
}

View File

@ -30,15 +30,21 @@ class XmlExport
if (isset($attribute['Object']) && empty($attribute['Object']['id'])) {
unset($attribute['Object']);
}
if (isset($attribute['AttributeTag'])) {
$attributeTags = array();
foreach ($attribute['AttributeTag'] as $tk => $tag) {
$attribute['Tag'][$tk] = $attribute['AttributeTag'][$tk]['Tag'];
$tagTypes = array('AttributeTag', 'EventTag');
foreach($tagTypes as $tagType) {
if (isset($attribute[$tagType])) {
$attributeTags = array();
foreach ($attribute[$tagType] as $tk => $tag) {
if ($tagType === 'EventTag') {
$attribute[$tagType][$tk]['Tag']['inherited'] = 1;
}
$attribute['Tag'][] = $attribute[$tagType][$tk]['Tag'];
}
unset($attribute[$tagType]);
}
unset($attribute['AttributeTag']);
unset($attribute['value1']);
unset($attribute['value2']);
}
unset($attribute['value1']);
unset($attribute['value2']);
$xmlObject = Xml::fromArray(array('Attribute' => $attribute), array('format' => 'tags'));
$xmlString = $xmlObject->asXML();
return substr($xmlString, strpos($xmlString, "\n") + 1);

View File

@ -69,7 +69,8 @@ class AppModel extends Model
public $db_changes = array(
1 => false, 2 => false, 3 => false, 4 => true, 5 => false, 6 => false,
7 => false, 8 => false, 9 => false, 10 => false, 11 => false, 12 => false,
13 => false, 14 => false, 15 => false, 16 => false, 17 => false
13 => false, 14 => false, 15 => false, 18 => false, 19 => false, 20 => false,
21 => false
);
public function afterSave($created, $options = array())
@ -1023,14 +1024,22 @@ class AppModel extends Model
INDEX `timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
break;
case '16':
case 18:
$sqlArray[] = 'ALTER TABLE `taxonomy_predicates` ADD COLUMN description text CHARACTER SET UTF8 collate utf8_bin;';
$sqlArray[] = 'ALTER TABLE `taxonomy_entries` ADD COLUMN description text CHARACTER SET UTF8 collate utf8_bin;';
$sqlArray[] = 'ALTER TABLE `taxonomy_predicates` ADD COLUMN exclusive tinyint(1) DEFAULT 0;';
break;
case '17':
case 19:
$sqlArray[] = 'ALTER TABLE `taxonomies` ADD COLUMN exclusive tinyint(1) DEFAULT 0;';
break;
case 20:
$sqlArray[] = "ALTER TABLE `servers` ADD `skip_proxy` tinyint(1) NOT NULL DEFAULT 0;";
break;
case 21:
$sqlArray[] = 'ALTER TABLE `tags` ADD COLUMN numerical_value int(11) NULL;';
$sqlArray[] = 'ALTER TABLE `taxonomy_predicates` ADD COLUMN numerical_value int(11) NULL;';
$sqlArray[] = 'ALTER TABLE `taxonomy_entries` ADD COLUMN numerical_value int(11) NULL;';
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';

View File

@ -2453,6 +2453,9 @@ class Attribute extends AppModel
// array 1 will have all of the non negated terms and array 2 all the negated terms
public function dissectArgs($args)
{
if (empty($args)) {
return array(0 => array(), 1 => array());
}
if (!is_array($args)) {
$args = explode('&&', $args);
}
@ -2865,6 +2868,9 @@ class Attribute extends AppModel
$pagesToFetch = 1;
}
$attributes = array();
if (!empty($options['includeEventTags'])) {
$eventTags = array();
}
while ($continue) {
if ($loop) {
$params['page'] = $params['page'] + 1;
@ -2887,6 +2893,27 @@ class Attribute extends AppModel
$results = array_values($results);
$proposals_block_attributes = Configure::read('MISP.proposals_block_attributes');
foreach ($results as $key => $attribute) {
if (!empty($options['includeEventTags'])) {
if (!isset($eventTags[$results[$key]['Event']['id']])) {
$tagConditions = array('EventTag.event_id' => $attribute['Event']['id']);
if (empty($options['includeAllTags'])) {
$tagConditions['Tag.exportable'] = 1;
}
$temp = $this->Event->EventTag->find('all', array(
'recursive' => -1,
'contain' => array('Tag'),
'conditions' => $tagConditions
));
foreach ($temp as $tag) {
$tag['EventTag']['Tag'] = $tag['Tag'];
unset($tag['Tag']);
$eventTags[$results[$key]['Event']['id']][] = $tag;
}
}
foreach ($eventTags[$results[$key]['Event']['id']] as $eventTag) {
$results[$key]['EventTag'][] = $eventTag['EventTag'];
}
}
if ($options['enforceWarninglist'] && !$this->Warninglist->filterWarninglistAttributes($warninglists, $attribute['Attribute'])) {
continue;
}

View File

@ -222,7 +222,10 @@ class Module extends AppModel
if ($post) {
$response = $httpSocket->post($url . $uri, $post, $request);
} else {
$response = $httpSocket->get($url . $uri, false, $request);
if ($moduleFamily == 'Cortex') {
unset($request['header']['Content-Type']);
}
$response = $httpSocket->get($url . $uri, false, $request);
}
return json_decode($response->body, true);
} catch (Exception $e) {

View File

@ -1812,8 +1812,6 @@ class Server extends AppModel
$eventIds = array_intersect($eventIds, $local_event_ids);
} elseif (is_numeric($technique)) {
$eventIds[] = intval($technique);
// if we are downloading a single event, don't fetch all proposals
$conditions = array('Event.id' => $technique);
} else {
return array('error' => array(4, null));
}
@ -1832,7 +1830,7 @@ class Server extends AppModel
return false;
}
private function __updatePulledEventBeforeInsert($event, $server, $user)
private function __updatePulledEventBeforeInsert(&$event, $server, $user)
{
// we have an Event array
// The event came from a pull, so it should be locked.
@ -1870,10 +1868,9 @@ class Server extends AppModel
return $event;
}
private function __checkIfPulledEventExistsAndAddOrUpdate($event, &$successes, &$fails, $eventModel, $server, $user, $passAlong, $job, $jobId)
private function __checkIfPulledEventExistsAndAddOrUpdate($event, $eventId, &$successes, &$fails, $eventModel, $server, $user, $jobId)
{
// check if the event already exist (using the uuid)
$existingEvent = null;
$existingEvent = $eventModel->find('first', array('conditions' => array('Event.uuid' => $event['Event']['uuid'])));
if (!$existingEvent) {
// add data for newly imported events
@ -1898,32 +1895,26 @@ class Server extends AppModel
}
}
private function __pullEvents($eventId, $successes, $fails, $eventModel, $server, $user, $passAlong, $job, $jobId)
private function __pullEvent($eventId, &$successes, &$fails, $eventModel, $server, $user, $jobId)
{
$event = $eventModel->downloadEventFromServer(
$eventId,
$server
);
);;
if (!empty($event)) {
if ($this->__checkIfEventIsBlockedBeforePull($event)) {
return false;
}
$this->__updatePulledEventBeforeInsert($event, $server, $user);
$this->__checkIfPulledEventExistsAndAddOrUpdate($event, $successes, $fails, $eventModel, $server, $user, $passAlong, $job, $jobId);
$event = $this->__updatePulledEventBeforeInsert($event, $server, $user);
$this->__checkIfPulledEventExistsAndAddOrUpdate($event, $eventId, $successes, $fails, $eventModel, $server, $user, $jobId);
} else {
// error
$fails[$eventId] = 'failed downloading the event';
}
if ($jobId) {
if ($k % 10 == 0) {
$job->id = $jobId;
$job->saveField('progress', 50 * (($k + 1) / count($eventIds)));
}
}
return true;
}
private function __handlePulledProposals($proposals, $events, $job, $jobId)
private function __handlePulledProposals($proposals, $events, $job, $jobId, $eventModel, $user)
{
$pulledProposals = array();
if (!empty($proposals)) {
@ -1971,7 +1962,7 @@ class Server extends AppModel
if ($jobId) {
if ($k % 50 == 0) {
$job->id = $jobId;
$job->saveField('progress', 50 * (($k + 1) / count($proposals)));
$job->saveField('progress', 50 * (($k + 1) / count($proposals)) + 50);
}
}
}
@ -1979,7 +1970,7 @@ class Server extends AppModel
return $pulledProposals;
}
public function pull($user, $id = null, $technique=false, $server, $jobId = false, $percent = 100, $current = 0)
public function pull($user, $id = null, $technique=false, $server, $jobId = false)
{
if ($jobId) {
$job = ClassRegistry::init('Job');
@ -1990,9 +1981,9 @@ class Server extends AppModel
$email = $user['email'];
}
$eventModel = ClassRegistry::init('Event');
App::uses('HttpSocket', 'Network/Http');
$eventIds = array();
$conditions = array();
// if we are downloading a single event, don't fetch all proposals
$conditions = is_numeric($technique) ? array('Event.id' => $technique) : array();
$eventIds = $this->__getEventIdListBasedOnPullTechnique($technique, $server);
if (!empty($eventIds['error'])) {
$errors = array(
@ -2020,9 +2011,13 @@ class Server extends AppModel
// now process the $eventIds to pull each of the events sequentially
if (!empty($eventIds)) {
// download each event
$HttpSocket = $this->setupHttpSocket($server);
foreach ($eventIds as $k => $eventId) {
$this->__pullEvents($eventId, $successes, $fails, $eventModel, $server, $user, $passAlong, $job, $jobId);
$this->__pullEvent($eventId, $successes, $fails, $eventModel, $server, $user, $jobId);
if ($jobId) {
if ($k % 10 == 0) {
$job->saveField('progress', 50 * (($k + 1) / count($eventIds)));
}
}
}
}
if ($jobId) {
@ -2033,9 +2028,10 @@ class Server extends AppModel
'recursive' => -1,
'conditions' => $conditions
));
$pulledProposals = array();
if (!empty($events)) {
$proposals = $eventModel->downloadProposalsFromServer($events, $server);
$pulledProposals = $this->__handlePulledProposals($proposals, $events, $job, $jobId);
$pulledProposals = $this->__handlePulledProposals($proposals, $events, $job, $jobId, $eventModel, $user);
}
if ($jobId) {
$job->saveField('progress', 100);

View File

@ -147,6 +147,9 @@ class Taxonomy extends AppModel
if (isset($entry['colour']) && !empty($entry['colour'])) {
$temp['colour'] = $entry['colour'];
}
if (isset($entry['numerical_value']) && $entry['numerical_value'] !== null) {
$temp['numerical_value'] = $entry['numerical_value'];
}
$entries[] = $temp;
}
} else {
@ -155,6 +158,9 @@ class Taxonomy extends AppModel
if (isset($predicate['colour']) && !empty($predicate['colour'])) {
$temp['colour'] = $predicate['colour'];
}
if (isset($predicate['numerical_value']) && $predicate['numerical_value'] !== null) {
$temp['numerical_value'] = $predicate['numerical_value'];
}
$entries[] = $temp;
}
}
@ -267,13 +273,20 @@ class Taxonomy extends AppModel
foreach ($taxonomy['entries'] as $k => $entry) {
if (isset($tags[strtoupper($entry['tag'])])) {
$temp = $tags[strtoupper($entry['tag'])];
if ((in_array('colour', $skipUpdateFields) && $temp['Tag']['colour'] != $colours[$k]) || (in_array('name', $skipUpdateFields) && $temp['Tag']['name'] !== $entry['tag'])) {
if (
(!in_array('colour', $skipUpdateFields) && $temp['Tag']['colour'] != $colours[$k]) ||
(!in_array('name', $skipUpdateFields) && $temp['Tag']['name'] !== $entry['tag']) ||
(!in_array('numerical_value', $skipUpdateFields) && isset($entry['numerical_value']) && isset($temp['Tag']['numerical_value']) && $temp['Tag']['numerical_value'] !== $entry['numerical_value'])
) {
if (!in_array('colour', $skipUpdateFields)) {
$temp['Tag']['colour'] = (isset($entry['colour']) && !empty($entry['colour'])) ? $entry['colour'] : $colours[$k];
}
if (!in_array('name', $skipUpdateFields)) {
$temp['Tag']['name'] = $entry['tag'];
}
if (!in_array('numerical_value', $skipUpdateFields)) {
$temp['Tag']['numerical_value'] = $entry['numerical_value'];
}
$this->Tag->save($temp['Tag']);
}
}

View File

@ -72,6 +72,7 @@
<?php endif;?>
<th><?php echo $this->Paginator->sort('tag');?></th>
<th><?php echo $this->Paginator->sort('expanded');?></th>
<th><?php echo $this->Paginator->sort('numerical_value');?></th>
<th><?php echo $this->Paginator->sort('events');?></th>
<th><?php echo $this->Paginator->sort('attributes');?></th>
<th><?php echo $this->Paginator->sort('tag');?></th>
@ -86,6 +87,7 @@
<?php endif; ?>
<td id="tag_<?php echo h($k); ?>" class="short"><?php echo h($item['tag']); ?></td>
<td><?php echo h($item['expanded']); ?>&nbsp;</td>
<td class="short"><?php echo isset($item['numerical_value']) ? h($item['numerical_value']) : ''; ?>&nbsp;</td>
<td class="short">
<?php
if ($item['existing_tag']) {