├── __init__.py ├── README.md ├── .gitignore ├── example.ipynb ├── LICENSE ├── snortparser.py └── dicts.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snortparser 2 | 3 | > This is a fork of snortparser by g-rd. The original repo can be found at https://github.com/g-rd/snortparser. 4 | 5 | Snort rule parser written in python. The main goal for this library is to validate snort rules and have them parsed into a workable dictionary object. A interactive python notebook can be found [here](example.ipynb). 6 | 7 | The parser class accepts a snort rule as input and returns a dictionary that contains the parsed output. 8 | 9 | ```python 10 | from snortparser import Parser 11 | 12 | # define a snort rule 13 | rule = ('alert tcp $HOME_NET any -> !$EXTERNAL_NET any (msg:\"MALWARE-BACKDOOR - Dagger_1.4.0\"; flow:to_client,established; content:\"2|00 00 00 06 00 00 00|Drives|24 00|\"; depth:16; metadata:ruleset community; classtype:misc-activity; sid:105; rev:14;)') 14 | 15 | # parse the rule 16 | parsed = Parser(rule) 17 | 18 | # print the parsed rule 19 | print(parsed.header) 20 | print(parsed.options) 21 | ``` 22 | 23 | **NOTE**: If the parser is unable to parse the rule, it will return a `ValueError` with the invalid rule item. Additionally, it does not care about misplaced spaces in the headers ip and port definitions like: " alert tcp ![ 127.0.0.1, !8.8.8.8 ] any --> ". This is by design, since I am not sure if snort cares about header syntax that much. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/windows,macos,python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,python 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Python ### 38 | # Byte-compiled / optimized / DLL files 39 | __pycache__/ 40 | *.py[cod] 41 | *$py.class 42 | 43 | # C extensions 44 | *.so 45 | 46 | # Distribution / packaging 47 | .Python 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | wheels/ 60 | share/python-wheels/ 61 | *.egg-info/ 62 | .installed.cfg 63 | *.egg 64 | MANIFEST 65 | 66 | # PyInstaller 67 | # Usually these files are written by a python script from a template 68 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 69 | *.manifest 70 | *.spec 71 | 72 | # Installer logs 73 | pip-log.txt 74 | pip-delete-this-directory.txt 75 | 76 | # Unit test / coverage reports 77 | htmlcov/ 78 | .tox/ 79 | .nox/ 80 | .coverage 81 | .coverage.* 82 | .cache 83 | nosetests.xml 84 | coverage.xml 85 | *.cover 86 | *.py,cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | cover/ 90 | 91 | # Translations 92 | *.mo 93 | *.pot 94 | 95 | # Django stuff: 96 | *.log 97 | local_settings.py 98 | db.sqlite3 99 | db.sqlite3-journal 100 | 101 | # Flask stuff: 102 | instance/ 103 | .webassets-cache 104 | 105 | # Scrapy stuff: 106 | .scrapy 107 | 108 | # Sphinx documentation 109 | docs/_build/ 110 | 111 | # PyBuilder 112 | .pybuilder/ 113 | target/ 114 | 115 | # Jupyter Notebook 116 | .ipynb_checkpoints 117 | 118 | # IPython 119 | profile_default/ 120 | ipython_config.py 121 | 122 | # pyenv 123 | # For a library or package, you might want to ignore these files since the code is 124 | # intended to run in multiple environments; otherwise, check them in: 125 | # .python-version 126 | 127 | # pipenv 128 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 129 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 130 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 131 | # install all needed dependencies. 132 | #Pipfile.lock 133 | 134 | # poetry 135 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 136 | # This is especially recommended for binary packages to ensure reproducibility, and is more 137 | # commonly ignored for libraries. 138 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 139 | #poetry.lock 140 | 141 | # pdm 142 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 143 | #pdm.lock 144 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 145 | # in version control. 146 | # https://pdm.fming.dev/#use-with-ide 147 | .pdm.toml 148 | 149 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 150 | __pypackages__/ 151 | 152 | # Celery stuff 153 | celerybeat-schedule 154 | celerybeat.pid 155 | 156 | # SageMath parsed files 157 | *.sage.py 158 | 159 | # Environments 160 | .env 161 | .venv 162 | env/ 163 | venv/ 164 | ENV/ 165 | env.bak/ 166 | venv.bak/ 167 | 168 | # Spyder project settings 169 | .spyderproject 170 | .spyproject 171 | 172 | # Rope project settings 173 | .ropeproject 174 | 175 | # mkdocs documentation 176 | /site 177 | 178 | # mypy 179 | .mypy_cache/ 180 | .dmypy.json 181 | dmypy.json 182 | 183 | # Pyre type checker 184 | .pyre/ 185 | 186 | # pytype static type analyzer 187 | .pytype/ 188 | 189 | # Cython debug symbols 190 | cython_debug/ 191 | 192 | # PyCharm 193 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 194 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 195 | # and can be added to the global gitignore or merged into this file. For a more nuclear 196 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 197 | #.idea/ 198 | 199 | ### Python Patch ### 200 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 201 | poetry.toml 202 | 203 | 204 | ### Windows ### 205 | # Windows thumbnail cache files 206 | Thumbs.db 207 | Thumbs.db:encryptable 208 | ehthumbs.db 209 | ehthumbs_vista.db 210 | 211 | # Dump file 212 | *.stackdump 213 | 214 | # Folder config file 215 | [Dd]esktop.ini 216 | 217 | # Recycle Bin used on file shares 218 | $RECYCLE.BIN/ 219 | 220 | # Windows Installer files 221 | *.cab 222 | *.msi 223 | *.msix 224 | *.msm 225 | *.msp 226 | 227 | # Windows shortcuts 228 | *.lnk 229 | 230 | # End of https://www.toptal.com/developers/gitignore/api/windows,macos,python 231 | -------------------------------------------------------------------------------- /example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from snortparser import Parser" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "{'header': OrderedDict([('action', 'alert'),\n", 21 | " ('proto', 'tcp'),\n", 22 | " ('source', (True, 'any')),\n", 23 | " ('src_port', (True, 'any')),\n", 24 | " ('arrow', '->'),\n", 25 | " ('destination', (True, 'any')),\n", 26 | " ('dst_port', (True, 'any'))]),\n", 27 | " 'options': OrderedDict([(0,\n", 28 | " ('msg',\n", 29 | " ['\"Non-Std TCP Client Traffic contains \"HX1|3a|\" \"HX2|3a|\" \"HX3|3a|\" \"HX4|3a|\" (PLUGX Variant)\"'])),\n", 30 | " (1, ('sid', ['7238331'])),\n", 31 | " (2, ('rev', ['20170428'])),\n", 32 | " (3, ('flow', ['established', 'to_server'])),\n", 33 | " (4, ('content', ['\"Accept|3a 20 2a 2f 2a|\"'])),\n", 34 | " (5, ('nocase', '')),\n", 35 | " (6, ('content', ['\"HX1|3a|\"'])),\n", 36 | " (7, ('distance', ['0'])),\n", 37 | " (8, ('within', ['6'])),\n", 38 | " (9, ('fast_pattern', '')),\n", 39 | " (10, ('content', ['\"HX2|3a|\"'])),\n", 40 | " (11, ('nocase', '')),\n", 41 | " (12, ('distance', ['0'])),\n", 42 | " (13, ('content', ['\"HX3|3a|\"'])),\n", 43 | " (14, ('nocase', '')),\n", 44 | " (15, ('distance', ['0'])),\n", 45 | " (16, ('content', ['\"HX4|3a|\"'])),\n", 46 | " (17, ('nocase', '')),\n", 47 | " (18, ('distance', ['0'])),\n", 48 | " (19, ('classtype', ['nonstd-tcp'])),\n", 49 | " (20,\n", 50 | " ('threshold',\n", 51 | " ['type limit', ' track by_src', ' count 1 ', ' seconds 60'])),\n", 52 | " (21, ('priority', ['X'])),\n", 53 | " (22,\n", 54 | " ('reference',\n", 55 | " ['url',\n", 56 | " 'cti.cert.europa.eu/index.php/mnuthreatobject/indicatorslist/details/83/12844'])),\n", 57 | " (23, ('gid', ['1']))])}" 58 | ] 59 | }, 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "rules = [\n", 67 | " 'alert tcp any any -> any any (msg:\"Non-Std TCP Client Traffic contains \"HX1|3a|\" \"HX2|3a|\" \"HX3|3a|\" \"HX4|3a|\" (PLUGX Variant)\"; sid:7238331; rev:20170428; flow:established,to_server; content:\"Accept|3a 20 2a 2f 2a|\"; nocase; content:\"HX1|3a|\"; distance:0; within:6; fast_pattern; content:\"HX2|3a|\"; nocase; distance:0; content:\"HX3|3a|\"; nocase; distance:0; content:\"HX4|3a|\"; nocase; distance:0; classtype:nonstd-tcp; threshold:type limit, track by_src, count 1 , seconds 60;priority:X; reference:url,cti.cert.europa.eu/index.php/mnuthreatobject/indicatorslist/details/83/12844; gid:1;)',\n", 68 | " 'alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg: \"CrowdStrike Possible Empire Powershell C2 Request\"; flow: to_server,established; content: \"GET /login/process.php HTTP/1.1|0d 0a|Cookie: session=\"; depth:49; content: \"=|0d 0a|User-Agent: Mozilla/5.0 (Windows NT 6.1\\\\; WOW64\\\\; Trident/7.0\\\\; rv:11.0) like Gecko|0d 0a|Host: \"; offset:76; depth:91; content: \"Connection: Keep-Alive|0d 0a 0d 0a|\"; classtype: trojan-activity; sid:8001380; rev:20190308;)',\n", 69 | " 'alert tcp $HOME_NET ANY -> $EXTERNAL_NET $HTTP_PORTS (msg: \"CrowdStrike VOODOO BEAR VBS Backdoor [CSIT-18082]\"; flow: established, to_server; content: \"POST\"; http_method; content: \"Content-Type:|20|application/x-www-form-urlencoded\"; http_header; content: \"ui=en-US&\"; http_client_body; depth: 9; content: \"_LinkId=\"; http_client_body; within: 13; classtype: trojan-activity; metadata: service http; sid:8001209; rev:20180523; reference:url,falcon.crowdstrike.com/intelligence/reports/CSIT-18082;)',\n", 70 | "]\n", 71 | "\n", 72 | "Parser(rules[0]).all" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "data": { 82 | "text/plain": [ 83 | "{'header': OrderedDict([('action', 'alert'),\n", 84 | " ('proto', 'tcp'),\n", 85 | " ('source', (True, '$HOME_NET')),\n", 86 | " ('src_port', (True, 'any')),\n", 87 | " ('arrow', '->'),\n", 88 | " ('destination', (True, '$EXTERNAL_NET')),\n", 89 | " ('dst_port', (True, 'any'))]),\n", 90 | " 'options': OrderedDict([(0,\n", 91 | " ('msg',\n", 92 | " [' \"CrowdStrike Possible Empire Powershell C2 Request\"'])),\n", 93 | " (1, ('flow', [' to_server', 'established'])),\n", 94 | " (2,\n", 95 | " ('content',\n", 96 | " [' \"GET /login/process.php HTTP/1.1|0d 0a|Cookie: session=\"'])),\n", 97 | " (3, ('depth', ['49'])),\n", 98 | " (4,\n", 99 | " ('content',\n", 100 | " [' \"=|0d 0a|User-Agent: Mozilla/5.0 (Windows NT 6.1\\\\ WOW64\\\\ Trident/7.0\\\\ rv:11.0) like Gecko|0d 0a|Host: \"'])),\n", 101 | " (5, ('offset', ['76'])),\n", 102 | " (6, ('depth', ['91'])),\n", 103 | " (7, ('content', [' \"Connection: Keep-Alive|0d 0a 0d 0a|\"'])),\n", 104 | " (8, ('classtype', [' trojan-activity'])),\n", 105 | " (9, ('sid', ['8001380'])),\n", 106 | " (10, ('rev', ['20190308']))])}" 107 | ] 108 | }, 109 | "execution_count": 3, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "Parser(rules[1]).all" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 4, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "data": { 125 | "text/plain": [ 126 | "{'header': OrderedDict([('action', 'alert'),\n", 127 | " ('proto', 'tcp'),\n", 128 | " ('source', (True, '$HOME_NET')),\n", 129 | " ('src_port', (True, 'ANY')),\n", 130 | " ('arrow', '->'),\n", 131 | " ('destination', (True, '$EXTERNAL_NET')),\n", 132 | " ('dst_port', (True, '$HTTP_PORTS'))]),\n", 133 | " 'options': OrderedDict([(0,\n", 134 | " ('msg',\n", 135 | " [' \"CrowdStrike VOODOO BEAR VBS Backdoor [CSIT-18082]\"'])),\n", 136 | " (1, ('flow', [' established', ' to_server'])),\n", 137 | " (2, ('content', [' \"POST\"'])),\n", 138 | " (3, ('http_method', '')),\n", 139 | " (4,\n", 140 | " ('content',\n", 141 | " [' \"Content-Type:|20|application/x-www-form-urlencoded\"'])),\n", 142 | " (5, ('http_header', '')),\n", 143 | " (6, ('content', [' \"ui=en-US&\"'])),\n", 144 | " (7, ('http_client_body', '')),\n", 145 | " (8, ('depth', [' 9'])),\n", 146 | " (9, ('content', [' \"_LinkId=\"'])),\n", 147 | " (10, ('http_client_body', '')),\n", 148 | " (11, ('within', [' 13'])),\n", 149 | " (12, ('classtype', [' trojan-activity'])),\n", 150 | " (13, ('metadata', [' service http'])),\n", 151 | " (14, ('sid', ['8001209'])),\n", 152 | " (15, ('rev', ['20180523'])),\n", 153 | " (16,\n", 154 | " ('reference',\n", 155 | " ['url',\n", 156 | " 'falcon.crowdstrike.com/intelligence/reports/CSIT-18082']))])}" 157 | ] 158 | }, 159 | "execution_count": 4, 160 | "metadata": {}, 161 | "output_type": "execute_result" 162 | } 163 | ], 164 | "source": [ 165 | "Parser(rules[2]).all" 166 | ] 167 | } 168 | ], 169 | "metadata": { 170 | "kernelspec": { 171 | "display_name": "Python 3", 172 | "language": "python", 173 | "name": "python3" 174 | }, 175 | "language_info": { 176 | "codemirror_mode": { 177 | "name": "ipython", 178 | "version": 3 179 | }, 180 | "file_extension": ".py", 181 | "mimetype": "text/x-python", 182 | "name": "python", 183 | "nbconvert_exporter": "python", 184 | "pygments_lexer": "ipython3", 185 | "version": "3.10.4" 186 | }, 187 | "orig_nbformat": 4, 188 | "vscode": { 189 | "interpreter": { 190 | "hash": "2ab98c4b59af728aa8bd9922870b996a7fc5861c87b81242319b92eec59b5fef" 191 | } 192 | } 193 | }, 194 | "nbformat": 4, 195 | "nbformat_minor": 2 196 | } 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /snortparser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import ipaddress 3 | import collections 4 | from typing import Tuple, List, Dict, Any 5 | 6 | try: 7 | from .dicts import Dicts 8 | except ImportError: 9 | from dicts import Dicts 10 | 11 | 12 | class Parser(object): 13 | """ 14 | This will take an array of lines and parse it and hand back a dictionary 15 | NOTE: if you pass an invalid rule to the parser, it will a raise ValueError. 16 | """ 17 | 18 | def __init__(self, rule): 19 | self.dicts = Dicts() 20 | self.rule = rule 21 | self.header = self.parse_header() 22 | self.options = self.parse_options() 23 | self.validate_options(self.options) 24 | self.data = {"header": self.header, "options": self.options} 25 | self.all = self.data 26 | 27 | def __iter__(self): 28 | yield self.data 29 | 30 | def __getitem__(self, key): 31 | if key == "all": 32 | return self.data 33 | else: 34 | return self.data[key] 35 | 36 | @staticmethod 37 | def actions(action: str) -> str: 38 | actions = { 39 | "alert", 40 | "log", 41 | "pass", 42 | "activate", 43 | "dynamic", 44 | "drop", 45 | "reject", 46 | "sdrop", 47 | } 48 | 49 | if action in actions: 50 | return action 51 | else: 52 | raise ValueError(f"Invalid action specified {action}") 53 | 54 | @staticmethod 55 | def proto(proto: str) -> str: 56 | protos = {"tcp", "udp", "icmp", "ip"} 57 | 58 | if proto.lower() in protos: 59 | return proto 60 | else: 61 | raise ValueError(f"Unsupported Protocol {proto}") 62 | 63 | @staticmethod 64 | def __ip_to_tuple(ip: str) -> Tuple: 65 | if ip.startswith("!"): 66 | ip = ip.lstrip("!") 67 | return False, ip 68 | else: 69 | return True, ip 70 | 71 | def __form_ip_list(self, ip_list: str) -> List: 72 | ip_list = ip_list.split(",") 73 | ips = [] 74 | for ip in ip_list: 75 | ips.append(self.__ip_to_tuple(ip)) 76 | return ips 77 | 78 | def __flatten_ip(self, ip): 79 | list_deny = True 80 | if ip.startswith("!"): 81 | list_deny = False 82 | ip = ip.lstrip("!") 83 | _ip_list = [] 84 | _not_nest = True 85 | ip = re.sub(r"^\[|\]$", "", ip) 86 | ip = re.sub(r'"', "", ip) 87 | if re.search(r"(\[.*\])", ip): 88 | _not_nest = False 89 | nest = re.split(r",(!?\[.*\])", ip) 90 | nest = filter(None, nest) 91 | # unnest from _ip_list 92 | _return_ips = [] 93 | for item in nest: 94 | if re.match(r"^\[|^!\[", item): 95 | nested = self.__flatten_ip(item) 96 | _return_ips.append(nested) 97 | continue 98 | else: 99 | _ip_list = self.__form_ip_list(item) 100 | for _ip in _ip_list: 101 | _return_ips.append(_ip) 102 | return list_deny, _return_ips 103 | if _not_nest: 104 | _ip_list = self.__form_ip_list(ip) 105 | return list_deny, _ip_list 106 | 107 | def __validate_ip(self, ips): 108 | variables = { 109 | "$EXTERNAL_NET", 110 | "$HTTP_SERVERS", 111 | "$INTERNAL_NET", 112 | "$SQL_SERVERS", 113 | "$SMTP_SERVERS", 114 | "$DNS_SERVERS", 115 | "$HOME_NET", 116 | "$TELNET_SERVERS", 117 | "$SIP_SERVERS", 118 | "any", 119 | } 120 | 121 | for item in ips: 122 | 123 | if isinstance(item, bool): 124 | pass 125 | 126 | if isinstance(item, list): 127 | for ip in item: 128 | self.__validate_ip(ip) 129 | 130 | if isinstance(item, str): 131 | if item not in variables: 132 | if "/" in item: 133 | ipaddress.ip_network(item) 134 | else: 135 | ipaddress.ip_address(item) 136 | return True 137 | 138 | def ip(self, ip): 139 | if isinstance(ip, str): 140 | ip = ip.strip('"') 141 | if re.search(r",", ip): 142 | item = self.__flatten_ip(ip) 143 | ip = item 144 | else: 145 | ip = self.__ip_to_tuple(ip) 146 | valid = self.__validate_ip(ip) 147 | if valid: 148 | return ip 149 | else: 150 | raise ValueError(f"Invalid ip or variable: {ip}") 151 | 152 | @staticmethod 153 | def port(port): 154 | variables = {"any", "$HTTP_PORTS"} 155 | 156 | # is the source marked as not 157 | if port.startswith("!"): 158 | if_not = False 159 | port = port.strip("!") 160 | else: 161 | if_not = True 162 | # is it a list ? 163 | # if it is, then make it a list from the string 164 | """ 165 | Snort allows for ports marked between 166 | square brackets and are used to define lists 167 | correct: 168 | >> [80:443,!90,8080] 169 | >> ![80:443] 170 | >> [!80:443] 171 | """ 172 | 173 | if port.startswith("["): 174 | if port.endswith("]"): 175 | port = port[1:-1].split(",") 176 | else: 177 | raise ValueError("Port list is malformed") 178 | 179 | if isinstance(port, list): 180 | ports = [] 181 | 182 | for item in port: 183 | not_range = True 184 | 185 | if ":" in item: 186 | # Checking later on if port is [prt:] or [:prt] 187 | open_range = False 188 | items = item.split(":", 1) 189 | message = "" 190 | for prt in items: 191 | message = "Port range is malformed %s" % item 192 | prt = prt.lstrip("!") 193 | if not prt: 194 | open_range = True 195 | continue 196 | 197 | try: 198 | prt = int(prt) 199 | except Exception: 200 | raise ValueError(message) 201 | 202 | if prt < 0 or prt > 65535: 203 | raise ValueError(message) 204 | 205 | for index, value in enumerate(items): 206 | value = value.lstrip("!") 207 | items[index] = value 208 | 209 | if not open_range: 210 | try: 211 | a = int(items[-1]) 212 | b = int(items[0]) 213 | except Exception: 214 | raise ValueError(message) 215 | if a - b < 0: 216 | raise ValueError(message) 217 | not_range = False 218 | 219 | port_not = True 220 | if re.search("^!", item): 221 | port_not = False 222 | item = item.strip("!") 223 | if not_range: 224 | if item.lower() or item in variables: 225 | ports.append((port_not, item)) 226 | continue 227 | try: 228 | prt = int(item) 229 | if prt < 0 or prt > 65535: 230 | raise ValueError("Port is out of range {}".format(item)) 231 | except ValueError: 232 | raise ValueError("Unknown port {}".format(item)) 233 | ports.append((port_not, item)) 234 | 235 | return if_not, ports 236 | 237 | if isinstance(port, str): 238 | """ 239 | Parsing ports like: :8080, 80:, 80:443 240 | and passes all variables ex: $HTTP 241 | ranges do not accept denial (!) 242 | """ 243 | if port or port.lower() in variables or re.search(r"^\$+", port): 244 | return if_not, port 245 | 246 | if re.search(":", port): 247 | ports = port.split(":") 248 | for portl in ports: 249 | portl.lstrip("!") 250 | if not portl: 251 | continue 252 | if portl or portl.lower() in variables: 253 | continue 254 | 255 | try: 256 | portl = int(portl) 257 | except ValueError: 258 | raise ValueError(f"Port is not an int: {port}") 259 | 260 | if portl < 0 or portl > 65535: 261 | raise ValueError(f"Port is out of range: {port}") 262 | 263 | return if_not, port 264 | 265 | # Parsing a single port single port accepts denial. 266 | try: 267 | if not int(port) > 65535 or int(port) < 0: 268 | return if_not, port 269 | 270 | if int(port) > 65535 or int(port) < 0: 271 | raise ValueError 272 | 273 | except Exception: 274 | raise ValueError(f"Unknown port: {port}") 275 | else: 276 | raise ValueError(f"Unknown port {port}") 277 | 278 | def destination(self, dst): 279 | destinations = {"->": "to_dst", "<>": "bi_direct"} 280 | 281 | if dst in destinations: 282 | return dst 283 | else: 284 | raise ValueError(f"Invalid destination variable {dst}") 285 | 286 | def get_header(self): 287 | if re.match(r"(^[a-z|A-Z].+?)?(\(.+;\)|;\s\))", self.rule.lstrip()): 288 | header = self.rule.split("(", 1) 289 | return header[0] 290 | else: 291 | msg = f"Error in syntax, check if rule has been closed properly {self.rule}" 292 | raise SyntaxError(msg) 293 | 294 | @staticmethod 295 | def remove_leading_spaces(string: str) -> str: 296 | return string.strip() 297 | 298 | def get_options(self): 299 | options = "{}".format(self.rule.split("(", 1)[-1].lstrip().rstrip()) 300 | if not options.endswith(")"): 301 | msg = "Snort rule options is not closed properly, you have a syntax error" 302 | raise ValueError(msg) 303 | 304 | op_list = list() 305 | 306 | value = "" 307 | option = "" 308 | last_char = "" 309 | 310 | for char in options.rstrip(")"): 311 | if char != ";": 312 | value = value + char 313 | option = option + char 314 | 315 | if char == ";" and last_char != "\\": 316 | op_list.append(option.strip()) 317 | value = option = "" 318 | 319 | last_char = char 320 | 321 | return op_list 322 | 323 | def parse_header(self): 324 | """ 325 | OrderedDict([('action', 'alert'), ('proto', 'tcp'), ('source', \ 326 | (True, '$HOME_NET')), ('src_port', (True, 'any')), ('arrow', '->'), \ 327 | ('destination', (False, '$EXTERNAL_NET')), ('dst_port', (True, 'any'))]) 328 | """ 329 | 330 | if self.get_header(): 331 | header = self.get_header() 332 | if re.search(r"[,\[\]]\s", header): 333 | header = re.sub(r",\s+", ",", header) 334 | header = re.sub(r"\s+,", ",", header) 335 | header = re.sub(r"\[\s+", "[", header) 336 | header = re.sub(r"\s+\]", "]", header) 337 | header = header.split() 338 | else: 339 | raise ValueError("Header is missing, or unparsable") 340 | 341 | # get rid of empty list elements 342 | header = list(filter(None, header)) 343 | header_dict = collections.OrderedDict() 344 | size = len(header) 345 | if not size == 7 and not size == 1: 346 | msg = "Snort rule header is malformed %s" % header 347 | raise ValueError(msg) 348 | 349 | for item in header: 350 | if "action" not in header_dict: 351 | action = self.actions(item) 352 | header_dict["action"] = action 353 | continue 354 | 355 | if "proto" not in header_dict: 356 | try: 357 | proto = self.proto(item) 358 | header_dict["proto"] = proto 359 | continue 360 | except Exception as perror: 361 | raise ValueError(perror) 362 | 363 | if "source" not in header_dict: 364 | try: 365 | src_ip = self.ip(item) 366 | header_dict["source"] = src_ip 367 | continue 368 | except Exception as serror: 369 | raise ValueError(serror) 370 | 371 | if "src_port" not in header_dict: 372 | src_port = self.port(item) 373 | header_dict["src_port"] = src_port 374 | continue 375 | 376 | if "arrow" not in header_dict: 377 | dst = self.destination(item) 378 | header_dict["arrow"] = dst 379 | continue 380 | 381 | if "destination" not in header_dict: 382 | dst_ip = self.ip(item) 383 | header_dict["destination"] = dst_ip 384 | continue 385 | 386 | if "dst_port" not in header_dict: 387 | dst_port = self.port(item) 388 | header_dict["dst_port"] = dst_port 389 | continue 390 | 391 | return header_dict 392 | 393 | def parse_options(self, rule=None): 394 | if rule: 395 | self.rule = rule 396 | opts = self.get_options() 397 | 398 | options_dict = collections.OrderedDict() 399 | for index, option_string in enumerate(opts): 400 | if ":" in option_string: 401 | option = option_string.split(":", 1) 402 | key, value = option 403 | if key != "pcre": 404 | value = [self.remove_leading_spaces(x) for x in value.split(",")] 405 | options_dict[index] = (key, value) 406 | else: 407 | options_dict[index] = (option_string, "") 408 | return options_dict 409 | 410 | def validate_options(self, options): 411 | 412 | for index, option in options.items(): 413 | key, value = option 414 | if len(value) == 1: 415 | content_mod = self.dicts.content_modifiers(value[0]) 416 | opt = False 417 | if content_mod: 418 | # An unfinished feature 419 | continue 420 | gen_option = self.dicts.options(key) 421 | if gen_option: 422 | opt = True 423 | continue 424 | pay_option = self.dicts.options(key) 425 | if pay_option: 426 | opt = True 427 | continue 428 | non_pay_option = self.dicts.options(key) 429 | if non_pay_option: 430 | opt = True 431 | continue 432 | post_detect = self.dicts.options(key) 433 | if post_detect: 434 | opt = True 435 | continue 436 | threshold = self.dicts.options(key) 437 | if threshold: 438 | opt = True 439 | continue 440 | if not opt: 441 | message = "unrecognized option: %s" % key 442 | raise ValueError(message) 443 | return options 444 | 445 | 446 | class Sanitizer(object): 447 | def __init__(self): 448 | self.methods = { 449 | "pcre": self.pcre, 450 | # "depth": self.depth 451 | } 452 | 453 | def sanitize(self, parsed): 454 | options = parsed["options"] 455 | for key, value in options.items(): 456 | if key in self.methods: 457 | options[key] = self.methods[key](value) 458 | 459 | parsed["options"] = options 460 | return parsed 461 | 462 | @staticmethod 463 | def pcre(value: list) -> List: 464 | value_string = value[0] 465 | if re.match(r'^"/.*/[ismxAEGRUBPHMCOIDKYS]+"$', value_string): 466 | return value 467 | else: 468 | if not str(value).startswith('"/') and value: 469 | start = re.split(r'^"', value) 470 | start[0] = '"/' 471 | value = "".join(start) 472 | if not re.search(r'(\/")$', value): 473 | end = re.split(r'"$', value) 474 | end[-1] = '/"' 475 | value = "".join(end) 476 | return value 477 | 478 | def depth(self, options): 479 | depth_idx = [idx for idx in options if "depth" in options[idx]][0] 480 | dsize_idx = [idx for idx in options if "dsize" in options[idx]][0] 481 | depth = options[depth_idx].get("depth")[0] 482 | dsize = options[dsize_idx].get("dsize")[0] 483 | full_dsize = re.split(r"[0-9]+", dsize) 484 | operand = [x for x in full_dsize if x] 485 | dsize = dsize.strip(operand[0]) 486 | if int(depth) < int(dsize): 487 | return dsize 488 | else: 489 | return depth 490 | 491 | 492 | class SerializeRule(object): 493 | def __init__(self, rule): 494 | self.rule = rule 495 | 496 | def __getitem__(self, key): 497 | if "rule" in key: 498 | return self.serialize_rule() 499 | if "header" in key: 500 | return self.serialize_header() 501 | if "options" in key: 502 | return self.serialize_options() 503 | 504 | def __str__(self): 505 | return self.serialize_rule() 506 | 507 | def __list_serializer(self, list_bool: bool, items: List) -> str: 508 | serialised = str() 509 | for _bool, item in items: 510 | if isinstance(item, list): 511 | serialised = "{},{}".format( 512 | serialised, self.__list_serializer(_bool, item) 513 | ) 514 | else: 515 | if _bool: 516 | serialised = "{},{}".format(serialised, item) 517 | if not _bool: 518 | serialised = "{},!{}".format(serialised, item) 519 | 520 | serialised_list = serialised.lstrip(",") 521 | 522 | if list_bool: 523 | serialised = "[{}]".format(serialised_list) 524 | else: 525 | serialised = "![{}]".format(serialised_list) 526 | 527 | return serialised 528 | 529 | def serialize_header_item(self, item: Any) -> str: 530 | if isinstance(item, str): 531 | return item 532 | 533 | if isinstance(item, tuple): 534 | _bool, item = item 535 | if isinstance(item, list): 536 | return self.__list_serializer(_bool, item) 537 | else: 538 | return item 539 | 540 | def serialize_header(self, header: Dict = None) -> str: 541 | serialised = str() 542 | if not header: 543 | header = self.rule["header"] 544 | for key, value in header.items(): 545 | item = self.serialize_header_item(value) 546 | serialised = "{} {}".format(serialised, item) 547 | return serialised 548 | 549 | def serialize_options(self, options: Dict = None) -> str: 550 | options_list = [] 551 | if not options: 552 | options = self.rule["options"] 553 | for index, option in options.items(): 554 | key, value = option 555 | if value: 556 | option_value = "{}:{}".format(key, ",".join(value)) 557 | else: 558 | option_value = "{}".format(key) 559 | options_list.append(option_value) 560 | 561 | _options = "; ".join(str(e) for e in options_list) 562 | serialized_options = "({})".format(_options) 563 | return serialized_options 564 | 565 | def serialize_rule(self): 566 | return "{} {}".format( 567 | self.serialize_header(), self.serialize_options() 568 | ).lstrip() 569 | -------------------------------------------------------------------------------- /dicts.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class Dicts: 5 | def classtypes(self, cltype): 6 | classtypes = { 7 | "attempted-admin": "Attempted Administrator Privilege Gain", 8 | "attempted-dos": "Attempted Denial of Service", 9 | "attempted-recon": "Attempted Information Leak", 10 | "attempted-user": "Attempted User Privilege Gain", 11 | "bad-unknown": "Potentially Bad Traffic", 12 | "client-side-exploit": "Known client side exploit attempt", 13 | "default-login-attempt": "Attempt to Login By a Default Username and Password", 14 | "denial-of-service": "Detection of a Denial of Service Attack", 15 | "file-format": "Known malicious file or file based exploit", 16 | "icmp-event": "Generic ICMP Event", 17 | "inappropriate-content": "Inappropriate content was detected", 18 | "malware-cnc": "Known malware command and control traffic", 19 | "misc-activity": "Misc Activity", 20 | "misc-attack": "Misc Attack", 21 | "network-scan": "Detection of a Network Scan", 22 | "non-standard-protocol": "Detection of a Non-Standard Protocol or Event", 23 | "not-suspicious": "Not Suspicious Traffic", 24 | "policy-violation": "Potential Corporate Policy Violation", 25 | "protocol-command-decode": "Generic Protocol Command Decode", 26 | "rpc-portmap-decode": " Decode of an RPC Query", 27 | "sdf": "Sensitive Data", 28 | "shellcode-detect": "Executable Code was Detected", 29 | "string-detect": "A Suspicious String was Detected", 30 | "successful-admin": "Successful Administrator Privilege Gain", 31 | "successful-dos": "Denial of Service", 32 | "successful-recon-largescale": "Large Scale Information Leak", 33 | "successful-recon-limited": "Information Leak", 34 | "successful-user": "Successful User Privilege Gain", 35 | "suspicious-filename-detect": "A Suspicious Filename was Detected", 36 | "suspicious-login": "An Attempted Login Using a Suspicious Username was Detected", 37 | "system-call-detect": "A System Call was Detected", 38 | "tcp-connection": "A TCP Connection was Detected", 39 | "trojan-activity": "A Network Trojan was Detected", 40 | "unknown": "Unknown Traffic", 41 | "unsuccessful-user": "Unsuccessful User Privilege Gain", 42 | "unusual-client-port-connection": "A Client was Using an Unusual Port", 43 | "web-application-activity": "Access to a Potentially Vulnerable Web Application", 44 | "web-application-attack": "Web Application Attack", 45 | "nonstd-tcp": "Detection of a Non-Standard TCP Protocol", 46 | } 47 | if cltype in classtypes: 48 | return classtypes[cltype] 49 | else: 50 | return False 51 | 52 | @staticmethod 53 | def general_options(option: str = None) -> Any: 54 | # TODO: maybe add Snort Default Classifications 55 | general_options = { 56 | "msg", 57 | # The msg keyword tells the 58 | # logging and alerting engine 59 | # the message to print with the packet 60 | # dump or alert. 61 | "reference", 62 | # The reference keyword allows rules to include 63 | # references to external attack identification 64 | # systems. 65 | "gid", 66 | # The gid keyword (generator id) is used to 67 | # identify what part of Snort generates the event 68 | # when a particular rule fires. 69 | "sid", 70 | # The sid keyword is used to uniquely identify 71 | # Snort rules. 72 | "rev", 73 | # The rev keyword is used to uniquely identify 74 | # revisions of Snort rules. 75 | "classtype", 76 | # The classtype keyword is used to categorize 77 | # a rule as detecting an attack that is part 78 | # of a more general type of attack class. 79 | "priority", 80 | # The priority keyword assigns a severity level to 81 | # rules. "priority": "priority", 82 | "metadata", 83 | # The metadata keyword allows a rule writer 84 | # to embed additional information about the rule, 85 | # typically in a key-value format. Keys: engine 86 | # ( Indicate a Shared Library Rule ) ex: "shared", 87 | # soid ( Shared Library Rule Generator and 88 | # SID ) ex: "gid|sid", service ( Target-Based 89 | # Service Identifier ) ex: "http" 90 | # 91 | # 92 | # TODO: figure out what these are 93 | "service", 94 | "bufferlen", 95 | } 96 | 97 | if option: 98 | if option in general_options: 99 | return option 100 | else: 101 | return False 102 | else: 103 | return general_options 104 | 105 | @staticmethod 106 | def payload_detection(option: str = None) -> Any: 107 | 108 | payload_detection = { 109 | "content", 110 | # The content keyword allows 111 | # the user to set rules that search for specific 112 | # content in the packet payload and trigger 113 | # response based on that data. 114 | "protected_content", 115 | # As with the content keyword, 116 | # its primary purpose is to match strings of 117 | # specific bytes. The search is performed by 118 | # hashing portions of incoming packets and 119 | # comparing the results against the hash provided, 120 | # and as such, it is computationally expensive. 121 | "hash", 122 | # The hash keyword is used to specify the hashing 123 | # algorithm to use when matching a 124 | # protected_content rule. 125 | "length", 126 | # The length keyword is used to specify the 127 | # original length of the content specified 128 | # in a protected_content rule digest. 129 | # The value provided must be greater than 0 and 130 | # less than 65536. 131 | "nocase", 132 | # The nocase keyword allows the rule writer to 133 | # specify that the Snort should look for the 134 | # specific pattern, ignoring case. nocase 135 | # modifies the previous content keyword 136 | # in the rule. 137 | "rawbytes", 138 | # The rawbytes keyword allows rules to look at 139 | # the raw packet data, ignoring any decoding 140 | # that was done by preprocessors. 141 | "depth", 142 | # The depth keyword allows the rule writer to 143 | # specify how far into a packet Snort should 144 | # search for the specified pattern. 145 | "offset", 146 | # The offset keyword allows the rule writer to 147 | # specify where to start searching for a pattern 148 | # within a packet. 149 | "distance", 150 | # The distance keyword allows the rule writer to 151 | # specify how far into a packet Snort should 152 | # ignore before starting to search for the 153 | # specified pattern relative to the end of the 154 | # previous pattern match. 155 | "within", 156 | # The within keyword is a content modifier that 157 | # makes sure that at most N bytes are between 158 | # pattern matches using the content keyword. 159 | # NOTE: The http_client_body modifier is not 160 | # allowed to be used with the rawbytes modifier 161 | # for the same content. 162 | "http_client_body", 163 | # The http_client_body keyword is a content 164 | # modifier that restricts the search to the body 165 | # of an HTTP client request. 166 | # NOTE: The http_cookie modifier is not 167 | # allowed to be used with the 168 | # rawbytes or fast_pattern modifiers for the same 169 | # content 170 | "http_cookie", 171 | # The http_cookie keyword is a content modifier 172 | # that restricts the search to the extracted 173 | # Cookie Header field As this keyword 174 | # is a modifier to the previous content 175 | # keyword, there must be a content in the rule 176 | # before http_cookie is specified. 177 | # This keyword is dependent 178 | # on the enable_cookie config option. 179 | # NOTE: The http_raw_cookie modifier 180 | # is not allowed to be used with the rawbytes, 181 | # http_cookie or fast_pattern modifiers for the 182 | # same content 183 | "http_raw_cookie", 184 | # The http_raw_cookie keyword is a content 185 | # modifier that restricts the search to the 186 | # extracted UNNORMALIZED Cookie Header field 187 | # NOTE: The http_header modifier is not allowed 188 | # to be used with the rawbytes modifier for the 189 | # same content. 190 | "http_header", 191 | # The http_header keyword is a content modifier 192 | # that restricts the search to the extracted 193 | # Header fields. 194 | # NOTE:The http_raw_header modifier 195 | # is not allowed to be used with the rawbytes, 196 | # http_header or fast_pattern modifiers for the 197 | # same content. 198 | "http_raw_header", 199 | # The http_raw_header keyword is a content 200 | # modifier that restricts the search to the 201 | # extracted UNNORMALIZED Header fields 202 | # NOTE: The http_method modifier 203 | # is not allowed to be used with the rawbytes 204 | # or fast_pattern 205 | # modifiers for the same content. 206 | "http_method", 207 | # The http_method keyword is a content modifier 208 | # that restricts the search to the extracted 209 | # Method from a HTTP client request. 210 | # NOTE: The http_uri modifier is not allowed 211 | # to be used with the rawbytes modifier for 212 | # the same content. 213 | "http_uri", 214 | # The http_uri keyword is a content modifier 215 | # that restricts the search to the NORMALIZED 216 | # request URI field. NOTE: The http_raw_uri 217 | # modifier is not allowed to be used with the 218 | # rawbytes, http_uri or fast_pattern modifiers 219 | # for the same content. 220 | "http_raw_uri", 221 | # The http_raw_uri keyword is a content modifie 222 | # that restricts the search to the UNNORMALIZED 223 | # request URI field. NOTE: The http_stat_code 224 | # modifier is not allowedi to be used with the 225 | # rawbytes or fast_pattern modifiers for the 226 | # same content. 227 | "http_stat_code", 228 | # The http_stat_code keyword is a content 229 | # modifier that restricts the search 230 | # to the extracted Status code field 231 | # from a HTTP server response. 232 | # NOTE: The http_stat_msg modifier is not allowed 233 | # to be used with the rawbytes or fast_pattern 234 | # modifiers for the same content. 235 | "http_stat_msg", 236 | # The http_stat_msg keyword is a content modifier 237 | # that restricts the search to the extracted 238 | # Status Message field from a 239 | # HTTP server response. 240 | # NOTE: Negation(!) and OR(|) operations cannot 241 | # be used in conjunction with each other for the 242 | # http_encode keyword. The OR and negation 243 | # operations work only on the encoding type 244 | # field and not on http buffer type field. 245 | # TODO: check for http_encode options 246 | "http_encode", 247 | # The http_encode keyword will enable alerting 248 | # based on encoding type present in a HTTP client 249 | # request or a HTTP server response 250 | # NOTE: The fast_pattern modifier cannot be used 251 | # with the following http content modifiers: 252 | # 1. http_cookie, 253 | # 2. http_raw_uri, 254 | # 3. http_raw_header, 255 | # 4. http_raw_cookie, 256 | # 5. http_method, 257 | # 6. http_stat_code, 258 | # 7. http_stat_msg 259 | # NOTE: The fast_pattern modifier can be used 260 | # with negated contents onlyi if those contents 261 | # are not modified with: 262 | # 1. offset, 263 | # 2. depth, 264 | # 3. distance or 265 | # 4. within. 266 | # NOTE: The fast pattern matcher is always case 267 | # insensitive. TODO: check for fast_pattern 268 | # format 269 | "fast_pattern", 270 | # The fast_pattern keyword is a content modifier 271 | # that sets the content within a rule to be used 272 | # with the fast pattern matcher. 273 | # NOTE: uricontent cannot be modified by 274 | # a rawbytes modifier or any of the other 275 | # HTTP modifiers. If you wish to search the 276 | # UNNORMALIZED request URI field, use the 277 | # http_raw_uri modifier with a content option. 278 | "uricontent", 279 | # The uricontent keyword in the Snort 280 | # rule language searches the normalized request 281 | # URI field. 282 | "urilen", 283 | # The urilen keyword in the Snort rule 284 | # language specifies the exact length, 285 | # the minimum length, the maximum length, 286 | # or range of URI lengths to match. 287 | "isdataat", 288 | # The isdataat keyword verifies that the 289 | # payload has data at a specified location. 290 | # TODO: check for Perl compatible modifiers 291 | # for pcre. NOTE: Since this is an advanced 292 | # option, check the manual for pitfalls. 293 | "pcre", 294 | # The pcre keyword allows rules to 295 | # be written using perl compatible regular 296 | # expressions. 297 | "pkt_data", 298 | # This option sets the cursor used for detection 299 | # to the raw transport payload. 300 | # NOTE: The argument mime to file_data is 301 | # deprecated. The rule options file_data will 302 | # itself point to the decoded MIME attachment. 303 | "file_data", 304 | # This option sets the cursor used for detection 305 | # to one of the following buffers: 306 | # 1. HTTP response body 307 | # 2. HTTP de-chunked response body 308 | # 3. HTTP decompressed response 309 | # 4. HTTP normalized response body 310 | # 5. HTTP UTF normalized response body 311 | # 6. All of the above 312 | # 7. SMTP/POP/IMAP data body 313 | # 8. Base64 decoded MIME attachment 314 | # 9. Non-Encoded MIME attachment 315 | # 10. Quoted-Printable decoded MIME attachment 316 | # 11. Unix-to-Unix decoded attachment 317 | # TODO: check for base64_decode options and 318 | # format 319 | "base64_decode", 320 | # This option is used to decode the 321 | # base64 encoded data. This option is 322 | # particularly useful in 323 | # case of HTTP headers such as HTTP authorization 324 | # headers. NOTE: Fast pattern content matches 325 | # are not allowed with this buffer. 326 | "base64_data", 327 | # This option is similar to the rule option 328 | # file_data and is used to set the 329 | # cursor used for detection to the beginning 330 | # of the base64 decoded 331 | # buffer if present. 332 | # TODO: check for options. 333 | "byte_test", 334 | # The byte_test keyword tests a byte 335 | # field against a specific value 336 | # (with operatori). TODO: check 337 | # for options 338 | "byte_jump", 339 | # The byte_jump keyword allows rules to read the 340 | # length of a portion of data, then skip that 341 | # far forward in the packet. NOTE: Only two 342 | # byte_extract variables may be created per rule. 343 | # They can be re-used in the same rule any number 344 | # of times. TODO. check for options. 345 | "byte_extract", 346 | # It reads in some number of bytes from the 347 | # packet payload and saves it to a variable. 348 | # TODO: check for byte_math syntax and options 349 | "byte_math", 350 | # Perform a mathematical 351 | # operation on an extracted 352 | # value and a specified value or 353 | # existing variable, 354 | # and store the outcome in a new resulting 355 | # variable 356 | "ftpbounce", 357 | # The ftpbounce keyword detects FTP bounce 358 | # attacks. TODO. check for options and syntax 359 | # for asn1 360 | "asn1", 361 | # The asn1 detection plugin decodes a packet or a 362 | # portion of a packet, and looks for variou 363 | # malicious encodings. 364 | # NOTE: This plugin cannot do detection over 365 | # encrypted sessions, e.g. SSH (usually port 22). 366 | # TODO: find a way to check if the rule uses 367 | # encrypted sessions 368 | "cvs", 369 | # The cvs keyword detects invalid entry strings. 370 | "dce_iface", 371 | # For DCE/RPC based rules it has been necessary 372 | # to set flow-bits based on a client bind to a 373 | # service to avoid false positives. 374 | "dce_opnum", 375 | # The opnum represents a specific function 376 | # call to an interface. 377 | "dce_stub_data", 378 | # This option is used to place the cursor 379 | # (used to walk the packet payload in rules 380 | # processing) at the beginning of the DCE/RPC 381 | # stub data SIP Preprocessor provides ways to 382 | # tackle Common Vulnerabilities and Exposures 383 | # (CVEs) related with SIP found over the past 384 | # few years. 385 | "sip_method", 386 | # The sip_method keyword is used to check for 387 | # specific SIP request methods. 388 | "sip_stat_code", 389 | # The sip_stat_code is used to check the SIP 390 | # response status code. 391 | # This option matches if any one of the state 392 | # codes specified matches the status codes of 393 | # the SIP response. 394 | "sip_header", 395 | # The sip_header keyword restricts the search 396 | # to the extracted Header fields of a SIP message 397 | # request or a response. This works similar to 398 | # file_data. 399 | "sip_body", 400 | # The sip_body keyword places the cursor at the 401 | # beginning of the Body fields of a SIP message. 402 | # This works similar to file_data and 403 | # dce_stub_data. The message body includes 404 | # channel information using SDP protocol 405 | # (Session Description Protocol). 406 | # GTP (GPRS Tunneling Protocol) is used in core 407 | # communication networks to establish a channel 408 | # between GSNs (GPRS Serving Node). GTP decoding 409 | # preprocessor provides ways to tackle 410 | # intrusion attempts to those networks through 411 | # GTP. It also makes detecting new attacks easier. 412 | # TODO: identify also gtp message types, but for 413 | # now keyword check has to cut it. 414 | "gtp_type", 415 | # The gtp_type keyword is used to check for 416 | # specific GTP types. User can input message type 417 | # value, an integer in [0, 255], or a string 418 | # defined in the Table below. 419 | # TODO: gtp_info table check. 420 | "gtp_info", 421 | # The gtp_info keyword is used to check for 422 | # specific GTP information element. 423 | # This keyword restricts the search to the 424 | # information element field. User can input 425 | # information element value, 426 | # an integer in $[0, 255]$, 427 | "gtp_version" 428 | # The gtp_version keyword is used to check for 429 | # specific GTP version. Relates to gtp_info 430 | # and gtp_type tables. 431 | } 432 | 433 | if option: 434 | if option in payload_detection: 435 | return option 436 | else: 437 | return False 438 | else: 439 | return payload_detection 440 | 441 | @staticmethod 442 | def non_payload_options(option: str = None) -> Any: 443 | non_payload_detect = { 444 | "fragoffset", 445 | # The fragoffset keyword allows one to compare 446 | # the IP fragment offset field against a 447 | # decimal value. 448 | "ttl", 449 | # The ttl keyword is used to check the IP 450 | # time-to-live value. 451 | "tos", 452 | # The tos keyword is used to check the IP 453 | # TOS field for a specific value. 454 | "id", 455 | # The id keyword is used to check the IP ID 456 | # field for a specific value. 457 | "ipopts", 458 | # The ipopts keyword is used to check if a 459 | # specific IP option is present. 460 | "fragbits", 461 | # The fragbits keyword is used to check if 462 | # fragmentation and reserved bits are set 463 | # in the IP header. 464 | "dsize", 465 | # The dsize keyword is used to test the 466 | # packet payload isize NOTE: The reserved bits 467 | # '1' and '2' have been replaced with 468 | # 'C' and 'E',respectively, to match 469 | # RFC 3168, "The Addition of Explicit 470 | # Congestion Notification (ECN) to IP". 471 | # The old values of '1' and '2' are still 472 | # valid for the flag keyword, but are now 473 | # deprecated. 474 | "flags", 475 | # The flags keyword is used to check if 476 | # specific TCP flag bits are present. 477 | # TODO: check for syntax and options 478 | "flow", 479 | # The flow keyword allows rules to only 480 | # apply to certain directions of the traffic 481 | # flow. TODO. check for options and syntax 482 | "flowbits", 483 | # The flowbits keyword allows rules to 484 | # track states during a transport protocol 485 | # session. 486 | "seq", 487 | # The seq keyword is used to check for a 488 | # specific TCP sequence number 489 | "ack", 490 | # The ack keyword is used to check for a 491 | # specific TCP acknowledge number 492 | "window", 493 | # The window keyword is used to check for 494 | # a specific TCP window size 495 | "itype", 496 | # The itype keyword is used to check for a 497 | # specific ICMP type value 498 | "icode", 499 | # The icode keyword is used to check for 500 | # a specific ICMP code value 501 | "icmp_id", 502 | # The icmp id keyword is used to check 503 | # for a specific ICMP ID value. 504 | "icmp_seq", 505 | # The icmp seq keyword is used to check 506 | # for a specific ICMP sequence value. 507 | "rpc", 508 | # The rpc keyword is used to check 509 | # for a RPC application, version, and 510 | # procedure numbers in SUNRPC CALL requests 511 | "ip_proto", 512 | # The ip proto keyword allows checks against 513 | # the IP protocol header 514 | "sameip", 515 | # The sameip keyword allows rules to check 516 | # if the source ip is the same as the 517 | # destination IP. 518 | # NOTE: The stream_reassemble option is 519 | # only available when the Stream preprocessor 520 | # is enabled. 521 | "stream_reassemble", 522 | # The stream_reassemble keyword allows a rule 523 | # to enable or disable TCP stream 524 | # reassembly on matching traffic. 525 | # NOTE: The stream_size option is only 526 | # available when the Stream preprocessor 527 | # is enabled. 528 | "stream_size" 529 | # The stream_size keyword allows a rule 530 | # to match traffic 531 | # according to the number of bytes observed, 532 | # as determined by the TCP sequence numbers. 533 | } 534 | 535 | if option: 536 | if option in non_payload_detect: 537 | return option 538 | else: 539 | return False 540 | else: 541 | return non_payload_detect 542 | 543 | @staticmethod 544 | def post_detect_options(option: str = None) -> Any: 545 | post_detect = { 546 | "logto", 547 | # The logto keyword tells Snort to log all packets 548 | # that trigger this rule to a special output log file. 549 | "session", 550 | # The session keyword is built to extract user data 551 | # from TCP Sessions 552 | "resp", 553 | # The resp keyword is used attempt to close sessions 554 | # when an alert is triggered. 555 | "react", 556 | # This keyword implements an ability for users to 557 | # react to traffic that matches a Snort rule by 558 | # closing connection and sending a noticei. 559 | # NOTE: also check for options 560 | "tag", 561 | # The tag keyword allow rules to log more than 562 | # just the single packet that triggered the rule 563 | "replace", 564 | # Replace the prior matching content with the given 565 | # string of the same length. Available in inline 566 | # mode only. NOTE: As mentioned above, Snort evaluates 567 | # detection_filter as the last step of the detection 568 | # and not in post-detection. 569 | "activates", 570 | # This keyword allows the rule writer to specify 571 | # a rule to add when a specific network event occurs. 572 | "activated_by", 573 | # This keyword allows the rule writer to dynamically 574 | # enable a rule when a specific activate rule is 575 | # triggered. 576 | "count", 577 | # This keyword must be used in combination with the 578 | # activated by keyword. It 579 | # allows the rule writer to specify how many packets 580 | # to leave the rule enabled for 581 | # after it is activated 582 | "detection_filter", 583 | # Replace the prior matching content with the given 584 | # string of the same length. Available 585 | # in inline mode only. 586 | } 587 | 588 | if option: 589 | if option in post_detect: 590 | return option 591 | else: 592 | return False 593 | else: 594 | return post_detect 595 | 596 | @staticmethod 597 | def content_modifiers(option: str = None) -> Any: 598 | content_modifiers = { 599 | "nocase", 600 | "rawbytes", 601 | "depth", 602 | "offset", 603 | "distance", 604 | "within", 605 | "http_client_body", 606 | "http_cookie", 607 | "http_raw_cookie", 608 | "http_header", 609 | "http_raw_header", 610 | "http_method", 611 | "http_uri", 612 | "http_raw_uri", 613 | "http_stat_code", 614 | "http_stat_msg", 615 | "http_encode", 616 | "fast_pattern", 617 | "uricontent", 618 | "urilen", 619 | "isdataat", 620 | "pcre", 621 | "pkt_data", 622 | "file_data", 623 | "base64_decode", 624 | "base64_data", 625 | "byte_test", 626 | "byte_jump", 627 | "byte_extract", 628 | "byte_math", 629 | "ftpbounce", 630 | "asn1", 631 | "cvs", 632 | "dce_iface", 633 | "dce_opnum", 634 | "dce_stub_data", 635 | "sip_method", 636 | "sip_stat_code", 637 | "sip_header", 638 | "sip_body", 639 | "gtp_type", 640 | "gtp_info", 641 | "gtp_version", 642 | "ssl_version", 643 | "ssl_state", 644 | } 645 | 646 | if option: 647 | if option in content_modifiers: 648 | return option 649 | else: 650 | return False 651 | else: 652 | return content_modifiers 653 | 654 | @staticmethod 655 | def rule_thresholds(option): 656 | 657 | threshold = {"threshold"} 658 | 659 | if option in threshold: 660 | return option 661 | else: 662 | return False 663 | 664 | def options(self, option): 665 | 666 | # TODO: maybe add Snort Default Classifications 667 | general_options = self.general_options() 668 | payload_detection = self.payload_detection() 669 | content_modifiers = self.content_modifiers() 670 | non_payload_detect = self.non_payload_options() 671 | post_detect = self.post_detect_options() 672 | 673 | # TODO: add threshold types ex: threshold: 674 | # type limit <<, but for now, this will have to suffice 675 | rule_tresholds = {"threshold": "threshold"} 676 | 677 | # check if rule is of payload detect type 678 | if option in payload_detection: 679 | return "payload", option 680 | if option in non_payload_detect: 681 | return "non-payload", option 682 | if option in general_options: 683 | return "general", option 684 | if option in rule_tresholds: 685 | return "threshold", option 686 | if option in content_modifiers: 687 | return "content_modifier", option 688 | if option in post_detect: 689 | return "post_detect", option 690 | 691 | def get_options(self): 692 | options = set() 693 | return options.union( 694 | self.general_options(), 695 | self.payload_detection(), 696 | self.content_modifiers(), 697 | self.non_payload_options(), 698 | self.post_detect_options(), 699 | ) 700 | --------------------------------------------------------------------------------