From db3f76d4408db61f357ccd90c8e252598b1a7175 Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Wed, 3 May 2023 20:00:23 -0300 Subject: [PATCH 1/9] Refactoring --- Exceptions.py | 20 + LICENSE | 661 ------------------ config/azure_sentinel/README.md | 64 -- .../azure_sentinel/aws-cloudtrail_logs.json | 21 - .../azure_sentinel/azure-activity_logs.json | 21 - .../azure_sentinel/azure-ad_audit_logs.json | 21 - config/azure_sentinel/suricata_logs.json | 21 - config/elastic/elastic-siem-eql.json | 42 -- config/elastic/elastic-siem.json | 9 +- converters/ConvertorsBase.py | 97 +++ converters/Elastic.py | 196 ++++++ converters/__init__.py | 13 + helpers/check_if_compliant.py | 145 ---- helpers/curl/curl.exe | Bin 421376 -> 0 bytes helpers/mitre_attack.py | 153 ---- helpers/utils.py | 72 -- poetry.lock | 294 ++++++++ pyproject.toml | 17 + requirements.txt | 6 - rule_file_creator_scripts/ala_rule.py | 533 -------------- rule_file_creator_scripts/es_eql.py | 572 --------------- rule_file_creator_scripts/es_qs.py | 572 --------------- siegma.py | 459 +++--------- tools/Backends.py | 22 + tools/DirectoryTools.py | 79 +++ tools/FileTools.py | 97 +++ tools/LogHandler.py | 82 +++ tools/MitreAttack.py | 34 + tools/SigmaUtils.py | 69 ++ 29 files changed, 1106 insertions(+), 3286 deletions(-) create mode 100644 Exceptions.py delete mode 100644 LICENSE delete mode 100644 config/azure_sentinel/README.md delete mode 100644 config/azure_sentinel/aws-cloudtrail_logs.json delete mode 100644 config/azure_sentinel/azure-activity_logs.json delete mode 100644 config/azure_sentinel/azure-ad_audit_logs.json delete mode 100644 config/azure_sentinel/suricata_logs.json delete mode 100644 config/elastic/elastic-siem-eql.json create mode 100644 converters/ConvertorsBase.py create mode 100644 converters/Elastic.py create mode 100644 converters/__init__.py delete mode 100644 helpers/check_if_compliant.py delete mode 100644 helpers/curl/curl.exe delete mode 100644 helpers/mitre_attack.py delete mode 100644 helpers/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 rule_file_creator_scripts/ala_rule.py delete mode 100644 rule_file_creator_scripts/es_eql.py delete mode 100644 rule_file_creator_scripts/es_qs.py create mode 100644 tools/Backends.py create mode 100644 tools/DirectoryTools.py create mode 100644 tools/FileTools.py create mode 100644 tools/LogHandler.py create mode 100644 tools/MitreAttack.py create mode 100644 tools/SigmaUtils.py diff --git a/Exceptions.py b/Exceptions.py new file mode 100644 index 0000000..5299b78 --- /dev/null +++ b/Exceptions.py @@ -0,0 +1,20 @@ +class RuleSintaxeError(Exception): + """ + Error in the RuleSintaxe. + """ + + pass + +class CreateRuleByApiError(Exception): + """ + Erro to create a rule by API. e.g. Invalid SIEM resource. + """ + + pass + +class FileExtensionError(Exception): + """ + File extension error, e.g. The SIEM needs a json and was passed a txt output file. + """ + + pass diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0ad25db..0000000 --- a/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/config/azure_sentinel/README.md b/config/azure_sentinel/README.md deleted file mode 100644 index 36e3173..0000000 --- a/config/azure_sentinel/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Azure Sentinel - -This folder holds the configuration file for the SIEM platform in question. All SIEM platform folders have the following structure: - -- Example use cases of SIEGMA -- Fields & Configurations -- Automatic upload - -## Example use cases of SIEGMA - -### Generate an Azure Sentinel SIEM output from a single Azure Activity Logs Sigma rule file - -`python siegma.py -c config/azure_sentinel/azure-activity_logs.json -r /path/to/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/tools/config/ala-azure-activitylogs.yml -o rule-output` - -### Generate an Azure Sentinel SIEM output from a folder with several Azure Activity logs Sigma rule files - -`python siegma.py -c config/azure_sentinel/azure-activity_logs.json -r /path/to/folder/with/sigma-rules/ -s /path/to/sigma/folder -sc /path/to/sigma/tools/config/ala-azure-activitylogs.yml -o rule-output` - -### Generate an Azure Sentinel SIEM output from a Azure AD Audit logs rule file and also pass Sigma backend options - -In this example we will utilize -sep to request SIEGMA to use the advanced Sigma backend options that would be defined in the Azure Sentinel config - -`python siegma.py -c config/azure_sentinel/azure-ad_audit_logs.json -r /path/to/folder/with/sigma-rules/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/tools/config/ala-azure-ad_auditlogs.yml -sep -o output-file` - -### Generate an Azure Sentinel SIEM output from an Azure AD Audit logs rule file and also override azure-ad_audit_logs.json config from commandline - -In this example we will utilize the Azure Sentinel config fields as they are defined (or supplied from the Sigma rule) while overwriting certain fields through the usage of -co. This is particularly useful if converting Sigma rules for which you'd like to apply different SIEM consumable fields. - -The example below will overwrite the `settings.queryPeriod`, `credentials.azure_client_id`, `credentials.azure_client_secret`, `credentials.azure_tenant_id`, `credentials.azure_subscription_id` and `credentials.azure_resource_group`. - -`python siegma.py -c config/azure_sentinel/azure-ad_audit_logs.json -r /path/to/folder/with/sigma-rules/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/tools/config/ala-azure-ad_auditlogs.yml -co settings.queryPeriod=PT20M,credentials.azure_client_id="client_id",credentials.azure_client_secret="secret",credentials.azure_tenant_id="tenant_id",credentials.azure_subscription_id="00000000-0000-0000-0000-000000000000",credentials.azure_resource_group="rg" -o output-file` - -## Fields that are worth looking into to adapt to your particular use case - -| Azure Sentinel SIEM Config Field | Description | Example | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| sigma_params | Used in conjunction with switch --sigma_extra_parameters. Dictionary under which any of the parameters supported by Sigma can be added. | {"sigma_params": {"--backend-option": ["key=value", "case_insensitive_whitelist=*"]}} | - -## Azure Sentinel SIEM Configuration & Data Dictionary - -| Azure Sentinel SIEM Config Field | Default Value | Field type | Description | -| -------------------------------- | ------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| sigma_query_format | ala-rule | Hardcoded | Preset value. This value is passed to sigmac | -| sigma_params | N/A | User input optional | Used in conjunction with switch --sigma_extra_parameters. Dictionary under which any of the parameters supported by Sigma can be added. Example: {"sigma_params": {"--backend-option": ["key=value", "case_insensitive_whitelist=*"]}} | -| azure_client_id | No default value | User input optional | Enables automatic rule upload if filled | -| azure_client_secret | No default value | User input optional | Enables automatic rule upload if filled | -| azure_tenant_id | No default value | User input optional | Enables automatic rule import if filled | -| azure_subscription_id | No default value | User input optional | Enables automatic rule import if filled | -| azure_resource_group | No default value. Resource group name | User input optional | Enables automatic rule import if filled | -| queryPeriod | PT15M | User input optional | Defines how much data in the past should be queried. Example: PT15M | -| queryFrequency | PT5M | User input optional | Configuration that specifies after what interval should the query execute. Example: PT5M | -| kind | Scheduled | User input needed | Define the rule type. Example: Scheduled | -| triggerOperator | GreaterThan | User input optional | Define the comparison operator between query results and results count. Eg: GreaterThan | -| triggerThreshold | 0 | User input optional | Define the threshold for the query results count | -| suppressionDuration | PT1H | User input optional | Define the time period after the first rule trigger during which the rule shall be suppressed and not triggered again | -| suppressionEnabled | false | User input optional | Define if the rule suppression for a certain time after first trigger should be enabled or not. Eg: false | -| notes_folder | No default value | Sigma Config: notes_folder | Contains path to a folder that contains .md (markdown formatted) file containing details for investigation guide / notes for a given detection use case. Optional field that can be set either in config or using -co from commandline. | - -## Automatic upload - -If you'd like to enable automatic upload of consumables into your SIEM, please enter your environment variables in the [config files](.) or specify them through `-co` as shown below. -Below is an example of Azure AD Audit logs Sigma rule being uploaded to Azure Sentinel SIEM via a service principal credentials. - -`python siegma.py -c config/azure_sentinel/azure-ad_audit_logs.json -r /path/to/folder/with/sigma-rules/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/tools/config/ala-azure-ad_auditlogs.yml -co credentials.azure_client_id="client_id",credentials.azure_client_secret="secret",credentials.azure_tenant_id="tenant_id",credentials.azure_subscription_id="00000000-0000-0000-0000-000000000000",credentials.azure_resource_group="rg" -o output-file` \ No newline at end of file diff --git a/config/azure_sentinel/aws-cloudtrail_logs.json b/config/azure_sentinel/aws-cloudtrail_logs.json deleted file mode 100644 index e84081f..0000000 --- a/config/azure_sentinel/aws-cloudtrail_logs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "sigma_query_format": "ala-rule", - "sigma_params": {}, - "credentials": { - "azure_client_id": "", - "azure_client_secret": "", - "azure_tenant_id": "", - "azure_subscription_id": "", - "azure_resource_group": "" - }, - "notes_folder": "..\\3cs-int-sigma\\notes", - "settings": { - "kind": "Scheduled", - "queryPeriod":"PT30M", - "queryFrequency":"PT10M", - "triggerOperator": "GreaterThan", - "triggerThreshold": 0, - "suppressionDuration": "PT1H", - "suppressionEnabled": false - } -} \ No newline at end of file diff --git a/config/azure_sentinel/azure-activity_logs.json b/config/azure_sentinel/azure-activity_logs.json deleted file mode 100644 index 926e468..0000000 --- a/config/azure_sentinel/azure-activity_logs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "sigma_query_format": "ala-rule", - "sigma_params": {}, - "credentials": { - "azure_client_id": "", - "azure_client_secret": "", - "azure_tenant_id": "", - "azure_subscription_id": "", - "azure_resource_group": "" - }, - "notes_folder": "..\\notes", - "settings": { - "kind": "Scheduled", - "queryPeriod":"PT15M", - "queryFrequency":"PT5M", - "triggerOperator": "GreaterThan", - "triggerThreshold": 0, - "suppressionDuration": "PT1H", - "suppressionEnabled": false - } -} diff --git a/config/azure_sentinel/azure-ad_audit_logs.json b/config/azure_sentinel/azure-ad_audit_logs.json deleted file mode 100644 index 926e468..0000000 --- a/config/azure_sentinel/azure-ad_audit_logs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "sigma_query_format": "ala-rule", - "sigma_params": {}, - "credentials": { - "azure_client_id": "", - "azure_client_secret": "", - "azure_tenant_id": "", - "azure_subscription_id": "", - "azure_resource_group": "" - }, - "notes_folder": "..\\notes", - "settings": { - "kind": "Scheduled", - "queryPeriod":"PT15M", - "queryFrequency":"PT5M", - "triggerOperator": "GreaterThan", - "triggerThreshold": 0, - "suppressionDuration": "PT1H", - "suppressionEnabled": false - } -} diff --git a/config/azure_sentinel/suricata_logs.json b/config/azure_sentinel/suricata_logs.json deleted file mode 100644 index f3bb35a..0000000 --- a/config/azure_sentinel/suricata_logs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "sigma_query_format": "ala-rule", - "sigma_params": {}, - "credentials": { - "azure_client_id": "", - "azure_client_secret": "", - "azure_tenant_id": "", - "azure_subscription_id": "", - "azure_resource_group": "" - }, - "notes_folder": "..\\3cs-int-sigma\\notes", - "settings": { - "kind": "Scheduled", - "queryPeriod":"PT14M", - "queryFrequency":"PT7M", - "triggerOperator": "GreaterThan", - "triggerThreshold": 0, - "suppressionDuration": "PT1H", - "suppressionEnabled": false - } -} \ No newline at end of file diff --git a/config/elastic/elastic-siem-eql.json b/config/elastic/elastic-siem-eql.json deleted file mode 100644 index b2639c6..0000000 --- a/config/elastic/elastic-siem-eql.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "sigma_query_format": "es-eql", - "sigma_params": { - "--backend-option": [ - "" - ] - }, - "credentials": { - "kibana_username": "", - "kibana_password": "", - "kibana_url": "" - }, - "notes_folder": "", - "settings": { - "rule_id": "", - "id": "", - "author":[], - "from":"now-15m", - "index":["*"], - "interval":"5m", - "language":"eql", - "output_index":".siem-signals-default", - "references":[], - "false_positives":[], - "risk_score":0, - "name":"", - "description":"", - "query":"", - "severity":"", - "tags":[], - "to":"now", - "type":"eql", - "threat":[], - "throttle":"no_actions", - "threshold": { - "field": null, - "value": null - }, - "note": "", - "actions": [] - } -} diff --git a/config/elastic/elastic-siem.json b/config/elastic/elastic-siem.json index a1fd608..26733fb 100644 --- a/config/elastic/elastic-siem.json +++ b/config/elastic/elastic-siem.json @@ -6,9 +6,10 @@ ] }, "credentials": { - "kibana_username": "", - "kibana_password": "", - "kibana_url": "" + "username": "", + "password": "", + "siem_url": "", + "apikey": "" }, "notes_folder": "", "settings": { @@ -18,7 +19,7 @@ "from":"now-15m", "index":["*"], "interval":"5m", - "language":"kuery", + "language":"lucene", "output_index":".siem-signals-default", "references":[], "false_positives":[], diff --git a/converters/ConvertorsBase.py b/converters/ConvertorsBase.py new file mode 100644 index 0000000..f146eea --- /dev/null +++ b/converters/ConvertorsBase.py @@ -0,0 +1,97 @@ +from abc import ABC, abstractclassmethod +from sigma.rule import SigmaRule +from sigma.collection import SigmaCollection +from tools.MitreAttack import get_techinique_infos +import re + +class ConvertorsBase(ABC): + """ + Convertors abstract base class, use this class to create a new converter. + """ + def __init__(self, siem_config: dict[any, any]): + """ + Class Inicializer + + Args: + siem_config (dict[any, any]): Pass the siem configuration. + """ + + @abstractclassmethod + def convert(self, sigma_rule: SigmaCollection, backend: callable) -> str: + """ + Function responsible for converting a sigma rule. + + Args: + sigma_rule (SigmaCollection): Sigma rule, use tools.SigmaUtils.SigmaRule.get_sigma_configuration to create a SigmaCollection. + backend (callable): Backend responsible for converting the rule. + + Returns: + str: Return the query as string + """ + + @abstractclassmethod + def write_rule(self, file_name: str, rule_infos: any) -> None: + """ + Use this function to save the output into a file. + + Args: + file_name (str): File name. + rule_infos (any): Rule content. + """ + + @abstractclassmethod + def create_rule(self, sigma_rule: SigmaRule, query: str) -> any: + """ + Use this function to create a rule following the SIEM sintaxe. + Args: + sigma_rule (SigmaRule): Sigma rule content, use tools.SigmaUtils.SigmaRule.get_yml_file_content to create a SigmaRule object + query (str): Query as string, use the convert method to get a query string + + Returns: + any: Return the rule following each SIEM sintaxe. + """ + + def create_rule_by_api(self, rule: dict[str, any], siem_url: str="", username: str="", __passwd: str="", apikey: str="") -> any: + """ + Create a new elastic rule by api. + + Args: + rule (dict[str, any]): Rule content, use converter.elastic.create_rule to get a valid rule. + siem_url (str): SIEM URL. + username (str): Username to authenticate. + __passwd (str): Password to authenticate. + apikey (str): ApiKey to authenticate. + + Returns: + any: SIEM response. + """ + + def get_mitre_attack_mapping(self, rule_tags: list[str]) -> dict[str, dict[str | list[dict[str, str]]]]: + """ + Gather information from a technique in the mitre attack + + Args: + rule_tags (list[str]): Techinique IDs + + Returns: + dict[str, dict[str | list[dict[str, str]]]]: The information of the technique. + """ + + mitre_attack_mapping = {} + + for tag in rule_tags: + if not re.match(r"^t\d", tag): + continue + + techinique_infos = get_techinique_infos(tag.upper()) + + if techinique_infos == None: + continue + + for tactic_infos in techinique_infos["tactics"]: + if not mitre_attack_mapping.get(tactic_infos["name"]): + mitre_attack_mapping[tactic_infos["name"]] = {"tactic_id": tactic_infos["id"], "tactic_reference": tactic_infos["reference"], "techniques": []} + + mitre_attack_mapping[tactic_infos["name"]]["techniques"].append(techinique_infos) + + return mitre_attack_mapping \ No newline at end of file diff --git a/converters/Elastic.py b/converters/Elastic.py new file mode 100644 index 0000000..db59cd3 --- /dev/null +++ b/converters/Elastic.py @@ -0,0 +1,196 @@ +from converters.ConvertorsBase import ConvertorsBase +from sigma.parser.collection import SigmaParser +from sigma.pipelines.elasticsearch.windows import ecs_windows +from Exceptions import CreateRuleByApiError, FileExtensionError +from tools.FileTools import FileTools +from sigma.rule import SigmaRule +from dataclasses import dataclass +from enum import Enum +import json +import requests +import secrets +import base64 + + +class RiskSocreMapping(Enum): + """ + Elastic risk socre mapping. + """ + + INFORMATIONAL = 1 + LOW = 25 + MEDIUM = 50 + HIGH = 75 + CRITICAL = 100 + + @classmethod + def collect_risk_socre(cls, risk: str) -> int: + """ + Use this function to collect the risk score. + + Args: + risk (str): Risk level + + Returns: + int: Return the number of risk associated with the rule. + """ + + risk = risk.upper() + + if cls[risk] is not None: + return cls[risk] + + return cls.MEDIUM + +@dataclass +class ElasticConverter(ConvertorsBase): + """ + Elastic converter, use this class to convert sigma rule into elastic rule. + + Args: + siem_config (dict[any, any]) : Siem configuration. + """ + + siem_config: dict[any, any] + + def write_rule(self, file_name: str, rule_infos: dict["str", any]) -> None: + """ + Use this function to save the output into a file. + + Args: + file_name (str): File name. + rule_infos (any): Rule content. + """ + + if FileTools.get_file_extension_name(file_name) != ".ndjson": + raise FileExtensionError("Output file extension must be .ndjson") + + with open(file_name, 'a') as convert_file: + convert_file.write(f"{str(json.dumps(rule_infos))}\n") + + def create_rule(self, sigma_rule: SigmaRule, query: str) -> dict[str, any]: + """ + Use this function to create a elastic rule. + Args: + sigma_rule (SigmaRule): Sigma rule content, use tools.SigmaUtils.SigmaRule.get_yml_file_content to create a SigmaRule object + query (str): Query as string, use the convert method to get a query string + + Returns: + any: Return the rule following each SIEM sintaxe. + """ + + elastic_config = self.siem_config + mitre_attack_threat_mapping = self.create_elastic_attack_mapping([tag.name for tag in sigma_rule.tags]) + + return { + "rule_id": str(sigma_rule.id), + "author": [author.strip() for author in sigma_rule.author.split(",")], + "from": elastic_config["settings"]["from"], + "index": elastic_config["settings"]["index"], + "interval": elastic_config["settings"]["interval"], + "language": elastic_config["settings"]["language"], + "references": sigma_rule.references, + "output_index": elastic_config["settings"]["output_index"], + "false_positives": sigma_rule.falsepositives, + "risk_score": RiskSocreMapping.collect_risk_socre(sigma_rule.level.name).value, + "name": sigma_rule.title, + "enabled": True, + "description": sigma_rule.description, + "query": query, + "severity": sigma_rule.level.name.lower(), + "tags": [tag.name for tag in sigma_rule.tags], + "to": elastic_config["settings"]["to"], + "type": elastic_config["settings"]["type"], + "threat": mitre_attack_threat_mapping, + "actions": elastic_config["settings"]["actions"], + "note": elastic_config["settings"]["note"], + "throttle": elastic_config["settings"]["throttle"] + } + + def convert(self, sigma_parser_rule: SigmaParser, backend: callable) -> str: + """ + Function responsible for converting a sigma rule into a elastic query. + + Args: + sigma_rule (SigmaCollection): Sigma rule, use tools.SigmaUtils.SigmaRule.get_sigma_configuration to create a SigmaCollection. + backend (callable): Backend responsible for converting the rule. + + Returns: + str: Return the query as string + """ + + rule = backend(ecs_windows()).convert(sigma_parser_rule)[0] + + return rule + + def create_rule_by_api(self, rule: dict[str, any], siem_url: str, username: str, _passwd: str, apikey="") -> dict[any, any]: + """ + Create a new elastic rule by api. + + Args: + rule (dict[str, any]): Rule content, use converter.elastic.create_rule to get a valid rule. + username (str): Username to authenticate. + __passwd (str): Password to authenticate. + + Raises: + CreateRuleByApiError: Custom exception, if you see it, it means some error happened in the process. + + Returns: + dict[any, any]: Rule content. + """ + + credentials = bytes(f"{username}:{_passwd}", encoding="utf-8") + encoded_credentials = base64.b64encode(credentials).decode("utf-8") + xsrf_token = secrets.token_hex(16) + + resp = requests.post(f"{siem_url}/api/detection_engine/rules", + headers={ + "Authorization": "Basic " + encoded_credentials, + "Content-Type": "application/json", + "kbn-xsrf": xsrf_token + }, + json=rule) + + resp_content = resp.json() + + if resp.status_code != 200: + raise CreateRuleByApiError(resp_content["message"]) + + return resp.json() + + def create_elastic_attack_mapping(self, techniques: list[str]) -> dict[str, any]: + """ + Gather information from a technique in the mitre attack mapping and return them as a elastic threat type. + + Args: + techniques (list[str]): Techniques to search + + Returns: + dict[str, any]: The informations of the given techniques. + """ + + mitre_attack_threat: dict[str, any] = [] + + for tactic, infos in self.get_mitre_attack_mapping(techniques).items(): + mitre = { + "framework": "MITRE ATT&CK", + "tactic": { + "id": infos["tactic_id"], + "name": tactic, + "reference": infos["tactic_reference"] + }, + "technique": [] + } + + for techinique in infos["techniques"]: + mitre["technique"].append( + { + "id": techinique["id"], + "name": techinique["name"], + "reference": techinique["reference"] + } + ) + + mitre_attack_threat.append(mitre) + + return mitre_attack_threat \ No newline at end of file diff --git a/converters/__init__.py b/converters/__init__.py new file mode 100644 index 0000000..1c7b59c --- /dev/null +++ b/converters/__init__.py @@ -0,0 +1,13 @@ +from importlib import import_module +from pathlib import Path +from pkgutil import iter_modules +from inspect import getmembers, isclass + + +# Use this to collect all Converters +all_converters = { + submodule.lower(): cls # Name Of Converor: ConverorClass + for _, submodule, _ in iter_modules([ str(Path(__file__).resolve().parent) ]) # Iterate over modules, str around Path is due to issue with PosixPath from Python 3.10 + for name, cls in getmembers(import_module(__name__ + "." + submodule, isclass)) # Iterate over classes + if name.endswith("Converter") # Class filtering (Collect only converters) +} diff --git a/helpers/check_if_compliant.py b/helpers/check_if_compliant.py deleted file mode 100644 index 6225990..0000000 --- a/helpers/check_if_compliant.py +++ /dev/null @@ -1,145 +0,0 @@ -import os -import sys -import argparse -from os import listdir -from pprint import pprint -from os.path import isfile, join - - -# CheckIfSiegmaCompliant.py -# By Wesley -# https://github.com/wesley587 - -# global vars -exit_status_value = 0 - - -class parsing: - - - def __init__(self, dir): - self.agg = ['count(', 'min(', 'max(', 'avg(', 'sum('] - self.dir = dir - - - def firstParsing(self, folders): - # if string then this means that this a single file needs to be checked for compliance - if type(folders) == str: - folders = [folders] - self.Parsing_file(folders) - # else whole folders - else: - for folder in folders: - path = get_slashes().join([self.dir, folder]) - files = [x for x in listdir(path) if isfile(join(path, x))] - # pprint(files) - f = [x for x in listdir(path) if not isfile(join(path, x))] - # pprint(f) - if len(files) > 0: - self.Parsing_files(files, path) - if len(f) > 0: - self.Parsing_folders(folder, f) - - - def Parsing_file(self, file): - global exit_status_value - error = dict() - for x in file: - yaml_file = open(x, 'r', encoding='utf8') - detect_count = 0 - local = x - error[local] = list() - for k in yaml_file: - if 'detection:' in k: - detect_count += 1 - if 'condition:' in k: - for x in self.agg: - if x in k: - x = x.replace('(', '') - error[local].append(f'\t* An unsupported Aggregation expression ({x}) is present on the sigma rule.') - - if detect_count > 1: - error[local].append('\t* There is more than 1 instance of detection on this sigma rule.') - for k, v in error.items(): - if len(v) > 0: - exit_status_value = 1 - print(f'ERROR FOUND - Sigma Rule: {k}') - for x in v: - print(x) - print("\n") - - - def Parsing_files(self, files, path): - global exit_status_value - error = dict() - for x in files: - yaml_file = open(get_slashes().join([path, x]), 'r', encoding='utf8') - detect_count = 0 - local = get_slashes().join([path, x]) - error[local] = list() - for k in yaml_file: - if 'detection:' in k: - detect_count += 1 - if 'condition:' in k: - for x in self.agg: - if x in k: - x = x.replace('(', '') - error[local].append(f'\t* An unsupported Aggregation expression ({x}) is present on the sigma rule.') - - if detect_count > 1: - error[local].append('\t* There is more than 1 instance of detection on this sigma rule.') - for k, v in error.items(): - if len(v) > 0: - exit_status_value = 1 - print(f'ERROR FOUND - Sigma Rule: {k}') - for x in v: - print(x) - print("\n") - - - def Parsing_folders(self, path, folder): - for o in folder: - path2 = get_slashes().join([self.dir, path, o]) - files2 = [x for x in listdir(path2) if isfile(join(path2, x))] - self.Parsing_files(files2, path2) - - -def get_slashes(): - ret = '\\' - if os.name != 'nt': - ret = '/' - # print('Non-windows machine...') - # else: - # print('Windows machine...') - # print('Returning {}...'.format(ret)) - return ret - - -def main(): - parse = argparse.ArgumentParser() - parse.add_argument('-p', help='Parsing the sigma rules in a Directory') - path = vars(parse.parse_args()) - dir = path['p'] - folders = '' - # for single file - if isfile(dir): - file = dir - start = parsing(dir=file) - start.firstParsing(file) - # for folders - else: - folders = [x for x in listdir(dir) if not isfile(join(dir, x))] - if folders == []: - # if single folder with n files - folders = [x for x in listdir(dir) if isfile(join(dir, x))] - start = parsing(dir=dir) - start.Parsing_files(folders, dir) - else: - start = parsing(dir=dir) - start.firstParsing(folders) - # pprint(folders) - print('Ending script with exit status {}'.format(exit_status_value)) - sys.exit(exit_status_value) - - -main() \ No newline at end of file diff --git a/helpers/curl/curl.exe b/helpers/curl/curl.exe deleted file mode 100644 index 54f60f3d090cc47e79379621c8da24ae098e741b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 421376 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�knV;y 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≠`@etxRCE$ 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 diff --git a/helpers/mitre_attack.py b/helpers/mitre_attack.py deleted file mode 100644 index d9a9c7e..0000000 --- a/helpers/mitre_attack.py +++ /dev/null @@ -1,153 +0,0 @@ -import re -import requests - - -class MitreAttack: - - - tactics = { - "Reconnaissance": "https://attack.mitre.org/tactics/TA0043", - "Resource Development": "https://attack.mitre.org/tactics/TA0042", - "Initial Access": "https://attack.mitre.org/tactics/TA0001", - "Execution": "https://attack.mitre.org/tactics/TA0002", - "Persistence": "https://attack.mitre.org/tactics/TA0003", - "Privilege Escalation": "https://attack.mitre.org/tactics/TA0004", - "Defense Evasion": "https://attack.mitre.org/tactics/TA0005", - "Credential Access": "https://attack.mitre.org/tactics/TA0006", - "Discovery": "https://attack.mitre.org/tactics/TA0007", - "Lateral Movement": "https://attack.mitre.org/tactics/TA0008", - "Collection": "https://attack.mitre.org/tactics/TA0009", - "Command and Control": "https://attack.mitre.org/tactics/TA0011", - "Exfiltration": "https://attack.mitre.org/tactics/TA0010", - "Impact": "https://attack.mitre.org/tactics/TA0040" - } - techniques = None - to_be_lowered_chrs = ['and'] - - - def get_tactics_name_set(self, yj_rule_tags): - ret = set() - for tag in yj_rule_tags: - is_tactic_boolean, tactic = self.is_tactic(tag) - is_technique_boolean, technique = self.is_technique(tag) - if is_tactic_boolean and (not is_technique_boolean): - print('tactic found...') - ret.add(tactic.get('name')) - return list(ret) - - - def is_tactic(self, item): - print('Starting is_tactic()...') - is_tactic_boolean = False - tactic = {} - match_list = re.findall(r'^attack\.\w{5,}.*$', item) - if item.count('.') > 1: match_list = [] - # print(match_list) - if len(match_list) > 0: - print(match_list) - is_tactic_boolean = True - # print('up') - # input('') - tactic = self.get_tactic_from_name(item) - # tactic = get_tactic_from_mitre(attack, item.replace('attack.', '').replace('_', ' ')) - # print('down') - # input('') - print('tactic item {} data:'.format(item)) - print(tactic) - return is_tactic_boolean, tactic - - - def is_technique(self, item): - print('Starting is_technique()...') - is_technique_boolean = False - technique = {} - match_list = re.findall(r'^attack\.t\d{4,}((\.\d{3,})*)$', item) - print(match_list) - if len(match_list) > 0: - print(match_list) - is_technique_boolean = True - technique = self.get_technique_from_id(item) - # technique = get_technique_from_mitre(attack, item.replace('attack.', ''), logger) - # if technique: is_technique_boolean = True - print('technique item {} data:'.format(item)) - print(technique) - return is_technique_boolean, technique - - - def format_tactic_name(self, tactic_name): - try: - temp = '' - if '.' in tactic_name: - # for tactic_names like attack.tactic to tactic - tactic_name = tactic_name.split('.')[1] - if '_' in tactic_name: - # for tactic_names like tactic_name to tactic name - tactic_name = tactic_name.replace('_', ' ') - for tactic_chr in tactic_name.split(' '): - # for tactic_names like tactic name to Tactic Name - temp += tactic_chr.capitalize() + ' ' - tactic_name = temp.rstrip(' ') - for tblc in self.to_be_lowered_chrs: - if tblc in tactic_name.lower(): - tactic_name = tactic_name.replace(tblc.capitalize(), tblc) - except Exception as e: - print('Exception {} occurred in format_tactic_name() for tactic_name {}...'.format(e, tactic_name)) - print('Returning tactic_name {}...'.format(tactic_name)) - return tactic_name - - - def get_tactic_id_from_url(self, tactic_url): - return tactic_url.split('/')[-1] - - - def get_tactic_id_from_name(self, tactic_name): - return self.tactics.get(tactic_name).split('/')[-1] - - - def get_tactic_from_name(self, tactic_name): - try: - # print('line 1') - tactic_name = self.format_tactic_name(tactic_name) - # print('line 2') - tactic_url = self.tactics.get(tactic_name) - return { - 'id': self.get_tactic_id_from_url(tactic_url), - 'name': tactic_name, - 'reference': tactic_url - } - except Exception as e: - print('Exception {} occurred in get_tactic_from_name() for tactic_name {}'.format(e, tactic_name)) - - - def format_technique_id(self, technique_id): - if 'attack.' in technique_id: - # for tactic_names like attack.tXXX to tXXX - technique_id = technique_id.split('attack.')[1] - technique_id = technique_id.capitalize() - return technique_id - - - def get_technique_name_from_url(self, technique_url, technique_id): - # Application Layer Protocol: Web Protocols, Sub-technique T1071.001 - Enterprise | MITRE ATT&CK® - # Application Layer Protocol, Technique T1071 - Enterprise | MITRE ATT&CK® - technique_name = 'Unknown Technique' - for line in (requests.get(technique_url).text).split('\n'): - if technique_id in line: - if '.' in technique_id: - # if sub technique - technique_name = line[line.index('>') + 1 : line.index(technique_id) - 16] - else: - # if technique - technique_name = line[line.index('>') + 1 : line.index(technique_id) - 12] - break - return technique_name - - - def get_technique_from_id(self, technique_id): - technique_id = self.format_technique_id(technique_id) - technique_url = "https://attack.mitre.org/techniques/{}/".format(technique_id.replace('.', '/')) - return { - 'id': technique_id, - 'name': self.get_technique_name_from_url(technique_url, technique_id), - 'reference': technique_url - } diff --git a/helpers/utils.py b/helpers/utils.py deleted file mode 100644 index 64afa5e..0000000 --- a/helpers/utils.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -import json -import logging -import colorlog - - -# Global Vars -color = { - "green": "#61ff33", - "yellow": "#ecff33", - "red": "#D00000" -} -############# - - -def create_log_file(log_file_name): - with open(log_file_name, 'w') as o: pass - - -def get_slash_set_path(path, logger): - try: - slash = '/' - if path and path != '': - if os.name == 'nt': - slash = '\\' - path = path.replace('/', slash) - else: - path = path.replace('\\', slash) - except Exception as e: - logger.error(f"Exception {e} occurred in get_slash_set_path() for path {path}...") - logger.info(f"get_slash_set_path() finished successfully for path {path}...") - return path - - -def get_slashes(): - ret = '\\' - if os.name != 'nt': - ret = '/' - # print('Non-windows machine...') - # else: - # print('Windows machine...') - # print('Returning {}...'.format(ret)) - return ret - - -def setup_logger(log_fmt="%(log_color)s%(asctime)s:%(levelname)s:%(message)s", log_file_name=".output.log", level='DEBUG'): - - # a new log file is created each time. - # no space issues are caused. - create_log_file(log_file_name) - - formatter = colorlog.ColoredFormatter( - log_fmt, - datefmt='%DT%H:%M:%SZ' - ) - - logger = logging.getLogger() - - handler2 = logging.FileHandler(log_file_name) - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.addHandler(handler2) - logger.setLevel(level) - - return logger - - -def config_file_to_dict(filename='config/.sample-config.json'): - config_dict = {} - with open(filename) as json_file: config_dict = json.load(json_file) - return config_dict \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..205f657 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,294 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "colorlog" +version = "6.7.0" +description = "Add colours to the output of Python's logging module." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, + {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "packaging" +version = "22.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, +] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pysigma" +version = "0.9.8" +description = "Sigma rule processing and conversion tools" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pysigma-0.9.8-py3-none-any.whl", hash = "sha256:fd344bf98cc33c4b2e424210a5b8f92248877186f459d7724ddf68fa87dc653f"}, + {file = "pysigma-0.9.8.tar.gz", hash = "sha256:01102da47dbb80d0c975f0d9ba91fbdc1ce95452142b2c314022b563ff8d4baf"}, +] + +[package.dependencies] +packaging = ">=22.0,<23.0" +pyparsing = ">=3.0.7,<4.0.0" +pyyaml = ">=6.0,<7.0" +requests = ">=2.28.1,<3.0.0" + +[[package]] +name = "pysigma-backend-elasticsearch" +version = "1.0.3" +description = "pySigma Elasticsearch backend" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pysigma_backend_elasticsearch-1.0.3-py3-none-any.whl", hash = "sha256:55b49763d3c9c11c6d5422894a47c90665691fd6e7141d3bea9b8824a0b9b85e"}, + {file = "pysigma_backend_elasticsearch-1.0.3.tar.gz", hash = "sha256:e0a3d2f05a78e413d041a2768f9e9b15dd6ff19fe9a274d2bef30292b8d29858"}, +] + +[package.dependencies] +pysigma = ">=0.9.5,<0.10.0" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "requests" +version = "2.29.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "urllib3" +version = "1.26.15" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "9334fa98db24e49cf4015de6297432dce5027b051d6c13fce748a4a1535d0a42" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..283c083 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "siegma" +version = "0.2.0" +description = "This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the Sigma rule format and engine." +authors = ["Wesley Cesar <72465364+wesley587@users.noreply.github.com>"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +colorlog = "^6.7.0" +pysigma = "^0.9.8" +pysigma-backend-elasticsearch = "^1.0.3" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 970cae3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pendulum -Pillow -pyfiglet -requests -colorlog -PyYAML \ No newline at end of file diff --git a/rule_file_creator_scripts/ala_rule.py b/rule_file_creator_scripts/ala_rule.py deleted file mode 100644 index 08adfed..0000000 --- a/rule_file_creator_scripts/ala_rule.py +++ /dev/null @@ -1,533 +0,0 @@ -from logging import log -import os -import re -import copy -import json -import requests -import subprocess -from pprint import pprint -from helpers.utils import get_slash_set_path, get_slashes - - -def get_author_name(query_d, yj_rule_d, logger): - ret = None - ret = query_d.replace(' by {}'.format(str(yj_rule_d)), '') - return ret - - -def dump_to_file(logger, dictionary, output='.output.azure.txt'): - try: - with open(output, "w") as outfile: - json.dump(dictionary, outfile) - outfile.write('\n') - logger.info('dict dumped to file {}'.format(output)) - except Exception as e: - logger.error("Exception {} occurred in dump_to_file()...".format(e)) - return output - - -def get_risk_score(severity): - ret = 15 - if severity == 'low': ret = 25 - elif severity == 'medium': ret = 50 - elif severity == 'high': ret = 75 - elif severity == 'critical': ret = 100 - else: ret = 0 - return ret - - -def get_tags(tag_metadata): - ret = [] - ret_aws = ['aws', 'cloudtrail'] - if tag_metadata in ret_aws: - ret = ret_aws - return ret - - -def is_technique(attack, item, logger): - logger.debug('Starting is_technique()...') - is_technique_boolean = False - technique = {} - match_list = re.findall(r'^attack\.t\d{4,}$', item) - logger.debug(match_list) - if len(match_list) > 0: - logger.debug(match_list) - is_technique_boolean = True - technique = attack.get_technique_from_id(item) - # if technique: is_technique_boolean = True - logger.debug('technique item {} data:'.format(item)) - logger.debug(technique) - return is_technique_boolean, technique - - -def get_technique_from_mitre(attack, technique_id, logger): - found = False - logger.debug('Starting get_technique_from_mitre()...') - ret = { - 'id': technique_id, - 'name': technique_id, - 'reference': '' - } - for technique in attack.enterprise.techniques: - if technique.id.lower() == technique_id: - logger.debug(technique.id) - logger.debug(technique.name) - logger.debug(technique.wiki) - ret['id'] = technique.id - ret['name'] = technique.name - ret['reference'] = technique.wiki - found = True - break - # in case of an incorrect technique ID, it won't be detected so we need to let the parent function know that we failed to find the unknown technique. - # if not found: ret = None - return ret - - -def get_tactic_from_mitre(attack, item): - print(f'Starting get_tactic_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for tactic in attack.enterprise.tactics: - # print('Tactic name: ') - # print(tactic.name) - if tactic.name.lower() == item: - # print(tactic.id) - # print(tactic.name) - # print(tactic.wiki) - ret['id'] = tactic.id - ret['name'] = tactic.name - ret['reference'] = tactic.wiki - return ret - - -def is_tactic(attack, item, logger): - logger.debug('Starting is_tactic()...') - is_tactic_boolean = False - tactic = {} - match_list = re.findall(r'^attack\.\w{5,}.*$', item) - if item.count('.') > 1: match_list = [] - # print(match_list) - if len(match_list) > 0: - # print(match_list) - is_tactic_boolean = True - tactic = get_tactic_from_mitre(attack, item.replace('attack.', '').replace('_', ' ')) - logger.debug('tactic item {} data:'.format(item)) - logger.debug(tactic) - return is_tactic_boolean, tactic - - -def get_subtechnique_from_mitre(attack, item, logger): - logger.debug(f'Starting get_subtechnique_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for technique in attack.enterprise.techniques: - for subtechnique in technique.subtechniques: - if subtechnique.id.lower() == item: - logger.debug(subtechnique.id) - logger.debug(subtechnique.name) - logger.debug(subtechnique.wiki) - ret['id'] = subtechnique.id - ret['name'] = subtechnique.name - ret['reference'] = subtechnique.wiki - return ret - - -def is_subtechnique(attack, item, logger): - logger.debug('Starting is_subtechnique()...') - is_subtechnique_boolean = False - subtechnique = {} - match_list = re.findall(r'^(attack\.t\d{4,})\.\d+$', item) - # print(match_list) - if len(match_list) > 0: - # print(match_list) - is_subtechnique_boolean = True - subtechnique = get_subtechnique_from_mitre(attack, item.replace('attack.', ''), logger) - return is_subtechnique_boolean, subtechnique - - -def get_mitre_ttps(attack, yj_rule, logger): - # Sample MITRE TTPs format - # tags: - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - ret = [] - temp = { - 'framework': 'MITRE ATT&CK', - 'tactic': {}, - 'technique': [] - } - # individual_technique_object = { - # 'id': '', - # 'reference': '', - # 'name': '' - # } - idx = 0 - # Read tactic. - for item in yj_rule[idx:]: - temp2 = copy.deepcopy(temp) - logger.debug(f'item: {item}\tidx: {idx}') - is_tactic_boolean, tactic = is_tactic(attack, item, logger) - logger.debug(f'is_tactic_boolean: {is_tactic_boolean}') - temp2['tactic'] = tactic - if is_tactic_boolean: - logger.debug('Now inside tactic techniques if/else...') - idx += 1 - logger.debug('Current idx after ++: {}'.format(idx)) - logger.debug('len_uj_rule ++: {}'.format(len(yj_rule))) - if idx <= len(yj_rule) - 1: - # Read techniques below - logger.debug('Started inside for loop...') - for idx2, item2 in enumerate(yj_rule[idx:]): - logger.debug('for loop is_technique part...') - is_technique_boolean, technique = is_technique(attack, item2, logger) - if not is_technique_boolean: - # Read subtechniques - logger.debug('for loop not is_technique part...') - is_subtechnique_boolean, subtechnique = is_subtechnique(attack, item2, logger) - if not is_subtechnique_boolean: - logger.debug('for loop not is_subtechnique part...') - break - if is_subtechnique_boolean: - logger.debug('for loop is_subtechnique part...') - temp2['technique'].append(subtechnique) - if is_technique_boolean: - temp2['technique'].append(technique) - idx += idx2 - logger.debug('Finished inside for loop...') - # So only non-empty and valid tactics and techniques make it to the final output - if temp2.get('tactic') != {} and temp2.get('technique') != [] and temp2.get('tactic').get('id') != '': - ret.append(temp2) - # pprint(temp) - pprint(ret) - return ret - - -def valid_credentials(credentials, logger): - ret = False - creds_exist = client_id_exist = tenant_id_exist = client_secret_exist = subscription_id_exist = resource_group_exist = False - if credentials and credentials is not None: - creds_exist = True - logger.debug('Credentials key exists...') - logger.debug('Checking further...') - if 'azure_client_id' in credentials: - if credentials.get('azure_client_id') and credentials.get('azure_client_id') != '': - client_id_exist = True - logger.debug('client_id exists...') - if 'azure_client_secret' in credentials: - if credentials.get('azure_client_secret') and credentials.get('azure_client_secret') != '': - client_secret_exist = True - logger.debug('client_secret exists...') - if 'azure_tenant_id' in credentials: - if credentials.get('azure_tenant_id') and credentials.get('azure_tenant_id') != '': - tenant_id_exist = True - logger.debug('tenant_id exists...') - if 'azure_subscription_id' in credentials: - if credentials.get('azure_subscription_id') and credentials.get('azure_subscription_id') != '': - subscription_id_exist = True - logger.debug('subscription_id exists...') - if 'azure_resource_group' in credentials: - if credentials.get('azure_resource_group') and credentials.get('azure_resource_group') != '': - resource_group_exist = True - logger.debug('resource_group exists...') - if creds_exist and client_id_exist and tenant_id_exist and client_secret_exist and subscription_id_exist and resource_group_exist: - ret = True - logger.info('Existing creds found. Output file shall be uploaded to Azure...') - else: - ret = False - logger.info('No creds found. Output file shall not be uploaded to Azure...') - return ret - - -def handle_response_errors(status_code, message, logger): - if status_code == 400 and 'invalid file extension'.lower() in message.lower(): - logger.error('Use .azure.txt as the file extension for output file...') - - -def get_access_token(credentials, logger): - token = None - url = "https://login.microsoftonline.com/{}/oauth2/token".format(credentials.get('azure_tenant_id')) - - payload='grant_type=client_credentials&client_id={}&client_secret={}&resource=https%3A%2F%2Fmanagement.azure.com%2F'.format(credentials.get('azure_client_id'), credentials.get('azure_client_secret')) - # headers = { - # 'Content-Type': 'application/x-www-form-urlencoded', - # 'Cookie': 'fpc=AqQZYV-ZmzxNuM0xabApxT4ac0cgAQAAAGqGVNgOAAAA' - # } - - # response = requests.request("POST", url, headers=headers, data=payload) - - response = requests.request("POST", url, data=payload) - - logger.debug('creds response:') - logger.debug(response.text) - pprint(response.json()) - logger.debug('status code: {}'.format(response.status_code)) - - token = response.json().get('access_token') - - return token - - -def install_rules(script_dir, credentials, rule_file, yj_rule, logger): - return_status = 0 - token = None - token = get_access_token(credentials, logger) - if token is None: - return_status = 1 - else: - # rule_file_json = json.load(open(rule_file)) - - url = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{1}/providers/Microsoft.SecurityInsights/alertRules/{2}?api-version=2020-01-01".format(credentials.get('azure_subscription_id'), credentials.get('azure_resource_group'), yj_rule.get('id')) - - payload = json.dumps(json.load(open(rule_file))) - headers = { - 'Authorization': 'Bearer {}'.format(token), - 'Content-Type': 'application/json' - } - - response = requests.request("PUT", url, headers=headers, data=payload) - - logger.debug('rule import response:') - logger.debug(response.text) - pprint(response.json()) - logger.debug('status code: {}'.format(response.status_code)) - - if response.status_code >= 200 and response.status_code <= 299: - return_status = 0 - logger.info('Rule {} successfully installed on Azure Sentinel...'.format(yj_rule.get('id'))) - else: - return_status = 1 - logger.error('Rule {} could not be installed on Azure Sentinel...'.format(yj_rule.get('id'))) - - return return_status - - -def rate_based_rule_settings(sigma_config, config, config_t, yj_rule_t, logger): - # directly make changes to config variable and then return it at the end - temp = {} - update_required = False - try: - logger.debug('if else starting...') - logger.debug(f'yj_rule_t: {yj_rule_t}') - logger.debug(f'config_t: {config_t}') - # if yj_rule has threshold fields - # if (not update_required) and yj_rule_t and yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - for i in range(1): - if (not update_required) and yj_rule_t: - # if in the rule, field or value under threshold are empty or null, then rule is not rate based. - if yj_rule_t.get('field') is None or yj_rule_t.get('value') is None: break - if yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - # if threshold is defined in rule - temp = yj_rule_t - update_required = True - logger.debug('rule threshold set...') - # elif (not update_required) and config_t and config_t.get('field') and (not config_t.get('field') == '') and config_t.get('value') and type(config_t.get('value')) == int: - elif (not update_required) and config_t and config_t.get('field') is not None and config_t.get('value') is not None and type(config_t.get('value')) == int: - # elif (not update_required) and config_t and config_t.get('value') and type(config_t.get('value')) == int: - # if threshold is defined in siegma config - temp = config_t - update_required = True - logger.debug('config threshold set...') - else: logger.debug('temp is empty...') - if update_required: - # change field name to ECS format and update rate threshold in config - ecs_field = '' - # handle empty field gracefully - # t = temp.get('field') - # logger.debug(f'temp.field: {t}') - # try to convert threshold field to sigma mapped field. - if temp.get('field') and temp.get('field') != '' and sigma_config.get('fieldmappings').get(temp.get('field')) is not None: ecs_field = sigma_config.get('fieldmappings').get(temp.get('field')) - # if conversion fails, then consider whatever value was in the threshold.field as ecs formatted and move forward with that - else: ecs_field = temp.get('field') - config['threshold'] = { - 'field' : ecs_field, - 'value': temp.get('value') - } - config['type'] = 'threshold' - else: - # if deafult config has threshold in it, then delete threshold key entirely if no changes were made - if config.get('threshold'): del config['threshold'] - except Exception as e: - logger.error(f'Exception {e} occurred in rate_based_rule_settings()...') - logger.info('rate_based_rule_settings() finished successfully...') - return config - - -# def get_notes(notes_folder, config_n, yj_rule_n, logger): -def get_notes(notes_folder, yj_rule_n, logger): - ret = '' - file_name = '' - try: - # if type(config_n) == str and config_n != "": - # config_n = [config_n] - # config_n = [get_slash_set_path(i, logger) for i in config_n] - if type(yj_rule_n) == str and yj_rule_n != "": - yj_rule_n = [yj_rule_n] - yj_rule_n = [get_slash_set_path(i, logger) for i in yj_rule_n] - update_required = False - if (not update_required) and yj_rule_n and type(yj_rule_n) == list and len(yj_rule_n) > 0: - file_name = yj_rule_n - update_required = True - logger.debug('File name set from rule...') - # elif (not update_required) and config_n and type(config_n) == list and len(config_n) > 0: - # file_name = config_n - # update_required = True - # logger.debug('File name set from config...') - else: logger.debug('File name not set...') - if update_required: - # add forward/back slash to end of folder name - if notes_folder and len(notes_folder) > 0 and notes_folder[-1] != get_slashes(): notes_folder += get_slashes() - # remove forward/back slash from start of file name - if file_name and len(file_name) > 0 and type(file_name) == list: - for i in file_name: - if i[0] == get_slashes(): i = i[1:] - with open(get_slash_set_path(notes_folder + i, logger)) as input_file: - ret += input_file.read() - ret += "\n" - logger.debug(ret) - except Exception as e: - logger.error(f'Exception {e} occurred in get_notes()...') - return ret - logger.info(f'get_notes() finished successfully...') - return ret - - -def find_d2_kv_in_d1(d1, d2): - for k2, v2 in d2.items(): - if k2 in d1: - d1[k2] = v2 - return d1 - - -def add_new_items_to_config(shared_config, rule_config, logger): - if 'settings' in rule_config: - shared_config = find_d2_kv_in_d1(shared_config, rule_config.get('settings')) - logger.info('add_new_items_to_config() finished successfully...') - return shared_config - - -# def get_value_from_co_dict(c, o, c_key, sub_o): -# ret = None -# if o and sub_o in o and o.get(sub_o): ret = o.get(sub_o) -# else: ret = c.get(c_key) -# return ret - - -def get_azure_severity(severity): - ret = None - allowed_severity = ['high', 'medium', 'low', 'informational'] - if severity.lower() in allowed_severity: - ret = severity - elif severity.lower() == 'critical': - ret = 'high' - else: ret = 'informational' - return ret - - -def list_to_str(item, logger): - ret = None - try: - if type(item) == list: - logger.debug('{} is a list...'.format(item)) - ret = '\n'.join(item) - elif type(item) == str: - logger.debug('{} is an str...'.format(item)) - ret = item - else: - logger.debug('{} is an unknown type...'.format(item)) - ret = item - except Exception as e: - logger.error('Exception {} occurred in list_to_str()...'.format(e)) - return ret - - -def create_rule(siegma_config, notes_folder, config, sigma_config, credentials, query, yj_rule, attack, output, script_dir, logger, testing=False): - logger.info('Starting create_rule()...') - rule_file = None - rule_content = None - try: - # logger.debug('config:') - # logger.debug(config) - # logger.debug('query:') - # pprint(query) - # logger.debug(type(query)) - # # set siegma config as per config defined in rule - config = add_new_items_to_config(config, yj_rule.get('siegma').get('config'), logger) if 'siegma' in yj_rule and 'config' in yj_rule.get('siegma') else config - # unset sigma original default values - # query['queryPeriod'] = '' - # query['queryFrequency'] = '' - # query['triggerOperator'] = '' - # query['triggerThreshold'] = '' - # query['suppressionDuration'] = '' - # query['suppressionEnabled'] = '' - - - # set query quotes - # query['query'] = query['query'].replace('\"', "'") - # query['query'] = query['query'].replace('(', "") - # query['query'] = query['query'].replace(')', "") - # set severity - query['severity'] = get_azure_severity(query['severity']) - # set queryPeriod - query['queryPeriod'] = config.get('queryPeriod') - # set queryFrequency - query['queryFrequency'] = config.get('queryFrequency') - # set triggerOperator - query['triggerOperator'] = config.get('triggerOperator') - # set triggerThreshold - query['triggerThreshold'] = config.get('triggerThreshold') if 'threshold' not in yj_rule else yj_rule.get('threshold').get('value') - # set suppressionDuration - query['suppressionDuration'] = config.get('suppressionDuration') - # set suppressionEnabled - query['suppressionEnabled'] = config.get('suppressionEnabled') - - # update displayName - query['displayName'] = get_author_name(query['displayName'], yj_rule.get('author'), logger) - - # Force addition of MITRE tactics - if 'tactics' not in query or query['tactics'] == []: - if 'tags' in yj_rule: - query['tactics'] = attack.get_tactics_name_set(yj_rule.get('tags')) - logger.debug('Returning tactics: ...') - pprint(query['tactics']) - - ######### description updates ########################## - # merge author in description - query['description'] += '' if yj_rule.get('author') is None else '\n\n# Author:\n\n' + list_to_str(yj_rule.get('author'), logger) - # merge falsepositives in tags - query['description'] += '' if yj_rule.get('tags') is None else '\n\n# MITRE ATT&CK Tags:\n\n' + list_to_str(yj_rule.get('tags'), logger) - # merge notes in description - # query['description'] += '\n\nADS/Notes:\n\n' + get_notes(notes_folder, config.get('note'), yj_rule.get('note'), logger) - query['description'] += '' if yj_rule.get('note') is None else '\n\n# ADS/Notes:\n\n' + get_notes(notes_folder, yj_rule.get('note'), logger) - # merge falsepositives in description - query['description'] += '' if yj_rule.get('falsepositives') is None else '\n\n# False Positives:\n\n' + list_to_str(yj_rule.get('falsepositives'), logger) - # merge references in description - query['description'] += '' if yj_rule.get('references') is None else '\n\n# References:\n\n' + list_to_str(yj_rule.get('references'), logger) - # only take first 5000 chars of description because Sentinel doesn't support more - if len(query['description']) > 5000: - logger.warning('description length exceeds 5000 for {}. Therefore going to select only first 5000 chars and omitting rest...'.format(query['displayName'])) - query['description'] = query['description'][:4960] + '\n\nSee rest in original rule.yml file.' - #################################################### - - rule_content = { - 'kind': config.get('kind'), - 'etag': '*', - 'properties': query - } - - ############# - logger.info('Final rule_content:') - logger.info(config) - rule_file = dump_to_file(logger, rule_content, output=output) - except Exception as e: - logger.error(f'Exception {e} occurred in create_rule()...') - return rule_file diff --git a/rule_file_creator_scripts/es_eql.py b/rule_file_creator_scripts/es_eql.py deleted file mode 100644 index d0df486..0000000 --- a/rule_file_creator_scripts/es_eql.py +++ /dev/null @@ -1,572 +0,0 @@ -import os -import re -import copy -import json -import subprocess -from pprint import pprint -from helpers.utils import get_slash_set_path, get_slashes - - -def get_author_name_s2(val): - ''' - Sub function of author name that performs several checks before finalizing the author name - ''' - ret = None - if type(val) == list: - ret = val - elif type(val) == str: - ret = [val] - else: ret = list(val) - return ret - - -def get_author_name(yj_rule, config, logger): - ret = None - already_done = False - if not already_done: - ret = get_author_name_s2(yj_rule) - if ret is not None: - already_done = True - logger.debug(f'Author {ret} name set from rule...') - if not already_done: - ret = get_author_name_s2(config) - if ret is not None: - already_done = True - logger.debug(f'Author {ret} name set from config...') - return ret - - -def dump_to_file(logger, dictionary, output='.output.ndjson'): - try: - with open(output, "a") as outfile: - json.dump(dictionary, outfile) - outfile.write('\n') - logger.info('dict dumped to file {}'.format(output)) - except Exception as e: - logger.error("Exception {} occurred in dump_to_file()...".format(e)) - return output - - -def get_risk_score(severity): - ret = 15 - if severity == 'low': ret = 25 - elif severity == 'medium': ret = 50 - elif severity == 'high': ret = 75 - elif severity == 'critical': ret = 100 - else: ret = 0 - return ret - - -def get_tags(tag_metadata): - ret = [] - ret_aws = ['aws', 'cloudtrail'] - if tag_metadata in ret_aws: - ret = ret_aws - return ret - - -def is_technique(attack, item, logger): - logger.debug('Starting is_technique()...') - is_technique_boolean = False - technique = {} - match_list = re.findall(r'^attack\.t\d{4,}((\.\d{3,})*)$', item) - logger.debug(match_list) - if len(match_list) > 0: - logger.debug(match_list) - is_technique_boolean = True - technique = attack.get_technique_from_id(item) - # technique = get_technique_from_mitre(attack, item.replace('attack.', ''), logger) - # if technique: is_technique_boolean = True - logger.debug('technique item {} data:'.format(item)) - logger.debug(technique) - return is_technique_boolean, technique - - -def get_technique_from_mitre(attack, technique_id, logger): - found = False - logger.debug('Starting get_technique_from_mitre()...') - ret = { - 'id': technique_id, - 'name': technique_id, - 'reference': '' - } - try: - for technique in attack.enterprise.techniques: - if technique.id.lower() == technique_id: - logger.debug(technique.id) - logger.debug(technique.name) - logger.debug(technique.wiki) - ret['id'] = technique.id - ret['name'] = technique.name - ret['reference'] = technique.wiki - found = True - break - except Exception as e: - logger.error('Exception {} occurred in get_technique_from_mitre()...'.format(e)) - # in case of an incorrect technique ID, it won't be detected so we need to let the parent function know that we failed to find the unknown technique. - # if not found: ret = None - return ret - - -def get_tactic_from_mitre(attack, item): - print(f'Starting get_tactic_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for tactic in attack.enterprise.tactics: - # print('Tactic name: ') - # print(tactic.name) - if tactic.name.lower() == item: - # print(tactic.id) - # print(tactic.name) - # print(tactic.wiki) - ret['id'] = tactic.id - ret['name'] = tactic.name - ret['reference'] = tactic.wiki - return ret - - -def is_tactic(attack, item, logger): - logger.debug('Starting is_tactic()...') - is_tactic_boolean = False - tactic = {} - match_list = re.findall(r'^attack\.\w{5,}.*$', item) - if item.count('.') > 1: match_list = [] - # print(match_list) - if len(match_list) > 0: - logger.debug(match_list) - is_tactic_boolean = True - # print('up') - # input('') - tactic = attack.get_tactic_from_name(item) - # tactic = get_tactic_from_mitre(attack, item.replace('attack.', '').replace('_', ' ')) - # print('down') - # input('') - logger.debug('tactic item {} data:'.format(item)) - logger.debug(tactic) - return is_tactic_boolean, tactic - - -def get_subtechnique_from_mitre(attack, item, logger): - logger.debug(f'Starting get_subtechnique_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for technique in attack.enterprise.techniques: - for subtechnique in technique.subtechniques: - if subtechnique.id.lower() == item: - logger.debug(subtechnique.id) - logger.debug(subtechnique.name) - logger.debug(subtechnique.wiki) - ret['id'] = subtechnique.id - ret['name'] = subtechnique.name - ret['reference'] = subtechnique.wiki - return ret - - -def is_subtechnique(attack, item, logger): - logger.debug('Starting is_subtechnique()...') - is_subtechnique_boolean = False - subtechnique = {} - match_list = re.findall(r'^(attack\.t\d{4,})\.\d+$', item) - # print(match_list) - if len(match_list) > 0: - # print(match_list) - is_subtechnique_boolean = True - subtechnique = attack.get_technique_from_id(item) - # subtechnique = get_subtechnique_from_mitre(attack, item.replace('attack.', ''), logger) - return is_subtechnique_boolean, subtechnique - - -def get_mitre_ttps(attack, yj_rule, logger): - # Sample MITRE TTPs format - # tags: - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - ret = [] - temp = { - 'framework': 'MITRE ATT&CK', - 'tactic': {}, - 'technique': [] - } - # individual_technique_object = { - # 'id': '', - # 'reference': '', - # 'name': '' - # } - idx = 0 - idx2 = 0 - try: - # Read tactic. - # for item in yj_rule[idx:]: - while(idx < len(yj_rule) - 1): - temp2 = copy.deepcopy(temp) - item = yj_rule[idx] - logger.debug(f'item: {item}\tidx: {idx}') - is_tactic_boolean, tactic = is_tactic(attack, item, logger) - logger.debug(f'is_tactic_boolean: {is_tactic_boolean}') - temp2['tactic'] = tactic - if is_tactic_boolean: - logger.debug('Now inside tactic techniques if/else...') - idx += 1 - logger.debug('Current idx after ++: {}'.format(idx)) - logger.debug('len_uj_rule ++: {}'.format(len(yj_rule))) - if idx <= len(yj_rule) - 1: - # Read techniques below - logger.debug('Started inside for loop...') - for idx2, item2 in enumerate(yj_rule[idx:]): - logger.debug('for loop is_technique part...') - is_technique_boolean, technique = is_technique(attack, item2, logger) - if is_technique_boolean: - temp2['technique'].append(technique) - else: - print('breaking inner for loop idx={} ==> idx2={}...'.format(idx, idx2)) - # idx -= 1 - print('breaking inner for loop after ++ idx={}...'.format(idx, idx2)) - break - idx += 1 - # if not is_technique_boolean: - # Read subtechniques - # logger.debug('for loop not is_technique part...') - # is_subtechnique_boolean, subtechnique = is_subtechnique(attack, item2, logger) - # if not is_subtechnique_boolean: - # logger.debug('for loop not is_subtechnique part...') - # break - # if is_subtechnique_boolean: - # logger.debug('for loop is_subtechnique part...') - # temp2['technique'].append(subtechnique) - # pass - logger.debug('Finished inside for loop...') - # So only non-empty and valid tactics and techniques make it to the final output - if temp2.get('tactic') != {} and temp2.get('technique') != [] and temp2.get('tactic').get('id') != '': - ret.append(temp2) - except Exception as e: - print('Exception {} occurred in get_mitre_ttps()...'.format(e)) - # pprint(temp) - pprint(ret) - return ret - - -def valid_credentials(credentials, logger): - ret = False - creds_exist = username_exist = password_exist = url_exist = False - if credentials and credentials is not None: - creds_exist = True - logger.debug('Credentials key exists...') - logger.debug('Checking further...') - if 'kibana_username' in credentials: - if credentials.get('kibana_username') and credentials.get('kibana_username') != '': - username_exist = True - logger.debug('Kibana_username exists...') - if 'kibana_password' in credentials: - if credentials.get('kibana_password') and credentials.get('kibana_password') != '': - password_exist = True - logger.debug('kibana_password exists...') - if 'kibana_url' in credentials: - if credentials.get('kibana_url') and credentials.get('kibana_url') != '': - url_exist = True - logger.debug('kibana_url exists...') - if creds_exist and username_exist and password_exist and url_exist: - ret = True - logger.info('Existing creds found. Output file shall be uploaded to ELK...') - else: - ret = False - logger.info('No creds found. Output file shall not be uploaded to ELK...') - return ret - - -def handle_response_errors(status_code, message, logger): - if status_code == 400 and 'invalid file extension'.lower() in message.lower(): - logger.error('Use .ndjson as the file extension for output file...') - - -def install_rules(script_dir, credentials, rule_file, logger): - return_status = 0 - # if windows, execute these commands - curl_path = get_slash_set_path(script_dir + '/helpers/curl/curl.exe', logger) - logger.debug('Script Dir: {}'.format(curl_path)) - logger.debug('Rule Output File: {}'.format(rule_file)) - result = result_out = None - query = None - # if windows machine - if os.name == 'nt': - command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X POST \"{}/api/detection_engine/rules/_import?overwrite=true\" -u '{}:{}' -H 'kbn-xsrf: true' -H 'Content-Type: multipart/form-data' --form 'file=@{}'".format(credentials.get('kibana_url'), credentials.get('kibana_username'), credentials.get('kibana_password'), rule_file)) - logger.debug('Command: {}'.format(command)) - logger.info('Windows powershell command shall be executed...') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = json.loads(result.stdout.decode('utf-8')) - result_error = result.stderr.decode('utf-8') - # if error code var is not empty, then set return status to 1 - if result.returncode != 0: return_status = 1 - logger.debug(result_out) - if return_status == 1: - logger.error(result_error) - # if linux machine - else: - command = """curl -X POST "{}/api/detection_engine/rules/_import?overwrite=true" -u '{}:{}' -H 'kbn-xsrf: true' -H 'Content-Type: multipart/form-data' --form "file=@{}" """.format(credentials.get('kibana_url'), credentials.get('kibana_username'), credentials.get('kibana_password'), rule_file) - logger.debug('Command: {}'.format(command)) - logger.info('Linux shell shall be executed...') - process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip().decode('utf-8') - print(proc_stdout) - result_out = json.loads(proc_stdout) - result_error = process.returncode - # if error code var is not empty, then set return status to 1 - if result_error != 0: return_status = 1 - logger.debug(result_out) - logger.info('Import Successful: {}...'.format(result_out.get('success'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('success') != True: - return_status = 1 - logger.info('Count Successfully Imported Rules: {}...'.format(result_out.get('success_count'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('success_count') <= 0: - return_status = 1 - logger.info('Import Errors: {}...'.format(result_out.get('errors'))) - # if elasticsearch output var is not empty, then set return status to 1 - if len(result_out.get('errors')) > 0: - return_status = 1 - logger.info('Response Message: {}...'.format(result_out.get('message'))) - logger.info('Response status code: {}...'.format(result_out.get('status_code'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('status_code') != None: - return_status = 1 - handle_response_errors(result_out.get('status_code'), result_out.get('message'), logger) - return return_status, query - - -def rate_based_rule_settings(sigma_config, config, config_t, yj_rule_t, logger): - # directly make changes to config variable and then return it at the end - temp = {} - update_required = False - try: - logger.debug('if else starting...') - logger.debug(f'yj_rule_t: {yj_rule_t}') - logger.debug(f'config_t: {config_t}') - # if yj_rule has threshold fields - # if (not update_required) and yj_rule_t and yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - for i in range(1): - if (not update_required) and yj_rule_t: - # if in the rule, field or value under threshold are empty or null, then rule is not rate based. - if yj_rule_t.get('field') is None or yj_rule_t.get('value') is None: break - if yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - # if threshold is defined in rule - temp = yj_rule_t - update_required = True - logger.debug('rule threshold set...') - # elif (not update_required) and config_t and config_t.get('field') and (not config_t.get('field') == '') and config_t.get('value') and type(config_t.get('value')) == int: - elif (not update_required) and config_t and config_t.get('field') is not None and config_t.get('value') is not None and type(config_t.get('value')) == int: - # elif (not update_required) and config_t and config_t.get('value') and type(config_t.get('value')) == int: - # if threshold is defined in siegma config - temp = config_t - update_required = True - logger.debug('config threshold set...') - else: logger.debug('temp is empty...') - if update_required: - # change field name to ECS format and update rate threshold in config - ecs_field = '' - # handle empty field gracefully - # t = temp.get('field') - # logger.debug(f'temp.field: {t}') - # try to convert threshold field to sigma mapped field. - if temp.get('field') and temp.get('field') != '' and sigma_config.get('fieldmappings').get(temp.get('field')) is not None: ecs_field = sigma_config.get('fieldmappings').get(temp.get('field')) - # if conversion fails, then consider whatever value was in the threshold.field as ecs formatted and move forward with that - else: ecs_field = temp.get('field') - config['threshold'] = { - 'field' : ecs_field, - 'value': temp.get('value') - } - config['type'] = 'threshold' - else: - # if deafult config has threshold in it, then delete threshold key entirely if no changes were made - if config.get('threshold'): del config['threshold'] - except Exception as e: - logger.error(f'Exception {e} occurred in rate_based_rule_settings()...') - logger.info('rate_based_rule_settings() finished successfully...') - return config - - -def get_notes(notes_folder, config_n, yj_rule_n, logger): - ret = '' - file_name = '' - if type(config_n) == str and config_n != "": - config_n = [config_n] - config_n = [get_slash_set_path(i, logger) for i in config_n] - if type(yj_rule_n) == str and yj_rule_n != "": - yj_rule_n = [yj_rule_n] - yj_rule_n = [get_slash_set_path(i, logger) for i in yj_rule_n] - update_required = False - try: - if (not update_required) and yj_rule_n and type(yj_rule_n) == list and len(yj_rule_n) > 0: - file_name = yj_rule_n - update_required = True - logger.debug('File name set from rule...') - elif (not update_required) and config_n and type(config_n) == list and len(config_n) > 0: - file_name = config_n - update_required = True - logger.debug('File name set from config...') - else: logger.debug('File name not set...') - if update_required: - # add forward/back slash to end of folder name - if notes_folder and len(notes_folder) > 0 and notes_folder[-1] != get_slashes(): notes_folder += get_slashes() - # remove forward/back slash from start of file name - if file_name and len(file_name) > 0 and type(file_name) == list: - for i in file_name: - if i[0] == get_slashes(): i = i[1:] - with open(get_slash_set_path(notes_folder + i, logger)) as input_file: - ret += input_file.read() - ret += "\n" - print(ret) - except Exception as e: - logger.error(f'Exception {e} occurred in get_notes()...') - return ret - logger.info(f'get_notes() finished successfully...') - return ret - - -def find_d2_kv_in_d1(d1, d2): - for k2, v2 in d2.items(): - if k2 in d1: - d1[k2] = v2 - return d1 - - -def add_new_items_to_config(shared_config, rule_config, logger): - if 'settings' in rule_config: - shared_config = find_d2_kv_in_d1(shared_config, rule_config.get('settings')) - logger.info('add_new_items_to_config() finished successfully...') - return shared_config - - -def get_enabled_state(id, credentials, script_dir, logger, testing=False): - enabled_state = True - return_status = 0 - - if testing: - logger.warn('Since testing switch is enabled, enablement check will not be performed. Considering status as enabled....') - return enabled_state - - curl_path = get_slash_set_path(script_dir + '/helpers/curl/curl.exe', logger) - # if windows machine - if os.name == 'nt': - # command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X GET {}/api/detection_engine/rules?rule_id={} -u '{}:{}' -H 'kbn-xsrf: true'".format(credentials.get('kibana_url'), "53209551-b8ce-4945-8df4-0d70314f91e7", credentials.get('kibana_username'), credentials.get('kibana_password'))) - command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X GET {}/api/detection_engine/rules?rule_id={} -u '{}:{}' -H 'kbn-xsrf: true'".format(credentials.get('kibana_url'), id, credentials.get('kibana_username'), credentials.get('kibana_password'))) - logger.debug('Command: {}'.format(command)) - logger.info('Windows powershell command shall be executed...') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = json.loads(result.stdout.decode('utf-8')) - result_error = result.stderr.decode('utf-8') - # if error code var is not empty, then set return status to 1 - if result.returncode != 0: return_status = 1 - logger.debug(result_out) - if 'enabled' in result_out and result_out.get('enabled') == False: - logger.warn('Rule {} state will be disabled...'.format(id)) - enabled_state = False - if return_status == 1: - logger.error(result_error) - # if linux machine - else: - # command = """curl -X GET "{}/api/detection_engine/rules?rule_id={}" -u '{}:{}' -H 'kbn-xsrf: true'""".format(credentials.get('kibana_url'), "53209551-b8ce-4945-8df4-0d70314f91e7", credentials.get('kibana_username'), credentials.get('kibana_password')) - command = """curl -X GET "{}/api/detection_engine/rules?rule_id={}" -u '{}:{}' -H 'kbn-xsrf: true'""".format(credentials.get('kibana_url'), id, credentials.get('kibana_username'), credentials.get('kibana_password')) - logger.debug('Command: {}'.format(command)) - logger.info('Linux shell shall be executed...') - process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip().decode('utf-8') - print(proc_stdout) - result_out = json.loads(proc_stdout) - result_error = process.returncode - # if error code var is not empty, then set return status to 1 - if result_error != 0: return_status = 1 - logger.debug(result_out) - if 'enabled' in result_out and result_out.get('enabled') == False: - logger.warn('Rule {} state will be disabled...'.format(id)) - enabled_state = False - - return enabled_state - - -def get_elastic_actions_s2(val): - ''' - Sub function of author name that performs several checks before finalizing the elastic actions - ''' - ret = None - if isinstance(val, list): - ret = val - elif val is None: - ret = val - else: ret = list(val) - return ret - - -def get_elastic_actions(yj_rule, config, logger): - ret = None - already_done = False - if not already_done: - ret = get_elastic_actions_s2(yj_rule) - if ret is not None: - already_done = True - logger.debug(f'Elastic actions {ret} set from rule...') - if not already_done: - if not config is None: - ret = get_elastic_actions_s2(config) - if ret is not None: - already_done = True - logger.debug(f'Elastic actions {ret} set from config...') - else: ret = [] - else: ret = [] - return ret - -def create_rule(siegma_config, notes_folder, config, sigma_config, credentials, query, yj_rule, attack, output, script_dir, logger, testing=False): - logger.info('Starting create_rule()...') - rule_file = None - try: - logger.debug(config) - # # set siegma config as per config defined in rule - config = add_new_items_to_config(config, yj_rule.get('siegma').get('config'), logger) if 'siegma' in yj_rule and 'config' in yj_rule.get('siegma') else config - # set query - config['query'] = query - # set author name - config['author'] = get_author_name(yj_rule.get('author'), config.get('author'), logger) - # name set - config['name'] = yj_rule.get('title') - # description set - config['description'] = yj_rule.get('description') - # falsepositives set - if yj_rule.get('falsepositives'): config['false_positives'] = yj_rule.get('falsepositives') - # references set - if yj_rule.get('references'): config['references'] = yj_rule.get('references') - # severity set - config['severity'] = yj_rule.get('level') - # risk score set - config['risk_score'] = get_risk_score(yj_rule.get('level')) if 'score' not in yj_rule else yj_rule.get('score') - # tags set - config['tags'] = yj_rule.get('siemtags') if yj_rule and 'siemtags' in yj_rule and type(yj_rule.get('siemtags')) == list else [] - # actions set - config['actions'] = get_elastic_actions(yj_rule.get('elastic_actions'), config.get('actions'), logger) - # MITRE settings - if yj_rule.get('tags'): config['threat'] = get_mitre_ttps(attack, yj_rule.get('tags'), logger) - # rule ID set - config['rule_id'] = config['id'] = yj_rule.get('id') - # time set - if 'timeframe' in yj_rule.get('detection'): config['from'] = 'now-' + yj_rule.get('detection').get('timeframe') - # rate_based_rule - config = rate_based_rule_settings(sigma_config, config, config.get('threshold'), yj_rule.get('threshold'), logger) - # investigation notes - config['note'] = get_notes(notes_folder, config.get('note'), yj_rule.get('note'), logger) - # get current enabled state of the rule from the SIEM - config['enabled'] = get_enabled_state(yj_rule.get('id'), credentials, script_dir, logger, testing=testing) - ############# - logger.info('Final config:') - logger.info(config) - rule_file = dump_to_file(logger, config, output=output) - except Exception as e: - logger.error(f'Exception {e} occurred in create_rule()...') - return rule_file diff --git a/rule_file_creator_scripts/es_qs.py b/rule_file_creator_scripts/es_qs.py deleted file mode 100644 index 8e017ee..0000000 --- a/rule_file_creator_scripts/es_qs.py +++ /dev/null @@ -1,572 +0,0 @@ -import os -import re -import copy -import json -import subprocess -from pprint import pprint -from helpers.utils import get_slash_set_path, get_slashes - - -def get_author_name_s2(val): - ''' - Sub function of author name that performs several checks before finalizing the author name - ''' - ret = None - if type(val) == list: - ret = val - elif type(val) == str: - ret = [val] - else: ret = list(val) - return ret - - -def get_author_name(yj_rule, config, logger): - ret = None - already_done = False - if not already_done: - ret = get_author_name_s2(yj_rule) - if ret is not None: - already_done = True - logger.debug(f'Author {ret} name set from rule...') - if not already_done: - ret = get_author_name_s2(config) - if ret is not None: - already_done = True - logger.debug(f'Author {ret} name set from config...') - return ret - - -def dump_to_file(logger, dictionary, output='.output.ndjson'): - try: - with open(output, "a") as outfile: - json.dump(dictionary, outfile) - outfile.write('\n') - logger.info('dict dumped to file {}'.format(output)) - except Exception as e: - logger.error("Exception {} occurred in dump_to_file()...".format(e)) - return output - - -def get_risk_score(severity): - ret = 15 - if severity == 'low': ret = 25 - elif severity == 'medium': ret = 50 - elif severity == 'high': ret = 75 - elif severity == 'critical': ret = 100 - else: ret = 0 - return ret - - -def get_tags(tag_metadata): - ret = [] - ret_aws = ['aws', 'cloudtrail'] - if tag_metadata in ret_aws: - ret = ret_aws - return ret - - -def is_technique(attack, item, logger): - logger.debug('Starting is_technique()...') - is_technique_boolean = False - technique = {} - match_list = re.findall(r'^attack\.t\d{4,}((\.\d{3,})*)$', item) - logger.debug(match_list) - if len(match_list) > 0: - logger.debug(match_list) - is_technique_boolean = True - technique = attack.get_technique_from_id(item) - # technique = get_technique_from_mitre(attack, item.replace('attack.', ''), logger) - # if technique: is_technique_boolean = True - logger.debug('technique item {} data:'.format(item)) - logger.debug(technique) - return is_technique_boolean, technique - -def get_technique_from_mitre(attack, technique_id, logger): - found = False - logger.debug('Starting get_technique_from_mitre()...') - ret = { - 'id': technique_id, - 'name': technique_id, - 'reference': '' - } - try: - for technique in attack.enterprise.techniques: - if technique.id.lower() == technique_id: - logger.debug(technique.id) - logger.debug(technique.name) - logger.debug(technique.wiki) - ret['id'] = technique.id - ret['name'] = technique.name - ret['reference'] = technique.wiki - found = True - break - except Exception as e: - logger.error('Exception {} occurred in get_technique_from_mitre()...'.format(e)) - # in case of an incorrect technique ID, it won't be detected so we need to let the parent function know that we failed to find the unknown technique. - # if not found: ret = None - return ret - - -def get_tactic_from_mitre(attack, item): - print(f'Starting get_tactic_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for tactic in attack.enterprise.tactics: - # print('Tactic name: ') - # print(tactic.name) - if tactic.name.lower() == item: - # print(tactic.id) - # print(tactic.name) - # print(tactic.wiki) - ret['id'] = tactic.id - ret['name'] = tactic.name - ret['reference'] = tactic.wiki - return ret - - -def is_tactic(attack, item, logger): - logger.debug('Starting is_tactic()...') - is_tactic_boolean = False - tactic = {} - match_list = re.findall(r'^attack\.\w{5,}.*$', item) - if item.count('.') > 1: match_list = [] - # print(match_list) - if len(match_list) > 0: - logger.debug(match_list) - is_tactic_boolean = True - # print('up') - # input('') - tactic = attack.get_tactic_from_name(item) - # tactic = get_tactic_from_mitre(attack, item.replace('attack.', '').replace('_', ' ')) - # print('down') - # input('') - logger.debug('tactic item {} data:'.format(item)) - logger.debug(tactic) - return is_tactic_boolean, tactic - - -def get_subtechnique_from_mitre(attack, item, logger): - logger.debug(f'Starting get_subtechnique_from_mitre() for item {item}...') - ret = { - 'id': '', - 'name': '', - 'reference': '' - } - for technique in attack.enterprise.techniques: - for subtechnique in technique.subtechniques: - if subtechnique.id.lower() == item: - logger.debug(subtechnique.id) - logger.debug(subtechnique.name) - logger.debug(subtechnique.wiki) - ret['id'] = subtechnique.id - ret['name'] = subtechnique.name - ret['reference'] = subtechnique.wiki - return ret - - -def is_subtechnique(attack, item, logger): - logger.debug('Starting is_subtechnique()...') - is_subtechnique_boolean = False - subtechnique = {} - match_list = re.findall(r'^(attack\.t\d{4,})\.\d+$', item) - # print(match_list) - if len(match_list) > 0: - # print(match_list) - is_subtechnique_boolean = True - subtechnique = attack.get_technique_from_id(item) - # subtechnique = get_subtechnique_from_mitre(attack, item.replace('attack.', ''), logger) - return is_subtechnique_boolean, subtechnique - - -def get_mitre_ttps(attack, yj_rule, logger): - # Sample MITRE TTPs format - # tags: - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - # - attack.defense_evasion - # - attack.t1562 - # - attack.t1078.004 - ret = [] - temp = { - 'framework': 'MITRE ATT&CK', - 'tactic': {}, - 'technique': [] - } - # individual_technique_object = { - # 'id': '', - # 'reference': '', - # 'name': '' - # } - idx = 0 - idx2 = 0 - try: - # Read tactic. - # for item in yj_rule[idx:]: - while(idx < len(yj_rule) - 1): - temp2 = copy.deepcopy(temp) - item = yj_rule[idx] - logger.debug(f'item: {item}\tidx: {idx}') - is_tactic_boolean, tactic = is_tactic(attack, item, logger) - logger.debug(f'is_tactic_boolean: {is_tactic_boolean}') - temp2['tactic'] = tactic - if is_tactic_boolean: - logger.debug('Now inside tactic techniques if/else...') - idx += 1 - logger.debug('Current idx after ++: {}'.format(idx)) - logger.debug('len_uj_rule ++: {}'.format(len(yj_rule))) - if idx <= len(yj_rule) - 1: - # Read techniques below - logger.debug('Started inside for loop...') - for idx2, item2 in enumerate(yj_rule[idx:]): - logger.debug('for loop is_technique part...') - is_technique_boolean, technique = is_technique(attack, item2, logger) - if is_technique_boolean: - temp2['technique'].append(technique) - else: - print('breaking inner for loop idx={} ==> idx2={}...'.format(idx, idx2)) - # idx -= 1 - print('breaking inner for loop after ++ idx={}...'.format(idx, idx2)) - break - idx += 1 - # if not is_technique_boolean: - # Read subtechniques - # logger.debug('for loop not is_technique part...') - # is_subtechnique_boolean, subtechnique = is_subtechnique(attack, item2, logger) - # if not is_subtechnique_boolean: - # logger.debug('for loop not is_subtechnique part...') - # break - # if is_subtechnique_boolean: - # logger.debug('for loop is_subtechnique part...') - # temp2['technique'].append(subtechnique) - # pass - logger.debug('Finished inside for loop...') - # So only non-empty and valid tactics and techniques make it to the final output - if temp2.get('tactic') != {} and temp2.get('technique') != [] and temp2.get('tactic').get('id') != '': - ret.append(temp2) - except Exception as e: - print('Exception {} occurred in get_mitre_ttps()...'.format(e)) - # pprint(temp) - pprint(ret) - return ret - - -def valid_credentials(credentials, logger): - ret = False - creds_exist = username_exist = password_exist = url_exist = False - if credentials and credentials is not None: - creds_exist = True - logger.debug('Credentials key exists...') - logger.debug('Checking further...') - if 'kibana_username' in credentials: - if credentials.get('kibana_username') and credentials.get('kibana_username') != '': - username_exist = True - logger.debug('Kibana_username exists...') - if 'kibana_password' in credentials: - if credentials.get('kibana_password') and credentials.get('kibana_password') != '': - password_exist = True - logger.debug('kibana_password exists...') - if 'kibana_url' in credentials: - if credentials.get('kibana_url') and credentials.get('kibana_url') != '': - url_exist = True - logger.debug('kibana_url exists...') - if creds_exist and username_exist and password_exist and url_exist: - ret = True - logger.info('Existing creds found. Output file shall be uploaded to ELK...') - else: - ret = False - logger.info('No creds found. Output file shall not be uploaded to ELK...') - return ret - - -def handle_response_errors(status_code, message, logger): - if status_code == 400 and 'invalid file extension'.lower() in message.lower(): - logger.error('Use .ndjson as the file extension for output file...') - - -def install_rules(script_dir, credentials, rule_file, logger): - return_status = 0 - # if windows, execute these commands - curl_path = get_slash_set_path(script_dir + '/helpers/curl/curl.exe', logger) - logger.debug('Script Dir: {}'.format(curl_path)) - logger.debug('Rule Output File: {}'.format(rule_file)) - result = result_out = None - query = None - # if windows machine - if os.name == 'nt': - command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X POST \"{}/api/detection_engine/rules/_import?overwrite=true\" -u '{}:{}' -H 'kbn-xsrf: true' -H 'Content-Type: multipart/form-data' --form 'file=@{}'".format(credentials.get('kibana_url'), credentials.get('kibana_username'), credentials.get('kibana_password'), rule_file)) - logger.debug('Command: {}'.format(command)) - logger.info('Windows powershell command shall be executed...') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = json.loads(result.stdout.decode('utf-8')) - result_error = result.stderr.decode('utf-8') - # if error code var is not empty, then set return status to 1 - if result.returncode != 0: return_status = 1 - logger.debug(result_out) - if return_status == 1: - logger.error(result_error) - # if linux machine - else: - command = """curl -X POST "{}/api/detection_engine/rules/_import?overwrite=true" -u '{}:{}' -H 'kbn-xsrf: true' -H 'Content-Type: multipart/form-data' --form "file=@{}" """.format(credentials.get('kibana_url'), credentials.get('kibana_username'), credentials.get('kibana_password'), rule_file) - logger.debug('Command: {}'.format(command)) - logger.info('Linux shell shall be executed...') - process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip().decode('utf-8') - print(proc_stdout) - result_out = json.loads(proc_stdout) - result_error = process.returncode - # if error code var is not empty, then set return status to 1 - if result_error != 0: return_status = 1 - logger.debug(result_out) - logger.info('Import Successful: {}...'.format(result_out.get('success'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('success') != True: - return_status = 1 - logger.info('Count Successfully Imported Rules: {}...'.format(result_out.get('success_count'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('success_count') <= 0: - return_status = 1 - logger.info('Import Errors: {}...'.format(result_out.get('errors'))) - # if elasticsearch output var is not empty, then set return status to 1 - if len(result_out.get('errors')) > 0: - return_status = 1 - logger.info('Response Message: {}...'.format(result_out.get('message'))) - logger.info('Response status code: {}...'.format(result_out.get('status_code'))) - # if elasticsearch output var is not empty, then set return status to 1 - if result_out.get('status_code') != None: - return_status = 1 - handle_response_errors(result_out.get('status_code'), result_out.get('message'), logger) - return return_status, query - - -def rate_based_rule_settings(sigma_config, config, config_t, yj_rule_t, logger): - # directly make changes to config variable and then return it at the end - temp = {} - update_required = False - try: - logger.debug('if else starting...') - logger.debug(f'yj_rule_t: {yj_rule_t}') - logger.debug(f'config_t: {config_t}') - # if yj_rule has threshold fields - # if (not update_required) and yj_rule_t and yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - for i in range(1): - if (not update_required) and yj_rule_t: - # if in the rule, field or value under threshold are empty or null, then rule is not rate based. - if yj_rule_t.get('field') is None or yj_rule_t.get('value') is None: break - if yj_rule_t.get('field') is not None and yj_rule_t.get('value') is not None and type(yj_rule_t.get('value')) == int: - # if threshold is defined in rule - temp = yj_rule_t - update_required = True - logger.debug('rule threshold set...') - # elif (not update_required) and config_t and config_t.get('field') and (not config_t.get('field') == '') and config_t.get('value') and type(config_t.get('value')) == int: - elif (not update_required) and config_t and config_t.get('field') is not None and config_t.get('value') is not None and type(config_t.get('value')) == int: - # elif (not update_required) and config_t and config_t.get('value') and type(config_t.get('value')) == int: - # if threshold is defined in siegma config - temp = config_t - update_required = True - logger.debug('config threshold set...') - else: logger.debug('temp is empty...') - if update_required: - # change field name to ECS format and update rate threshold in config - ecs_field = '' - # handle empty field gracefully - # t = temp.get('field') - # logger.debug(f'temp.field: {t}') - # try to convert threshold field to sigma mapped field. - if temp.get('field') and temp.get('field') != '' and sigma_config.get('fieldmappings').get(temp.get('field')) is not None: ecs_field = sigma_config.get('fieldmappings').get(temp.get('field')) - # if conversion fails, then consider whatever value was in the threshold.field as ecs formatted and move forward with that - else: ecs_field = temp.get('field') - config['threshold'] = { - 'field' : ecs_field, - 'value': temp.get('value') - } - config['type'] = 'threshold' - else: - # if deafult config has threshold in it, then delete threshold key entirely if no changes were made - if config.get('threshold'): del config['threshold'] - except Exception as e: - logger.error(f'Exception {e} occurred in rate_based_rule_settings()...') - logger.info('rate_based_rule_settings() finished successfully...') - return config - - -def get_notes(notes_folder, config_n, yj_rule_n, logger): - ret = '' - file_name = '' - if type(config_n) == str and config_n != "": - config_n = [config_n] - config_n = [get_slash_set_path(i, logger) for i in config_n] - if type(yj_rule_n) == str and yj_rule_n != "": - yj_rule_n = [yj_rule_n] - yj_rule_n = [get_slash_set_path(i, logger) for i in yj_rule_n] - update_required = False - try: - if (not update_required) and yj_rule_n and type(yj_rule_n) == list and len(yj_rule_n) > 0: - file_name = yj_rule_n - update_required = True - logger.debug('File name set from rule...') - elif (not update_required) and config_n and type(config_n) == list and len(config_n) > 0: - file_name = config_n - update_required = True - logger.debug('File name set from config...') - else: logger.debug('File name not set...') - if update_required: - # add forward/back slash to end of folder name - if notes_folder and len(notes_folder) > 0 and notes_folder[-1] != get_slashes(): notes_folder += get_slashes() - # remove forward/back slash from start of file name - if file_name and len(file_name) > 0 and type(file_name) == list: - for i in file_name: - if i[0] == get_slashes(): i = i[1:] - with open(get_slash_set_path(notes_folder + i, logger)) as input_file: - ret += input_file.read() - ret += "\n" - print(ret) - except Exception as e: - logger.error(f'Exception {e} occurred in get_notes()...') - return ret - logger.info(f'get_notes() finished successfully...') - return ret - - -def find_d2_kv_in_d1(d1, d2): - for k2, v2 in d2.items(): - if k2 in d1: - d1[k2] = v2 - return d1 - - -def add_new_items_to_config(shared_config, rule_config, logger): - if 'settings' in rule_config: - shared_config = find_d2_kv_in_d1(shared_config, rule_config.get('settings')) - logger.info('add_new_items_to_config() finished successfully...') - return shared_config - - -def get_enabled_state(id, credentials, script_dir, logger, testing=False): - enabled_state = True - return_status = 0 - - if testing: - logger.warn('Since testing switch is enabled, enablement check will not be performed. Considering status as enabled....') - return enabled_state - - curl_path = get_slash_set_path(script_dir + '/helpers/curl/curl.exe', logger) - # if windows machine - if os.name == 'nt': - # command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X GET {}/api/detection_engine/rules?rule_id={} -u '{}:{}' -H 'kbn-xsrf: true'".format(credentials.get('kibana_url'), "53209551-b8ce-4945-8df4-0d70314f91e7", credentials.get('kibana_username'), credentials.get('kibana_password'))) - command = 'powershell -nop -c \"{} {};\"'.format(curl_path, "-X GET {}/api/detection_engine/rules?rule_id={} -u '{}:{}' -H 'kbn-xsrf: true'".format(credentials.get('kibana_url'), id, credentials.get('kibana_username'), credentials.get('kibana_password'))) - logger.debug('Command: {}'.format(command)) - logger.info('Windows powershell command shall be executed...') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = json.loads(result.stdout.decode('utf-8')) - result_error = result.stderr.decode('utf-8') - # if error code var is not empty, then set return status to 1 - if result.returncode != 0: return_status = 1 - logger.debug(result_out) - if 'enabled' in result_out and result_out.get('enabled') == False: - logger.warn('Rule {} state will be disabled...'.format(id)) - enabled_state = False - if return_status == 1: - logger.error(result_error) - # if linux machine - else: - # command = """curl -X GET "{}/api/detection_engine/rules?rule_id={}" -u '{}:{}' -H 'kbn-xsrf: true'""".format(credentials.get('kibana_url'), "53209551-b8ce-4945-8df4-0d70314f91e7", credentials.get('kibana_username'), credentials.get('kibana_password')) - command = """curl -X GET "{}/api/detection_engine/rules?rule_id={}" -u '{}:{}' -H 'kbn-xsrf: true'""".format(credentials.get('kibana_url'), id, credentials.get('kibana_username'), credentials.get('kibana_password')) - logger.debug('Command: {}'.format(command)) - logger.info('Linux shell shall be executed...') - process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip().decode('utf-8') - print(proc_stdout) - result_out = json.loads(proc_stdout) - result_error = process.returncode - # if error code var is not empty, then set return status to 1 - if result_error != 0: return_status = 1 - logger.debug(result_out) - if 'enabled' in result_out and result_out.get('enabled') == False: - logger.warn('Rule {} state will be disabled...'.format(id)) - enabled_state = False - - return enabled_state - - -def get_elastic_actions_s2(val): - ''' - Sub function of author name that performs several checks before finalizing the elastic actions - ''' - ret = None - if isinstance(val, list): - ret = val - elif val is None: - ret = val - else: ret = list(val) - return ret - - -def get_elastic_actions(yj_rule, config, logger): - ret = None - already_done = False - if not already_done: - ret = get_elastic_actions_s2(yj_rule) - if ret is not None: - already_done = True - logger.debug(f'Elastic actions {ret} set from rule...') - if not already_done: - if not config is None: - ret = get_elastic_actions_s2(config) - if ret is not None: - already_done = True - logger.debug(f'Elastic actions {ret} set from config...') - else: ret = [] - else: ret = [] - return ret - - -def create_rule(siegma_config, notes_folder, config, sigma_config, credentials, query, yj_rule, attack, output, script_dir, logger, testing=False): - logger.info('Starting create_rule()...') - rule_file = None - try: - logger.debug(config) - # # set siegma config as per config defined in rule - config = add_new_items_to_config(config, yj_rule.get('siegma').get('config'), logger) if 'siegma' in yj_rule and 'config' in yj_rule.get('siegma') else config - # set query - config['query'] = query - # set author name - config['author'] = get_author_name(yj_rule.get('author'), config.get('author'), logger) - # name set - config['name'] = yj_rule.get('title') - # description set - config['description'] = yj_rule.get('description') - # falsepositives set - if yj_rule.get('falsepositives'): config['false_positives'] = yj_rule.get('falsepositives') - # references set - if yj_rule.get('references'): config['references'] = yj_rule.get('references') - # severity set - config['severity'] = yj_rule.get('level') - # risk score set - config['risk_score'] = get_risk_score(yj_rule.get('level')) if 'score' not in yj_rule else yj_rule.get('score') - # tags set - config['tags'] = yj_rule.get('siemtags') if yj_rule and 'siemtags' in yj_rule and type(yj_rule.get('siemtags')) == list else [] - # actions set - config['actions'] = get_elastic_actions(yj_rule.get('elastic_actions'), config.get('actions'), logger) - # MITRE settings - if yj_rule.get('tags'): config['threat'] = get_mitre_ttps(attack, yj_rule.get('tags'), logger) - # rule ID set - config['rule_id'] = config['id'] = yj_rule.get('id') - # time set - if 'timeframe' in yj_rule.get('detection'): config['from'] = 'now-' + yj_rule.get('detection').get('timeframe') - # rate_based_rule - config = rate_based_rule_settings(sigma_config, config, config.get('threshold'), yj_rule.get('threshold'), logger) - # investigation notes - config['note'] = get_notes(notes_folder, config.get('note'), yj_rule.get('note'), logger) - # get current enabled state of the rule from the SIEM - config['enabled'] = get_enabled_state(yj_rule.get('id'), credentials, script_dir, logger, testing=testing) - ############# - logger.info('Final config:') - logger.info(config) - rule_file = dump_to_file(logger, config, output=output) - except Exception as e: - logger.error(f'Exception {e} occurred in create_rule()...') - return rule_file diff --git a/siegma.py b/siegma.py index 6519134..b4becb8 100644 --- a/siegma.py +++ b/siegma.py @@ -1,403 +1,106 @@ -import os -import sys -import json -import copy -import yaml +from tools.LogHandler import LogHandler, logging_levels +from Exceptions import CreateRuleByApiError, RuleSintaxeError, FileExtensionError +from tools.FileTools import FileTools +from tools.DirectoryTools import DirectoryTools +from tools.SigmaUtils import SigmaRule, get_sigma_configuration +from tools.Backends import Backends import argparse -import subprocess -import collections -from pprint import pprint - -from yaml.loader import SafeLoader -from helpers import mitre_attack -from rule_file_creator_scripts import es_qs, ala_rule, es_eql -from helpers.utils import setup_logger, config_file_to_dict, get_slash_set_path, get_slashes - - -# global vars -############# -logger = None -args = None -slash = '/' -attack = mitre_attack.MitreAttack() -############# - - -def empty_output_file(output='.output.ndjson'): - with open(output, "w") as outfile: pass - logger.info('Output file {} has been created and emptied...'.format(output)) - return output - - -def setup_args(): - parser = argparse.ArgumentParser(os.path.basename(__file__)) - parser.add_argument('-c', '--config', metavar='', type=str, default='config/.config.json', help='Config file path. Eg: /path/to/config.json') - parser.add_argument('-r', '--rule', metavar=' / ', type=str, help='Rule file / folder path. This should be either the absolute path from root folder or should be relative to sigma, NOT siegma. Eg: /path/to/rule/file.yml or /path/to/rules/folder.') - parser.add_argument('-s', '--sigma', metavar='', type=str, default='', help='Sigma repository path. Eg: /path/to/sigma.') - parser.add_argument('-sc', '--sigma_config', metavar='', type=str, default='', help='Sigma config file path. Eg: /path/to/sigma/tools/config/ecs-cloudtrail.yml.') - parser.add_argument('-o', '--output', metavar='', type=str, default='.output', help='Output file path. Eg: /path/to/output_file.') - parser.add_argument('-co', '--config_override', metavar='', type=str, default='', help='Values that can be used to override config. Eg: settings.rule_id="some_id",settings.custom_field="custom_value",custom_field="custom_value",settings.author=none,credentials.kibana_url="www.example.com",sigma_query_format="es-qs".') - parser.add_argument('-t', '--testing', dest='testing', action='store_true', help='Switch for testing. Default "False". If testing, output file will be created but the rule file will not be installed on SIEM. Eg: -t or --testing.') - parser.add_argument('-sep', '--sigma_extra_parameters', dest='sigma_extra_parameters', action='store_true', help='Switch for enabling backend options feature of sigma. If this switch is passed here, sigma_params values from config file will be read and used by the script. Default "False". Eg: -sep or --sigma_extra_parameters.') - parser.add_argument('-v', '--verbosity', metavar='', type=str, default='DEBUG', help='Execution verbosity level. Eg: SUCCESS|WARN|INFO|DEBUG.') - logger.info('Arguments parsed successfully...') - return parser.parse_args() - - -def force_exit(msg, exit=1): - if exit == 1: - logger.error(msg) - else: - logger.info(msg) - sys.exit(exit) - - -def initialize_g_vars(): - global logger, args - logger = setup_logger() - args = setup_args() - # get siem config - args.config = config_file_to_dict(filename=args.config) - logger.setLevel(args.verbosity) - logger.debug(args.config) - # get sigma folder path - args.sigma = args.sigma if not (args.sigma is None or args.sigma == '') else force_exit('Sigma folder path is required...', exit=1) - logger.debug(args.sigma) - # get sigma config file path - args.sigma_config = args.sigma_config if not (args.sigma_config is None or args.sigma_config == '') else force_exit('Sigma Config is required...', exit=1) - logger.debug(args.sigma_config) - args.sigma = args.sigma.rstrip('\\') - args.sigma = args.sigma.rstrip('/') - args.rule = args.rule.rstrip('\\') - args.rule = args.rule.rstrip('/') - logger.info('initialize_g_vars() finished successfully...') - - -def get_sigma_config_from_config(config): - return config.get('sigma_config') - - -def get_sigma_path_from_config(config): - return config.get('path_to_sigma_folder') - - -def get_sigma_query_conversion_result(sigma, sigma_config, sigma_query_format, rule, sigma_extra_parameters): - # if windows, execute these commands - result = query = command = None - return_status = 0 - try: - # if windows machine - if os.name == 'nt': - logger.info('Windows powershell command shall be executed...') - command = 'powershell -nop -c "cd {0};pipenv run python tools\\sigmac -c {1} -t {2} {4} {3};"'.format(sigma, sigma_config, sigma_query_format, rule, sigma_extra_parameters) - # command = 'powershell -nop -c "pipenv run python {0}\\tools\\sigmac -c {1} -t {2} {4} {3};"'.format(sigma, sigma_config, sigma_query_format, rule, sigma_extra_parameters) - logger.debug(command) - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = result.stdout.decode('utf-8') - result_error = result.stderr.decode('utf-8') - # if error code var is not empty, then set return status to 1 - if result.returncode != 0: return_status = 1 - query = result_out.splitlines()[0] - logger.debug(query) - if return_status == 1: - logger.error(result_error) - # if linux machine - else: - logger.info('Linux shell shall be executed...') - # command = "pipenv run python {0}/tools/sigmac -c {1} -t {2} {4} {3};".format(sigma, sigma_config, sigma_query_format, rule, sigma_extra_parameters) - command = "cd {0};pipenv run python tools/sigmac -c {1} -t {2} {4} {3};".format(sigma, sigma_config, sigma_query_format, rule, sigma_extra_parameters) - logger.debug('Command:') - logger.debug(command) - process = subprocess.Popen(get_slash_set_path(command, logger), stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip().decode('utf-8') - result_error = process.returncode - # if error code var is not empty, then set return status to 1 - if result_error != 0: return_status = 1 - logger.info(proc_stdout) - query = proc_stdout.splitlines()[-1] - logger.info(query) - except Exception as e: - logger.error('Exception {} occurred in get_sigma_query_conversion_result()...'.format(e)) - return_status = 1 - return return_status, query - - -def load_yaml_rule_into_json(yj_rule): - with open(yj_rule) as f: - yj_rule = json.loads(json.dumps(yaml.load(f, Loader=SafeLoader))) - logger.debug(yj_rule) - return yj_rule - - -def create_rule_file_for_siem(siegma_config, notes_folder, sigma_query_format, sigma_config, config, credentials, query, yj_rule, output, testing=False): - rule_file = None - config_copy = copy.deepcopy(config) - yj_rule = load_yaml_rule_into_json(yj_rule) - sigma_config = load_yaml_rule_into_json(sigma_config) - if sigma_query_format in ['es-qs']: - rule_file = es_qs.create_rule(siegma_config, notes_folder, config_copy, sigma_config, credentials, query, yj_rule, attack, output, os.path.dirname(os.path.realpath(__file__)), logger, testing=testing) - elif sigma_query_format in ['ala-rule']: - rule_file = ala_rule.create_rule(siegma_config, notes_folder, config_copy, sigma_config, credentials, json.loads(query), yj_rule, attack, output, os.path.dirname(os.path.realpath(__file__)), logger, testing=testing) - elif sigma_query_format in ['es-eql']: - rule_file = es_eql.create_rule(siegma_config, notes_folder, config_copy, sigma_config, credentials, query, yj_rule, attack, output, os.path.dirname(os.path.realpath(__file__)), logger, testing=testing) - else: pass - return rule_file - - -def getListOfYMLFiles(dirName): - # create a list of file and sub directories - # names in the given directory - listOfFile = os.listdir(dirName) - allFiles = list() - # Iterate over all the entries - for entry in listOfFile: - # Create full path - fullPath = os.path.join(dirName, entry) - # If entry is a directory then get the list of files in this directory - if os.path.isdir(fullPath): - allFiles = allFiles + getListOfYMLFiles(fullPath) - else: - if fullPath.lower().endswith('.yml'): - allFiles.append(fullPath) - return allFiles +import os +from converters import all_converters +import traceback +def setup_args() -> argparse.Namespace: + """ + Setup the command line arguments + """ -def get_all_rule_files(rule_path): - ret = [] - is_dir = os.path.isdir(rule_path) - if is_dir: - ret = getListOfYMLFiles(rule_path) - else: - ret.append(rule_path) - logger.info('Printing {} rules identified: '.format(len(ret))) - logger.info(ret) - logger.info('get_all_rule_files() finished successfully...') - return ret + parser = argparse.ArgumentParser(os.path.basename(__file__)) + parser.add_argument("-c", "--config", required=True, dest="config", help="Config file path. Eg: /path/to/config.json") + parser.add_argument("-b", "--backend", choices=Backends._member_names_, dest="backend", default="elastic", help=f"SIEM backend to perform the conversion. Eg: {'|'.join(Backends._member_names_)}") + parser.add_argument("--api", dest="api", action="store_true", default=False, help="Pass this argument to create a rule in the SIEM via the api.") + parser.add_argument('-p', '--path', type=str, required=True, help='Rule file / folder path. This should be either the absolute path from root folder or should be relative to sigma, NOT siegma. Eg: /path/to/rule/file.yml or /path/to/rules/folder.') + parser.add_argument("-sc", "--sigma_config", dest="sigma_config_file", type=str, help="Sigma config file path. Eg: /path/to/sigma/tools/config/ecs-cloudtrail.yml.", required=True) + parser.add_argument("-o", "--output", dest="output_file", type=str, help="Output file path. Eg: /path/to/output_file.", default=".output.ndjson") + parser.add_argument("-v", "--verbosity", dest="verbosity_level", choices=logging_levels, type=str, default="INFO", help=f"Execution verbosity level. Eg: {'|'.join(logging_levels)}") + parser.add_argument("-lf", "--log_file", dest="log_file", type=str, default=".output.log", help="File to save the logs infos.") + return parser.parse_args() -def get_sigma_extra_parameters(sigma_extra_parameters, sigma_params, yj_rule): - sigma_extra_params = '' - already_done = False - try: - logger.debug('Checking sigma params from rule file...') - if (not already_done) and yj_rule is not None and yj_rule != '' and type(yj_rule) == dict and len(yj_rule) > 0 and 'sigma' in yj_rule: - logger.debug('sigma params from rule file will be used...') - already_done = True - for key, value in yj_rule.get('sigma').items(): - logger.debug(value) - if type(value) == dict: - for k2, v2 in value.items(): - if type(v2) == list: - sigma_extra_params += f'--{key} {k2}=' + ','.join(v2) + ' ' - if type(v2) == str: - if v2 == "": - v2 = "\"\"" - logger.debug("sep empty string...") - sigma_extra_params += f'--{key} {k2}={v2} ' - logger.debug("sep string...") - if type(v2) == bool: - sigma_extra_params += f'--{key} {k2}={v2} ' - logger.debug('Checking sigma_params from config...') - if (not already_done) and type(sigma_params) == dict and len(sigma_params) > 0: - logger.debug('sigma_params from config will be used...') - for key, value in sigma_params.items(): - if type(value) == list: - # ignore sigma params with empty lists - if value == [""]: continue - logger.debug('list type params found for key {}...'.format(key)) - for item in value: - sigma_extra_params += '{} {} '.format(key, item) - elif type(value) == str: - logger.debug('str type params found for key {}...'.format(key)) - sigma_extra_params += '{} {} '.format(key, value) - else: logger.error('Unhandled type params found for key {} and type {}...'.format(key, type(value))) - else: logger.warning('sigma_params are empty in config...') - except Exception as e: - logger.error('Exception {} occurred in get_sigma_extra_parameters()...'.format(e)) - logger.info('Final sigma_extra_params: {}'.format(sigma_extra_params)) - return sigma_extra_params +def main(): + # Setup arguments + args = setup_args() -def install_rule_files_on_siem(sigma_query_format, credentials, out_file_name, rule): - return_status = 0 - if sigma_query_format in ['es-qs']: - if es_qs.valid_credentials(credentials, logger): - return_status, query = es_qs.install_rules(os.path.dirname(os.path.realpath(__file__)), credentials, out_file_name, logger) - else: - return_status = 1 - elif sigma_query_format in ['ala-rule']: - if ala_rule.valid_credentials(credentials, logger): - return_status = ala_rule.install_rules(os.path.dirname(os.path.realpath(__file__)), credentials, out_file_name, load_yaml_rule_into_json(rule), logger) - else: - return_status = 1 - elif sigma_query_format in ['es-eql']: - if es_eql.valid_credentials(credentials, logger): - return_status, query = es_eql.install_rules(os.path.dirname(os.path.realpath(__file__)), credentials, out_file_name, logger) - else: - return_status = 1 - return return_status + # Create logger instance + logger = LogHandler(log_file_name=args.log_file, level=args.verbosity_level).setup_logger() + logger.debug("Loading the configuration files.") + siem_config = FileTools.load_json_file(file_path=args.config) -def get_dict_from_dot_separated_string(ret, len_dlist, dlist, value): - try: - logger.debug(f'len(dlist) - len_dlist: {len(dlist) - len_dlist}') - logger.debug('start ret:') - logger.debug(ret) - if len_dlist == 1: - ret[dlist[len(dlist) - len_dlist]] = value - elif len_dlist > 0: - ret[dlist[len(dlist) - len_dlist]] = {} - logger.debug('elif ret:') - logger.debug(ret) - ret[dlist[len(dlist) - len_dlist]] = get_dict_from_dot_separated_string(ret[dlist[len(dlist) - len_dlist]], len_dlist - 1, dlist, value) - else: pass - except Exception as e: - logger.error(f'Exception {e} occurred in get_dict_from_dot_separated_string()...') - logger.debug('end ret:') - logger.debug(ret) - return ret + sigma_config = get_sigma_configuration(args.sigma_config_file) + logger.debug("Configuration files has been loaded.") + logger.debug("Loading backend rule convertor.") + backend = Backends.get_backend(args.backend) + logger.info(f"{backend.name} backend has been loaded.") -def parse_config_override(config_override): - ret = {} - for pairs in config_override.split(','): - p_split = pairs.split('=') - logger.debug('p_split: {}'.format(p_split)) - k = p_split[0] - v = p_split[1] - k_split = k.split('.') - ret = update_dict(ret, get_dict_from_dot_separated_string({}, len(k_split), k_split, v)) - logger.debug('String to dict:') - logger.debug(ret) - logger.info('String to dict:') - logger.info(ret) - return ret + logger.debug(f"Loading {backend.name} convertor") + rule_convertor = all_converters[backend.name](siem_config) + if not FileTools.check_if_is_a_file(args.path): + logger.info("Loading rules files") + files = DirectoryTools.get_all_files_basename(args.path) + dir_path = args.path -def update_dict(orig_dict, new_dict): - for key, val in new_dict.items(): - if isinstance(val, collections.Mapping): - tmp = update_dict(orig_dict.get(key, { }), val) - orig_dict[key] = tmp - elif isinstance(val, list): - orig_dict[key] = (orig_dict.get(key, []) + val) - else: - orig_dict[key] = new_dict[key] - return orig_dict + else: + logger.info("Loading rule file") + files = [FileTools.get_file_basename(args.path)] + dir_path = FileTools.get_dirname(args.path) + for file in files: + try: + file_path = FileTools.file_path(dir_path, file) + logger.debug(f"Analyzing rule file {file_path}") -def update_config(config_override, config): - ret = config - try: - if config_override == "": return ret - dict_config_override = parse_config_override(config_override) - ret = update_dict(ret, dict_config_override) - logger.info('Updated config:') - logger.info(ret) - except Exception as e: - logger.error('Exception {} occurred in update_config()...'.format(e)) - return ret + logger.debug("Creating SigmaRule object.") + sigma_rule_object = SigmaRule(file_path=file_path, sigma_config=sigma_config) + logger.debug("Getting rule content.") + rule_content = sigma_rule_object.get_yml_file_content() -def check_rules_compliance(rules, return_status): - rules_are_compliant = True - result_out = None - result_error = None - result = None - logger.info('Following command shall be executed...') - # command = 'pipenv run python {2}{0}helpers{0}check_if_compliant.py -p {2}{1}'.format(get_slashes(), rules, os.path.abspath(os.getcwd())) - # command = 'pipenv run python helpers{0}check_if_compliant.py -p {1}'.format(get_slashes(), rules, os.path.abspath(os.getcwd())) - # if windows machine - if os.name == 'nt': - command = 'pipenv run python helpers{0}check_if_compliant.py -p {1}'.format(get_slashes(), rules, os.path.abspath(os.getcwd())) - logger.debug('Command:') - logger.debug(command) - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - result_out = result.stdout.decode('utf-8') - result_error = result.stderr.decode('utf-8') - # for i in result_out.splitlines(): - # logger.error(i) - # if error code var is not empty, then set return status to 1 - # if linux machine - else: - logger.info('Linux shell shall be executed...') - command = 'pipenv run python helpers{0}check_if_compliant.py -p {1}'.format(get_slashes(), rules, os.path.abspath(os.getcwd())) - logger.debug('Command:') - logger.debug(command) - result = subprocess.Popen(get_slash_set_path(command, logger), stdout=subprocess.PIPE, shell=True) - result_out = result.communicate()[0].strip().decode('utf-8') - # result_error = process.returncode - # if error code var is not empty, then set return status to 1 - # if result_error != 0: return_status = 1 - # print(proc_stdout) - # query = proc_stdout.splitlines()[-1] - # logger.info(query) - - for i in result_out.splitlines(): - logger.error(i) - if result.returncode != 0: - rules_are_compliant = False - return_status = 1 - return return_status, rules_are_compliant + logger.info(f"Find the rule {rule_content[0].title}") + logger.debug("Checking rule sintaxe.") + sigma_rule_object.check_rule_sintaxe(rule_content) -def quit_script_with_error_if_failed(status): - # if the command did not return status 0, consider it to be ended in error and therefore, exit the script with bash return code of 1 - if status != 0: - logger.error('Ending script with error code: {}'.format(status)) - sys.exit(1) + logger.debug("Getting rule query.") + query = rule_convertor.convert(rule_content, backend.value) + logger.debug("Creating rule.") + elastic_rule = rule_convertor.create_rule(rule_content[0], query) -def main(): - try: - initialize_g_vars() - empty_output_file(output=args.output) - out_file_name = '' if args.output is None else args.output - logger.info('Output file: {}...'.format(out_file_name)) - return_status = 0 + logger.debug("Saving rule to a file.") + rule_convertor.write_rule(args.output_file, elastic_rule) + logger.info(f"The rule has been saved in the {args.output_file} file.") - # rule compliance check - logger.info('Checking if rule is in a siegma convertible format...') - return_status, rules_are_compliant = check_rules_compliance(args.rule, return_status) - if return_status != 0 or not rules_are_compliant: - logger.error('Some of the rules are not in the format that is considered convertible by SIEGMA. Exting with error...') - quit_script_with_error_if_failed(return_status) - ######################## + if args.api: + logger.info(f"Creating rule in {backend.name} via API.") - for idx, rule in enumerate(get_all_rule_files(args.rule)): - logger.debug('rule iteration {}...'.format(idx)) - return_status, query = get_sigma_query_conversion_result(args.sigma, args.sigma_config, args.config.get('sigma_query_format'), rule, get_sigma_extra_parameters(args.sigma_extra_parameters, args.config.get('sigma_params'), load_yaml_rule_into_json(rule))) - if args.config_override != "": - # if config override switch has values then update config - args.config = update_config(args.config_override, args.config) - out_file_name = create_rule_file_for_siem(args.config, args.config.get('notes_folder'), args.config.get('sigma_query_format'), args.sigma_config, args.config.get('settings'), args.config.get('credentials'), query, rule, args.output, testing=args.testing) - logger.info('Output file name: {}...'.format(out_file_name)) - quit_script_with_error_if_failed(return_status) - # backends that only support single rule installation at a time - if (not args.testing) and (args.config.get('sigma_query_format') == 'ala-rule'): - return_status = install_rule_files_on_siem(args.config.get('sigma_query_format'), args.config.get('credentials'), out_file_name, rule) - quit_script_with_error_if_failed(return_status) - else: - logger.info('No rules installed on SIEM since Testing switch is enabled...') - quit_script_with_error_if_failed(return_status) - # ignore code section for backend targets that only support single rule installation at a time - if (not ((args.config.get('sigma_query_format') == 'ala-rule'))): - # backends that support bulk/multiple rules installation at the same time - if (not args.testing) and (args.config.get('sigma_query_format') == 'es-qs'): - return_status = install_rule_files_on_siem(args.config.get('sigma_query_format'), args.config.get('credentials'), out_file_name, '') - quit_script_with_error_if_failed(return_status) - elif (not args.testing) and (args.config.get('sigma_query_format') == 'es-eql'): - return_status = install_rule_files_on_siem(args.config.get('sigma_query_format'), args.config.get('credentials'), out_file_name, '') - quit_script_with_error_if_failed(return_status) - else: - logger.info('No rules installed on SIEM since Testing switch is enabled...') - quit_script_with_error_if_failed(return_status) - except Exception as e: - logger.error('Exception {} occurred in main of file {}...'.format(e, os.path.basename(__file__))) - quit_script_with_error_if_failed(1) + if rule_convertor.create_rule_by_api( + rule=elastic_rule, + siem_url=siem_config["credentials"]["siem_url"], + username=siem_config["credentials"]["username"], + _passwd=siem_config["credentials"]["password"], + apikey=siem_config["credentials"]["apikey"] + ): + logger.info(f"The rule {rule_content[0].title} has been created.") + except Exception as e: + logger.error(f"Error during the conversion process of the rule {rule_content[0].title}") + logger.error(e) + errors += 1 + if args.verbosity_level == "DEBUG": + traceback.print_exc() -# main flow of the program -########################## -main() -########################## \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/tools/Backends.py b/tools/Backends.py new file mode 100644 index 0000000..6e17b5f --- /dev/null +++ b/tools/Backends.py @@ -0,0 +1,22 @@ +from sigma.backends.elasticsearch import LuceneBackend +from enum import Enum + +class Backends(Enum): + """ + Enun with all available backends + """ + elastic = LuceneBackend + + @classmethod + def get_backend(cls, backend_name: str) -> callable: + """ + Use this method to get a backend function + + Args: + backend_name (str): Pass the name of the backend + + Returns: + callable: It returns a backend function + """ + + return cls[backend_name] diff --git a/tools/DirectoryTools.py b/tools/DirectoryTools.py new file mode 100644 index 0000000..034d73f --- /dev/null +++ b/tools/DirectoryTools.py @@ -0,0 +1,79 @@ +import os + +class DirectoryTools: + """ + Some common functions to manipulate Directories. + """ + + @staticmethod + def get_file_extension(file_name: str) -> str: + """ + Get the file extension. + + Args: + file_name (str): File name. + + Returns: + str: Return the type of the file. + """ + + extension = os.path.splitext(file_name)[1] + return extension + + @staticmethod + def get_files_by_extension(folder_path: str, get_sub_directories: bool = True, extension: str = ".yml") -> list[str]: + """ + Get files by a determined extension. + + Args: + folder_path (str): Directory to look for files. + get_sub_directories (bool, optional): Pass True to collect all the file of the sub directories. Defaults to True. + extension (str, optional): File extesion that you looking for . Defaults to ".yml". + + Returns: + list[str]: Return all the files. + """ + + files: list[str] = [] + + for folder_obj in DirectoryTools.get_files(folder_path): + current_obj = os.path.join(folder_path, folder_obj) + + if os.path.isfile(current_obj) and DirectoryTools.get_file_extension(folder_obj) == extension: + files.append(current_obj) + + elif get_sub_directories: + DirectoryTools.get_folders_files(current_obj) + + return files + + @staticmethod + def get_all_files_basename(folder_path: str) -> list[str]: + """ + Get all files basename of the given folder. + + Args: + folder_path (str): Folder path. + + Returns: + list[str]: All files basename of the given folder. + """ + + return os.listdir(folder_path) + + @staticmethod + def it_is_a_directory(path: str) -> bool: + """ + Check if the given path is a valid direcotry. + + Args: + path (str): Directory path. + + Returns: + bool: Returns True if the given path is a valid directory. + """ + + if os.path.isdir(path): + return True + + return False diff --git a/tools/FileTools.py b/tools/FileTools.py new file mode 100644 index 0000000..4e2069a --- /dev/null +++ b/tools/FileTools.py @@ -0,0 +1,97 @@ +import os +import json + + +class FileTools: + """ + Some common functions to manipulate files. + """ + + @staticmethod + def get_file_basename(path: str) -> str: + """ + Get the basename of a file. + + Args: + path (str): Pass the path of the file. + + Returns: + str: Filename of the path given as argument. + """ + + return os.path.basename(path) + + @staticmethod + def file_path(dir_path: str, file_path: str) -> str: + """ + Join the directory path with the file path. + + Args: + dir_path (str): Directory path. + file_path (str): File path. + + Returns: + str: File path. + """ + + return os.path.join(dir_path, file_path) + + @staticmethod + def get_dirname(path: str) -> str: + """ + Get the directory of a file, pass the full path. + + Args: + path (str): File path. + + Returns: + str: Directory path. + """ + + return os.path.dirname(path) + + @staticmethod + def load_json_file(file_path: str) -> dict[str, any]: + """ + Load a json file. + + Args: + file_path (str): File path to be loaded. + + Returns: + dict[str, any]: File content. + """ + + with open(file_path, "r") as json_file: + config_dict = json.load(json_file) + + return config_dict + + @staticmethod + def get_file_extension_name(file_path: str) -> str: + """ + Collect the file extensions name. + + Args: + file_path (str): File path. + + Returns: + str: Extension name. + """ + + return os.path.splitext(file_path)[1] + + @staticmethod + def check_if_is_a_file(path: str) -> bool: + """ + Check if a path is a valid file. + + Args: + path (str): Full path. + + Returns: + bool: Return True if it is a valid path. + """ + + absolute_path = os.path.abspath(path) + return os.path.isfile(absolute_path) diff --git a/tools/LogHandler.py b/tools/LogHandler.py new file mode 100644 index 0000000..4d8479b --- /dev/null +++ b/tools/LogHandler.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass +import logging +import colorlog + +logging_levels = ["INFO", "WARN", "DEBUG", "ERROR"] + +@dataclass(kw_only=True) +class LogHandler: + """ + Class responsible for creating logger handlers + + Args: + log_file_name (str): name of the logger out file + console_log_fmt (str): Console logger format (default: %(log_color)s[%(asctime)s] - %(levelname)s - %(message)s) + file_log_fmt (str): File logger format (default: [%(asctime)s] - %(levelname)s - %(message)s) + datetimefmt (str): Datetime logger format (default: %Y-%m-%d %H:%M:%S) + level (str): Logger level (default: DEBUG) + """ + + log_file_name: str + console_log_fmt: str = "%(log_color)s[%(asctime)s] - %(levelname)s - %(message)s" + file_log_fmt: str = "[%(asctime)s] - %(levelname)s - %(message)s" + datetimefmt: str = "%Y-%m-%d %H:%M:%S" + level: str = "DEBUG" + + def setup_logger(self) -> logging.RootLogger: + """ + Setup the logging module. + + Returns: + logging.RootLogger: Logger object + """ + + logger = logging.getLogger() + logger.setLevel(self.level) + + file_handler = self.create_file_handler() + console_handler = self.create_console_handler() + + logger.addHandler(console_handler) + logger.addHandler(file_handler) + + return logger + + def create_console_handler(self) -> logging.StreamHandler: + """ + Create a console handler. + + Returns: + logging.StreamHandler: Console handler + """ + + console_handler = logging.StreamHandler() + console_handler.setFormatter(self.colored_formatter()) + + return console_handler + + def create_file_handler(self) -> logging.FileHandler: + """ + Create a console handler. + + Returns: + logging.FileHandler: File handler + """ + + file_handler = logging.FileHandler(self.log_file_name) + file_handler.setFormatter(logging.Formatter(self.file_log_fmt, datefmt=self.datetimefmt)) + + return file_handler + + def colored_formatter(self) -> colorlog.formatter.ColoredFormatter: + """ + Create a colored formatter for logging console messages. + + Returns: + colorlog.formatter.ColoredFormatter: Colorlog object + """ + + return colorlog.ColoredFormatter( + self.console_log_fmt, + datefmt=self.datetimefmt + ) diff --git a/tools/MitreAttack.py b/tools/MitreAttack.py new file mode 100644 index 0000000..88f42fe --- /dev/null +++ b/tools/MitreAttack.py @@ -0,0 +1,34 @@ +from bs4 import BeautifulSoup +import requests + +def get_techinique_infos(techinique_id: str) -> dict[str, str]: + + techinique_id = techinique_id.upper() + techinique_url = f"https://attack.mitre.org/techniques/{techinique_id.replace('.', '/')}" + + resp = requests.get(techinique_url) + + if resp.status_code != 200: + return None + + soup = BeautifulSoup(resp.content, "html.parser") + title = soup.find("h1", {"class": ""}).text + + title = " ".join(title.replace("\n", "").split()) + + tactics = [] + for tactic in soup.find("div", {"id": "card-tactics"}).find_all("a"): + tactics.append( + { + "name": tactic.text, + "reference": f'https://attack.mitre.org/tactics{tactic["href"]}', + "id": tactic["href"].split("/")[-1] + } + ) + + return { + "id": techinique_id, + "name": title, + "reference": techinique_url, + "tactics": tactics + } diff --git a/tools/SigmaUtils.py b/tools/SigmaUtils.py new file mode 100644 index 0000000..5a90040 --- /dev/null +++ b/tools/SigmaUtils.py @@ -0,0 +1,69 @@ +from sigma.validators.core.condition import AllOfThemConditionValidator +from dataclasses import dataclass +from sigma.collection import SigmaCollection +from sigma.validation import SigmaValidator +from sigma.configuration import SigmaConfiguration +from Exceptions import RuleSintaxeError + + +def get_sigma_configuration(sigma_config_file_path : str) -> SigmaConfiguration: + """ + Use this function to load the sigma configuration. + + Args: + sigma_config_file_path (str): Pass the path to the configuration file. + + Returns: + SigmaConfiguration: Returns the sigma configuration object. + """ + + with open(sigma_config_file_path, 'r') as config_file: + sigma_configs = SigmaConfiguration(configyaml=config_file) + + return sigma_configs + +@dataclass(kw_only=True) +class SigmaRule: + """ + Class responsible for performing actions on Sigma rules. + + Args: + file_path (str): Path of the sigma rule. + sigma_config (SigmaConfiguration): SigmaConfiguration object, use tools.SigmaUtils.get_sigma_configuration to generate it. + + """ + + file_path: str + sigma_config: SigmaConfiguration + + def get_yml_file_content(self) -> SigmaCollection: + """ + Get the contents of a yml file and return it as SigmaCollection. + + Returns: + SigmaCollection: The contents of a yml file. + """ + + with open(self.file_path, "r") as file: + rule = SigmaCollection.from_yaml(file) + + return rule + + def check_rule_sintaxe(self, rule: SigmaCollection) -> None: + """ + Check if the rule sintaxe is satisfied. + + Args: + rule (SigmaCollection): Loaded sigma rule object. + + Raises: + RuleSintaxeError: Rule sintaxe error exception. + """ + + rule_validator = SigmaValidator(validators={AllOfThemConditionValidator}) + + issues = rule_validator.validate_rules(rule) + + if len(issues) > 0: + raise RuleSintaxeError(f"Issues in the rule sintaxe. Errors: {issues}") + From e8a4c2edc098b125d63f69f5a73c2f037e736b71 Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Tue, 9 May 2023 08:55:58 -0300 Subject: [PATCH 2/9] Update --- Exceptions.py | 2 + Pipfile | 17 -- Pipfile.lock | 215 ------------------ README.md | 37 +-- .../BackendBase.py | 4 +- {tools => backends}/Backends.py | 0 {converters => backends}/Elastic.py | 13 +- {converters => backends}/__init__.py | 8 +- config/README.md | 4 +- config/elastic/README.md | 64 +----- config/elastic/elastic-siem.json | 12 - development-guide.md | 25 -- docs/README.md | 12 + docs/arguments.md | 79 +++++++ docs/automatic-upload.md | 3 + docs/backend.md | 91 ++++++++ docs/config.md | 3 + docs/development-guide.md | 21 ++ docs/output.md | 4 + docs/tools.md | 3 + output/output.md | 4 + siegma.py | 14 +- tools/LogHandler.py | 2 +- 23 files changed, 268 insertions(+), 369 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock rename converters/ConvertorsBase.py => backends/BackendBase.py (98%) rename {tools => backends}/Backends.py (100%) rename {converters => backends}/Elastic.py (92%) rename {converters => backends}/__init__.py (67%) delete mode 100644 development-guide.md create mode 100644 docs/README.md create mode 100644 docs/arguments.md create mode 100644 docs/automatic-upload.md create mode 100644 docs/backend.md create mode 100644 docs/config.md create mode 100644 docs/development-guide.md create mode 100644 docs/output.md create mode 100644 docs/tools.md create mode 100644 output/output.md diff --git a/Exceptions.py b/Exceptions.py index 5299b78..5de69cc 100644 --- a/Exceptions.py +++ b/Exceptions.py @@ -1,3 +1,5 @@ +# To get more information about the errors, user DEBUG verbosity level. + class RuleSintaxeError(Exception): """ Error in the RuleSintaxe. diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d755e26..0000000 --- a/Pipfile +++ /dev/null @@ -1,17 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -pendulum = "*" -pyfiglet = "*" -requests = "*" -colorlog = "*" -Pillow = "*" -PyYAML = "*" - -[dev-packages] - -[requires] -python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 24f5881..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,215 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "9760acf266d364adc15f4bf168c3bc08b868ee531bfe6afcf2c4d2b8a33d781d" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" - ], - "version": "==2021.5.30" - }, - "charset-normalizer": { - "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" - ], - "markers": "python_version >= '3'", - "version": "==2.0.4" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.4" - }, - "colorlog": { - "hashes": [ - "sha256:4e6be13d9169254e2ded6526a6a4a1abb8ac564f2fa65b310a98e4ca5bea2c04", - "sha256:f17c013a06962b02f4449ee07cfdbe6b287df29efc2c9a1515b4a376f4e588ea" - ], - "index": "pypi", - "version": "==5.0.1" - }, - "idna": { - "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" - ], - "markers": "python_version >= '3'", - "version": "==3.2" - }, - "pendulum": { - "hashes": [ - "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394", - "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b", - "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a", - "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087", - "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739", - "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269", - "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0", - "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5", - "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be", - "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7", - "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3", - "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207", - "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe", - "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360", - "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0", - "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b", - "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052", - "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002", - "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116", - "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db", - "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b" - ], - "index": "pypi", - "version": "==2.1.2" - }, - "pillow": { - "hashes": [ - "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc", - "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63", - "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d", - "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e", - "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd", - "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4", - "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77", - "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723", - "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba", - "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792", - "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae", - "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e", - "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367", - "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77", - "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856", - "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4", - "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de", - "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8", - "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a", - "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636", - "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab", - "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79", - "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d", - "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229", - "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf", - "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500", - "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04", - "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093", - "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844", - "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8", - "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82", - "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf", - "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83", - "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0", - "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c", - "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8", - "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37", - "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24", - "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14" - ], - "index": "pypi", - "version": "==8.3.1" - }, - "pyfiglet": { - "hashes": [ - "sha256:c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639", - "sha256:d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c" - ], - "index": "pypi", - "version": "==0.8.post1" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pytzdata": { - "hashes": [ - "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540", - "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2020.1" - }, - "pyyaml": { - "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "index": "pypi", - "version": "==5.4.1" - }, - "requests": { - "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" - ], - "index": "pypi", - "version": "==2.26.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.6" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index 771aaaf..f385129 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SIEGMA -This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format and engine. +This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format engine and [pysigma](https://pypi.org/project/pysigma/) library pysigma in the converter processes.

