Merge branch 'develop' of github.com:MISP/MISP into develop

pull/8625/head
Sami Mokaddem 2022-09-23 14:04:00 +02:00
commit 2e815627db
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
12 changed files with 76 additions and 421 deletions

View File

@ -49,14 +49,6 @@ class AppController extends Controller
public $restResponsePayload = null;
// Used for _isAutomation(), a check that returns true if the controller & action combo matches an action that is a non-xml and non-json automation method
// This is used to allow authentication via headers for methods not covered by _isRest() - as that only checks for JSON and XML formats
public $automationArray = array(
'events' => array('csv', 'nids', 'hids', 'xml', 'restSearch', 'stix', 'updateGraph', 'downloadOpenIOCEvent'),
'attributes' => array('text', 'downloadAttachment', 'returnAttributes', 'restSearch', 'rpz', 'bro'),
'objects' => array('restSearch')
);
protected $_legacyParams = array();
/** @var array */
public $userRole;

View File

@ -10,6 +10,14 @@ class IndexFilterComponent extends Component
public $Controller;
public $isRest = null;
// Used for isApiFunction(), a check that returns true if the controller & action combo matches an action that is a non-xml and non-json automation method
// This is used to allow authentication via headers for methods not covered by _isRest() - as that only checks for JSON and XML formats
const AUTOMATION_ARRAY = array(
'events' => array('csv', 'nids', 'hids', 'xml', 'restSearch', 'stix', 'updateGraph', 'downloadOpenIOCEvent'),
'attributes' => array('text', 'downloadAttachment', 'returnAttributes', 'restSearch', 'rpz', 'bro'),
'objects' => array('restSearch'),
);
public function initialize(Controller $controller)
{
$this->Controller = $controller;
@ -121,6 +129,6 @@ class IndexFilterComponent extends Component
*/
public function isApiFunction($controller, $action)
{
return isset($this->Controller->automationArray[$controller]) && in_array($action, $this->Controller->automationArray[$controller], true);
return isset(self::AUTOMATION_ARRAY[$controller]) && in_array($action, self::AUTOMATION_ARRAY[$controller], true);
}
}

View File

@ -1,6 +1,6 @@
<?php
class NidsExport
abstract class NidsExport
{
public $rules = array();
@ -858,15 +858,16 @@ class NidsExport
}
}
/**
* @param array $attribute
* @return array|string[]
*/
public static function getIpPort($attribute)
{
$ipport = array();
if (strpos($attribute['type'], 'port') !== false) {
$ipport = explode('|', $attribute['value']);
return explode('|', $attribute['value']);
} else {
$ipport[0] = $attribute['value'];
$ipport[1] = 'any';
return [$attribute['value'], 'any'];
}
return $ipport;
}
}

View File

