Merge branch '3.x' into feature/3.x_HttpTool

feature/3.x_HttpTool
Christophe Vandeplas 2024-02-20 14:23:41 +00:00
commit 448e78ffa6
53 changed files with 11286 additions and 3112 deletions

View File

@ -1,9 +1,9 @@
{
"name": "cakephp/app",
"description": "CakePHP skeleton app",
"name": "misp/misp",
"description": "MISP: Open Source Threat Intelligence Platform & Open Standards For Threat Information Sharing",
"homepage": "https://cakephp.org",
"type": "project",
"license": "MIT",
"license": " AGPL-3.0",
"require": {
"php": ">=8.0",
"admad/cakephp-social-auth": "^1.1",
@ -19,7 +19,7 @@
"pear/crypt_gpg": "^1.6",
"php-http/message": "^1.16",
"php-http/message-factory": "^1.1",
"supervisorphp/supervisor": "^4.0"
"supervisorphp/supervisor": "^5.0"
},
"require-dev": {
"cakephp/bake": "^2.0.3",
@ -86,4 +86,4 @@
"php-http/discovery": true
}
}
}
}

View File

@ -7,4 +7,6 @@ parameters:
- message: '#Undefined variable: \$this#'
path: %currentWorkingDirectory%/templates
- message: '#Function brotli_compress not found.#'
path: %currentWorkingDirectory%/*
- message: '#Call to an undefined method [a-zA-Z0-9\\_]+::changeKey()#'
path: %currentWorkingDirectory%/*

View File

@ -0,0 +1,283 @@
<?php
namespace App\Command;
use App\Lib\Tools\LogExtendedTrait;
use App\Model\Entity\Job;
use Cake\Console\ConsoleIo;
use Cake\Core\Configure;
use Exception;
class FeedsCommand extends MISPCommand
{
use LogExtendedTrait;
protected $defaultTable = 'Feeds';
/** @var \App\Model\Table\FeedsTable */
protected $Feeds;
protected $validActions = [
'fetchFeed',
'listFeeds',
'viewFeed',
'toggleFeed',
'toggleFeedCaching',
'loadDefaultFeeds',
'cacheFeed',
];
/** @var array */
protected $usage = [
'test' => 'bin/cake servers test `server_id`',
'fetchIndex' => 'bin/cake servers fetchIndex `server_id`',
'fetchFeed' => 'bin/cake servers `fetchFeed` `user_id` feed_id|all|csv|text|misp [job_id]',
'pullAll' => 'bin/cake servers pullAll `user_id` [full|update]',
'pull' => 'bin/cake servers pull `user_id` `server_id` [full|update]',
'push' => 'bin/cake servers push `user_id` `server_id` [full|update] [job_id]',
'pushAll' => 'bin/cake servers pushAll `user_id` [full|update]',
'listFeeds' => 'bin/cake servers listFeeds [json|table]',
'viewFeed' => 'bin/cake servers viewFeed `feed_id` [json|table]',
'toggleFeed' => 'bin/cake servers toggleFeed `feed_id`',
'toggleFeedCaching' => 'bin/cake servers toggleFeedCaching `feed_id`',
'cacheServer' => 'bin/cake servers cacheServer `user_id` `server_id|all` [job_id]',
'cacheServerAll' => 'bin/cake servers cacheServerAll `user_id` [job_id]',
'cacheFeed' => 'bin/cake servers cacheFeed `user_id` [feed_id|all|csv|text|misp] [job_id]',
];
public function fetchFeed($userId, $feedId, $jobId = null)
{
if (empty($userId) || empty($feedId)) {
$this->showActionUsageAndExit();
}
$UsersTable = $this->fetchTable('Users');
$user = $UsersTable->getAuthUser($userId, true);
Configure::write('CurrentUserId', $userId);
$JobsTable = $this->fetchTable('Jobs');
if (empty($jobId)) {
$jobId = $JobsTable->createJob($user->toArray(), Job::WORKER_DEFAULT, 'fetch_feeds', 'Feed: ' . $feedId, 'Starting fetch from Feed.');
}
if ($feedId === 'all') {
$feedIds = $this->Feeds->find(
'column',
[
'fields' => ['id'],
'conditions' => ['enabled' => 1]
]
)->toArray();
$successes = 0;
$fails = 0;
foreach ($feedIds as $k => $feedId) {
$JobsTable->saveProgress($jobId, 'Fetching feed: ' . $feedId, 100 * $k / count($feedIds));
$result = $this->Feeds->downloadFromFeedInitiator($feedId, $user);
if ($result) {
$successes++;
} else {
$fails++;
}
}
$message = 'Job done. ' . $successes . ' feeds pulled successfully, ' . $fails . ' feeds could not be pulled.';
$JobsTable->saveStatus($jobId, true, $message);
$this->io->out($message);
} else {
$feedEnabled = $this->Feeds->exists(
[
'enabled' => 1,
'id' => $feedId,
]
);
if ($feedEnabled) {
$result = $this->Feeds->downloadFromFeedInitiator($feedId, $user, $jobId);
if (!$result) {
$JobsTable->saveStatus($jobId, false, 'Job failed. See error log for more details.');
$this->io->error('Job failed.');
} else {
$JobsTable->saveStatus($jobId, true);
$this->io->out('Job done.');
}
} else {
$message = "Feed with ID $feedId not found or not enabled.";
$JobsTable->saveStatus($jobId, false, $message);
$this->io->error($message);
}
}
}
public function listFeeds($outputStyle = 'json')
{
$fields = [
'id' => 3,
'source_format' => 10,
'provider' => 15,
'url' => 50,
'enabled' => 8,
'caching_enabled' => 7
];
$feeds = $this->Feeds->find(
'all',
[
'recursive' => -1,
'fields' => array_keys($fields)
]
);
if ($outputStyle === 'table') {
$this->io->out(str_repeat('=', 114));
$this->io->out(
sprintf(
'| %s | %s | %s | %s | %s | %s |',
str_pad('ID', $fields['id'], ' ', STR_PAD_RIGHT),
str_pad('Format', $fields['source_format'], ' ', STR_PAD_RIGHT),
str_pad('Provider', $fields['provider'], ' ', STR_PAD_RIGHT),
str_pad('Url', $fields['url'], ' ', STR_PAD_RIGHT),
str_pad('Fetching', $fields['enabled'], ' ', STR_PAD_RIGHT),
str_pad('Caching', $fields['caching_enabled'], ' ', STR_PAD_RIGHT)
),
1,
ConsoleIo::NORMAL
);
$this->io->out(str_repeat('=', 114));
foreach ($feeds as $feed) {
$this->io->out(
sprintf(
'| %s | %s | %s | %s | %s | %s |',
str_pad($feed['id'], $fields['id'], ' ', STR_PAD_RIGHT),
str_pad($feed['source_format'], $fields['source_format'], ' ', STR_PAD_RIGHT),
str_pad(mb_substr($feed['provider'], 0, 13), $fields['provider'], ' ', STR_PAD_RIGHT),
str_pad(
mb_substr($feed['url'], 0, 48),
$fields['url'],
' ',
STR_PAD_RIGHT
),
$feed['enabled'] ?
'<info>' . str_pad(__('Yes'), $fields['enabled'], ' ', STR_PAD_RIGHT) . '</info>' :
str_pad(__('No'), $fields['enabled'], ' ', STR_PAD_RIGHT),
$feed['caching_enabled'] ?
'<info>' . str_pad(__('Yes'), $fields['caching_enabled'], ' ', STR_PAD_RIGHT) . '</info>' :
str_pad(__('No'), $fields['caching_enabled'], ' ', STR_PAD_RIGHT)
),
1,
ConsoleIo::NORMAL
);
}
$this->io->out(str_repeat('=', 114));
} else {
$this->outputJson($feeds);
}
}
public function viewFeed($feedId = null, $outputStyle = 'json')
{
if (empty($feedId)) {
$this->showActionUsageAndExit();
}
$feed = $this->Feeds->get($feedId)->toArray();
if (empty($feed)) {
throw new Exception(__('Invalid feed.'));
}
if ($outputStyle === 'table') {
$this->io->out(str_repeat('=', 114));
foreach ($feed as $field => $value) {
if (is_array($value)) {
$value = json_encode($value, JSON_PRETTY_PRINT);
}
$this->io->out(
sprintf(
'| %s | %s |',
str_pad($field, 20, ' ', STR_PAD_RIGHT),
str_pad($value ?? '', 87)
),
1,
ConsoleIo::NORMAL
);
}
$this->io->out(str_repeat('=', 114));
} else {
$this->outputJson($feed);
}
}
public function toggleFeed($feedId = null)
{
if (empty($feedId)) {
$this->showActionUsageAndExit();
}
$feed = $this->Feeds->get($feedId);
$feed['enabled'] = ($feed['enabled']) ? 0 : 1;
if ($this->Feeds->save($feed)) {
$this->io->out(__('Feed fetching {0} for feed {1}', ($feed['enabled'] ? __('enabled') : __('disabled')), $feed['id']));
} else {
$this->io->out(__('Could not toggle fetching for feed {0}', $feed['id']));
}
}
public function toggleFeedCaching($feedId = null)
{
if (empty($feedId)) {
$this->showActionUsageAndExit();
}
$feed = $this->Feeds->get($feedId);
$feed['caching_enabled'] = ($feed['caching_enabled']) ? 0 : 1;
if ($this->Feeds->save($feed)) {
$this->io->out(__('Feed caching {0} for feed {1}', ($feed['caching_enabled'] ? __('enabled') : __('disabled')), $feed['id']));
} else {
$this->io->out(__('Could not toggle caching for feed {0}', $feed['id']));
}
}
public function loadDefaultFeeds()
{
$this->Feeds->load_default_feeds();
$this->io->out(__('Default feed metadata loaded.'));
}
public function cacheFeed($userId = null, $scope = null, $jobId = null)
{
if (empty($userId) || empty($scope)) {
$this->showActionUsageAndExit();
}
$user = $this->getUser($userId);
$JobsTable = $this->fetchTable('Jobs');
if (!empty($jobId)) {
$jobId = $JobsTable->createJob($user, Job::WORKER_DEFAULT, 'cache_feeds', 'Feed: ' . $scope, 'Starting feed caching.');
}
try {
$result = $this->Feeds->cacheFeedInitiator($user, $jobId, $scope);
} catch (Exception $e) {
$this->logException("Failed caching Feed: $scope", $e);
$result = false;
}
if ($result === false) {
$message = __('Job failed. See error logs for more details.');
$JobsTable->saveStatus($jobId, false, $message);
} else {
$total = $result['successes'] + $result['fails'];
$message = __(
'{0} feed from {1} cached. Failed: {2}',
$result['successes'],
$total,
$result['fails']
);
if ($result['fails'] > 0) {
$message .= ' ' . __('See error logs for more details.');
}
$JobsTable->saveStatus($jobId, true, $message);
}
$this->io->out($message);
}
}

101
src/Command/MISPCommand.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace App\Command;
use App\Lib\Tools\BackgroundJobsTool;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Core\Configure;
class MISPCommand extends Command
{
/** @var ConsoleIo */
protected $io;
/** @var array */
protected $arguments = [];
/** @var string */
protected $action = '';
/** @var array */
protected $validActions = [];
/** @var array */
protected $usage = [];
/** @var BackgroundJobsTool */
private static $loadedBackgroundJobsTool;
public function execute(Arguments $args, ConsoleIo $io)
{
$this->io = $io;
$this->arguments = $args->getArguments();
$this->action = array_shift($this->arguments);
if (empty($this->action)) {
$this->showActionUsageAndExit();
}
if (!in_array($this->action, $this->validActions)) {
$this->invalidAction();
}
call_user_func([$this, $this->action], ...$this->arguments);
parent::execute($args, $io);
}
protected function outputJson($data)
{
$this->io->out(json_encode($data, JSON_PRETTY_PRINT));
}
protected function showActionUsageAndExit()
{
$this->io->error('Invalid usage.');
if (!empty($this->usage) && isset($this->usage[$this->action])) {
$this->io->info('Usage: ' . $this->usage[$this->action]);
}
die();
}
protected function invalidAction()
{
$this->io->warning('Invalid action.');
$this->io->out('Valid actions: ' . implode(', ', $this->validActions));
die();
}
/**
* @return BackgroundJobsTool
*/
public function getBackgroundJobsTool(): BackgroundJobsTool
{
if (!self::$loadedBackgroundJobsTool) {
self::$loadedBackgroundJobsTool = new BackgroundJobsTool(Configure::read('BackgroundJobs'));
;
}
return self::$loadedBackgroundJobsTool;
}
/**
* @param int $userId
* @return array
*/
protected function getUser($userId): array
{
$UsersTable = $this->fetchTable('Users');
$user = $UsersTable->getAuthUser($userId, true);
if (empty($user)) {
$this->io->error('User ID do not match an existing user.');
die();
}
return $user->toArray();
}
}

View File

@ -1,97 +0,0 @@
<?php
namespace App\Command;
use App\Model\Entity\Server;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Core\Configure;
use App\Model\Entity\Job;
class ServerCommand extends Command
{
use LocatorAwareTrait;
/** @var ConsoleIo */
private $io;
public function execute(Arguments $args, ConsoleIo $io)
{
$this->io = $io;
$arguments = $args->getArguments();
$action = array_shift($arguments);
switch ($action) {
case 'fetchFeed':
$this->fetchFeed(...$arguments);
break;
default:
$this->io->err('Invalid action.');
}
$this->io->out("Bye.");
}
public function fetchFeed($userId, $feedId, $jobId = null)
{
if (empty($userId) || empty($feedId)) {
$this->io->err('Usage: ' . (new Server())->command_line_functions['console_automation_tasks']['data']['Fetch feeds as local data'] . PHP_EOL);
die();
}
$UsersTable = $this->fetchTable('Users');
$user = $UsersTable->getAuthUser($userId, true);
Configure::write('CurrentUserId', $userId);
$FeedsTable = $this->fetchTable('Feeds');
$JobsTable = $this->fetchTable('Jobs');
if (!empty($jobId)) {
$jobId = $this->args[2];
} else {
$jobId = $JobsTable->createJob($user->toArray(), Job::WORKER_DEFAULT, 'fetch_feeds', 'Feed: ' . $feedId, 'Starting fetch from Feed.');
}
if ($feedId === 'all') {
$feedIds = $FeedsTable->find('column', array(
'fields' => array('id'),
'conditions' => array('enabled' => 1)
))->toArray();
$successes = 0;
$fails = 0;
foreach ($feedIds as $k => $feedId) {
$JobsTable->saveProgress($jobId, 'Fetching feed: ' . $feedId, 100 * $k / count($feedIds));
$result = $FeedsTable->downloadFromFeedInitiator($feedId, $user);
if ($result) {
$successes++;
} else {
$fails++;
}
}
$message = 'Job done. ' . $successes . ' feeds pulled successfully, ' . $fails . ' feeds could not be pulled.';
$JobsTable->saveStatus($jobId, true, $message);
$this->io->out($message);
} else {
$feedEnabled = $FeedsTable->exists([
'enabled' => 1,
'id' => $feedId,
]);
if ($feedEnabled) {
$result = $FeedsTable->downloadFromFeedInitiator($feedId, $user, $jobId);
if (!$result) {
$JobsTable->saveStatus($jobId, false, 'Job failed. See error log for more details.');
$this->io->err('Job failed.');
} else {
$JobsTable->saveStatus($jobId, true);
$this->io->out('Job done.');
}
} else {
$message = "Feed with ID $feedId not found or not enabled.";
$JobsTable->saveStatus($jobId, false, $message);
$this->io->err($message);
}
}
}
}

View File