@@ -8,26 +8,24 @@ It is also our objective to take a community approach to SIEM schemas, maintaini For platforms that support it, SIEGMA also enables automatic upload of the SIEM consumable. Check the [Automatic Import](https://github.com/3CORESec/SIEGMA#siem-automatic-import) section for more information. -How does it differ from `sigmac`? The reason we decided to create our own artifacts is to have more control over the mappings and allow for a different level of automation. This project is not mean to be a replacement to Sigma or `sigmac`, especially since it utilizes `sigmac`. +How does it differ from `pysigma`? The reason we decided to create our own artifacts is to have more control over the mappings and allow for a different level of automation. This project is not mean to be a replacement to Sigma or `pysigma`, especially since it utilizes `pysigma`. ## Supported SIEM's - Elastic SIEM -- Azure Sentinel +- Azure Sentinel (in development) - Splunk (in development) # Installation -We'll run the software and install dependencies, for both this project as well as Sigma, under a Python virtual environment. +We'll run the software and install dependencies, for both this project as well as Sigma, under a Python virtual environment using poetry. -`pip3 install pipenv` +`pip3 install poetry` - Setup Sigma - + ``` -git clone https://github.com/Neo23x0/sigma -cd sigma -pipenv install --skip-lock +git clone https://github.com/SigmaHQ/sigma.git ``` - Setup SIEGMA @@ -35,11 +33,9 @@ pipenv install --skip-lock ``` git clone https://github.com/3CORESec/SIEGMA cd SIEGMA -pipenv install +poetry install ``` -_Note for Windows users_: Powershell must be enabled for command and script execution. Open `Administrative Powershell` and execute following command: `Set-ExecutionPolicy Bypass` - **Before running SIEGMA:** Sigma rules might not hold all required fields in use by your SIEM. To make sure that all fields are mapped correctly, each product holds a README where we warn you if there are fields that need to be filled before running this software. Visit your SIEM [config](config/) folder to learn more about this. @@ -50,11 +46,9 @@ Invoke the script by providing it a Sigma rule or Sigma rule folder as well as t Activate the virtual environment: -`pipenv shell` +`poetry shell` -It is recommended to consult the `siegma.py` help, especially for advanced usage instructions: - -`python siegma.py -h` +It is recommended to consult the [docs](docs/) folder to help, especially for advanced usage instructions. In order to provide examples for each specific platform, we have moved the examples section to their own README section inside of the [config folder](./config) of the SIEM in question. @@ -64,16 +58,9 @@ Please consult each SIEM folder for detailed instructions on how to convert sing SIEGMA natively makes use of this script for rule format compliance check. -However, to manually check if the rules are in the correct format and processable by SIEGMA, run following commands: - -``` -cd helpers -python check_if_compliant.py -p path/to/rules/directory/ -``` - # SIEM Automatic Import -As part of our objective of developing tools, techniques and know-how to [Detection as Code](https://blog.3coresec.com/search/label/Detection), it has always been the goal of this project to allow the usage of SIEGMA in a CI/CD pipeline. Please consult the README of the desired SIEM for additional information on how to enable this feature. +As part of our objective of developing tools, techniques and know-how to [Detection as Code](https://blog.3coresec.com/search/label/Detection), it has always been the goal of this project to allow the usage of SIEGMA in a CI/CD pipeline. By consulting the [automatic upload](docs/automatic-upload.md) document, you can gain a better understanding of the steps involved. # Contributions and Development @@ -83,7 +70,7 @@ Want to know more how it all comes together or want to contribute support for a - Additional platform/SIEM support - ~~Elastic SIEM~~ - - ~~Azure Sentinel~~ + - Azure Sentinel (To be developed) - Splunk (To be developed) - Additional Features - Elastic diff --git a/converters/ConvertorsBase.py b/backends/BackendBase.py similarity index 98% rename from converters/ConvertorsBase.py rename to backends/BackendBase.py index f146eea..bc7d75f 100644 --- a/converters/ConvertorsBase.py +++ b/backends/BackendBase.py @@ -4,10 +4,10 @@ from tools.MitreAttack import get_techinique_infos import re -class ConvertorsBase(ABC): +class BackendBase(ABC): """ Convertors abstract base class, use this class to create a new converter. - """ + """ def __init__(self, siem_config: dict[any, any]): """ Class Inicializer diff --git a/tools/Backends.py b/backends/Backends.py similarity index 100% rename from tools/Backends.py rename to backends/Backends.py diff --git a/converters/Elastic.py b/backends/Elastic.py similarity index 92% rename from converters/Elastic.py rename to backends/Elastic.py index db59cd3..1535498 100644 --- a/converters/Elastic.py +++ b/backends/Elastic.py @@ -1,6 +1,7 @@ -from converters.ConvertorsBase import ConvertorsBase -from sigma.parser.collection import SigmaParser +from backends.BackendBase import BackendBase +from sigma.collection import SigmaCollection from sigma.pipelines.elasticsearch.windows import ecs_windows +from sigma.pipelines.elasticsearch.zeek import ecs_zeek_beats from Exceptions import CreateRuleByApiError, FileExtensionError from tools.FileTools import FileTools from sigma.rule import SigmaRule @@ -43,7 +44,7 @@ def collect_risk_socre(cls, risk: str) -> int: return cls.MEDIUM @dataclass -class ElasticConverter(ConvertorsBase): +class ElasticBackend(BackendBase): """ Elastic converter, use this class to convert sigma rule into elastic rule. @@ -84,7 +85,7 @@ def create_rule(self, sigma_rule: SigmaRule, query: str) -> dict[str, any]: return { "rule_id": str(sigma_rule.id), - "author": [author.strip() for author in sigma_rule.author.split(",")], + "author": elastic_config["settings"]["author"] if len(elastic_config["settings"]["author"]) > 0 else [author.strip() for author in sigma_rule.author.split(",")], "from": elastic_config["settings"]["from"], "index": elastic_config["settings"]["index"], "interval": elastic_config["settings"]["interval"], @@ -107,7 +108,7 @@ def create_rule(self, sigma_rule: SigmaRule, query: str) -> dict[str, any]: "throttle": elastic_config["settings"]["throttle"] } - def convert(self, sigma_parser_rule: SigmaParser, backend: callable) -> str: + def convert(self, sigma_parser_rule: SigmaCollection, backend: callable) -> str: """ Function responsible for converting a sigma rule into a elastic query. @@ -119,6 +120,8 @@ def convert(self, sigma_parser_rule: SigmaParser, backend: callable) -> str: str: Return the query as string """ + print(type(sigma_parser_rule)) + print(sigma_parser_rule) rule = backend(ecs_windows()).convert(sigma_parser_rule)[0] return rule diff --git a/converters/__init__.py b/backends/__init__.py similarity index 67% rename from converters/__init__.py rename to backends/__init__.py index 1c7b59c..5bb5041 100644 --- a/converters/__init__.py +++ b/backends/__init__.py @@ -4,10 +4,10 @@ from inspect import getmembers, isclass -# Use this to collect all Converters -all_converters = { - submodule.lower(): cls # Name Of Converor: ConverorClass +# Use to collect all Backends +all_backends = { + submodule.lower(): cls # Name Of Backend: BackendClass for _, submodule, _ in iter_modules([ str(Path(__file__).resolve().parent) ]) # Iterate over modules, str around Path is due to issue with PosixPath from Python 3.10 for name, cls in getmembers(import_module(__name__ + "." + submodule, isclass)) # Iterate over classes - if name.endswith("Converter") # Class filtering (Collect only converters) + if name.endswith("Backend") # Class filtering (Collect only Backends) } diff --git a/config/README.md b/config/README.md index b6d3aaf..4869757 100644 --- a/config/README.md +++ b/config/README.md @@ -1,5 +1,3 @@ # Platform configurations -This directory contains the mapping files and configuration for each of the supported platforms as well as example queries and use cases. Each folder will also document any changes you might have to do before being able to run SIEGMA. - -Please select the desired SIEM from the folders above for additional information. +This directory includes configurations files, example queries, and use cases for each of the supported platforms. Each folder provides documentation on any necessary modifications required before running SIEGMA. To obtain more information about a specific SIEM, please select the corresponding folder from the options above. \ No newline at end of file diff --git a/config/elastic/README.md b/config/elastic/README.md index 12f793b..0eb5c9b 100644 --- a/config/elastic/README.md +++ b/config/elastic/README.md @@ -10,76 +10,36 @@ This folder holds the configuration file for the SIEM platform in question. All ### Generate an Elastic SIEM output from a single Sigma rule file -`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -o rule-output` +`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/rule.yml -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -o rule_output.ndjson` -### Generate an Elastic SIEM output from a folder with several Sigma rule files +### Generate an Elastic SIEM output from a folder with several Sigma rule files and automatic uploading the rules into elastic -`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/folder/with/sigma-rules/ -s /path/to/sigma/folder -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -o rule-output` +`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/folder/with/sigma-rules/ -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -api` An example where we utilize our [AWS CloudTrail Sigma configuration](https://blog.3coresec.com/2020/05/contributions-to-sigma-cloudtrailecs.html) to convert a single rule to Elastic SIEM output: -`python siegma.py -c config/elastic/elastic-siem.json -r rules/cloudtrail_rule.yml -s sigma/ -sc sigma/tools/config/ecs-cloudtrail.yml -o rule-output` - -### Generate an Elastic SIEM output from a rule file and also pass Sigma backend options - -In this example we will utilize `-sep` to request SIEGMA to use the advanced Sigma backend options that would be defined in the [Elastic config](config/elastic/) - -`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/folder/with/sigma-rules/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -sep -o output-file` - -### Generate an Elastic SIEM output from a rule file and also override elastic-siem.json config from commandline - -In this example we will utilize the [Elastic config](config/elastic/) fields as they are definied _(or supplied from the Sigma rule)_ **while** overwriting certain fields through the usage of `-co`. This is particularly useful if converting Sigma rules for which you'd like to apply different SIEM consumable fields. - -The example below will overwrite the `settings.author`, `credentials.kibana_url` and `credentials.kibana_username`. - -`python siegma.py -c config/elastic/elastic-siem.json -r /path/to/folder/with/sigma-rules/rule.yml -s /path/to/sigma/folder -sc /path/to/sigma/config/file/sigma/tools/config/file.yml -co settings.author=none,credentials.kibana_url="www.example.com",credentials.kibana_username="bfd" -o output-file` - -## Fields that are worth looking into to adapt to your particular use case - -| Elastic SIEM Config Field | Description | Example | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| from | No need to change if you're using `timeframe` in your Sigma rules | now-15m | -| timeline_id | Not included in the configuration but can be added | "timeline_id":"e17d2870-6bb5-11ea-9871-d10df4e7cd14" | -| timeline_title | Not included in the configuration but can be added | "timeline_title":"AWS CloudTrail" | -| sigma_params | Used in conjunction with switch --sigma_extra_parameters. Dictionary under which any of the parameters supported by Sigma can be added. | {"sigma_params": {"--backend-option": ["key=value", "case_insensitive_whitelist=*"]}} | +`python siegma.py -c config/elastic/elastic-siem.json -r rules/cloudtrail_rule.yml -sc sigma/tools/config/ecs-cloudtrail.yml -o rule_output.ndjson` ## Elastic SIEM Configuration & Data Dictionary | Elastic SIEM Config Field | Default Value | Field type | Description | | ------------------------- | --------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| sigma_query_format | Allowed Values: `es-qs`, `es-eql` | Hardcoded | Preset value. This value is passed to sigmac | -| sigma_params | N/A | User input optional | Used in conjunction with switch --sigma_extra_parameters. Dictionary under which any of the parameters supported by Sigma can be added. Example: {"sigma_params": {"--backend-option": ["key=value", "case_insensitive_whitelist=*"]}} | -| kibana_username | No default value | User input optional | Enables automatic rule upload if filled | -| kibana_password | No default value | User input optional | Enables automatic rule upload if filled | -| kibana_url | No default value | User input optional | Enables automatic rule import if filled (i.e http://my_kibana:5601) | +| username | No default value | User input optional | Enables automatic rule upload if filled | +| password | No default value | User input optional | Enables automatic rule upload if filled | + siem_url | No default value | Elastic URL | Enables automatic rule import if filled (i.e http://my_kibana:5601) | + apikey | No default value | Elastic apikey | In this version, these specific parameters do not need to be passed in the configuration file. | | rule_id | No default value | Sigma: id | Rule identifier | | id | No default value | Sigma: id | Rule identifier | actions | No default value or [] | Sigma: Elastic actions | Rule actions as supported by Elastic | | -| author | No default value | Sigma: author | Rule author | +| author | No default value | Sigma: author | Rule author, By default, it is loaded from the Sigma rule itself and do not need to be explicitly specified in the configuration file. | | from | now-15m | Sigma: timeframe | Defines how much data in the past should be queried. Example: now-15m | | index | ["*"] | User input needed | Define the indexes that should be queried. Example: filebeat-\* | | interval | 5m | User input needed | Define how often the rule should run in Elastic SIEM | -| language | kuery | Hardcoded | Default value used by Elastic for Kibana Query Language (KQL). Can be changed to lucene if needed. | -| output_index | .siem-signals-default | Hardcoded | Default index used by Elastic SIEM | -| references | No default value | Sigma: references | References and documentation related to the detection | -| false_positives | No default value | Sigma: falsepositives | Event under which a false positive can trigger the detection | -| risk_score | No default value | Sigma: severity or score | Custom defined value in custom attribute/field "score" takes precedence. Otherwise, tag low=25, medium=50, high=75, critical=100 | -| name | No default value | Sigma: title | Rule name | +| language | lucene | Hardcoded | Default value used by Elastic for lucene. | +| output_index | .siem-signals-default | Hardcoded | Default index used by Elastic SIEM | note | No default value | Sigma: note | Contains relative path (from notes_folder) to a .md (markdown formatted) file with details for investigation guide / notes for a given detection use case. Optional field that can be set in config or rule.yml file. Settings in rule.yml take precedence over settings in config. | -| notes_folder | No default value | Sigma Config: notes_folder | Contains path to a folder that contains .md (markdown formatted) file containing details for investigation guide / notes for a given detection use case. Optional field that can be set either in config or using -co from commandline. | -| description | No default value | Sigma: description | Rule description | | query | No default value | Hardcoded | Comes from the result of running sigmac | -| severity | No default value | Sigma: severity | Rule severity | -| tags | No default value | Sigma: siemtags | Tags to aid in rule identification | | to | now | Hardcoded | Preset field and value. Don't change | -| threshold.field | null | Sigma: threshold.field | Optional parent field. Used for aggregate based rules on Kibana. 'threshold' key can be skipped entirely. Value must be a "string" that may or may not be mapped into ECS format based on the sigma config file 'fieldmappings' passed to SIEGMA. Same fields in the rule.yml file will take precendence over the values defined in siegma config | -| threshold.value | null | Sigma: threshold.value | Optional parent field. Used for aggregate based rules on Kibana to depict count. If events greater than equal to threshold.value are observed, rule will trigger. 'threshold' key can be skipped entirely. Value preferably should be an "Integer". Same fields in the rule.yml file will take precendence over the values defined in siegma config | | type | query | Hardcoded | Preset field and value. Don't change | | threat | No default value | Sigma: tags | ATT&CK mapping | -| throttle | no_actions | Hardcoded | Preset field and value. Don't change | -| timeline_id | No default value | Must be added to config | SIEM Timeline ID (i.e e17d2870-6bb5-11ea-9871-d10df4e7cd14) | -| timeline_title | No default value | Must be added to config | Desired name to be associated with the Timeline (i.e AWS CloudTrail) | - -## Automatic upload - -If you'd like to enable automatic upload of consumables into your SIEM, please enter your environment variables in the [config file](config/elastic-siem.json) or specify them through `-co` as previously shown. +| throttle | no_actions | Hardcoded | Preset field and value. Don't change | \ No newline at end of file diff --git a/config/elastic/elastic-siem.json b/config/elastic/elastic-siem.json index 26733fb..af24ddb 100644 --- a/config/elastic/elastic-siem.json +++ b/config/elastic/elastic-siem.json @@ -1,5 +1,4 @@ { - "sigma_query_format": "es-qs", "sigma_params": { "--backend-option": [ "" @@ -11,24 +10,13 @@ "siem_url": "", "apikey": "" }, - "notes_folder": "", "settings": { - "rule_id": "", - "id": "", "author":[], "from":"now-15m", "index":["*"], "interval":"5m", "language":"lucene", "output_index":".siem-signals-default", - "references":[], - "false_positives":[], - "risk_score":0, - "name":"", - "description":"", - "query":"", - "severity":"", - "tags":[], "to":"now", "type":"query", "threat":[], diff --git a/development-guide.md b/development-guide.md deleted file mode 100644 index eaa774b..0000000 --- a/development-guide.md +++ /dev/null @@ -1,25 +0,0 @@ -# Development & Contribution Notes - -This page provides developers with the information required to develop support for additional products or make contributions to this project. - -## How to contribute (Developers adding support for more SIEMs & platforms) - -* Define the configuration for a given SIEM in: - * An already created folder under [config](config). See [elastic](config/elastic) for reference. - * Or create a new folder under [config](config) along with the configuration file and corresponding README. See [elastic](config/elastic) for reference. -* Setup Sigma repository. -* Setup SIEGMA repository. -* Under [rule_file_creator_scripts](rule_file_creator_scripts) folder, add a script by the name of the format that you want to support. For example: [es_qs.py](rule_file_creator_scripts/es_qs.py). This script shall contain all the heavy lifting for conversions. See [es_qs.py](rule_file_creator_scripts/es_qs.py) for reference. - * 3 main functions inside the [es_qs.py](rule_file_creator_scripts/es_qs.py) get called from [siegma.py](siegma.py) and performs all the tasks related to rule file creation. - * Function 1 - create_rule_file: This function will take in Sigma rule and configuration as input and prepare the rule file. - * Function 2 - validate_credentials: This function will take in credentials from the configuration and confirm if the credentials are valid or not. - * Function 3 - install_rule: This function will upload and install the final rule file on the SIEM/Product. -* At the top of [siegma.py](siegma.py), import your newly created script file. -* Go to create_rule_file_for_siem() in [siegma.py](siegma.py). Add an if/else and point it to the function in your new_format.py. -* Go to main() in [siegma.py](siegma.py) and update it to point to the correct installation and validation functions for the newly created rule format script. - -## Folder Hierarchy -* **[config](config)**: Folder that contains sample and production configurations so rule file creation can take place. - * **[elastic](config/elastic)**: Folder that contains Elastic SIEM configuration files. -* **[rule_file_creator_scripts](rule_file_creator_scripts)**: Folder that contains individual scripts that handle the heavy lifting for different formats. -* **[helpers](helpers)**: Folder containing helper scripts & binaries for siegma.py. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..df18a8b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# Documentation Folder + +This folder contains all the documentation related to the project. + +## Contents + +- [arguments](./arguments.md) +- [backend](./backend.md) +- [config](./config.md) +- [development-guide](./development-guide.md) +- [output](./output.md) +- [tools](./tools.md) diff --git a/docs/arguments.md b/docs/arguments.md new file mode 100644 index 0000000..ce98aab --- /dev/null +++ b/docs/arguments.md @@ -0,0 +1,79 @@ +# Arguments + +A brief documentation detailing all the available arguments. + +--- + +- -h, --help + +Show all the available arguments and their descriptions. + + +- -config, --config +Config file path. Eg: /path/to/config.json. + +This argument is **MANDATORY**. + +``` +dest: config +``` + +- -b, --backend + +SIEM backend used for the conversion process. Eg: elastic + +This argument is **MANDATORY**. + +``` +dest: backend +``` +- --api + +To create a rule in the SIEM using the API, include this argument when executing the command. + +``` +dest: api +``` +- -p , --path + +Provide the file or folder path for the rule. The path should be either an absolute path from the root folder or a relative path to the sigma folder, not the siegma folder. Eg: /path/to/rule/file.yml or /path/to/rules/folder. + +This argument is **MANDATORY**. + +``` +dest: path +default: False +``` + +-sc --sigma_config + +Sigma config file path. Eg: /path/to/sigma/tools/config/ecs-cloudtrail.yml. + +``` +dest: sigma_config_file +``` + +- -o --output +SIEM rule output file name. Eg: output.json. + +``` +dest: output_file +default: .output.ndjson +``` + +- -v --verbosity + +Execution verbosity level. Eg: INFO|WARN|DEBUG|ERROR + +``` +dest: verbosity_level +default: INFO +``` + +- -lf , --log_file + +- File to save the logs infos. +``` +dest: verbosity_level +default: .output.log +``` \ No newline at end of file diff --git a/docs/automatic-upload.md b/docs/automatic-upload.md new file mode 100644 index 0000000..f1e87d4 --- /dev/null +++ b/docs/automatic-upload.md @@ -0,0 +1,3 @@ +## Automatic upload + +Ensure that you have correctly configured the credentials for the SIEM platform in the configuration file (More information about the config file [here](config.md)). Then, when executing the command, include the `--api` parameter to enable the API functionality for automatic rule uploads (More information about the parameters [here](arguments.md)). \ No newline at end of file diff --git a/docs/backend.md b/docs/backend.md new file mode 100644 index 0000000..5d00098 --- /dev/null +++ b/docs/backend.md @@ -0,0 +1,91 @@ +# Backend + +Backends are responsible for converting Sigma rules into target query languages and automatically uploading them to the desired SIEM platform, In this project. the conversion process is handled by the [pysigma library](https://pypi.org/project/pysigma/). + +--- + +## Backends + +To create a new Backend, you can utilize the [pysigma](https://pypi.org/project/pysigma/) library, which offers mapping for SIEM platforms, conversion processes, pipelines, and other helpful functions. For detailed instructions and examples, it is recommended to refer to the pysigma repository. + +You can visit the [pysigma repository](https://github.com/SigmaHQ/pySigma) on platforms such as GitHub or the [official project website](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html). These sources provide comprehensive documentation, code samples, and examples specific to the Backends module. Accessing the repository will allow you to gain a deeper understanding of how to import, utilize, and create new Backends using the pysigma library. + +--- + +## Implementation example +To create a new backend instance, use the base backend located at the `backend.BackendBase` abstract class. + +```python +from BackendBase import BackendBase + +class MyAwesomeBackend(BackendBase) + # implementation details here +``` + +MyAwesomeBackend will need to implement some functions to work, such as: + +```python +convert(sigma_rule: SigmaCollection, backend: callable) -> str: + """ + Function responsible for converting a sigma rule. + + Arguments: + sigma_rule (SigmaCollection): Sigma rule, use `tools.SigmaUtils.SigmaRule.get_sigma_configuration` to create a SigmaCollection. + backend (callable): Backend responsible for converting the rule. + + Returns: + str: Return the query as string + """ +``` + +```python +write_rule(file_name: str, rule_infos: any) -> None: + """ + Use this function to save the output into a file. + + Args: + file_name (str): File name. + rule_infos (any): Rule content. + """ +``` + +```python +create_rule(sigma_rule: SigmaRule, query: str) -> any: + """ + Use this function to create a rule following the SIEM syntax. + + Args: + sigma_rule (SigmaRule): Sigma rule content, use `tools.SigmaUtils.SigmaRule.get_yml_file_content` to create a SigmaRule object + query (str): Query as string, use the `convert` method to get a query string + + Returns: + any: Return the rule following each SIEM syntax. + """ +``` + +```python +create_rule_by_api(rule: dict[str, any], siem_url: str="", username: str="", __passwd: str="", apikey: str="") -> any: + """ + Create a new elastic rule by API. + + Args: + rule (dict[str, any]): Rule content, use `converter.elastic.create_rule` to get a valid rule. +``` + +All of these functions are necessary to be implemented when creating a new backend, To see a practical example, take a look at the Elastic backend. + +For a comprehensive understanding of how a rule is converted, please refer to the documentation of the pysigma library, which can be found [here](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html) + +--- + +## Collect all backends + +To retrieve all available backends, you can use the backends.Backends.Backends located in the [Backends](..\backends\Backends.py) file. This contains a collection of all the available backends in the project. + +Example: + +```python +from backends.Backends import Backends + +print(Backends._member_names_) +``` diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..b708857 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,3 @@ +# Config + +The config folder is responsible for storing the configurations for each SIEM. Before running SIEGMA, it is essential to review the documentation specific to each SIEM. It will provide detailed instructions and guidelines on configuring the necessary settings for a successful execution. \ No newline at end of file diff --git a/docs/development-guide.md b/docs/development-guide.md new file mode 100644 index 0000000..d396ee8 --- /dev/null +++ b/docs/development-guide.md @@ -0,0 +1,21 @@ +# Development & Contribution Notes + +This page serves as a valuable resource for developers who are interested in contributing to the project or extending support for additional products. It provides the necessary information and guidance to facilitate the development process and make meaningful contributions to the project's growth. + +## How to contribute (Developers adding support for more SIEMs & platforms) + +- Define the configuration for a given SIEM in: + * An already created folder under [config](../config/). See [elastic](../config/elastic) for reference. + * Or create a new folder under [config](../config) along with the configuration file and corresponding README. See [elastic](../config/elastic) for reference. +* Setup SIEGMA repository. +* Under the [backend](../backends/) folder, add a script by the name of the format that you want to support. For example: [Elastic.py](../backend/elastic.py). This script shall contain all the heavy lifting for conversions. +* To create a new backend converter, you need to create a new class that ends with `Backend`. Eg `class MyAwesomeBackend`, you need to do this for the [all_backends](../backends/__init__.py) mapping collect the new backend. +* Inside the new class, you need to implement the following functions: + - create_rule_by_api: This function is responsible for creating a new backend rule using the API. + - create_rule: This function is used to create a rule following the SIEM syntax. + - write_rule: This function allows you to save the output into a file. + - convert: This function is responsible for converting a sigma rule. + +Please consult the [backend documentation](backend.md) for more information about these functions, including their specific usage, parameters, and return values. + +* In the [Backend.py](../backends/BackendBase.py) script, import the backend module from [pysigma](https://pypi.org/project/pysigma/) and include it in the Backend enum class. diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 0000000..bbf30cc --- /dev/null +++ b/docs/output.md @@ -0,0 +1,4 @@ +# output + +The output folder contains logs of the execution and SIEM rule files that are ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For further details, please refer to the [arguments documentation](./arguments.md) or type --help for assistance. + diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 0000000..c7f97a9 --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,3 @@ +# Tools + +The [tool]() folder contains various utility scripts and tools that assist in automating tasks, simplifying workflows, or providing specific functionalities for a project. \ No newline at end of file diff --git a/output/output.md b/output/output.md new file mode 100644 index 0000000..42be4e0 --- /dev/null +++ b/output/output.md @@ -0,0 +1,4 @@ +# output folder + +The output folder contains logs of the execution and SIEM rule files that are ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For further details, please refer to the [documentation](../docs/arguments.md) on arguments or type --help for assistance. + diff --git a/siegma.py b/siegma.py index b4becb8..d703a09 100644 --- a/siegma.py +++ b/siegma.py @@ -1,12 +1,11 @@ from tools.LogHandler import LogHandler, logging_levels -from Exceptions import CreateRuleByApiError, RuleSintaxeError, FileExtensionError from tools.FileTools import FileTools from tools.DirectoryTools import DirectoryTools from tools.SigmaUtils import SigmaRule, get_sigma_configuration -from tools.Backends import Backends +from backends.Backends import Backends import argparse import os -from converters import all_converters +from backends import all_backends import traceback def setup_args() -> argparse.Namespace: @@ -16,8 +15,8 @@ def setup_args() -> argparse.Namespace: parser = argparse.ArgumentParser(os.path.basename(__file__)) parser.add_argument("-c", "--config", required=True, dest="config", help="Config file path. Eg: /path/to/config.json") - parser.add_argument("-b", "--backend", choices=Backends._member_names_, dest="backend", default="elastic", help=f"SIEM backend to perform the conversion. Eg: {'|'.join(Backends._member_names_)}") - parser.add_argument("--api", dest="api", action="store_true", default=False, help="Pass this argument to create a rule in the SIEM via the api.") + parser.add_argument("-b", "--backend", choices=Backends._member_names_, required=True, dest="backend", help=f"SIEM backend to perform the conversion. Eg: {'|'.join(Backends._member_names_)}") + parser.add_argument("--api", dest="api", action="store_true", default=False, help="Create a rule in the SIEM using the API.") parser.add_argument('-p', '--path', type=str, required=True, help='Rule file / folder path. This should be either the absolute path from root folder or should be relative to sigma, NOT siegma. Eg: /path/to/rule/file.yml or /path/to/rules/folder.') parser.add_argument("-sc", "--sigma_config", dest="sigma_config_file", type=str, help="Sigma config file path. Eg: /path/to/sigma/tools/config/ecs-cloudtrail.yml.", required=True) parser.add_argument("-o", "--output", dest="output_file", type=str, help="Output file path. Eg: /path/to/output_file.", default=".output.ndjson") @@ -45,7 +44,7 @@ def main(): logger.info(f"{backend.name} backend has been loaded.") logger.debug(f"Loading {backend.name} convertor") - rule_convertor = all_converters[backend.name](siem_config) + rule_convertor = all_backends[backend.name](siem_config) if not FileTools.check_if_is_a_file(args.path): logger.info("Loading rules files") @@ -80,7 +79,7 @@ def main(): elastic_rule = rule_convertor.create_rule(rule_content[0], query) logger.debug("Saving rule to a file.") - rule_convertor.write_rule(args.output_file, elastic_rule) + rule_convertor.write_rule(f"output\{args.output_file}", elastic_rule) logger.info(f"The rule has been saved in the {args.output_file} file.") if args.api: @@ -98,7 +97,6 @@ def main(): except Exception as e: logger.error(f"Error during the conversion process of the rule {rule_content[0].title}") logger.error(e) - errors += 1 if args.verbosity_level == "DEBUG": traceback.print_exc() diff --git a/tools/LogHandler.py b/tools/LogHandler.py index 4d8479b..0890b41 100644 --- a/tools/LogHandler.py +++ b/tools/LogHandler.py @@ -63,7 +63,7 @@ def create_file_handler(self) -> logging.FileHandler: logging.FileHandler: File handler """ - file_handler = logging.FileHandler(self.log_file_name) + file_handler = logging.FileHandler(f"output\{self.log_file_name}") file_handler.setFormatter(logging.Formatter(self.file_log_fmt, datefmt=self.datetimefmt)) return file_handler From ea5f00c440673b18ca6b11c75c08cc6020feb649 Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Tue, 9 May 2023 20:41:45 -0300 Subject: [PATCH 3/9] Create LICENCE --- LICENCE | 661 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 LICENCE diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENCE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 138f70085018e663344fadc5c138d3eccca5502b Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Tue, 9 May 2023 20:43:12 -0300 Subject: [PATCH 4/9] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f385129..45f7ff7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SIEGMA -This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format engine and [pysigma](https://pypi.org/project/pysigma/) library pysigma in the converter processes. +This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format engine and [pysigma](https://pypi.org/project/pysigma/) library in the converter processes.

@@ -48,7 +48,7 @@ Activate the virtual environment: `poetry shell` -It is recommended to consult the [docs](docs/) folder to help, especially for advanced usage instructions. +It is recommended to consult the [docs](docs/) folder for help, especially for advanced usage instructions. In order to provide examples for each specific platform, we have moved the examples section to their own README section inside of the [config folder](./config) of the SIEM in question. From c3368057dfcbe27f8d218cfb74b987079726fe8c Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Tue, 9 May 2023 20:44:00 -0300 Subject: [PATCH 5/9] Update tools.md --- docs/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tools.md b/docs/tools.md index c7f97a9..9ee3f57 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -1,3 +1,3 @@ # Tools -The [tool]() folder contains various utility scripts and tools that assist in automating tasks, simplifying workflows, or providing specific functionalities for a project. \ No newline at end of file +The [tool](../tools/) folder contains various utility scripts and tools that assist in automating tasks, simplifying workflows, or providing specific functionalities for a project. From bf084766a51d26640a8803d491d5a562e6bdcd8a Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Tue, 9 May 2023 23:56:58 -0300 Subject: [PATCH 6/9] Update --- README.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45f7ff7..74fcecd 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,11 @@ Found this interesting? Have a question/comment/request? Let us know! Feel free to open an [issue](https://github.com/3CORESec/SIEGMA/issues) or ping us on [Twitter](https://twitter.com/3CORESec). [![Twitter](https://img.shields.io/twitter/follow/3CORESec.svg?style=social&label=Follow)](https://twitter.com/3CORESec) + + +### Authors + +- [DiogoBraz](https://github.com/DiogoBraz) +- [heyibrahimkhan](https://github.com/heyibrahimkhan) +- [wesley587](https://github.com/wesley587) +- [w0rk3r](https://github.com/w0rk3r) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 283c083..7e70bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "siegma" version = "0.2.0" description = "This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the Sigma rule format and engine." -authors = ["Wesley Cesar <72465364+wesley587@users.noreply.github.com>"] +authors = ["DiogoBraz ", "heyibrahimkhan ", "wesley587 ", "w0rk3r "] readme = "README.md" [tool.poetry.dependencies] From 3b34e800fffa7703c9023b587b3c1aa76d9529e8 Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Sat, 20 May 2023 15:44:52 -0300 Subject: [PATCH 7/9] Fixed typing errors --- Exceptions.py | 10 +++++----- README.md | 11 +++++++---- backends/BackendBase.py | 8 ++++---- backends/Backends.py | 2 +- backends/Elastic.py | 21 +++++++++------------ backends/__init__.py | 3 ++- config/README.md | 2 +- docs/arguments.md | 8 ++++---- docs/backend.md | 14 ++++++++------ docs/config.md | 2 +- docs/development-guide.md | 6 +++--- docs/output.md | 5 ++--- docs/tools.md | 2 +- output/output.md | 4 ++-- pyproject.toml | 2 +- siegma.py | 6 +++--- tools/DirectoryTools.py | 4 ++-- tools/FileTools.py | 10 +++++----- tools/SigmaUtils.py | 10 +++++----- 19 files changed, 66 insertions(+), 64 deletions(-) diff --git a/Exceptions.py b/Exceptions.py index 5de69cc..e2c9ab0 100644 --- a/Exceptions.py +++ b/Exceptions.py @@ -1,22 +1,22 @@ # To get more information about the errors, user DEBUG verbosity level. -class RuleSintaxeError(Exception): +class RuleSyntaxError(Exception): """ - Error in the RuleSintaxe. + Error in the RuleSyntax. """ pass -class CreateRuleByApiError(Exception): +class CreateRuleViaApiError(Exception): """ - Erro to create a rule by API. e.g. Invalid SIEM resource. + Error while creating the rule via API. e.g., Invalid SIEM resource """ pass class FileExtensionError(Exception): """ - File extension error, e.g. The SIEM needs a json and was passed a txt output file. + File extension error, e.g., The SIEM requires a JSON and was provided a file with another extension. """ pass diff --git a/README.md b/README.md index 74fcecd..53e883b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SIEGMA -This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format engine and [pysigma](https://pypi.org/project/pysigma/) library in the converter processes. +This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and utilizing the [Sigma](https://github.com/Neo23x0/sigma) rule format and [pySigma](https://pypi.org/project/pysigma/) library in the conversion process.

@@ -86,8 +86,11 @@ Feel free to open an [issue](https://github.com/3CORESec/SIEGMA/issues) or ping ### Authors - -- [DiogoBraz](https://github.com/DiogoBraz) - [heyibrahimkhan](https://github.com/heyibrahimkhan) - [wesley587](https://github.com/wesley587) -- [w0rk3r](https://github.com/w0rk3r) \ No newline at end of file + +### Contributors + +- [DiogoBraz](https://github.com/DiogoBraz) +- [w0rk3r](https://github.com/w0rk3r) +- [Tiago Faria](https://github.com/0xtf) \ No newline at end of file diff --git a/backends/BackendBase.py b/backends/BackendBase.py index bc7d75f..8838317 100644 --- a/backends/BackendBase.py +++ b/backends/BackendBase.py @@ -6,7 +6,7 @@ class BackendBase(ABC): """ - Convertors abstract base class, use this class to create a new converter. + Convertors abstract base class. use this class to create a new converter. """ def __init__(self, siem_config: dict[any, any]): """ @@ -42,13 +42,13 @@ def write_rule(self, file_name: str, rule_infos: any) -> None: @abstractclassmethod def create_rule(self, sigma_rule: SigmaRule, query: str) -> any: """ - Use this function to create a rule following the SIEM sintaxe. + Use this function to create a rule following the SIEM syntax. Args: sigma_rule (SigmaRule): Sigma rule content, use tools.SigmaUtils.SigmaRule.get_yml_file_content to create a SigmaRule object query (str): Query as string, use the convert method to get a query string Returns: - any: Return the rule following each SIEM sintaxe. + any: Return the rule following each SIEM syntax. """ def create_rule_by_api(self, rule: dict[str, any], siem_url: str="", username: str="", __passwd: str="", apikey: str="") -> any: @@ -94,4 +94,4 @@ def get_mitre_attack_mapping(self, rule_tags: list[str]) -> dict[str, dict[str | mitre_attack_mapping[tactic_infos["name"]]["techniques"].append(techinique_infos) - return mitre_attack_mapping \ No newline at end of file + return mitre_attack_mapping diff --git a/backends/Backends.py b/backends/Backends.py index 6e17b5f..2dcc8fa 100644 --- a/backends/Backends.py +++ b/backends/Backends.py @@ -3,7 +3,7 @@ class Backends(Enum): """ - Enun with all available backends + Enum all available backends. """ elastic = LuceneBackend diff --git a/backends/Elastic.py b/backends/Elastic.py index 1535498..387d69a 100644 --- a/backends/Elastic.py +++ b/backends/Elastic.py @@ -1,8 +1,7 @@ from backends.BackendBase import BackendBase from sigma.collection import SigmaCollection from sigma.pipelines.elasticsearch.windows import ecs_windows -from sigma.pipelines.elasticsearch.zeek import ecs_zeek_beats -from Exceptions import CreateRuleByApiError, FileExtensionError +from Exceptions import CreateRuleViaApiError, FileExtensionError from tools.FileTools import FileTools from sigma.rule import SigmaRule from dataclasses import dataclass @@ -15,7 +14,7 @@ class RiskSocreMapping(Enum): """ - Elastic risk socre mapping. + Elastic risk score mapping. """ INFORMATIONAL = 1 @@ -33,7 +32,7 @@ def collect_risk_socre(cls, risk: str) -> int: risk (str): Risk level Returns: - int: Return the number of risk associated with the rule. + int: Return the risk number associated with the rule. """ risk = risk.upper() @@ -77,7 +76,7 @@ def create_rule(self, sigma_rule: SigmaRule, query: str) -> dict[str, any]: query (str): Query as string, use the convert method to get a query string Returns: - any: Return the rule following each SIEM sintaxe. + any: Return the rule following each SIEM syntax. """ elastic_config = self.siem_config @@ -120,15 +119,13 @@ def convert(self, sigma_parser_rule: SigmaCollection, backend: callable) -> str: str: Return the query as string """ - print(type(sigma_parser_rule)) - print(sigma_parser_rule) rule = backend(ecs_windows()).convert(sigma_parser_rule)[0] return rule def create_rule_by_api(self, rule: dict[str, any], siem_url: str, username: str, _passwd: str, apikey="") -> dict[any, any]: """ - Create a new elastic rule by api. + Create a new elastic rule via api. Args: rule (dict[str, any]): Rule content, use converter.elastic.create_rule to get a valid rule. @@ -136,7 +133,7 @@ def create_rule_by_api(self, rule: dict[str, any], siem_url: str, username: str, __passwd (str): Password to authenticate. Raises: - CreateRuleByApiError: Custom exception, if you see it, it means some error happened in the process. + CreateRuleViaApiError: Custom exception, if you see it, it means some error happened in the process. Returns: dict[any, any]: Rule content. @@ -157,13 +154,13 @@ def create_rule_by_api(self, rule: dict[str, any], siem_url: str, username: str, resp_content = resp.json() if resp.status_code != 200: - raise CreateRuleByApiError(resp_content["message"]) + raise CreateRuleViaApiError(resp_content["message"]) return resp.json() def create_elastic_attack_mapping(self, techniques: list[str]) -> dict[str, any]: """ - Gather information from a technique in the mitre attack mapping and return them as a elastic threat type. + Gather technique information in the mitre attack mapping and return them as an elastic threat type. Args: techniques (list[str]): Techniques to search @@ -196,4 +193,4 @@ def create_elastic_attack_mapping(self, techniques: list[str]) -> dict[str, any] mitre_attack_threat.append(mitre) - return mitre_attack_threat \ No newline at end of file + return mitre_attack_threat diff --git a/backends/__init__.py b/backends/__init__.py index 5bb5041..53f1887 100644 --- a/backends/__init__.py +++ b/backends/__init__.py @@ -4,7 +4,8 @@ from inspect import getmembers, isclass -# Use to collect all Backends +# Used to collect all Backends + all_backends = { submodule.lower(): cls # Name Of Backend: BackendClass for _, submodule, _ in iter_modules([ str(Path(__file__).resolve().parent) ]) # Iterate over modules, str around Path is due to issue with PosixPath from Python 3.10 diff --git a/config/README.md b/config/README.md index 4869757..ca7d0ab 100644 --- a/config/README.md +++ b/config/README.md @@ -1,3 +1,3 @@ # Platform configurations -This directory includes configurations files, example queries, and use cases for each of the supported platforms. Each folder provides documentation on any necessary modifications required before running SIEGMA. To obtain more information about a specific SIEM, please select the corresponding folder from the options above. \ No newline at end of file +This directory includes configuration files, example queries, and use cases for each of the supported platforms. Each folder provides documentation on any necessary modifications required before running SIEGMA. To obtain more information about a specific SIEM, please select the corresponding folder from the options above. \ No newline at end of file diff --git a/docs/arguments.md b/docs/arguments.md index ce98aab..7ae9f9e 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -12,7 +12,7 @@ Show all the available arguments and their descriptions. - -config, --config Config file path. Eg: /path/to/config.json. -This argument is **MANDATORY**. +This argument is **required**. ``` dest: config @@ -22,7 +22,7 @@ dest: config SIEM backend used for the conversion process. Eg: elastic -This argument is **MANDATORY**. +This argument is **required**. ``` dest: backend @@ -36,9 +36,9 @@ dest: api ``` - -p , --path -Provide the file or folder path for the rule. The path should be either an absolute path from the root folder or a relative path to the sigma folder, not the siegma folder. Eg: /path/to/rule/file.yml or /path/to/rules/folder. +Provide the file or folder path for the rule. The path should be either an absolute path from the root folder or a relative path to the sigma folder, not the siegma folder. E.g., `/path/to/rule/file.yml` or `/path/to/rules/folder`. -This argument is **MANDATORY**. +This argument is **required**. ``` dest: path diff --git a/docs/backend.md b/docs/backend.md index 5d00098..f62144a 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -1,18 +1,19 @@ # Backend -Backends are responsible for converting Sigma rules into target query languages and automatically uploading them to the desired SIEM platform, In this project. the conversion process is handled by the [pysigma library](https://pypi.org/project/pysigma/). +Backends are responsible for converting Sigma rules into target query languages and automatically uploading them to the desired SIEM platform. In this project, the conversion process is handled by the [pySigma library](https://pypi.org/project/pysigma/). --- ## Backends -To create a new Backend, you can utilize the [pysigma](https://pypi.org/project/pysigma/) library, which offers mapping for SIEM platforms, conversion processes, pipelines, and other helpful functions. For detailed instructions and examples, it is recommended to refer to the pysigma repository. +To create a new Backend, you can utilize the [pySigma](https://pypi.org/project/pysigma/) library, which provides mapping for SIEM platforms, conversion processes, pipelines, and other helpful functions. For detailed instructions and examples, it is recommended to refer to the pySigma repository. -You can visit the [pysigma repository](https://github.com/SigmaHQ/pySigma) on platforms such as GitHub or the [official project website](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html). These sources provide comprehensive documentation, code samples, and examples specific to the Backends module. Accessing the repository will allow you to gain a deeper understanding of how to import, utilize, and create new Backends using the pysigma library. +You can visit the [pySigma repository](https://github.com/SigmaHQ/pySigma) on platforms such as GitHub or the [official package docs](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html). These sources provide comprehensive documentation, code samples, and examples specific to the Backends module. Accessing the repository will allow you to gain a deeper understanding of how to import, utilize, and create new Backends using the pysigma library. --- ## Implementation example + To create a new backend instance, use the base backend located at the `backend.BackendBase` abstract class. ```python @@ -66,15 +67,16 @@ create_rule(sigma_rule: SigmaRule, query: str) -> any: ```python create_rule_by_api(rule: dict[str, any], siem_url: str="", username: str="", __passwd: str="", apikey: str="") -> any: """ - Create a new elastic rule by API. + Create a new elastic rule via API. Args: rule (dict[str, any]): Rule content, use `converter.elastic.create_rule` to get a valid rule. ``` -All of these functions are necessary to be implemented when creating a new backend, To see a practical example, take a look at the Elastic backend. +All of these functions are required when creating a new backend, To see a practical example, take a look at the Elastic backend. + +For a comprehensive understanding of how a rule is converted, please refer to the documentation of the pySigma library, which can be found [here](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html) -For a comprehensive understanding of how a rule is converted, please refer to the documentation of the pysigma library, which can be found [here](https://sigmahq-pysigma.readthedocs.io/en/latest/index.html) --- diff --git a/docs/config.md b/docs/config.md index b708857..7c00092 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,3 +1,3 @@ # Config -The config folder is responsible for storing the configurations for each SIEM. Before running SIEGMA, it is essential to review the documentation specific to each SIEM. It will provide detailed instructions and guidelines on configuring the necessary settings for a successful execution. \ No newline at end of file +The config folder is responsible for storing the configurations for each SIEM. Before running SIEGMA, it is essential to review the documentation specific to each SIEM. It will provide detailed instructions and requirements for successful execution. \ No newline at end of file diff --git a/docs/development-guide.md b/docs/development-guide.md index d396ee8..e0c35f3 100644 --- a/docs/development-guide.md +++ b/docs/development-guide.md @@ -2,14 +2,14 @@ This page serves as a valuable resource for developers who are interested in contributing to the project or extending support for additional products. It provides the necessary information and guidance to facilitate the development process and make meaningful contributions to the project's growth. -## How to contribute (Developers adding support for more SIEMs & platforms) +## How to contribute with support for more SIEMs/platforms - Define the configuration for a given SIEM in: * An already created folder under [config](../config/). See [elastic](../config/elastic) for reference. * Or create a new folder under [config](../config) along with the configuration file and corresponding README. See [elastic](../config/elastic) for reference. * Setup SIEGMA repository. -* Under the [backend](../backends/) folder, add a script by the name of the format that you want to support. For example: [Elastic.py](../backend/elastic.py). This script shall contain all the heavy lifting for conversions. -* To create a new backend converter, you need to create a new class that ends with `Backend`. Eg `class MyAwesomeBackend`, you need to do this for the [all_backends](../backends/__init__.py) mapping collect the new backend. +* Under the [backend](../backends/) folder, add a script named after the platform you want to support. For example: [Elastic.py](../backend/elastic.py). This script shall contain all the heavy lifting for conversions. +* To create a new backend converter, you need to create a new class that ends with `Backend`. E.g., `class MyAwesomeBackend`, you need to do this for the [all_backends](../backends/__init__.py) mapping to collect the new backend. * Inside the new class, you need to implement the following functions: - create_rule_by_api: This function is responsible for creating a new backend rule using the API. - create_rule: This function is used to create a rule following the SIEM syntax. diff --git a/docs/output.md b/docs/output.md index bbf30cc..6fb9008 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,4 +1,3 @@ -# output - -The output folder contains logs of the execution and SIEM rule files that are ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For further details, please refer to the [arguments documentation](./arguments.md) or type --help for assistance. +# # Output Folder +The output folder contains logs of the execution and SIEM rule files ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For more details, please look at the [arguments documentation](./arguments.md) or type --help for assistance. diff --git a/docs/tools.md b/docs/tools.md index 9ee3f57..96ff9d2 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -1,3 +1,3 @@ # Tools -The [tool](../tools/) folder contains various utility scripts and tools that assist in automating tasks, simplifying workflows, or providing specific functionalities for a project. +The [tools](../tools/) folder contains various utility scripts and tools that assist in automating tasks, simplifying workflows, or providing specific functionalities for a project. diff --git a/output/output.md b/output/output.md index 42be4e0..449640e 100644 --- a/output/output.md +++ b/output/output.md @@ -1,4 +1,4 @@ -# output folder +# # Output Folder -The output folder contains logs of the execution and SIEM rule files that are ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For further details, please refer to the [documentation](../docs/arguments.md) on arguments or type --help for assistance. +The output folder contains logs of the execution and SIEM rule files ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For more details, please look at the [arguments documentation](./arguments.md) or type --help for assistance. diff --git a/pyproject.toml b/pyproject.toml index 7e70bb4..8b52b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "siegma" version = "0.2.0" description = "This project aims to automate the creation of SIEM rule consumables by leveraging a pre-defined set of configurations/mappings and by utilizing the Sigma rule format and engine." -authors = ["DiogoBraz ", "heyibrahimkhan ", "wesley587 ", "w0rk3r "] +authors = authors = ["heyibrahimkhan ", "wesley587 "] readme = "README.md" [tool.poetry.dependencies] diff --git a/siegma.py b/siegma.py index d703a09..e2c858c 100644 --- a/siegma.py +++ b/siegma.py @@ -17,7 +17,7 @@ def setup_args() -> argparse.Namespace: parser.add_argument("-c", "--config", required=True, dest="config", help="Config file path. Eg: /path/to/config.json") parser.add_argument("-b", "--backend", choices=Backends._member_names_, required=True, dest="backend", help=f"SIEM backend to perform the conversion. Eg: {'|'.join(Backends._member_names_)}") parser.add_argument("--api", dest="api", action="store_true", default=False, help="Create a rule in the SIEM using the API.") - parser.add_argument('-p', '--path', type=str, required=True, help='Rule file / folder path. This should be either the absolute path from root folder or should be relative to sigma, NOT siegma. Eg: /path/to/rule/file.yml or /path/to/rules/folder.') + parser.add_argument('-p', '--path', type=str, required=True, help='Rule file / folder path. This should be the absolute path from root folder or should be relative to sigma, NOT siegma. Eg: /path/to/rule/file.yml or /path/to/rules/folder.') parser.add_argument("-sc", "--sigma_config", dest="sigma_config_file", type=str, help="Sigma config file path. Eg: /path/to/sigma/tools/config/ecs-cloudtrail.yml.", required=True) parser.add_argument("-o", "--output", dest="output_file", type=str, help="Output file path. Eg: /path/to/output_file.", default=".output.ndjson") parser.add_argument("-v", "--verbosity", dest="verbosity_level", choices=logging_levels, type=str, default="INFO", help=f"Execution verbosity level. Eg: {'|'.join(logging_levels)}") @@ -69,8 +69,8 @@ def main(): logger.info(f"Find the rule {rule_content[0].title}") - logger.debug("Checking rule sintaxe.") - sigma_rule_object.check_rule_sintaxe(rule_content) + logger.debug("Checking rule syntax.") + sigma_rule_object.check_rule_syntax(rule_content) logger.debug("Getting rule query.") query = rule_convertor.convert(rule_content, backend.value) diff --git a/tools/DirectoryTools.py b/tools/DirectoryTools.py index 034d73f..73d2ee1 100644 --- a/tools/DirectoryTools.py +++ b/tools/DirectoryTools.py @@ -2,7 +2,7 @@ class DirectoryTools: """ - Some common functions to manipulate Directories. + Provides common functions to work with directories. """ @staticmethod @@ -23,7 +23,7 @@ def get_file_extension(file_name: str) -> str: @staticmethod def get_files_by_extension(folder_path: str, get_sub_directories: bool = True, extension: str = ".yml") -> list[str]: """ - Get files by a determined extension. + Return files of a determined extension from a given directory. Args: folder_path (str): Directory to look for files. diff --git a/tools/FileTools.py b/tools/FileTools.py index 4e2069a..97c434b 100644 --- a/tools/FileTools.py +++ b/tools/FileTools.py @@ -4,7 +4,7 @@ class FileTools: """ - Some common functions to manipulate files. + Provides common functions to work with files. """ @staticmethod @@ -39,7 +39,7 @@ def file_path(dir_path: str, file_path: str) -> str: @staticmethod def get_dirname(path: str) -> str: """ - Get the directory of a file, pass the full path. + Return the directory of a file. Pass the full file path as an argument. Args: path (str): File path. @@ -53,7 +53,7 @@ def get_dirname(path: str) -> str: @staticmethod def load_json_file(file_path: str) -> dict[str, any]: """ - Load a json file. + Loads a given JSON file.. Args: file_path (str): File path to be loaded. @@ -70,7 +70,7 @@ def load_json_file(file_path: str) -> dict[str, any]: @staticmethod def get_file_extension_name(file_path: str) -> str: """ - Collect the file extensions name. + Returns the extension of a given file. Args: file_path (str): File path. @@ -84,7 +84,7 @@ def get_file_extension_name(file_path: str) -> str: @staticmethod def check_if_is_a_file(path: str) -> bool: """ - Check if a path is a valid file. + Check if a file exists by validating its full path. Args: path (str): Full path. diff --git a/tools/SigmaUtils.py b/tools/SigmaUtils.py index 5a90040..392093b 100644 --- a/tools/SigmaUtils.py +++ b/tools/SigmaUtils.py @@ -3,7 +3,7 @@ from sigma.collection import SigmaCollection from sigma.validation import SigmaValidator from sigma.configuration import SigmaConfiguration -from Exceptions import RuleSintaxeError +from Exceptions import RuleSyntaxError def get_sigma_configuration(sigma_config_file_path : str) -> SigmaConfiguration: @@ -49,15 +49,15 @@ def get_yml_file_content(self) -> SigmaCollection: return rule - def check_rule_sintaxe(self, rule: SigmaCollection) -> None: + def check_rule_syntax(self, rule: SigmaCollection) -> None: """ - Check if the rule sintaxe is satisfied. + Check if the rule syntax is satisfied. Args: rule (SigmaCollection): Loaded sigma rule object. Raises: - RuleSintaxeError: Rule sintaxe error exception. + RuleSyntaxError: Rule sintaxe error exception. """ rule_validator = SigmaValidator(validators={AllOfThemConditionValidator}) @@ -65,5 +65,5 @@ def check_rule_sintaxe(self, rule: SigmaCollection) -> None: issues = rule_validator.validate_rules(rule) if len(issues) > 0: - raise RuleSintaxeError(f"Issues in the rule sintaxe. Errors: {issues}") + raise RuleSyntaxError(f"Issues in the rule sintaxe. Errors: {issues}") From 9f165e3e8f43afff8ce85da652ff123a0c660cdf Mon Sep 17 00:00:00 2001 From: Wesley Cesar <72465364+wesley587@users.noreply.github.com> Date: Sat, 20 May 2023 15:55:19 -0300 Subject: [PATCH 8/9] Update output.md --- docs/output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/output.md b/docs/output.md index 6fb9008..36b3eaa 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,3 +1,3 @@ -# # Output Folder +# Output The output folder contains logs of the execution and SIEM rule files ready for upload. These files are generated during the SIEGMA execution and can be directly utilized for uploading rules to the relevant SIEM platform. If you wish to modify the file names, you can do so by passing appropriate arguments. For more details, please look at the [arguments documentation](./arguments.md) or type --help for assistance. From edd61ed819afb90963916d39912db415d56889b9 Mon Sep 17 00:00:00 2001 From: Ibrahim Ali Khan Date: Tue, 23 May 2023 23:16:35 +0500 Subject: [PATCH 9/9] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 53e883b..8a3759e 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,10 @@ Feel free to open an [issue](https://github.com/3CORESec/SIEGMA/issues) or ping ### Authors -- [heyibrahimkhan](https://github.com/heyibrahimkhan) -- [wesley587](https://github.com/wesley587) +- [heyibrahimkhan](https://github.com/heyibrahimhan) ### Contributors - - [DiogoBraz](https://github.com/DiogoBraz) - [w0rk3r](https://github.com/w0rk3r) -- [Tiago Faria](https://github.com/0xtf) \ No newline at end of file +- [Tiago Faria](https://github.com/0xtf) +- [wesley587](https://github.com/wesley587)