Skip to content

Commit

Permalink
implemented ids_user_rule module
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Dec 20, 2023
1 parent f057c97 commit 5e0578b
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 16 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ not implemented => development => [testing](https://github.com/ansibleguy/collec
| **IDS/IPS** | ansibleguy.opnsense.ids_policy_rule | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | development |
| **IDS/IPS** | ansibleguy.opnsense.ids_rule | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | unstable |
| **IDS/IPS** | ansibleguy.opnsense.ids_ruleset | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | unstable |
| **IDS/IPS** | ansibleguy.opnsense.ids_ruleset_properties | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | development |
| **IDS/IPS** | ansibleguy.opnsense.ids_user_rule | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | development |
| **IDS/IPS** | ansibleguy.opnsense.ids_user_rule | [Docs](https://opnsense.ansibleguy.net/en/latest/modules/ids.html) | unstable |


### Roadmap
Expand Down
99 changes: 87 additions & 12 deletions docs/source/modules/ids.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ Intrusion Prevention System

**STATE**: unstable

**TESTS**: `ansibleguy.opnsense.ids_general <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_general.yml>`_,
`ansibleguy.opnsense.ids_action <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_action.yml>`_,
`ansibleguy.opnsense.ids_policy <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_policy.yml>`_,
`ansibleguy.opnsense.ids_policy_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_policy_rule.yml>`_,
`ansibleguy.opnsense.ids_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_rule.yml>`_,
`ansibleguy.opnsense.ids_ruleset <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_ruleset.yml>`_,
`ansibleguy.opnsense.ids_ruleset_properties <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_ruleset_properties.yml>`_,
`ansibleguy.opnsense.ids_user_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_user_rule.yml>`_,
**TESTS**: `ansibleguy.opnsense.ids_general <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_general.yml>`_ |
`ansibleguy.opnsense.ids_action <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_action.yml>`_ |
`ansibleguy.opnsense.ids_policy <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_policy.yml>`_ |
`ansibleguy.opnsense.ids_policy_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_policy_rule.yml>`_ |
`ansibleguy.opnsense.ids_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_rule.yml>`_ |
`ansibleguy.opnsense.ids_ruleset <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_ruleset.yml>`_ |
`ansibleguy.opnsense.ids_user_rule <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/ids_user_rule.yml>`_

**API Docs**: `IDS <https://docs.opnsense.org/development/api/core/ids.html>`_

Expand Down Expand Up @@ -92,6 +91,22 @@ ansibleguy.opnsense.ids_rule
"reload","boolean","false","true","\-", .. include:: ../_include/param_reload.rst


ansibleguy.opnsense.ids_user_rule
=================================

.. csv-table:: Definition
:header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment"
:widths: 15 10 10 10 10 45

"name","string","true","\-","description, desc","Unique rule name"
"source_ip","string","false","\-","source, src_ip, src","Set the source IP or network to match. Leave this field empty for using 'any'"
"destination_ip","string","false","\-","destination, dst_ip, dst","Set the destination IP or network to match. Leave this field empty for using 'any'"
"ssl_fingerprint","string","false","\-","fingerprint, ssl_fp","The SSL fingerprint, for example: 'B5:E1:B3:70:5E:7C:FF:EB:92:C4:29:E5:5B:AC:2F:AE:70:17:E9:9E'"
"action","string","false","alert",a","One of 'alert', 'drop', 'pass'. Set action to perform here, only used when in IPS mode"
"bypass","boolean","false","false","bp","Set bypass keyword. Increases traffic throughput. Suricata reads a packet, decodes it, checks it in the flow table. If the corresponding flow is local bypassed then it simply skips all streaming, detection and output and the packet goes directly out in IDS mode and to verdict in IPS mode"
"enabled","boolean","false","true","\-","En- or disable the rule"
"reload","boolean","false","true","\-", .. include:: ../_include/param_reload.rst

Usage
*****

Expand Down Expand Up @@ -295,11 +310,71 @@ ansibleguy.opnsense.ids_rule
sid: 2400011
enabled: false
- name: Listing Settings
- name: Listing Rules
ansibleguy.opnsense.list:
# target: 'ids_rule'
register: existing_settings
register: existing_rules
- name: Printing Settings
- name: Printing Rules
ansible.builtin.debug:
var: existing_settings.data
var: existing_rules.data
ansibleguy.opnsense.ids_user_rule
=================================

.. code-block:: yaml
- hosts: localhost
gather_facts: false
module_defaults:
group/ansibleguy.opnsense.all:
firewall: 'opnsense.template.ansibleguy.net'
api_credential_file: '/home/guy/.secret/opn.key'
ansibleguy.opnsense.list:
target: 'ids_user_rule'
tasks:
- name: Example
ansibleguy.opnsense.ids_user_rule:
name: 'Example'
# source_ip: ''
# destination_ip: ''
# ssl_fingerprint: ''
# action: 'alert'
# bypass: false
# enabled: true
# reload: true
# debug: false
- name: Adding
ansibleguy.opnsense.ids_user_rule:
name: 'ANSIBLE_TEST_1_1'
source_ip: '192.168.10.1'
destination_ip: '1.1.1.1'
action: 'alert'
bypass: false
- name: Disabling
ansibleguy.opnsense.ids_user_rule:
name: 'ANSIBLE_TEST_1_1'
source_ip: '192.168.10.1'
destination_ip: '1.1.1.1'
action: 'alert'
bypass: false
enabled: false
- name: Removing
ansibleguy.opnsense.ids_user_rule:
name: 'ANSIBLE_TEST_1_1'
state: 'absent'
- name: Listing Rules
ansibleguy.opnsense.list:
# target: 'ids_user_rule'
register: existing_rules
- name: Printing Rules
ansible.builtin.debug:
var: existing_rules.data
4 changes: 3 additions & 1 deletion plugins/module_utils/main/ids_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Rule(BaseModule):
API_MOD = 'ids'
API_CONT = 'settings'
API_CONT_REL = 'service'
API_CMD_REL = 'reconfigure'
API_CMD_REL = 'reloadRules'
FIELDS_CHANGE = ['action']
FIELDS_ALL = ['enabled']
FIELDS_ALL.extend(FIELDS_CHANGE)
Expand Down Expand Up @@ -51,6 +51,8 @@ def process(self) -> None:
self.toggle()

def _search_call(self) -> list:
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
# https://github.com/opnsense/core/issues/7094
existing = self.s.post(cnf={
**self.call_cnf,
'command': self.CMDS['search'],
Expand Down
2 changes: 2 additions & 0 deletions plugins/module_utils/main/ids_ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def process(self) -> None:
self.toggle()

def _search_call(self) -> list:
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
# https://github.com/opnsense/core/issues/7094
existing = self.s.post(cnf={
**self.call_cnf,
'command': self.CMDS['search'],
Expand Down
75 changes: 75 additions & 0 deletions plugins/module_utils/main/ids_user_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Rule(BaseModule):
FIELD_ID = 'description'
CMDS = {
'add': 'addUserRule',
'set': 'setUserRule',
'del': 'delUserRule',
'search': 'searchUserRule',
'detail': 'getUserRule',
'toggle': 'toggleUserRule',
}
API_KEY = 'rule'
API_KEY_PATH = f'userDefinedRules.{API_KEY}'
API_MOD = 'ids'
API_CONT = 'settings'
API_CONT_REL = 'service'
API_CMD_REL = 'reloadRules'
FIELDS_CHANGE = ['source_ip', 'destination_ip', 'ssl_fingerprint', 'action', 'bypass']
FIELDS_ALL = ['enabled', FIELD_ID]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TRANSLATE = {
'source_ip': 'source',
'destination_ip': 'destination',
'ssl_fingerprint': 'fingerprint',
}
FIELDS_TYPING = {
'bool': ['enabled', 'bypass'],
'select': ['action'],
}
EXIST_ATTR = 'rule'
QUERY_MAX_RULES = 5000

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.rule = {}
self.exists = False

def check(self):
self._search_call()
self.r['diff']['after'] = self.b.build_diff(data=self.p)

def get_existing(self) -> list:
return self._search_call()

def _search_call(self) -> list:
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
# https://github.com/opnsense/core/issues/7094
existing = self.s.post(cnf={
**self.call_cnf,
'command': self.CMDS['search'],
'data': {'current': 1, 'rowCount': self.QUERY_MAX_RULES, 'sort': self.FIELD_ID},
})['rows']

if self.FIELD_ID in self.p: # list module
for rule in existing:
if rule[self.FIELD_ID] == self.p[self.FIELD_ID]:
self.exists = True
self.call_cnf['params'] = [rule['uuid']]
# pylint: disable=W0212
self.rule = self.b._simplify_existing(
self.s.get(cnf={
**self.call_cnf,
'command': self.CMDS['detail'],
})[self.API_KEY]
)
self.rule['uuid'] = rule['uuid']
self.r['diff']['before'] = self.rule

return existing
89 changes: 89 additions & 0 deletions plugins/modules/ids_user_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

# Copyright: (C) 2023, AnsibleGuy <guy@ansibleguy.net>
# GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt)

# see: https://docs.opnsense.org/development/api/core/ids.html

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.handler import \
module_dependency_error, MODULE_EXCEPTIONS

try:
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.wrapper import module_wrapper
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \
diff_remove_empty
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.defaults.main import \
OPN_MOD_ARGS, STATE_MOD_ARG, RELOAD_MOD_ARG
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.ids_user_rule import Rule

except MODULE_EXCEPTIONS:
module_dependency_error()


# DOCUMENTATION = 'https://opnsense.ansibleguy.net/en/latest/modules/ids.html'
# EXAMPLES = 'https://opnsense.ansibleguy.net/en/latest/modules/ids.html'


def run_module():
module_args = dict(
description=dict(
type='str', required=True, aliases=['name', 'desc'],
description='Unique rule name',
),
source_ip=dict(
type='str', required=False, aliases=['source', 'src_ip', 'src'], default='',
description="Set the source IP or network to match. Leave this field empty for using 'any'",
),
destination_ip=dict(
type='str', required=False, aliases=['destination', 'dst_ip', 'dst'], default='',
description="Set the destination IP or network to match. Leave this field empty for using 'any'",
),
ssl_fingerprint=dict(
type='str', required=False, aliases=['fingerprint', 'ssl_fp'], default='',
description="The SSL fingerprint, for example: "
"'B5:E1:B3:70:5E:7C:FF:EB:92:C4:29:E5:5B:AC:2F:AE:70:17:E9:9E'",
),
action=dict(
type='str', required=False, aliases=['a'], default='alert',
choices=['alert', 'drop', 'pass'],
description='Set action to perform here, only used when in IPS mode',
),
bypass=dict(
type='bool', required=False, aliases=['bp'], default=False,
description='Set bypass keyword. Increases traffic throughput. Suricata reads a packet, '
'decodes it, checks it in the flow table. If the corresponding flow is local '
'bypassed then it simply skips all streaming, detection and output and the packet '
'goes directly out in IDS mode and to verdict in IPS mode',
),
**STATE_MOD_ARG,
**RELOAD_MOD_ARG,
**OPN_MOD_ARGS,
)

result = dict(
changed=False,
diff={
'before': {},
'after': {},
}
)

module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)

module_wrapper(Rule(module=module, result=result))

result['diff'] = diff_remove_empty(result['diff'])
module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()
6 changes: 5 additions & 1 deletion plugins/modules/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
'frr_bgp_as_path', 'frr_bgp_route_map', 'frr_ospf_prefix_list', 'frr_ospf_route_map', 'webproxy_forward',
'webproxy_acl', 'webproxy_icap', 'webproxy_auth', 'ipsec_connection', 'ipsec_pool',
'ipsec_child', 'ipsec_vti', 'ipsec_auth_local', 'ipsec_auth_remote', 'frr_general', 'unbound_general',
'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset',
'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset', 'ids_user_rule',
]


Expand Down Expand Up @@ -340,6 +340,10 @@ def run_module():
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.ids_ruleset import \
Ruleset as Target_Obj

elif target == 'ids_user_rule':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.ids_user_rule import \
Rule as Target_Obj

except AttributeError:
module_dependency_error()

Expand Down
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ run_test 'ids_action' 1
run_test 'ids_general' 1
run_test 'ids_ruleset' 1
run_test 'ids_rule' 1
run_test 'ids_user_rule' 1
# run_test 'ids_policy' 1
run_test 'system' 1
run_test 'package' 1
Expand Down
5 changes: 5 additions & 0 deletions tests/cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,8 @@
- name: Cleanup IDS Rule
ansibleguy.opnsense.ids_rule:
sid: 2400000

- name: Cleanup IDS User-Rule
ansibleguy.opnsense.ids_user_rule:
name: 'ANSIBLE_TEST_1_1'
state: 'absent'
Loading

0 comments on commit 5e0578b

Please sign in to comment.