mirror of https://github.com/MISP/MISP
Merge pull request #6924 from JakubOnderka/cidr-tool
new: [internal] Cidr tool for faster checking CIDR rangespull/6926/head
commit
df9f1075d5
|
@ -212,6 +212,7 @@ jobs:
|
|||
./app/Vendor/bin/parallel-lint --exclude app/Lib/cakephp/ --exclude app/Vendor/ --exclude app/Lib/random_compat/ -e php,ctp app/
|
||||
./app/Vendor/bin/phpunit app/Test/ComplexTypeToolTest.php
|
||||
./app/Vendor/bin/phpunit app/Test/JSONConverterToolTest.php
|
||||
./app/Vendor/bin/phpunit app/Test/CidrToolTest.php
|
||||
|
||||
# Ensure the perms of config files
|
||||
sudo chown -R $USER:www-data `pwd`/app/Config
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
class CidrTool
|
||||
{
|
||||
/** @var array */
|
||||
private $ipv4 = [];
|
||||
|
||||
/** @var array */
|
||||
private $ipv6 = [];
|
||||
|
||||
public function __construct(array $list)
|
||||
{
|
||||
$this->filterInputList($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value IPv4 or IPv6 address or range
|
||||
* @return false|string
|
||||
*/
|
||||
public function contains($value)
|
||||
{
|
||||
$valueMask = null;
|
||||
if (strpos($value, '/') !== false) {
|
||||
list($value, $valueMask) = explode('/', $value);
|
||||
}
|
||||
|
||||
$match = false;
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
// This code converts IP address to all possible CIDRs that can contains given IP address
|
||||
// and then check if given hash table contains that CIDR.
|
||||
$ip = ip2long($value);
|
||||
// Start from 1, because doesn't make sense to check 0.0.0.0/0 match
|
||||
for ($bits = 1; $bits <= 32; $bits++) {
|
||||
$mask = -1 << (32 - $bits);
|
||||
$needle = long2ip($ip & $mask) . "/$bits";
|
||||
if (isset($this->ipv4[$needle])) {
|
||||
$match = $needle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$value = unpack('n*', inet_pton($value));
|
||||
foreach ($this->ipv6 as $lv) {
|
||||
if ($this->ipv6InCidr($value, $lv)) {
|
||||
$match = $lv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($match && $valueMask) {
|
||||
$matchMask = explode('/', $match)[1];
|
||||
if ($valueMask < $matchMask) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php
|
||||
*
|
||||
* @param array $ip
|
||||
* @param string $cidr
|
||||
* @return bool
|
||||
*/
|
||||
private function ipv6InCidr($ip, $cidr)
|
||||
{
|
||||
list($address, $netmask) = explode('/', $cidr);
|
||||
$bytesAddr = unpack('n*', inet_pton($address));
|
||||
|
||||
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) != ($ip[$i] & $mask)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out invalid IPv4 or IPv4 CIDR and append maximum netmask if no netmask is given.
|
||||
* @param array $list
|
||||
*/
|
||||
private function filterInputList(array $list)
|
||||
{
|
||||
foreach ($list as $v) {
|
||||
$parts = explode('/', $v, 2);
|
||||
if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$maximumNetmask = 32;
|
||||
} else if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$parts[0] = strtolower($parts[0]);
|
||||
$maximumNetmask = 128;
|
||||
} else {
|
||||
// IP address part of CIDR is invalid
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($parts[1])) {
|
||||
// If CIDR doesnt contains '/', we will consider CIDR as /32 for IPv4 or /128 for IPv6
|
||||
$v = "$v/$maximumNetmask";
|
||||
} else if ($parts[1] > $maximumNetmask || $parts[1] < 0) {
|
||||
// Netmask part of CIDR is invalid
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($maximumNetmask === 32) {
|
||||
$this->ipv4[$v] = true;
|
||||
} else {
|
||||
$this->ipv6[] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
App::uses('AppModel', 'Model');
|
||||
App::uses('CidrTool', 'Tools');
|
||||
|
||||
/**
|
||||
* @property WarninglistType $WarninglistType
|
||||
|
@ -398,39 +399,6 @@ class Warninglist extends AppModel
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out invalid IPv4 or IPv4 CIDR and append maximum netmaks if no netmask is given.
|
||||
* @param array $inputValues
|
||||
* @return array
|
||||
*/
|
||||
private function filterCidrList($inputValues)
|
||||
{
|
||||
$outputValues = [];
|
||||
foreach ($inputValues as $v) {
|
||||
$v = strtolower($v);
|
||||
$parts = explode('/', $v, 2);
|
||||
if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$maximumNetmask = 32;
|
||||
} else if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$maximumNetmask = 128;
|
||||
} else {
|
||||
// IP address part of CIDR is invalid
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($parts[1])) {
|
||||
// If CIDR doesnt contains '/', we will consider CIDR as /32 for IPv4 or /128 for IPv6
|
||||
$v = "$v/$maximumNetmask";
|
||||
} else if ($parts[1] > $maximumNetmask || $parts[1] < 0) {
|
||||
// Netmask part of CIDR is invalid
|
||||
continue;
|
||||
}
|
||||
|
||||
$outputValues[$v] = true;
|
||||
}
|
||||
return $outputValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* For 'hostname', 'string' and 'cidr' warninglist type, values are just in keys to save memory.
|
||||
*
|
||||
|
@ -459,7 +427,7 @@ class Warninglist extends AppModel
|
|||
}
|
||||
$values = $output;
|
||||
} else if ($warninglist['Warninglist']['type'] === 'cidr') {
|
||||
$values = $this->filterCidrList($values);
|
||||
$values = new CidrTool($values);
|
||||
}
|
||||
|
||||
$this->entriesCache[$id] = $values;
|
||||
|
@ -497,7 +465,7 @@ class Warninglist extends AppModel
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $listValues
|
||||
* @param array|CidrTool $listValues
|
||||
* @param string $value
|
||||
* @param string $type
|
||||
* @param string $listType
|
||||
|
@ -512,7 +480,7 @@ class Warninglist extends AppModel
|
|||
}
|
||||
foreach ($value as $v) {
|
||||
if ($listType === 'cidr') {
|
||||
$result = $this->__evalCidrList($listValues, $v);
|
||||
$result = $listValues->contains($v);
|
||||
} elseif ($listType === 'string') {
|
||||
$result = $this->__evalString($listValues, $v);
|
||||
} elseif ($listType === 'substring') {
|
||||
|
@ -536,75 +504,6 @@ class Warninglist extends AppModel
|
|||
return $this->__checkValue($listValues, $value, '', $type) !== false;
|
||||
}
|
||||
|
||||
private function __evalCidrList($listValues, $value)
|
||||
{
|
||||
$valueMask = null;
|
||||
if (strpos($value, '/') !== false) {
|
||||
list($value, $valueMask) = explode('/', $value);
|
||||
}
|
||||
|
||||
$match = false;
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
// This code converts IP address to all possible CIDRs that can contains given IP address
|
||||
// and then check if given hash table contains that CIDR.
|
||||
$ip = ip2long($value);
|
||||
// Start from 1, because doesn't make sense to check 0.0.0.0/0 match
|
||||
for ($bits = 1; $bits <= 32; $bits++) {
|
||||
$mask = -1 << (32 - $bits);
|
||||
$needle = long2ip($ip & $mask) . "/$bits";
|
||||
if (isset($listValues[$needle])) {
|
||||
$match = $needle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
foreach ($listValues as $lv => $foo) {
|
||||
if (strpos($lv, ':') !== false) { // Filter out IPv4 CIDR, IPv6 CIDR must contain colon
|
||||
if ($this->__ipv6InCidr($value, $lv)) {
|
||||
$match = $lv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($match && $valueMask) {
|
||||
$matchMask = explode('/', $match)[1];
|
||||
if ($valueMask < $matchMask) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php
|
||||
*
|
||||
* @param string $ip
|
||||
* @param string $cidr
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for exact match.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/../Lib/Tools/CidrTool.php';
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CidrToolTest extends TestCase
|
||||
{
|
||||
public function testEmptyList(): void
|
||||
{
|
||||
$cidrTool = new CidrTool([]);
|
||||
$this->assertFalse($cidrTool->contains('1.2.3.4'));
|
||||
}
|
||||
|
||||
public function testIpv4Fullmask(): void
|
||||
{
|
||||
$cidrTool = new CidrTool(['1.2.3.4/32']);
|
||||
$this->assertEquals('1.2.3.4/32', $cidrTool->contains('1.2.3.4'));
|
||||
}
|
||||
|
||||
public function testIpv4WithoutNetmask(): void
|
||||
{
|
||||
$cidrTool = new CidrTool(['1.2.3.4']);
|
||||
$this->assertEquals('1.2.3.4/32', $cidrTool->contains('1.2.3.4'));
|
||||
}
|
||||
|
||||
public function testIpv4(): void
|
||||
{
|
||||
$cidrTool = new CidrTool(['10.0.0.0/8', '8.0.0.0/8', '9.0.0.0/8']);
|
||||
$this->assertEquals('8.0.0.0/8', $cidrTool->contains('8.8.8.8'));
|
||||
$this->assertFalse($cidrTool->contains('::1'));
|
||||
$this->assertFalse($cidrTool->contains('7.1.2.3'));
|
||||
}
|
||||
|
||||
public function testIpv6(): void
|
||||
{
|
||||
$cidrTool = new CidrTool(['2001:0db8:1234::/48']);
|
||||
$this->assertEquals('2001:0db8:1234::/48', $cidrTool->contains('2001:0db8:1234:0000:0000:0000:0000:0000'));
|
||||
$this->assertEquals('2001:0db8:1234::/48', $cidrTool->contains('2001:0db8:1234:ffff:ffff:ffff:ffff:ffff'));
|
||||
$this->assertFalse($cidrTool->contains('2002:0db8:1234:ffff:ffff:ffff:ffff:ffff'));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue