First version of the sightings

- add / delete sightings via REST
- add sightings via the UI
- View sightings info on an event and attribute level (event view only for now)
- differentiate between own sightings and that of other orgs (additional information via popover still coming)

- settings:
  - 1. enable / disable sightings server wide
  - 2. set sightings policy
    - a. Only Event owner can see sightings + everyone sees what they themeselves contribute
    - b. Anyone that contributes sightings to an event can see the sightings data
    - c. Everyone that can see the event can see the sightings
  - 3. Anonymisisation (in progress, data correctly retrieved in business logic)
    - a. if true, then only own org + "other" is shown
    - b. otherwise all orgs that submitted sightings are shown

Further improvements needed for version 1 of sightings:
  - 1. Delete via the interface
  - 2. View detailed sightings information
  - 3. Graph the sightings data for the event
  - 4. Include the Sightings data in the XML/JSON views
  - 5. View sighting for attribute / event via the API
pull/1092/head
Iglocska 2015-12-20 13:41:52 +01:00
parent f8f367a996
commit 868d4cdd3f
10 changed files with 284 additions and 41 deletions

View File

@ -6,26 +6,94 @@ class SightingsController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
if (!Configure::read('Plugin.Sightings_enable')) throw new MethodNotAllowedException('This feature is not enabled on this instance.');
}
public $paginate = array(
'limit' => 60,
'maxLimit' => 9999, // LATER we will bump here on a problem once we have more than 9999 events <- no we won't, this is the max a user van view/page.
'order' => array(
'Sighting.created' => 'DESC'
),
'order' => array('Sighting.date_sighting' => 'DESC'),
);
public function index() {
}
public function add() {
// takes an attribute ID or UUID
public function add($id) {
if (!$this->userRole['perm_add']) throw new MethodNotAllowedException('You are not authorised to add sightings data as you don\'t have write access.');
if (!$this->request->is('post')) throw new MethodNotAllowedException('This action can only be accessed via a post request.');
if (strlen($id) == 36) $conditions = array('Attribute.uuid' => $id);
else $conditions = array('Attribute.id' => $id);
$attribute = $this->Sighting->Attribute->fetchAttributes($this->Auth->user(), array('conditions' => $conditions));
if (empty($attribute)) throw new NotFoundException('Could not add sighting information, invalid attribute.');
// normalise the request data if it exists
if (isset($this->request->data['request'])) $this->request->data = $this->request->data['request'];
if (isset($this->request->data['Sighting'])) $this->request->data = $this->request->data['Sighting'];
$attribute = $attribute[0];
$this->Sighting->create();
$date = date('Y-m-d H:i:s');
$sighting = array(
'attribute_id' => $attribute['Attribute']['id'],
'event_id' => $attribute['Attribute']['event_id'],
'org_id' => $this->Auth->user('org_id'),
'date_sighting' => isset($this->request->data['date_sighting']) ? $this->request->data['date_sighting'] : $date,
);
$result = $this->Sighting->save($sighting);
if ($this->request->is('ajax')) {
if (!$result) {
$error_message = 'Could not add the Sighting. Reason: ' . json_encode($this->Sighting->validationErrors);
return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => $error_message)), 'status' => 200));
} else {
return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => 'Sighting added.')), 'status' => 200));
}
} else {
if (!$result) {
$this->set('errors', json_encode($this->Sighting->validatidationErrors));
$this->set('name', 'Failed');
$this->set('message', 'Could not add the Sighting.');
$this->set('_serialize', array('name', 'message', 'errors'));
} else {
$this->set('name', 'Success');
$this->set('message', 'Sighting successfuly added.');
$this->set('url', '/sightings/add/' . $id);
$this->set('id', $this->Sighting->id);
$this->set('_serialize', array('name', 'message', 'url', 'id'));
}
}
}
public function edit($id) {
}
// takes a sighting ID
public function delete($id) {
}
if (!$this->userRole['perm_modify_org']) throw new MethodNotAllowedException('You are not authorised to remove sightings data as you don\'t have permission to modify your organisation\'s data.');
if (!$this->request->is('post')) throw new MethodNotAllowedException('This action can only be accessed via a post request.');
$sighting = $this->Sighting->find('first', array('conditions' => array('Sighting.id' => $id), 'recursive' => -1));
if (empty($sighting)) throw new NotFoundException('Invalid sighting.');
if (!$this->_isSiteAdmin()) {
if ($sighting['Sighting']['org_id'] != $this->Auth->user('org_id')) throw new NotFoundException('Invalid sighting.');
}
$result = $this->Sighting->delete($sighting['Sighting']['id']);
if (!$result) {
$this->set('errors', '');
$this->set('name', 'Failed');
$this->set('message', 'Could not delete the Sighting.');
$this->set('_serialize', array('name', 'message', 'errors'));
} else {
$this->set('name', 'Success');
$this->set('message', 'Sighting successfuly deleted.');
$this->set('url', '/sightings/delete/' . $id);
$this->set('_serialize', array('name', 'message', 'url'));
}
}
public function resetLikeButton($id) {
$attribute = $this->Sighting->Attribute->fetchAttributes($this->Auth->user(), array('conditions' => array('Attribute.id' => $id)));
if (empty($attribute)) throw new MethodNotAllowedException('Invalid attribute');
$this->layout = 'text/default';
$this->set('id', $id);
$this->autoRender = false;
$this->render('/Sightings/ajax/like_button');
}
}

