IDPS: deprecate filter option on file downloads in favour of new policy option. migrates exsting filters to policies while there. for https://github.com/opnsense/core/issues/4445

This commit is contained in:
Ad Schellevis 2020-11-23 16:42:41 +01:00
parent 54663d2cb1
commit a7a3d1f2d4
8 changed files with 74 additions and 83 deletions

View File

@ -275,8 +275,6 @@ class SettingsController extends ApiMutableModelControllerBase
// retrieve status from model
$fileNode = $this->getModel()->getFileNode($fileinfo['filename']);
$item['enabled'] = (string)$fileNode->enabled;
$item['filter'] = $fileNode->filter->getNodeData(); // filter (option list)
$item['filter_str'] = (string)$fileNode->filter; // filter current value
$result[] = $item;
}
}
@ -455,12 +453,6 @@ class SettingsController extends ApiMutableModelControllerBase
$node = $this->getModel()->getFileNode($filename);
if ($enabled == "0" || $enabled == "1") {
$node->enabled = (string)$enabled;
} elseif ($enabled == "drop") {
$node->enabled = "1";
$node->filter = "drop";
} elseif ($enabled == "clear") {
$node->enabled = "1";
$node->filter = "";
} elseif ((string)$node->enabled == "1") {
$node->enabled = "0";
} else {

View File

@ -15,10 +15,4 @@
<label>Source</label>
<type>info</type>
</field>
<field>
<id>filter</id>
<label>Input Filter</label>
<type>dropdown</type>
<help>Filter to use when downloading this ruleset, applies this action to all incoming lines.</help>
</field>
</form>

View File

@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/IDS</mount>
<version>1.0.5</version>
<version>1.0.6</version>
<description>
OPNsense IDS
</description>
@ -121,12 +121,6 @@
<Required>Y</Required>
<mask>/^([\t\n\v\f\r\- 0-9a-zA-Z.,_\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
</filename>
<filter type="OptionField">
<Required>N</Required>
<OptionValues>
<drop>Change all alerts to drop actions</drop>
</OptionValues>
</filter>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>

View File

@ -0,0 +1,62 @@
<?php
/*
* Copyright (C) 2020 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IDS\Migrations;
use OPNsense\Base\FieldTypes\BaseField;
use OPNsense\Core\Config;
use OPNsense\Base\BaseModelMigration;
use OPNSense\IDS\IDS;
class M1_0_6 extends BaseModelMigration
{
/**
* Migrate ruleset filters
* @param IDS $model
*/
public function run($model)
{
$cfgObj = Config::getInstance()->object();
if (!isset($cfgObj->OPNsense->IDS->files->file)) {
return;
}
$rulesets = [];
foreach ($cfgObj->OPNsense->IDS->files->file as $file) {
if (!empty($file->filter) && !empty($file->enabled)) {
$rulesets[] = (string)$file->attributes()['uuid'];
}
}
if (!empty($rulesets)){
$policy = $model->policies->policy->Add();
$policy->action = "alert";
$policy->new_action = "drop";
$policy->rulesets = implode(",", $rulesets);
$policy->description = "imported legacy import filter";
}
}
}

View File

@ -299,7 +299,7 @@ POSSIBILITY OF SUCH DAMAGE.
}
});
/**
* disable/enable[with optional filter] selected rulesets
* disable/enable selected rulesets
*/
$("#disableSelectedRuleSets").unbind('click').click(function(){
actionToggleSelected('grid-rule-files', '/api/ids/settings/toggleRuleset/', 0, 20);
@ -307,12 +307,6 @@ POSSIBILITY OF SUCH DAMAGE.
$("#enableSelectedRuleSets").unbind('click').click(function(){
actionToggleSelected('grid-rule-files', '/api/ids/settings/toggleRuleset/', 1, 20);
});
$("#enabledropSelectedRuleSets").unbind('click').click(function(){
actionToggleSelected('grid-rule-files', '/api/ids/settings/toggleRuleset/', "drop", 20);
});
$("#enableclearSelectedRuleSets").click(function(){
actionToggleSelected('grid-rule-files', '/api/ids/settings/toggleRuleset/', "clear", 20);
});
} else if (e.target.id == 'rule_tab'){
//
// activate rule tab page
@ -742,12 +736,6 @@ POSSIBILITY OF SUCH DAMAGE.
<button data-toggle="tooltip" id="enableSelectedRuleSets" type="button" class="btn btn-xs btn-default btn-primary">
{{ lang._('Enable selected') }}
</button>
<button data-toggle="tooltip" id="enabledropSelectedRuleSets" type="button" class="btn btn-xs btn-default btn-primary">
{{ lang._('Enable (drop filter)') }}
</button>
<button data-toggle="tooltip" id="enableclearSelectedRuleSets" type="button" class="btn btn-xs btn-default btn-primary">
{{ lang._('Enable (clear filter)') }}
</button>
<button data-toggle="tooltip" id="disableSelectedRuleSets" type="button" class="btn btn-xs btn-default btn-primary">
{{ lang._('Disable selected') }}
</button>
@ -768,7 +756,6 @@ POSSIBILITY OF SUCH DAMAGE.
<th data-column-id="description" data-type="string" data-sortable="false" data-visible="true">{{ lang._('Description') }}</th>
<th data-column-id="modified_local" data-type="rulets" data-sortable="false" data-visible="true">{{ lang._('Last updated') }}</th>
<th data-column-id="enabled" data-formatter="boolean" data-sortable="false" data-width="10em">{{ lang._('Enabled') }}</th>
<th data-column-id="filter_str" data-type="string" data-identifier="true">{{ lang._('Filter') }}</th>
<th data-column-id="edit" data-formatter="editor" data-sortable="false" data-width="10em">{{ lang._('Edit') }}</th>
</tr>
</thead>

View File

@ -45,34 +45,6 @@ class Downloader(object):
self._target_dir = target_dir
self._download_cache = dict()
def filter(self, in_data, filter_type):
""" apply input filter to downloaded data
:param in_data: raw input data (ruleset)
:param filter_type: filter type to use on input data
:return: ruleset data
"""
if filter_type == "drop":
return self.filter_drop(in_data)
else:
return in_data
def filter_drop(self, in_data):
""" change all alert rules to block
:param in_data: raw input data (ruleset)
:return: new ruleset
"""
output = list()
for line in in_data.split('\n'):
if len(line) > 10:
flowbits_noalert = line.replace(' ', '').find('flowbits:noalert;') > -1
if flowbits_noalert:
pass
elif re.match("^\s*alert", line):
line = "drop %s" % line[line.find('alert')+5:]
elif re.match("^#\s*alert", line):
line = '#drop %s' % line[line.find('alert')+5:]
output.append(line)
return '\n'.join(output)
@staticmethod
def _unpack(src, source_filename, filename=None):
@ -165,10 +137,9 @@ class Downloader(object):
else:
return None
def fetch_version_hash(self, check_url, input_filter, auth=None, headers=None):
def fetch_version_hash(self, check_url, auth=None, headers=None):
""" Calculate a hash value using the download settings and a predefined version url (check_url).
:param check_url: download url, version identifier
:param input_filter: filter to use on received data before save
:param auth: authentication
:param headers: headers to send
:return: None or hash
@ -179,8 +150,7 @@ class Downloader(object):
version_fetch = self.fetch(url=check_url, auth=auth, headers=headers)
if version_fetch:
version_response = version_fetch['handle'].read().decode()
hash_value = [json.dumps(input_filter), json.dumps(auth),
json.dumps(headers), version_response]
hash_value = [json.dumps(auth), json.dumps(headers), version_response]
if not version_fetch['cached']:
syslog.syslog(syslog.LOG_NOTICE, 'version response for %s : %s' % (check_url, version_response))
return hashlib.md5(('\n'.join(hash_value)).encode()).hexdigest()
@ -199,12 +169,11 @@ class Downloader(object):
return line.split(':')[1].strip()
return None
def download(self, url, url_filename, filename, input_filter, auth=None, headers=None, version=None):
def download(self, url, url_filename, filename, auth=None, headers=None, version=None):
""" download ruleset file
:param url: download url
:param url_filename: if provided the filename within the (packet) resource
:param filename: target filename
:param input_filter: filter to use on received data before save
:param auth: authentication
:param headers: headers to send
:param version: version hash
@ -218,10 +187,10 @@ class Downloader(object):
save_data = "#@opnsense_download_hash:%s\n" % version
else:
save_data = ""
save_data += self._unpack(src=fetch_result['handle'],
source_filename=fetch_result['filename'],
filename=url_filename)
save_data = self.filter(save_data, input_filter)
save_data += self._unpack(
src=fetch_result['handle'], source_filename=fetch_result['filename'],
filename=url_filename
)
open(target_filename, 'w', buffering=10240).write(save_data)
except IOError:
syslog.syslog(syslog.LOG_ERR, 'cannot write to %s' % target_filename)

View File

@ -63,11 +63,6 @@ if __name__ == '__main__':
rule_properties[item[0]] = item[1]
elif cnf.has_option(section, 'enabled') and cnf.getint(section, 'enabled') == 1:
enabled_rulefiles[section.strip()] = {}
# input filter
if cnf.has_option(section, 'filter'):
enabled_rulefiles[section.strip()]['filter'] = cnf.get(section, 'filter').strip()
else:
enabled_rulefiles[section.strip()]['filter'] = ""
# download / remove rules
md = metadata.Metadata()
@ -82,24 +77,23 @@ if __name__ == '__main__':
# Required files are always sorted last in list_rules(), add required when there's at least one
# file selected from the metadata package or not on disk yet.
if metadata_sources[rule['metadata_source']] > 0 or not os.path.isfile(full_path):
enabled_rulefiles[rule['filename']] = {'filter': ''}
enabled_rulefiles[rule['filename']] = {}
if rule['filename'] not in enabled_rulefiles or rule['deprecated']:
if not rule['required']:
if os.path.isfile(full_path):
os.remove(full_path)
else:
input_filter = enabled_rulefiles[rule['filename']]['filter']
if ('username' in rule['source'] and 'password' in rule['source']):
auth = (rule['source']['username'], rule['source']['password'])
else:
auth = None
# when metadata supports versioning, check if either version or settings changed before download
remote_hash = dl.fetch_version_hash(check_url=rule['version_url'], input_filter=input_filter,
remote_hash = dl.fetch_version_hash(check_url=rule['version_url'],
auth=auth, headers=rule['http_headers'])
local_hash = dl.installed_file_hash(rule['filename'])
if remote_hash is None or remote_hash != local_hash:
dl.download(url=rule['url'], url_filename=rule['url_filename'],
filename=rule['filename'], input_filter=input_filter, auth=auth,
filename=rule['filename'], auth=auth,
headers=rule['http_headers'], version=remote_hash)
# count number of downloaded files/rules from this metadata package
metadata_sources[rule['metadata_source']] += 1

View File

@ -14,7 +14,6 @@
{% for file in helpers.toList('OPNsense.IDS.files.file') %}
[{{file.filename|default('-')}}]
enabled={{ file.enabled|default('0') }}
filter={{ file.filter|default('') }}
{% endfor %}
{% endif %}