@ -635,11 +635,14 @@ class AttributeValidationTool
}
/**
* @param $value
* @param string $value
* @return bool
*/
private static function isSsdeep($value)
{
if (strpos($value, "\n") !== false) {
return false;
}
$parts = explode(':', $value);
if (count($parts) !== 3) {
return false;

View File

@ -1,8 +1,10 @@
<?php
class ColourPaletteTool
{
// pass the number of distinct colours to receive an array of colours
/**
* @param int $count Pass the number of distinct colours to receive an array of colours
* @return array
*/
public function createColourPalette($count)
{
$interval = 1 / $count;
@ -13,6 +15,10 @@ class ColourPaletteTool
return $colours;
}
/**
* @param array $hsv
* @return string
*/
public function HSVtoRGB(array $hsv)
{
list($H, $S, $V) = $hsv;
@ -50,12 +56,16 @@ class ColourPaletteTool
return $this->convertToHex(array($R, $G, $B));
}
/**
* @param array $channels
* @return string
*/
public function convertToHex($channels)
{
$colour = '#';
foreach ($channels as $channel) {
$channel = strval(dechex(round($channel*255)));
if (strlen($channel) == 1) {
$channel = dechex(round($channel*255));
if (strlen($channel) === 1) {
$channel = '0' . $channel;
}
$colour .= $channel;

View File

@ -24,7 +24,7 @@ class SendEmailTemplate
/**
* This value will be used for grouping emails in mail client.
* @param string|null $referenceId
* @return string
* @return string|void
*/
public function referenceId($referenceId = null)
{
@ -49,7 +49,7 @@ class SendEmailTemplate
/**
* Get subject from template. Must be called after render method.
* @param string|null $subject
* @return string
* @return string|void
*/
public function subject($subject = null)
{
@ -84,7 +84,6 @@ class SendEmailTemplate
$View->set($this->viewVars);
$View->set('hideDetails', $hideDetails);
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'html';
try {
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'html' . DS . 'Custom';
$html = $View->render($this->viewName); // Attempt to load a custom template if it exists
@ -93,7 +92,7 @@ class SendEmailTemplate
try {
$html = $View->render($this->viewName);
} catch (MissingViewException $e) {
$html = null; // HTMl template is optional
$html = null; // HTML template is optional
}
}

View File

@ -44,7 +44,7 @@ class TrendingTool
}
$allTags[$tag] = true;
$trendAnalysis[$timestamp][$tag] = [
'occurence' => round($amount / $eventNumberPerRollingWindow[$timestamp], 2),
'occurrence' => round($amount / $eventNumberPerRollingWindow[$timestamp], 2),
'raw_change' => $rawChange,
'percent_change' => $percentChange,
'change_sign' => $rawChange > 0 ? 1 : ($rawChange < 0 ? -1 : 0),
@ -54,9 +54,9 @@ class TrendingTool
foreach (array_keys($trendAnalysis[$timestamp]) as $tag) {
if (empty($trendAnalysis[$previousTimestamp][$tag])) {
$trendAnalysis[$previousTimestamp][$tag] = [
'occurence' => 0,
'occurrence' => 0,
'raw_change' => -$amount,
'percent_change' => 100 * (-$amount / $amount),
'percent_change' => round(100 * (-$amount / $amount), 2),
'change_sign' => -$amount > 0 ? 1 : (-$amount < 0 ? -1 : 0),
];
}

View File

@ -2622,7 +2622,7 @@ class AppModel extends Model
return $remainingTime > 0 || $failThresholdReached;
}
public function getUpdateFailNumber()
private function getUpdateFailNumber()
{
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$updateFailNumber = $this->AdminSetting->getSetting('update_fail_number');
@ -2635,7 +2635,7 @@ class AppModel extends Model
$this->AdminSetting->changeSetting('update_fail_number', 0);
}
public function __increaseUpdateFailNumber()
private function __increaseUpdateFailNumber()
{
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$updateFailNumber = $this->AdminSetting->getSetting('update_fail_number');
@ -2737,7 +2737,7 @@ class AppModel extends Model
return true;
}
public function removeDuplicatedUUIDs()
private function removeDuplicatedUUIDs()
{
$removedResults = array(
'Event' => $this->removeDuplicateEventUUIDs(),
@ -2782,7 +2782,7 @@ class AppModel extends Model
return $counter;
}
public function removeDuplicateAttributeUUIDs()
private function removeDuplicateAttributeUUIDs()
{
$this->Attribute = ClassRegistry::init('Attribute');
$this->Log = ClassRegistry::init('Log');
@ -2836,7 +2836,7 @@ class AppModel extends Model
return $counter;
}
public function removeDuplicateEventUUIDs()
private function removeDuplicateEventUUIDs()
{
$this->Event = ClassRegistry::init('Event');
$this->Log = ClassRegistry::init('Log');

View File

@ -1024,163 +1024,6 @@ class Attribute extends AppModel
return $data;
}
public function hids($user, $type, $tags = '', $from = false, $to = false, $last = false, $jobId = false, $enforceWarninglist = false)
{
if (empty($user)) {
throw new MethodNotAllowedException(__('Could not read user.'));
}
// check if it's a valid type
if ($type != 'md5' && $type != 'sha1' && $type != 'sha256') {
throw new UnauthorizedException(__('Invalid hash type.'));
}
$conditions = array();
$typeArray = array($type, 'filename|' . $type);
if ($type == 'md5') {
$typeArray[] = 'malware-sample';
}
$rules = array();
$eventIds = $this->Event->fetchEventIds($user, [
'from' => $from,
'to' => $to,
'last' => $last
]);
if (!empty($tags)) {
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
if (!empty($tagArray[0])) {
foreach ($eventIds as $k => $v) {
if (!in_array($v['Event']['id'], $tagArray[0])) {
unset($eventIds[$k]);
}
}
}
if (!empty($tagArray[1])) {
foreach ($eventIds as $k => $v) {
if (in_array($v['Event']['id'], $tagArray[1])) {
unset($eventIds[$k]);
}
}
}
}
App::uses('HidsExport', 'Export');
$continue = false;
$eventCount = count($eventIds);
if ($jobId) {
$this->Job = ClassRegistry::init('Job');
$this->Job->id = $jobId;
if (!$this->Job->exists()) {
$jobId = false;
}
}
foreach ($eventIds as $k => $event) {
$conditions['AND'] = array('Attribute.to_ids' => 1, 'Event.published' => 1, 'Attribute.type' => $typeArray, 'Attribute.event_id' => $event['Event']['id']);
$options = array(
'conditions' => $conditions,
'group' => array('Attribute.type', 'Attribute.value1'),
'enforceWarninglist' => $enforceWarninglist,
'flatten' => true
);
$items = $this->fetchAttributes($user, $options);
if (empty($items)) {
continue;
}
$export = new HidsExport();
$rules = array_merge($rules, $export->export($items, strtoupper($type), $continue));
$continue = true;
if ($jobId && ($k % 10 == 0)) {
$this->Job->saveField('progress', $k * 80 / $eventCount);
}
}
return $rules;
}
public function nids($user, $format, $id = false, $continue = false, $tags = false, $from = false, $to = false, $last = false, $type = false, $enforceWarninglist = false, $includeAllTags = false)
{
if (empty($user)) {
throw new MethodNotAllowedException(__('Could not read user.'));
}
$eventIds = $this->Event->fetchEventIds($user, [
'from' => $from,
'to' => $to,
'last' => $last
]);
// If we sent any tags along, load the associated tag names for each attribute
if ($tags) {
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
if (!empty($tagArray[0])) {
foreach ($eventIds as $k => $v) {
if (!in_array($v['Event']['id'], $tagArray[0])) {
unset($eventIds[$k]);
}
}
}
if (!empty($tagArray[1])) {
foreach ($eventIds as $k => $v) {
if (in_array($v['Event']['id'], $tagArray[1])) {
unset($eventIds[$k]);
}
}
}
}
if ($id) {
foreach ($eventIds as $k => $v) {
if ($v['Event']['id'] !== $id) {
unset($eventIds[$k]);
}
}
}
if ($format == 'suricata') {
App::uses('NidsSuricataExport', 'Export');
} else {
App::uses('NidsSnortExport', 'Export');
}
$rules = array();
foreach ($eventIds as $event) {
$conditions['AND'] = array('Attribute.to_ids' => 1, "Event.published" => 1, 'Attribute.event_id' => $event['Event']['id']);
$valid_types = array('ip-dst', 'ip-src', 'ip-dst|port', 'ip-src|port', 'eppn', 'email', 'email-src', 'email-dst', 'email-subject', 'email-attachment', 'domain', 'domain|ip', 'hostname', 'url', 'user-agent', 'snort');
$conditions['AND']['Attribute.type'] = $valid_types;
if (!empty($type)) {
$conditions['AND'][] = array('Attribute.type' => $type);
}
$params = array(
'conditions' => $conditions, // array of conditions
'recursive' => -1, // int
'fields' => array('Attribute.id', 'Attribute.event_id', 'Attribute.type', 'Attribute.value'),
'contain' => array('Event'=> array('fields' => array('Event.id', 'Event.threat_level_id'))),
'group' => array('Attribute.type', 'Attribute.value1'), // fields to GROUP BY
'enforceWarninglist' => $enforceWarninglist,
'includeAllTags' => $includeAllTags,
'flatten' => true
);
$items = $this->fetchAttributes($user, $params);
if (empty($items)) {
continue;
}
// export depending on the requested type
switch ($format) {
case 'suricata':
$export = new NidsSuricataExport();
break;
case 'snort':
$export = new NidsSnortExport();
break;
}
$rules = array_merge($rules, $export->export($items, $user['nids_sid'], $format, $continue));
// Only prepend the comments once
$continue = true;
}
return $rules;
}
public function set_filter_tags(&$params, $conditions, $options)
{
if (empty($params['tags']) && empty($params['event_tags'])) {
@ -1315,212 +1158,6 @@ class Attribute extends AppModel
return $conditions;
}
public function text($user, $type, $tags = false, $eventId = false, $allowNonIDS = false, $from = false, $to = false, $last = false, $enforceWarninglist = false, $allowNotPublished = false)
{
//permissions are taken care of in fetchAttributes()
$conditions['AND'] = array();
if ($allowNonIDS === false) {
$conditions['AND']['Attribute.to_ids'] = 1;
if ($allowNotPublished === false) {
$conditions['AND']['Event.published'] = 1;
}
}
if (!is_array($type) && $type !== 'all') {
$conditions['AND']['Attribute.type'] = $type;
}
if ($from) {
$conditions['AND']['Event.date >='] = $from;
}
if ($to) {
$conditions['AND']['Event.date <='] = $to;
}
if ($last) {
$conditions['AND']['Event.publish_timestamp >='] = $last;
}
if ($eventId !== false) {
$conditions['AND'][] = array('Event.id' => $eventId);
} elseif ($tags !== false) {
$passed_param = array('tags' => $tags);
$conditions = $this->set_filter_tags($passed_param, $conditions, array('scope' => 'Attribute'));
}
$attributes = $this->fetchAttributes($user, array(
'conditions' => $conditions,
'order' => 'Attribute.value1 ASC',
'fields' => array('value'),
'contain' => array('Event' => array(
'fields' => array('Event.id', 'Event.published', 'Event.date', 'Event.publish_timestamp'),
)),
'enforceWarninglist' => $enforceWarninglist,
'flatten' => 1
));
return $attributes;
}
public function rpz($user, $tags = false, $eventId = false, $from = false, $to = false, $enforceWarninglist = false)
{
// we can group hostname and domain as well as ip-src and ip-dst in this case
$conditions['AND'] = array('Attribute.to_ids' => 1, 'Event.published' => 1);
$typesToFetch = array('ip' => array('ip-src', 'ip-dst'), 'domain' => array('domain'), 'hostname' => array('hostname'));
if ($from) {
$conditions['AND']['Event.date >='] = $from;
}
if ($to) {
$conditions['AND']['Event.date <='] = $to;
}
if ($eventId !== false) {
$conditions['AND'][] = array('Event.id' => $eventId);
}
if ($tags !== false) {
// If we sent any tags along, load the associated tag names for each attribute
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
$temp = array();
foreach ($tagArray[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($tagArray[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
$values = array();
foreach ($typesToFetch as $k => $v) {
$tempConditions = $conditions;
$tempConditions['type'] = $v;
$temp = $this->fetchAttributes(
$user,
array(
'conditions' => $tempConditions,
'fields' => array('Attribute.value'), // array of field names
'enforceWarninglist' => $enforceWarninglist,
'flatten' => 1
)
);
if (empty($temp)) {
continue;
}
if ($k == 'hostname') {
foreach ($temp as $value) {
$found = false;
if (isset($values['domain'])) {
foreach ($values['domain'] as $domain) {
if (strpos($value['Attribute']['value'], $domain) != 0) {
$found = true;
}
}
}
if (!$found) {
$values[$k][] = $value['Attribute']['value'];
}
}
} else {
foreach ($temp as $value) {
$values[$k][] = $value['Attribute']['value'];
}
}
unset($temp);
}
return $values;
}
public function bro($user, $type, $tags = false, $eventId = false, $from = false, $to = false, $last = false, $enforceWarninglist = false, $skipHeader = false)
{
App::uses('BroExport', 'Export');
$export = new BroExport();
if ($type == 'all') {
$types = array_keys($export->mispTypes);
} else {
$types = array($type);
}
$intel = array();
foreach ($types as $type) {
//restricting to non-private or same org if the user is not a site-admin.
$conditions['AND'] = array('Attribute.to_ids' => 1, 'Event.published' => 1);
if ($from) {
$conditions['AND']['Event.date >='] = $from;
}
if ($to) {
$conditions['AND']['Event.date <='] = $to;
}
if ($last) {
$conditions['AND']['Event.publish_timestamp >='] = $last;
}
if ($eventId !== false) {
$temp = array();
$args = $this->dissectArgs($eventId);
foreach ($args[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($args[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
if ($tags !== false) {
// If we sent any tags along, load the associated tag names for each attribute
$tag = ClassRegistry::init('Tag');
$args = $this->dissectArgs($tags);
$tagArray = $tag->fetchEventTagIds($args[0], $args[1]);
$temp = array();
foreach ($tagArray[0] as $accepted) {
$temp['OR'][] = array('Event.id' => $accepted);
}
$conditions['AND'][] = $temp;
$temp = array();
foreach ($tagArray[1] as $rejected) {
$temp['AND'][] = array('Event.id !=' => $rejected);
}
$conditions['AND'][] = $temp;
}
$this->Allowedlist = ClassRegistry::init('Allowedlist');
$this->allowedlist = $this->Allowedlist->getBlockedValues();
$instanceString = 'MISP';
if (Configure::read('MISP.host_org_id') && Configure::read('MISP.host_org_id') > 0) {
$this->Event->Orgc->id = Configure::read('MISP.host_org_id');
if ($this->Event->Orgc->exists()) {
$instanceString = $this->Event->Orgc->field('name') . ' MISP';
}
}
$mispTypes = $export->getMispTypes($type);
foreach ($mispTypes as $mispType) {
$conditions['AND']['Attribute.type'] = $mispType[0];
$intel = array_merge($intel, $this->__bro($user, $conditions, $mispType[1], $export, $this->allowedlist, $instanceString, $enforceWarninglist));
}
}
natsort($intel);
$intel = array_unique($intel);
if (empty($skipHeader)) {
array_unshift($intel, $export->header);
}
return $intel;
}
private function __bro($user, $conditions, $valueField, $export, $allowedlist, $instanceString, $enforceWarninglist)
{
$attributes = $this->fetchAttributes(
$user,
array(
'conditions' => $conditions, // array of conditions
'order' => 'Attribute.value' . $valueField . ' ASC',
'recursive' => -1, // int
'fields' => array('Attribute.id', 'Attribute.event_id', 'Attribute.type', 'Attribute.category', 'Attribute.comment', 'Attribute.to_ids', 'Attribute.value', 'Attribute.value' . $valueField),
'contain' => array('Event' => array('fields' => array('Event.id', 'Event.threat_level_id', 'Event.orgc_id', 'Event.uuid'))),
'enforceWarninglist' => $enforceWarninglist,
'flatten' => 1
)
);
$orgs = $this->Event->Orgc->find('list', array(
'fields' => array('Orgc.id', 'Orgc.name')
));
return $export->export($attributes, $orgs, $valueField, $allowedlist, $instanceString);
}
/**
* @param int|false $jobId
* @param int|false $eventId

View File

@ -1782,25 +1782,25 @@ class User extends AppModel
/**
* generatePeriodicSummary
*
* @param int $user_id
* @param string $period
* @param int $userId
* @param string $period Can be 'daily', 'weekly' or 'monthly'
* @param bool $rendered When false, instance of SendEmailTemplate will returned
* @return string|SendEmailTemplate
* @throws NotFoundException
* @throws InvalidArgumentException
*/
public function generatePeriodicSummary(int $user_id, string $period, $rendered=true)
public function generatePeriodicSummary(int $userId, string $period, $rendered=true)
{
$existingUser = $this->getUserById($user_id);
$user = $this->rearrangeToAuthForm($existingUser);
$allowed_periods = array_map(function($period) {
$allowedPeriods = array_map(function($period) {
return substr($period, strlen('notification_'));
}, self::PERIODIC_NOTIFICATIONS);
if (!in_array($period, $allowed_periods)) {
if (!in_array($period, $allowedPeriods, true)) {
throw new InvalidArgumentException(__('Invalid period. Must be one of %s', JsonTool::encode(self::PERIODIC_NOTIFICATIONS)));
}
$user = $this->getAuthUser($userId);
App::import('Tools', 'SendEmail');
$emailTemplate = $this->prepareEmailTemplate($period);
$periodicSettings = $this->fetchPeriodicSettingForUser($user_id, true);
$periodicSettings = $this->fetchPeriodicSettingForUser($userId, true);
$filters = $this->getUsablePeriodicSettingForUser($periodicSettings, $period);
$filtersForRestSearch = $filters; // filters for restSearch are slightly different than fetchEvent
$filters['last'] = $this->resolveTimeDelta($filters['last']);
@ -1823,7 +1823,7 @@ class User extends AppModel
unset($filtersForRestSearch['tags']);
}
$finalContext = $this->Event->restSearch($user, 'context', $filtersForRestSearch, false, false, $elementCounter, $renderView);
$finalContext = json_decode($finalContext->intoString(), true);
$finalContext = JsonTool::decode($finalContext->intoString());
$aggregated_context = $this->__renderAggregatedContext($finalContext);
$rollingWindows = $periodicSettings['trending_period_amount'] ?: 2;
@ -1835,6 +1835,7 @@ class User extends AppModel
];
$trending_summary = $this->__renderTrendingSummary($trendData);
$emailTemplate = $this->prepareEmailTemplate($period);
$emailTemplate->set('baseurl', $this->Event->__getAnnounceBaseurl());
$emailTemplate->set('events', $events);
$emailTemplate->set('filters', $filters);
@ -1845,9 +1846,9 @@ class User extends AppModel
$emailTemplate->set('trending_summary', $trending_summary);
$emailTemplate->set('analysisLevels', $this->Event->analysisLevels);
$emailTemplate->set('distributionLevels', $this->Event->distributionLevels);
if (!empty($rendered)) {
if ($rendered) {
$summary = $emailTemplate->render();
return $summary->format() == 'text' ? $summary->text : $summary->html;
return $summary->format() === 'text' ? $summary->text : $summary->html;
}
return $emailTemplate;
}
@ -1877,23 +1878,21 @@ class User extends AppModel
private function __genTimerangeFilter(string $period='daily'): string
{
$timerange = '1d';
if ($period == 'weekly') {
$timerange = '7d';
} else if ($period == 'monthly'){
$timerange = '31d';
}
return $timerange;
return $this->periodToDays($period) . 'd';
}
public function periodToDays(string $period='daily'): int
private function periodToDays(string $period='daily'): int
{
return ($period == 'daily' ? 1 : (
$period == 'weekly' ? 7 : 31)
);
if ($period === 'daily') {
return 1;
} else if ($period === 'weekly') {
return 7;
} else {
return 31;
}
}
public function prepareEmailTemplate(string $period='daily'): SendEmailTemplate
private function prepareEmailTemplate(string $period = 'daily'): SendEmailTemplate
{
$subject = sprintf('[%s MISP] %s %s', Configure::read('MISP.org'), Inflector::humanize($period), __('Notification - %s', (new DateTime())->format('Y-m-d')));
$template = new SendEmailTemplate("notification_$period");

View File

@ -135,6 +135,12 @@ class AttributeValidationToolTest extends TestCase
$this->assertEquals('xn--hkyrky-ptac70bc.cz|127.0.0.1', AttributeValidationTool::modifyBeforeValidation('domain|ip', 'HÁČKYČÁRKY.CZ|127.0.0.1'));
}
public function testSssdeep()
{
$this->shouldBeValid('ssdeep', ["768:+OFu8Q3w6QzfR5Jni6SQD7qSFDs6P93/q0XIc/UB5EPABWX:RFu8QAFzffJui79f13/AnB5EPAkX"]);
$this->shouldBeInvalid('ssdeep', ["768:+OFu8Q3w6QzfR5Jni6SQD7qSFDs6P93/q0XIc/UB5EPABWX\n\n:RFu8QAFzffJui79f13/AnB5EPAkX"]);
}
private function shouldBeValid($type, array $values)
{
foreach ($values as $value) {

View File

@ -46,6 +46,7 @@ foreach ($allUniqueTags as $i => $tag) {
$chartData[$tag] = array_reverse($chartData[$tag]);
$maxValue = max($maxValue, max($chartData[$tag]));
}
$colorForTags[$tag] = $COLOR_PALETTE[$i];
}
$canvasWidth = 600;
$canvasHeight = 150;
@ -136,7 +137,7 @@ if (!function_exists('getColorFromYlOrBr')) {
<div>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, 0, -25) ?>"><?= h($maxValue) ?></span>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, ($canvasHeight - 20) / 2, 0) ?>"><?= h(round($maxValue / 2, 2)) ?></span>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, ($canvasHeight - 20), 25) ?>"><?= 0 ?></span>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, ($canvasHeight - 20), 25) ?>">0</span>
</div>
</div>
<div class="canvas">
@ -247,7 +248,6 @@ if (!function_exists('getColorFromYlOrBr')) {
$colorGradient[] = sprintf('%s %s%%', $color, $length);
}
?>
<div class="heatbar" style="background: <?= sprintf('linear-gradient(90deg, %s);', implode(', ', $colorGradient)) ?>;"></div>
</td>
<?php endforeach; ?>