View File

@ -64,6 +64,19 @@ class AppModel extends Model {
case 'addEventBlacklistsContext':
$sql = 'ALTER TABLE `event_blacklists` ADD `event_orgc` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL , ADD `event_info` TEXT CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, ADD `comment` TEXT CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL ;';
break;
case 'addSightings':
$sql = "CREATE TABLE IF NOT EXISTS `sightings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`attribute_id` int(11) NOT NULL,
`event_id` int(11) NOT NULL,
`org_id` int(11) NOT NULL,
`date_sighting` datetime NOT NULL,
PRIMARY KEY (`id`),
INDEX `attribute_id` (`attribute_id`),
INDEX `event_id` (`event_id`),
INDEX `org_id` (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ;";
break;
case 'makeAttributeUUIDsUnique':
$this->__dropIndex('attributes', 'uuid');
$sql = 'ALTER TABLE `attributes` ADD UNIQUE (uuid);';

View File

@ -1808,7 +1808,8 @@ class Attribute extends AppModel {
if (isset($options['contain'])) $params['contain'] = $options['contain'];
if (isset($options['fields'])) $params['fields'] = $options['fields'];
if (isset($options['conditions'])) $params['conditions']['AND'][] = $options['conditions'];
if (isset($options['order'])) $params['conditions']['AND'][] = $options['order'];
if (isset($options['order'])) $params['order'] = $options['order'];
else ($params['order'] = array());
if (isset($options['group'])) $params['group'] = $options['group'];
if (Configure::read('MISP.unpublishedprivate')) $params['conditions']['AND'][] = array('Event.published' => 1);
$results = $this->find('all', $params);

View File

@ -1178,6 +1178,7 @@ class Event extends AppModel {
if (empty($results)) return array();
// Do some refactoring with the event
$sgsids = $this->SharingGroup->fetchAllAuthorised($user);
if (Configure::read('Plugin.Sightings_enable')) $this->Sighting = ClassRegistry::init('Sighting');
foreach ($results as $eventKey => &$event) {
// unset the empty sharing groups that are created due to the way belongsTo is handled
if (isset($event['SharingGroup']['SharingGroupServer'])) {
@ -1215,6 +1216,9 @@ class Event extends AppModel {
}
}
}
if (Configure::read('Plugin.Sightings_enable')) {
$event['Sighting'] = $this->Sighting->attachToEvent($event, $user);
}
}
return $results;
}

View File

@ -640,7 +640,7 @@ class Server extends AppModel {
'Plugin' => array(
'branch' => 1,
'RPZ_policy' => array(
'level' => 1,
'level' => 2,
'description' => 'The default policy action for the values added to the RPZ.',
'value' => 0,
'errorMessage' => '',
@ -783,7 +783,32 @@ class Server extends AppModel {
'type' => 'string',
'afterHook' => 'zmqAfterHook',
),
'Sightings_enable' => array(
'level' => 1,
'description' => 'Enables or disables the sighting functionality. When enabled, users can use the UI or the appropriate APIs to submit sightings data about indicators.',
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
'beforeHook' => 'sightingsBeforeHook',
),
'Sightings_policy' => array(
'level' => 1,
'description' => 'This setting defines who will have access to seeing the reported sightings. The default setting is the event owner alone (in addition to everyone seeing their own contribution) with the other options being Sighting reporters (meaning the event owner and anyone that provided sighting data about the event) and Everyone (meaning anyone that has access to seeing the event / attribute).',
'value' => 0,
'errorMessage' => '',
'test' => 'testForSightingVisibility',
'type' => 'numeric',
'options' => array(0 => 'Event Owner', 1 => 'Sighting reporters', 2 => 'Everyone'),
),
'Sightings_anonymise' => array(
'level' => 1,
'description' => 'Enabling the anonymisation of sightings will simply aggregate all sightings instead of showing the organisations that have reported a sighting. Users will be able to tell the number of sightings their organisation has submitted and the number of sightings for other organisations',
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
),
),
'debug' => array(
'level' => 0,
@ -1567,6 +1592,20 @@ class Server extends AppModel {
return true;
}
public function testForSightingVisibility($value) {
$numeric = $this->testforNumeric($value);
if ($numeric !== true) return $numeric;
if ($value < 0 || $value > 2) return 'Invalid setting, valid range is 0-2 (0 = Event owner, 1 = Sighting reporters, 2 = Everyone.';
return true;
}
public function sightingsBeforeHook($setting, $value) {
if ($value == true) {
$this->updateDatabase('addSightings');
}
return true;
}
public function testForRPZSerial($value) {
if ($this->testForEmpty($value) !== true) return $this->testForEmpty($value);
if (!preg_match('/^((\$date(\d*)|\d*))$/', $value)) return 'Invalid format.';

View File

@ -8,25 +8,69 @@ class Sighting extends AppModel{
);
public $validate = array(
'event_uuid' => array(
'unique' => array(
'rule' => 'isUnique',
'message' => 'Event already blacklisted.'
),
'uuid' => array(
'rule' => array('uuid'),
'message' => 'Please provide a valid UUID'
),
)
'event_id' => 'numeric',
'attribute_id' => 'numeric',
'org_id' => 'numeric',
'date_sighting' => 'datetime'
);
public $belongsTo = array(
'Attribute' => array(
'className' => 'Attribute',
),
'Event' => array(
'className' => 'Event',
),
'Organisation' => array(
'className' => 'Organisation',
'foreignKey' => 'org_id'
),
);
public function beforeValidate($options = array()) {
parent::beforeValidate();
$date = date('Y-m-d H:i:s');
if (empty($this->data['Sighting']['id'])) {
$this->data['Sighting']['date_created'] = $date;
if (empty($this->data['Sighting']['id']) && empty($this->data['Sighting']['date_sighting'])) {
$this->data['Sighting']['date_sighting'] = $date;
}
$this->data['Sighting']['date_modified'] = $date;
return true;
}
public function attachToEvent(&$event, &$user, $eventOnly=false) {
$ownEvent = false;
if ($user['Role']['perm_site_admin'] || $event['Event']['org_id'] == $user['org_id']) $ownEvent = true;
$conditions = array('Sighting.event_id' => $event['Event']['id']);
if (!$ownEvent && (!Configure::read('Plugin.Sightings_policy') || Configure::read('Plugin.Sightings_policy') == 0)) $conditions['Sighting.org_id'] = $user['org_id'];
$contain = array();
if (Configure::read('MISP.showorg')) $contain['Organisation'] = array('fields' => array('Organisation.id', 'Organisation.uuid', 'Organisation.name'));
// Sighting reporters setting
// If the event has any sightings for the user's org, then the user is a sighting reporter for the event too.
// This means that he /she has access to the sightings data contained within
if (!$ownEvent && Configure::read('Plugin.Sightings_policy') == 1) {
$temp = $this->find('first', array('recursive' => -1, 'conditions' => array('Sighting.event_id' => $event['Event']['id'], 'Sighting.org_id' => $user['org_id'])));
if (empty($temp)) return array();
}
$sightings = $this->find('all', array(
'conditions' => $conditions,
'recursive' => -1,
'contain' => $contain,
));
if (empty($sightings)) return array();
$anonymise = Configure::read('Plugin.Sightings_anonymise');
foreach ($sightings as $k => &$sighting) {
if ($anonymise && !$user['Role']['perm_site_admin']) {
if ($sighting['Sighting']['org_id'] != $user['org_id']) {
unset($sightings[$k]['Sighting']['org_id']);
unset($sightings[$k]['Organisation']);
}
}
//rearrange it to match the event format of fetchevent
if (isset($sightings[$k]['Organisation'])) $sightings[$k]['Sighting']['Organisation'] = $sightings[$k]['Organisation'];
$sightings[$k] = $sightings[$k]['Sighting'] ;
}
return $sightings;
}
}

View File

@ -4,7 +4,25 @@
$possibleAction = 'Proposal';
if ($mayModify) $possibleAction = 'Attribute';
$all = false;
if (isset($this->params->params['paging']['Event']['page']) && $this->params->params['paging']['Event']['page'] == 0) $all = true;
if (isset($this->params->params['paging']['Event']['page'])) {
if ($this->params->params['paging']['Event']['page'] == 0) $all = true;
$page = $this->params->params['paging']['Event']['page'];
} else {
$page = 0;
}
if (Configure::read('Plugin.Sightings_enable')) {
$attributeSightings = array();
$attributeOwnSightings = array();
if (isset($event['Sighting']) && !empty($event['Sighting'])) {
foreach ($event['Sighting'] as $sighting) {
$attributeSightings[$sighting['attribute_id']][] = $sighting;
if (isset($sighting['org_id']) && $sighting['org_id'] == $me['org_id']) {
if (isset($attributeOwnSightings[$sighting['attribute_id']])) $attributeOwnSightings[$sighting['attribute_id']]++;
else $attributeOwnSightings[$sighting['attribute_id']] = 1;
}
}
}
}
?>
<div class="pagination">
<ul>
@ -94,6 +112,7 @@
<th>Related Events</th>
<th title="<?php echo $attrDescriptions['signature']['desc'];?>"><?php echo $this->Paginator->sort('to_ids', 'IDS');?></th>
<th title="<?php echo $attrDescriptions['distribution']['desc'];?>"><?php echo $this->Paginator->sort('distribution');?></th>
<?php if (Configure::read('Plugin.Sightings_enable'))?><th>Sightings</th>
<th class="actions">Actions</th>
</tr>
<?php
@ -260,6 +279,25 @@
?>&nbsp;
</div>
</td>
<?php
endif;
if (Configure::read('Plugin.Sightings_enable')):
?>
<td class="short <?php echo $extra;?>">
<span id="sightingForm_<?php echo h($object['id']);?>">
<?php
if($object['objectType'] == 0):
echo $this->Form->create('Sighting', array('id' => 'Sighting_' . $object['id'], 'url' => '/sightings/add/' . $object['id'], 'style' => 'display:none;'));
echo $this->Form->end();
?>
</span>
<span class="icon-thumbs-up useCursorPointer" onClick="addSighting('<?php echo h($object['id']); ?>', '<?php echo h($event['Event']['id']);?>', '<?php echo h($page); ?>');">&nbsp;</span>
<span id="sightingCount_<?php echo h($object['id']); ?>" class="bold sightingsCounter_<?php echo h($object['id']); ?>"><?php echo (!empty($attributeSightings[$object['id']]) ? count($attributeSightings[$object['id']]) : 0); ?></span>
(<span id="ownSightingCount_<?php echo h($object['id']); ?>" class="bold green sightingsCounter_<?php echo h($object['id']); ?>"><?php echo (isset($attributeOwnSightings[$object['id']]) ? $attributeOwnSightings[$object['id']] : 0); ?></span>)
<?php
endif;
?>
</td>
<?php
endif;
?>

View File

@ -1,6 +1,14 @@
<?php
$mayModify = (($isAclModify && $event['Event']['user_id'] == $me['id'] && $event['Orgc']['id'] == $me['org_id']) || ($isAclModifyOrg && $event['Orgc']['id'] == $me['org_id']));
$mayPublish = ($isAclPublish && $event['Orgc']['id'] == $me['org_id']);
if (Configure::read('Plugin.Sightings_enable')) {
if (isset($event['Sighting']) && !empty($event['Sighting'])) {
$ownSightings = array();
foreach ($event['Sighting'] as $sighting) {
if (isset($sighting['org_id']) && $sighting['org_id'] == $me['org_id']) $ownSightings[] = $sighting;
}
}
}
?>
<?php
echo $this->element('side_menu', array('menuList' => 'event', 'menuItem' => 'viewEvent', 'mayModify' => $mayModify, 'mayPublish' => $mayPublish));
@ -126,26 +134,27 @@ $mayPublish = ($isAclPublish && $event['Orgc']['id'] == $me['org_id']);
&nbsp;
</dd>
<?php
$published = '';
$notPublished = 'style="display:none;"';
if ($event['Event']['published'] == 0) {
$published = 'style="display:none;"';
$notPublished = '';
$dtclass='published';
$ddclass='published green';
if (!$event['Event']['published']) {
if ($isAclPublish) {
$dtclass = $ddclass = 'visibleDL notPublished';
} else {
$dtclass = 'notPublished';
$ddclass = 'nobPublished red';
}
}
?>
<dt class="published" <?php echo $published;?>>Published</dt>
<dd class="published green" <?php echo $published;?>>Yes</dd>
<?php
if ($isAclPublish) :
?>
<dt class="visibleDL notPublished" <?php echo $notPublished;?>>Published</dt>
<dd class="visibleDL notPublished" <?php echo $notPublished;?>>No</dd>
<?php
else:
?>
<dt class="notPublished" <?php echo $notPublished;?>>Published</dt>
<dd class="notPublished red" <?php echo $notPublished;?>>No</dd>
<?php endif; ?>
<dt class="<?php echo $dtclass;?>">Published</dt>
<dd class="<?php echo $ddclass;?>"><?php echo ($event['Event']['published'] ? 'Yes' : 'No');?></dd>
<?php if (Configure::read('Plugin.Sightings_enable')): ?>
<dt>Sightings</dt>
<dd style="word-wrap: break-word;">
<span id="eventSightingCount" class="bold sightingsCounter"><?php echo count($event['Sighting']); ?></span>
(<span id="eventOwnSightingCount" class="green bold sightingsCounter"><?php echo isset($ownSightings) ? count($ownSightings) : 0; ?></span>)
<?php if(!Configure::read('Plugin.Sightings_policy')) echo '- restricted to own organisation only.'?>
</dd>
<?php endif;?>
</dl>
</div>
<?php if (!empty($event['RelatedEvent'])):?>

View File

@ -0,0 +1,4 @@
<?php
echo $this->Form->create('Sighting', array('id' => 'Sighting_' . $id, 'url' => '/sightings/add/' . $id, 'style' => 'display:none;'));
echo $this->Form->end();
?>

View File

@ -236,6 +236,29 @@ function postActivationScripts(name, type, id, field, event) {
$(name + '_solid').hide();
}
function addSighting(attribute_id, event_id, $page) {
$.ajax({
data: $('#Sighting_' + attribute_id).closest("form").serialize(),
cache: false,
success:function (data, textStatus) {
handleGenericAjaxResponse(data);
var result = JSON.parse(data);
if (result.saved == true) {
$('.sightingsCounter').each(function( counter ) {
$(this).html(parseInt($(this).html()) + 1);
});
updateIndex(event_id, 'event');
}
},
error:function() {
showMessage('fail', 'Request failed for an unknown reason.');
updateIndex(context, 'event');
},
type:"post",
url:"/sightings/add/" + attribute_id
});
}
function resetForms() {
$('.inline-field-solid').show();
$('.inline-field-placeholder').empty();