@ -0,0 +1,371 @@
<?php
namespace App\Command;
use App\Lib\Tools\BackgroundJobsTool;
use App\Lib\Tools\HttpTool;
use App\Lib\Tools\LogExtendedTrait;
use App\Lib\Tools\ServerSyncTool;
use App\Model\Entity\Job;
use Cake\Chronos\Chronos;
use Exception;
class ServersCommand extends MISPCommand
{
use LogExtendedTrait;
protected $defaultTable = 'Servers';
/** @var \App\Model\Table\ServersTable */
protected $Servers;
protected $validActions = [
'fetchFeed',
'list',
'listServers',
'test',
'fetchIndex',
'pullAll',
'pull',
'push',
'pushAll',
'listFeeds',
'viewFeed',
'toggleFeed',
'toggleFeedCaching',
'loadDefaultFeeds',
'cacheServer',
'cacheServerAll',
'cacheFeed',
'sendPeriodicSummaryToUsers',
];
/** @var array */
protected $usage = [
'test' => 'bin/cake servers test `server_id`',
'fetchIndex' => 'bin/cake servers fetchIndex `server_id`',
'fetchFeed' => 'bin/cake servers `fetchFeed` `user_id` feed_id|all|csv|text|misp [job_id]',
'pullAll' => 'bin/cake servers pullAll `user_id` [full|update]',
'pull' => 'bin/cake servers pull `user_id` `server_id` [full|update]',
'push' => 'bin/cake servers push `user_id` `server_id` [full|update] [job_id]',
'pushAll' => 'bin/cake servers pushAll `user_id` [full|update]',
'listFeeds' => 'bin/cake servers listFeeds [json|table]',
'viewFeed' => 'bin/cake servers viewFeed `feed_id` [json|table]',
'toggleFeed' => 'bin/cake servers toggleFeed `feed_id`',
'toggleFeedCaching' => 'bin/cake servers toggleFeedCaching `feed_id`',
'cacheServer' => 'bin/cake servers cacheServer `user_id` `server_id|all` [job_id]',
'cacheServerAll' => 'bin/cake servers cacheServerAll `user_id` [job_id]',
'cacheFeed' => 'bin/cake servers cacheFeed `user_id` [feed_id|all|csv|text|misp] [job_id]',
];
public function list()
{
$servers = $this->Servers->find(
'all',
[
'fields' => ['id', 'name', 'url'],
'recursive' => 0
]
);
foreach ($servers as $server) {
$this->io->out(
sprintf(
'%sServer #%s :: %s :: %s',
PHP_EOL,
$server['id'],
$server['name'],
$server['url']
)
);
}
}
public function listServers()
{
$servers = $this->Servers->find(
'all',
[
'fields' => ['id', 'name', 'url'],
'recursive' => 0
]
)->toArray();
$res = ['servers' => $servers];
$this->outputJson($res);
}
public function test($serverId = null)
{
if (empty($serverId)) {
$this->showActionUsageAndExit();
}
$serverId = intval($serverId);
$server = $this->getServer($serverId);
$res = $this->Servers->runConnectionTest($server, false);
$this->outputJson($res);
}
public function fetchIndex($serverId = null)
{
if (empty($serverId)) {
$this->showActionUsageAndExit();
}
$server = $this->getServer($serverId);
$serverSync = new ServerSyncTool($server, $this->Servers->setupSyncRequest($server));
$index = $this->Servers->getEventIndexFromServer($serverSync);
$this->outputJson($index);
}
public function pullAll($userId = null, $technique = 'full')
{
if (empty($userId)) {
$this->showActionUsageAndExit();
}
$user = $this->getUser($userId);
$servers = $this->Servers->find(
'list',
[
'conditions' => ['pull' => 1],
'recursive' => -1,
'order' => 'priority',
'fields' => ['id', 'name'],
]
)->toArray();
foreach ($servers as $serverId => $serverName) {
$JobsTable = $this->fetchTable('Jobs');
$jobId = $JobsTable->createJob($user, Job::WORKER_DEFAULT, 'pull', "Server: $serverId", 'Pulling.');
$backgroundJobId = $this->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
[
'pull',
$user['id'],
$serverId,
$technique,
$jobId,
],
true,
$jobId
);
$this->io->out("Enqueued pulling from $serverName server as job $backgroundJobId");
}
}
public function pull($userId = null, $serverId = null, $technique = 'full', $jobId = null, $force = false)
{
if (empty($userId) || empty($serverId)) {
$this->showActionUsageAndExit();
}
$user = $this->getUser($userId);
$server = $this->getServer($serverId);
$JobsTable = $this->fetchTable('Jobs');
if (empty($jobId)) {
$jobId = $JobsTable->createJob($user, Job::WORKER_DEFAULT, 'pull', 'Server: ' . $serverId, 'Pulling.');
}
try {
$result = $this->Servers->pull($user, $technique, $server, $jobId, $force);
if (is_array($result)) {
$message = __('Pull completed. {0} events pulled, {1} events could not be pulled, {2} proposals pulled, {3} sightings pulled, {4} clusters pulled.', count($result[0]), count($result[1]), $result[2], $result[3], $result[4]);
$JobsTable->saveStatus($jobId, true, $message);
} else {
$message = __('ERROR: {0}', $result);
$JobsTable->saveStatus($jobId, false, $message);
}
} catch (Exception $e) {
$JobsTable->saveStatus($jobId, false, __('ERROR: {0}', $e->getMessage()));
throw $e;
}
$this->io->out($message);
}
public function push($userId = null, $serverId = null, $technique = 'full', $jobId = null)
{
if (empty($userId) || empty($serverId)) {
$this->showActionUsageAndExit();
}
$JobsTable = $this->fetchTable('Jobs');
$user = $this->getUser($userId);
$server = $this->getServer($serverId);
if (empty($jobId)) {
$jobId = $JobsTable->createJob($user, Job::WORKER_DEFAULT, 'push', 'Server: ' . $serverId, 'Pushing.');
}
$HttpSocket = new HttpTool();
$HttpSocket->configFromServer($server);
$result = $this->Servers->push($serverId, $technique, $jobId, $HttpSocket, $user);
if ($result !== true && !is_array($result)) {
$message = 'Job failed. Reason: ' . $result;
$JobsTable->saveStatus($jobId, false, $message);
} else {
$message = 'Job done.';
$JobsTable->saveStatus($jobId, true, $message);
}
}
public function pushAll($userId = null, $technique = 'full')
{
$user = $this->getUser($userId);
$servers = $this->Servers->find(
'list',
[
'conditions' => ['push' => 1],
'recursive' => -1,
'order' => 'priority',
'fields' => ['id', 'name'],
]
);
foreach ($servers as $serverId => $serverName) {
$jobId = $this->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
[
'push',
$user['id'],
$serverId,
$technique
]
);
$this->io->out("Enqueued pushing from $serverName server as job $jobId");
}
}
public function cacheServer($userId = null, $scope = null, $jobId = null)
{
if (empty($userId) || empty($scope)) {
$this->showActionUsageAndExit();
}
$JobsTable = $this->fetchTable('Jobs');
$user = $this->getUser($userId);
if (empty($jobId)) {
$data = [
'worker' => 'default',
'job_type' => 'cache_servers',
'job_input' => 'Server: ' . $scope,
'status' => 0,
'retries' => 0,
'org' => $user['Organisation']['name'],
'message' => 'Starting server caching.',
];
$job = $JobsTable->newEntity($data);
$JobsTable->save($job);
$jobId = $job->id;
}
$result = $this->Servers->cacheServerInitiator($user, $scope, $jobId);
if ($result !== true) {
$message = 'Job Failed. Reason: ' . $result;
$JobsTable->saveStatus($jobId, false, $message);
} else {
$message = 'Job done.';
$JobsTable->saveStatus($jobId, true, $message);
}
$this->io->out($message);
}
public function cacheServerAll($userId = null)
{
if (empty($userId)) {
$this->showActionUsageAndExit();
}
$user = $this->getUser($userId);
$servers = $this->Servers->find(
'list',
[
'conditions' => ['pull' => 1],
'recursive' => -1,
'order' => 'priority',
'fields' => ['id', 'name'],
]
);
foreach ($servers as $serverId => $serverName) {
$jobId = $this->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
[
'cacheServer',
$user['id'],
$serverId
]
);
$this->io->out("Enqueued cacheServer from $serverName server as job $jobId");
}
}
public function sendPeriodicSummaryToUsers()
{
$periods = $this->__getPeriodsForToday();
$start_time = time();
$this->io->out(__('Started periodic summary generation for the {0} period', 'Started periodic summary generation for periods: {1}', count($periods), implode(', ', $periods)));
$UsersTable = $this->fetchTable('Users');
foreach ($periods as $period) {
$users = $UsersTable->getSubscribedUsersForPeriod($period);
$this->io->out(__('{0} user has subscribed for the `{1}` period', '{2} users has subscribed for the `{3}` period', count($users), count($users), $period));
foreach ($users as $user) {
$this->io->out(__('Sending `{0}` report to `{1}`', $period, $user['email']));
$emailTemplate = $UsersTable->generatePeriodicSummary($user['id'], $period, false);
if ($emailTemplate === null) {
continue; // no new event for this user
}
$UsersTable->sendEmail($user, $emailTemplate, false, null);
}
}
$this->io->out(__('All reports sent. Task took {0} seconds', time() - $start_time));
}
private function __getPeriodsForToday(): array
{
$today = new Chronos();
$periods = ['daily'];
if ($today->format('j') == 1) {
$periods[] = 'monthly';
}
if ($today->format('N') == 1) {
$periods[] = 'weekly';
}
return $periods;
}
/**
* @param int $serverId
* @return array
*/
private function getServer($serverId): array
{
$server = $this->Servers->get($serverId);
if (!$server) {
$this->io->error("Server with ID $serverId doesn't exists.");
die();
}
return $server->toArray();
}
}

View File

@ -199,7 +199,7 @@ class AuditLogsController extends AppController
$this->paginate['conditions'] = $this->__createEventIndexConditions($event);
$this->set('passedArgsArray', ['eventId' => $eventId, 'org' => $org]);
$params = $this->IndexFilter->harvestParameters(['created', 'org']);
$params = $this->harvestParameters(['created', 'org']);
if ($org) {
$params['org'] = $org;
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
@ -55,6 +56,13 @@ class AppController extends Controller
*/
protected $AuditLogs = null;
private $__queryVersion = '0';
public $pyMispVersion = '3.0.0';
public $phpmin = '8.0';
public $phprec = '8.4';
public $phptoonew = null;
private $isApiAuthed = false;
/**
* Initialization hook method.
*
@ -317,9 +325,9 @@ class AppController extends Controller
* isSiteAdmin
* checks if the currently logged user is a site administrator (an admin that can manage any user or event on the instance and create / edit the roles).
*
* @return void
* @return bool
*/
protected function isSiteAdmin()
protected function isSiteAdmin(): bool
{
return $this->ACL->getUser()->Role->perm_site_admin;
}

View File

@ -293,6 +293,93 @@ class ACLComponent extends Component
'update' => [],
'possibleObjectTemplates' => ['*'],
],
'Feeds' => [
'add' => [],
'cacheFeeds' => [],
'compareFeeds' => ['host_org_user'],
'delete' => [],
'disable' => [],
'edit' => [],
'enable' => [],
'feedCoverage' => ['host_org_user'],
'fetchFromAllFeeds' => [],
'fetchFromFeed' => [],
'fetchSelectedFromFreetextIndex' => [],
'getEvent' => [],
'importFeeds' => [],
'index' => ['host_org_user'],
'loadDefaultFeeds' => [],
'previewEvent' => ['host_org_user'],
'previewIndex' => ['host_org_user'],
'searchCaches' => ['host_org_user'],
'toggleSelected' => [],
'view' => ['host_org_user'],
],
'Servers' => [
'add' => [],
'dbSchemaDiagnostic' => [],
'dbConfiguration' => [],
'cache' => [],
'changePriority' => [],
'checkout' => [],
'clearWorkerQueue' => [],
'createSync' => ['perm_sync'],
'delete' => [],
'deleteFile' => [],
'edit' => [],
'eventBlockRule' => [],
'fetchServersForSG' => ['perm_sharing_group'],
'filterEventIndex' => [],
'getAvailableSyncFilteringRules' => ['*'],
'getInstanceUUID' => ['perm_sync'],
'getPyMISPVersion' => ['*'],
'getRemoteUser' => [],
'getSetting' => [],
'getSubmodulesStatus' => [],
'getSubmoduleQuickUpdateForm' => [],
'getWorkers' => [],
'getVersion' => ['perm_auth'],
'idTranslator' => ['host_org_user'],
'import' => [],
'index' => [],
'ipUser' => ['perm_site_admin'],
'ondemandAction' => [],
'postTest' => ['*'],
'previewEvent' => [],
'previewIndex' => [],
'compareServers' => [],
'pull' => [],
'purgeSessions' => [],
'push' => [],
'queryAvailableSyncFilteringRules' => [],
'releaseUpdateLock' => [],
'resetRemoteAuthKey' => [],
'removeOrphanedCorrelations' => [],
'restartDeadWorkers' => [],
'restartWorkers' => [],
'serverSettings' => [],
'serverSettingsEdit' => [],
'serverSettingsReloadSetting' => [],
'startWorker' => [],
'startZeroMQServer' => [],
'statusZeroMQServer' => [],
'stopWorker' => [],
'stopZeroMQServer' => [],
'testConnection' => [],
'update' => [],
'updateJSON' => [],
'updateProgress' => [],
'updateSubmodule' => [],
'uploadFile' => [],
'killAllWorkers' => [],
'cspReport' => ['*'],
'pruneDuplicateUUIDs' => [],
'removeDuplicateEvents' => [],
'upgrade2324' => [],
'cleanModelCaches' => [],
'updateDatabase' => [],
'rest' => ['perm_auth'],
],
'Api' => [
'index' => ['*']
]

View File

@ -0,0 +1,80 @@
<?php
namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Http\Exception\BadRequestException;
use Cake\Event\EventInterface;
use Cake\Controller\Controller;
class CompressedRequestHandlerComponent extends Component
{
public function startup(EventInterface $event)
{
$controller = $this->getController();
$contentEncoding = $_SERVER['HTTP_CONTENT_ENCODING'] ?? null;
if ($contentEncoding === 'application/json') {
return;
}
if (!empty($contentEncoding)) {
if ($contentEncoding === 'br') {
$controller->request->setInput($this->decodeBrotliEncodedContent($controller));
} else if ($contentEncoding === 'gzip') {
$controller->request->setInput($this->decodeGzipEncodedContent($controller));
} else {
throw new BadRequestException("Unsupported content encoding '$contentEncoding'.");
}
}
}
/**
* @return array
*/
public function supportedEncodings()
{
$supportedEncodings = [];
if (function_exists('gzdecode')) {
$supportedEncodings[] = 'gzip';
}
if (function_exists('brotli_uncompress')) {
$supportedEncodings[] = 'br';
}
return $supportedEncodings;
}
/**
* @return string
* @throws Exception
*/
private function decodeGzipEncodedContent(Controller $controller)
{
if (function_exists('gzdecode')) {
$decoded = gzdecode($controller->request->input());
if ($decoded === false) {
throw new BadRequestException('Invalid compressed data.');
}
return $decoded;
} else {
throw new BadRequestException("This server doesn't support GZIP compressed requests.");
}
}
/**
* @param Controller $controller
* @return string
* @throws Exception
*/
private function decodeBrotliEncodedContent(Controller $controller)
{
if (function_exists('brotli_uncompress')) {
$decoded = brotli_uncompress($controller->request->input());
if ($decoded === false) {
throw new BadRequestException('Invalid compressed data.');
}
return $decoded;
} else {
throw new BadRequestException("This server doesn't support brotli compressed requests.");
}
}
}

View File

