diff --git a/INSTALL/INSTALL.centos6.txt b/INSTALL/INSTALL.centos6.txt index 8257f3abe..5deaa08d9 100644 --- a/INSTALL/INSTALL.centos6.txt +++ b/INSTALL/INSTALL.centos6.txt @@ -191,7 +191,6 @@ chmod 700 /var/www/MISP/.gnupg # can't connect to `/var/www/MISP/.gnupg/S.gpg-agent': No such file or directory gpg --homedir /var/www/MISP/.gnupg --gen-key chown -R apache:apache /var/www/MISP/.gnupg -# Recommended key type: RSA # The email address should match the one set in the config.php configuration file # Make sure that you use the same settings in the MISP Server Settings tool (Described on line 212) diff --git a/INSTALL/INSTALL.centos7.txt b/INSTALL/INSTALL.centos7.txt index faae937ba..37bd3910f 100644 --- a/INSTALL/INSTALL.centos7.txt +++ b/INSTALL/INSTALL.centos7.txt @@ -205,7 +205,7 @@ chmod 700 /var/www/MISP/.gnupg # can't connect to `/var/www/MISP/.gnupg/S.gpg-agent': No such file or directory gpg --homedir /var/www/MISP/.gnupg --gen-key chown -R apache:apache /var/www/MISP/.gnupg -# Recommended key type: RSA + # The email address should match the one set in the config.php configuration file # Make sure that you use the same settings in the MISP Server Settings tool (Described on line 226) diff --git a/INSTALL/INSTALL.ubuntu1404.txt b/INSTALL/INSTALL.ubuntu1404.txt index 4e2e0d61b..3a7f694ae 100644 --- a/INSTALL/INSTALL.ubuntu1404.txt +++ b/INSTALL/INSTALL.ubuntu1404.txt @@ -151,6 +151,7 @@ cp -a config.default.php config.php # require_once dirname(__DIR__) . '/Vendor/autoload.php'; # Important! Change the salt key in /var/www/MISP/app/Config/config.php +# The salt key must be an at least 32 byte long string. # The admin user account will be generated on the first login, make sure that the salt is changed before you create that user # If you forget to do this step, and you are still dealing with a fresh installation, just alter the salt, # delete the user from mysql and log in again using the default admin credentials (admin@admin.test / admin) @@ -164,7 +165,7 @@ mkdir /var/www/MISP/.gnupg chown www-data:www-data /var/www/MISP/.gnupg chmod 700 /var/www/MISP/.gnupg sudo -u www-data gpg --homedir /var/www/MISP/.gnupg --gen-key -# Recommended key type: RSA + # The email address should match the one set in the config.php configuration file # Make sure that you use the same settings in the MISP Server Settings tool (Described on line 184) diff --git a/INSTALL/UPDATE.txt b/INSTALL/UPDATE.txt index 5965b23f9..cdc1933db 100644 --- a/INSTALL/UPDATE.txt +++ b/INSTALL/UPDATE.txt @@ -7,7 +7,7 @@ git pull # 2. Update CakePHP to the latest 2.6 code -cd /var/www/MISP/Lib/cakephp +cd /var/www/MISP/app/Lib/cakephp git fetch origin git checkout 2.6 diff --git a/VERSION.json b/VERSION.json index b3ea3fe9c..0e2afcbbe 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1 +1 @@ -{"major":2, "minor":3, "hotfix":63} +{"major":2, "minor":3, "hotfix":64} diff --git a/app/Config/bootstrap.default.php b/app/Config/bootstrap.default.php index 1c795864c..45280f5b1 100755 --- a/app/Config/bootstrap.default.php +++ b/app/Config/bootstrap.default.php @@ -108,6 +108,12 @@ CakePlugin::load('Assets'); // having Logable CakePlugin::load('SysLogLogable'); CakePlugin::load('UrlCache'); +/** + * Uncomment the following line to enable client SSL certificate authentication. + * It's also necessary to configure the plugin — for more information, please read app/Plugin/CertAuth/reame.md + */ +// CakePlugin::load('CertAuth'); + /** * You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters: * diff --git a/app/Config/config.default.php b/app/Config/config.default.php index 457973c24..84275a6e4 100644 --- a/app/Config/config.default.php +++ b/app/Config/config.default.php @@ -6,6 +6,7 @@ $config = array ( 'level' => 'medium', 'salt' => 'Rooraenietu8Eeyo '', + //'auth'=>array('CertAuth.Certificate'), // additional authentication methods ), 'MISP' => array ( @@ -48,4 +49,33 @@ $config = array ( 'amount' => 5, 'expire' => 300, ), + // Uncomment the following to enable client SSL certificate authentication + /* + 'CertAuth' => + array( + 'ca' => array( 'FIRST.Org' ), // allowed CAs + 'caId' => 'O', // which attribute will be used to verify the CA + 'userModel' => 'User', // name of the User class to check if user exists + 'userModelKey' => 'nids_sid', // User field that will be used for querying + 'map' => array( // maps client certificate attributes to User properties + 'O' => 'org', + 'emailAddress'=>'email', + ), + 'syncUser' => true, // should the User be synchronized with an external REST API + 'userDefaults'=> array( // default user attributes, only used when creating new users + 'role_id' => 4, + ), + 'restApi' => array( // API parameters + 'url' => 'https://example.com/data/users', // URL to query + 'headers' => array(), // additional headers, used for authentication + 'param' => array( 'email' => 'email'), // query parameters to add to the URL, mapped to USer properties + 'map' => array( // maps REST result to the User properties + 'uid' => 'nids_sid', + 'team' => 'org', + 'email' => 'email', + 'pgp_public'=> 'gpgkey', + ), + ), + ), + */ ); diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 2deba301b..43380e564 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -98,6 +98,7 @@ class AppController extends Controller { } } if ($user) { + unset($user['User']['gpgkey']); // User found in the db, add the user info to the session $this->Session->renew(); $this->Session->write(AuthComponent::$sessionKey, $user['User']); @@ -107,14 +108,33 @@ class AppController extends Controller { $this->Session->destroy(); throw new ForbiddenException('The authentication key provided cannot be used for syncing.'); } + unset($user); } + } else if(!$this->Session->read(AuthComponent::$sessionKey)) { + // load authentication plugins from Configure::read('Security.auth') + $auth = Configure::read('Security.auth'); + if($auth) { + $this->Auth->authenticate = array_merge($auth, $this->Auth->authenticate); + if($this->Auth->startup($this)) { + $user = $this->Auth->user(); + if ($user) { + unset($user['gpgkey']); + // User found in the db, add the user info to the session + $this->Session->renew(); + $this->Session->write(AuthComponent::$sessionKey, $user); + } + unset($user); + } + } + unset($auth); } + // user must accept terms // - if ($this->Session->check('Auth.User') && !$this->Auth->user('termsaccepted') && (!in_array($this->request->here, array('/users/terms', '/users/logout', '/users/login')))) { + if ($this->Session->check(AuthComponent::$sessionKey) && !$this->Auth->user('termsaccepted') && (!in_array($this->request->here, array('/users/terms', '/users/logout', '/users/login')))) { $this->redirect(array('controller' => 'users', 'action' => 'terms', 'admin' => false)); } - if ($this->Session->check('Auth.User') && $this->Auth->user('change_pw') && (!in_array($this->request->here, array('/users/terms', '/users/change_pw', '/users/logout', '/users/login')))) { + if ($this->Session->check(AuthComponent::$sessionKey) && $this->Auth->user('change_pw') && (!in_array($this->request->here, array('/users/terms', '/users/change_pw', '/users/logout', '/users/login')))) { $this->redirect(array('controller' => 'users', 'action' => 'change_pw', 'admin' => false)); } diff --git a/app/Controller/AttributesController.php b/app/Controller/AttributesController.php index 4b45abdaa..44983df30 100755 --- a/app/Controller/AttributesController.php +++ b/app/Controller/AttributesController.php @@ -1495,11 +1495,6 @@ class AttributesController extends AppController { // ! - you can negate a search term. For example: google.com&&!mail would search for all attributes with value google.com but not ones that include mail. www.google.com would get returned, mail.google.com wouldn't. public function restSearch($key='download', $value=false, $type=false, $category=false, $org=false, $tags=false, $from=false, $to=false) { if ($tags) $tags = str_replace(';', ':', $tags); - if ($tags === 'null') $tags = null; - if ($value === 'null') $value = null; - if ($type === 'null') $type = null; - if ($category === 'null') $category = null; - if ($org === 'null') $org = null; if ($key!=null && $key!='download') { $user = $this->checkAuthUser($key); } else { @@ -1530,6 +1525,13 @@ class AttributesController extends AppController { else ${$p} = null; } } + $simpleFalse = array('value' , 'type', 'category', 'org', 'tags', 'from', 'to'); + foreach ($simpleFalse as $sF) { + if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; + } + + if ($from) $from = $this->Attribute->Event->dateFieldCheck($from); + if ($to) $from = $this->Attribute->Event->dateFieldCheck($to); if (!isset($this->request->params['ext']) || $this->request->params['ext'] !== 'json') { $this->response->type('xml'); // set the content type $this->layout = 'xml/default'; @@ -1768,8 +1770,8 @@ class AttributesController extends AppController { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } if ($type === 'null' || $type === '0' || $type === 'false') $type = 'all'; - if ($from && !preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $from)) $from = false; - if ($to && !preg_match('/^[0-9]{4}-[l0-9]{2}-[0-9]{2}$/', $from)) $from = false; + if ($from) $from = $this->Attribute->Event->dateFieldCheck($from); + if ($to) $from = $this->Attribute->Event->dateFieldCheck($to); if ($key != 'download') { // check if the key is valid -> search for users based on key $user = $this->checkAuthUser($key); diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index 2e205065c..cdc4df007 100755 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -1725,6 +1725,8 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); if ($tags) $tags = str_replace(';', ':', $tags); $eventIdArray = array(); @@ -1802,6 +1804,9 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); if ($tags) $tags = str_replace(';', ':', $tags); // backwards compatibility, swap key and format if ($format != 'snort' && $format != 'suricata') { @@ -1837,6 +1842,9 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); if ($tags) $tags = str_replace(';', ':', $tags); $this->response->type('txt'); // set the content type $this->header('Content-Disposition: download; filename="misp.' . $type . '.rules"'); @@ -1869,6 +1877,9 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); if ($tags) $tags = str_replace(';', ':', $tags); $list = array(); if ($key != 'download') { @@ -2399,6 +2410,9 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); if ($tags) $tags = str_replace(';', ':', $tags); if ($searchall === 'true') $searchall = "1"; @@ -2974,6 +2988,8 @@ class EventsController extends AppController { foreach ($simpleFalse as $sF) { if (${$sF} === 'null' || ${$sF} == '0' || ${$sF} === false || strtolower(${$sF}) === 'false') ${$sF} = false; } + if ($from) $from = $this->Event->dateFieldCheck($from); + if ($to) $from = $this->Event->dateFieldCheck($to); // set null if a null string is passed $numeric = false; diff --git a/app/Model/Event.php b/app/Model/Event.php index ba2c4b197..32b0c78d7 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -1916,4 +1916,12 @@ class Event extends AppModel { } return $fn; } + + // expects a date string in the DD-MM-YYYY format + // returns the passed string or false if the format is invalid + // based on the fix provided by stevengoosensB + public function dateFieldCheck($date) { + // regex check for from / to field by stevengoossensB + return (preg_match('/^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])$/', $date)) ? $date : false; + } } diff --git a/app/Plugin/CertAuth/Controller/Component/Auth/CertificateAuthenticate.php b/app/Plugin/CertAuth/Controller/Component/Auth/CertificateAuthenticate.php new file mode 100644 index 000000000..e638e5756 --- /dev/null +++ b/app/Plugin/CertAuth/Controller/Component/Auth/CertificateAuthenticate.php @@ -0,0 +1,273 @@ +self::$user[$k]); + } else { + $q = self::$user; + } + $User = ClassRegistry::init($cn); + $U = $User->find('first', array( + 'conditions' => $q, + 'recursive' => false + )); + if($U) { + if($sync) { + $write = array(); + foreach(self::$user as $k=>$v) { + if(array_key_exists($k, $U[$cn]) && trim($U[$cn][$k])!=trim($v)) { + $write[] = $k; + $U[$cn][$k] = trim($v); + } + unset($k, $v); + } + if($write && !$User->save($U[$cn], true, $write)) { + CakeLog::write('alert', 'Could not update model at database with RestAPI data.'); + } + unset($write); + } + self::$user = $U[$cn]; + } else if($sync) { + $User->create(); + $d = Configure::read('CertAuth.userDefaults'); + if($d && is_array($d)) { + self::$user += $d; + } + unset($d); + if($User->save(self::$user, true, array_keys(self::$user))) { + $U = $User->read(); + self::$user = $U[$cn]; + } else { + CakeLog::write('alert', 'Could not insert model at database from RestAPI data.'); + } + } + unset($U, $User, $q, $k); + } + unset($cn); + } + } + + return self::$user; + } + + // to enable stateless authentication + public function authenticate(CakeRequest $request, CakeResponse $response) + { + return self::getUser($request); + } + + /** + * Fetches user information from external REST API + * + * Valid options (should be configured under CertAuth.restApi): + * + * @param (optional) array $options API configuration + * url (string) Where to fetch information from + * headers (array) list of additional headers to be used, reserved for authentication tokens + * params (array) mapping of additional params to be included at the url, uses $user values + * map (array) mapping of the return values to be added to the self::$user + * @return array updated user object + */ + public function getRestUser($options=null, $user=null) + { + if(is_null($options)) { + $options = Configure::read('CertAuth.restApi'); + } + if(!is_null($user)) { + self::$user = $user; + } + + if(!isset($options['url'])) { + return null; + } + + // Create a stream + $req = array( + 'http'=>array( + 'method'=>'GET', + 'header'=>"Accept: application/json\r\n" + ), + ); + if(isset($options['headers'])) { + foreach($options['headers'] as $k=>$v) { + if(is_int($k)) { + $req['header'] .= "{$v}\r\n"; + } else { + $req['header'] .= "{$k}: {$v}\r\n"; + } + unset($k, $v); + } + } + + $url = $options['url']; + if(isset($options['param'])) { + foreach($options['param'] as $k=>$v) { + if(isset(self::$user[$v])) { + $url .= ((strpos($url, '?'))?('&'):('?')) + . $k . '=' . urlencode(self::$user[$v]); + } + unset($k, $v); + } + } + $ctx = stream_context_create($req); + $a = file_get_contents($url, false, $ctx); + if(!$a) return null; + + $A = json_decode($a, true); + if(!isset($A['data'][0])) return false; + if(isset($options['map'])) { + foreach($options['map'] as $k=>$v) { + if(isset($A['data'][0][$k])) { + self::$user[$v] = $A['data'][0][$k]; + } + unset($k, $v); + } + } + + return self::$user; + } + + protected static $instance; + + public static function ca() + { + if(is_null(self::$ca)) new CertificateAuthenticate(); + return self::$ca; + } + + public static function client() + { + if(is_null(self::$client)) new CertificateAuthenticate(); + return self::$client; + } + +} \ No newline at end of file diff --git a/app/Plugin/CertAuth/README.md b/app/Plugin/CertAuth/README.md new file mode 100644 index 000000000..065668144 --- /dev/null +++ b/app/Plugin/CertAuth/README.md @@ -0,0 +1,45 @@ +#Client SSL Certificate Authentication for CakePHP + +This plugin enables CakePHP applications to use client SSL certificates to stateless authenticate its users. It reads information from the client certificate and can synchronize data with a foreign REST API and the client User model. + +Basically it loads the `SSL_CLIENT_*` variables, parses and maps the certificate information to the user. So you first need a server that checks client certificates and forwards that information to the PHP `$_SERVER` environment. + +## Usage + +Enable the plugin at bootstrap.php: + +```php +CakePlugin::load('CertAuth'); +``` + +And configure it: + +```php +Configure::write('CertAuth', + array( + 'ca' => array( 'FIRST.Org' ), // allowed CAs + 'caId' => 'O', // which attribute will be used to verify the CA + 'userModel' => 'User', // name of the User class to check if user exists + 'userModelKey' => 'nids_sid', // User field that will be used for querying + 'map' => array( // maps client certificate attributes to User properties + 'O' => 'org', + 'emailAddress'=>'email', + ), + 'syncUser' => true, // should the User be synchronized with an external REST API + 'restApi' => array( // API parameters + 'url' => 'https://example.com/data/users', // URL to query + 'headers' => array(), // additional headers, used for authentication + 'param' => array( 'email' => 'email'), // query parameters to add to the URL, mapped to USer properties + 'map' => array( // maps REST result to the User properties + 'uid' => 'id', + 'name' => 'name', + 'company' => 'org', + 'email' => 'email', + ), + ), + ), +)); +``` + + + diff --git a/app/View/Users/admin_add.ctp b/app/View/Users/admin_add.ctp index 5d421c4b5..5c9531449 100644 --- a/app/View/Users/admin_add.ctp +++ b/app/View/Users/admin_add.ctp @@ -13,6 +13,9 @@ echo $this->Form->input('authkey', array('value' => $authkey, 'readonly' => 'readonly')); echo $this->Form->input('nids_sid'); echo $this->Form->input('gpgkey', array('label' => 'GPG key', 'div' => 'clear', 'class' => 'input-xxlarge')); + ?> +
Fetch GPG key
+ Form->input('autoalert', array('label' => 'Receive alerts when events are published')); echo $this->Form->input('contactalert', array('label' => 'Receive alerts from "contact reporter" requests')); diff --git a/app/View/Users/admin_edit.ctp b/app/View/Users/admin_edit.ctp index 85703d9d7..42101f08d 100755 --- a/app/View/Users/admin_edit.ctp +++ b/app/View/Users/admin_edit.ctp @@ -18,6 +18,9 @@ 'class' => 'datepicker', )); echo $this->Form->input('gpgkey', array('label' => 'GPG key', 'div' => 'clear', 'class' => 'input-xxlarge')); + ?> +
Fetch GPG key
+ Form->input('termsaccepted', array('label' => 'Terms accepted')); echo $this->Form->input('change_pw', array('type' => 'checkbox', 'label' => 'Change Password')); echo $this->Form->input('autoalert', array('label' => 'Receive alerts when events are published')); diff --git a/app/View/Users/edit.ctp b/app/View/Users/edit.ctp index b03204a49..fec270cd6 100755 --- a/app/View/Users/edit.ctp +++ b/app/View/Users/edit.ctp @@ -12,6 +12,9 @@ else echo $this->Form->input('role_id', array('disabled' => 'disabled')); // TODO ACL, check, My Profile not edit role_id. echo $this->Form->input('nids_sid'); echo $this->Form->input('gpgkey', array('label' => 'GPG key', 'div' => 'clear', 'class' => 'input-xxlarge')); + ?> +
Fetch GPG key
+ Form->input('autoalert', array('label' => 'Receive alerts when events are published')); echo $this->Form->input('contactalert', array('label' => 'Receive alerts from "contact reporter" requests')); ?> diff --git a/app/webroot/js/ajaxification.js b/app/webroot/js/ajaxification.js index 57dac8682..6aaed501d 100644 --- a/app/webroot/js/ajaxification.js +++ b/app/webroot/js/ajaxification.js @@ -1350,3 +1350,18 @@ function freetextImportResultsSubmit(id, count) { }, }); } + +function lookupPGPKey(emailFieldName) { + $.ajax({ + type: "get", + url: "https://pgp.mit.edu/pks/lookup?op=get&search=" + $('#' + emailFieldName).val(), + success: function (data) { + var result = $("
").html(data)[0].getElementsByTagName("pre")[0]['innerText']; + $("#UserGpgkey").val(result); + showMessage('success', "Key found!"); + }, + error: function (data, textStatus, errorThrown) { + showMessage('fail', textStatus + ": " + errorThrown); + } + }); +}