Merge branch 'develop' into 2.4

pull/9570/head
iglocska 2024-01-04 20:11:53 +01:00
commit d9901334bd
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
41 changed files with 1204 additions and 485 deletions

View File

@ -271,6 +271,7 @@ jobs:
pushd PyMISP/examples/events/
python ./create_massive_dummy_events.py -l 5 -a 30
popd
pip install jsonschema
python tools/misp-feed/validate.py
deactivate

2
PyMISP

@ -1 +1 @@
Subproject commit 190f82923687cebaf06138724e7c03239e529cb4
Subproject commit 81f5d596a7dd5cb1ca7213ac4fbdf07b402420b7

View File

@ -1 +1 @@
{"major":2, "minor":4, "hotfix":182}
{"major":2, "minor":4, "hotfix":183}

View File

@ -213,16 +213,18 @@ class UserShell extends AppShell
public function authkey_valid()
{
$cache = [];
$randomKey = random_bytes(16);
do {
$authkey = fgets(STDIN); // read line from STDIN
$authkey = trim($authkey);
if (strlen($authkey) !== 40) {
fwrite(STDOUT, "0\n"); // authkey is not in valid format
$this->log("Authkey in incorrect format provided.", LOG_WARNING);
continue;
}
$time = time();
// Generate hash from authkey to not store raw authkey in memory
$keyHash = hash('sha256', $authkey, true);
$keyHash = sha1($authkey . $randomKey, true);
if (isset($cache[$keyHash]) && $cache[$keyHash][1] > $time) {
fwrite(STDOUT, $cache[$keyHash][0] ? "1\n" : "0\n");
continue;
@ -250,6 +252,13 @@ class UserShell extends AppShell
}
$user = (bool)$user;
if (!$user) {
$start = substr($authkey, 0, 4);
$end = substr($authkey, -4);
$authKeyToStore = $start . str_repeat('*', 32) . $end;
$this->log("Not valid authkey $authKeyToStore provided.", LOG_WARNING);
}
// Cache results for 5 seconds
$cache[$keyHash] = [$user, $time + 5];
fwrite(STDOUT, $user ? "1\n" : "0\n");

View File

@ -34,7 +34,7 @@ class AppController extends Controller
public $helpers = array('OrgImg', 'FontAwesome', 'UserName');
private $__queryVersion = '157';
public $pyMispVersion = '2.4.182';
public $pyMispVersion = '2.4.183';
public $phpmin = '7.2';
public $phprec = '7.4';
public $phptoonew = '8.0';
@ -101,9 +101,7 @@ class AppController extends Controller
{
$controller = $this->request->params['controller'];
$action = $this->request->params['action'];
if (empty($this->Session->read('creation_timestamp'))) {
$this->Session->write('creation_timestamp', time());
}
if (Configure::read('MISP.system_setting_db')) {
App::uses('SystemSetting', 'Model');
SystemSetting::setGlobalSetting();
@ -362,6 +360,10 @@ class AppController extends Controller
}
}
}
if (Configure::read('MISP.enable_automatic_garbage_collection') && mt_rand(1,100) % 100 == 0) {
$this->loadModel('AdminSetting');
$this->AdminSetting->garbageCollect();
}
}
public function beforeRender()
@ -440,8 +442,7 @@ class AppController extends Controller
// User found in the db, add the user info to the session
if (Configure::read('MISP.log_auth')) {
$this->loadModel('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$change = $this->User->UserLoginProfile->_getUserProfile();
$change['http_method'] = $_SERVER['REQUEST_METHOD'];
$change['target'] = $this->request->here;
$this->Log->createLogEntry(
@ -541,13 +542,18 @@ class AppController extends Controller
}
}
$sessionCreationTime = $this->Session->read('creation_timestamp');
if (empty($sessionCreationTime)) {
$sessionCreationTime = $_SERVER['REQUEST_TIME'] ?? time();
$this->Session->write('creation_timestamp', $sessionCreationTime);
}
// kill existing sessions for a user if the admin/instance decides so
// exclude API authentication as it doesn't make sense
if (!$this->isApiAuthed && $this->User->checkForSessionDestruction($user['id'])) {
if (!$this->isApiAuthed && $this->User->checkForSessionDestruction($user['id'], $sessionCreationTime)) {
$this->Auth->logout();
$this->Session->destroy();
$message = __('User deauthenticated on administrator request. Please reauthenticate.');
$this->Flash->warning($message);
$this->Flash->warning(__('User deauthenticated on administrator request. Please reauthenticate.'));
$this->_redirectToLogin();
return false;
}
@ -564,8 +570,7 @@ class AppController extends Controller
if ($user['disabled'] || (isset($user['logged_by_authkey']) && $user['logged_by_authkey']) && !$this->User->checkIfUserIsValid($user)) {
if ($this->_shouldLog('disabled:' . $user['id'])) {
$this->Log = ClassRegistry::init('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$change = $this->User->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], 'Login attempt by disabled user.', json_encode($change));
}
@ -585,9 +590,9 @@ class AppController extends Controller
if ($user['authkey_expiration'] < $time) {
if ($this->_shouldLog('expired:' . $user['authkey_id'])) {
$this->Log = ClassRegistry::init('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt by expired auth key {$user['authkey_id']}.", json_encode($change)); }
$change = $this->User->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt by expired auth key {$user['authkey_id']}.", json_encode($change));
}
$this->Auth->logout();
throw new ForbiddenException('Auth key is expired');
}
@ -604,9 +609,9 @@ class AppController extends Controller
if (!$cidrTool->contains($remoteIp)) {
if ($this->_shouldLog('not_allowed_ip:' . $user['authkey_id'] . ':' . $remoteIp)) {
$this->Log = ClassRegistry::init('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt from not allowed IP address {$remoteIp} for auth key {$user['authkey_id']}.", json_encode($change)); }
$change = $this->User->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt from not allowed IP address {$remoteIp} for auth key {$user['authkey_id']}.", json_encode($change));
}
$this->Auth->logout();
throw new ForbiddenException('It is not possible to use this Auth key from your IP address');
}
@ -1149,8 +1154,7 @@ class AppController extends Controller
$this->Session->write(AuthComponent::$sessionKey, $user['User']);
if (Configure::read('MISP.log_auth')) {
$this->Log = ClassRegistry::init('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$change = $this->User->UserLoginProfile->_getUserProfile();
$change['http_method'] = $_SERVER['REQUEST_METHOD'];
$change['target'] = $this->request->here;
$this->Log->createLogEntry(
@ -1166,8 +1170,7 @@ class AppController extends Controller
// User not authenticated correctly
// reset the session information
$this->Log = ClassRegistry::init('Log');
$this->UserLoginProfile = ClassRegistry::init('UserLoginProfile');
$change = $this->UserLoginProfile->_getUserProfile();
$change = $this->User->UserLoginProfile->_getUserProfile();
$this->Log->createLogEntry(
'SYSTEM',
'auth_fail',
@ -1434,14 +1437,13 @@ class AppController extends Controller
/**
* @return string|null
* @deprecated Use User::_remoteIp() instead
*/
protected function _remoteIp()
{
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR';
return isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : $_SERVER['REMOTE_ADDR'];
return $this->User->_remoteIp();
}
/**
* @param string $key
* @return bool Returns true if the same log defined by $key was not stored in last hour

View File

@ -2469,7 +2469,7 @@ class EventsController extends AppController
$original_file,
$this->data['Event']['publish'],
$this->data['Event']['distribution'],
$this->data['Event']['sharing_group_id'],
$this->data['Event']['sharing_group_id'] ?? null,
$this->data['Event']['galaxies_handling'],
$debug
);
@ -2501,15 +2501,31 @@ class EventsController extends AppController
foreach ($distributionLevels as $key => $value) {
$fieldDesc['distribution'][$key] = $this->Event->distributionDescriptions[$key]['formdesc'];
}
$debugOptions = $this->Event->debugOptions;
$debugOptions = [
0 => __('Standard debugging'),
1 => __('Advanced debugging'),
];
$debugDescriptions = [
0 => __('The critical errors are logged in the usual log file.'),
1 => __('All the errors and warnings are logged in the usual log file.'),
];
$galaxiesOptions = [
0 => __('As MISP standard format'),
1 => __('As tag names'),
];
$galaxiesOptionsDescriptions = [
0 => __('Galaxies and Clusters are passed as MISP standard format. New generic Galaxies and Clusters are created when there is no match with existing ones.'),
1 => __('Galaxies are passed as tags and there is only a simple search with existing galaxy tag names.'),
];
$this->set('debugOptions', $debugOptions);
foreach ($debugOptions as $key => $value) {
$fieldDesc['debug'][$key] = $this->Event->debugDescriptions[$key];
$fieldDesc['debug'][$key] = $debugDescriptions[$key];
}
$galaxiesOptions = $this->Event->galaxiesOptions;
$this->set('galaxiesOptions', $galaxiesOptions);
foreach ($galaxiesOptions as $key => $value) {
$fieldDesc['galaxies_handling'][$key] = $this->Event->galaxiesOptionsDescriptions[$key];
$fieldDesc['galaxies_handling'][$key] = $galaxiesOptionsDescriptions[$key];
}
$this->set('sharingGroups', $sgs);
$this->set('fieldDesc', $fieldDesc);

View File

@ -79,7 +79,7 @@ class UserLoginProfilesController extends AppController
'fields' => ['UserLoginProfile.*']
));
if (empty($profile)) {
throw new NotFoundException(__('Invalid UserLoginProfile'));
throw new NotFoundException(__('Invalid user login profile'));
}
if ($this->UserLoginProfile->delete($id)) {
$this->loadModel('Log');
@ -87,13 +87,13 @@ class UserLoginProfilesController extends AppController
$this->Log->createLogEntry($this->Auth->user(), 'delete', 'UserLoginProfile', $id, $fieldsDescrStr, json_encode($profile));
if ($this->_isRest()) {
return $this->RestResponse->saveSuccessResponse('UserLoginProfile', 'admin_delete', $id, $this->response->type(), 'UserLoginProfile deleted.');
return $this->RestResponse->saveSuccessResponse('UserLoginProfile', 'admin_delete', $id, $this->response->type(), 'User login profile deleted.');
} else {
$this->Flash->success(__('UserLoginProfile deleted'));
$this->redirect(array('admin'=> false, 'controller' => 'userLoginProfiles', 'action' => 'index', $profile['UserLoginProfile']['user_id']));
}
}
$this->Flash->error(__('UserLoginProfile was not deleted'));
$this->Flash->error(__('User login profile was not deleted'));
$this->redirect(array('admin'=> false, 'controller' => 'userLoginProfiles', 'action' => 'index', $profile['UserLoginProfile']['user_id']));
}
}
@ -110,7 +110,7 @@ class UserLoginProfilesController extends AppController
{
if ($this->request->is('post')) {
$userLoginProfile = $this->__setTrust($logId, 'malicious');
$this->Flash->info(__('You marked a login suspicious. You must change your password NOW !'));
$this->Flash->info(__('You marked a login suspicious. You must change your password NOW!'));
$this->loadModel('Log');
$details = 'User reported suspicious login for log ID: '. $logId;
// raise an alert (the SIEM component should ensure (org)admins are informed)
@ -123,9 +123,9 @@ class UserLoginProfilesController extends AppController
'recursive' => -1
));
unset($user['User']['password']);
$this->UserLoginProfile->email_report_malicious($user, $userLoginProfile);
$this->UserLoginProfile->emailReportMalicious($user, $userLoginProfile);
// change account info to force password change, redirect to new password page.
$this->User->id = $this->Auth->user('id');
$this->User->id = $this->Auth->user('id');
$this->User->saveField('change_pw', 1);
$this->redirect(array('controller' => 'users', 'action' => 'change_pw'));
return;
@ -153,14 +153,13 @@ class UserLoginProfilesController extends AppController
$data['hash'] = $this->UserLoginProfile->hash($data);
// add the userLoginProfile trust status if it not already there, based on the hash
$result = $this->UserLoginProfile->find('count', array(
'conditions' => array('UserLoginProfile.hash' => $data['hash'])
));
if ($result == 0) {
$exists = $this->UserLoginProfile->hasAny([
'UserLoginProfile.hash' => $data['hash']
]);
if (!$exists) {
// no row yet, save it.
$this->UserLoginProfile->save($data);
}
return $data;
}
}

View File

@ -1332,34 +1332,37 @@ class UsersController extends AppController
private function _postlogin()
{
$this->User->extralog($this->Auth->user(), "login");
$authUser = $this->Auth->user();
$this->User->extralog($authUser, "login");
$this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
$this->User->id = $this->Auth->user('id');
$user = $this->User->find('first', array(
'conditions' => array(
'User.id' => $this->Auth->user('id')
'User.id' => $authUser['id'],
),
'fields' => ['User.id', 'User.current_login', 'User.last_login'],
'recursive' => -1
));
unset($user['User']['password']);
// update login timestamp and welcome user
$this->User->updateLoginTimes($user['User']);
$lastUserLogin = $user['User']['last_login'];
$this->User->Behaviors->enable('SysLogLogable.SysLogLogable');
$lastUserLogin = $user['User']['last_login'];
if ($lastUserLogin) {
$readableDatetime = (new DateTime())->setTimestamp($lastUserLogin)->format('D, d M y H:i:s O'); // RFC822
$this->Flash->info(__('Welcome! Last login was on %s', $readableDatetime));
}
if (Configure::read('Security.alert_on_suspicious_logins')) {
try {
// there are reasons to believe there is evil happening, suspicious. Inform user and (org)admins.
$suspiciousness_reason = $this->User->UserLoginProfile->_isSuspicious();
if ($suspiciousness_reason) {
$suspiciousnessReason = $this->User->UserLoginProfile->_isSuspicious();
if ($suspiciousnessReason) {
// raise an alert (the SIEM component should ensure (org)admins are informed)
$this->loadModel('Log');
$this->Log->createLogEntry($this->Auth->user(), 'auth_alert', 'User', $this->Auth->user('id'), 'Suspicious login.', $suspiciousness_reason);
$this->Log->createLogEntry($authUser, 'auth_alert', 'User', $authUser['id'], 'Suspicious login.', $suspiciousnessReason);
// Line below commented out to NOT inform user/org admin of the suspicious login.
// The reason is that we want to prevent other user actions cause trouble.
// The reason is that we want to prevent other user actions cause trouble.
// However this also means we're sitting on data that could be used to detect new evil logins.
// As we're generating alerts, the sysadmin should be keeping an eye on these
// $this->User->UserLoginProfile->email_suspicious($user, $suspiciousness_reason);
@ -1367,11 +1370,12 @@ class UsersController extends AppController
// verify UserLoginProfile trust status and perform informative actions
if (!$this->User->UserLoginProfile->_isTrusted()) {
// send email to inform the user
$this->User->UserLoginProfile->email_newlogin($user);
$this->User->UserLoginProfile->emailNewLogin($authUser);
}
} catch (Exception $e) {
// At first login after code update and before DB schema update we might end up with problems.
// Just catch it cleanly here to prevent problems.
$this->log($e->getMessage(), LOG_WARNING);
}
}
@ -3065,7 +3069,7 @@ class UsersController extends AppController
* @return array
* @throws NotFoundException
*/
private function __adminFetchConditions($id, $edit = True)
private function __adminFetchConditions($id, $edit = true)
{
if (empty($id)) {
throw new NotFoundException(__('Invalid user'));
@ -3076,7 +3080,7 @@ class UsersController extends AppController
if (!$user['Role']['perm_site_admin']) {
$conditions['User.org_id'] = $user['org_id']; // org admin
if ($edit) {
$conditions['Role.perm_site_admin'] = False;
$conditions['Role.perm_site_admin'] = false;
}
}
return $conditions;
@ -3096,106 +3100,102 @@ class UsersController extends AppController
}
}
if (!empty($conditions)) {
$user_ids = $this->User->find('list', [
$userIds = $this->User->find('list', [
'recursive' => -1,
'fields' => ['email', 'id'],
'conditions' => $conditions
]);
} else {
$user_ids = [__('Every user') => 'all'];
$userIds = [__('Every user') => 'all'];
}
if ($this->request->is('post')) {
$redis = RedisTool::init();
$kill_before = time();
foreach (array_values($user_ids) as $user_id) {
$redis->set('misp:session_destroy:' . $user_id, $kill_before);
$killBefore = time();
foreach ($userIds as $userId) {
$redis->set('misp:session_destroy:' . $userId, $killBefore);
}
$message = __(
'Session destruction cutoff set to the current timestamp for the given selection (%s). Session(s) will be destroyed on the next user interaction.',
implode(', ', array_keys($user_ids))
implode(', ', array_keys($userIds))
);
if ($this->_isRest()) {
return $this->RestResponse->saveSuccessResponse('User', 'admin_destroy', false, $this->response->type(), $message);
return $this->RestResponse->successResponse(null, $message);
}
$this->Flash->success($message);
$this->redirect($this->referer());
} else {
$this->set(
'question',
__(
'Do you really wish to destroy the session for: %s ? The session destruction will occur when the users try to interact with MISP the next time.',
implode(', ', array_keys($user_ids))
)
);
$this->set('title', __('Destroy sessions'));
$this->set('actionName', 'Destroy');
$this->render('/genericTemplates/confirm');
}
$this->set(
'question',
__(
'Do you really wish to destroy the session for: %s? The session destruction will occur when the users try to interact with MISP the next time.',
implode(', ', array_keys($userIds))
)
);
$this->set('title', __('Destroy sessions'));
$this->set('actionName', 'Destroy');
$this->render('/genericTemplates/confirm');
}
public function view_login_history($user_id = null) {
if ($user_id && $this->_isAdmin()) { // org and site admins
$user = $this->User->find('first', array(
'recursive' => -1,
'conditions' => $this->__adminFetchConditions($user_id),
'contain' => [
'UserSetting',
'Role',
'Organisation'
]
));
if (empty($user)) {
public function view_login_history($userId = null)
{
if ($userId && $this->_isAdmin()) { // org and site admins
$userExists = $this->User->hasAny($this->__adminFetchConditions($userId));
if (!$userExists) {
throw new NotFoundException(__('Invalid user'));
}
} else {
$user_id = $this->Auth->user('id');
$userId = $this->Auth->user('id');
}
$this->loadModel('UserLoginProfile');
$this->loadModel('Log');
$logs = $this->Log->find('all', array(
'conditions' => array(
'Log.user_id' => $user_id,
'OR' => array ('Log.action' => array('login', 'login_fail', 'auth', 'auth_fail'))
'Log.user_id' => $userId,
'OR' => array('Log.action' => array('login', 'login_fail', 'auth', 'auth_fail'))
),
'fields' => array('Log.action', 'Log.created', 'Log.ip', 'Log.change', 'Log.id'),
'order' => array('Log.created DESC'),
'order' => array('Log.id DESC'),
'limit' => 100 // relatively high limit, as we'll be grouping data afterwards.
));
$lst = array();
$profiles = [];
$prevProfile = null;
$prevCreatedLast = null;
$prevCreatedFirst = null;
$prevLogEntry = null;
$prevActions = array();
$actions_translator = [
$actionsTranslator = [
'auth_fail' => 'API:failed',
'auth' => 'API:login',
'login' => 'web:login',
'login_fail' => 'web:failed'
];
$max_rows = 6; // limit to a few rows, to prevent cluttering the interface.
$maxRows = 6; // limit to a few rows, to prevent cluttering the interface.
// We didn't filter the data at SQL query too much, nor by age, as we want to show "enough" data, even if old
$rows = 0;
// group authentications by type of loginprofile, to make the list shorter
foreach($logs as $logEntry) {
$loginProfile = $this->UserLoginProfile->_fromLog($logEntry['Log']);
if (!$loginProfile) continue; // skip if empty log
foreach ($logs as $logEntry) {
$loginProfile = $this->User->UserLoginProfile->_fromLog($logEntry['Log']);
if (!$loginProfile) {
continue; // skip if empty log
}
$loginProfile['ip'] = $logEntry['Log']['ip'] ?? null; // transitional workaround
if ($this->UserLoginProfile->_isSimilar($loginProfile, $prevProfile)) {
if ($this->User->UserLoginProfile->_isSimilar($loginProfile, $prevProfile)) {
// continue find as same type of login
$prevCreatedFirst = $logEntry['Log']['created'];
$prevActions[] = $actions_translator[$logEntry['Log']['action']] ?? $logEntry['Log']['action'];
$prevActions[] = $actionsTranslator[$logEntry['Log']['action']];
} else {
// add as new entry
if (null != $prevProfile) {
if (null !== $prevProfile) {
$actionsString = ''; // count actions
foreach(array_count_values($prevActions) as $action => $cnt) {
foreach (array_count_values($prevActions) as $action => $cnt) {
$actionsString .= $action . ' (' . $cnt . "x) ";
}
$lst[] = array(
'status' => $this->UserLoginProfile->_getTrustStatus($prevProfile, $user_id),
$profiles[] = [
'status' => $this->User->UserLoginProfile->_getTrustStatus($prevProfile, $userId),
'platform' => $prevProfile['ua_platform'],
'browser' => $prevProfile['ua_browser'],
'region' => $prevProfile['geoip'],
@ -3204,40 +3204,47 @@ class UsersController extends AppController
'last_seen' => $prevCreatedLast,
'first_seen' => $prevCreatedFirst,
'actions' => $actionsString,
'actions_button' => ('unknown' == $this->UserLoginProfile->_getTrustStatus($prevProfile, $user_id)) ? true : false,
'id' => $prevLogEntry);
'actions_button' => ('unknown' == $this->User->UserLoginProfile->_getTrustStatus($prevProfile, $userId)) ? true : false,
'id' => $prevLogEntry
];
}
// build new entry
$prevProfile = $loginProfile;
$prevCreatedFirst = $prevCreatedLast = $logEntry['Log']['created'];
$prevActions[] = $actions_translator[$logEntry['Log']['action']] ?? $logEntry['Log']['action'];
$prevActions[] = $actionsTranslator[$logEntry['Log']['action']];
$prevLogEntry = $logEntry['Log']['id'];
$rows += 1;
if ($rows == $max_rows) break;
$rows++;
if ($rows === $maxRows) {
break;
}
}
}
// add last entry
$actionsString = ''; // count actions
foreach(array_count_values($prevActions) as $action => $cnt) {
$actionsString .= $action . ' (' . $cnt . "x) ";
if (null !== $prevProfile) {
$actionsString = ''; // count actions
foreach (array_count_values($prevActions) as $action => $cnt) {
$actionsString .= $action . ' (' . $cnt . "x) ";
}
$profiles[] = array(
'status' => $this->User->UserLoginProfile->_getTrustStatus($prevProfile, $userId),
'platform' => $prevProfile['ua_platform'],
'browser' => $prevProfile['ua_browser'],
'region' => $prevProfile['geoip'],
'ip' => $prevProfile['ip'],
'accept_lang' => $prevProfile['accept_lang'],
'last_seen' => $prevCreatedLast,
'first_seen' => $prevCreatedFirst,
'actions' => $actionsString,
'actions_button' => ('unknown' == $this->User->UserLoginProfile->_getTrustStatus($prevProfile, $userId)) ? true : false,
'id' => $prevLogEntry
);
}
$lst[] = array(
'status' => $this->UserLoginProfile->_getTrustStatus($prevProfile, $user_id),
'platform' => $prevProfile['ua_platform'],
'browser' => $prevProfile['ua_browser'],
'region' => $prevProfile['geoip'],
'ip' => $prevProfile['ip'],
'accept_lang' => $prevProfile['accept_lang'],
'last_seen' => $prevCreatedLast,
'first_seen' => $prevCreatedFirst,
'actions' => $actionsString,
'actions_button' => ('unknown' == $this->UserLoginProfile->_getTrustStatus($prevProfile, $user_id)) ? true : false,
'id' => $prevLogEntry);
$this->set('data', $lst);
$this->set('user_id', $user_id);
$this->set('data', $profiles);
$this->set('user_id', $userId);
}
public function logout401() {
public function logout401()
{
# You should read the documentation in docs/CONFIG.ApacheSecureAuth.md
# before using this endpoint. It is not useful without webserver config
# changes.
@ -3262,16 +3269,9 @@ class UsersController extends AppController
if (empty($this->request->data['User']['email'])) {
throw new MethodNotAllowedException(__('No email provided, cannot generate password reset message.'));
}
$user = [
'id' => 0,
'email' => 'SYSTEM',
'Organisation' => [
'name' => 'SYSTEM'
]
];
$this->loadModel('Log');
$this->Log->createLogEntry($user, 'forgot', 'User', 0, 'Password reset requested for: ' . $this->request->data['User']['email']);
$this->User->forgotRouter($this->request->data['User']['email'], $this->_remoteIp());
$this->Log->createLogEntry('SYSTEM', 'forgot', 'User', 0, 'Password reset requested for: ' . $this->request->data['User']['email']);
$this->User->forgotRouter($this->request->data['User']['email'], $this->User->_remoteIp());
$message = __('Password reset request submitted. If a valid user is found, you should receive an e-mail with a temporary reset link momentarily. Please be advised that this link is only valid for 10 minutes.');
if ($this->_isRest()) {
return $this->RestResponse->saveSuccessResponse('User', 'forgot', false, $this->response->type(), $message);
@ -3308,5 +3308,4 @@ class UsersController extends AppController
return $this->__pw_change(['User' => $user], 'password_reset', $abortPost, $token, true);
}
}
}

View File

@ -11,7 +11,7 @@ class Stix1Export extends StixExport
{
return [
ProcessTool::pythonBin(),
$this->__framing_script,
self::FRAMING_SCRIPT,
'stix1',
'-s', $this->__scope,
'-v', $this->__version,
@ -25,7 +25,7 @@ class Stix1Export extends StixExport
{
$command = [
ProcessTool::pythonBin(),
$this->__scripts_dir . 'misp2stix.py',
self::SCRIPTS_DIR . 'misp2stix.py',
'-s', $this->__scope,
'-v', $this->__version,
'-f', $this->__return_format,
@ -33,6 +33,10 @@ class Stix1Export extends StixExport
'-i',
];
$command = array_merge($command, $this->__filenames);
return ProcessTool::execute($command, null, true);
try {
return ProcessTool::execute($command, null, true);
} catch (ProcessException $e) {
return $e->stdout();
}
}
}

View File

@ -11,16 +11,20 @@ class Stix2Export extends StixExport
{
return [
ProcessTool::pythonBin(),
$this->__framing_script,
self::FRAMING_SCRIPT,
'stix2',
'-v', $this->__version,
'--uuid', CakeText::uuid(),
];
}
/**
* @return string
* @throws Exception
*/
protected function __parse_misp_data()
{
$scriptFile = $this->__scripts_dir . 'stix2/misp2stix2.py';
$scriptFile = self::SCRIPTS_DIR . 'stix2/misp2stix2.py';
$command = [
ProcessTool::pythonBin(),
$scriptFile,
@ -28,7 +32,11 @@ class Stix2Export extends StixExport
'-i',
];
$command = array_merge($command, $this->__filenames);
$result = ProcessTool::execute($command, null, true);
try {
$result = ProcessTool::execute($command, null, true);
} catch (ProcessException $e) {
$result = $e->stdout();
}
$result = preg_split("/\r\n|\n|\r/", trim($result));
return end($result);
}

View File

@ -6,13 +6,14 @@ App::uses('ProcessTool', 'Tools');
abstract class StixExport
{
const SCRIPTS_DIR = APP . 'files/scripts/',
FRAMING_SCRIPT = APP . 'files/scripts/misp_framing.py';
public $additional_params = array(
'includeEventTags' => 1,
'includeGalaxy' => 1
);
protected $__return_format = 'json';
protected $__scripts_dir = APP . 'files/scripts/';
protected $__framing_script = APP . 'files/scripts/misp_framing.py';
protected $__return_type = null;
/** @var array Full paths to files to convert */

View File

@ -1,7 +1,7 @@
<?php
class ProcessException extends Exception
{
/** @var string|null */
/** @var string */
private $stderr;
/** @var string */
@ -10,14 +10,13 @@ class ProcessException extends Exception
/**
* @param string|array $command
* @param int $returnCode
* @param string|null $stderr
* @param string $stderr
* @param string $stdout
*/
public function __construct($command, $returnCode, $stderr, $stdout)
{
$commandForException = is_array($command) ? implode(' ', $command) : $command;
$stderrToMessage = $stderr === null ? 'Logged to tmp/logs/exec-errors.log' : "'$stderr'";
$message = "Command '$commandForException' finished with error code $returnCode.\nSTDERR: $stderrToMessage\nSTDOUT: '$stdout'";
$message = "Command '$commandForException' finished with error code $returnCode.\nSTDERR: '$stderr'\nSTDOUT: '$stdout'";
$this->stderr = $stderr;
$this->stdout = $stdout;
parent::__construct($message, $returnCode);
@ -41,21 +40,20 @@ class ProcessTool
/**
* @param array $command If command is array, it is not necessary to escape arguments
* @param string|null $cwd
* @param bool $stderrToFile IF true, log stderrr output to LOG_FILE
* @param bool $logToFile If true, log stderr output to LOG_FILE
* @return string Stdout
* @throws ProcessException
* @throws Exception
*/
public static function execute(array $command, $cwd = null, $stderrToFile = false)
public static function execute(array $command, $cwd = null, $logToFile = false)
{
$descriptorSpec = [
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
if ($stderrToFile) {
if ($logToFile) {
self::logMessage('Running command ' . implode(' ', $command));
$descriptorSpec[2] = ['file', self::LOG_FILE, 'a'];
}
// PHP older than 7.4 do not support proc_open with array, so we need to convert values to string manually
@ -75,20 +73,24 @@ class ProcessTool
throw new Exception("Could not get STDOUT of command '$commandForException'.");
}
if ($stderrToFile) {
$stderr = null;
} else {
$stderr = stream_get_contents($pipes[2]);
$stderr = stream_get_contents($pipes[2]);
if ($stderr === false) {
$commandForException = self::commandFormat($command);
throw new Exception("Could not get STDERR of command '$commandForException'.");
}
$returnCode = proc_close($process);
if ($stderrToFile) {
self::logMessage("Process finished with return code $returnCode");
if ($logToFile) {
self::logMessage("Process finished with return code $returnCode", $stderr);
}
if ($returnCode !== 0) {
throw new ProcessException($command, $returnCode, $stderr, $stdout);
$exception = new ProcessException($command, $returnCode, $stderr, $stdout);
if ($logToFile && Configure::read('Security.ecs_log')) {
EcsLog::handleException($exception);
}
throw $exception;
}
return $stdout;
@ -116,9 +118,17 @@ class ProcessTool
return Configure::read('MISP.python_bin') ?: 'python3';
}
private static function logMessage($message)
/**
* @param string $message
* @param string|null $stderr
* @return void
*/
private static function logMessage($message, $stderr = null)
{
$logMessage = '[' . date("Y-m-d H:i:s") . ' ' . getmypid() . "] $message\n";
if ($stderr) {
$logMessage = rtrim($stderr) . "\n" . $logMessage;
}
file_put_contents(self::LOG_FILE, $logMessage, FILE_APPEND | LOCK_EX);
}

View File

@ -68,6 +68,13 @@ class SecurityAudit
];
}
if (!Configure::read('Security.alert_on_suspicious_logins')) {
$output['Login'][] = [
'warning',
__('Warning about suspicious logins is disabled. You can enable alert by setting `Security.alert_on_suspicious_logins` to `true`.'),
];
}
if (empty(Configure::read('Security.disable_browser_cache'))) {
$output['Browser'][] = [
'warning',

View File

@ -555,14 +555,18 @@ class SendEmail
}
try {
return [
'contents' => $email->send(),
'encrypted' => $encrypted,
'subject' => $subject,
];
$content = $email->send();
} catch (Exception $e) {
throw new SendEmailException('The message could not be sent.', 0, $e);
}
return [
'to' => $user['User']['email'],
'message_id' => $email->messageId(),
'contents' => $content,
'encrypted' => $encrypted,
'subject' => $subject,
];
}
/**

View File

@ -69,4 +69,69 @@ class AdminSetting extends AppModel
return empty($this->findUpgrades($db_version['AdminSetting']['value']));
}
}
public function garbageCollect()
{
$last_collection = $this->find('first', [
'conditions' => ['setting' => 'last_gc_timestamp'],
'recursive' => -1
]);
if (empty($last_collection)) {
$last_collection = 0;
} else {
$last_collection = $last_collection['AdminSetting']['value'];
}
if ((time()) > ($last_collection + 3600)) {
$this->__cleanTmpFiles();
}
}
private function __cleanTmpFiles() {
$time = time();
$this->__deleteScriptTmpFiles($time);
$this->__deleteTaxiiTmpFiles($time);
}
private function __deleteScriptTmpFiles($time) {
$scripts_tmp_path = APP . 'files/scripts/tmp';
$dir = new Folder($scripts_tmp_path);
$contents = $dir->read(false, false);
foreach ($contents[1] as $file) {
if (preg_match('/^[a-zA-Z0-9]{12}$/', $file)) {
$tmp_file = new File($scripts_tmp_path . '/' . $file);
if ($time > $tmp_file->lastChange() + 3600) {
$tmp_file->delete();
}
unlink($scripts_tmp_path . '/' . $file);
}
}
}
private function __deleteTaxiiTmpFiles($time) {
$taxii_path = APP . 'files/scripts/tmp/Taxii';
$taxii_dir = new Folder($taxii_path);
$taxii_contents = $taxii_dir->read(false, false);
if (!empty($taxii_contents[0])) {
foreach ($taxii_contents[0] as $taxii_temp_dir) {
if (preg_match('/^[a-zA-Z0-9]{12}$/', $taxii_temp_dir)) {
$tmp_dir = new Folder($taxii_path . $taxii_temp_dir);
$taxii_temp_dir_contents = $tmp_dir->read(false, false);
if (!empty(count($taxii_temp_dir_contents[1]))) {
$files_count = count($taxii_temp_dir_contents[1]);
$files_removed = 0;
foreach ($taxii_temp_dir_contents[1] as $tmp_file) {
$tmp_file = new File($taxii_path . $taxii_temp_dir . '/' . $tmp_file);
if ($time > $tmp_file->lastChange() + 3600) {
$tmp_file->delete();
$files_removed += 1;
}
}
if ($files_count === $files_removed) {
$tmp_dir->delete();
}
}
}
}
}
}
}

View File

@ -3657,13 +3657,11 @@ class AppModel extends Model
{
// If Sentry is installed, send exception to Sentry
if (function_exists('\Sentry\captureException') && $type === LOG_ERR) {
\Sentry\captureException($exception);
\Sentry\captureException(new Exception($message, $type, $exception));
}
$message .= "\n";
do {
$message .= sprintf("[%s] %s", get_class($exception), $exception->getMessage());
$message .= sprintf("\n[%s] %s", get_class($exception), $exception->getMessage());
$message .= "\nStack Trace:\n" . $exception->getTraceAsString();
$exception = $exception->getPrevious();
} while ($exception !== null);
@ -4022,14 +4020,20 @@ class AppModel extends Model
*/
public function _remoteIp()
{
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: null;
if ($ipHeader && isset($_SERVER[$ipHeader])) {
return trim($_SERVER[$ipHeader]);
$clientIpHeader = Configure::read('MISP.log_client_ip_header');
if ($clientIpHeader && isset($_SERVER[$clientIpHeader])) {
$headerValue = $_SERVER[$clientIpHeader];
// X-Forwarded-For can contain multiple IPs, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
if (($commaPos = strpos($headerValue, ',')) !== false) {
$headerValue = substr($headerValue, 0, $commaPos);
}
return trim($headerValue);
}
return $_SERVER['REMOTE_ADDR'] ?? null;
}
public function find($type = 'first', $query = array()) {
public function find($type = 'first', $query = array())
{
if (!empty($query['order']) && $this->validOrderClause($query['order']) === false) {
throw new InvalidArgumentException('Invalid order clause');
}
@ -4037,9 +4041,10 @@ class AppModel extends Model
return parent::find($type, $query);
}
private function validOrderClause($order){
private function validOrderClause($order)
{
$pattern = '/^[\w\_\-\.\(\) ]+$/';
if(is_string($order) && preg_match($pattern, $order)){
if (is_string($order) && preg_match($pattern, $order)) {
return true;
}
@ -4048,7 +4053,7 @@ class AppModel extends Model
if (is_string($key) && is_string($value) && preg_match($pattern, $key) && in_array(strtolower($value), ['asc', 'desc'])) {
return true;
}
if(is_numeric($key) && is_string($value) && preg_match($pattern, $value)){
if (is_numeric($key) && is_string($value) && preg_match($pattern, $value)) {
return true;
}
}

View File

@ -255,7 +255,7 @@ class AttachmentScan extends AppModel
}
$scanned++;
} catch (NotFoundException $e) {
// skip
// skip if file doesn't exists
} catch (Exception $e) {
$this->logException("Could not scan attachment for $type {$attribute['Attribute']['id']}", $e);
$fails++;
@ -341,7 +341,17 @@ class AttachmentScan extends AppModel
$file = $this->attachmentTool()->getShadowFile($attribute['event_id'], $attribute['id']);
}
if (in_array('attachment', $moduleInfo['types'])) {
if (in_array('attachment', $moduleInfo['types'], true)) {
$fileSize = $file->size();
if ($fileSize === false) {
throw new Exception("Could not read size of file '$file->path'.");
}
if ($fileSize === 0) {
return false; // empty file is automatically considered as not infected
}
/* if ($file->size() > 50 * 1024 * 1024) {
$this->log("File '$file->path' is bigger than 50 MB, will be not scanned.", LOG_NOTICE);
return false;
@ -349,7 +359,7 @@ class AttachmentScan extends AppModel
$fileContent = $file->read();
if ($fileContent === false) {
throw new Exception("Could not read content of file '$file->path'.");
throw new Exception("Could not read content of file '$file->path'.");
}
$attribute['data'] = base64_encode($fileContent);
} else {

View File

@ -25,11 +25,7 @@ class AuditLog extends AppModel
ACTION_REMOVE_GALAXY = 'remove_galaxy',
ACTION_REMOVE_GALAXY_LOCAL = 'remove_local_galaxy',
ACTION_PUBLISH = 'publish',
ACTION_PUBLISH_SIGHTINGS = 'publish_sightings',
ACTION_LOGIN = 'login',
ACTION_PASSWDCHANGE = 'password_change',
ACTION_LOGOUT = 'logout',
ACTION_LOGIN_FAILED = 'login_failed';
ACTION_PUBLISH_SIGHTINGS = 'publish_sightings';
const REQUEST_TYPE_DEFAULT = 0,
REQUEST_TYPE_API = 1,

View File

@ -162,6 +162,7 @@ class AuthKey extends AppModel
* @param string $authkey
* @param bool $includeExpired
* @return array|false
* @throws Exception
*/
public function getAuthUserByAuthKey($authkey, $includeExpired = false)
{
@ -187,24 +188,8 @@ class AuthKey extends AppModel
]);
$passwordHasher = $this->getHasher();
foreach ($possibleAuthkeys as $possibleAuthkey) {
if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) { // valid authkey
// store IP in db if not there yet
if (!Configure::read("MISP.disable_seen_ips_authkeys")) {
$remote_ip = $this->_remoteIp();
$update_db_ip = true;
if (in_array($remote_ip, $possibleAuthkey['AuthKey']['unique_ips'])) {
$update_db_ip = false; // IP already seen, skip saving in DB
} else { // first time this IP is seen for this API key
$possibleAuthkey['AuthKey']['unique_ips'][] = $remote_ip;
}
if ($update_db_ip) {
// prevent double entries due to race condition
$possibleAuthkey['AuthKey']['unique_ips'] = array_unique($possibleAuthkey['AuthKey']['unique_ips']);
// save in db
$this->save($possibleAuthkey, ['fieldList' => ['unique_ips']]);
}
}
// fetch user
if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) {
$this->updateUniqueIp($possibleAuthkey);
$user = $this->User->getAuthUser($possibleAuthkey['AuthKey']['user_id']);
if ($user) {
$user = $this->setUserData($user, $possibleAuthkey);
@ -215,6 +200,26 @@ class AuthKey extends AppModel
return false;
}
/**
* @param array $authkey
* @return void
* @throws Exception
*/
private function updateUniqueIp(array $authkey)
{
if (Configure::read("MISP.disable_seen_ips_authkeys")) {
return;
}
$remoteIp = $this->_remoteIp();
if ($remoteIp === null || in_array($remoteIp, $authkey['AuthKey']['unique_ips'], true)) {
return;
}
$authkey['AuthKey']['unique_ips'][] = $remoteIp;
$this->save($authkey, ['fieldList' => ['unique_ips']]);
}
/**
* @param array $user
* @param array $authkey

View File

@ -25,6 +25,7 @@ class AuditLogBehavior extends ModelBehavior
'date_modified' => true, // User
'current_login' => true, // User
'last_login' => true, // User
'last_api_access' => true, // User
'newsread' => true, // User
'unique_ips' => true, // User
'proposal_email_lock' => true, // Event

View File

@ -63,11 +63,6 @@ class Event extends AppModel
2 => array('desc' => '*Complete* means that the event\'s creation is complete', 'formdesc' => 'The event creator considers the analysis complete')
);
public $debugDescriptions = array(
0 => 'The critical errors are logged in the usual log file.',
1 => 'All the errors and warnings are logged in the usual log file.'
);
public $distributionDescriptions = [
self::DISTRIBUTION_ORGANISATION => [
'desc' => 'This field determines the current distribution of the event',
@ -91,16 +86,6 @@ class Event extends AppModel
],
];
public $galaxiesOptionsDescriptions = array(
0 => 'Galaxies and Clusters are passed as MISP standard format. New generic Galaxies and Clusters are created when there is no match with existing ones.',
1 => 'Galaxies are passed as tags and there is only a simple search with existing galaxy tag names.'
);
public $debugOptions = array(
0 => 'Standard debugging',
1 => 'Advanced debugging'
);
public $distributionLevels = [
self::DISTRIBUTION_ORGANISATION => 'Your organisation only',
self::DISTRIBUTION_COMMUNITY => 'This community only',
@ -109,11 +94,6 @@ class Event extends AppModel
self::DISTRIBUTION_SHARING_GROUP => 'Sharing group',
];
public $galaxiesOptions = array(
0 => 'As MISP standard format',
1 => 'As tag names'
);
public $analysisLevels = array(
0 => 'Initial', 1 => 'Ongoing', 2 => 'Completed'
);
@ -5922,61 +5902,24 @@ class Event extends AppModel
/**
* @param array $user
* @param string $file Path
* @param string $stix_version
* @param string $original_file
* @param string $stixVersion
* @param string $originalFile
* @param bool $publish
* @param int $distribution
* @param int|null $sharingGroupId
* @param bool $galaxiesAsTags
* @param bool $debug
* @return int|string|array
* @throws JsonException
* @throws InvalidArgumentException
* @throws Exception
*/
public function upload_stix(array $user, $file, $stix_version, $original_file, $publish, $distribution, $sharingGroupId, $galaxiesAsTags, $debug = false)
public function upload_stix(array $user, $file, $stixVersion, $originalFile, $publish, $distribution, $sharingGroupId, $galaxiesAsTags, $debug = false)
{
$scriptDir = APP . 'files' . DS . 'scripts';
if ($stix_version == '2' || $stix_version == '2.0' || $stix_version == '2.1') {
$scriptFile = $scriptDir . DS . 'stix2' . DS . 'stix2misp.py';
$output_path = $file . '.out';
$shell_command = [
ProcessTool::pythonBin(),
$scriptFile,
'-i', $file,
'--distribution', $distribution
];
if ($distribution == 4) {
array_push($shell_command, '--sharing_group_id', $sharingGroupId);
}
if ($galaxiesAsTags) {
$shell_command[] = '--galaxies_as_tags';
}
if ($debug) {
$shell_command[] = '--debug';
}
$stix_version = "STIX 2.1";
} elseif ($stix_version == '1' || $stix_version == '1.1' || $stix_version == '1.2') {
$scriptFile = $scriptDir . DS . 'stix2misp.py';
$output_path = $file . '.json';
$shell_command = [
ProcessTool::pythonBin(),
$scriptFile,
$file,
Configure::read('MISP.default_event_distribution'),
Configure::read('MISP.default_attribute_distribution'),
$this->__getTagNamesFromSynonyms($scriptDir)
];
$stix_version = "STIX 1.1";
} else {
throw new InvalidArgumentException('Invalid STIX version');
}
$decoded = $this->convertStixToMisp($stixVersion, $file, $distribution, $sharingGroupId, $galaxiesAsTags, $debug);
$result = ProcessTool::execute($shell_command, null, true);
$result = preg_split("/\r\n|\n|\r/", trim($result));
$result = trim(end($result));
$tempFile = file_get_contents($file);
unlink($file);
$decoded = JsonTool::decode($result);
if (!empty($decoded['success'])) {
$data = FileAccessTool::readAndDelete($output_path);
$data = $this->jsonDecode($data);
$data = JsonTool::decodeArray($decoded['converted']);
if (empty($data['Event'])) {
$data = array('Event' => $data);
}
@ -6000,15 +5943,13 @@ class Event extends AppModel
}
}
}
if (!empty($decoded['stix_version'])) {
$stix_version = 'STIX ' . $decoded['stix_version'];
}
$stixVersion = $decoded['stix_version'];
$created_id = false;
$validationIssues = false;
$result = $this->_add($data, true, $user, '', null, false, null, $created_id, $validationIssues);
if ($result === true) {
if ($original_file) {
$this->add_original_file($tempFile, $original_file, $created_id, $stix_version);
if ($originalFile) {
$this->add_original_file($decoded['original'], $originalFile, $created_id, $stixVersion);
}
if ($publish && $user['Role']['perm_publish']) {
$this->publish($created_id);
@ -6031,6 +5972,76 @@ class Event extends AppModel
return $response;
}
/**
* @param string $stixVersion
* @param string $file
* @param int $distribution
* @param int|null $sharingGroupId
* @param bool $galaxiesAsTags
* @param bool $debug
* @return array
* @throws Exception
*/
private function convertStixToMisp($stixVersion, $file, $distribution, $sharingGroupId, $galaxiesAsTags, $debug)
{
$scriptDir = APP . 'files' . DS . 'scripts';
if ($stixVersion === '2' || $stixVersion === '2.0' || $stixVersion === '2.1') {
$scriptFile = $scriptDir . DS . 'stix2' . DS . 'stix2misp.py';
$outputPath = $file . '.out';
$shellCommand = [
ProcessTool::pythonBin(),
$scriptFile,
'-i', $file,
'--distribution', $distribution,
];
if ($distribution == 4) {
array_push($shellCommand, '--sharing_group_id', $sharingGroupId);
}
if ($galaxiesAsTags) {
$shellCommand[] = '--galaxies_as_tags';
}
if ($debug) {
$shellCommand[] = '--debug';
}
$stixVersion = "STIX 2.1";
} else if ($stixVersion === '1' || $stixVersion === '1.1' || $stixVersion === '1.2') {
$scriptFile = $scriptDir . DS . 'stix2misp.py';
$outputPath = $file . '.json';
$shellCommand = [
ProcessTool::pythonBin(),
$scriptFile,
$file,
Configure::read('MISP.default_event_distribution'),
Configure::read('MISP.default_attribute_distribution'),
$this->__getTagNamesFromSynonyms($scriptDir)
];
$stixVersion = "STIX 1.1";
} else {
throw new InvalidArgumentException('Invalid STIX version');
}
try {
$stdout = ProcessTool::execute($shellCommand, null, true);
} catch (ProcessException $e) {
$stdout = $e->stdout();
}
$stdout = preg_split("/\r\n|\n|\r/", trim($stdout));
$stdout = trim(end($stdout));
$decoded = JsonTool::decode($stdout);
if (empty($decoded['stix_version'])) {
$decoded['stix_version'] = $stixVersion;
}
$decoded['original'] = FileAccessTool::readAndDelete($file);
if (!empty($decoded['success'])) {
$decoded['converted'] = FileAccessTool::readAndDelete($outputPath);
}
return $decoded;
}
private function __handleGalaxiesAndClusters($user, &$data)
{
if (!empty($data['Galaxy'])) {

View File

@ -3,16 +3,30 @@ App::uses('AppModel', 'Model');
class Log extends AppModel
{
const WARNING_ACTIONS = array(
const WARNING_ACTIONS = [
'warning',
'change_pw',
'login_fail',
'version_warning',
'auth_fail'
);
const ERROR_ACTIONS = array(
];
const ERROR_ACTIONS = [
'error'
);
];
const AUTH_ACTIONS = [
'auth',
'auth_fail',
'auth_alert',
'change_pw',
'login',
'login_fail',
'logout',
'password_reset',
'forgot',
];
public $validate = array(
'action' => array(
'rule' => array(
@ -159,11 +173,6 @@ class Log extends AppModel
if (!in_array($this->data['Log']['model'], ['Log', 'Workflow'])) {
$trigger_id = 'log-after-save';
$workflowErrors = [];
$logging = [
'model' => 'Log',
'action' => 'execute_workflow',
'id' => $this->data['Log']['user_id']
];
$this->executeTrigger($trigger_id, $this->data, $workflowErrors);
}
return true;
@ -206,7 +215,7 @@ class Log extends AppModel
$validDates = $this->query($sql);
}
$data = array();
foreach ($validDates as $k => $date) {
foreach ($validDates as $date) {
$data[$date[0]['Date']] = intval($date[0]['count']);
}
return $data;
@ -286,7 +295,7 @@ class Log extends AppModel
public function validationError($user, $action, $model, $title, array $validationErrors, array $fullObject)
{
$this->log($title, LOG_WARNING);
$change = 'Validation errors: ' . json_encode($validationErrors) . ' Full ' . $model . ': ' . json_encode($fullObject);
$change = 'Validation errors: ' . JsonTool::encode($validationErrors) . ' Full ' . $model . ': ' . JsonTool::encode($fullObject);
$this->createLogEntry($user, $action, $model, 0, $title, $change);
}
@ -365,7 +374,7 @@ class Log extends AppModel
}
}
public function logData($data)
private function logData(array $data)
{
if ($this->pubToZmq('audit')) {
$this->getPubSubTool()->publish($data, 'audit', 'log');
@ -386,6 +395,14 @@ class Log extends AppModel
}
// write to syslogd as well if enabled
$this->sendToSyslog($data);
$this->sendToEcs($data);
return true;
}
private function sendToSyslog(array $data)
{
if ($this->syslog === null) {
if (Configure::read('Security.syslog')) {
$options = [];
@ -407,8 +424,7 @@ class Log extends AppModel
if (isset($data['Log']['action'])) {
if (in_array($data['Log']['action'], self::ERROR_ACTIONS, true)) {
$action = LOG_ERR;
}
if (in_array($data['Log']['action'], self::WARNING_ACTIONS, true)) {
} else if (in_array($data['Log']['action'], self::WARNING_ACTIONS, true)) {
$action = LOG_WARNING;
}
}
@ -420,11 +436,49 @@ class Log extends AppModel
if (!empty($data['Log']['description'])) {
$entry .= " -- {$data['Log']['description']}";
} else if (!empty($data['Log']['change'])) {
$entry .= " -- " . json_encode($data['Log']['change']);
$entry .= " -- " . JsonTool::encode($data['Log']['change']);
}
$this->syslog->write($action, $entry);
}
return true;
}
/**
* @param array $data
* @return void
* @throws JsonException
*/
private function sendToEcs(array $data)
{
if (!Configure::read('Security.ecs_log')) {
return;
}
$log = $data['Log'];
$action = $log['action'];
if ($action === 'email') {
return; // do not log email actions as it is logged with more details by `writeEmailLog` function
}
if (in_array($action, self::ERROR_ACTIONS, true)) {
$type = 'error';
} else if (in_array($action, self::WARNING_ACTIONS, true)) {
$type = 'warning';
} else {
$type = 'info';
}
$message = $action;
if (!empty($log['title'])) {
$message .= " -- {$log['title']}";
}
if (!empty($log['description'])) {
$message .= " -- {$log['description']}";
} else if (!empty($log['change'])) {
$message .= " -- " . (is_string($log['change']) ? $log['change'] : JsonTool::encode($log['change']));
}
EcsLog::writeApplicationLog($type, $action, $message);
}
public function filterSiteAdminSensitiveLogs($list)
@ -1176,11 +1230,17 @@ class Log extends AppModel
return $this->elasticSearchClient;
}
/**
* @param $data
* @param $options
* @return array|bool|mixed
*/
public function saveOrFailSilently($data, $options = null)
{
try {
return $this->save($data, $options);
} catch (Exception $e) {
$this->logException('Could not save log to database', $e);
return false;
}
}

View File

@ -5062,6 +5062,14 @@ class Server extends AppModel
'type' => 'boolean',
'null' => true
],
'enable_automatic_garbage_collection' => [
'level' => 1,
'description' => __('Enable to execute an automatic garbage collection of temporary data such as export files. When enabled, on agerage every 100th query will check whether to garbage collect. Garbage collection can run at maximum once an hour.'),
'value' => false,
'test' => 'testBool',
'type' => 'boolean',
'null' => true,
],
'server_settings_skip_backup_rotate' => array(
'level' => 1,
'description' => __('Enable this setting to directly save the config.php file without first creating a temporary file and moving it to avoid concurency issues. Generally not recommended, but useful when for example other tools modify/maintain the config.php file.'),

View File

@ -12,6 +12,7 @@ App::uses('BlowfishConstantPasswordHasher', 'Controller/Component/Auth');
* @property Organisation $Organisation
* @property Role $Role
* @property UserSetting $UserSetting
* @property UserLoginProfile $UserLoginProfile
* @property Event $Event
* @property AuthKey $AuthKey
* @property Server $Server
@ -889,7 +890,11 @@ class User extends AppModel
$logTitle = $result['encrypted'] ? 'Encrypted email' : 'Email';
// Intentional two spaces to pass test :)
$logTitle .= $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $result['subject'] . '".';
$logTitle .= $replyToLog . ' to ' . $result['to'] . ' sent, titled "' . $result['subject'] . '".';
if (Configure::read('Security.ecs_log')) {
EcsLog::writeEmailLog($logTitle, $result, $replyToUser ? $replyToUser['User']['email'] : null);
}
$log->create();
$log->saveOrFailSilently(array(
@ -1027,18 +1032,18 @@ class User extends AppModel
}
/**
* @param int $org_id
* @param int $orgId
* @param int|false $excludeUserId
* @return array
* @return array User ID => Email
*/
public function getOrgAdminsForOrg($org_id, $excludeUserId = false)
public function getOrgAdminsForOrg($orgId, $excludeUserId = false)
{
$adminRoles = $this->Role->find('column', array(
'conditions' => array('perm_admin' => 1),
'fields' => array('Role.id')
));
$conditions = array(
'User.org_id' => $org_id,
'User.org_id' => $orgId,
'User.disabled' => 0,
'User.role_id' => $adminRoles
);
@ -1054,7 +1059,12 @@ class User extends AppModel
));
}
public function getSiteAdmins($excludeUserId = false) {
/**
* @param int|false $excludeUserId
* @return array User ID => Email
*/
public function getSiteAdmins($excludeUserId = false)
{
$adminRoles = $this->Role->find('column', array(
'conditions' => array('perm_site_admin' => 1),
'fields' => array('Role.id')
@ -1264,37 +1274,44 @@ class User extends AppModel
return $newkey;
}
public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null)
/**
* @param string|array $user
* @param string $action
* @param string $description
* @param string $fieldsResult
* @param array|null $modifiedUser
* @return void
* @throws JsonException
*/
public function extralog($user, $action, $description = null, $fieldsResult = null, $modifiedUser = null)
{
if (!is_array($user) && $user === 'SYSTEM') {
if ($user === 'SYSTEM') {
$user = [
'id' => 0,
'email' => 'SYSTEM',
'Organisation' => [
'name' => 'SYSTEM'
]
],
];
}
// new data
$model = 'User';
$modelId = $user['id'];
if (!empty($modifiedUser)) {
$modelId = $modifiedUser['User']['id'];
}
if ($action == 'login') {
if ($action === 'login') {
$description = "User (" . $user['id'] . "): " . $user['email'];
$fieldsResult = json_encode($this->UserLoginProfile->_getUserProfile());
} elseif ($action == 'logout') {
$fieldsResult = JsonTool::encode($this->UserLoginProfile->_getUserProfile());
} else if ($action === 'logout') {
$description = "User (" . $user['id'] . "): " . $user['email'];
} elseif ($action == 'edit') {
} else if ($action === 'edit') {
$description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
} elseif ($action == 'change_pw') {
} else if ($action === 'change_pw') {
$description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
$fieldsResult = "Password changed.";
}
// query
$result = $this->loadLog()->createLogEntry($user, $action, $model, $modelId, $description, $fieldsResult);
$result = $this->loadLog()->createLogEntry($user, $action, 'User', $modelId, $description, $fieldsResult);
// write to syslogd as well
if ($result) {
App::import('Lib', 'SysLog.SysLog');
@ -2037,29 +2054,36 @@ class User extends AppModel
return $users;
}
public function checkForSessionDestruction($id)
/**
* @param int $id
* @param int $sessionCreationTimestamp
* @return bool
* @throws RedisException
*/
public function checkForSessionDestruction($id, $sessionCreationTimestamp)
{
if (empty(CakeSession::read('creation_timestamp'))) {
try {
$redis = RedisTool::init();
} catch (Exception $e) {
return false;
}
$redis = $this->setupRedis();
if ($redis) {
$cutoff = $redis->get('misp:session_destroy:' . $id);
$allcutoff = $redis->get('misp:session_destroy:all');
if (
empty($cutoff) ||
(
!empty($cutoff) &&
!empty($allcutoff) &&
$allcutoff < $cutoff
)
) {
$cutoff = $allcutoff;
}
if ($cutoff && CakeSession::read('creation_timestamp') < $cutoff) {
return true;
}
$cutoff = $redis->get('misp:session_destroy:' . $id);
$allcutoff = $redis->get('misp:session_destroy:all');
if (
empty($cutoff) ||
(
!empty($cutoff) &&
!empty($allcutoff) &&
$allcutoff < $cutoff
)
) {
$cutoff = $allcutoff;
}
if ($cutoff && $sessionCreationTimestamp < $cutoff) {
return true;
}
return false;
}

View File

@ -1,7 +1,9 @@
<?php
App::uses('AppModel', 'Model');
/**
* @property User $User
*/
class UserLoginProfile extends AppModel
{
public $actsAs = array(
@ -12,7 +14,6 @@ class UserLoginProfile extends AppModel
'userKey' => 'user_id',
'change' => 'full'
),
'Containable'
);
public $validate = [
@ -20,34 +21,37 @@ class UserLoginProfile extends AppModel
'rule' => '/^(trusted|malicious)$/',
'message' => 'Must be one of: trusted, malicious'
],
];
public $order = array("UserLoginProfile.id" => "DESC");
public $belongsTo = [
'User' => [
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
]];
'User' => [
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
]
];
protected $browscapCacheDir = APP . DS . 'tmp' . DS . 'browscap';
protected $browscapIniFile = APP . DS . 'files' . DS . 'browscap'. DS . 'browscap.ini'; // Browscap file managed by MISP - https://browscap.org/stream?q=Lite_PHP_BrowsCapINI
protected $geoIpDbFile = APP . DS . 'files' . DS . 'geo-open' . DS . 'GeoOpen-Country.mmdb'; // GeoIP file managed by MISP - https://data.public.lu/en/datasets/geo-open-ip-address-geolocation-per-country-in-mmdb-format/
const BROWSER_CACHE_DIR = APP . DS . 'tmp' . DS . 'browscap';
const BROWSER_INI_FILE = APP . DS . 'files' . DS . 'browscap'. DS . 'browscap.ini'; // Browscap file managed by MISP - https://browscap.org/stream?q=Lite_PHP_BrowsCapINI
const GEOIP_DB_FILE = APP . DS . 'files' . DS . 'geo-open' . DS . 'GeoOpen-Country.mmdb'; // GeoIP file managed by MISP - https://data.public.lu/en/datasets/geo-open-ip-address-geolocation-per-country-in-mmdb-format/
private $userProfile;
private $knownUserProfiles = [];
public function _buildBrowscapCache() {
$this->log("Browscap - building new cache from browscap.ini file.", "info");
$fileCache = new \Doctrine\Common\Cache\FilesystemCache($this->browscapCacheDir);
private function _buildBrowscapCache()
{
$this->log("Browscap - building new cache from browscap.ini file.", LOG_INFO);
$fileCache = new \Doctrine\Common\Cache\FilesystemCache(UserLoginProfile::BROWSER_CACHE_DIR);
$cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache);
$logger = new \Monolog\Logger('name');
$bc = new \BrowscapPHP\BrowscapUpdater($cache, $logger);
$bc->convertFile($this->browscapIniFile);
$bc->convertFile(UserLoginProfile::BROWSER_INI_FILE);
}
public function beforeSave($options = [])
@ -56,7 +60,8 @@ class UserLoginProfile extends AppModel
return true;
}
public function hash($data) {
public function hash(array $data)
{
unset($data['hash']);
unset($data['created_at']);
return md5(serialize($data));
@ -66,12 +71,13 @@ class UserLoginProfile extends AppModel
* slow function - don't call it too often
* @return array
*/
public function _getUserProfile() {
public function _getUserProfile()
{
if (!$this->userProfile) {
// below uses https://github.com/browscap/browscap-php
if (class_exists('\BrowscapPHP\Browscap')) {
try {
$fileCache = new \Doctrine\Common\Cache\FilesystemCache($this->browscapCacheDir);
$fileCache = new \Doctrine\Common\Cache\FilesystemCache(UserLoginProfile::BROWSER_CACHE_DIR);
$cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache);
$logger = new \Monolog\Logger('name');
$bc = new \BrowscapPHP\Browscap($cache, $logger);
@ -82,7 +88,7 @@ class UserLoginProfile extends AppModel
}
} else {
// a primitive OS & browser extraction capability
$ua = env('HTTP_USER_AGENT');
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
$browser = new stdClass();
$browser->browser_name_pattern = $ua;
if (mb_strpos($ua, 'Linux') !== false) $browser->platform = "Linux";
@ -95,16 +101,21 @@ class UserLoginProfile extends AppModel
}
$ip = $this->_remoteIp();
if (class_exists('GeoIp2\Database\Reader')) {
$geoDbReader = new GeoIp2\Database\Reader($this->geoIpDbFile);
$record = $geoDbReader->country($ip);
$country = $record->country->isoCode;
try {
$geoDbReader = new GeoIp2\Database\Reader(UserLoginProfile::GEOIP_DB_FILE);
$record = $geoDbReader->country($ip);
$country = $record->country->isoCode;
} catch (InvalidArgumentException $e) {
$this->logException("Could not get country code for IP address", $e);
$country = 'None';
}
} else {
$country = 'None';
}
$this->userProfile = [
'user_agent' => env('HTTP_USER_AGENT'),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'ip' => $ip,
'accept_lang' => env('HTTP_ACCEPT_LANGUAGE'),
'accept_lang' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null,
'geoip' => $country,
'ua_pattern' => $browser->browser_name_pattern,
'ua_platform' => $browser->platform,
@ -114,62 +125,82 @@ class UserLoginProfile extends AppModel
return $this->userProfile;
}
public function _fromLog($logEntry) {
$data = json_decode('{"user_agent": "", "ip": "", "accept_lang":"", "geoip":"", "ua_pattern":"", "ua_platform":"", "ua_browser":""}', true);
$data = array_merge($data, json_decode($logEntry['change'], true) ?? []);
/**
* @param array $logEntry
* @return array|false|string[]
* @throws JsonException
*/
public function _fromLog(array $logEntry)
{
if (!$logEntry['change']) {
return false;
}
$data = ["user_agent" => "", "ip" => "", "accept_lang" => "", "geoip" => "", "ua_pattern" => "", "ua_platform" => "", "ua_browser" => ""];
$data = array_merge($data, JsonTool::decode($logEntry['change']));
if ($data['user_agent'] === "") {
return false;
}
$data['ip'] = $logEntry['ip'];
$data['timestamp'] = $logEntry['created'];
if ($data['user_agent'] == "") return false;
return $data;
}
public function _isSimilar($a, $b) {
public function _isSimilar($a, $b)
{
// if one is not initialized
if (!$a || !$b) return false;
// transition for old logs where UA was not known
if (!$a['ua_browser'])
return false;
// really similar session, from same browser, region, but different IP
if ($a['ua_browser'] == $b['ua_browser'] &&
$a['ua_platform'] == $b['ua_platform'] &&
$a['accept_lang'] == $b['accept_lang'] &&
$a['geoip'] == $b['geoip']) {
if ($a['ua_browser'] === $b['ua_browser'] &&
$a['ua_platform'] === $b['ua_platform'] &&
$a['accept_lang'] === $b['accept_lang'] &&
$a['geoip'] === $b['geoip']) {
return true;
}
// similar browser pattern, OS and region
if ($a['ua_pattern'] == $b['ua_pattern'] &&
$a['ua_platform'] == $b['ua_platform'] &&
$a['accept_lang'] == $b['accept_lang'] &&
$a['geoip'] == $b['geoip']) {
if ($a['ua_pattern'] === $b['ua_pattern'] &&
$a['ua_platform'] === $b['ua_platform'] &&
$a['accept_lang'] === $b['accept_lang'] &&
$a['geoip'] === $b['geoip']) {
return true;
}
return false;
}
public function _isIdentical($a, $b) {
if ($a['ip'] == $b['ip'] &&
$a['ua_browser'] == $b['ua_browser'] &&
$a['ua_platform'] == $b['ua_platform'] &&
$a['accept_lang'] == $b['accept_lang'] &&
$a['geoip'] == $b['geoip']) {
public function _isIdentical(array $a, array $b)
{
if ($a['ip'] === $b['ip'] &&
$a['ua_browser'] === $b['ua_browser'] &&
$a['ua_platform'] === $b['ua_platform'] &&
$a['accept_lang'] === $b['accept_lang'] &&
$a['geoip'] === $b['geoip']) {
return true;
}
return false;
}
public function _getTrustStatus($userProfileToCheck, $user_id = null) {
if (!$user_id) {
$user_id = AuthComponent::user('id');
/**
* @param array $userProfileToCheck
* @param int $userId
* @return mixed|string
*/
public function _getTrustStatus(array $userProfileToCheck, $userId = null)
{
if (!$userId) {
$userId = AuthComponent::user('id');
}
// load Singleton / caching
if (!isset($this->knownUserProfiles[$user_id])) {
$this->knownUserProfiles[$user_id] = $this->find('all', [
'conditions' => ['UserLoginProfile.user_id' => $user_id],
'recursive' => 0]
);
if (!isset($this->knownUserProfiles[$userId])) {
$this->knownUserProfiles[$userId] = $this->find('all', [
'conditions' => ['UserLoginProfile.user_id' => $userId],
'recursive' => -1,
]);
}
// perform check on all entries, and stop when check OK
foreach ($this->knownUserProfiles[$user_id] as $knownUserProfile) {
foreach ($this->knownUserProfiles[$userId] as $knownUserProfile) {
// when it is the same
if ($this->_isIdentical($knownUserProfile['UserLoginProfile'], $userProfileToCheck)) {
return $knownUserProfile['UserLoginProfile']['status'];
@ -183,29 +214,28 @@ class UserLoginProfile extends AppModel
return 'unknown';
}
public function _isTrusted() {
public function _isTrusted()
{
if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'trusted') !== false) {
return true;
}
return false;
}
public function _isSuspicious() {
public function _isSuspicious()
{
// previously marked loginuserprofile as malicious by the user
if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'malicious') !== false) {
return _('A user reported a similar login profile as malicious.');
return __('A user reported a similar login profile as malicious.');
}
// same IP as previous malicious user
$maliciousWithSameIP = $this->find('first', [
'conditions' => [
'UserLoginProfile.ip' => $this->_getUserProfile()['ip'],
'UserLoginProfile.status' => 'malicious'
],
'recursive' => 0,
'fields' => array('UserLoginProfile.*')]
);
$maliciousWithSameIP = $this->hasAny([
'UserLoginProfile.ip' => $this->_getUserProfile()['ip'],
'UserLoginProfile.status' => 'malicious'
]);
if ($maliciousWithSameIP) {
return _('The source IP was reported as as malicious by a user.');
return __('The source IP was reported as as malicious by a user.');
}
// LATER - use other data to identify suspicious logins, such as:
// - what with use-case where a user marks something as legitimate, but is marked by someone else as suspicious?
@ -214,7 +244,8 @@ class UserLoginProfile extends AppModel
return false;
}
public function email_newlogin($user) {
public function emailNewLogin(array $user)
{
if (!Configure::read('MISP.disable_emailing')) {
$date_time = date('c');
@ -224,16 +255,12 @@ class UserLoginProfile extends AppModel
$body->set('misp_org', Configure::read('MISP.org'));
$body->set('date_time', $date_time);
// Fetch user that contains also PGP or S/MIME keys for e-mail encryption
$result = $this->User->sendEmail($user, $body, false, "[" . Configure::read('MISP.org') . " MISP] New sign in.");
if ($result) {
// all is well, email sent to user
} else {
// email flow system already logs errors
}
$this->User->sendEmail($user, $body, false, "[" . Configure::read('MISP.org') . " MISP] New sign in.");
}
}
public function email_report_malicious($user, $userLoginProfile) {
public function emailReportMalicious(array $user, array $userLoginProfile)
{
// inform the org admin
$date_time = $userLoginProfile['timestamp']; // LATER not ideal as timestamp is string without timezone info
$body = new SendEmailTemplate('userloginprofile_report_malicious');
@ -242,24 +269,23 @@ class UserLoginProfile extends AppModel
$body->set('baseurl', Configure::read('MISP.baseurl'));
$body->set('misp_org', Configure::read('MISP.org'));
$body->set('date_time', $date_time);
$org_admins = $this->User->getOrgAdminsForOrg($user['User']['org_id']);
$admins = $this->User->getSiteAdmins();
$all_admins = array_unique(array_merge($org_admins, $admins));
foreach($all_admins as $admin_email) {
$orgAdmins = array_keys($this->User->getOrgAdminsForOrg($user['User']['org_id']));
$admins = array_keys($this->User->getSiteAdmins());
$allAdmins = array_unique(array_merge($orgAdmins, $admins));
$subject = __("[%s MISP] Suspicious login reported.", Configure::read('MISP.org'));
foreach ($allAdmins as $adminUserId) {
$admin = $this->User->find('first', array(
'recursive' => -1,
'conditions' => ['User.email' => $admin_email]
'conditions' => ['User.id' => $adminUserId]
));
$result = $this->User->sendEmail($admin, $body, false, "[" . Configure::read('MISP.org') . " MISP] Suspicious login reported.");
if ($result) {
// all is well, email sent to user
} else {
// email flow system already logs errors
}
$this->User->sendEmail($admin, $body, false, $subject);
}
}
public function email_suspicious($user, $suspiciousness_reason) {
public function email_suspicious(array $user, $suspiciousness_reason)
{
if (!Configure::read('MISP.disable_emailing')) {
$date_time = date('c');
// inform the user
@ -271,12 +297,8 @@ class UserLoginProfile extends AppModel
$body->set('date_time', $date_time);
$body->set('suspiciousness_reason', $suspiciousness_reason);
// inform the user
$result = $this->User->sendEmail($user, $body, false, "[" . Configure::read('MISP.org') . " MISP] Suspicious login with your account.");
if ($result) {
// all is well, email sent to user
} else {
// email flow system already logs errors
}
$this->User->sendEmail($user, $body, false, "[" . Configure::read('MISP.org') . " MISP] Suspicious login with your account.");
// inform the org admin
$body = new SendEmailTemplate('userloginprofile_suspicious_orgadmin');
$body->set('userLoginProfile', $this->_getUserProfile());
@ -285,21 +307,15 @@ class UserLoginProfile extends AppModel
$body->set('misp_org', Configure::read('MISP.org'));
$body->set('date_time', $date_time);
$body->set('suspiciousness_reason', $suspiciousness_reason);
$org_admins = $this->User->getOrgAdminsForOrg($user['User']['org_id']);
foreach($org_admins as $org_admin_email) {
$orgAdmins = array_keys($this->User->getOrgAdminsForOrg($user['User']['org_id']));
foreach ($orgAdmins as $orgAdminID) {
$org_admin = $this->User->find('first', array(
'recursive' => -1,
'conditions' => ['User.email' => $org_admin_email]
'conditions' => ['User.id' => $orgAdminID]
));
$result = $this->User->sendEmail($org_admin, $body, false, "[" . Configure::read('MISP.org') . " MISP] Suspicious login detected.");
if ($result) {
// all is well, email sent to user
} else {
// email flow system already logs errors
}
$this->User->sendEmail($org_admin, $body, false, "[" . Configure::read('MISP.org') . " MISP] Suspicious login detected.");
}
}
}
}

View File

@ -0,0 +1,424 @@
<?php
App::uses('JsonTool', 'Tools');
/**
* Logging class that sends logs in JSON format to UNIX socket in Elastic Common Schema (ECS) format
* Logs are separated by new line characters, so basically it is send as JSONL
*/
class EcsLog implements CakeLogInterface
{
const ECS_VERSION = '8.11';
/** @var string Unix socket path where logs will be send in JSONL format */
const SOCKET_PATH = '/run/vector';
/** @var false|resource */
private static $socket;
/** @var string[] */
private static $messageBuffer = [];
/** @var array[] */
private static $meta;
const LOG_LEVEL_STRING = [
LOG_EMERG => 'emergency',
LOG_ALERT => 'alert',
LOG_CRIT => 'critical',
LOG_ERR => 'error',
LOG_WARNING => 'warning',
LOG_NOTICE => 'notice',
LOG_INFO => 'info',
LOG_DEBUG => 'debug',
];
/**
* @param string $type The type of log you are making.
* @param string $message The message you want to log.
* @return void
*/
public function write($type, $message)
{
if (strpos($message, 'Could not convert ECS log message into JSON: ') !== false) {
return; // prevent recursion when saving logs
}
$message = [
'@timestamp' => self::now(),
'ecs' => [
'version' => self::ECS_VERSION,
],
'event' => [
'kind' => 'event',
'provider' => 'misp',
'module' => 'system',
'dataset' => 'system.logs',
],
'log' => [
'level' => $type,
],
'message' => $message,
];
static::writeMessage($message);
}
/**
* @param string $type
* @param string $action
* @param string $message
* @return void
*/
public static function writeApplicationLog($type, $action, $message)
{
$message = [
'@timestamp' => self::now(),
'ecs' => [
'version' => self::ECS_VERSION,
],
'event' => [
'kind' => 'event',
'provider' => 'misp',
'module' => 'application',
'dataset' => 'application.logs',
'action' => $action,
],
'log' => [
'level' => $type,
],
'message' => $message,
];
if (in_array($action, Log::AUTH_ACTIONS, true)) {
$message['event']['category'] = 'authentication';
if (in_array($action, ['auth_fail', 'login_fail'], true)) {
$message['event']['outcome'] = 'failure';
}
}
static::writeMessage($message);
}
/**
* Include more meta information about email than would provide default `writeApplicationLog` log
* @param string $logTitle
* @param array $emailResult
* @param string|null $replyTo
* @return void
*/
public static function writeEmailLog($logTitle, array $emailResult, $replyTo = null)
{
$message = [
'@timestamp' => self::now(),
'ecs' => [
'version' => self::ECS_VERSION,
],
'event' => [
'kind' => 'event',
'provider' => 'misp',
'module' => 'application',
'dataset' => 'application.logs',
'category' => 'email',
'action' => 'email',
'type' => 'info',
],
'email' => [
'message_id' => $emailResult['message_id'],
'subject' => $emailResult['subject'],
'to' => [
'address' => $emailResult['to'],
],
],
'message' => $logTitle,
];
if ($replyTo) {
$message['email']['reply_to'] = ['address' => $replyTo];
}
static::writeMessage($message);
}
/**
* @param int $code
* @param string $description
* @param string|null $file
* @param int|null $line
* @return void
*/
public static function handleError($code, $description, $file = null, $line = null)
{
list($name, $log) = ErrorHandler::mapErrorCode($code);
$level = self::LOG_LEVEL_STRING[$log];
$message = [
'@timestamp' => self::now(),
'ecs' => [
'version' => self::ECS_VERSION,
],
'event' => [
'kind' => 'event',
'provider' => 'misp',
'module' => 'system',
'dataset' => 'system.logs',
'type' => 'error',
],
'error' => [
'code' => $code,
'message' => $description,
],
'log' => [
'level' => $level,
'origin' => [
'file' => [
'name' => $file,
'line' => $line,
],
],
],
];
static::writeMessage($message);
}
/**
* @param Exception $exception
* @return void
*/
public static function handleException(Exception $exception)
{
$code = $exception->getCode();
$code = ($code && is_int($code)) ? $code : 1;
$message = [
'@timestamp' => self::now(),
'ecs' => [
'version' => self::ECS_VERSION,
],
'event' => [
'kind' => 'event',
'provider' => 'misp',
'module' => 'system',
'dataset' => 'system.logs',
'type' => 'error',
],
'error' => [
'code' => $code,
'type' => get_class($exception),
'message' => $exception->getMessage(),
'stack_trace' => $exception->getTraceAsString(),
],
'log' => [
'level' => 'error',
'origin' => [
'file' => [
'name' => $exception->getFile(),
'line' => $exception->getLine(),
],
],
],
];
static::writeMessage($message);
}
/**
* @return array|null
*/
private static function clientIpFromHeaders()
{
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: null;
if ($ipHeader && isset($_SERVER[$ipHeader])) {
return array_map('trim', explode(',', $_SERVER[$ipHeader]));
}
return null;
}
/**
* @return array[]
*/
private static function createLogMeta()
{
if (self::$meta) {
return self::$meta;
}
$meta = ['process' => ['pid' => getmypid()]];
// Add metadata if log was generated because of HTTP request
if (PHP_SAPI !== 'cli') {
if (isset($_SERVER['HTTP_X_REQUEST_ID'])) {
$meta['http'] = ['request' => ['id' => $_SERVER['HTTP_X_REQUEST_ID']]];
}
$meta['client'] = self::createClientMeta();
$meta['url'] = self::createUrlMeta();
} else {
$meta['process']['argv'] = $_SERVER['argv'];
}
$userMeta = self::createUserMeta();
if ($userMeta) {
$meta['user'] = $userMeta;
}
return self::$meta = $meta;
}
/**
* @return array
*/
private static function createClientMeta()
{
$client = [
'ip' => $_SERVER['REMOTE_ADDR'],
'port' => (int) $_SERVER['REMOTE_PORT'],
];
$clientIps = static::clientIpFromHeaders();
if ($clientIps) {
$clientIps[] = $_SERVER['REMOTE_ADDR'];
return [
'address' => $clientIps,
'ip' => $clientIps[0], // consider first IP as real client IP address
'nat' => $client,
];
}
$client['address'] = [$client['ip']];
return $client;
}
/**
* @return array
*/
private static function createUrlMeta()
{
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
list($path, $query) = explode('?', $_SERVER['REQUEST_URI'], 2);
$url = [
'path' => $path,
'query' => $query,
];
} else {
$url = ['path' => $_SERVER['REQUEST_URI']];
}
if (strpos($_SERVER['HTTP_HOST'], ':') !== false) {
list($domain, $port) = explode(':', $_SERVER['HTTP_HOST'], 2);
$url['domain'] = $domain;
$url['port'] = (int) $port;
} else {
$url['domain'] = $_SERVER['HTTP_HOST'];
}
return $url;
}
/**
* Get user metadata (use unique id and email address)
* @return array|null
*/
private static function createUserMeta()
{
if (PHP_SAPI === 'cli') {
$currentUserId = Configure::read('CurrentUserId');
if (!empty($currentUserId)) {
/** @var User $userModel */
$userModel = ClassRegistry::init('User');
$user = $userModel->find('first', [
'recursive' => -1,
'conditions' => ['id' => $currentUserId],
'fields' => ['sub', 'email'],
]);
if (!empty($user)) {
return [
'id' => $user['User']['sub'] ?? $currentUserId,
'email' => $user['User']['email'],
];
}
}
} else if (session_status() === PHP_SESSION_ACTIVE) {
// include session data just when session is active to avoid unnecessary session starting
App::uses('AuthComponent', 'Controller/Component');
$authUser = AuthComponent::user();
if (!empty($authUser)) {
return [
'id' => $authUser['sub'] ?? $authUser['id'],
'email' => $authUser['email'],
];
}
}
return null;
}
/**
* ISO 8601 timestamp with microsecond precision
* @return string
*/
private static function now()
{
return (new DateTime())->format('Y-m-d\TH:i:s.uP');
}
/**
* @param array $message
* @return bool True when message was successfully send to socket, false if message was saved to buffer
*/
private static function writeMessage(array $message)
{
$message = array_merge($message, self::createLogMeta());
try {
$data = JsonTool::encode($message) . "\n";
} catch (JsonException $e) {
CakeLog::error('Could not convert ECS log message into JSON: ' . $e->getMessage());
return null;
}
if (static::$socket === null) {
static::connect();
}
if (static::$socket) {
$bytesWritten = fwrite(static::$socket, $data);
if ($bytesWritten !== false) {
return true;
}
// In case of failure, try reconnect and send log again
static::connect();
if (static::$socket) {
$bytesWritten = fwrite(static::$socket, $data);
if ($bytesWritten !== false) {
return true;
}
}
}
// If sending message was not successful, save to buffer
self::$messageBuffer[] = $data;
if (count(self::$messageBuffer) > 100) {
array_shift(self::$messageBuffer); // remove oldest log
}
return false;
}
private static function connect()
{
static::$socket = null;
if (!file_exists(static::SOCKET_PATH)) {
return;
}
static::$socket = stream_socket_client('unix://' . static::SOCKET_PATH, $errorCode, $errorMessage);
if (static::$socket) {
foreach (self::$messageBuffer as $message) {
fwrite(static::$socket, $message);
}
self::$messageBuffer = [];
}
}
}

View File

@ -44,7 +44,7 @@ class Oidc
if (!$user) { // User by sub not found, try to find by email
$user = $this->_findUser($settings, ['User.email' => $mispUsername]);
if ($user && $user['sub'] !== null && $user['sub'] !== $sub) {
$this->log($mispUsername, "User sub doesn't match ({$user['sub']} != $sub), could not login.");
$this->log($mispUsername, "User sub doesn't match ({$user['sub']} != $sub), could not login.", LOG_ERR);
return false;
}
}
@ -66,7 +66,7 @@ class Oidc
$roleProperty = $this->getConfig('roles_property', 'roles');
$roles = $claims->{$roleProperty} ?? $oidc->requestUserInfo($roleProperty);
if ($roles === null) {
$this->log($mispUsername, "Role property `$roleProperty` is missing in claims.");
$this->log($mispUsername, "Role property `$roleProperty` is missing in claims.", LOG_WARNING);
return false;
}
@ -79,6 +79,8 @@ class Oidc
return false;
}
$offlineAccessEnabled = $this->getConfig('offline_access', false);
if ($user) {
$this->log($mispUsername, "Found in database with ID {$user['id']}.");
@ -112,7 +114,10 @@ class Oidc
$user['disabled'] = false;
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$refreshToken = $offlineAccessEnabled ? $oidc->getRefreshToken() : null;
if ($offlineAccessEnabled && $refreshToken === null) {
$this->log($mispUsername, 'Refresh token requested, but not provided.', LOG_WARNING);
}
$this->storeMetadata($user['id'], $claims, $refreshToken);
$this->log($mispUsername, 'Logged in.');
@ -138,7 +143,10 @@ class Oidc
throw new RuntimeException("Could not create user `$mispUsername` in database.");
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$refreshToken = $offlineAccessEnabled ? $oidc->getRefreshToken() : null;
if ($offlineAccessEnabled && $refreshToken === null) {
$this->log($mispUsername, 'Refresh token requested, but not provided.', LOG_WARNING);
}
$this->storeMetadata($this->User->id, $claims, $refreshToken);
$this->log($mispUsername, "User created in database with ID {$this->User->id}");
@ -518,19 +526,20 @@ class Oidc
/**
* @param string|null $username
* @param string $message
* @param int $type
*/
private function log($username, $message)
private function log($username, $message, $type = LOG_INFO)
{
$sessionId = substr(session_id(), 0, 6);
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR';
$ip = isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : $_SERVER['REMOTE_ADDR'];
$log = $username ? "OIDC user `$username`" : "OIDC";
if ($username) {
$message = "OIDC user `$username` [$ip;$sessionId] $message";
if (PHP_SAPI !== 'cli') {
$sessionId = substr(session_id(), 0, 6);
$ip = $this->User->_remoteIp();
$log .= " [$ip;$sessionId] - $message";
} else {
$message = "OIDC [$ip;$sessionId] $message";
$log .= " - $message";
}
CakeLog::info($message);
CakeLog::write($type, $log);
}
}

View File

@ -103,6 +103,16 @@
'popover-popup' => $baseurl . '/galaxies/selectGalaxyNamespace/selected/attribute/eventid:' . $eventId,
],
),
array(
'id' => 'multi-galaxy-button',
'title' => __('Add new local cluster to selected Attributes'),
'class' => 'mass-select hidden',
'fa-icon' => 'empire',
'fa-source' => 'fab',
'data' => [
'popover-popup' => $baseurl . '/galaxies/selectGalaxyNamespace/selected/attribute/local:1/eventid:' . $eventId,
],
),
array(
'id' => 'group-into-object-button',
'title' => __('Group selected Attributes into an Object'),

View File

@ -27,9 +27,6 @@
'label' => __('Distribution ') . $distributionFormInfo,
'selected' => $initialDistribution,
));
?>
<div id="SGContainer" style="display:none;">
<?php
if (!empty($sharingGroups)) {
echo $this->Form->input('sharing_group_id', array(
'options' => array($sharingGroups),
@ -37,7 +34,6 @@
));
}
?>
</div>
<div class="input clear"></div>
<?php
echo $this->Form->input('publish', array(
@ -52,7 +48,7 @@
'label' => __('Include the original imported file as attachment')
));
if ($me['Role']['perm_site_admin'] || $me['Role']['perm_galaxy_editor']) {
$galaxiesFormInfo = $this-> element(
$galaxiesFormInfo = $this->element(
'genericElements/Form/formInfo',
[
'field' => [
@ -101,11 +97,8 @@
<script>
$(function(){
$('#EventDistribution').change(function() {
if ($(this).val() == 4) {
$('#SGContainer').show();
} else {
$('#SGContainer').hide();
}
}).change();
checkSharingGroup('Event');
});
checkSharingGroup('Event');
});
</script>

View File

@ -1,5 +1,4 @@
<?php
$modelForForm = 'Cerebrates';
$edit = $this->request->params['action'] === 'edit' ? true : false;
$fields = [
[

View File

@ -25,7 +25,7 @@ if (!function_exists('str_contains')) {
}
}
foreach($data as $entry ){
foreach ($data as $entry) {
$platform = h(strtolower($entry['platform']));
if (str_contains($platform, 'win')) $platform = 'windows';
if (str_contains($platform, 'macosx')) $platform = 'apple';
@ -34,7 +34,7 @@ foreach($data as $entry ){
if (str_contains($entry['status'], 'malicious')) $bgcolor = '#ffdddd';
elseif (str_contains($entry['status'], 'trusted')) $bgcolor = '#ddffdd';
?>
<div class="device-overview"style="background-color:<?= $bgcolor ?>;">
<div class="device-overview" style="background-color:<?= $bgcolor ?>;">
<div>
<span class="device-icon fab fa-<?= h(strtolower($platform))?>" style="font-size:30px;"></span>
<span class="device-icon fab fa-<?= h(strtolower($entry['browser']))?>" style="font-size:30px;"></span>
@ -79,5 +79,4 @@ echo '</div>';
if (!$this->request->is('ajax')) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'globalActions', 'menuItem' => 'view'));
}

@ -1 +1 @@
Subproject commit e5b4209f3a88147985120215eb898e563e4976af
Subproject commit c8e8a14b04f549c97550ce6cbd5d5d092be4fefd

@ -1 +1 @@
Subproject commit 587b298e1e7f87426182d55d44aa045a1522dc98
Subproject commit 888e0dceda905076635ecc7d589ee3effe3c45d6

@ -1 +1 @@
Subproject commit 260920bf7c9d8f678b0d69730acb17e9a34811f2
Subproject commit 07caf0b5177731b9dce9cd35d467465471e8539e

View File

@ -78,9 +78,13 @@ class StixExport:
if self._parser.errors:
self._handle_errors()
print(json.dumps(results))
except Exception as e:
print(json.dumps({'error': e.__str__()}))
error = type(e).__name__ + ': ' + e.__str__()
print(json.dumps({'error': error}))
traceback.print_tb(e.__traceback__)
print(error, file=sys.stderr)
sys.exit(1)
class StixAttributesExport(StixExport):
@ -157,19 +161,23 @@ class StixEventsExport(StixExport):
if __name__ == "__main__":
argparser = argparse.ArgumentParser(description='Export MISP into STIX1.')
argparser.add_argument('-s', '--scope', default='Event', choices=['Attribute', 'Event'], help='Scope: which kind of data is exported.')
argparser.add_argument('-v', '--version', default='1.1.1', choices=['1.1.1', '1.2'], help='STIX version (1.1.1 or 1.2).')
argparser.add_argument('-f', '--format', default='xml', choices=['json', 'xml'], help='Output format (xml or json).')
argparser.add_argument('-s', '--scope', default='Event', choices=('Attribute', 'Event'), help='Scope: which kind of data is exported.')
argparser.add_argument('-v', '--version', default='1.1.1', choices=('1.1.1', '1.2'), help='STIX version (1.1.1 or 1.2).')
argparser.add_argument('-f', '--format', default='xml', choices=('json', 'xml'), help='Output format (xml or json).')
argparser.add_argument('-i', '--input', nargs='+', help='Input file(s) containing MISP standard format.')
argparser.add_argument('-o', '--orgname', default='MISP', help='Default Org name to use if no Orgc value is provided.')
argparser.add_argument('-d', '--debug', action='store_true', help='Allow debug mode with warnings.')
try:
args = argparser.parse_args()
if args.input is None:
print(json.dumps({'error': 'No input file provided.'}))
else:
arguments = (args.orgname, args.format, args.version, args.debug)
exporter = globals()[f'Stix{args.scope}sExport'](*arguments)
exporter.parse_misp_files(args.input)
except SystemExit:
print(json.dumps({'error': 'Arguments error, please check you entered a valid version and provided input file names.'}))
sys.exit(1)
if args.input is None:
print(json.dumps({'error': 'No input file provided.'}))
sys.exit(1)
arguments = (args.orgname, args.format, args.version, args.debug)
exporter = globals()[f'Stix{args.scope}sExport'](*arguments)
exporter.parse_misp_files(args.input)
sys.exit(0)

View File

@ -49,14 +49,14 @@ def _process_misp_files(
version: str, input_names: Union[list, None], debug: bool):
if input_names is None:
print(json.dumps({'error': 'No input file provided.'}))
return
sys.exit(1)
try:
parser = MISPtoSTIX20Parser() if version == '2.0' else MISPtoSTIX21Parser()
for name in input_names:
parser.parse_json_content(name)
with open(f'{name}.out', 'wt', encoding='utf-8') as f:
f.write(
f'{json.dumps(parser.stix_objects, cls=STIXJSONEncoder)}'
json.dumps(parser.stix_objects, cls=STIXJSONEncoder)
)
if parser.errors:
_handle_messages('Errors', parser.errors)
@ -64,8 +64,11 @@ def _process_misp_files(
_handle_messages('Warnings', parser.warnings)
print(json.dumps({'success': 1}))
except Exception as e:
print(json.dumps({'error': e.__str__()}))
error = type(e).__name__ + ': ' + e.__str__()
print(json.dumps({'error': error}))
traceback.print_tb(e.__traceback__)
print(error, file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
@ -84,7 +87,6 @@ if __name__ == "__main__":
)
try:
args = argparser.parse_args()
_process_misp_files(args.version, args.input, args.debug)
except SystemExit:
print(
json.dumps(
@ -94,3 +96,7 @@ if __name__ == "__main__":
}
)
)
sys.exit(1)
_process_misp_files(args.version, args.input, args.debug)
sys.exit(0)

View File

@ -44,7 +44,7 @@ def _handle_return_message(traceback):
return '\n - '.join(traceback)
def _process_stix_file(args: argparse.ArgumentParser):
def _process_stix_file(args: argparse.Namespace):
try:
with open(args.input, 'rt', encoding='utf-8') as f:
bundle = stix2_parser(
@ -81,8 +81,11 @@ def _process_stix_file(args: argparse.ArgumentParser):
file=sys.stderr
)
except Exception as e:
print(json.dumps({'error': e.__str__()}))
error = type(e).__name__ + ': ' + e.__str__()
print(json.dumps({'error': error}))
traceback.print_tb(e.__traceback__)
print(error, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
@ -109,8 +112,7 @@ if __name__ == '__main__':
)
try:
args = argparser.parse_args()
_process_stix_file(args)
except SystemExit:
except SystemExit as e:
print(
json.dumps(
{
@ -118,4 +120,6 @@ if __name__ == '__main__':
}
)
)
sys.exit(1)
_process_stix_file(args)

View File

@ -96,7 +96,7 @@ class StixParser():
# Convert the MISP event we create from the STIX document into json format
# and write it in the output file
def saveFile(self):
def save_to_file(self):
for attribute in self.misp_event.attributes:
attribute_uuid = uuid.UUID(attribute.uuid) if isinstance(attribute.uuid, str) else attribute.uuid
if attribute_uuid.version not in _RFC_UUID_VERSIONS:
@ -1547,19 +1547,20 @@ def generate_event(filename, tries=0):
return STIXPackage.from_xml(filename)
except NamespaceNotFoundError:
if tries == 1:
print(json.dump({'error': 'Cannot handle STIX namespace'}))
sys.exit()
print(json.dumps({'error': 'Cannot handle STIX namespace'}))
sys.exit(1)
_update_namespaces()
return generate_event(filename, 1)
except NotImplementedError:
print(json.dumps({'error': 'Missing python library: stix_edh'}))
sys.exit(1)
except Exception as e:
try:
import maec
print(json.dumps({'error': f'Error while loading the STIX file: {e.__str__()}'}))
except ImportError:
print(json.dumps({'error': 'Missing python library: maec'}))
sys.exit(0)
sys.exit(1)
def is_from_misp(event):
@ -1567,7 +1568,7 @@ def is_from_misp(event):
title = event.stix_header.title
except AttributeError:
return False
return ('Export from ' in title and 'MISP' in title)
return 'Export from ' in title and 'MISP' in title
def main(args):
@ -1578,11 +1579,16 @@ def main(args):
stix_parser = StixFromMISPParser() if from_misp else ExternalStixParser()
stix_parser.load_event(args[2:], filename, from_misp, event.version)
stix_parser.build_misp_event(event)
stix_parser.saveFile()
stix_parser.save_to_file()
print(json.dumps({'success': 1}))
sys.exit(0)
except Exception as e:
print(json.dumps({'error': e.__str__()}))
error = type(e).__name__ + ': ' + e.__str__()
print(json.dumps({'error': error}))
traceback.print_tb(e.__traceback__)
print(error, file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main(sys.argv)

@ -1 +1 @@
Subproject commit 8d957d224ef339303d001167900ee38ce586d22d
Subproject commit 3d61b20e7ee8bca21f9bffe53c0952c54a6b72b0

@ -1 +1 @@
Subproject commit e7255574c79b946365fdd066429d822730010726
Subproject commit 6a9956dbcab676b42f1d245a1e12522078e3ee86

View File

@ -6,7 +6,7 @@ misp-lib-stix2>=3.0.1.1
mixbox>=1.0.5
plyara>=2.1.1
pydeep2>=0.5.1
pymisp==2.4.182
pymisp==2.4.183
python-magic>=0.4.27
pyzmq>=25.1.1
redis>=5.0.1