2021-04-22 09:46:10 +02:00
|
|
|
<?php
|
|
|
|
App::uses('AppModel', 'Model');
|
|
|
|
|
2021-04-29 11:41:02 +02:00
|
|
|
/**
|
|
|
|
* @property Attribute $Attribute
|
2022-08-09 15:20:53 +02:00
|
|
|
* @property Event $Event
|
|
|
|
* @property CorrelationValue $CorrelationValue
|
|
|
|
* @method saveCorrelations(array $correlations)
|
2022-09-07 18:09:47 +02:00
|
|
|
* @method createCorrelationEntry(string $value, array $a, array $b)
|
|
|
|
* @method runBeforeSaveCorrelation(array $attribute)
|
2022-08-09 15:20:53 +02:00
|
|
|
* @method fetchRelatedEventIds(array $user, int $eventId, array $sgids)
|
2022-08-09 12:16:44 +02:00
|
|
|
* @method getFieldRules
|
2022-08-09 15:20:53 +02:00
|
|
|
* @method getContainRules($filter = null)
|
2022-10-13 09:51:12 +02:00
|
|
|
* @method updateContainedCorrelations(array $data, string $type, array $options = [])
|
2022-10-19 21:16:17 +02:00
|
|
|
* @method purgeCorrelations(int $evenId = null)
|
2021-04-29 11:41:02 +02:00
|
|
|
*/
|
2021-04-22 09:46:10 +02:00
|
|
|
class Correlation extends AppModel
|
|
|
|
{
|
2021-04-29 11:41:02 +02:00
|
|
|
const CACHE_NAME = 'misp:top_correlations',
|
|
|
|
CACHE_AGE = 'misp:top_correlations_age';
|
2021-04-27 00:40:40 +02:00
|
|
|
|
2021-04-25 17:36:29 +02:00
|
|
|
public $belongsTo = array(
|
|
|
|
'Attribute' => [
|
|
|
|
'className' => 'Attribute',
|
|
|
|
'foreignKey' => 'attribute_id'
|
|
|
|
],
|
|
|
|
'Event' => array(
|
|
|
|
'className' => 'Event',
|
|
|
|
'foreignKey' => 'event_id'
|
2022-07-31 23:48:38 +02:00
|
|
|
),
|
|
|
|
'Object' => array(
|
|
|
|
'className' => 'Object',
|
|
|
|
'foreignKey' => 'object_id'
|
|
|
|
),
|
|
|
|
'CorrelationValue' => [
|
|
|
|
'className' => 'CorrelationValue',
|
|
|
|
'foreignKey' => 'value_id'
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
2022-08-03 15:44:27 +02:00
|
|
|
public $validEngines = [
|
|
|
|
'Default' => 'default_correlations',
|
|
|
|
'NoAcl' => 'no_acl_correlations',
|
|
|
|
'Legacy' => 'correlations'
|
|
|
|
];
|
|
|
|
|
2022-07-31 23:48:38 +02:00
|
|
|
public $actsAs = array(
|
|
|
|
'Containable'
|
2021-04-25 17:36:29 +02:00
|
|
|
);
|
|
|
|
|
2021-04-29 11:47:38 +02:00
|
|
|
/** @var array */
|
2021-08-12 15:05:39 +02:00
|
|
|
private $exclusions;
|
2021-04-29 11:41:02 +02:00
|
|
|
|
2022-05-09 21:53:37 +02:00
|
|
|
/** @var bool */
|
|
|
|
private $advancedCorrelationEnabled;
|
|
|
|
|
2022-05-11 11:51:53 +02:00
|
|
|
/** @var array */
|
|
|
|
private $cidrListCache;
|
|
|
|
|
2022-10-05 15:22:39 +02:00
|
|
|
private $containCache = [];
|
2022-07-31 23:48:38 +02:00
|
|
|
|
2022-08-08 18:31:00 +02:00
|
|
|
/** @var OverCorrelatingValue */
|
|
|
|
public $OverCorrelatingValue;
|
2022-07-31 23:48:38 +02:00
|
|
|
|
2021-04-29 11:47:38 +02:00
|
|
|
public function __construct($id = false, $table = null, $ds = null)
|
|
|
|
{
|
|
|
|
parent::__construct($id, $table, $ds);
|
2022-09-08 15:32:35 +02:00
|
|
|
$correlationEngine = $this->getCorrelationModelName();
|
|
|
|
$deadlockAvoidance = Configure::read('MISP.deadlock_avoidance') ?: false;
|
2022-07-31 23:48:38 +02:00
|
|
|
// load the currently used correlation engine
|
2022-09-08 15:32:35 +02:00
|
|
|
$this->Behaviors->load($correlationEngine . 'Correlation', ['deadlockAvoidance' => $deadlockAvoidance]);
|
2022-07-31 23:48:38 +02:00
|
|
|
// getTableName() needs to be implemented by the engine - this points us to the table to be used
|
|
|
|
$this->useTable = $this->getTableName();
|
2022-05-09 21:53:37 +02:00
|
|
|
$this->advancedCorrelationEnabled = (bool)Configure::read('MISP.enable_advanced_correlations');
|
2022-07-31 23:48:38 +02:00
|
|
|
// load the overcorrelatingvalue model for chaining
|
|
|
|
$this->OverCorrelatingValue = ClassRegistry::init('OverCorrelatingValue');
|
2021-04-29 11:47:38 +02:00
|
|
|
}
|
|
|
|
|
2021-04-22 09:46:10 +02:00
|
|
|
public function correlateValueRouter($value)
|
|
|
|
{
|
|
|
|
if (Configure::read('MISP.background_jobs')) {
|
2021-11-02 15:35:23 +01:00
|
|
|
|
|
|
|
/** @var Job $job */
|
|
|
|
$job = ClassRegistry::init('Job');
|
|
|
|
$jobId = $job->createJob(
|
|
|
|
'SYSTEM',
|
|
|
|
Job::WORKER_DEFAULT,
|
|
|
|
'correlateValue',
|
|
|
|
$value,
|
|
|
|
'Recorrelating'
|
2021-04-22 09:46:10 +02:00
|
|
|
);
|
2021-11-02 15:35:23 +01:00
|
|
|
|
|
|
|
$this->getBackgroundJobsTool()->enqueue(
|
|
|
|
BackgroundJobsTool::DEFAULT_QUEUE,
|
|
|
|
BackgroundJobsTool::CMD_EVENT,
|
|
|
|
[
|
|
|
|
'correlateValue',
|
|
|
|
$value,
|
|
|
|
$jobId
|
|
|
|
],
|
2021-11-02 16:25:43 +01:00
|
|
|
true,
|
|
|
|
$jobId
|
2021-04-22 09:46:10 +02:00
|
|
|
);
|
2021-11-02 15:35:23 +01:00
|
|
|
|
2021-04-22 09:46:10 +02:00
|
|
|
return true;
|
|
|
|
} else {
|
2021-04-25 17:36:29 +02:00
|
|
|
return $this->correlateValue($value);
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-19 21:16:17 +02:00
|
|
|
/**
|
|
|
|
* Generate correlation for given attributes or events.
|
|
|
|
*
|
|
|
|
* @param int|false $jobId
|
|
|
|
* @param int|false $eventId
|
|
|
|
* @param int|false $attributeId
|
|
|
|
* @return int Number of processed attributes
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function generateCorrelation($jobId = false, $eventId = false, $attributeId = false)
|
|
|
|
{
|
|
|
|
$this->purgeCorrelations($eventId);
|
|
|
|
|
|
|
|
$this->FuzzyCorrelateSsdeep = ClassRegistry::init('FuzzyCorrelateSsdeep');
|
|
|
|
$this->FuzzyCorrelateSsdeep->purge($eventId, $attributeId);
|
|
|
|
|
|
|
|
$this->OverCorrelatingValue->truncateTable();
|
|
|
|
|
|
|
|
if (!$eventId) {
|
|
|
|
$eventIds = $this->Event->find('column', [
|
|
|
|
'fields' => ['Event.id'],
|
|
|
|
'conditions' => ['Event.disable_correlation' => 0],
|
|
|
|
]);
|
|
|
|
$full = true;
|
|
|
|
} else {
|
|
|
|
$eventIds = [$eventId];
|
|
|
|
$full = false;
|
|
|
|
}
|
|
|
|
$attributeCount = 0;
|
|
|
|
if (Configure::read('MISP.background_jobs') && $jobId) {
|
|
|
|
$this->Job = ClassRegistry::init('Job');
|
|
|
|
} else {
|
|
|
|
$jobId = false;
|
|
|
|
}
|
|
|
|
if (!empty($eventIds)) {
|
|
|
|
$eventCount = count($eventIds);
|
|
|
|
foreach ($eventIds as $j => $currentEventId) {
|
|
|
|
$attributeCount += $this->__iteratedCorrelation(
|
|
|
|
$jobId,
|
|
|
|
$full,
|
|
|
|
$attributeId,
|
|
|
|
$eventCount,
|
|
|
|
$currentEventId,
|
|
|
|
$j
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($jobId) {
|
|
|
|
$this->Job->saveStatus($jobId, true);
|
|
|
|
}
|
|
|
|
return $attributeCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int|false $jobId
|
|
|
|
* @param bool $full
|
|
|
|
* @param int $attributeId
|
|
|
|
* @param int $eventCount
|
|
|
|
* @param int $eventId
|
|
|
|
* @param int $j
|
|
|
|
* @return int
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
private function __iteratedCorrelation(
|
|
|
|
$jobId = false,
|
|
|
|
$full = false,
|
|
|
|
$attributeId = null,
|
|
|
|
$eventCount = null,
|
|
|
|
$eventId = null,
|
|
|
|
$j = 0
|
|
|
|
)
|
|
|
|
{
|
|
|
|
if ($jobId) {
|
|
|
|
$message = $attributeId ? __('Correlating Attribute %s', $attributeId) : __('Correlating Event %s (%s MB used)', $eventId, intval(memory_get_usage() / 1024 / 1024));
|
|
|
|
$this->Job->saveProgress($jobId, $message, !empty($eventCount) ? ($j / $eventCount) * 100 : 0);
|
|
|
|
}
|
|
|
|
$attributeConditions = [
|
|
|
|
'Attribute.deleted' => 0,
|
|
|
|
'Attribute.disable_correlation' => 0,
|
|
|
|
'NOT' => [
|
|
|
|
'Attribute.type' => Attribute::NON_CORRELATING_TYPES,
|
|
|
|
],
|
|
|
|
];
|
|
|
|
if ($eventId) {
|
|
|
|
$attributeConditions['Attribute.event_id'] = $eventId;
|
|
|
|
$event = $this->Event->find('first', [
|
|
|
|
'conditions' => ['Event.id' => $eventId],
|
|
|
|
'recursive' => -1,
|
|
|
|
'fields' => $this->getContainRules('Event')['fields'],
|
|
|
|
]);
|
|
|
|
if (empty($event)) {
|
|
|
|
return 0; // event not found, skip
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$event = false;
|
|
|
|
}
|
|
|
|
if ($attributeId) {
|
|
|
|
$attributeConditions['Attribute.id'] = $attributeId;
|
|
|
|
}
|
|
|
|
$query = [
|
|
|
|
'recursive' => -1,
|
|
|
|
'conditions' => $attributeConditions,
|
|
|
|
// fetch just necessary fields to save memory
|
|
|
|
'fields' => $this->getFieldRules(),
|
|
|
|
'order' => 'Attribute.id',
|
|
|
|
'limit' => 5000,
|
|
|
|
'callbacks' => false, // memory leak fix
|
|
|
|
];
|
|
|
|
$attributeCount = 0;
|
|
|
|
do {
|
|
|
|
$attributes = $this->Attribute->find('all', $query);
|
|
|
|
foreach ($attributes as $attribute) {
|
|
|
|
$this->afterSaveCorrelation($attribute['Attribute'], $full, $event);
|
|
|
|
}
|
|
|
|
$fetchedAttributes = count($attributes);
|
|
|
|
unset($attributes);
|
|
|
|
$attributeCount += $fetchedAttributes;
|
|
|
|
if ($fetchedAttributes === 5000) { // maximum number of attributes fetched, continue in next loop
|
|
|
|
$query['conditions']['Attribute.id >'] = $attribute['Attribute']['id'];
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} while (true);
|
|
|
|
|
|
|
|
// Generating correlations can take long time, so clear caches after each event to refresh them
|
|
|
|
$this->cidrListCache = null;
|
|
|
|
$this->OverCorrelatingValue->cleanCache();
|
|
|
|
$this->containCache = [];
|
|
|
|
|
|
|
|
return $attributeCount;
|
|
|
|
}
|
|
|
|
|
2022-05-09 16:44:44 +02:00
|
|
|
/**
|
2022-05-09 17:15:56 +02:00
|
|
|
* @param array $attribute Simple attribute array
|
2022-05-09 16:44:44 +02:00
|
|
|
* @return array|null
|
|
|
|
*/
|
2022-05-09 17:15:56 +02:00
|
|
|
private function __buildAdvancedCorrelationConditions($attribute)
|
2021-04-22 09:46:10 +02:00
|
|
|
{
|
2022-05-09 21:53:37 +02:00
|
|
|
if (!$this->advancedCorrelationEnabled) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-07-31 23:48:38 +02:00
|
|
|
if (in_array($attribute['Attribute']['type'], ['ip-src', 'ip-dst', 'ip-src|port', 'ip-dst|port'], true)) {
|
2022-05-09 21:53:37 +02:00
|
|
|
return $this->cidrCorrelation($attribute);
|
2022-07-31 23:48:38 +02:00
|
|
|
} else if ($attribute['Attribute']['type'] === 'ssdeep' && function_exists('ssdeep_fuzzy_compare')) {
|
2022-05-09 21:53:37 +02:00
|
|
|
return $this->ssdeepCorrelation($attribute);
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|
2022-05-09 21:53:37 +02:00
|
|
|
return null;
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|
|
|
|
|
2021-04-25 17:36:29 +02:00
|
|
|
private function __addAdvancedCorrelations($correlatingAttribute)
|
|
|
|
{
|
2022-05-09 21:53:37 +02:00
|
|
|
if (!$this->advancedCorrelationEnabled) {
|
2021-04-25 17:36:29 +02:00
|
|
|
return [];
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
$extraConditions = $this->__buildAdvancedCorrelationConditions($correlatingAttribute);
|
2021-04-25 17:36:29 +02:00
|
|
|
if (empty($extraConditions)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return $this->Attribute->find('all', [
|
|
|
|
'conditions' => [
|
|
|
|
'AND' => $extraConditions,
|
|
|
|
'NOT' => [
|
2021-07-27 15:19:41 +02:00
|
|
|
'Attribute.type' => Attribute::NON_CORRELATING_TYPES,
|
2021-04-25 17:36:29 +02:00
|
|
|
],
|
|
|
|
'Attribute.disable_correlation' => 0,
|
|
|
|
'Event.disable_correlation' => 0,
|
|
|
|
'Attribute.deleted' => 0
|
|
|
|
],
|
|
|
|
'recursive' => -1,
|
2022-07-31 23:48:38 +02:00
|
|
|
'fields' => $this->getFieldRules(),
|
|
|
|
'contain' => $this->getContainRules(),
|
2021-04-25 17:36:29 +02:00
|
|
|
'order' => [],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function __getMatchingAttributes($value)
|
2021-04-22 09:46:10 +02:00
|
|
|
{
|
2022-07-31 23:48:38 +02:00
|
|
|
// stupid hack to allow statically retrieving the constants
|
|
|
|
ClassRegistry::init('Attribute');
|
2021-04-22 09:46:10 +02:00
|
|
|
$conditions = [
|
2021-04-25 17:36:29 +02:00
|
|
|
'OR' => [
|
|
|
|
'Attribute.value1' => $value,
|
|
|
|
'AND' => [
|
|
|
|
'Attribute.value2' => $value,
|
2021-07-21 08:42:05 +02:00
|
|
|
'NOT' => ['Attribute.type' => Attribute::PRIMARY_ONLY_CORRELATING_TYPES]
|
2021-04-25 17:36:29 +02:00
|
|
|
]
|
|
|
|
],
|
2021-04-22 09:46:10 +02:00
|
|
|
'NOT' => [
|
2021-07-27 15:19:41 +02:00
|
|
|
'Attribute.type' => Attribute::NON_CORRELATING_TYPES,
|
2021-04-22 09:46:10 +02:00
|
|
|
],
|
|
|
|
'Attribute.disable_correlation' => 0,
|
|
|
|
'Event.disable_correlation' => 0,
|
|
|
|
'Attribute.deleted' => 0
|
|
|
|
];
|
2021-04-25 17:36:29 +02:00
|
|
|
$correlatingAttributes = $this->Attribute->find('all', [
|
2021-04-22 09:46:10 +02:00
|
|
|
'conditions' => $conditions,
|
|
|
|
'recursive' => -1,
|
2022-07-31 23:48:38 +02:00
|
|
|
'fields' => $this->getFieldRules(),
|
|
|
|
'contain' => $this->getContainRules(),
|
2021-04-22 09:46:10 +02:00
|
|
|
'order' => [],
|
|
|
|
]);
|
2021-04-25 17:36:29 +02:00
|
|
|
return $correlatingAttributes;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function correlateValue($value, $jobId = false)
|
|
|
|
{
|
|
|
|
$correlatingAttributes = $this->__getMatchingAttributes($value);
|
2021-04-22 09:46:10 +02:00
|
|
|
$count = count($correlatingAttributes);
|
|
|
|
$correlations = [];
|
2021-04-25 17:36:29 +02:00
|
|
|
if ($jobId) {
|
|
|
|
if (empty($this->Job)) {
|
|
|
|
$this->Job = ClassRegistry::init('Job');
|
|
|
|
}
|
|
|
|
$job = $this->Job->find('first', [
|
|
|
|
'recursive' => -1,
|
|
|
|
'conditions' => ['id' => $jobId]
|
|
|
|
]);
|
|
|
|
if (empty($job)) {
|
|
|
|
$jobId = false;
|
|
|
|
}
|
|
|
|
}
|
2021-04-22 09:46:10 +02:00
|
|
|
foreach ($correlatingAttributes as $k => $correlatingAttribute) {
|
2021-04-29 11:41:02 +02:00
|
|
|
foreach ($correlatingAttributes as $correlatingAttribute2) {
|
2022-05-10 20:30:35 +02:00
|
|
|
if ($correlatingAttribute['Attribute']['event_id'] === $correlatingAttribute2['Attribute']['event_id']) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-09-07 18:09:47 +02:00
|
|
|
$correlations[] = $this->createCorrelationEntry($value, $correlatingAttribute, $correlatingAttribute2);
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
|
|
|
$extraCorrelations = $this->__addAdvancedCorrelations($correlatingAttribute);
|
|
|
|
if (!empty($extraCorrelations)) {
|
2021-04-29 11:41:02 +02:00
|
|
|
foreach ($extraCorrelations as $extraCorrelation) {
|
2022-05-10 20:30:35 +02:00
|
|
|
if ($correlatingAttribute['Attribute']['event_id'] === $extraCorrelation['Attribute']['event_id']) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-09-07 18:09:47 +02:00
|
|
|
$correlations[] = $this->createCorrelationEntry($value, $correlatingAttribute, $extraCorrelation);
|
|
|
|
//$correlations = $this->createCorrelationEntry($value, $extraCorrelation, $correlatingAttribute, $correlations);
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-25 17:36:29 +02:00
|
|
|
if ($jobId && $k % 100 === 0) {
|
2021-04-29 11:41:02 +02:00
|
|
|
$this->Job->saveProgress($jobId, __('Correlating Attributes based on value. %s attributes correlated out of %s.', $k, $count), floor(100 * $k / $count));
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|
2022-05-12 09:34:28 +02:00
|
|
|
if (empty($correlations)) {
|
|
|
|
return true;
|
|
|
|
}
|
2021-04-25 17:36:29 +02:00
|
|
|
return $this->__saveCorrelations($correlations);
|
|
|
|
}
|
2021-04-22 09:46:10 +02:00
|
|
|
|
2022-05-09 16:44:44 +02:00
|
|
|
/**
|
|
|
|
* @param array $correlations
|
|
|
|
* @return array|bool|bool[]|mixed
|
|
|
|
*/
|
2022-07-31 23:48:38 +02:00
|
|
|
private function __saveCorrelations(array $correlations)
|
2021-04-25 17:36:29 +02:00
|
|
|
{
|
2022-08-09 14:41:59 +02:00
|
|
|
try {
|
|
|
|
return $this->saveCorrelations($correlations);
|
|
|
|
} catch (Exception $e) {
|
|
|
|
// Correlations may fail for different reasons, such as the correlation already existing. We don't care and don't want to break the process
|
|
|
|
return true;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2021-04-29 11:47:38 +02:00
|
|
|
|
2022-08-08 16:02:50 +02:00
|
|
|
public function beforeSaveCorrelation(array $attribute)
|
2021-04-25 17:36:29 +02:00
|
|
|
{
|
2022-07-31 23:48:38 +02:00
|
|
|
$this->runBeforeSaveCorrelation($attribute);
|
|
|
|
}
|
|
|
|
|
2022-09-07 18:09:47 +02:00
|
|
|
/**
|
|
|
|
* @param string $scope
|
|
|
|
* @param int $id
|
2022-10-05 15:22:39 +02:00
|
|
|
* @return bool|array Returns array if object is found, false if object doesn't exists or true if object is not required
|
2022-09-07 18:09:47 +02:00
|
|
|
*/
|
2022-08-03 15:44:27 +02:00
|
|
|
private function __cachedGetContainData($scope, $id)
|
2022-07-31 23:48:38 +02:00
|
|
|
{
|
2022-10-05 15:22:39 +02:00
|
|
|
$rules = $this->getContainRules($scope);
|
|
|
|
if (!empty($rules)) {
|
|
|
|
if (!isset($this->containCache[$scope][$id])) {
|
2022-07-31 23:48:38 +02:00
|
|
|
$temp = $this->Attribute->$scope->find('first', array(
|
|
|
|
'recursive' => -1,
|
2022-10-05 15:22:39 +02:00
|
|
|
'fields' => $rules['fields'],
|
2022-07-31 23:48:38 +02:00
|
|
|
'conditions' => ['id' => $id],
|
|
|
|
'order' => array(),
|
|
|
|
));
|
2022-10-05 15:22:39 +02:00
|
|
|
if (empty($temp)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return $this->containCache[$scope][$id] = $temp[$scope];
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2022-10-05 15:22:39 +02:00
|
|
|
return $this->containCache[$scope][$id];
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
2022-10-05 15:22:39 +02:00
|
|
|
|
|
|
|
return true;
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
|
|
|
|
2022-05-09 16:44:44 +02:00
|
|
|
/**
|
|
|
|
* @param array $a
|
|
|
|
* @param bool $full
|
|
|
|
* @param array|false $event
|
|
|
|
* @return array|bool|bool[]|mixed
|
2022-08-08 18:31:00 +02:00
|
|
|
* @throws Exception
|
2022-05-09 16:44:44 +02:00
|
|
|
*/
|
2021-04-25 17:36:29 +02:00
|
|
|
public function afterSaveCorrelation($a, $full = false, $event = false)
|
|
|
|
{
|
2022-07-31 23:48:38 +02:00
|
|
|
$a = ['Attribute' => $a];
|
|
|
|
if (!empty($a['Attribute']['disable_correlation']) || Configure::read('MISP.completely_disable_correlation')) {
|
2021-04-25 17:36:29 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// Don't do any correlation if the type is a non correlating type
|
2022-07-31 23:48:38 +02:00
|
|
|
if (in_array($a['Attribute']['type'], Attribute::NON_CORRELATING_TYPES, true)) {
|
2021-04-25 17:36:29 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!$event) {
|
2022-07-31 23:48:38 +02:00
|
|
|
$a['Event'] = $this->__cachedGetContainData('Event', $a['Attribute']['event_id']);
|
|
|
|
if (!$a['Event']) {
|
|
|
|
// orphaned attribute, do not correlate
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$a['Event'] = $event['Event'];
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
if (!empty($a['Event']['disable_correlation'])) {
|
2021-04-25 17:36:29 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// generate additional correlating attribute list based on the advanced correlations
|
2022-07-31 23:48:38 +02:00
|
|
|
if (!$this->__preventExcludedCorrelations($a['Attribute']['value1'])) {
|
2022-05-09 16:54:10 +02:00
|
|
|
$extraConditions = $this->__buildAdvancedCorrelationConditions($a);
|
2022-07-31 23:48:38 +02:00
|
|
|
$correlatingValues = [$a['Attribute']['value1']];
|
2022-05-09 16:54:10 +02:00
|
|
|
} else {
|
|
|
|
$extraConditions = null;
|
2022-07-31 23:48:38 +02:00
|
|
|
$correlatingValues = [];
|
2022-05-09 16:54:10 +02:00
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
if (!empty($a['Attribute']['value2']) && !in_array($a['Attribute']['type'], Attribute::PRIMARY_ONLY_CORRELATING_TYPES, true) && !$this->__preventExcludedCorrelations($a['Attribute']['value2'])) {
|
|
|
|
$correlatingValues[] = $a['Attribute']['value2'];
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
2022-05-09 16:54:10 +02:00
|
|
|
if (empty($correlatingValues)) {
|
|
|
|
return true;
|
|
|
|
}
|
2022-10-21 16:18:35 +02:00
|
|
|
if (!empty($a['Attribute']['object_id'])) {
|
|
|
|
$a['Object'] = $this->__cachedGetContainData('Object', $a['Attribute']['object_id']);
|
|
|
|
if (!$a['Object']) {
|
|
|
|
// orphaned attribute, do not correlate
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2022-05-14 11:37:33 +02:00
|
|
|
$correlations = [];
|
2022-08-08 18:31:00 +02:00
|
|
|
foreach ($correlatingValues as $cV) {
|
2022-05-09 17:14:54 +02:00
|
|
|
if ($cV === null) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-10-19 21:16:17 +02:00
|
|
|
if ($full && $this->OverCorrelatingValue->isBlocked($cV)) {
|
|
|
|
continue; // skip already blocked values when doing full correlation
|
|
|
|
}
|
2021-04-25 17:36:29 +02:00
|
|
|
$conditions = [
|
|
|
|
'OR' => [
|
|
|
|
'Attribute.value1' => $cV,
|
|
|
|
'AND' => [
|
|
|
|
'Attribute.value2' => $cV,
|
2021-07-21 08:42:05 +02:00
|
|
|
'NOT' => ['Attribute.type' => Attribute::PRIMARY_ONLY_CORRELATING_TYPES]
|
2022-05-09 16:44:44 +02:00
|
|
|
],
|
2022-08-08 18:31:00 +02:00
|
|
|
$extraConditions,
|
2021-04-25 17:36:29 +02:00
|
|
|
],
|
|
|
|
'NOT' => [
|
2022-07-31 23:48:38 +02:00
|
|
|
'Attribute.event_id' => $a['Attribute']['event_id'],
|
2021-07-27 15:19:41 +02:00
|
|
|
'Attribute.type' => Attribute::NON_CORRELATING_TYPES,
|
2021-04-25 17:36:29 +02:00
|
|
|
],
|
|
|
|
'Attribute.disable_correlation' => 0,
|
|
|
|
'Event.disable_correlation' => 0,
|
2022-05-09 16:44:44 +02:00
|
|
|
'Attribute.deleted' => 0,
|
2021-04-25 17:36:29 +02:00
|
|
|
];
|
2022-08-03 15:44:27 +02:00
|
|
|
$correlationLimit = $this->OverCorrelatingValue->getLimit();
|
|
|
|
|
2022-05-14 12:26:45 +02:00
|
|
|
$correlatingAttributes = $this->Attribute->find('all', [
|
2021-04-25 17:36:29 +02:00
|
|
|
'conditions' => $conditions,
|
|
|
|
'recursive' => -1,
|
2022-07-31 23:48:38 +02:00
|
|
|
'fields' => $this->getFieldRules(),
|
|
|
|
'contain' => $this->getContainRules(),
|
2022-05-09 17:45:15 +02:00
|
|
|
'order' => [],
|
|
|
|
'callbacks' => 'before', // memory leak fix
|
2022-08-03 15:44:27 +02:00
|
|
|
// let's fetch the limit +1 - still allows us to detect overcorrelations, but we'll also never need more
|
|
|
|
'limit' => empty($correlationLimit) ? null : ($correlationLimit+1)
|
2022-05-14 12:26:45 +02:00
|
|
|
]);
|
2022-08-03 15:44:27 +02:00
|
|
|
|
2022-07-31 23:48:38 +02:00
|
|
|
// Let's check if we don't have a case of an over-correlating attribute
|
|
|
|
$count = count($correlatingAttributes);
|
|
|
|
if ($count > $correlationLimit) {
|
|
|
|
// If we have more correlations for the value than the limit, set the block entry and stop the correlation process
|
2022-08-10 14:17:20 +02:00
|
|
|
$this->OverCorrelatingValue->block($cV);
|
2022-07-31 23:48:38 +02:00
|
|
|
return true;
|
2022-09-08 12:00:02 +02:00
|
|
|
} else if ($count !== 0) {
|
2022-07-31 23:48:38 +02:00
|
|
|
// If we have fewer hits than the limit, proceed with the correlation, but first make sure we remove any existing blockers
|
|
|
|
$this->OverCorrelatingValue->unblock($cV);
|
|
|
|
}
|
|
|
|
foreach ($correlatingAttributes as $b) {
|
2022-08-09 14:41:59 +02:00
|
|
|
// On a full correlation, only correlate with attributes that have a higher ID to avoid duplicate correlations
|
2022-09-08 09:45:02 +02:00
|
|
|
if ($full && $a['Attribute']['id'] < $b['Attribute']['id']) {
|
2022-08-09 14:41:59 +02:00
|
|
|
continue;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
if (isset($b['Attribute']['value1'])) {
|
2022-05-14 12:26:45 +02:00
|
|
|
// TODO: Currently it is hard to check if value1 or value2 correlated, so we check value2 and if not, it is value1
|
2022-07-31 23:48:38 +02:00
|
|
|
$value = $cV === $b['Attribute']['value2'] ? $b['Attribute']['value2'] : $b['Attribute']['value1'];
|
2022-05-14 12:26:45 +02:00
|
|
|
} else {
|
|
|
|
$value = $cV;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
if ($a['Attribute']['id'] > $b['Attribute']['id']) {
|
2022-09-07 18:09:47 +02:00
|
|
|
$correlations[] = $this->createCorrelationEntry($value, $a, $b);
|
2022-07-31 23:48:38 +02:00
|
|
|
} else {
|
2022-09-07 18:09:47 +02:00
|
|
|
$correlations[] = $this->createCorrelationEntry($value, $b, $a);
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
|
|
|
}
|
2022-05-12 09:34:28 +02:00
|
|
|
if (empty($correlations)) {
|
|
|
|
return true;
|
|
|
|
}
|
2021-04-25 17:36:29 +02:00
|
|
|
return $this->__saveCorrelations($correlations);
|
|
|
|
}
|
|
|
|
|
2021-08-12 15:05:39 +02:00
|
|
|
/**
|
2022-05-09 16:54:10 +02:00
|
|
|
* @param string $value
|
2021-08-12 15:05:39 +02:00
|
|
|
* @return bool True if attribute value is excluded
|
|
|
|
*/
|
2022-05-09 16:54:10 +02:00
|
|
|
private function __preventExcludedCorrelations($value)
|
2021-04-25 17:36:29 +02:00
|
|
|
{
|
2021-08-12 15:05:39 +02:00
|
|
|
if ($this->exclusions === null) {
|
2021-04-25 17:36:29 +02:00
|
|
|
try {
|
2022-10-08 18:16:54 +02:00
|
|
|
$this->exclusions = RedisTool::init()->sMembers('misp:correlation_exclusions');
|
2021-04-25 17:36:29 +02:00
|
|
|
} catch (Exception $e) {
|
2021-04-29 11:41:02 +02:00
|
|
|
return false;
|
2021-04-25 17:36:29 +02:00
|
|
|
}
|
2021-08-12 15:05:39 +02:00
|
|
|
} else if (empty($this->exclusions)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-25 17:36:29 +02:00
|
|
|
foreach ($this->exclusions as $exclusion) {
|
|
|
|
if (!empty($exclusion)) {
|
|
|
|
$firstChar = $exclusion[0];
|
|
|
|
$lastChar = substr($exclusion, -1);
|
|
|
|
if ($firstChar === '%' && $lastChar === '%') {
|
|
|
|
$exclusion = substr($exclusion, 1, -1);
|
|
|
|
if (strpos($value, $exclusion) !== false) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else if ($firstChar === '%') {
|
|
|
|
$exclusion = substr($exclusion, 1);
|
|
|
|
if (substr($value, -strlen($exclusion)) === $exclusion) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else if ($lastChar === '%') {
|
|
|
|
$exclusion = substr($exclusion, 0, -1);
|
|
|
|
if (substr($value, 0, strlen($exclusion)) === $exclusion) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ($value === $exclusion) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-05-09 17:15:56 +02:00
|
|
|
/**
|
|
|
|
* @param array $attribute Simple attribute array
|
|
|
|
* @return array[]|false
|
|
|
|
*/
|
|
|
|
private function ssdeepCorrelation($attribute)
|
2021-04-25 18:09:37 +02:00
|
|
|
{
|
2021-09-03 16:20:14 +02:00
|
|
|
if (!isset($this->FuzzyCorrelateSsdeep)) {
|
2021-04-25 18:09:37 +02:00
|
|
|
$this->FuzzyCorrelateSsdeep = ClassRegistry::init('FuzzyCorrelateSsdeep');
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
$value = $attribute['Attribute']['value1'];
|
|
|
|
$fuzzyIds = $this->FuzzyCorrelateSsdeep->query_ssdeep_chunks($value, $attribute['Attribute']['id']);
|
2021-04-25 18:09:37 +02:00
|
|
|
if (!empty($fuzzyIds)) {
|
|
|
|
$ssdeepIds = $this->Attribute->find('list', array(
|
|
|
|
'recursive' => -1,
|
|
|
|
'conditions' => array(
|
|
|
|
'Attribute.type' => 'ssdeep',
|
|
|
|
'Attribute.id' => $fuzzyIds
|
|
|
|
),
|
|
|
|
'fields' => array('Attribute.id', 'Attribute.value1')
|
|
|
|
));
|
|
|
|
$threshold = Configure::read('MISP.ssdeep_correlation_threshold') ?: 40;
|
2022-05-09 17:15:56 +02:00
|
|
|
$attributeIds = [];
|
2021-04-25 18:09:37 +02:00
|
|
|
foreach ($ssdeepIds as $attributeId => $v) {
|
2022-05-09 17:15:56 +02:00
|
|
|
$ssdeepValue = ssdeep_fuzzy_compare($value, $v);
|
|
|
|
if ($ssdeepValue >= $threshold) {
|
2021-04-25 18:09:37 +02:00
|
|
|
$attributeIds[] = $attributeId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ['Attribute.id' => $attributeIds];
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-05-09 16:44:44 +02:00
|
|
|
/**
|
2022-05-09 17:15:56 +02:00
|
|
|
* @param array $attribute Simple attribute array
|
2022-05-09 16:44:44 +02:00
|
|
|
* @return array|array[][]
|
|
|
|
*/
|
2022-05-09 17:15:56 +02:00
|
|
|
private function cidrCorrelation($attribute)
|
2021-04-25 17:36:29 +02:00
|
|
|
{
|
|
|
|
$ipValues = array();
|
2022-07-31 23:48:38 +02:00
|
|
|
$ip = $attribute['Attribute']['value1'];
|
2021-04-25 17:36:29 +02:00
|
|
|
if (strpos($ip, '/') !== false) { // IP is CIDR
|
|
|
|
list($networkIp, $mask) = explode('/', $ip);
|
|
|
|
$ip_version = filter_var($networkIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 4 : 6;
|
|
|
|
|
|
|
|
$conditions = array(
|
|
|
|
'type' => array('ip-src', 'ip-dst', 'ip-src|port', 'ip-dst|port'),
|
|
|
|
'value1 NOT LIKE' => '%/%', // do not return CIDR, just plain IPs
|
|
|
|
'disable_correlation' => 0,
|
|
|
|
'deleted' => 0,
|
|
|
|
);
|
|
|
|
|
2022-05-14 10:17:06 +02:00
|
|
|
if ($this->isMysql()) {
|
2021-04-25 17:36:29 +02:00
|
|
|
// Massive speed up for CIDR correlation. Instead of testing all in PHP, database can do that work much
|
|
|
|
// faster. But these methods are just supported by MySQL.
|
|
|
|
if ($ip_version === 4) {
|
|
|
|
$startIp = ip2long($networkIp) & ((-1 << (32 - $mask)));
|
|
|
|
$endIp = $startIp + pow(2, (32 - $mask)) - 1;
|
|
|
|
// Just fetch IP address that fit in CIDR range.
|
|
|
|
$conditions['INET_ATON(value1) BETWEEN ? AND ?'] = array($startIp, $endIp);
|
|
|
|
|
2021-04-25 17:54:53 +02:00
|
|
|
// Just fetch IPv4 address that starts with given prefix. This is fast, because value1 is indexed.
|
|
|
|
// This optimisation is possible just to mask bigger than 8 bites.
|
2021-04-25 17:36:29 +02:00
|
|
|
if ($mask >= 8) {
|
|
|
|
$ipv4Parts = explode('.', $networkIp);
|
|
|
|
$ipv4Parts = array_slice($ipv4Parts, 0, intval($mask / 8));
|
|
|
|
$prefix = implode('.', $ipv4Parts);
|
|
|
|
$conditions['value1 LIKE'] = $prefix . '%';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$conditions[] = 'IS_IPV6(value1)';
|
|
|
|
// Just fetch IPv6 address that starts with given prefix. This is fast, because value1 is indexed.
|
|
|
|
if ($mask >= 16) {
|
|
|
|
$ipv6Parts = explode(':', rtrim($networkIp, ':'));
|
|
|
|
$ipv6Parts = array_slice($ipv6Parts, 0, intval($mask / 16));
|
|
|
|
$prefix = implode(':', $ipv6Parts);
|
|
|
|
$conditions['value1 LIKE'] = $prefix . '%';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-09 22:18:44 +02:00
|
|
|
$ipList = $this->Attribute->find('column', [
|
2021-04-25 17:36:29 +02:00
|
|
|
'conditions' => $conditions,
|
|
|
|
'fields' => ['Attribute.value1'],
|
|
|
|
'unique' => true,
|
|
|
|
'order' => false,
|
2022-05-09 22:18:44 +02:00
|
|
|
'callbacks' => false,
|
|
|
|
]);
|
2021-04-25 17:36:29 +02:00
|
|
|
foreach ($ipList as $ipToCheck) {
|
|
|
|
$ipToCheckVersion = filter_var($ipToCheck, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 4 : 6;
|
|
|
|
if ($ipToCheckVersion === $ip_version) {
|
|
|
|
if ($ip_version === 4) {
|
|
|
|
if ($this->__ipv4InCidr($ipToCheck, $ip)) {
|
|
|
|
$ipValues[] = $ipToCheck;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ($this->__ipv6InCidr($ipToCheck, $ip)) {
|
|
|
|
$ipValues[] = $ipToCheck;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$ip_version = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 4 : 6;
|
2022-05-11 11:51:53 +02:00
|
|
|
$cidrList = $this->getCidrList();
|
2021-04-25 17:36:29 +02:00
|
|
|
foreach ($cidrList as $cidr) {
|
|
|
|
if (strpos($cidr, '.') !== false) {
|
|
|
|
if ($ip_version === 4 && $this->__ipv4InCidr($ip, $cidr)) {
|
|
|
|
$ipValues[] = $cidr;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ($ip_version === 6 && $this->__ipv6InCidr($ip, $cidr)) {
|
|
|
|
$ipValues[] = $cidr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$extraConditions = array();
|
|
|
|
if (!empty($ipValues)) {
|
|
|
|
$extraConditions = array('OR' => array(
|
|
|
|
'Attribute.value1' => $ipValues,
|
|
|
|
'Attribute.value2' => $ipValues
|
|
|
|
));
|
|
|
|
}
|
|
|
|
return $extraConditions;
|
|
|
|
}
|
|
|
|
|
|
|
|
// using Alnitak's solution from http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5
|
|
|
|
private function __ipv4InCidr($ip, $cidr)
|
|
|
|
{
|
|
|
|
list($subnet, $bits) = explode('/', $cidr);
|
|
|
|
$ip = ip2long($ip);
|
|
|
|
$subnet = ip2long($subnet);
|
|
|
|
$mask = -1 << (32 - $bits);
|
|
|
|
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
|
|
|
|
return ($ip & $mask) == $subnet;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php
|
|
|
|
private function __ipv6InCidr($ip, $cidr)
|
|
|
|
{
|
|
|
|
list($address, $netmask) = explode('/', $cidr);
|
|
|
|
|
|
|
|
$bytesAddr = unpack('n*', inet_pton($address));
|
|
|
|
$bytesTest = unpack('n*', inet_pton($ip));
|
|
|
|
|
|
|
|
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
|
|
|
|
$left = $netmask - 16 * ($i - 1);
|
|
|
|
$left = ($left <= 16) ? $left : 16;
|
|
|
|
$mask = ~(0xffff >> $left) & 0xffff;
|
|
|
|
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2021-04-27 00:40:40 +02:00
|
|
|
|
2021-04-30 15:56:07 +02:00
|
|
|
/**
|
|
|
|
* @return int|bool
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
2021-04-27 00:40:40 +02:00
|
|
|
public function generateTopCorrelationsRouter()
|
|
|
|
{
|
|
|
|
if (Configure::read('MISP.background_jobs')) {
|
2021-11-02 15:35:23 +01:00
|
|
|
/** @var Job $job */
|
|
|
|
$job = ClassRegistry::init('Job');
|
|
|
|
$jobId = $job->createJob(
|
|
|
|
'SYSTEM',
|
|
|
|
Job::WORKER_DEFAULT,
|
|
|
|
'generateTopCorrelations',
|
|
|
|
'',
|
|
|
|
'Starting generation of top correlations.'
|
2021-04-27 00:40:40 +02:00
|
|
|
);
|
2021-11-02 15:35:23 +01:00
|
|
|
|
|
|
|
$this->getBackgroundJobsTool()->enqueue(
|
|
|
|
BackgroundJobsTool::DEFAULT_QUEUE,
|
|
|
|
BackgroundJobsTool::CMD_EVENT,
|
|
|
|
[
|
|
|
|
'generateTopCorrelations',
|
|
|
|
$jobId
|
|
|
|
],
|
2021-11-02 16:25:43 +01:00
|
|
|
true,
|
|
|
|
$jobId
|
2021-04-27 00:40:40 +02:00
|
|
|
);
|
2021-11-02 15:35:23 +01:00
|
|
|
|
2021-04-30 15:56:07 +02:00
|
|
|
return $jobId;
|
2021-04-27 00:40:40 +02:00
|
|
|
} else {
|
|
|
|
return $this->generateTopCorrelations();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function generateTopCorrelations($jobId = false)
|
|
|
|
{
|
|
|
|
try {
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis = RedisTool::init();
|
2021-04-27 00:40:40 +02:00
|
|
|
} catch (Exception $e) {
|
|
|
|
throw new NotFoundException(__('No redis connection found.'));
|
|
|
|
}
|
2022-05-09 14:34:38 +02:00
|
|
|
$maxId = $this->find('first', [
|
2021-04-27 00:40:40 +02:00
|
|
|
'fields' => ['MAX(id) AS max_id'],
|
2022-05-09 14:34:38 +02:00
|
|
|
'recursive' => -1,
|
2021-04-27 00:40:40 +02:00
|
|
|
]);
|
2022-05-09 14:34:38 +02:00
|
|
|
if (empty($maxId)) {
|
2021-04-27 00:40:40 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ($jobId) {
|
|
|
|
if (empty($this->Job)) {
|
|
|
|
$this->Job = ClassRegistry::init('Job');
|
|
|
|
}
|
|
|
|
$job = $this->Job->find('first', [
|
|
|
|
'recursive' => -1,
|
|
|
|
'conditions' => ['id' => $jobId]
|
|
|
|
]);
|
|
|
|
if (empty($job)) {
|
|
|
|
$jobId = false;
|
|
|
|
}
|
|
|
|
}
|
2022-05-09 14:34:38 +02:00
|
|
|
$maxId = $maxId[0]['max_id'];
|
2021-04-27 00:40:40 +02:00
|
|
|
|
2022-10-01 09:48:13 +02:00
|
|
|
RedisTool::unlink($redis, self::CACHE_NAME);
|
2021-04-29 11:41:02 +02:00
|
|
|
$redis->set(self::CACHE_AGE, time());
|
2022-05-09 14:34:38 +02:00
|
|
|
$chunkSize = 1000000;
|
|
|
|
$maxPage = ceil($maxId / $chunkSize);
|
|
|
|
for ($page = 0; $page < $maxPage; $page++) {
|
2021-04-27 00:40:40 +02:00
|
|
|
$correlations = $this->find('column', [
|
2022-07-31 23:48:38 +02:00
|
|
|
'fields' => ['value_id'],
|
2021-04-27 00:40:40 +02:00
|
|
|
'conditions' => [
|
2022-05-09 14:34:38 +02:00
|
|
|
'id >' => $page * $chunkSize,
|
|
|
|
'id <=' => ($page + 1) * $chunkSize
|
2022-05-09 14:59:07 +02:00
|
|
|
],
|
|
|
|
'callbacks' => false, // when callbacks are enabled, memory is leaked
|
2021-04-27 00:40:40 +02:00
|
|
|
]);
|
|
|
|
$newElements = count($correlations);
|
|
|
|
$correlations = array_count_values($correlations);
|
2021-04-29 11:41:02 +02:00
|
|
|
$pipeline = $redis->pipeline();
|
2021-04-27 00:40:40 +02:00
|
|
|
foreach ($correlations as $correlation => $count) {
|
2022-05-09 16:44:44 +02:00
|
|
|
$pipeline->zIncrBy(self::CACHE_NAME, $count, $correlation);
|
2021-04-27 00:40:40 +02:00
|
|
|
}
|
|
|
|
$pipeline->exec();
|
|
|
|
if ($jobId) {
|
2022-05-09 14:34:38 +02:00
|
|
|
$this->Job->saveProgress($jobId, __('Generating top correlations. Processed %s IDs.', ($page * $chunkSize) + $newElements), floor(100 * $page / $maxPage));
|
2021-04-27 00:40:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-08-08 16:02:50 +02:00
|
|
|
public function findTop(array $query)
|
2021-04-27 00:40:40 +02:00
|
|
|
{
|
|
|
|
try {
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis = RedisTool::init();
|
2021-04-27 00:40:40 +02:00
|
|
|
} catch (Exception $e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$start = $query['limit'] * ($query['page'] -1);
|
2022-07-20 15:56:30 +02:00
|
|
|
$end = $query['limit'] * $query['page'] - 1;
|
2021-04-29 11:41:02 +02:00
|
|
|
$list = $redis->zRevRange(self::CACHE_NAME, $start, $end, true);
|
2021-04-27 00:40:40 +02:00
|
|
|
$results = [];
|
|
|
|
foreach ($list as $value => $count) {
|
2022-07-31 23:48:38 +02:00
|
|
|
$realValue = $this->CorrelationValue->find('first',
|
|
|
|
[
|
|
|
|
'recursive' => -1,
|
|
|
|
'conditions' => ['CorrelationValue.id' => $value],
|
|
|
|
'fields' => 'CorrelationValue.value'
|
|
|
|
]
|
|
|
|
);
|
2021-04-27 00:40:40 +02:00
|
|
|
$results[] = [
|
|
|
|
'Correlation' => [
|
2022-07-31 23:48:38 +02:00
|
|
|
'value' => $realValue['CorrelationValue']['value'],
|
2021-04-27 08:41:41 +02:00
|
|
|
'count' => $count,
|
2022-05-09 16:54:10 +02:00
|
|
|
'excluded' => $this->__preventExcludedCorrelations($value),
|
2021-04-27 00:40:40 +02:00
|
|
|
]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getTopTime()
|
|
|
|
{
|
|
|
|
try {
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis = RedisTool::init();
|
2021-04-27 00:40:40 +02:00
|
|
|
} catch (Exception $e) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-04-29 11:41:02 +02:00
|
|
|
return $redis->get(self::CACHE_AGE);
|
2021-04-27 00:40:40 +02:00
|
|
|
}
|
2022-05-11 11:51:53 +02:00
|
|
|
|
2022-07-07 09:36:23 +02:00
|
|
|
/**
|
|
|
|
* @param array $attribute
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function advancedCorrelationsUpdate(array $attribute)
|
|
|
|
{
|
|
|
|
if ($this->advancedCorrelationEnabled && in_array($attribute['type'], ['ip-src', 'ip-dst'], true) && strpos($attribute['value'], '/')) {
|
|
|
|
$this->updateCidrList();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-11 11:51:53 +02:00
|
|
|
/**
|
|
|
|
* Get list of all CIDR for correlation from database
|
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
private function getCidrListFromDatabase()
|
2022-05-11 11:51:53 +02:00
|
|
|
{
|
|
|
|
return $this->Attribute->find('column', [
|
|
|
|
'conditions' => [
|
|
|
|
'type' => ['ip-src', 'ip-dst'],
|
|
|
|
'disable_correlation' => 0,
|
|
|
|
'deleted' => 0,
|
|
|
|
'value1 LIKE' => '%/%',
|
|
|
|
],
|
|
|
|
'fields' => ['Attribute.value1'],
|
|
|
|
'unique' => true,
|
|
|
|
'order' => false,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
public function updateCidrList()
|
2022-05-11 11:51:53 +02:00
|
|
|
{
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis = RedisTool::init();
|
2022-05-11 11:51:53 +02:00
|
|
|
$cidrList = [];
|
|
|
|
$this->cidrListCache = null;
|
|
|
|
if ($redis) {
|
|
|
|
$cidrList = $this->getCidrListFromDatabase();
|
|
|
|
|
2022-10-01 09:48:13 +02:00
|
|
|
RedisTool::unlink($redis, 'misp:cidr_cache_list');
|
2022-05-11 11:51:53 +02:00
|
|
|
if (method_exists($redis, 'saddArray')) {
|
|
|
|
$redis->sAddArray('misp:cidr_cache_list', $cidrList);
|
|
|
|
} else {
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis->pipeline();
|
2022-05-11 11:51:53 +02:00
|
|
|
foreach ($cidrList as $cidr) {
|
|
|
|
$redis->sadd('misp:cidr_cache_list', $cidr);
|
|
|
|
}
|
2022-10-01 09:48:13 +02:00
|
|
|
$redis->exec();
|
2022-05-11 11:51:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $cidrList;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
public function getCidrList()
|
2022-05-11 11:51:53 +02:00
|
|
|
{
|
|
|
|
if ($this->cidrListCache !== null) {
|
|
|
|
return $this->cidrListCache;
|
|
|
|
}
|
|
|
|
|
2022-10-01 09:48:13 +02:00
|
|
|
try {
|
|
|
|
$redis = RedisTool::init();
|
2022-05-11 11:51:53 +02:00
|
|
|
if (!$redis->exists('misp:cidr_cache_list')) {
|
|
|
|
$cidrList = $this->updateCidrList();
|
|
|
|
} else {
|
|
|
|
$cidrList = $redis->smembers('misp:cidr_cache_list');
|
|
|
|
}
|
2022-10-01 09:48:13 +02:00
|
|
|
} catch (Exception $e) {
|
2022-05-11 11:51:53 +02:00
|
|
|
$cidrList = $this->getCidrListFromDatabase();
|
|
|
|
}
|
2022-10-01 09:48:13 +02:00
|
|
|
|
2022-05-11 11:51:53 +02:00
|
|
|
$this->cidrListCache = $cidrList;
|
|
|
|
return $cidrList;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $user User array
|
|
|
|
* @param int $eventIds List of event IDs
|
|
|
|
* @param array $sgids List of sharing group IDs
|
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
public function getAttributesRelatedToEvent(array $user, $eventIds, array $sgids)
|
2022-07-31 23:48:38 +02:00
|
|
|
{
|
|
|
|
return $this->runGetAttributesRelatedToEvent($user, $eventIds, $sgids);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $user User array
|
|
|
|
* @param array $attribute Attribute Array
|
|
|
|
* @param array $fields List of fields to include
|
|
|
|
* @param bool $includeEventData Flag to include the event data in the response
|
2022-08-04 14:51:37 +02:00
|
|
|
* @return array
|
2022-07-31 23:48:38 +02:00
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
public function getRelatedAttributes($user, $sgids, $attribute, $fields=[], $includeEventData = false)
|
2022-07-31 23:48:38 +02:00
|
|
|
{
|
2022-09-11 10:25:09 +02:00
|
|
|
if (in_array($attribute['type'], Attribute::NON_CORRELATING_TYPES, true)) {
|
2022-08-04 14:51:37 +02:00
|
|
|
return [];
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
|
|
|
return $this->runGetRelatedAttributes($user, $sgids, $attribute, $fields, $includeEventData);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $user User array
|
2022-09-11 10:25:09 +02:00
|
|
|
* @param int $eventId Event ID
|
2022-07-31 23:48:38 +02:00
|
|
|
* @param array $sgids List of sharing group IDs
|
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-08 15:59:59 +02:00
|
|
|
public function getRelatedEventIds(array $user, int $eventId, array $sgids)
|
2022-07-31 23:48:38 +02:00
|
|
|
{
|
|
|
|
$relatedEventIds = $this->fetchRelatedEventIds($user, $eventId, $sgids);
|
|
|
|
if (empty($relatedEventIds)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return $relatedEventIds;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachExclusionsToOverCorrelations($data)
|
|
|
|
{
|
|
|
|
foreach ($data as $k => $v) {
|
|
|
|
$data[$k]['OverCorrelatingValue']['excluded'] = $this->__preventExcludedCorrelations($data[$k]['OverCorrelatingValue']['value']);
|
|
|
|
}
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2022-08-08 18:31:00 +02:00
|
|
|
/**
|
2022-08-09 12:16:44 +02:00
|
|
|
* @param array $attributes
|
2022-08-08 18:31:00 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
2022-08-09 12:16:44 +02:00
|
|
|
public function attachCorrelationExclusion(array $attributes)
|
2022-07-31 23:48:38 +02:00
|
|
|
{
|
2022-09-21 11:04:19 +02:00
|
|
|
$compositeTypes = $this->Attribute->getCompositeTypes();
|
2022-08-09 12:16:44 +02:00
|
|
|
$valuesToCheck = [];
|
|
|
|
foreach ($attributes as &$attribute) {
|
2022-09-21 11:04:19 +02:00
|
|
|
if ($attribute['disable_correlation'] || in_array($attribute['type'],Attribute::NON_CORRELATING_TYPES, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$primaryOnly = in_array($attribute['type'], Attribute::PRIMARY_ONLY_CORRELATING_TYPES, true);
|
|
|
|
if (in_array($attribute['type'], $compositeTypes, true)) {
|
2022-08-09 12:16:44 +02:00
|
|
|
$values = explode('|', $attribute['value']);
|
|
|
|
$valuesToCheck[$values[0]] = true;
|
2022-09-21 11:04:19 +02:00
|
|
|
if (!$primaryOnly) {
|
|
|
|
$valuesToCheck[$values[1]] = true;
|
|
|
|
}
|
2022-08-09 12:16:44 +02:00
|
|
|
} else {
|
|
|
|
$values = [$attribute['value']];
|
|
|
|
$valuesToCheck[$values[0]] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->__preventExcludedCorrelations($values[0])) {
|
|
|
|
$attribute['correlation_exclusion'] = true;
|
2022-09-21 11:04:19 +02:00
|
|
|
} elseif (!empty($values[1]) && !$primaryOnly && $this->__preventExcludedCorrelations($values[1])) {
|
2022-08-09 12:16:44 +02:00
|
|
|
$attribute['correlation_exclusion'] = true;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2022-08-08 18:31:00 +02:00
|
|
|
|
2022-08-29 10:50:59 +02:00
|
|
|
$overCorrelatingValues = array_flip($this->OverCorrelatingValue->findOverCorrelatingValues(array_keys($valuesToCheck)));
|
2022-08-09 12:16:44 +02:00
|
|
|
unset($valuesToCheck);
|
|
|
|
|
|
|
|
foreach ($attributes as &$attribute) {
|
2022-09-21 11:04:19 +02:00
|
|
|
if ($attribute['disable_correlation'] || in_array($attribute['type'],Attribute::NON_CORRELATING_TYPES, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$primaryOnly = in_array($attribute['type'], Attribute::PRIMARY_ONLY_CORRELATING_TYPES, true);
|
|
|
|
if (in_array($attribute['type'], $compositeTypes, true)) {
|
2022-08-09 12:16:44 +02:00
|
|
|
$values = explode('|', $attribute['value']);
|
2022-09-21 11:04:19 +02:00
|
|
|
$values = OverCorrelatingValue::truncateValues($values);
|
2022-08-09 12:16:44 +02:00
|
|
|
} else {
|
2022-09-21 11:04:19 +02:00
|
|
|
$values = [OverCorrelatingValue::truncate($attribute['value'])];
|
2022-08-09 12:16:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($overCorrelatingValues[$values[0]])) {
|
|
|
|
$attribute['over_correlation'] = true;
|
2022-09-21 11:04:19 +02:00
|
|
|
} elseif (!empty($values[1]) && !$primaryOnly && isset($overCorrelatingValues[$values[1]])) {
|
2022-08-09 12:16:44 +02:00
|
|
|
$attribute['over_correlation'] = true;
|
|
|
|
}
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2022-08-08 18:31:00 +02:00
|
|
|
|
2022-08-09 12:16:44 +02:00
|
|
|
return $attributes;
|
2022-07-31 23:48:38 +02:00
|
|
|
}
|
2022-08-03 15:44:27 +02:00
|
|
|
|
|
|
|
public function collectMetrics()
|
|
|
|
{
|
2022-09-11 10:25:09 +02:00
|
|
|
$results = [
|
|
|
|
'engine' => $this->getCorrelationModelName(),
|
|
|
|
'db' => [
|
|
|
|
'Default' => [
|
|
|
|
'name' => __('Default correlation engine'),
|
|
|
|
'tables' => [
|
|
|
|
'default_correlations' => [
|
|
|
|
'id_limit' => 4294967295
|
|
|
|
],
|
|
|
|
'correlation_values' => [
|
|
|
|
'id_limit' => 4294967295
|
|
|
|
]
|
2022-08-03 15:44:27 +02:00
|
|
|
]
|
2022-09-11 10:25:09 +02:00
|
|
|
],
|
|
|
|
'NoAcl' => [
|
|
|
|
'name' => __('No ACL correlation engine'),
|
|
|
|
'tables' => [
|
|
|
|
'no_acl_correlations' => [
|
|
|
|
'id_limit' => 4294967295
|
|
|
|
],
|
|
|
|
'correlation_values' => [
|
|
|
|
'id_limit' => 4294967295
|
|
|
|
]
|
2022-08-03 15:44:27 +02:00
|
|
|
]
|
2022-09-11 10:25:09 +02:00
|
|
|
],
|
|
|
|
'Legacy' => [
|
|
|
|
'name' => __('Legacy correlation engine (< 2.4.160)'),
|
|
|
|
'tables' => [
|
|
|
|
'correlations' => [
|
|
|
|
'id_limit' => 2147483647
|
|
|
|
]
|
2022-08-03 15:44:27 +02:00
|
|
|
]
|
|
|
|
]
|
2022-09-11 10:25:09 +02:00
|
|
|
],
|
|
|
|
'over_correlations' => $this->OverCorrelatingValue->find('count'),
|
2022-08-03 15:44:27 +02:00
|
|
|
];
|
|
|
|
$this->CorrelationExclusion = ClassRegistry::init('CorrelationExclusion');
|
|
|
|
$results['excluded_correlations'] = $this->CorrelationExclusion->find('count');
|
|
|
|
foreach ($results['db'] as &$result) {
|
|
|
|
foreach ($result['tables'] as $table_name => &$table_data) {
|
|
|
|
$size_metrics = $this->query(sprintf('show table status like \'%s\';', $table_name));
|
|
|
|
if (!empty($size_metrics)) {
|
|
|
|
$table_data['size_on_disk'] = $this->query(
|
|
|
|
//'select FILE_SIZE from information_schema.innodb_sys_tablespaces where FILENAME like \'%/' . $table_name . '.ibd\';'
|
|
|
|
sprintf(
|
|
|
|
'select TABLE_NAME, ROUND((DATA_LENGTH + INDEX_LENGTH)) AS size FROM information_schema.TABLES where TABLE_SCHEMA="%s" AND TABLE_NAME="%s"',
|
|
|
|
$this->getDataSource()->config['database'],
|
|
|
|
$table_name
|
|
|
|
)
|
|
|
|
)[0][0]['size'];
|
|
|
|
$last_id = $this->query(sprintf('select max(id) as max_id from %s;', $table_name));
|
|
|
|
$table_data['row_count'] = $size_metrics[0]['TABLES']['Rows'];
|
|
|
|
$table_data['last_id'] = $last_id[0][0]['max_id'];
|
|
|
|
$table_data['id_saturation'] = round(100 * $table_data['last_id'] / $table_data['id_limit'], 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function truncate(array $user, string $engine)
|
|
|
|
{
|
|
|
|
$table = $this->validEngines[$engine];
|
|
|
|
$result = $this->query('truncate table ' . $table);
|
|
|
|
if ($result !== true) {
|
|
|
|
$this->loadLog()->createLogEntry(
|
|
|
|
$user,
|
|
|
|
'truncate',
|
|
|
|
'Correlation',
|
|
|
|
0,
|
|
|
|
'Could not truncate table ' . $table,
|
|
|
|
'Errors: ' . json_encode($result)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $result === true;
|
|
|
|
}
|
2022-09-08 15:32:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
private function getCorrelationModelName()
|
|
|
|
{
|
|
|
|
return Configure::read('MISP.correlation_engine') ?: 'Default';
|
|
|
|
}
|
2021-04-22 09:46:10 +02:00
|
|
|
}
|