@ -14,13 +14,10 @@ use Cake\Core\Configure;
use Cake\Event\EventInterface;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
use Cake\ORM\Locator\LocatorAwareTrait;
use Exception;
class FeedsController extends AppController
{
use LocatorAwareTrait;
public $paginate = [
'limit' => 60,
'order' => [
@ -525,7 +522,7 @@ class FeedsController extends AppController
$this->Feeds->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
BackgroundJobsTool::CMD_FEEDS,
[
'fetchFeed',
$this->ACL->getUser()->id,
@ -596,7 +593,7 @@ class FeedsController extends AppController
$this->Feeds->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
BackgroundJobsTool::CMD_FEEDS,
[
'fetchFeed',
$this->Auth->user('id'),
@ -972,7 +969,7 @@ class FeedsController extends AppController
$this->Feeds->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::DEFAULT_QUEUE,
BackgroundJobsTool::CMD_SERVER,
BackgroundJobsTool::CMD_FEEDS,
[
'cacheFeed',
$this->Auth->user('id'),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Exception;
use Exception;
use Cake\Http\Client\Response;
use Throwable;
class HttpSocketJsonException extends Exception
{
/** @var HttpSocketResponseExtended */
private $response;
public function __construct($message, Response $response, Throwable $previous = null)
{
$this->response = $response;
parent::__construct($message, 0, $previous);
}
/**
* @return HttpSocketResponseExtended
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Lib\Tools;
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
class AWSS3Client
{
private $__settings = false;
private $__client = false;
private function __getSetSettings()
{
$settings = array(
'enable' => false,
'bucket_name' => 'my-malware-bucket',
'region' => 'eu-west-1',
'aws_access_key' => '',
'aws_secret_key' => '',
'aws_endpoint' => '',
'aws_compatible' => false,
'aws_ca' => '',
'aws_validate_ca' => true
);
// We have 2 situations
// Either we're running on EC2 and we can assume an IAM role
// Or we're not and need explicitly set AWS key
if (strlen($settings['aws_access_key']) > 0) {
putenv('AWS_ACCESS_KEY_ID=' . $settings['aws_access_key']);
}
if (strlen($settings['aws_secret_key']) > 0) {
putenv('AWS_SECRET_ACCESS_KEY=' . $settings['aws_secret_key']);
}
foreach ($settings as $key => $setting) {
$temp = Configure::read('Plugin.S3_' . $key);
if ($temp) {
$settings[$key] = $temp;
}
}
return $settings;
}
public function initTool()
{
$settings = $this->__getSetSettings();
$s3Config = array(
'version' => 'latest',
'region' => $settings['region'],
);
if ($settings['aws_compatible']) {
$s3Config = array(
'version' => 'latest',
'region' => $settings['region'],
// MinIO compatibility
// Reference: https://docs.min.io/docs/how-to-use-aws-sdk-for-php-with-minio-server.html
'endpoint' => $settings['aws_endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $settings['aws_access_key'],
'secret' => $settings['aws_secret_key'],
],
);
}
// This line should points to server certificate
// Generically, this verify is set to false so that any certificate is valid
// Reference:
// - https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_configuration.html
// - https://docs.guzzlephp.org/en/5.3/clients.html#verify
// Example:
// -- Verify certificate
// 'http' => ['verify' => '/usr/lib/ssl/certs/minio.pem'],
// -- Do not verify certificate, securitywise, this option is not recommended, however due to
// internal deployment scheme it is acceptable risk to set this to false
// 'http' => ['verify' => false],
// -- Verify againts built in CA certificates
// 'http' => ['verify' => true],
if ($settings['aws_validate_ca']) {
$s3Config['http']['verify'] = true;
if (!empty($settings['aws_ca'])) {
$s3Config['http']['verify'] = $settings['aws_ca'];
}
} else {
$s3Config['http']['verify'] = false;
}
$s3Client = new S3Client($s3Config);
$this->__client = $s3Client;
$this->__settings = $settings;
return $s3Client;
}
public function exist($key)
{
return $this->__client->doesObjectExist([
'Bucket' => $this->__settings['bucket_name'],
'Key' => $key,
]);
}
public function upload($key, $data)
{
$this->__client->putObject([
'Bucket' => $this->__settings['bucket_name'],
'Key' => $key,
'Body' => $data
]);
}
public function download($key)
{
try {
$result = $this->__client->getObject([
'Bucket' => $this->__settings['bucket_name'],
'Key' => $key
]);
return $result['Body'];
} catch (AwsException $e) {
throw new NotFoundException('Could not download object ' . $e->getMessage());
}
}
public function delete($key)
{
$this->__client->deleteObject([
'Bucket' => $this->__settings['bucket_name'],
'Key' => $key
]);
}
public function deleteDirectory($prefix)
{
$keys = $this->__client->listObjectsV2([
'Bucket' => $this->__settings['bucket_name'],
'Prefix' => $prefix
]);
$toDelete = array_map(
function ($key) {
return ['Key' => $key['Key']];
},
is_array($keys['Contents']) ? $keys['Contents'] : []
);
if (sizeof($toDelete) != 0) {
$this->__client->deleteObjects([
'Bucket' => $this->__settings['bucket_name'],
'Delete' => [
'Objects' => $toDelete
]
]);
}
}
}

View File

@ -0,0 +1,514 @@
<?php
declare(strict_types=1);
namespace App\Lib\Tools;
use Cake\Http\Exception\NotFoundException;
use Exception;
use InvalidArgumentException;
use SplFileInfo;
use Cake\Core\Configure;
class AttachmentTool
{
const ZIP_PASSWORD = 'infected';
const ADVANCED_EXTRACTION_SCRIPT_PATH = APP . 'files/scripts/generate_file_objects.py';
/** @var AWSS3Client */
private $s3client;
/**
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return bool
* @throws Exception
*/
public function exists($eventId, $attributeId, $path_suffix = '')
{
return $this->_exists(false, $eventId, $attributeId, $path_suffix);
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return bool
* @throws Exception
*/
public function shadowExists($eventId, $attributeId, $path_suffix = '')
{
return $this->_exists(true, $eventId, $attributeId, $path_suffix);
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return bool
* @throws Exception
*/
protected function _exists($shadow, $eventId, $attributeId, $path_suffix = '')
{
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$path = $this->getPath($shadow, $eventId, $attributeId, $path_suffix);
return $s3->exist($path);
} else {
try {
$this->_getFile($shadow, $eventId, $attributeId, $path_suffix);
} catch (NotFoundException $e) {
return false;
}
}
return true;
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return string
* @throws Exception
*/
public function getContent($eventId, $attributeId, $path_suffix = '')
{
return $this->_getContent(false, $eventId, $attributeId, $path_suffix);
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return string
* @throws Exception
*/
public function getShadowContent($eventId, $attributeId, $path_suffix = '')
{
return $this->_getContent(true, $eventId, $attributeId, $path_suffix);
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $path_suffix
* @return string
* @throws Exception
*/
protected function _getContent($shadow, $eventId, $attributeId, $path_suffix = '')
{
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$path = $this->getPath($shadow, $eventId, $attributeId, $path_suffix);
return $s3->download($path);
} else {
$file = $this->_getFile($shadow, $eventId, $attributeId, $path_suffix);
$result = $file->read();
if ($result === false) {
throw new Exception("Could not read file '{$file->path}'.");
}
return $result;
}
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return File
* @throws Exception
*/
public function getFile($eventId, $attributeId, $pathSuffix = '')
{
return $this->_getFile(false, $eventId, $attributeId, $pathSuffix);
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return File
* @throws Exception
*/
public function getShadowFile($eventId, $attributeId, $pathSuffix = '')
{
return $this->_getFile(true, $eventId, $attributeId, $pathSuffix);
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return File
* @throws Exception
*/
protected function _getFile($shadow, $eventId, $attributeId, $pathSuffix = '')
{
$path = $this->getPath($shadow, $eventId, $attributeId, $pathSuffix);
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$content = $s3->download($path);
$file = FileAccessTool::writeToTempFile($content);
} else {
$filepath = $this->attachmentDir() . DS . $path;
$file = FileAccessTool::createFile($filepath);
}
return $file;
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $data
* @param string $pathSuffix
* @return bool
* @throws Exception
*/
public function save($eventId, $attributeId, $data, $pathSuffix = '')
{
return $this->_save(false, $eventId, $attributeId, $data, $pathSuffix);
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $data
* @param string $pathSuffix
* @return bool
* @throws Exception
*/
public function saveShadow($eventId, $attributeId, $data, $pathSuffix = '')
{
return $this->_save(true, $eventId, $attributeId, $data, $pathSuffix);
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $data
* @param string $pathSuffix
* @return bool
* @throws Exception
*/
protected function _save($shadow, $eventId, $attributeId, $data, $pathSuffix = '')
{
$path = $this->getPath($shadow, $eventId, $attributeId, $pathSuffix);
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$s3->upload($path, $data);
} else {
$path = $this->attachmentDir() . DS . $path;
FileAccessTool::writeToFile($path, $data, true);
}
return true;
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return bool
* @throws Exception
*/
public function delete($eventId, $attributeId, $pathSuffix = '')
{
return $this->_delete(false, $eventId, $attributeId, $pathSuffix);
}
/**
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return bool
* @throws Exception
*/
public function deleteShadow($eventId, $attributeId, $pathSuffix = '')
{
return $this->_delete(true, $eventId, $attributeId, $pathSuffix);
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return bool Return true if file was deleted, `false` if file doesn't exists.
* @throws Exception
*/
protected function _delete($shadow, $eventId, $attributeId, $pathSuffix = '')
{
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$path = $this->getPath($shadow, $eventId, $attributeId, $pathSuffix);
$s3->delete($path);
} else {
try {
$file = $this->_getFile($shadow, $eventId, $attributeId, $pathSuffix);
} catch (NotFoundException $e) {
return false;
}
if (!$file->delete()) {
throw new Exception(__('Delete of file attachment failed. Please report to administrator.'));
}
}
return true;
}
/**
* Deletes all attributes and shadow attributes files.
*
* @param int $eventId
* @return bool
* @throws Exception
*/
public function deleteAll($eventId)
{
if ($this->attachmentDirIsS3()) {
$s3 = $this->loadS3Client();
$s3->deleteDirectory($eventId);
} else {
$dirPath = $this->attachmentDir();
foreach (array($dirPath, $dirPath . DS . 'shadow') as $dirPath) {
$folder = new SplFileInfo($dirPath . DS . $eventId);
if (!FileAccessTool::deleteFileIfExists($folder->getPath())) {
throw new Exception("Delete of directory '{$folder->getPath()()}' failed.");
}
}
}
return true;
}
/**
* It is not possible to use PHP extensions for compressing. The reason is, that extensions support just AES encrypted
* files, but these files are not supported in Windows and in Python. So the only solution is to use 'zip' command.
*
* @param string $originalFilename
* @param string $content
* @param string $md5
* @return string Content of zipped file
* @throws Exception
*/
public function encrypt($originalFilename, $content, $md5)
{
$tempDir = $this->tempDir();
FileAccessTool::writeToFile($tempDir . DS . $md5, $content);
FileAccessTool::writeToFile($tempDir . DS . $md5 . '.filename.txt', $originalFilename);
$zipFile = $tempDir . DS . $md5 . '.zip';
$exec = [
'zip',
'-j', // junk (don't record) directory names
'-P', // use standard encryption
self::ZIP_PASSWORD,
$zipFile,
$tempDir . DS . $md5,
$tempDir . DS . $md5 . '.filename.txt',
];
try {
ProcessTool::execute($exec);
return FileAccessTool::readFromFile($zipFile);
} catch (Exception $e) {
throw new Exception("Could not create encrypted ZIP file '$zipFile'.", 0, $e);
} finally {
FileAccessTool::deleteFile($tempDir . DS . $md5);
FileAccessTool::deleteFile($tempDir . DS . $md5 . '.filename.txt');
FileAccessTool::deleteFile($zipFile);
}
}
/**
* @param string $content
* @param array $hashTypes
* @return array
* @throws InvalidArgumentException
*/
public function computeHashes($content, array $hashTypes = array())
{
$validHashes = array('md5', 'sha1', 'sha256');
$hashes = [];
foreach ($hashTypes as $hashType) {
if (!in_array($hashType, $validHashes)) {
throw new InvalidArgumentException("Hash type '$hashType' is not valid hash type.");
}
$hashes[$hashType] = hash($hashType, $content);
}
return $hashes;
}
/**
* @param string $pythonBin
* @param string $filePath
* @return array
* @throws Exception
*/
public function advancedExtraction($filePath)
{
return $this->executeAndParseJsonOutput([
ProcessTool::pythonBin(),
self::ADVANCED_EXTRACTION_SCRIPT_PATH,
'-p',
$filePath,
]);
}
/**
* @param string $pythonBin
* @return array
* @throws Exception
*/
public function checkAdvancedExtractionStatus()
{
return $this->executeAndParseJsonOutput([ProcessTool::pythonBin(), self::ADVANCED_EXTRACTION_SCRIPT_PATH, '-c']);
}
/**
* @param string $data
* @param int $maxWidth
* @param int $maxHeight
* @param string $outputFormat Can be 'png' or 'webp'
* @return string
* @throws Exception
*/
public function resizeImage($data, $maxWidth, $maxHeight, $outputFormat = 'png')
{
$image = imagecreatefromstring($data);
if ($image === false) {
throw new Exception("Image is not valid.");
}
$currentWidth = imagesx($image);
$currentHeight = imagesy($image);
// Compute thumbnail size with keeping ratio
if ($currentWidth > $currentHeight) {
$newWidth = min($currentWidth, $maxWidth);
$divisor = $currentWidth / $newWidth;
$newHeight = floor($currentHeight / $divisor);
} else {
$newHeight = min($currentHeight, $maxHeight);
$divisor = $currentHeight / $newHeight;
$newWidth = floor($currentWidth / $divisor);
}
$imageThumbnail = imagecreatetruecolor($newWidth, $newHeight);
// Allow transparent background
imagealphablending($imageThumbnail, false);
imagesavealpha($imageThumbnail, true);
$transparent = imagecolorallocatealpha($imageThumbnail, 255, 255, 255, 127);
imagefilledrectangle($imageThumbnail, 0, 0, $newWidth, $newHeight, $transparent);
// Resize image
imagecopyresampled($imageThumbnail, $image, 0, 0, 0, 0, $newWidth, $newHeight, $currentWidth, $currentHeight);
imagedestroy($image);
// Output image to string
ob_start();
if ($outputFormat === 'webp') {
if (!function_exists('imagewebp')) {
throw new InvalidArgumentException("Webp image format is not supported.");
}
imagewebp($imageThumbnail);
} elseif ($outputFormat === 'png') {
imagepng($imageThumbnail, null, 9);
} else {
throw new InvalidArgumentException("Unsupported image format $outputFormat.");
}
$imageData = ob_get_clean();
imagedestroy($imageThumbnail);
return $imageData;
}
private function tempFileName()
{
$randomName = (new RandomTool())->random_str(false, 12);
return $this->tempDir() . DS . $randomName;
}
/**
* @return string
*/
private function tempDir()
{
return Configure::read('MISP.tmpdir') ?: sys_get_temp_dir();
}
/**
* @return string
*/
private function attachmentDir()
{
return Configure::read('MISP.attachments_dir') ?: (APP . 'files');
}
/**
* Naive way to detect if we're working in S3
* @return bool
*/
public function attachmentDirIsS3()
{
$attachmentsDir = Configure::read('MISP.attachments_dir');
return $attachmentsDir && substr($attachmentsDir, 0, 2) === "s3";
}
/**
* @return AWSS3Client
*/
private function loadS3Client()
{
if ($this->s3client) {
return $this->s3client;
}
$client = new AWSS3Client();
$client->initTool();
$this->s3client = $client;
return $client;
}
/**
* @param bool $shadow
* @param int $eventId
* @param int $attributeId
* @param string $pathSuffix
* @return string
*/
private function getPath($shadow, $eventId, $attributeId, $pathSuffix)
{
$path = $shadow ? ('shadow' . DS) : '';
return $path . $eventId . DS . $attributeId . $pathSuffix;
}
/**
* @param array $command
* @return array
* @throws Exception
*/
private function executeAndParseJsonOutput(array $command)
{
$output = ProcessTool::execute($command);
try {
return JsonTool::decode($output);
} catch (Exception $e) {
throw new Exception("Command output is not valid JSON.", 0, $e);
}
}
}

View File

@ -72,13 +72,15 @@ class BackgroundJobsTool
public const
CMD_EVENT = 'event',
CMD_SERVER = 'server',
CMD_SERVER = 'servers',
CMD_FEEDS = 'feeds',
CMD_ADMIN = 'admin',
CMD_WORKFLOW = 'workflow';
public const ALLOWED_COMMANDS = [
self::CMD_EVENT,
self::CMD_SERVER,
self::CMD_FEEDS,
self::CMD_ADMIN,
self::CMD_WORKFLOW
];
@ -385,7 +387,7 @@ class BackgroundJobsTool
foreach ($procs as $proc) {
if ($proc->offsetGet('group') === self::MISP_WORKERS_PROCESS_GROUP) {
$name = explode("_", $proc->offsetGet('name'))[0];
if ($name === $queue && $proc->offsetGet('state') != \Supervisor\Process::RUNNING) {
if ($name === $queue && $proc->offsetGet('state') != \Supervisor\ProcessStates::Running) {
return $this->getSupervisor()->startProcess(
sprintf(
'%s:%s',
@ -512,7 +514,7 @@ class BackgroundJobsTool
*/
public function getSupervisorStatus(): bool
{
return $this->getSupervisor()->getState()['statecode'] === \Supervisor\Supervisor::RUNNING;
return $this->getSupervisor()->getState()['statecode'] === \Supervisor\ProcessStates::Running;
}
/**
@ -662,12 +664,6 @@ class BackgroundJobsTool
)
);
if (class_exists('Supervisor\Connector\XmlRpc')) {
// for compatibility with older versions of supervisor
$connector = new \Supervisor\Connector\XmlRpc($client);
return new \Supervisor\Supervisor($connector);
}
return new \Supervisor\Supervisor($client);
}
@ -725,9 +721,9 @@ class BackgroundJobsTool
private function convertProcessStatus(int $stateId): int
{
switch ($stateId) {
case \Supervisor\Process::RUNNING:
case \Supervisor\ProcessStates::Running:
return Worker::STATUS_RUNNING;
case \Supervisor\Process::UNKNOWN:
case \Supervisor\ProcessStates::Unknown:
return Worker::STATUS_UNKNOWN;
default:
return Worker::STATUS_FAILED;
@ -750,7 +746,7 @@ class BackgroundJobsTool
}
}
static function getInstance()
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new BackgroundJobsTool(Configure::read('BackgroundJobs'));

View File

@ -29,12 +29,14 @@ class EncryptedValue implements JsonSerializable
* @throws JsonException
* @throws Exception
*/
public function decrypt($key=false)
public function decrypt($key = false)
{
if (!$key) {
$key = Configure::read('Security.encryption_key');
}
if (!$key) return '';
if (!$key) {
return '';
}
$decrypt = BetterSecurity::decrypt(substr($this->value, 2), $key);
return $this->isJson ? JsonTool::decode($decrypt) : $decrypt;
}
@ -55,7 +57,7 @@ class EncryptedValue implements JsonSerializable
* @return string
* @throws Exception
*/
public static function encryptIfEnabled($value, $key=false)
public static function encryptIfEnabled($value, $key = false)
{
if (!$key) {
$key = Configure::read('Security.encryption_key');
@ -72,15 +74,16 @@ class EncryptedValue implements JsonSerializable
* @return string
* @throws Exception
*/
public static function decryptIfEncrypted($value, $key=false)
public static function decryptIfEncrypted($value, $key = false)
{
if(is_resource($value))
if (is_resource($value)) {
$value = stream_get_contents($value);
}
if (EncryptedValue::isEncrypted($value)) {
$self = new EncryptedValue($value);
return $self->decrypt($key);
return $self->decrypt($key);
} else {
return $value;
return trim($value, "\x00");
}
}

View File

@ -13,6 +13,8 @@ use Cake\I18n\FrozenTime;
class HttpTool extends CakeClient
{
public const ALLOWED_CERT_FILE_EXTENSIONS = ['pem', 'crt'];
/**
* Create a new MISP specific HTTP Client
* {@inheritdoc} In addition brings some MISP specifics to the game.
@ -406,4 +408,16 @@ class HttpTool extends CakeClient
return $output;
}
/**
* @return array|null
*/
public function getMetaData()
{
// TODO: [3.x-MIGRATION]
// if ($this->connection) {
// return stream_get_meta_data($this->connection);
// }
return null;
}
}

View File

@ -0,0 +1,600 @@
<?php
namespace App\Lib\Tools;
use App\Model\Entity\Server;
use Cake\Core\Configure;
use Cake\Datasource\ConnectionManager;
use RuntimeException;
use App\Http\Exception\HttpSocketHttpException;
use Exception;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Mailer\Mailer as CakeEmail;
use Cake\Chronos\Chronos;
use Cake\Chronos\ChronosInterface;
class SecurityAudit
{
const STRONG_PASSWORD_LENGTH = 17;
use LocatorAwareTrait;
/**
* @param Server $server
* @param bool $systemOnly Run only system checks
* @return array
*/
public function run(Server $server, $systemOnly = false)
{
$output = [];
foreach (['config.php', 'config.php.bk', 'database.php', 'email.php'] as $configFile) {
if (!file_exists(CONFIG . $configFile)) {
continue;
}
$perms = fileperms(CONFIG . $configFile);
if ($perms & 0x0004) {
$output['File permissions'][] = ['error', __('%s config file is readable for any user.', $configFile)];
}
}
$redisPassword = Configure::read('MISP.redis_password');
if (empty($redisPassword)) {
$output['Redis'][] = ['error', __('Redis password not set.')];
} else if (strlen($redisPassword) < 32) { // for Redis, password should be stronger
$output['Redis'][] = [
'warning',
__('Redis password is too short, should be at least 32 chars long.'),
'https://redis.io/topics/security#authentication-feature',
];
}
$databasePassword = ConnectionManager::get('default')->config['password'];
if (empty($databasePassword)) {
$output['Database'][] = ['error', __('Database password not set.')];
} else if (strlen($databasePassword) < self::STRONG_PASSWORD_LENGTH) {
$output['Database'][] = ['warning', __('Database password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
}
if (!Configure::read('Security.encryption_key')) {
$output['Database'][] = ['warning', __('Sensitive information like keys to remote server are stored in database unencrypted. Set `Security.encryption_key` to encrypt these values.')];
}
$passwordPolicyLength = Configure::read('Security.password_policy_length') ?: $server->serverSettings['Security']['password_policy_length']['value'];
if ($passwordPolicyLength < 8) {
$output['Password'][] = ['error', __('Minimum password length is set to %s, it is highly advised to increase it.', $passwordPolicyLength)];
} elseif ($passwordPolicyLength < 12) {
$output['Password'][] = ['warning', __('Minimum password length is set to %s, consider raising to at least 12 characters.', $passwordPolicyLength)];
}
if (empty(Configure::read('Security.require_password_confirmation'))) {
$output['Password'][] = [
'warning',
__('Password confirmation is not enabled. %s', $server->serverSettings['Security']['require_password_confirmation']['description']),
];
}
if (!empty(Configure::read('Security.auth')) && !Configure::read('Security.auth_enforced')) {
$output['Login'][] = [
'hint',
__('External authentication is enabled, but local accounts will still work. You can disable the ability to log in via local accounts by setting `Security.auth_enforced` to `true`.'),
];
}
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',
__('Browser cache is enabled. An attacker could obtain sensitive data from the user cache. You can disable the cache by setting `Security.disable_browser_cache` to `true`.'),
];
}
if (empty(Configure::read('Security.check_sec_fetch_site_header'))) {
$output['Browser'][] = [
'warning',
__('The MISP server is not checking `Sec-Fetch` HTTP headers. This is a protection mechanism against CSRF used by modern browsers. You can enable this check by setting `Security.check_sec_fetch_site_header` to `true`.'),
];
}
if (empty(Configure::read('Security.csp_enforce'))) {
$output['Browser'][] = [
'warning',
__('Content security policies (CSP) are not enforced. Consider enabling them by setting `Security.csp_enforce` to `true`.'),
'https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP',
];
}
if (!env('HTTPS') && strpos(Configure::read('MISP.baseurl'), 'https://') === 0) {
$output['Browser'][] = [
'error',
__('MISP base URL is set to https://, but MISP thinks that the connection is insecure. This usually happens when a server is running behind a reverse proxy. By setting `Security.force_https` to `true`, session cookies will be set as Secure and CSP headers will upgrade insecure requests.'),
];
}
$sessionConfig = Configure::read('Session');
if (isset($sessionConfig['ini']['session.cookie_secure']) && !$sessionConfig['ini']['session.cookie_secure']) {
$output['Browser'][] = ['error', __('Setting session cookies as not secure is never a good idea.')];
}
if (empty(Configure::read('Security.advanced_authkeys'))) {
$output['Auth Key'][] = ['warning', __('Consider enabling Advanced Auth Keys, they provide increased security by only storing the API key hashes.')];
}
if (Configure::read('Security.allow_unsafe_apikey_named_param')) {
$output['Auth Key'][] = ['error', __('It is possible to pass API keys via the URL, meaning that the keys can be logged by proxies.')];
}
if (empty(Configure::read('Security.do_not_log_authkeys'))) {
$output['Auth Key'][] = ['warning', __('Auth Key logging is not disabled. Auth Keys in cleartext can be visible in the Audit log.')];
}
$salt = Configure::read('Security.salt');
if (empty($salt)) {
$output['Security salt'][] = ['error', __('Salt is not set.')];
} else if (strlen($salt) < 32) {
$output['Security salt'][] = ['warning', __('Salt is too short, should contain at least 32 characters.')];
} else if ($salt === "Rooraenietu8Eeyo<Qu2eeNfterd-dd+") {
$output['Security salt'][] = ['error', __('Salt is set to the default value.')];
}
if (empty(Configure::read('MISP.log_client_ip'))) {
$output['Logging'][] = ['warning', __('Logging client IP in audit log is disabled. Logging IP address can help to solve potential security breaches.')];
}
if (empty(Configure::read('MISP.log_user_ips'))) {
$output['Logging'][] = ['warning', __('Logging client IP in Redis is disabled. Logging IP addresses can help investigate potential security breaches.')];
}
if (Configure::read('MISP.log_user_ips') && Configure::read('Security.advanced_authkeys') && empty(Configure::read('MISP.log_user_ips_authkeys'))) {
$output['Logging'][] = [
'hint',
__('You can enable the logging of advanced authkeys by setting `MISP.log_user_ips_authkeys` to `true`.'),
];
}
if (empty(Configure::read('Security.username_in_response_header'))) {
$output['Logging'][] = [
'hint',
__('Passing user information to response headers is disabled. This can be useful for logging user info at the reverse proxy level. You can enable it by setting `Security.username_in_response_header` to `true`.'),
];
}
if (!Configure::read('MISP.log_new_audit')) {
$output['Logging'][] = [
'hint',
__('New audit log stores more information, like used authkey ID or request ID that can help when analysing or correlating audit logs. Set `MISP.log_new_audit` to `true` to enable.'),
];
}
if (empty(Configure::read('MISP.attachment_scan_module'))) {
$output['Attachment scanning'][] = ['hint', __('No module for scanning attachments for viruses is currently defined.')];
}
if (Configure::read('debug')) {
$output['Debug'][] = ['error', __('Debug mode is enabled for all users.')];
}
if (Configure::read('Proxy.host')) {
$proxyPassword = Configure::read('Proxy.password');
if (empty($proxyPassword)) {
$output['Proxy'][] = ['error', __('Proxy password is empty.')];
} else if (strlen($proxyPassword) < self::STRONG_PASSWORD_LENGTH) {
$output['Proxy'][] = ['warning', __('Proxy password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
}
}
if (Configure::read('Security.rest_client_enable_arbitrary_urls')) {
$output['REST client'][] = [
'hint',
__('Users can use the REST client to query any remote URL. This is generally not a good idea if your instance is public.')
];
}
if (Configure::read('Plugins.ZeroMQ_enable')) {
$zeroMqPassword = Configure::read('Plugins.ZeroMQ_password');
if (empty($zeroMqPassword)) {
$output['ZeroMQ'][] = ['error', __('ZeroMQ password is not set.')];
} else if (strlen($zeroMqPassword) < self::STRONG_PASSWORD_LENGTH) {
$output['ZeroMQ'][] = ['warning', __('ZeroMQ password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
}
$redisPassword = Configure::read('Plugins.ZeroMQ_redis_password');
if (empty($redisPassword)) {
$output['ZeroMQ'][] = ['error', __('Redis password is not set.')];
} else if (strlen($redisPassword) < 32) { // for Redis, password should be stronger
$output['ZeroMQ'][] = [
'warning',
__('Redis password is too short, should be at least 32 chars long.'),
'https://redis.io/topics/security#authentication-feature',
];
}
}
$this->email($output);
/*
* These settings are dangerous and break both the transparency and potential introduce sync issues
if (!Configure::read('Security.hide_organisation_index_from_users')) {
$output['MISP'][] = [
'hint',
__('Any user can see list of all organisations. You can disable that by setting `Security.hide_organisation_index_from_users` to `true`. %s', $server->serverSettings['Security']['hide_organisation_index_from_users']['description']),
];
}
if (!Configure::read('Security.hide_organisations_in_sharing_groups')) {
$output['MISP'][] = [
'hint',
__('Any user can see list of all organisations in sharing group that user can see. You can disable that by setting `Security.hide_organisations_in_sharing_groups` to `true`. %s', $server->serverSettings['Security']['hide_organisations_in_sharing_groups']['description']),
];
}
*/
if (!$systemOnly) {
$this->feeds($output);
$this->remoteServers($output);
}
try {
$cakeVersion = $this->getCakeVersion();
if (version_compare($cakeVersion, '2.10.21', '<')) {
$output['Dependencies'][] = ['warning', __('CakePHP version %s is outdated.', $cakeVersion)];
}
} catch (RuntimeException $e) {
}
if (version_compare(PHP_VERSION, '7.4.0', '<')) {
$output['PHP'][] = [
'warning',
__('PHP version %s is not supported anymore. It can be still supported by your distribution.', PHP_VERSION),
'https://www.php.net/supported-versions.php'
];
}
if (ini_get('expose_php')) {
$output['PHP'][] = [
'hint',
__('PHP `expose_php` setting is enabled. That means that PHP version will be send in `X-Powered-By` header. This can help attackers.'),
];
}
if (extension_loaded('xdebug')) {
$output['PHP'][] = [
'error',
__('The xdebug extension can reveal code and data to an attacker.'),
];
}
if (ini_get('session.use_strict_mode') != 1) {
$output['PHP'][] = [
'warning',
__('Session strict mode is disabled.'),
'https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode',
];
}
if (empty(ini_get('session.cookie_httponly'))) {
$output['PHP'][] = ['error', __('Session cookie is not set as HTTP only. Session cookie can be accessed from JavaScript.')];
}
if (!in_array(strtolower(ini_get('session.cookie_samesite')), ['strict', 'lax'])) {
$output['PHP'][] = [
'error',
__('Session cookie SameSite parameter is not defined or set to None.'),
'https://developer.mozilla.org/en-us/docs/Web/HTTP/Headers/Set-Cookie/SameSite',
];
}
$sidLength = ini_get('session.sid_length');
if ($sidLength !== false && $sidLength < 32) {
$output['PHP'][] = [
'warning',
__('Session ID length is set to %s, at least 32 is recommended.', $sidLength),
'https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length',
];
}
$sidBits = ini_get('session.sid_bits_per_character');
if ($sidBits !== false && $sidBits <= 4) {
$output['PHP'][] = [
'warning',
__('Session ID bit per character is set to %s, at least 5 is recommended.', $sidBits),
'https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character',
];
}
$this->system($output);
return $output;
}
/**
* @return array|string[][]
* @throws Exception
*/
public function tlsConnections()
{
$urls = [
'TLSv1.0' => ['url' => 'https://tls-v1-0.badssl.com:1010/'],
'TLSv1.1' => ['url' => 'https://tls-v1-1.badssl.com:1011/'],
'TLSv1.2' => ['url' => 'https://tls-v1-2.badssl.com:1012/', 'expected' => true],
'TLSv1.3' => [
'url' => 'https://check-tls.akamai.io/v1/tlsinfo.json',
'expected' => true,
'process' => function (HttpSocketHttpException $response) {
return $response->getResponse()->getJson()['tls_version'] === 'tls1.3';
}
],
'DH480' => ['url' => 'https://dh480.badssl.com/', 'expected' => false],
'DH512' => ['url' => 'https://dh512.badssl.com/', 'expected' => false],
'DH1024' => ['url' => 'https://dh1024.badssl.com/', 'expected' => false],
'DH2048' => ['url' => 'https://dh2048.badssl.com/'],
'RC4-MD5' => ['url' => 'https://rc4-md5.badssl.com/', 'expected' => false],
'RC4' => ['url' => 'https://rc4.badssl.com/', 'expected' => false],
'3DES' => ['url' => 'https://3des.badssl.com/', 'expected' => false],
'NULL' => ['url' => 'https://null.badssl.com/', 'expected' => false],
'SHA1 2016' => ['url' => 'https://sha1-2016.badssl.com/', 'expected' => false],
'SHA1 2017' => ['url' => 'https://sha1-2017.badssl.com/', 'expected' => false],
'SHA1 intermediate' => ['url' => 'https://sha1-intermediate.badssl.com/', 'expected' => false],
'Invalid expected sct' => ['url' => 'https://invalid-expected-sct.badssl.com/', 'expected' => false],
'Expired' => ['url' => 'https://expired.badssl.com/', 'expected' => false],
'Wrong host' => ['url' => 'https://wrong.host.badssl.com/', 'expect' => false],
'Self-signed' => ['url' => 'https://self-signed.badssl.com/', 'expected' => false],
'Untrusted-root' => ['url' => 'https://untrusted-root.badssl.com/', 'expected' => false],
'Revoked' => ['url' => 'https://revoked.badssl.com/'],
'Pinning test' => ['url' => 'https://pinning-test.badssl.com/'],
'Bad DNSSEC' => ['url' => 'http://rhybar.cz', 'expected' => false],
];
foreach ($urls as &$details) {
$httpSocket = new HttpTool();
try {
$response = $httpSocket->get($details['url']);
if (isset($details['process'])) {
$details['success'] = $details['process']($response);
} else {
$details['success'] = true;
}
} catch (Exception $e) {
$details['success'] = false;
$details['exception'] = $e;
}
}
return $urls;
}
private function feeds(array &$output)
{
/** @var Feed $feed */
$FeedsTable = $this->fetchTable('Feeds');
$enabledFeeds = $FeedsTable->find('list', [
'conditions' => [
'input_source' => 'network',
'OR' => [
'enabled' => true,
'caching_enabled' => true,
]
],
'fields' => ['name', 'url'],
]);
foreach ($enabledFeeds as $feedName => $feedUrl) {
if (substr($feedUrl, 0, strlen('http://')) === 'http://') {
$output['Feeds'][] = ['warning', __('Feed %s uses insecure (HTTP) connection.', $feedName)];
}
}
}
private function remoteServers(array &$output)
{
$ServersTable = $this->fetchTable('Servers');
$enabledServers = $ServersTable->find('all', [
'conditions' => ['OR' => [
'push' => true,
'pull' => true,
'push_sightings' => true,
'caching_enabled' => true,
]],
'fields' => ['id', 'name', 'url', 'self_signed', 'cert_file', 'client_cert_file'],
]);
foreach ($enabledServers as $enabledServer) {
if (substr($enabledServer['Server']['url'], 0, strlen('http://')) === 'http://') {
$output['Remote servers'][] = ['warning', __('Server %s uses insecure (HTTP) connection.', $enabledServer['Server']['name'])];
} else if ($enabledServer['Server']['self_signed']) {
$output['Remote servers'][] = ['warning', __('Server %s uses self signed certificate. This is considered insecure.', $enabledServer['Server']['name'])];
}
try {
$parsed = HttpTool::getServerClientCertificateInfo($enabledServer);
if (isset($parsed['public_key_size_ok']) && !$parsed['public_key_size_ok']) {
$algo = $parsed['public_key_type'] . " " . $parsed['public_key_size'];
$output['Remote servers'][] = ['warning', __('Server %s uses weak client certificate (%s).', $enabledServer['Server']['name'], $algo)];
}
} catch (Exception $e) {
}
try {
$parsed = HttpTool::getServerCaCertificateInfo($enabledServer);
if (isset($parsed['public_key_size_ok']) && !$parsed['public_key_size_ok']) {
$algo = $parsed['public_key_type'] . " " . $parsed['public_key_size'];
$output['Remote servers'][] = ['warning', __('Server %s uses weak CA certificate (%s).', $enabledServer['Server']['name'], $algo)];
}
} catch (Exception $e) {
}
}
}
private function email(array &$output)
{
$canSignPgp = Configure::read('GnuPG.sign');
$canSignSmime = Configure::read('SMIME.enabled') &&
!empty(Configure::read('SMIME.cert_public_sign')) &&
!empty(Configure::read('SMIME.key_sign'));
if (!$canSignPgp && !$canSignSmime) {
$output['Email'][] = [
'warning',
__('Email signing (PGP or S/MIME) is not enabled.')
];
}
if ($canSignPgp) {
$gpgKeyPassword = Configure::read('GnuPG.password');
if (empty($gpgKeyPassword)) {
$output['Email'][] = ['error', __('PGP private key password is empty.')];
} else if (strlen($gpgKeyPassword) < self::STRONG_PASSWORD_LENGTH) {
$output['Email'][] = ['warning', __('PGP private key password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
}
}
if (!Configure::read('GnuPG.bodyonlyencrypted')) {
$output['Email'][] = [
'hint',
__('Full email body with all event information will be sent, even without encryption.')
];
}
if ($canSignPgp && !Configure::read('GnuPG.obscure_subject')) {
$output['Email'][] = [
'hint',
__('Even for encrypted emails, the email subject will be sent unencrypted. You can change that behaviour by setting `GnuPG.obscure_subject` to `true`.'),
];
}
$email = new CakeEmail();
$emailConfig = $email->config();
if ($emailConfig['transport'] === 'Smtp' && $emailConfig['port'] == 25 && empty($emailConfig['tls'])) {
$output['Email'][] = [
'warning',
__('STARTTLS is not enabled.'),
'https://en.wikipedia.org/wiki/Opportunistic_TLS',
];
}
}
private function system(array &$output)
{
$kernelBuildTime = $this->getKernelBuild();
if ($kernelBuildTime) {
$diff = (new Chronos())->diff($kernelBuildTime);
$diffDays = $diff->format('a');
if ($diffDays > 300) {
$output['System'][] = [
'warning',
__('Kernel build time was %s days ago. This usually means that the system kernel is not updated.', $diffDays),
];
}
}
// uptime
try {
$since = ProcessTool::execute(['uptime', '-s']);
$since = new Chronos($since);
$diff = (new Chronos())->diff($since);
$diffDays = $diff->format('a');
if ($diffDays > 100) {
$output['System'][] = [
'warning',
__('Uptime of this server is %s days. This usually means that the system kernel is outdated.', $diffDays),
];
}
} catch (Exception $e) {
}
// Python version
try {
$pythonVersion = ProcessTool::execute([ProcessTool::pythonBin(), '-V']);
$parts = explode(' ', $pythonVersion);
if ($parts[0] !== 'Python') {
throw new Exception("Invalid python version response: $pythonVersion");
}
if (version_compare($parts[1], '3.6', '<')) {
$output['System'][] = [
'warning',
__('You are using Python %s. This version is not supported anymore, but it can be still supported by your distribution.', $parts[1]),
'https://endoflife.date/python',
];
} else if (version_compare($parts[1], '3.7', '<')) {
$output['System'][] = [
'hint',
__('You are using Python %s. This version will not be supported beyond 23 Dec 2021, but it can be that it is still supported by your distribution.', $parts[1]),
'https://endoflife.date/python',
];
}
} catch (Exception $e) {
}
$linuxVersion = $this->getLinuxVersion();
if ($linuxVersion) {
list($name, $version) = $linuxVersion;
if ($name === 'Ubuntu') {
if (in_array($version, ['14.04', '16.04', '19.10', '20.10', '21.04', '21.10'], true)) {
$output['System'][] = [
'warning',
__('You are using Ubuntu %s. This version doesn\'t receive security support anymore.', $version),
'https://endoflife.date/ubuntu',
];
}
} else if ($name === 'CentOS Linux' && $version == 8) {
$output['System'][] = [
'warning',
__('You are using CentOS 8. This version doesn\'t receive security support anymore. Please migrate to CentOS 8 Stream.'),
'https://endoflife.date/centos',
];
}
}
}
/**
* @return ChronosInterface|false
*/
private function getKernelBuild()
{
if (PHP_OS !== 'Linux') {
return false;
}
$version = php_uname('v');
if (substr($version, 0, 7) !== '#1 SMP ') {
return false;
}
try {
return new Chronos('@' . substr($version, 7));
} catch (Exception $e) {
return false;
}
}
/**
* @return array|false
*/
private function getLinuxVersion()
{
if (PHP_OS !== 'Linux') {
return false;
}
if (!is_readable('/etc/os-release')) {
return false;
}
$content = file_get_contents('/etc/os-release');
if ($content === false) {
return false;
}
$parsed = parse_ini_string($content);
if ($parsed === false) {
return false;
}
if (!isset($parsed['NAME']) || !isset($parsed['VERSION_ID'])) {
return false;
}
return [$parsed['NAME'], $parsed['VERSION_ID']];
}
/**
* @return string
*/
private function getCakeVersion()
{
$filePath = CAKE_CORE_INCLUDE_PATH . '/Cake/VERSION.txt';
$version = file_get_contents($filePath);
if (!$version) {
throw new RuntimeException("Could not open CakePHP version file '$filePath'.");
}
foreach (explode("\n", $version) as $line) {
if ($line[0] === '/') {
continue;
}
return trim($line);
}
throw new RuntimeException("CakePHP version not found in file '$filePath'.");
}
}

View File

@ -3,9 +3,16 @@
namespace App\Lib\Tools;
use App\Http\Exception\HttpSocketHttpException;
use Cake\Core\Configure;
use Cake\Http\Client\Response;
use Cake\ORM\Locator\LocatorAwareTrait;
use Exception;
use InvalidArgumentException;
class ServerSyncTool
{
use LocatorAwareTrait;
const FEATURE_BR = 'br',
FEATURE_GZIP = 'gzip',
FEATURE_ORG_RULE = 'org_rule',
@ -24,7 +31,7 @@ class ServerSyncTool
/** @var array */
private $request;
/** @var HttpSocketExtended */
/** @var HttpTool */
private $socket;
/** @var CryptographicKey */
@ -41,15 +48,17 @@ class ServerSyncTool
*/
public function __construct(array $server, array $request)
{
if (!isset($server['Server'])) {
if (!isset($server)) {
throw new InvalidArgumentException("Invalid server provided.");
}
$this->server = $server;
$this->request = $request;
$syncTool = new SyncTool();
$this->socket = $syncTool->setupHttpSocket($server);
$HttpTool = new HttpTool();
$HttpTool->configFromServer($server);
$this->socket = $HttpTool;
}
/**
@ -60,14 +69,14 @@ class ServerSyncTool
*/
public function eventExists(array $event)
{
$url = $this->server['Server']['url'] . '/events/view/' . $event['Event']['uuid'];
$url = $this->server['url'] . '/events/view/' . $event['uuid'];
$start = microtime(true);
$exists = $this->socket->head($url, [], $this->request);
$this->log($start, 'HEAD', $url, $exists);
if ($exists->code == '404') {
if ($exists->getStatusCode() == '404') {
return false;
}
if ($exists->code == '200') {
if ($exists->getStatusCode() == '200') {
return true;
}
throw new HttpSocketHttpException($exists, $url);
@ -135,7 +144,7 @@ class ServerSyncTool
// There is bug in MISP API, that returns response code 404 with Location if event already exists
} else if ($e->getResponse()->getHeader('Location')) {
$urlPath = $e->getResponse()->getHeader('Location');
$urlPath = $e->getResponse()->getHeader('Location')[0];
$pieces = explode('/', $urlPath);
$lastPart = end($pieces);
return $this->updateEvent($event, $lastPart);
@ -153,7 +162,7 @@ class ServerSyncTool
*/
public function createEvent(array $event)
{
$logMessage = "Pushing Event #{$event['Event']['id']} to Server #{$this->serverId()}";
$logMessage = "Pushing Event #{$event['id']} to Server #{$this->serverId()}";
return $this->post("/events/add/metadata:1", $event, $logMessage);
}
@ -167,9 +176,9 @@ class ServerSyncTool
public function updateEvent(array $event, $eventId = null)
{
if ($eventId === null) {
$eventId = $event['Event']['uuid'];
$eventId = $event['uuid'];
}
$logMessage = "Pushing Event #{$event['Event']['id']} to Server #{$this->serverId()}";
$logMessage = "Pushing Event #{$event['id']} to Server #{$this->serverId()}";
return $this->post("/events/edit/$eventId/metadata:1", $event, $logMessage);
}
@ -239,12 +248,15 @@ class ServerSyncTool
*/
public function fetchSightingsForEvents(array $eventUuids)
{
return $this->post('/sightings/restSearch/event', [
'returnFormat' => 'json',
'last' => 0, // fetch all
'includeUuid' => true,
'uuid' => $eventUuids,
])->json()['response'];
return $this->post(
'/sightings/restSearch/event',
[
'returnFormat' => 'json',
'last' => 0, // fetch all
'includeUuid' => true,
'uuid' => $eventUuids,
]
)->getJson()['response'];
}
/**
@ -260,8 +272,8 @@ class ServerSyncTool
return [];
}
$response = $this->post('/sightings/filterSightingUuidsForPush/' . $event['Event']['uuid'], $sightingUuids);
return $response->json();
$response = $this->post('/sightings/filterSightingUuidsForPush/' . $event['uuid'], $sightingUuids);
return $response->getJson();
}
/**
@ -304,7 +316,7 @@ class ServerSyncTool
}
$response = $this->get('/servers/getVersion');
$info = $response->json();
$info = $response->getJson();
if (!isset($info['version'])) {
throw new Exception("Invalid response when fetching server version: `version` field missing.");
}
@ -354,7 +366,15 @@ class ServerSyncTool
*/
public function serverId()
{
return $this->server['Server']['id'];
return $this->server['id'];
}
/**
* @return string
*/
public function serverName()
{
return $this->server['name'];
}
/**
@ -362,7 +382,7 @@ class ServerSyncTool
*/
public function pullRules()
{
return $this->decodeRule('pull_rules');
return $this->server['pull_rules'];
}
/**
@ -370,7 +390,7 @@ class ServerSyncTool
*/
public function pushRules()
{
return $this->decodeRule('push_rules');
return $this->server['push_rules'];
}
/**
@ -431,7 +451,7 @@ class ServerSyncTool
*/
private function get($url)
{
$url = $this->server['Server']['url'] . $url;
$url = $this->server['url'] . $url;
$start = microtime(true);
$response = $this->socket->get($url, [], $this->request);
$this->log($start, 'GET', $url, $response);
@ -453,7 +473,7 @@ class ServerSyncTool
*/
private function post($url, $data, $logMessage = null, $etag = null)
{
$protectedMode = !empty($data['Event']['protected']);
$protectedMode = !empty($data['protected']);
$data = JsonTool::encode($data);
if ($logMessage && !empty(Configure::read('Security.sync_audit'))) {
@ -491,11 +511,11 @@ class ServerSyncTool
$data = gzencode($data, 1);
}
}
$url = $this->server['Server']['url'] . $url;
$url = $this->server['url'] . $url;
$start = microtime(true);
$response = $this->socket->post($url, $data, $request);
$this->log($start, 'POST', $url, $response);
if ($etag && $response->isNotModified()) {
if ($etag && $response->getStatusCode() === 304) {
return $response; // if etag was provided and response code is 304, it is valid response
}
if (!$response->isOk()) {
@ -516,7 +536,7 @@ class ServerSyncTool
}
if (!$this->cryptographicKey) {
$this->cryptographicKey = ClassRegistry::init('CryptographicKey');
$this->cryptographicKey = $this->fetchTable('CryptographicKeys');
}
$signature = $this->cryptographicKey->signWithInstanceKey($data);
if (empty($signature)) {
@ -525,16 +545,6 @@ class ServerSyncTool
return base64_encode($signature);
}
/**
* @param string $key
* @return array
*/
private function decodeRule($key)
{
$rules = $this->server['Server'][$key];
return json_decode($rules, true);
}
/**
* @param array $params
* @return string
@ -558,14 +568,17 @@ class ServerSyncTool
* @param float $start Microtime when request was send
* @param string $method HTTP method
* @param string $url
* @param HttpSocketResponse $response
* @param Response $response
*/
private function log($start, $method, $url, HttpSocketResponse $response)
private function log($start, $method, $url, Response $response)
{
$duration = round(microtime(true) - $start, 3);
$responseSize = strlen($response->body);
$ce = $response->getHeader('Content-Encoding');
$logEntry = '[' . date('Y-m-d H:i:s', intval($start)) . "] \"$method $url\" {$response->code} $responseSize $duration $ce\n";
file_put_contents(APP . 'tmp/logs/server-sync.log', $logEntry, FILE_APPEND | LOCK_EX);
$responseSize = strlen($response->getBody());
$ce = '';
if ($response->getHeader('Content-Encoding')) {
$ce = $response->getHeader('Content-Encoding')[0];
}
$logEntry = '[' . date('Y-m-d H:i:s', intval($start)) . "] \"$method $url\" {$response->getStatusCode()} $responseSize $duration $ce\n";
file_put_contents(APP . '../logs/server-sync.log', $logEntry, FILE_APPEND | LOCK_EX);
}
}

View File

@ -25,8 +25,10 @@ class JsonFieldsBehavior extends Behavior
$config = $this->getConfig();
foreach ($config['fields'] as $field => $fieldConfig) {
if (!$entity->has($field) && array_key_exists('default', $fieldConfig)) {
if (!isset($data[$field]) && array_key_exists('default', $fieldConfig)) {
$entity->set($field, $fieldConfig['default']);
} else {
$entity->set($field, $data[$field] ?? []);
}
}
}
@ -44,6 +46,17 @@ class JsonFieldsBehavior extends Behavior
}
}
public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
$config = $this->getConfig();
foreach ($config['fields'] as $field => $fieldConfig) {
if ($entity[$field] !== null) {
$entity[$field] = JsonTool::decode($entity[$field]);
}
}
}
public function beforeFind(EventInterface $event, Query $query, ArrayObject $options)
{
$config = $this->getConfig();

View File

@ -12,4 +12,117 @@ class Event extends AppModel
public const ANALYSIS_LEVELS = [
0 => 'Initial', 1 => 'Ongoing', 2 => 'Completed'
];
/**
* @return array[]
*/
public static function exportTypes()
{
return [
'json' => [
'extension' => '.json',
'type' => 'JSON',
'scope' => 'Event',
'requiresPublished' => 0,
'params' => ['includeAttachments' => 1, 'ignore' => 1, 'returnFormat' => 'json'],
'description' => __('Click this to download all events and attributes that you have access to in MISP JSON format.'),
],
'xml' => [
'extension' => '.xml',
'type' => 'XML',
'scope' => 'Event',
'params' => ['includeAttachments' => 1, 'ignore' => 1, 'returnFormat' => 'xml'],
'requiresPublished' => 0,
'description' => __('Click this to download all events and attributes that you have access to in MISP XML format.'),
],
'csv_sig' => [
'extension' => '.csv',
'type' => 'CSV_Sig',
'scope' => 'Event',
'requiresPublished' => 1,
'params' => ['published' => 1, 'to_ids' => 1, 'returnFormat' => 'csv'],
'description' => __('Click this to download all attributes that are indicators and that you have access to (except file attachments) in CSV format.'),
],
'csv_all' => [
'extension' => '.csv',
'type' => 'CSV_All',
'scope' => 'Event',
'requiresPublished' => 0,
'params' => ['ignore' => 1, 'returnFormat' => 'csv'],
'description' => __('Click this to download all attributes that you have access to (except file attachments) in CSV format.'),
],
'suricata' => [
'extension' => '.rules',
'type' => 'Suricata',
'scope' => 'Attribute',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'suricata'],
'description' => __('Click this to download all network related attributes that you have access to under the Suricata rule format. Only published events and attributes marked as IDS Signature are exported. Administration is able to maintain a allowedlist containing host, domain name and IP numbers to exclude from the NIDS export.'),
],
'snort' => [
'extension' => '.rules',
'type' => 'Snort',
'scope' => 'Attribute',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'snort'],
'description' => __('Click this to download all network related attributes that you have access to under the Snort rule format. Only published events and attributes marked as IDS Signature are exported. Administration is able to maintain a allowedlist containing host, domain name and IP numbers to exclude from the NIDS export.'),
],
'bro' => [
'extension' => '.intel',
'type' => 'Bro',
'scope' => 'Attribute',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'bro'],
'description' => __('Click this to download all network related attributes that you have access to under the Bro rule format. Only published events and attributes marked as IDS Signature are exported. Administration is able to maintain a allowedlist containing host, domain name and IP numbers to exclude from the NIDS export.'),
],
'stix' => [
'extension' => '.xml',
'type' => 'STIX',
'scope' => 'Event',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'stix', 'includeAttachments' => 1],
'description' => __('Click this to download a STIX document containing the STIX version of all events and attributes that you have access to.')
],
'stix2' => [
'extension' => '.json',
'type' => 'STIX2',
'scope' => 'Event',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'stix2', 'includeAttachments' => 1],
'description' => __('Click this to download a STIX2 document containing the STIX2 version of all events and attributes that you have access to.')
],
'rpz' => [
'extension' => '.txt',
'type' => 'RPZ',
'scope' => 'Attribute',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'rpz'],
'description' => __('Click this to download an RPZ Zone file generated from all ip-src/ip-dst, hostname, domain attributes. This can be useful for DNS level firewalling. Only published events and attributes marked as IDS Signature are exported.')
],
'text' => [
'extension' => '.txt',
'type' => 'TEXT',
'scope' => 'Attribute',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'text', 'includeAttachments' => 1],
'description' => __('Click on one of the buttons below to download all the attributes with the matching type. This list can be used to feed forensic software when searching for susipicious files. Only published events and attributes marked as IDS Signature are exported.')
],
'yara' => [
'extension' => '.yara',
'type' => 'Yara',
'scope' => 'Event',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'yara'],
'description' => __('Click this to download Yara rules generated from all relevant attributes.')
],
'yara-json' => [
'extension' => '.json',
'type' => 'Yara',
'scope' => 'Event',
'requiresPublished' => 1,
'params' => ['returnFormat' => 'yara-json'],
'description' => __('Click this to download Yara rules generated from all relevant attributes. Rules are returned in a JSON format with information about origin (generated or parsed) and validity.')
],
];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
class SystemSetting extends AppModel
{
public const BLOCKED_SETTINGS = [
'Security.encryption_key',
'Security.disable_local_feed_access',
'GnuPG.binary',
'MISP.python_bin',
'MISP.ca_path',
'MISP.tmpdir',
'MISP.system_setting_db',
'MISP.attachments_dir',
'MISP.self_update',
'MISP.online_version_check',
];
// Allow to set config values just for these categories
public const ALLOWED_CATEGORIES = [
'MISP',
'Security',
'GnuPG',
'SMIME',
'Proxy',
'SecureAuth',
'Session',
'Plugin',
'debug',
'site_admin_debug',
];
/**
* Sensitive setting are passwords or api keys.
* @param string $setting Setting name
* @return bool
*/
public static function isSensitive($setting)
{
if ($setting === 'Security.encryption_key' || $setting === 'Security.salt') {
return true;
}
if (substr($setting, 0, 7) === 'Plugin.' && (strpos($setting, 'apikey') !== false || strpos($setting, 'secret') !== false)) {
return true;
}
return strpos($setting, 'password') !== false;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Model\Table;
use App\Lib\Tools\BackgroundJobsTool;
use App\Lib\Tools\EncryptedValue;
use App\Lib\Tools\FileAccessTool;
use App\Lib\Tools\GitTool;
use Cake\Collection\CollectionInterface;
@ -358,4 +359,65 @@ class AppTable extends Table
return self::$loadedBackgroundJobsTool;
}
public function validateAuthkey($value)
{
if (empty($value)) {
return 'Empty authkey found. Make sure you set the 40 character long authkey.';
}
if (!preg_match('/[a-z0-9]{40}/i', $value)) {
return 'The authkey has to be exactly 40 characters long and consist of alphanumeric characters.';
}
return true;
}
public function valueIsID($value)
{
if (!is_numeric($value) || $value < 0) {
return 'Invalid ' . ucfirst($value) . ' ID';
}
return true;
}
/**
* @param array $server
* @param string $model
* @return array[]
* @throws JsonException
*/
public function setupSyncRequest(array $server)
{
$version = implode('.', $this->checkMISPVersion());
$commit = $this->checkMISPCommit();
$authkey = $server['authkey'];
if (EncryptedValue::isEncrypted($authkey)) {
$authkey = (string)new EncryptedValue($authkey);
}
return [
'headers' => [
'Authorization' => $authkey,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'MISP ' . $version . (empty($commit) ? '' : ' - #' . $commit),
]
];
}
/**
* @param string $name
* @return bool
*/
protected function pubToZmq($name)
{
static $zmqEnabled;
if ($zmqEnabled === null) {
$zmqEnabled = (bool)Configure::read('Plugin.ZeroMQ_enable');
}
if ($zmqEnabled) {
return Configure::read("Plugin.ZeroMQ_{$name}_notifications_enable");
}
return false;
}
}

View File

@ -56,44 +56,44 @@ class CryptographicKeysTable extends AppTable
'uuid',
'uuid',
[
'rule' => 'uuid',
'message' => 'Please provide a valid RFC 4122 UUID'
'rule' => 'uuid',
'message' => 'Please provide a valid RFC 4122 UUID'
]
)
->add(
'type',
'inList',
[
'rule' => ['inList', self::VALID_TYPES],
'message' => 'Invalid key type'
'rule' => ['inList', self::VALID_TYPES],
'message' => 'Invalid key type'
]
)
->add(
'key_data',
'notBlankKey',
[
'rule' => 'notBlank',
'message' => 'No key data received.'
'rule' => 'notBlank',
'message' => 'No key data received.'
]
)
->add(
'key_data',
'validKey',
[
'rule' => function ($value, $context) {
return $this->validateKey($context['data']['type'], $value);
},
'message' => 'Invalid key.'
'rule' => function ($value, $context) {
return $this->validateKey($context['data']['type'], $value);
},
'message' => 'Invalid key.'
]
)
->add(
'key_data',
'uniqueKeyForElement',
[
'rule' => function ($value, $context) {
return $this->uniqueKeyForElement($value, $context);
},
'message' => 'This key is already assigned to the target.'
'rule' => function ($value, $context) {
return $this->uniqueKeyForElement($value, $context);
},
'message' => 'This key is already assigned to the target.'
]
);
@ -189,13 +189,13 @@ class CryptographicKeysTable extends AppTable
return $this->find(
'column',
[
'conditions' => [
'CryptographicKey.parent_type' => 'Event',
'CryptographicKey.parent_id' => $eventIds,
'CryptographicKey.fingerprint' => $instanceKey,
],
'fields' => ['CryptographicKey.parent_id'],
'recursive' => -1,
'conditions' => [
'CryptographicKeys.parent_type' => 'Event',
'CryptographicKeys.parent_id' => $eventIds,
'CryptographicKeys.fingerprint' => $instanceKey,
],
'fields' => ['CryptographicKeys.parent_id'],
'recursive' => -1,
]
);
}
@ -299,10 +299,10 @@ class CryptographicKeysTable extends AppTable
{
return $this->find()->where(
[
'parent_type' => $context['data']['parent_type'],
'parent_id' => $context['data']['parent_id'],
'key_data' => $value,
'type' => $context['data']['type'],
'parent_type' => $context['data']['parent_type'],
'parent_id' => $context['data']['parent_id'],
'key_data' => $value,
'type' => $context['data']['type'],
]
)->all()->isEmpty();
}
@ -343,19 +343,19 @@ class CryptographicKeysTable extends AppTable
$existingKeys = $this->find(
'all',
[
'recursive' => -1,
'conditions' => [
'parent_type' => $type,
'parent_id' => $parent_id,
],
'fields' => [
'id',
'type',
'parent_type',
'parent_id',
'revoked',
'fingerprint',
]
'recursive' => -1,
'conditions' => [
'parent_type' => $type,
'parent_id' => $parent_id,
],
'fields' => [
'id',
'type',
'parent_type',
'parent_id',
'revoked',
'fingerprint',
]
]
)->first();
$toRemove = [];
@ -377,13 +377,13 @@ class CryptographicKeysTable extends AppTable
foreach ($cryptographicKeys as $cryptographicKey) {
$cryptoKeyEntity = $this->newEntity(
[
'uuid' => $cryptographicKey['uuid'],
'key_data' => $cryptographicKey['key_data'],
'fingerprint' => $cryptographicKey['fingerprint'],
'revoked' => $cryptographicKey['revoked'],
'parent_type' => $cryptographicKey['parent_type'],
'parent_id' => $parent_id,
'type' => $cryptographicKey['type']
'uuid' => $cryptographicKey['uuid'],
'key_data' => $cryptographicKey['key_data'],
'fingerprint' => $cryptographicKey['fingerprint'],
'revoked' => $cryptographicKey['revoked'],
'parent_type' => $cryptographicKey['parent_type'],
'parent_id' => $parent_id,
'type' => $cryptographicKey['type']
]
);
@ -399,7 +399,7 @@ class CryptographicKeysTable extends AppTable
$cryptographicKey['parent_type'],
$parent_id
);
$this->deleteAll(['CryptographicKey.id' => $toRemove]);
$this->deleteAll(['CryptographicKeys.id' => $toRemove]);
$this->loadLog()->createLogEntry($user, 'updateCryptoKeys', $cryptographicKey['parent_type'], $cryptographicKey['parent_id'], $message);
}

View File

@ -54,13 +54,17 @@ class EventBlocklistsTable extends AppTable
*/
public function removeBlockedEvents(array &$eventArray)
{
if (empty($eventArray)) {
return;
}
// When event array contains a lot events, it is more efficient to fetch all blocked events
$conditions = (count($eventArray) > 10000) ? [] : ['EventBlocklist.event_uuid' => array_column($eventArray, 'uuid')];
$conditions = (count($eventArray) > 10000) ? [] : ['event_uuid IN' => array_column($eventArray, 'uuid')];
$blocklistHits = $this->find(
'column',
[
'conditions' => $conditions,
'fields' => ['EventBlocklist.event_uuid'],
'conditions' => $conditions,
'fields' => ['event_uuid'],
]
);
if (empty($blocklistHits)) {

View File

@ -2,6 +2,7 @@
namespace App\Model\Table;
use App\Lib\Tools\ServerSyncTool;
use App\Model\Table\AppTable;
use ArrayObject;
use Cake\Core\Configure;
@ -18,6 +19,34 @@ class EventsTable extends AppTable
parent::initialize($config);
$this->addBehavior('AuditLog');
$this->belongsTo(
'User',
[
'className' => 'Users',
'foreignKey' => 'user_id'
]
);
$this->belongsTo(
'ThreatLevel',
[
'className' => 'ThreatLevels',
'foreignKey' => 'threat_level_id'
]
);
$this->belongsTo(
'Org',
[
'className' => 'Organisations',
'foreignKey' => 'org_id'
]
);
$this->belongsTo(
'Orgc',
[
'className' => 'Organisations',
'foreignKey' => 'orgc_id'
]
);
$this->belongsTo(
'SharingGroup',
[
@ -25,6 +54,7 @@ class EventsTable extends AppTable
'foreignKey' => 'sharing_group_id'
]
);
$this->hasMany(
'Attributes',
[
@ -32,6 +62,52 @@ class EventsTable extends AppTable
'propertyName' => 'Attribute'
]
);
$this->hasMany(
'ShadowAttributes',
[
'dependent' => true,
'propertyName' => 'ShadowAttribute'
]
);
$this->hasMany(
'Objects',
[
'dependent' => true,
'propertyName' => 'Object',
]
);
$this->hasMany(
'EventTags',
[
'dependent' => true,
'propertyName' => 'EventTag',
]
);
$this->hasMany(
'Sightings',
[
'dependent' => true,
'propertyName' => 'Sighting',
]
);
$this->hasMany(
'EventReports',
[
'dependent' => true,
'propertyName' => 'EventReport',
]
);
$this->hasMany(
'CryptographicKeys',
[
'dependent' => true,
'propertyName' => 'CryptographicKey',
'foreignKey' => 'parent_id',
'conditions' => [
'parent_type' => 'Events'
],
]
);
$this->setDisplayField('title');
}
@ -124,6 +200,8 @@ class EventsTable extends AppTable
public function _add(array &$data, $fromXml, array $user, $org_id = 0, $passAlong = null, $fromPull = false, $jobId = null, &$created_id = 0, &$validationErrors = [])
{
// TODO: [3.x-MIGRATION] implement when events controller is migrated see #9391
// THIS IS A PLACEHOLDER !
$data['Event']['user_id'] = $user['id'];
if ($fromPull) {
$data['Event']['org_id'] = $org_id;
@ -143,6 +221,46 @@ class EventsTable extends AppTable
public function _edit(array &$data, array $user, $id = null, $jobId = null, $passAlong = null, $force = false, $fast_update = false)
{
// TODO: [3.x-MIGRATION] implement when events controller is migrated see #9391
// THIS IS A PLACEHOLDER !
return true;
}
public function fetchEvent($user, $options = [], $useCache = false)
{
// TODO: [3.x-MIGRATION] implement when events controller is migrated see #9391
// THIS IS A PLACEHOLDER !
if (isset($options['event_uuid'])) {
return $this->find(
'all',
[
'conditions' => [
'uuid' => $options['event_uuid']
]
]
)->disableHydration()->toArray();
}
return [];
}
/**
* @param array $event
* @param array $server
* @param ServerSyncTool $serverSync
* @return false|string
* @throws HttpSocketJsonException
* @throws JsonException
* @throws Exception
*/
public function uploadEventToServer(array $event, array $server, ServerSyncTool $serverSync)
{
// TODO: [3.x-MIGRATION] implement when events controller is migrated see #9391
// THIS IS A PLACEHOLDER !
$serverSync->pushEvent($event)->getJson();
return 'Success';
}
}

View File

@ -193,7 +193,7 @@ class FeedsTable extends AppTable
* Gets the event UUIDs from the feed by ID
* Returns an array with the UUIDs of events that are new or that need updating.
*
* @param array $feed
* @param Feed $feed
* @param HttpClient|null $HttpSocket
* @return array
* @throws Exception
@ -228,12 +228,12 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param HttpClient|null $HttpSocket Null can be for local feed
* @return Generator<string>
* @throws Exception
*/
public function getCache(array $feed, HttpClient $HttpSocket = null)
public function getCache(Feed $feed, HttpClient $HttpSocket = null)
{
$uri = $feed['url'] . '/hashes.csv';
$data = $this->feedGetUri($feed, $uri, $HttpSocket);
@ -288,13 +288,13 @@ class FeedsTable extends AppTable
/**
* Get remote manifest for feed with etag checking.
* @param array $feed
* @param Feed $feed
* @param HttpClient $HttpSocket
* @return array
* @throws HttpException
* @throws JsonException
*/
private function getRemoteManifest(array $feed, HttpClient $HttpSocket)
private function getRemoteManifest(Feed $feed, HttpClient $HttpSocket)
{
$feedCache = Feed::CACHE_DIR . 'misp_feed_' . (int)$feed['id'] . '_manifest.cache.gz';
$feedCacheEtag = Feed::CACHE_DIR . 'misp_feed_' . (int)$feed['id'] . '_manifest.etag';
@ -350,12 +350,12 @@ class FeedsTable extends AppTable
/**
* Load remote file with cache support and etag checking.
* @param array $feed
* @param Feed $feed
* @param HttpClient $HttpSocket
* @return string
* @throws HttpException
*/
private function getFreetextFeedRemote(array $feed, HttpClient $HttpSocket)
private function getFreetextFeedRemote(Feed $feed, HttpClient $HttpSocket)
{
$feedCache = Feed::CACHE_DIR . 'misp_feed_' . (int)$feed['id'] . '.cache.gz';
$feedCacheEtag = Feed::CACHE_DIR . 'misp_feed_' . (int)$feed['id'] . '.etag';
@ -403,7 +403,7 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param HttpClient|null $HttpSocket Null can be for local feed
* @param string $type
* @return array|bool
@ -728,14 +728,14 @@ class FeedsTable extends AppTable
/**
* @param array $actions
* @param array $feed
* @param Feed $feed
* @param HttpClient|null $HttpSocket
* @param array $user
* @param int|false $jobId
* @return array
* @throws Exception
*/
private function downloadFromFeed(array $actions, array $feed, HttpClient $HttpSocket = null, array $user, $jobId = false)
private function downloadFromFeed(array $actions, Feed $feed, HttpClient $HttpSocket = null, array $user, $jobId = false)
{
$total = count($actions['add']) + count($actions['edit']);
$currentItem = 0;
@ -1012,11 +1012,11 @@ class FeedsTable extends AppTable
/**
* @param array $event
* @param array $feed
* @param Feed $feed
* @param array $filterRules
* @return array|string
*/
private function __prepareEvent($event, array $feed, $filterRules)
private function __prepareEvent($event, Feed $feed, $filterRules)
{
if (isset($event['response'])) {
$event = $event['response'];
@ -1150,7 +1150,7 @@ class FeedsTable extends AppTable
/**
* @param HttpClient|null $HttpSocket
* @param array $feed
* @param Feed $feed
* @param string $uuid
* @param array $user
* @param array|bool $filterRules
@ -1236,7 +1236,7 @@ class FeedsTable extends AppTable
if ($feed['source_format'] === 'misp') {
$this->jobProgress($jobId, 'Fetching event manifest.');
try {
$actions = $this->getNewEventUuids($feed->toArray(), $HttpSocket);
$actions = $this->getNewEventUuids($feed, $HttpSocket);
} catch (Exception $e) {
$this->logException("Could not get new event uuids for feed $feedId.", $e);
$this->jobProgress($jobId, 'Could not fetch event manifest. See error log for more details.');
@ -1249,12 +1249,12 @@ class FeedsTable extends AppTable
$total = count($actions['add']) + count($actions['edit']);
$this->jobProgress($jobId, __("Fetching %s events.", $total));
$result = $this->downloadFromFeed($actions, $feed->toArray(), $HttpSocket, $user->toArray(), $jobId);
$result = $this->downloadFromFeed($actions, $feed, $HttpSocket, $user->toArray(), $jobId);
$this->__cleanupFile($feed, '/manifest.json');
} else {
$this->jobProgress($jobId, 'Fetching data.');
try {
$temp = $this->getFreetextFeed($feed->toArray(), $HttpSocket, $feed['source_format']);
$temp = $this->getFreetextFeed($feed, $HttpSocket, $feed['source_format']);
} catch (Exception $e) {
$this->logException("Could not get freetext feed $feedId", $e);
$this->jobProgress($jobId, 'Could not fetch freetext feed. See error log for more details.');
@ -1511,7 +1511,7 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param Redis $redis
* @param int|false $jobId
* @return bool
@ -1535,13 +1535,13 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param Redis $redis
* @param HttpClient|null $HttpSocket
* @param int|false $jobId
* @return bool
*/
private function __cacheFreetextFeed(array $feed, $redis, HttpClient $HttpSocket = null, $jobId = false)
private function __cacheFreetextFeed(Feed $feed, $redis, HttpClient $HttpSocket = null, $jobId = false)
{
$feedId = $feed['id'];
@ -2092,7 +2092,7 @@ class FeedsTable extends AppTable
/**
* Download and parse event from feed.
*
* @param array $feed
* @param Feed $feed
* @param string $eventUuid
* @param HttpClient|null $HttpSocket Null can be for local feed
* @return array
@ -2115,7 +2115,7 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param string $uri
* @param HttpClient|null $HttpSocket Null can be for local feed
* @return string
@ -2136,14 +2136,14 @@ class FeedsTable extends AppTable
}
/**
* @param array $feed
* @param Feed $feed
* @param string $uri
* @param HttpClient $HttpSocket
* @param string|null $etag
* @return false|HttpClientResponse
* @throws HttpException
*/
private function feedGetUriRemote(array $feed, $uri, HttpClient $HttpSocket, $etag = null)
private function feedGetUriRemote(Feed $feed, $uri, HttpClient $HttpSocket, $etag = null)
{
$request = $this->__createFeedRequest($feed['headers']);
if ($etag) {

View File

@ -4,6 +4,10 @@ namespace App\Model\Table;
use App\Model\Entity\Distribution;
use App\Model\Table\AppTable;
use ArrayObject;
use Cake\Collection\CollectionInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Query;
use Cake\Utility\Hash;
use Cake\Validation\Validator;
@ -88,17 +92,24 @@ class GalaxyClusterRelationsTable extends AppTable
);
}
public function afterFind($results, $primary = false)
public function beforeFind(EventInterface $event, Query $query, ArrayObject $options)
{
foreach ($results as $k => $result) {
if (isset($result['TargetCluster']) && key_exists('id', $result['TargetCluster']) && is_null($result['TargetCluster']['id'])) {
$results[$k]['TargetCluster'] = [];
}
if (isset($result['GalaxyClusterRelation']['distribution']) && $result['GalaxyClusterRelation']['distribution'] != 4) {
unset($results[$k]['SharingGroup']);
}
}
return $results;
$query->formatResults(
function (CollectionInterface $results) {
return $results->map(
function ($row) {
if (isset($row['TargetCluster']) && key_exists('id', $row['TargetCluster']) && is_null($row['TargetCluster']['id'])) {
$row['TargetCluster'] = [];
}
if (isset($row['GalaxyClusterRelation']['distribution']) && $row['GalaxyClusterRelation']['distribution'] != 4) {
unset($row['SharingGroup']);
}
return $row;
}
);
},
$query::APPEND
);
}
public function buildConditions($user, $clusterConditions = true)

View File

@ -5,6 +5,7 @@ namespace App\Model\Table;
use App\Http\Exception\HttpSocketHttpException;
use App\Lib\Tools\BackgroundJobsTool;
use App\Lib\Tools\HttpTool;
use App\Lib\Tools\LogExtendedTrait;
use App\Lib\Tools\ServerSyncTool;
use App\Lib\Tools\TmpFileTool;
use App\Model\Entity\Distribution;
@ -37,6 +38,8 @@ use Exception;
*/
class GalaxyClustersTable extends AppTable
{
use LogExtendedTrait;
private $__assetCache = [];
private $__clusterCache = [];
private $deletedClusterUUID;

View File

@ -3,6 +3,7 @@
namespace App\Model\Table;
use App\Lib\Tools\BackgroundJobsTool;
use App\Lib\Tools\LogExtendedTrait;
use App\Model\Entity\Job;
use App\Model\Table\AppTable;
use ArrayObject;
@ -12,6 +13,8 @@ use Exception;
class JobsTable extends AppTable
{
use LogExtendedTrait;
public function initialize(array $config): void
{
parent::initialize($config);

View File

@ -1348,4 +1348,20 @@ class LogsTable extends AppTable
}
return $this->elasticSearchClient;
}
/**
* @param $data
* @param $options
* @return array|bool|mixed
*/
public function saveOrFailSilently($data, $options = [])
{
try {
$entity = $this->newEntity($data, $options);
return $this->save($entity, $options);
} catch (Exception $e) {
$this->logException('Could not save log to database', $e);
return false;
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
class MispObjects extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('objects');
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use App\Lib\Tools\RedisTool;
use Cake\ORM\Query;
use Cake\Collection\CollectionInterface;
class OrgBlocklistsTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('AuditLog');
}
public $blocklistFields = ['org_uuid', 'comment', 'org_name'];
public $blocklistTarget = 'org';
private $blockedCache = [];
public function validationDefault(Validator $validator): Validator
{
$validator
->requirePresence('org_uuid')
->notEmptyString('org_uuid')
->uuid('org_uuid');
return $validator;
}
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->isUnique(['org_uuid']));
return $rules;
}
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
if (empty($entity->id)) {
$entity->created = date('Y-m-d H:i:s');
}
return true;
}
public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
parent::afterDelete();
if (!empty($entity['org_uuid'])) {
$this->cleanupBlockedCount($entity['org_uuid']);
}
}
public function beforeFind(EventInterface $event, Query $query, ArrayObject $options)
{
$query->formatResults(
function (CollectionInterface $results) {
return $results->map(
function ($row) {
if (isset($row['org_uuid'])) {
$row['blocked_data'] = $this->getBlockedData($row['org_uuid']);
}
return $row;
}
);
},
$query::APPEND
);
}
/**
* @param array $eventArray
*/
public function removeBlockedEvents(array &$eventArray)
{
if (empty($eventArray)) {
return;
}
// When event array contains a lot events, it is more efficient to fetch all blocked events
$blocklistHits = $this->find(
'column',
[
'conditions' => ['org_uuid IN' => array_column($eventArray, 'orgc_uuid')],
'fields' => ['org_uuid'],
]
);
if (empty($blocklistHits)) {
return;
}
$blocklistHits = array_flip($blocklistHits->toArray());
foreach ($eventArray as $k => $event) {
if (isset($blocklistHits[$event['orgc_uuid']])) {
unset($eventArray[$k]);
}
}
}
/**
* @param int|string $orgIdOrUuid Organisation ID or UUID
* @return bool
*/
public function isBlocked($orgIdOrUuid)
{
if (isset($this->blockedCache[$orgIdOrUuid])) {
return $this->blockedCache[$orgIdOrUuid];
}
if (is_numeric($orgIdOrUuid)) {
$orgUuid = $this->getUUIDFromID($orgIdOrUuid);
} else {
$orgUuid = $orgIdOrUuid;
}
$isBlocked = $this->exists(['org_uuid' => $orgUuid]);
$this->blockedCache[$orgIdOrUuid] = $isBlocked;
return $isBlocked;
}
private function getUUIDFromID($orgID)
{
$OrganisationsTable = $this->fetchTable('Organisations');
$orgUuid = $OrganisationsTable->get($orgID, [
'fields' => ['Organisation.uuid'],
]);
if (empty($orgUuid)) {
return false; // org not found by ID, so it is not blocked
}
$orgUuid = $orgUuid['uuid'];
return $orgUuid;
}
public function saveEventBlocked($orgIdOrUUID)
{
if (is_numeric($orgIdOrUUID)) {
$orgcUUID = $this->getUUIDFromID($orgIdOrUUID);
} else {
$orgcUUID = $orgIdOrUUID;
}
$lastBlockTime = time();
$redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}";
$redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}";
$redis = RedisTool::init();
if ($redis !== false) {
$pipe = $redis->multi(Redis::PIPELINE)
->incr($redisKeyBlockAmount)
->set($redisKeyBlockLastTime, $lastBlockTime);
$pipe->exec();
}
}
private function cleanupBlockedCount($orgcUUID)
{
$redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}";
$redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}";
$redis = RedisTool::init();
if ($redis !== false) {
$pipe = $redis->multi(Redis::PIPELINE)
->del($redisKeyBlockAmount)
->del($redisKeyBlockLastTime);
$pipe->exec();
}
}
public function getBlockedData($orgcUUID)
{
$redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}";
$redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}";
$blockData = [
'blocked_amount' => false,
'blocked_last_time' => false,
];
$redis = RedisTool::init();
if ($redis !== false) {
$blockData['blocked_amount'] = $redis->get($redisKeyBlockAmount);
$blockData['blocked_last_time'] = $redis->get($redisKeyBlockLastTime);
}
return $blockData;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use App\Lib\Tools\ServerSyncTool;
class ShadowAttributesTable extends AppTable
{
/**
* @param array $user
* @param ServerSyncTool $serverSync
* @return int
* @throws HttpSocketHttpException
* @throws HttpSocketJsonException
*/
public function pullProposals(array $user, ServerSyncTool $serverSync)
{
// TODO: [3.x-MIGRATION] Implement pullProposals() method.
return 0;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Model\Table;
use App\Lib\Tools\ServerSyncTool;
use App\Model\Table\AppTable;
class SightingsTable extends AppTable
{
/**
* @param array $user
* @param ServerSyncTool $serverSync
* @return int Number of saved sighting.
* @throws Exception
*/
public function pullSightings(array $user, ServerSyncTool $serverSync)
{
// TODO: [3.x-MIGRATION] Implement pullSightings() method.
return 0;
}
/**
* Push sightings to remote server.
* @param array $user
* @param ServerSyncTool $serverSync
* @return array
* @throws Exception
*/
public function pushSightings(array $user, ServerSyncTool $serverSync)
{
// TODO: [3.x-MIGRATION] Implement pushSightings() method.
return [];
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Model\Validation;
use Cake\Validation\Validator;
class ContactValidator extends Validator
{
public function __construct()
{
parent::__construct();
}
}

View File

@ -0,0 +1,49 @@
7 Gate Immortal All-In (vs. Zerg)
Overview
This 2 base all-in aims to kill the Zerg opponent's third base before they are able to Tech towards Mutalisks or Infestors. It relies on 2 or 3 Immortals and a high number of Sentries that will be used to cut the opponent's army into pieces with Force Fields. Also of importance is a Warp Prism that will be used both for reinforcing your army and moving back injured units, as demonstrated by Squirtle in this game against BBoongBBoong.
9 Pylon
17 Nexus
17 Forge
17 Pylon
18 Gateway
18 Photon Cannon
20 Assimilator x2 (1)(2)
@100% Gateway: Cybernetics Core, Zealot.
@100% Gas: +1 Ground Weapons
@100% Cybernetics Core: Warpgate Research, Stalker
38 Robotics Facility
@100% Robotics Facility: Constant chrono boosted Immortal production (until 3)
Assimilator x2 (3)(4)
@~7:30: 3 Gateways (2)(3)(4)
@100% Warpgate Research: Gateway x3 (5)(6)(7)
Notes
Stop Probe production at ~45. You want optimal saturation (16 Probes) at both mineral lines and 3 Probes in all 4 Assimilators.
As your last Immortals finishes, queue up a Warp Prism and push out immediately. Your last three Gateways should finish shortly afterwards. If you have a proxy Pylon you should start warping-in there, if you do not then warp-in one round of units at home before waiting for the Warp Prism to arrive for further warp-ins. If you do not have a proxy Pylon then you should bring a Probe to build one so that your Warp Prism can focus on protecting your Immortals.
When you push out, your army should consist of 3 Immortals, 1 Zealot, 1 Stalker and 7 Sentries.
After the Warp Prism, you should build an Observer to be on the safe side against Burrow micro.
Scouting
Ideally you want to make sure your opponent is not going for a 2 base all-in because your Gateways will not finish in time to defend against this. To scout for this, use your first Zealot to check whether or not they have a third base in production. If they are going for a 2 base opening then build 2 or 3 more Photon Cannons whilst continuing producing Sentries. After you're safe, continue with the build as best you can; if they opened with a 2 base build and you managed to defend their pressure, your Immortal timing-attack will be devastating as they will not have the economy nor the Tech to defend against it.
Execution
Squirtle positions his units in a small choke and builds a Pylon next to it, making it easier to Force Field efficiently.
A classic base-trade situation at the Protoss player's base.
A classic base-trade situation at the Zerg player's base.
Push when you have your 3rd Immortal and close your wall behind you. While you are pushing, you should have a Probe on the map building proxy Pylons as close as possible to your opponent's 3rd base. As soon as the Warp Prism joins your army, you can be more aggressive. Always try to fight in small chokes near some kind of natural wall to avoid being surrounded and to minimize the amount of Force Fields that you need to create a full wall. This is of importance as your push relies entirely on Force Fields and Immortals, so not having either one of them makes this push nearly impossible to execute successfully.
If your opponent builds a high number of Zerglings then you should respond by warping-in Zealots. If they build a high number of Roaches you will need more Stalkers. If you run out of Force Fields, you will need more Sentries.
Place your Force Fields to avoid fighting against your opponent's whole army and to prevent being attacked from multiple sides. If your Immortals are targeted or low on hitpoints, or if your Sentries are surrounded or low on hitpoints, use your Warp Prism to save them by loading them in. Drop them as fast as possible in a safer position since you will need all the damage output possible. The Warp Prism's main role here is this kind of micro, that is why having proxy Pylons is important as using the Warp Prism for both warp-ins and micro is not viable. This is because when the Warp Prism takes 2 seconds to switch between phasing and transport mode. Be careful not to let your Warp Prism get sniped by Zerg anti-air (usually Queens, rarely Hydralisks).
If you can get close enough to the Zerg player's third base (or any other non-defensive structure) then you should treat it as a large, natural Force Field.
If the Zerg chooses to avoid your army and instead goes for the base trade, it can be incredibly difficult to defend your natural expansion. This is why it's necessary to completely wall off behind you when you push out. If you are on a map with a wide ramp in the natural (>2 Force Fields required to block it off) then do not try to defend it and instead move all your Probes into your main base. Constant Force Fields will prevent the Zerg player from getting up the ramp whilst you destroy their third base and natural expansion with your army. To not let the Zerg have a better economy than you, you will have to kill their 3rd base and second base quickly before they can build too many Spine Crawlers. Do not try to fight them up their main ramp if they've built a lot of Spine Crawlers in their main to defend; instead come home and kill their main army since your army should be stronger with appropriate micro. Take your natural back if you lost it, take 3rd if you didn't, and then try going back into a normal game. Use an Observer to scout what the Zerg is going to try to do to get back into the game; research Blink and get High Templar if they are going for Mutalisks, get Colossi if they are going for Infestors.
Follow Up
If your opponent successfully stops your push then you will be in a very difficult spot. To be able to defend any counter-attack, you will have to start producing Immortals again and re-build your Sentries. With your Observer you will have to scout if they are transitioning into Mutalisks or Infestors. If they go for Mutalisks, build a Twilight Council and start researching Blink. If they go for Infestors, you will need to build Colossi. You should also be looking to take a 3rd base, although this can be hard against a Zerg opponent who didn't lose their own 3rd base during your push; for this reason, it might be better to max out on two bases and go for a Colossus-based all-in.

View File

@ -10,9 +10,25 @@ class AttributesFixture extends TestFixture
{
public $connection = 'test';
public const ATTRIBUTE_1_ID = 1000;
public const ATTRIBUTE_1_UUID = '60d515a6-efd1-4ae8-a561-1a5203ec9ade';
public function init(): void
{
$this->records = [];
$this->records = [
[
'id' => self::ATTRIBUTE_1_ID,
'uuid' => self::ATTRIBUTE_1_UUID,
'event_id' => EventsFixture::EVENT_1_ID,
'distribution' => 3,
'category' => 'Network activity',
'type' => 'ip-src',
'value1' => '127.0.0.1',
'value2' => '',
'sharing_group_id' => 0,
]
];
parent::init();
}
}

View File

@ -4,15 +4,35 @@ declare(strict_types=1);
namespace App\Test\Fixture;
use App\Model\Entity\Distribution;
use Cake\TestSuite\Fixture\TestFixture;
class EventsFixture extends TestFixture
{
public $connection = 'test';
public const EVENT_1_ID = 1000;
public const EVENT_1_UUID = '02a5f2e5-3c6c-4d40-b973-de465fd2f370';
public function init(): void
{
$this->records = [];
$this->records = [
[
'id' => self::EVENT_1_ID,
'info' => 'Event 1',
'org_id' => OrganisationsFixture::ORGANISATION_A_ID,
'orgc_id' => OrganisationsFixture::ORGANISATION_A_ID,
'user_id' => UsersFixture::USER_ADMIN_ID,
'distribution' => Distribution::ALL_COMMUNITIES,
'analysis' => 0,
'threat_level_id' => 0,
'date' => '2021-01-01 00:00:00',
'published' => 1,
'uuid' => self::EVENT_1_UUID,
'attribute_count' => 1,
'sharing_group_id' => 0,
]
];
parent::init();
}
}

View File

@ -12,12 +12,16 @@ class ServersFixture extends TestFixture
public const SERVER_A_ID = 1000;
public const SERVER_A_NAME = 'Server A';
public const SERVER_A_URL = 'http://aaa.local';
public const SERVER_A_AUTHKEY = '8843d7f92416211de9ebb963ff4ce28125932878';
public const SERVER_B_ID = 2000;
public const SERVER_B_NAME = 'Server B';
public const SERVER_B_URL = 'http://bbb.local';
public const SERVER_C_ID = 3000;
public const SERVER_C_NAME = 'Server C';
public const SERVER_C_URL = 'http://ccc.local';
public function init(): void
{
@ -28,8 +32,8 @@ class ServersFixture extends TestFixture
'id' => self::SERVER_A_ID,
'org_id' => OrganisationsFixture::ORGANISATION_A_ID,
'name' => self::SERVER_A_NAME,
'url' => $faker->url,
'authkey' => $faker->sha1(),
'url' => self::SERVER_A_URL,
'authkey' => self::SERVER_A_AUTHKEY,
'push' => true,
'pull' => true,
'push_sightings' => true,
@ -52,7 +56,7 @@ class ServersFixture extends TestFixture
'id' => self::SERVER_B_ID,
'org_id' => OrganisationsFixture::ORGANISATION_B_ID,
'name' => self::SERVER_B_NAME,
'url' => $faker->url,
'url' => self::SERVER_B_URL,
'authkey' => $faker->sha1(),
'push' => true,
'pull' => true,
@ -76,7 +80,7 @@ class ServersFixture extends TestFixture
'id' => self::SERVER_C_ID,
'org_id' => OrganisationsFixture::ORGANISATION_C_ID,
'name' => self::SERVER_C_NAME,
'url' => $faker->url,
'url' => self::SERVER_C_URL,
'authkey' => $faker->sha1(),
'push' => true,
'pull' => true,

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\OrganisationsFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class AddServerApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/add';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers'
];
public function testAddServer(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$faker = \Faker\Factory::create();
$this->post(
self::ENDPOINT,
[
"name" => "Test Server",
"url" => $faker->url,
"remote_org_id" => OrganisationsFixture::ORGANISATION_A_ID,
"authkey" => $faker->sha256(),
"self_signed" => true,
]
);
$this->assertResponseOk();
$this->assertDbRecordExists('Servers', ['name' => 'Test Server']);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class DeleteServerApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/delete';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
];
public function testDeleteServer(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%d', self::ENDPOINT, ServersFixture::SERVER_B_ID);
$this->assertDbRecordExists('Servers', ['id' => ServersFixture::SERVER_B_ID]);
$this->post($url);
$this->assertResponseOk();
$this->assertDbRecordNotExists('Servers', ['id' => ServersFixture::SERVER_B_ID]);
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class EditServerApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/edit';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers'
];
public function testEditServer(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%s', self::ENDPOINT, ServersFixture::SERVER_A_ID);
$newUrl = 'http://new-url.local';
$this->put(
$url,
[
"url" => $newUrl,
"push" => false,
"pull" => true,
"pull_rules" => [
"tags" => [
"OR" => [],
"NOT" => ["tlp:red"]
],
"orgs" => [
"OR" => [],
"NOT" => []
],
"type_attributes" => [
"NOT" => []
], "type_objects" => [
"NOT" => []
],
"url_params" => ""
]
]
);
$this->assertResponseOk();
$response = $this->getJsonResponseAsArray();
$this->assertDbRecordExists(
'Servers',
[
'id' => ServersFixture::SERVER_A_ID,
'url' => $newUrl,
'push' => false,
'pull' => true,
]
);
$this->assertEquals($response['pull_rules']['tags']['NOT'], ['tlp:red']);
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class GetServerVersionApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/getVersion';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
];
public function testGetServerVersion(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$this->get(self::ENDPOINT);
$this->assertResponseOk();
$response = $this->getJsonResponseAsArray();
// read the version from the VERSION.json file
$versionJson = json_decode(file_get_contents(ROOT . DS . 'VERSION.json'), true);
$expectedVersion = $versionJson['major'] . '.' . $versionJson['minor'] . '.' . $versionJson['hotfix'];
$this->assertEquals($expectedVersion, $response['version']);
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\OrganisationsFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class ImportServerApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/import';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers'
];
public function testAddServer(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$faker = \Faker\Factory::create();
$server = [
"name" => "Test Import Server",
"url" => $faker->url,
"authkey" => $faker->sha256(),
"Organisation" => [
"name" => "ORGNAME",
"uuid" => OrganisationsFixture::ORGANISATION_A_UUID
]
];
$this->post(
self::ENDPOINT,
$server
);
$this->assertResponseOk();
$this->assertDbRecordExists('Servers', ['name' => 'Test Import Server']);
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
class IndexServersApiTest extends TestCase
{
use ApiTestTrait;
protected const ENDPOINT = '/servers/index';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
];
public function testIndexServers(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$this->get(self::ENDPOINT);
$this->assertResponseOk();
$this->assertResponseContains(sprintf('"name": "%s"', ServersFixture::SERVER_A_NAME));
$this->assertResponseContains(sprintf('"name": "%s"', ServersFixture::SERVER_B_NAME));
$this->assertResponseContains(sprintf('"name": "%s"', ServersFixture::SERVER_C_NAME));
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\Core\Configure;
use Cake\Http\TestSuite\HttpClientTrait;
use Cake\TestSuite\TestCase;
class PullServerApiTest extends TestCase
{
use ApiTestTrait;
use HttpClientTrait;
protected const ENDPOINT = '/servers/pull';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
'app.Events',
'app.Attributes',
];
public function testPullFromServer(): void
{
$this->skipOpenApiValidations();
Configure::write('BackgroundJobs.enabled', false);
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%d', self::ENDPOINT, ServersFixture::SERVER_A_ID);
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: MISP 3.0.0 - #cc1f8cc2e89ec692168ffbfea8ed49cc879c469b',
'ETag: W/"2a-1b6e3e8e"',
];
// mock the /servers/getVersion request
$getVersionBody = json_encode(
[
"version" => "3.0.0",
"pymisp_recommended_version" => "3.0.0",
"perm_sync" => true,
"perm_sighting" => true,
"perm_galaxy_editor" => true,
"request_encoding" => [
"gzip",
"br"
],
"filter_sightings" => true
]
);
$this->mockClientGet(
ServersFixture::SERVER_A_URL . '/servers/getVersion',
$this->newClientResponse(200, $headers, $getVersionBody)
);
// mock the /events/index request
$eventsIndexBody = json_encode(
[
[
"id" => "10",
"timestamp" => "1700488705",
"sighting_timestamp" => "0",
"published" => true,
"uuid" => "56bf399d-c46c-4fdb-a9cf-d9bb02de0b81",
"orgc_uuid" => "55f6ea5e-2c60-40e5-964f-47a8950d210f"
]
]
);
$this->mockClientPost(
ServersFixture::SERVER_A_URL . '/events/index',
$this->newClientResponse(200, $headers, $eventsIndexBody)
);
// mock the /events/view/[uuid] request
$eventBody = json_encode(
[
"Event" => [
"analysis" => "2",
"date" => "2015-12-18",
"extends_uuid" => "",
"info" => "OSINT - Hunting for Malware with Machine Learning",
"publish_timestamp" => "1455373314",
"sharing_group_id" => "0",
"distribution" => "0",
"published" => true,
"threat_level_id" => "3",
"timestamp" => "1455373240",
"uuid" => "56bf399d-c46c-4fdb-a9cf-d9bb02de0b81",
"Orgc" => [
"name" => "CIRCL",
"uuid" => "55f6ea5e-2c60-40e5-964f-47a8950d210f"
],
"Tag" => [
[
"colour" => "#004646",
"local" => "0",
"name" => "type:OSINT",
"relationship_type" => ""
],
[
"colour" => "#ffffff",
"local" => "0",
"name" => "tlp:white",
"relationship_type" => ""
]
],
"Attribute" => [
[
"category" => "External analysis",
"comment" => "",
"deleted" => false,
"disable_correlation" => false,
"sharing_group_id" => "0",
"distribution" => "0",
"timestamp" => "1455372745",
"to_ids" => false,
"type" => "link",
"uuid" => "56bf39c9-c078-4368-9555-6cf802de0b81",
"value" => "http://blog.cylance.com/hunting-for-malware-with-machine-learning"
]
]
]
]
);
// mock the event [uuid].json request
$this->mockClientGet(
'http://aaa.local/events/view/56bf399d-c46c-4fdb-a9cf-d9bb02de0b81/deleted%5B%5D:0/deleted%5B%5D:1/excludeGalaxy:1/includeEventCorrelations:0/includeFeedCorrelations:0/includeWarninglistHits:0/excludeLocalTags:1',
$this->newClientResponse(200, $headers, $eventBody)
);
$this->post($url);
$this->assertResponseOk();
$response = $this->getJsonResponseAsArray();
$this->assertEquals(
'Pull completed. 1 events pulled, 0 events could not be pulled, 0 proposals pulled, 0 sightings pulled, 0 clusters pulled.',
$response['message']
);
// check that the event was added
$this->assertDbRecordExists('Events', ['uuid' => '56bf399d-c46c-4fdb-a9cf-d9bb02de0b81']);
$this->assertDbRecordExists('Attributes', ['uuid' => '56bf39c9-c078-4368-9555-6cf802de0b81']);
// TODO: check that the proposals were added
// TODO: check that the objects were added
// TODO: check that the event reports were added
// TODO: check that the sightings were added
// TODO: check that the tags were added
// TODO: check that the galaxies were added
// TODO: check that the cryptographic keys were added
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\EventsFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\Core\Configure;
use Cake\Http\TestSuite\HttpClientTrait;
use Cake\TestSuite\TestCase;
class PushServerApiTest extends TestCase
{
use ApiTestTrait;
use HttpClientTrait;
protected const ENDPOINT = '/servers/push';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
'app.Events',
'app.Attributes',
];
public function testPushToServer(): void
{
$this->skipOpenApiValidations();
Configure::write('BackgroundJobs.enabled', false);
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%d', self::ENDPOINT, ServersFixture::SERVER_B_ID);
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: MISP 3.0.0 - #cc1f8cc2e89ec692168ffbfea8ed49cc879c469b',
'ETag: W/"2a-1b6e3e8e"',
];
// mock the /servers/getVersion request
$getVersionBody = json_encode(
[
"version" => "3.0.0",
"pymisp_recommended_version" => "3.0.0",
"perm_sync" => true,
"perm_sighting" => true,
"perm_galaxy_editor" => true,
"request_encoding" => [
"gzip",
"br"
],
"filter_sightings" => true
]
);
$this->mockClientGet(
ServersFixture::SERVER_B_URL . '/servers/getVersion',
$this->newClientResponse(200, $headers, $getVersionBody)
);
// mock the /events/filterEventIdsForPush request
$filterEventIdsForPushBody = json_encode(
[
EventsFixture::EVENT_1_UUID
]
);
$this->mockClientPost(
ServersFixture::SERVER_B_URL . '/events/filterEventIdsForPush',
$this->newClientResponse(200, $headers, $filterEventIdsForPushBody)
);
// mock the /events/index request, triggered by syncProposals()
$this->mockClientPost(
ServersFixture::SERVER_B_URL . '/events/index',
$this->newClientResponse(200, $headers, '[]')
);
// mock the /events/view/[uuid] request
$this->mockClientGet(
ServersFixture::SERVER_B_URL . '/events/view/' . EventsFixture::EVENT_1_UUID,
$this->newClientResponse(200, $headers, '[]')
);
// mock the /events/add/metadata:1
$this->mockClientPost(
ServersFixture::SERVER_B_URL . '/events/add/metadata:1',
$this->newClientResponse(200, $headers, '[]')
);
$this->post($url);
$this->assertResponseOk();
$response = $this->getJsonResponseAsArray();
$this->assertEquals(
'Push complete. 1 events pushed, 0 events could not be pushed.',
$response['message']
);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Servers;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\ServersFixture;
use App\Test\Helper\ApiTestTrait;
use Cake\TestSuite\TestCase;
use Cake\Http\TestSuite\HttpClientTrait;
class TestConnectionApiTest extends TestCase
{
use ApiTestTrait;
use HttpClientTrait;
protected const ENDPOINT = '/servers/testConnection';
protected $fixtures = [
'app.Organisations',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Servers',
];
public function testTestConnection(): void
{
$this->skipOpenApiValidations();
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%s', self::ENDPOINT, ServersFixture::SERVER_A_ID);
$headers = [
'Content-Type: application/json',
'Connection: close',
];
$getVersionBody = json_encode(
[
"version" => "3.0.0",
"pymisp_recommended_version" => "3.0.0",
"perm_sync" => true,
"perm_sighting" => true,
"perm_galaxy_editor" => true,
"request_encoding" => [
"gzip",
"br"
],
"filter_sightings" => true
]
);
// mock the [remote]/servers/getVersion request
$this->mockClientGet(
ServersFixture::SERVER_A_URL . '/servers/getVersion',
$this->newClientResponse(200, $headers, $getVersionBody)
);
$this->get($url);
$this->assertResponseOk();
$response = $this->getJsonResponseAsArray();
$this->assertArrayHasKey('version', $response);
$this->assertEquals('3.0.0', $response['version']);
}
}

View File

@ -48,7 +48,7 @@ xWV4oBk=
-----END CERTIFICATE-----
";
public function testGoogle($options=[])
public function testGoogle($options = [])
{
$client = new HttpTool($options);
$response = $client->get('https://www.google.com');
@ -73,7 +73,9 @@ xWV4oBk=
$config = [
'ssl_verify_peer' => true,
'ssl_verify_host' => false];
'ssl_verify_host' => false
];
$client = new HttpTool($config);
try {
@ -172,7 +174,6 @@ xWV4oBk=
$this->assertEquals($result['issuer'], 'C=US, O=Google Trust Services LLC, CN=GTS CA 1C3');
$this->assertEquals($result['public_key_size_ok'], true);
$this->assertEquals($result['valid_from_ok'], true);
$this->assertEquals($result['valid_to_ok'], true);
$this->assertEquals($result['valid_from'], new FrozenTime("2023-11-20 08:09:47.000000+00:00"));
$this->assertEquals($result['valid_to'], new FrozenTime("2024-02-12 08:09:46.000000+00:00"));
$this->assertEquals($result['signature_type'], "RSA-SHA256");
@ -189,9 +190,8 @@ xWV4oBk=
// $certificates = $client->fetchCertificates(self::HTTPS_SELF_SIGNED_URI);
// $certificates = $client->fetchCertificates('http://www.google.com');
// we get one or more certificates from the server. No function yet to select "the right one"
foreach($certificates as $certificate) {
foreach ($certificates as $certificate) {
// debug($certificate);
}
}
}