- Performance improvements when exporting a large number of attributes into Bro format.

- Fixed file header formatting for the export to Bro format (tabs used consistently).
- Computing the time needed for generating the export to Bro format when done using a background job.
- When generating the Bro export from the UI all the attributes are generated in one single text file similar to the CSV export instead of a zip file with different files inside.
- Changed the file extension of Bro export files from ".intel" to ".txt".
- Removed the allowNonIDS option from the Bro export as it doesn’t make sense to have it (Bro is an IDS).
- Fixed some of the API endpoints which were not accepted (ACL issues).
- Added support for a list of events that should be / should not be included in the export.
- Added a new "meta.desc" column (added in Bro 2.5, see https://www.bro.org/sphinx/frameworks/intel.html) containing the description of the event and of the attribute.
- Sanitized the exported data for Bro.
- Fixed a number of value substitutions which were imported from Snort/Suricata and which were not working for Bro. Did instead substitutions needed for Bro.
pull/1726/head
Liviu Valsan 2016-12-07 16:33:17 +01:00
parent 59509699e4
commit 4c022beafc
7 changed files with 162 additions and 179 deletions

View File

@ -329,27 +329,36 @@ class EventShell extends AppShell
public function cachebro()
{
$broHeader = "#fields indicator\tindicator_type\tmeta.source\tmeta.url\tmeta.do_notice\tmeta.if_in\n";
$timeStart = time();
$broHeader = "#fields\tindicator\tindicator_type\tmeta.source\tmeta.desc\tmeta.url\tmeta.do_notice\tmeta.if_in\n";
$userId = $this->args[0];
$user = $this->User->getAuthUser($userId);
$id = $this->args[1];
$this->Job->id = $id;
$format = $this->args[2];
$this->Job->saveField('progress', 1);
$types = array('ip', 'email', 'domain', 'filename', 'filehash', 'certhash', 'software', 'url'); //Bro types
App::uses('BroExport', 'Export');
$export = new BroExport();
$types = array_keys($export->mispTypes);
$typeCount = count($types);
$dir = new Folder(APP . DS . '/tmp/cached_exports/' . $format, true, 0750);
$dir = new Folder(APP . DS . '/tmp/cached_exports/bro', true, 0750);
if ($user['Role']['perm_site_admin']) {
$zipname = DS . 'misp.bro.ADMIN.intel.zip';
$file = new File($dir->pwd() . DS . 'misp.bro.ADMIN.txt');
} else {
$zipname = DS . 'misp.bro.' . $user['Organisation']['name'] . '.intel.zip';
$file = new File($dir->pwd() . DS . 'misp.bro.' . $user['Organisation']['name'] . '.txt');
}
$tmpZipname = $this->Attribute->brozip($user, false, false, false, false, false, false, $id);
rename($tmpZipname[0] . $tmpZipname[1], $dir->pwd() . $zipname);
$folder = new Folder($tmpZipname[0]);
$folder->delete();
foreach ($types as $k => $type) {
$final = $this->Attribute->bro($user, $type);
foreach ($final as $attribute) {
$file->append($attribute . PHP_EOL);
}
$this->Job->saveField('progress', $k / $typeCount * 100);
}
$file->close();
$timeDelta = (time()-$timeStart);
$this->Job->saveField('progress', 100);
$this->Job->saveField('message', 'Job done.');
$this->Job->saveField('message', 'Job done. (in '.$timeDelta.'s)');
$this->Job->saveField('date_modified', date("y-m-d H:i:s"));
}
public function alertemail() {

View File

@ -54,7 +54,7 @@ class AppController extends Controller {
// This is used to allow authentication via headers for methods not covered by _isRest() - as that only checks for JSON and XML formats
public $automationArray = array(
'events' => array('csv', 'nids', 'hids', 'xml', 'restSearch', 'stix', 'updateGraph', 'downloadOpenIOCEvent'),
'attributes' => array('text', 'downloadAttachment', 'returnAttributes', 'restSearch', 'rpz'),
'attributes' => array('text', 'downloadAttachment', 'returnAttributes', 'restSearch', 'rpz', 'bro'),
);
public function __construct($id = false, $table = null, $ds = null) {

View File

@ -23,6 +23,7 @@ class AttributesController extends AppController {
$this->Auth->allow('downloadAttachment');
$this->Auth->allow('text');
$this->Auth->allow('rpz');
$this->Auth->allow('bro');
// permit reuse of CSRF tokens on the search page.
if ('search' == $this->request->params['action']) {
@ -459,7 +460,7 @@ class AttributesController extends AppController {
$info['distribution'][$key] = array('key' => $value, 'desc' => $this->Attribute->distributionDescriptions[$key]['formdesc']);
}
$this->set('info', $info);
$this->loadModel('SharingGroup');
$sgs = $this->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', 1);
$this->set('sharingGroups', $sgs);
@ -671,7 +672,7 @@ class AttributesController extends AppController {
}
if ($this->request->is('post') || $this->request->is('put')) {
if (!isset($this->request->data['Attribute'])) {
$this->request->data['Attribute'] = $this->request->data;
$this->request->data['Attribute'] = $this->request->data;
}
$existingAttribute = $this->Attribute->findByUuid($this->Attribute->data['Attribute']['uuid']);
// check if the attribute has a timestamp already set (from a previous instance that is trying to edit via synchronisation)
@ -1624,7 +1625,7 @@ class AttributesController extends AppController {
foreach ($found_orgs as $o) $subcondition['OR'][] = array('Event.orgc_id' => $o['Org']['id']);
} else if ($parameters[$k] === 'eventid') {
if (!empty($v)) $subcondition['OR'][] = array('Attribute.event_id' => $v);
} else if ($parameters[$k] === 'uuid') {
} else if ($parameters[$k] === 'uuid') {
$subcondition['OR'][] = array('Attribute.uuid' => $v);
$subcondition['OR'][] = array('Event.uuid' => $v);
} else {
@ -1666,7 +1667,7 @@ class AttributesController extends AppController {
}
if ($last) $conditions['AND'][] = array('Event.publish_timestamp >=' => $last);
if ($published) $conditions['AND'][] = array('Event.published' => $published);
// change the fields here for the attribute export!!!! Don't forget to check for the permissions, since you are not going through fetchevent. Maybe create fetchattribute?
$params = array(
'conditions' => $conditions,
@ -1919,7 +1920,7 @@ class AttributesController extends AppController {
$this->render('/Attributes/rpz');
}
public function bro($key='download', $type='all', $tags=false, $eventId=false, $allowNonIDS=false, $from=false, $to=false, $last=false) {
public function bro($key='download', $type='all', $tags=false, $eventId=false, $from=false, $to=false, $last=false) {
if ($this->request->is('post')) {
if ($this->request->input('json_decode', true)) {
$data = $this->request->input('json_decode', true);
@ -1929,49 +1930,39 @@ class AttributesController extends AppController {
if (!empty($data) && !isset($data['request'])) {
$data = array('request' => $data);
}
$paramArray = array('type', 'tags', 'eventId', 'allowNonIDS', 'from', 'to', 'last');
$paramArray = array('type', 'tags', 'eventId', 'from', 'to', 'last');
foreach ($paramArray as $p) {
if (isset($data['request'][$p])) ${$p} = $data['request'][$p];
}
}
$simpleFalse = array('type', 'tags', 'eventId', 'allowNonIDS', 'from', 'to', 'last');
$simpleFalse = array('type', 'tags', 'eventId', 'from', 'to', 'last');
foreach ($simpleFalse as $sF) {
if (!is_array(${$sF}) && (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false')) ${$sF} = false;
}
if ($type !== 'null' || $type !== '0' || $type !== 'false') {
if ($from) $from = $this->Attribute->Event->dateFieldCheck($from);
if ($to) $to = $this->Attribute->Event->dateFieldCheck($to);
if ($last) $last = $this->Attribute->Event->resolveTimeDelta($last);
if ($key != 'download') {
// check if the key is valid -> search for users based on key
$user = $this->checkAuthUser($key);
if (!$user) {
throw new UnauthorizedException('This authentication key is not authorized to be used for exports. Contact your administrator.');
}
} else {
if (!$this->Auth->user('id')) {
throw new UnauthorizedException('You have to be logged in to do that.');
}
if ($type === 'null' || $type === '0' || $type === 'false') $type = 'all';
if ($from) $from = $this->Attribute->Event->dateFieldCheck($from);
if ($to) $to = $this->Attribute->Event->dateFieldCheck($to);
if ($last) $last = $this->Attribute->Event->resolveTimeDelta($last);
if ($key != 'download') {
// check if the key is valid -> search for users based on key
$user = $this->checkAuthUser($key);
if (!$user) {
throw new UnauthorizedException('This authentication key is not authorized to be used for exports. Contact your administrator.');
}
$filename = 'misp.' . $type . '.intel';
if ($eventId) {
$filename = 'misp.' . $type . '.event_' . $eventId . '.intel';
} else {
if (!$this->Auth->user('id')) {
throw new UnauthorizedException('You have to be logged in to do that.');
}
if ($type != 'all') {
$responseFile = implode(PHP_EOL, $this->Attribute->bro($this->Auth->user(), $type, $tags, $eventId, $allowNonIDS, $from, $to, $last)) . PHP_EOL;
$this->response->body($responseFile);
$this->response->type('txt');
} else {
$tmpZipname = $this->Attribute->brozip($this->Auth->user(), $tags, $eventId, $allowNonIDS, $from, $to, $last);
$this->response->body(file_get_contents($tmpZipname[0] . $tmpZipname[1]));
$this->response->type('zip');
$folder = new Folder($tmpZipname[0]);
$folder->delete();
$filename .= '.zip';
}
$this->response->download($filename);
return $this->response;
}
$filename = 'misp.' . $type . '.txt';
if ($eventId) {
$filename = 'misp.' . $type . '.event_' . $eventId . '.txt';
}
$responseFile = implode(PHP_EOL, $this->Attribute->bro($this->Auth->user(), $type, $tags, $eventId, $from, $to, $last)) . PHP_EOL;
$this->response->body($responseFile);
$this->response->type('txt');
$this->response->download($filename);
return $this->response;
}
public function reportValidationIssuesAttributes($eventId = false) {
@ -2350,7 +2341,7 @@ class AttributesController extends AppController {
$this->Session->setFlash('Removed ' . count($orphans) . ' attribute(s).');
$this->redirect(Router::url($this->referer(), true));
}
public function checkOrphanedAttributes() {
if (!$this->_isSiteAdmin()) throw new MethodNotAllowedException('You are not authorised to do that.');
$this->loadModel('Attribute');
@ -2457,7 +2448,7 @@ class AttributesController extends AppController {
public function describeTypes() {
$result = array();
foreach ($this->Attribute->typeDefinitions as $key => $value) {
$result['sane_defaults'][$key] = array('default_category' => $value['default_category'], 'to_ids' => $value['to_ids']);
$result['sane_defaults'][$key] = array('default_category' => $value['default_category'], 'to_ids' => $value['to_ids']);
}
$result['types'] = array_keys($this->Attribute->typeDefinitions);
$result['categories'] = array_keys($this->Attribute->categoryDefinitions);

View File

@ -56,6 +56,7 @@ class ACLComponent extends Component {
'restSearch' => array('*'),
'returnAttributes' => array('*'),
'rpz' => array('*'),
'bro' => array('*'),
'search' => array('*'),
'searchAlternate' => array('*'),
'text' => array('*'),

View File

@ -1,11 +1,11 @@
<?php
class BroExport {
public $rules = array();
public $header = "#fields indicator\tindicator_type\tmeta.source\tmeta.url\tmeta.do_notice\tmeta.if_in";
public $header = "#fields\tindicator\tindicator_type\tmeta.source\tmeta.desc\tmeta.url\tmeta.do_notice\tmeta.if_in";
// mapping from misp attribute type to the bro intel type
// alternative mechanisms are:
// - alternate: array containing a detection regex and a replacement bro type
@ -32,7 +32,7 @@ class BroExport {
'filename|sha256' => array('brotype' => 'FILE_NAME', 'composite' => 'FILE_HASH'),
'x509-fingerprint-sha1' => array('brotype' => 'CERT_HASH'),
);
// export group to misp type mapping
// the mapped type is in an array format, first value being the misp type, second being the value field used
public $mispTypes = array(
@ -81,7 +81,8 @@ class BroExport {
private $whitelist = null;
public function export($items, $orgs, $valueField, $intel, $whitelist, $instanceString) {
public function export($items, $orgs, $valueField, $whitelist, $instanceString) {
$intel = array();
//For bro format organisation
$orgsName = array();
// generate the rules
@ -92,65 +93,62 @@ class BroExport {
$orgName = $instanceString . ' (' . $item['Event']['uuid'] . ')' . ' - ' . $orgs[$item['Event']['orgc_id']];
}
$ruleFormatReference = Configure::read('MISP.baseurl') . '/events/view/' . $item['Event']['id'];
$ruleFormat = "%s\t%s\t" . $orgName . "\t" . $ruleFormatReference . "\t%s\t%s";
$ruleFormat = "%s\t%s\t" . $orgName . "\t" . $this->replaceIllegalChars($item['Event']['info']) . ". %s" . "\t" . $ruleFormatReference . "\t%s\t%s";
$rule = $this->__generateRule($item['Attribute'], $ruleFormat, $valueField, $whitelist);
if (!empty($rule)) {
if (!in_array($rule, $intel)) {
$intel[] = $rule;
}
$intel[] = $rule;
}
}
return $intel;
}
}
private function __generateRule($attribute, $ruleFormat, $valueField, $whitelist) {
if (isset($this->mapping[$attribute['type']])) {
$brotype = $this->mapping[$attribute['type']]['brotype'];
$overruled = $this->checkWhitelist($attribute['value'], $whitelist);
if (isset($this->mapping[$attribute['type']]['alternate'])) {
if (preg_match($this->mapping[$attribute['type']]['alternate'][0], $attribute['value'])) {
$brotype = $this->mapping[$attribute['type']]['alternate'][1];
if (! $this->checkWhitelist($attribute['value'], $whitelist)) {
$brotype = $this->mapping[$attribute['type']]['brotype'];
if (isset($this->mapping[$attribute['type']]['alternate'])) {
if (preg_match($this->mapping[$attribute['type']]['alternate'][0], $attribute['value'])) {
$brotype = $this->mapping[$attribute['type']]['alternate'][1];
}
}
}
if ($valueField == 2 && isset($this->mapping[$attribute['type']]['composite'])) {
$brotype = $this->mapping[$attribute['type']]['composite'];
}
$attribute['value'] = $this->replaceIllegalChars($attribute['value']); // substitute chars not allowed in rule
if (isset($this->mapping[$attribute['type']]['replace'])) {
$attribute['value'] = preg_replace(
$this->mapping[$attribute['type']]['replace'][0],
$this->mapping[$attribute['type']]['replace'][1],
$attribute['value']
);
}
return sprintf($ruleFormat,
($overruled) ? '#OVERRULED BY WHITELIST# ' :
$attribute['value'], // value - for composite values only the relevant element is taken
'Intel::' . $brotype, // type
'T', // meta.do_notice
'-' // meta.if_in
if ($valueField == 2 && isset($this->mapping[$attribute['type']]['composite'])) {
$brotype = $this->mapping[$attribute['type']]['composite'];
}
$attribute['value'] = $this->replaceIllegalChars($attribute['value']); // substitute chars not allowed in rule
if (isset($this->mapping[$attribute['type']]['replace'])) {
$attribute['value'] = preg_replace(
$this->mapping[$attribute['type']]['replace'][0],
$this->mapping[$attribute['type']]['replace'][1],
$attribute['value']
);
}
}
return sprintf($ruleFormat,
$attribute['value'], // value - for composite values only the relevant element is taken
'Intel::' . $brotype, // type
$attribute['comment'],
'T', // meta.do_notice
'-' // meta.if_in
);
}
}
return false;
}
/**
* Replaces characters that are not allowed in a signature.
* example: " is converted to |22|
* @param unknown_type $value
*/
public static function replaceIllegalChars($value) {
$replace_pairs = array(
'|' => '|7c|', // Needs to stay on top !
'"' => '|22|',
';' => '|3b|',
':' => '|3a|',
'\\' => '|5c|',
'0x' => '|30 78|'
"\t" => ' ',
"\x0B" => ' ',
"\r" => ' ',
"\r\n" => ' ',
"\n" => ' '
);
return strtr($value, $replace_pairs);
return html_entity_decode(filter_var(strtr($value, $replace_pairs), FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH));
}
public function checkWhitelist($value, $whitelist) {
foreach ($whitelist as $wlitem) {
if (preg_match($wlitem, $value)) {
@ -159,12 +157,12 @@ class BroExport {
}
return false;
}
public function getMispTypes($type) {
$mispTypes = array();
if (isset($this->mispTypes[$type])) {
$mispTypes = $this->mispTypes[$type];
$mispTypes = $this->mispTypes[$type];
}
return $mispTypes;
}
}
}

View File

@ -1413,57 +1413,77 @@ class Attribute extends AppModel {
return $values;
}
function bro($user, $type, $tags = false, $eventId = false, $from = false, $to = false, $last = false) {
//restricting to non-private or same org if the user is not a site-admin.
$conditions['AND'] = array('Attribute.to_ids' => 1, 'Event.published' => 1);
if ($from) $conditions['AND']['Event.date >='] = $from;
if ($to) $conditions['AND']['Event.date <='] = $to;
if ($last) $conditions['AND']['Event.publish_timestamp >='] = $last;
if ($eventId !== false) {
$conditions['AND'][] = array('Event.id' => $eventId);
}
else if ($tags !== false) {
// If we sent any tags along, load the associated tag names for each attribute
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
$temp = array();
foreach ($tagArray[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($tagArray[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
public function bro($user, $type, $tags = false, $eventId = false, $from = false, $to = false, $last = false) {
App::uses('BroExport', 'Export');
$export = new BroExport();
$this->Whitelist = ClassRegistry::init('Whitelist');
$this->whitelist = $this->Whitelist->getBlockedValues();
$instanceString = 'MISP';
if (Configure::read('MISP.host_org_id') && Configure::read('MISP.host_org_id') > 0) {
$this->Event->Orgc->id = Configure::read('MISP.host_org_id');
if ($this->Event->Orgc->exists()) {
$instanceString = $this->Event->Orgc->field('name') . ' MISP';
if ($type == 'all') {
$types = array_keys($export->mispTypes);
} else {
$types = array($type);
}
$intel = array();
foreach ($types as $type) {
//restricting to non-private or same org if the user is not a site-admin.
$conditions['AND'] = array('Attribute.to_ids =' => 1, 'Event.published =' => 1);
if ($from) $conditions['AND']['Event.date >='] = $from;
if ($to) $conditions['AND']['Event.date <='] = $to;
if ($last) $conditions['AND']['Event.publish_timestamp >='] = $last;
if ($eventId !== false) {
$temp = array();
$args = $this->dissectArgs($eventId);
foreach ($args[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($args[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
if ($tags !== false) {
// If we sent any tags along, load the associated tag names for each attribute
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
$temp = array();
foreach ($tagArray[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($tagArray[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
$this->Whitelist = ClassRegistry::init('Whitelist');
$this->whitelist = $this->Whitelist->getBlockedValues();
$instanceString = 'MISP';
if (Configure::read('MISP.host_org_id') && Configure::read('MISP.host_org_id') > 0) {
$this->Event->Orgc->id = Configure::read('MISP.host_org_id');
if ($this->Event->Orgc->exists()) {
$instanceString = $this->Event->Orgc->field('name') . ' MISP';
}
}
$mispTypes = $export->getMispTypes($type);
foreach($mispTypes as $mispType) {
$conditions['AND']['Attribute.type'] = $mispType[0];
$intel = array_merge($intel, $this->__bro($user, $conditions, $mispType[1], $export, $this->whitelist, $instanceString));
}
}
$mispTypes = $export->getMispTypes($type);
$intel = array($export->header);
foreach($mispTypes as $mispType) {
$conditions['AND']['Attribute.type'] = $mispType[0];
$intel = $this->__bro($intel, $user, $conditions, $mispType[1], $export, $this->whitelist, $instanceString);
}
natsort($intel);
$intel = array_unique($intel);
array_unshift($intel, $export->header);
return $intel;
}
private function __bro($intel, $user, $conditions, $valueField, $export, $whitelist, $instanceString) {
private function __bro($user, $conditions, $valueField, $export, $whitelist, $instanceString) {
$attributes = $this->fetchAttributes($user, array(
'conditions' => $conditions, // array of conditions
'order' => 'Attribute.value' . $valueField . ' ASC',
'recursive' => -1, // int
'fields' => array('Attribute.id', 'Attribute.event_id', 'Attribute.type', 'Attribute.value' . $valueField . " as value"),
'fields' => array('Attribute.id', 'Attribute.event_id', 'Attribute.type', 'Attribute.comment', 'Attribute.value' . $valueField . " as value"),
'contain' => array('Event' => array('fields' => array('Event.id', 'Event.threat_level_id', 'Event.orgc_id', 'Event.uuid'))),
'group' => array('Attribute.type', 'Attribute.value' . $valueField), // fields to GROUP BY
)
@ -1471,43 +1491,7 @@ class Attribute extends AppModel {
$orgs = $this->Event->Orgc->find('list', array(
'fields' => array('Orgc.id', 'Orgc.name')
));
return $export->export($attributes, $orgs, $valueField, $intel, $whitelist, $instanceString);
}
public function brozip($user, $tags, $eventId, $allowNonIDS, $from, $to, $last, $jobId = false) {
App::uses('BroExport', 'Export');
$export = new BroExport();
$types = array_keys($export->mispTypes);
$typeCount = count($types);
if ($jobId) {
$this->Job = ClassRegistry::init('Job');
$this->Job->id = $jobId;
if (!$this->Job->exists()) {
$jobId = false;
}
}
$dir = new Folder(APP . 'tmp/files/' . $this->Event->generateRandomFileName(), true, 0750);
$tmpZipname = DS . "bro_export_tmp.zip";
$zip = new File($dir->pwd() . $tmpZipname);
foreach ($types as $k => $type) {
$final = $this->bro($user, $type, $tags, $eventId, $allowNonIDS, $from, $to, $last);
$filename = $type . '.intel';
$file = new File($dir->pwd() . DS . $filename);
$file->write(implode(PHP_EOL, $final));
$file->close();
$execRetval = '';
$execOutput = array();
exec('zip -gj ' . $zip->path . ' ' . $dir->pwd() . '/' . $filename, $execOutput, $execRetval);
if ($execRetval != 0) { // not EXIT_SUCCESS
throw new Exception('An error has occured while attempting to zip the intel files.');
}
$file->delete(); // delete the original non-zipped-file
if ($jobId) {
$this->Job->saveField('progress', $k / $typeCount * 100);
}
}
$zip->close();
return array($dir->pwd(), $tmpZipname);
return $export->export($attributes, $orgs, $valueField, $whitelist, $instanceString);
}
public function generateCorrelation($jobId = false, $startPercentage = 0) {

View File

@ -99,7 +99,7 @@ class Event extends AppModel {
'description' => 'Click this to download all network related attributes that you have access to under the Snort rule format. Only published events and attributes marked as IDS Signature are exported. Administration is able to maintain a whitelist containing host, domain name and IP numbers to exclude from the NIDS export.',
),
'bro' => array(
'extension' => '.intel.zip',
'extension' => '.txt',
'type' => 'Bro',
'requiresPublished' => 1,
'canHaveAttachments' => false,