├── .git-blame-ignore-revs ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── conftest.py ├── poetry.lock ├── print-coverage.py ├── pyproject.toml ├── sigma ├── backends │ └── splunk │ │ ├── __init__.py │ │ └── splunk.py └── pipelines │ └── splunk │ ├── __init__.py │ └── splunk.py └── tests ├── test_backend_splunk.py ├── test_backend_splunk_correlations.py └── test_splunk_pipelines.py /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Reformatting with black 2 | cc2e353b04c86a26b0bab1fea3b9ddff6a1568fa -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [thomaspatzke] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | push: 6 | tags: 7 | - v*.*.* 8 | 9 | jobs: 10 | build-and-publish: 11 | runs-on: ubuntu-20.04 12 | environment: release 13 | permissions: 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install Poetry 18 | run: pipx install poetry 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.13' 23 | - name: Verify versioning 24 | run: | 25 | [ "$(poetry version -s)" == "${GITHUB_REF#refs/tags/v}" ] 26 | - name: Install dependencies 27 | run: poetry install 28 | - name: Run tests 29 | run: poetry run pytest 30 | - name: Build packages 31 | run: poetry build 32 | - name: Publish to test PyPI 33 | if: ${{ github.event_name == 'push' }} 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | repository-url: https://test.pypi.org/legacy/ 37 | - name: Publish to PyPI 38 | if: ${{ github.event_name == 'release' }} 39 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ 'ubuntu-22.04' ] 14 | python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install Poetry 19 | run: pipx install poetry 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: poetry install 26 | - name: Run tests 27 | run: poetry run pytest --cov=sigma --cov-report term --cov-report xml:cov.xml -vv 28 | - name: Store coverage for badge 29 | if: ${{ runner.os == 'Linux' }} 30 | run: poetry run python print-coverage.py >> $GITHUB_ENV 31 | - name: Create coverage badge 32 | if: ${{ github.repository == 'SigmaHQ/pySigma-backend-splunk' && github.event_name == 'push' && runner.os == 'Linux' }} 33 | uses: schneegans/dynamic-badges-action@v1.7.0 34 | with: 35 | auth: ${{ secrets.GIST_SECRET }} 36 | gistID: 47c292239759399a6e3c73b0e9656b33 37 | filename: SigmaHQ-pySigma-backend-splunk.json 38 | label: Coverage 39 | message: ${{ env.COVERAGE }} 40 | color: ${{ env.COVERAGE_COLOR }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage* 2 | .vscode/ 3 | **/__pycache__ 4 | .pytest_cache/ 5 | cov.xml 6 | dist/ 7 | docs/_build -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.1.1 4 | hooks: 5 | - id: black 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests](https://github.com/SigmaHQ/pySigma-backend-splunk/actions/workflows/test.yml/badge.svg) 2 | ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/thomaspatzke/47c292239759399a6e3c73b0e9656b33/raw/SigmaHQ-pySigma-backend-splunk.json) 3 | ![Status](https://img.shields.io/badge/Status-pre--release-orange) 4 | 5 | # pySigma Splunk Backend 6 | 7 | This is the Splunk backend for pySigma. It provides the package `sigma.backends.splunk` with the `SplunkBackend` class. 8 | Further, it contains the following processing pipelines in `sigma.pipelines.splunk`: 9 | 10 | * splunk_windows_pipeline: Splunk Windows log support 11 | * splunk_windows_sysmon_acceleration_keywords: Adds fiels name keyword search terms to generated query to accelerate search. 12 | 13 | It supports the following output formats: 14 | 15 | * default: plain Splunk queries 16 | * savedsearches: Splunk savedsearches.conf format. 17 | 18 | This backend is currently maintained by: 19 | 20 | * [Thomas Patzke](https://github.com/thomaspatzke/) -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SigmaHQ/pySigma-backend-splunk/1e6b5d7839f74c341699fd6dc90f04a3d722cfb2/conftest.py -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.10.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 11 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 12 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 13 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 14 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 15 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 16 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 17 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 18 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 19 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 20 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 21 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 22 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 23 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 24 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 25 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 26 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 27 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 28 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 29 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 30 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 31 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 32 | ] 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | packaging = ">=22.0" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 41 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 42 | 43 | [package.extras] 44 | colorama = ["colorama (>=0.4.3)"] 45 | d = ["aiohttp (>=3.10)"] 46 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 47 | uvloop = ["uvloop (>=0.15.2)"] 48 | 49 | [[package]] 50 | name = "certifi" 51 | version = "2024.12.14" 52 | description = "Python package for providing Mozilla's CA Bundle." 53 | optional = false 54 | python-versions = ">=3.6" 55 | files = [ 56 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 57 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 58 | ] 59 | 60 | [[package]] 61 | name = "cfgv" 62 | version = "3.4.0" 63 | description = "Validate configuration and produce human readable error messages." 64 | optional = false 65 | python-versions = ">=3.8" 66 | files = [ 67 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 68 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 69 | ] 70 | 71 | [[package]] 72 | name = "charset-normalizer" 73 | version = "3.4.1" 74 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 75 | optional = false 76 | python-versions = ">=3.7" 77 | files = [ 78 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 79 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 80 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 81 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 82 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 83 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 84 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 85 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 86 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 87 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 88 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 89 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 90 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 91 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 92 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 93 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 94 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 95 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 96 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 97 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 98 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 99 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 100 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 101 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 102 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 103 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 104 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 105 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 106 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 107 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 108 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 109 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 110 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 111 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 112 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 113 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 114 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 115 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 116 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 117 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 118 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 119 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 120 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 121 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 122 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 123 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 124 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 125 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 126 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 127 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 128 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 129 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 130 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 131 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 132 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 133 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 134 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 135 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 136 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 137 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 138 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 139 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 140 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 141 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 142 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 143 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 144 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 145 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 146 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 147 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 148 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 149 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 150 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 151 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 152 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 153 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 154 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 155 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 156 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 157 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 158 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 159 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 160 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 161 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 162 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 163 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 164 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 165 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 166 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 167 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 168 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 169 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 170 | ] 171 | 172 | [[package]] 173 | name = "click" 174 | version = "8.1.8" 175 | description = "Composable command line interface toolkit" 176 | optional = false 177 | python-versions = ">=3.7" 178 | files = [ 179 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 180 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 181 | ] 182 | 183 | [package.dependencies] 184 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 185 | 186 | [[package]] 187 | name = "colorama" 188 | version = "0.4.6" 189 | description = "Cross-platform colored terminal text." 190 | optional = false 191 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 192 | files = [ 193 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 194 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 195 | ] 196 | 197 | [[package]] 198 | name = "coverage" 199 | version = "7.6.10" 200 | description = "Code coverage measurement for Python" 201 | optional = false 202 | python-versions = ">=3.9" 203 | files = [ 204 | {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, 205 | {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, 206 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, 207 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, 208 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, 209 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, 210 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, 211 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, 212 | {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, 213 | {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, 214 | {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, 215 | {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, 216 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, 217 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, 218 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, 219 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, 220 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, 221 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, 222 | {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, 223 | {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, 224 | {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, 225 | {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, 226 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, 227 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, 228 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, 229 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, 230 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, 231 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, 232 | {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, 233 | {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, 234 | {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, 235 | {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, 236 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, 237 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, 238 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, 239 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, 240 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, 241 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, 242 | {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, 243 | {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, 244 | {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, 245 | {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, 246 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, 247 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, 248 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, 249 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, 250 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, 251 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, 252 | {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, 253 | {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, 254 | {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, 255 | {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, 256 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, 257 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, 258 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, 259 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, 260 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, 261 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, 262 | {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, 263 | {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, 264 | {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, 265 | {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, 266 | ] 267 | 268 | [package.dependencies] 269 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 270 | 271 | [package.extras] 272 | toml = ["tomli"] 273 | 274 | [[package]] 275 | name = "defusedxml" 276 | version = "0.7.1" 277 | description = "XML bomb protection for Python stdlib modules" 278 | optional = false 279 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 280 | files = [ 281 | {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, 282 | {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, 283 | ] 284 | 285 | [[package]] 286 | name = "distlib" 287 | version = "0.3.9" 288 | description = "Distribution utilities" 289 | optional = false 290 | python-versions = "*" 291 | files = [ 292 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 293 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 294 | ] 295 | 296 | [[package]] 297 | name = "exceptiongroup" 298 | version = "1.2.2" 299 | description = "Backport of PEP 654 (exception groups)" 300 | optional = false 301 | python-versions = ">=3.7" 302 | files = [ 303 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 304 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 305 | ] 306 | 307 | [package.extras] 308 | test = ["pytest (>=6)"] 309 | 310 | [[package]] 311 | name = "filelock" 312 | version = "3.17.0" 313 | description = "A platform independent file lock." 314 | optional = false 315 | python-versions = ">=3.9" 316 | files = [ 317 | {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, 318 | {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, 319 | ] 320 | 321 | [package.extras] 322 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 323 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 324 | typing = ["typing-extensions (>=4.12.2)"] 325 | 326 | [[package]] 327 | name = "identify" 328 | version = "2.6.6" 329 | description = "File identification library for Python" 330 | optional = false 331 | python-versions = ">=3.9" 332 | files = [ 333 | {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, 334 | {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, 335 | ] 336 | 337 | [package.extras] 338 | license = ["ukkonen"] 339 | 340 | [[package]] 341 | name = "idna" 342 | version = "3.10" 343 | description = "Internationalized Domain Names in Applications (IDNA)" 344 | optional = false 345 | python-versions = ">=3.6" 346 | files = [ 347 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 348 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 349 | ] 350 | 351 | [package.extras] 352 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 353 | 354 | [[package]] 355 | name = "iniconfig" 356 | version = "2.0.0" 357 | description = "brain-dead simple config-ini parsing" 358 | optional = false 359 | python-versions = ">=3.7" 360 | files = [ 361 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 362 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 363 | ] 364 | 365 | [[package]] 366 | name = "jinja2" 367 | version = "3.1.5" 368 | description = "A very fast and expressive template engine." 369 | optional = false 370 | python-versions = ">=3.7" 371 | files = [ 372 | {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, 373 | {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, 374 | ] 375 | 376 | [package.dependencies] 377 | MarkupSafe = ">=2.0" 378 | 379 | [package.extras] 380 | i18n = ["Babel (>=2.7)"] 381 | 382 | [[package]] 383 | name = "markupsafe" 384 | version = "3.0.2" 385 | description = "Safely add untrusted strings to HTML/XML markup." 386 | optional = false 387 | python-versions = ">=3.9" 388 | files = [ 389 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 390 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 391 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 392 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 393 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 394 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 395 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 396 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 397 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 398 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 399 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 400 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 401 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 402 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 403 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 404 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 405 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 406 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 407 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 408 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 409 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 410 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 411 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 412 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 413 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 414 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 415 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 416 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 417 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 418 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 419 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 420 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 421 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 422 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 423 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 424 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 425 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 426 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 427 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 428 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 429 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 430 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 431 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 432 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 433 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 434 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 435 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 436 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 437 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 438 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 439 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 440 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 441 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 442 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 443 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 444 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 445 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 446 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 447 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 448 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 449 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 450 | ] 451 | 452 | [[package]] 453 | name = "mypy-extensions" 454 | version = "1.0.0" 455 | description = "Type system extensions for programs checked with the mypy type checker." 456 | optional = false 457 | python-versions = ">=3.5" 458 | files = [ 459 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 460 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 461 | ] 462 | 463 | [[package]] 464 | name = "nodeenv" 465 | version = "1.9.1" 466 | description = "Node.js virtual environment builder" 467 | optional = false 468 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 469 | files = [ 470 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 471 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 472 | ] 473 | 474 | [[package]] 475 | name = "packaging" 476 | version = "24.2" 477 | description = "Core utilities for Python packages" 478 | optional = false 479 | python-versions = ">=3.8" 480 | files = [ 481 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 482 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 483 | ] 484 | 485 | [[package]] 486 | name = "pathspec" 487 | version = "0.12.1" 488 | description = "Utility library for gitignore style pattern matching of file paths." 489 | optional = false 490 | python-versions = ">=3.8" 491 | files = [ 492 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 493 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 494 | ] 495 | 496 | [[package]] 497 | name = "platformdirs" 498 | version = "4.3.6" 499 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 500 | optional = false 501 | python-versions = ">=3.8" 502 | files = [ 503 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 504 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 505 | ] 506 | 507 | [package.extras] 508 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 509 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 510 | type = ["mypy (>=1.11.2)"] 511 | 512 | [[package]] 513 | name = "pluggy" 514 | version = "1.5.0" 515 | description = "plugin and hook calling mechanisms for python" 516 | optional = false 517 | python-versions = ">=3.8" 518 | files = [ 519 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 520 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 521 | ] 522 | 523 | [package.extras] 524 | dev = ["pre-commit", "tox"] 525 | testing = ["pytest", "pytest-benchmark"] 526 | 527 | [[package]] 528 | name = "pre-commit" 529 | version = "3.8.0" 530 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 531 | optional = false 532 | python-versions = ">=3.9" 533 | files = [ 534 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 535 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 536 | ] 537 | 538 | [package.dependencies] 539 | cfgv = ">=2.0.0" 540 | identify = ">=1.0.0" 541 | nodeenv = ">=0.11.1" 542 | pyyaml = ">=5.1" 543 | virtualenv = ">=20.10.0" 544 | 545 | [[package]] 546 | name = "pyparsing" 547 | version = "3.2.1" 548 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 549 | optional = false 550 | python-versions = ">=3.9" 551 | files = [ 552 | {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, 553 | {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, 554 | ] 555 | 556 | [package.extras] 557 | diagrams = ["jinja2", "railroad-diagrams"] 558 | 559 | [[package]] 560 | name = "pysigma" 561 | version = "0.11.19" 562 | description = "Sigma rule processing and conversion tools" 563 | optional = false 564 | python-versions = "<4.0,>=3.9" 565 | files = [ 566 | {file = "pysigma-0.11.19-py3-none-any.whl", hash = "sha256:5e5f669fbb9fdf1e6e2a3f096b5c5c0c167ec333dafa9975e6481f9fb34294b2"}, 567 | {file = "pysigma-0.11.19.tar.gz", hash = "sha256:075df35e56f2128491f8ad501cc063d88d282879d37b50213fc6b5008fac8625"}, 568 | ] 569 | 570 | [package.dependencies] 571 | jinja2 = ">=3.1,<4.0" 572 | packaging = ">=24.1,<25.0" 573 | pyparsing = ">=3.1,<4.0" 574 | pyyaml = ">=6.0,<7.0" 575 | requests = ">=2.31,<3.0" 576 | 577 | [[package]] 578 | name = "pytest" 579 | version = "8.3.4" 580 | description = "pytest: simple powerful testing with Python" 581 | optional = false 582 | python-versions = ">=3.8" 583 | files = [ 584 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 585 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 586 | ] 587 | 588 | [package.dependencies] 589 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 590 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 591 | iniconfig = "*" 592 | packaging = "*" 593 | pluggy = ">=1.5,<2" 594 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 595 | 596 | [package.extras] 597 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 598 | 599 | [[package]] 600 | name = "pytest-cov" 601 | version = "4.1.0" 602 | description = "Pytest plugin for measuring coverage." 603 | optional = false 604 | python-versions = ">=3.7" 605 | files = [ 606 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 607 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 608 | ] 609 | 610 | [package.dependencies] 611 | coverage = {version = ">=5.2.1", extras = ["toml"]} 612 | pytest = ">=4.6" 613 | 614 | [package.extras] 615 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 616 | 617 | [[package]] 618 | name = "pyyaml" 619 | version = "6.0.2" 620 | description = "YAML parser and emitter for Python" 621 | optional = false 622 | python-versions = ">=3.8" 623 | files = [ 624 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 625 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 626 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 627 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 628 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 629 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 630 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 631 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 632 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 633 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 634 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 635 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 636 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 637 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 638 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 639 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 640 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 641 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 642 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 643 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 644 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 645 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 646 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 647 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 648 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 649 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 650 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 651 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 652 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 653 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 654 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 655 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 656 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 657 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 658 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 659 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 660 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 661 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 662 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 663 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 664 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 665 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 666 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 667 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 668 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 669 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 670 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 671 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 672 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 673 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 674 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 675 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 676 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 677 | ] 678 | 679 | [[package]] 680 | name = "requests" 681 | version = "2.32.3" 682 | description = "Python HTTP for Humans." 683 | optional = false 684 | python-versions = ">=3.8" 685 | files = [ 686 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 687 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 688 | ] 689 | 690 | [package.dependencies] 691 | certifi = ">=2017.4.17" 692 | charset-normalizer = ">=2,<4" 693 | idna = ">=2.5,<4" 694 | urllib3 = ">=1.21.1,<3" 695 | 696 | [package.extras] 697 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 698 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 699 | 700 | [[package]] 701 | name = "tomli" 702 | version = "2.2.1" 703 | description = "A lil' TOML parser" 704 | optional = false 705 | python-versions = ">=3.8" 706 | files = [ 707 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 708 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 709 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 710 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 711 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 712 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 713 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 714 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 715 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 716 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 717 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 718 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 719 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 720 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 721 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 722 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 723 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 724 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 725 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 726 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 727 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 728 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 729 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 730 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 731 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 732 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 733 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 734 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 735 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 736 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 737 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 738 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 739 | ] 740 | 741 | [[package]] 742 | name = "typing-extensions" 743 | version = "4.12.2" 744 | description = "Backported and Experimental Type Hints for Python 3.8+" 745 | optional = false 746 | python-versions = ">=3.8" 747 | files = [ 748 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 749 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 750 | ] 751 | 752 | [[package]] 753 | name = "urllib3" 754 | version = "2.3.0" 755 | description = "HTTP library with thread-safe connection pooling, file post, and more." 756 | optional = false 757 | python-versions = ">=3.9" 758 | files = [ 759 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 760 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 761 | ] 762 | 763 | [package.extras] 764 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 765 | h2 = ["h2 (>=4,<5)"] 766 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 767 | zstd = ["zstandard (>=0.18.0)"] 768 | 769 | [[package]] 770 | name = "virtualenv" 771 | version = "20.29.1" 772 | description = "Virtual Python Environment builder" 773 | optional = false 774 | python-versions = ">=3.8" 775 | files = [ 776 | {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, 777 | {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, 778 | ] 779 | 780 | [package.dependencies] 781 | distlib = ">=0.3.7,<1" 782 | filelock = ">=3.12.2,<4" 783 | platformdirs = ">=3.9.1,<5" 784 | 785 | [package.extras] 786 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 787 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 788 | 789 | [metadata] 790 | lock-version = "2.0" 791 | python-versions = "^3.9" 792 | content-hash = "086dc5f2a0b082395ae5d85c5374f3a1765eb6e23eaf9b963d243f4e8d63f735" 793 | -------------------------------------------------------------------------------- /print-coverage.py: -------------------------------------------------------------------------------- 1 | # Prints code testing coverage as percentage for badge generation. 2 | from defusedxml.ElementTree import parse 3 | 4 | tree = parse("cov.xml") 5 | root = tree.getroot() 6 | coverage = float(root.attrib["line-rate"]) * 100 7 | print(f"COVERAGE={coverage:3.4}%") 8 | if coverage >= 95.0: 9 | print("COVERAGE_COLOR=green") 10 | elif coverage >= 90.0: 11 | print("COVERAGE_COLOR=yellow") 12 | elif coverage >= 85.0: 13 | print("COVERAGE_COLOR=orange") 14 | else: 15 | print("COVERAGE_COLOR=red") 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pysigma-backend-splunk" 3 | version = "1.1.3" 4 | description = "pySigma Splunk backend" 5 | readme = "README.md" 6 | authors = ["Thomas Patzke "] 7 | license = "LGPL-2.1-only" 8 | repository = "https://github.com/SigmaHQ/pySigma-backend-splunk" 9 | packages = [ 10 | { include = "sigma" } 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.9" 15 | pysigma = "^0.11.18" 16 | 17 | [tool.poetry.dev-dependencies] 18 | black = "^24.1" 19 | pre-commit = "^3.5" 20 | pytest = "^8.0" 21 | pytest-cov = "^4.1" 22 | defusedxml = "^0.7" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.8.1"] 26 | build-backend = "poetry.core.masonry.api" 27 | -------------------------------------------------------------------------------- /sigma/backends/splunk/__init__.py: -------------------------------------------------------------------------------- 1 | from .splunk import SplunkBackend 2 | 3 | backends = { 4 | "splunk": SplunkBackend, 5 | } 6 | -------------------------------------------------------------------------------- /sigma/backends/splunk/splunk.py: -------------------------------------------------------------------------------- 1 | import re 2 | from sigma.conversion.state import ConversionState 3 | from sigma.modifiers import SigmaRegularExpression 4 | from sigma.rule import SigmaRule, SigmaDetection 5 | from sigma.conversion.base import TextQueryBackend, DeferredQueryExpression 6 | from sigma.conversion.deferred import DeferredTextQueryExpression 7 | from sigma.conditions import ( 8 | ConditionFieldEqualsValueExpression, 9 | ConditionOR, 10 | ConditionAND, 11 | ConditionNOT, 12 | ConditionItem, 13 | ) 14 | from sigma.types import SigmaCompareExpression, SigmaString 15 | from sigma.exceptions import SigmaFeatureNotSupportedByBackendError, SigmaError 16 | from sigma.pipelines.splunk.splunk import ( 17 | splunk_sysmon_process_creation_cim_mapping, 18 | splunk_windows_registry_cim_mapping, 19 | splunk_windows_file_event_cim_mapping, 20 | splunk_web_proxy_cim_mapping, 21 | ) 22 | import sigma 23 | from typing import Any, Callable, ClassVar, Dict, List, Optional, Pattern, Tuple, Union 24 | 25 | 26 | class SplunkDeferredRegularExpression(DeferredTextQueryExpression): 27 | template = 'regex {field}{op}"{value}"' 28 | operators = { 29 | True: "!=", 30 | False: "=", 31 | } 32 | default_field = "_raw" 33 | 34 | 35 | class SplunkDeferredORRegularExpression(DeferredTextQueryExpression): 36 | field_counts = {} 37 | default_field = "_raw" 38 | operators = { 39 | True: "!=", 40 | False: "=", 41 | } 42 | 43 | def __init__(self, state, field, arg) -> None: 44 | self.add_field(field) 45 | field_condition = self.get_field_condition(field) 46 | field_match = self.get_field_match(field) 47 | self.template = 'rex field={{field}} "(?<{field_match}>{{value}})"\n| eval {field_condition}=if(isnotnull({field_match}), "true", "false")'.format( 48 | field_match=field_match, field_condition=field_condition 49 | ) 50 | return super().__init__(state, field, arg) 51 | 52 | @staticmethod 53 | def clean_field(field): 54 | # splunk does not allow dots in regex group, so we need to clean variables 55 | return re.sub(".*\\.", "", field) 56 | 57 | @classmethod 58 | def add_field(cls, field): 59 | cls.field_counts[field] = ( 60 | cls.field_counts.get(field, 0) + 1 61 | ) # increment the field count 62 | 63 | @classmethod 64 | def get_field_suffix(cls, field): 65 | index_suffix = cls.field_counts.get(field, "") 66 | if index_suffix == 1: 67 | index_suffix = "" 68 | return index_suffix 69 | 70 | @classmethod 71 | def construct_field_variable(cls, field, variable): 72 | cleaned_field = cls.clean_field(field) 73 | index_suffix = cls.get_field_suffix(field) 74 | return f"{cleaned_field}{variable}{index_suffix}" 75 | 76 | @classmethod 77 | def get_field_match(cls, field): 78 | return cls.construct_field_variable(field, "Match") 79 | 80 | @classmethod 81 | def get_field_condition(cls, field): 82 | return cls.construct_field_variable(field, "Condition") 83 | 84 | @classmethod 85 | def reset(cls): 86 | cls.field_counts = {} 87 | 88 | 89 | class SplunkDeferredFieldRefExpression(DeferredTextQueryExpression): 90 | template = "where {op}'{field}'='{value}'" 91 | operators = { 92 | True: "NOT ", 93 | False: "", 94 | } 95 | default_field = "_raw" 96 | 97 | 98 | class SplunkBackend(TextQueryBackend): 99 | """Splunk SPL backend.""" 100 | 101 | name: ClassVar[str] = ( 102 | "Splunk SPL & tstats data model queries" # A descriptive name of the backend 103 | ) 104 | formats: ClassVar[Dict[str, str]] = ( 105 | { # Output formats provided by the backend as name -> description mapping. The name should match to finalize_output_. 106 | "default": "Plain SPL queries", 107 | "savedsearches": "Plain SPL in a savedsearches.conf file", 108 | "data_model": "Data model queries with tstats", 109 | } 110 | ) 111 | requires_pipeline: ClassVar[bool] = ( 112 | True # Does the backend requires that a processing pipeline is provided? 113 | ) 114 | 115 | precedence: ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = ( 116 | ConditionNOT, 117 | ConditionOR, 118 | ConditionAND, 119 | ) 120 | group_expression: ClassVar[str] = "({expr})" 121 | 122 | bool_values = {True: "true", False: "false"} 123 | or_token: ClassVar[str] = "OR" 124 | and_token: ClassVar[str] = " " 125 | not_token: ClassVar[str] = "NOT" 126 | eq_token: ClassVar[str] = "=" 127 | 128 | field_quote: ClassVar[str] = '"' 129 | field_quote_pattern: ClassVar[Pattern] = re.compile(r"^[\w.]+$") 130 | 131 | str_quote: ClassVar[str] = '"' 132 | escape_char: ClassVar[str] = "\\" 133 | wildcard_multi: ClassVar[str] = "*" 134 | wildcard_single: ClassVar[str] = "*" 135 | add_escaped: ClassVar[str] = "\\" 136 | 137 | re_expression: ClassVar[str] = "{regex}" 138 | re_escape_char: ClassVar[str] = "\\" 139 | re_escape: ClassVar[Tuple[str]] = ('"',) 140 | 141 | cidr_expression: ClassVar[str] = '{field}="{value}"' 142 | 143 | compare_op_expression: ClassVar[str] = "{field}{operator}{value}" 144 | compare_operators: ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = { 145 | SigmaCompareExpression.CompareOperators.LT: "<", 146 | SigmaCompareExpression.CompareOperators.LTE: "<=", 147 | SigmaCompareExpression.CompareOperators.GT: ">", 148 | SigmaCompareExpression.CompareOperators.GTE: ">=", 149 | } 150 | 151 | field_equals_field_expression: ClassVar[str] = "{field2}" 152 | field_null_expression: ClassVar[str] = "NOT {field}=*" 153 | 154 | convert_or_as_in: ClassVar[bool] = True 155 | convert_and_as_in: ClassVar[bool] = False 156 | in_expressions_allow_wildcards: ClassVar[bool] = True 157 | field_in_list_expression: ClassVar[str] = "{field} {op} ({list})" 158 | or_in_operator: ClassVar[Optional[str]] = "IN" 159 | list_separator: ClassVar[str] = ", " 160 | field_exists_expression: ClassVar[str] = "{field}=*" 161 | field_not_exists_expression: ClassVar[str] = "NOT {field}=*" 162 | 163 | unbound_value_str_expression: ClassVar[str] = "{value}" 164 | unbound_value_num_expression: ClassVar[str] = "{value}" 165 | unbound_value_re_expression: ClassVar[str] = "{value}" 166 | 167 | deferred_start: ClassVar[str] = "\n| " 168 | deferred_separator: ClassVar[str] = "\n| " 169 | deferred_only_query: ClassVar[str] = "*" 170 | 171 | # Correlations 172 | correlation_methods: ClassVar[Dict[str, str]] = { 173 | "stats": "Correlation using stats command (more efficient, static time window)", 174 | # "transaction": "Correlation using transaction command (less efficient, sliding time window", 175 | } 176 | default_correlation_method: ClassVar[str] = "stats" 177 | default_correlation_query: ClassVar[str] = { 178 | "stats": "{search}\n\n{aggregate}\n\n{condition}" 179 | } 180 | 181 | correlation_search_single_rule_expression: ClassVar[str] = "{query}" 182 | correlation_search_multi_rule_expression: ClassVar[str] = "| multisearch\n{queries}" 183 | correlation_search_multi_rule_query_expression: ClassVar[str] = ( 184 | '[ search {query} | eval event_type="{ruleid}"{normalization} ]' 185 | ) 186 | correlation_search_multi_rule_query_expression_joiner: ClassVar[str] = "\n" 187 | 188 | correlation_search_field_normalization_expression: ClassVar[str] = ( 189 | " | rename {field} as {alias}" 190 | ) 191 | correlation_search_field_normalization_expression_joiner: ClassVar[str] = "" 192 | 193 | event_count_aggregation_expression: ClassVar[Dict[str, str]] = { 194 | "stats": "| bin _time span={timespan}\n| stats count as event_count by _time{groupby}", 195 | } 196 | value_count_aggregation_expression: ClassVar[Dict[str, str]] = { 197 | "stats": "| bin _time span={timespan}\n| stats dc({field}) as value_count by _time{groupby}", 198 | } 199 | temporal_aggregation_expression: ClassVar[Dict[str, str]] = { 200 | "stats": "| bin _time span={timespan}\n| stats dc(event_type) as event_type_count by _time{groupby}", 201 | } 202 | 203 | timespan_mapping: ClassVar[Dict[str, str]] = { 204 | "M": "mon", 205 | } 206 | 207 | groupby_expression: ClassVar[Dict[str, str]] = {"stats": " {fields}"} 208 | groupby_field_expression: ClassVar[Dict[str, str]] = {"stats": "{field}"} 209 | groupby_field_expression_joiner: ClassVar[Dict[str, str]] = {"stats": " "} 210 | 211 | event_count_condition_expression: ClassVar[Dict[str, str]] = { 212 | "stats": "| search event_count {op} {count}" 213 | } 214 | value_count_condition_expression: ClassVar[Dict[str, str]] = { 215 | "stats": "| search value_count {op} {count}" 216 | } 217 | temporal_condition_expression: ClassVar[Dict[str, str]] = { 218 | "stats": "| search event_type_count {op} {count}" 219 | } 220 | 221 | def __init__( 222 | self, 223 | processing_pipeline: Optional[ 224 | "sigma.processing.pipeline.ProcessingPipeline" 225 | ] = None, 226 | collect_errors: bool = False, 227 | min_time: str = "-30d", 228 | max_time: str = "now", 229 | query_settings: Callable[[SigmaRule], Dict[str, str]] = lambda x: {}, 230 | output_settings: Dict = {}, 231 | **kwargs, 232 | ): 233 | super().__init__(processing_pipeline, collect_errors, **kwargs) 234 | self.query_settings = query_settings 235 | self.output_settings = { 236 | "dispatch.earliest_time": min_time, 237 | "dispatch.latest_time": max_time, 238 | } 239 | self.output_settings.update(output_settings) 240 | 241 | @staticmethod 242 | def _generate_settings(settings): 243 | """Format a settings dict into newline separated k=v string. Escape multi-line values.""" 244 | output = "" 245 | for k, v in settings.items(): 246 | output += f"\n{k} = " + " \\\n".join( 247 | v.split("\n") 248 | ) # cannot use \ in f-strings 249 | return output 250 | 251 | def convert_condition_field_eq_val_re( 252 | self, 253 | cond: ConditionFieldEqualsValueExpression, 254 | state: "sigma.conversion.state.ConversionState", 255 | ) -> SplunkDeferredRegularExpression: 256 | """Defer regular expression matching to pipelined regex command after main search expression.""" 257 | 258 | if cond.parent_condition_chain_contains(ConditionOR): 259 | # adding the deferred to the state 260 | SplunkDeferredORRegularExpression( 261 | state, 262 | cond.field, 263 | super().convert_condition_field_eq_val_re(cond, state), 264 | ).postprocess(None, cond) 265 | 266 | cond_true = ConditionFieldEqualsValueExpression( 267 | SplunkDeferredORRegularExpression.get_field_condition(cond.field), 268 | SigmaString("true"), 269 | ) 270 | # returning fieldX=true 271 | return super().convert_condition_field_eq_val_str(cond_true, state) 272 | return SplunkDeferredRegularExpression( 273 | state, cond.field, super().convert_condition_field_eq_val_re(cond, state) 274 | ).postprocess(None, cond) 275 | 276 | def convert_condition_field_eq_field( 277 | self, 278 | cond: ConditionFieldEqualsValueExpression, 279 | state: "sigma.conversion.state.ConversionState", 280 | ) -> SplunkDeferredFieldRefExpression: 281 | """Defer FieldRef matching to pipelined with `where` command after main search expression.""" 282 | if cond.parent_condition_chain_contains(ConditionOR): 283 | raise SigmaFeatureNotSupportedByBackendError( 284 | "ORing FieldRef matching is not yet supported by Splunk backend", 285 | source=cond.source, 286 | ) 287 | return SplunkDeferredFieldRefExpression( 288 | state, cond.field, super().convert_condition_field_eq_field(cond, state) 289 | ).postprocess(None, cond) 290 | 291 | def finalize_query( 292 | self, 293 | rule: SigmaRule, 294 | query: Union[str, DeferredQueryExpression], 295 | index: int, 296 | state: ConversionState, 297 | output_format: str, 298 | ) -> Union[str, DeferredQueryExpression]: 299 | 300 | if state.has_deferred(): 301 | deferred_regex_or_expressions = [] 302 | no_regex_oring_deferred_expressions = [] 303 | 304 | for index, deferred_expression in enumerate(state.deferred): 305 | 306 | if type(deferred_expression) == SplunkDeferredORRegularExpression: 307 | deferred_regex_or_expressions.append( 308 | deferred_expression.finalize_expression() 309 | ) 310 | else: 311 | no_regex_oring_deferred_expressions.append(deferred_expression) 312 | 313 | if len(deferred_regex_or_expressions) > 0: 314 | SplunkDeferredORRegularExpression.reset() # need to reset class for potential future conversions 315 | # remove deferred oring regex expressions from the state 316 | # as they will be taken into account by the super().finalize_query 317 | state.deferred = no_regex_oring_deferred_expressions 318 | 319 | return super().finalize_query( 320 | rule, 321 | self.deferred_start 322 | + self.deferred_separator.join(deferred_regex_or_expressions) 323 | + "\n| search " 324 | + query, 325 | index, 326 | state, 327 | output_format, 328 | ) 329 | 330 | return super().finalize_query(rule, query, index, state, output_format) 331 | 332 | def finalize_query_default( 333 | self, rule: SigmaRule, query: str, index: int, state: ConversionState 334 | ) -> str: 335 | table_fields = " | table " + ",".join(rule.fields) if rule.fields else "" 336 | return query + table_fields 337 | 338 | def finalize_query_savedsearches( 339 | self, rule: SigmaRule, query: str, index: int, state: ConversionState 340 | ) -> str: 341 | clean_title = rule.title.translate( 342 | {ord(c): None for c in "[]"} 343 | ) # remove brackets from title 344 | query_settings = self.query_settings(rule) 345 | query_settings["description"] = ( 346 | rule.description.strip() if rule.description else "" 347 | ) 348 | query_settings["search"] = query + ( 349 | "\n| table " + ",".join(rule.fields) if rule.fields else "" 350 | ) 351 | 352 | return f"\n[{clean_title}]" + self._generate_settings(query_settings) 353 | 354 | def finalize_output_savedsearches(self, queries: List[str]) -> str: 355 | return ( 356 | f"\n[default]" 357 | + self._generate_settings(self.output_settings) 358 | + "\n" 359 | + "\n".join(queries) 360 | ) 361 | 362 | def finalize_query_data_model( 363 | self, rule: SigmaRule, query: str, index: int, state: ConversionState 364 | ) -> str: 365 | data_model = None 366 | data_set = None 367 | cim_fields = None 368 | if rule.logsource.product and rule.logsource.category: 369 | if rule.logsource.product == "windows": 370 | if rule.logsource.category == "process_creation": 371 | data_model = "Endpoint" 372 | data_set = "Processes" 373 | cim_fields = " ".join( 374 | splunk_sysmon_process_creation_cim_mapping.values() 375 | ) 376 | elif rule.logsource.category in [ 377 | "registry_add", 378 | "registry_delete", 379 | "registry_event", 380 | "registry_set", 381 | ]: 382 | data_model = "Endpoint" 383 | data_set = "Registry" 384 | cim_fields = " ".join(splunk_windows_registry_cim_mapping.values()) 385 | elif rule.logsource.category == "file_event": 386 | data_model = "Endpoint" 387 | data_set = "Filesystem" 388 | cim_fields = " ".join( 389 | splunk_windows_file_event_cim_mapping.values() 390 | ) 391 | elif rule.logsource.product == "linux": 392 | if rule.logsource.category == "process_creation": 393 | data_model = "Endpoint" 394 | data_set = "Processes" 395 | cim_fields = " ".join( 396 | splunk_sysmon_process_creation_cim_mapping.values() 397 | ) 398 | 399 | elif rule.logsource.category == "proxy": 400 | data_model = "Web" 401 | data_set = "Proxy" 402 | cim_fields = " ".join(splunk_web_proxy_cim_mapping.values()) 403 | 404 | try: 405 | data_model_set = state.processing_state["data_model_set"] 406 | except KeyError: 407 | raise SigmaFeatureNotSupportedByBackendError( 408 | "No data model specified by processing pipeline" 409 | ) 410 | 411 | if not data_model_set: 412 | raise SigmaFeatureNotSupportedByBackendError( 413 | "No data set specified by processing pipeline" 414 | ) 415 | 416 | if "." in data_model_set: 417 | parts = data_model_set.split(".") 418 | if len(parts) != 2 or not all(parts): 419 | raise SigmaFeatureNotSupportedByBackendError( 420 | "Expected format 'data_model.data_set', but got: {}".format(data_model_set) 421 | ) 422 | data_set = parts[1] 423 | 424 | try: 425 | fields = " ".join(state.processing_state["fields"]) 426 | except KeyError: 427 | raise SigmaFeatureNotSupportedByBackendError( 428 | "No fields specified by processing pipeline" 429 | ) 430 | 431 | return f"""| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel={data_model_set} where {query} by {fields} 432 | | `drop_dm_object_name({data_set})` 433 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 434 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 435 | """.replace( 436 | "\n", " " 437 | ) 438 | 439 | def finalize_output_data_model(self, queries: List[str]) -> List[str]: 440 | return queries 441 | -------------------------------------------------------------------------------- /sigma/pipelines/splunk/__init__.py: -------------------------------------------------------------------------------- 1 | from .splunk import ( 2 | splunk_windows_pipeline, 3 | splunk_windows_sysmon_acceleration_keywords, 4 | splunk_cim_data_model, 5 | ) 6 | 7 | pipelines = { 8 | "splunk_windows": splunk_windows_pipeline, 9 | "splunk_sysmon_acceleration": splunk_windows_sysmon_acceleration_keywords, 10 | "splunk_cim": splunk_cim_data_model, 11 | } 12 | -------------------------------------------------------------------------------- /sigma/pipelines/splunk/splunk.py: -------------------------------------------------------------------------------- 1 | from sigma.pipelines.common import ( 2 | logsource_windows, 3 | logsource_windows_process_creation, 4 | logsource_windows_registry_add, 5 | logsource_windows_registry_delete, 6 | logsource_windows_registry_event, 7 | logsource_windows_registry_set, 8 | logsource_windows_file_event, 9 | logsource_linux_process_creation, 10 | generate_windows_logsource_items, 11 | ) 12 | from sigma.processing.transformations import ( 13 | AddConditionTransformation, 14 | FieldMappingTransformation, 15 | DetectionItemFailureTransformation, 16 | RuleFailureTransformation, 17 | SetStateTransformation, 18 | ) 19 | from sigma.processing.conditions import ( 20 | LogsourceCondition, 21 | ExcludeFieldCondition, 22 | RuleProcessingItemAppliedCondition, 23 | ) 24 | from sigma.processing.pipeline import ProcessingItem, ProcessingPipeline 25 | 26 | windows_sysmon_acceleration_keywords = { # Map Sysmon event sources and keywords that are added to search for Sysmon optimization pipeline 27 | "process_creation": "ParentProcessGuid", 28 | "file_event": "TargetFilename", 29 | } 30 | 31 | splunk_sysmon_process_creation_cim_mapping = { 32 | "CommandLine": "Processes.process", 33 | "Computer": "Processes.dest", 34 | "CurrentDirectory": "Processes.process_current_directory", 35 | "Image": "Processes.process_path", 36 | "IntegrityLevel": "Processes.process_integrity_level", 37 | "OriginalFileName": "Processes.original_file_name", 38 | "ParentCommandLine": "Processes.parent_process", 39 | "ParentImage": "Processes.parent_process_path", 40 | "ParentProcessGuid": "Processes.parent_process_guid", 41 | "ParentProcessId": "Processes.parent_process_id", 42 | "ProcessGuid": "Processes.process_guid", 43 | "ProcessId": "Processes.process_id", 44 | "User": "Processes.user", 45 | } 46 | 47 | splunk_windows_registry_cim_mapping = { 48 | "Computer": "Registry.dest", 49 | "Details": "Registry.registry_value_data", 50 | "EventType": "Registry.action", # EventType: DeleteKey is parsed to action: deleted 51 | "Image": "Registry.process_path", 52 | "ProcessGuid": "Registry.process_guid", 53 | "ProcessId": "Registry.process_id", 54 | "TargetObject": "Registry.registry_key_name", 55 | } 56 | 57 | splunk_windows_file_event_cim_mapping = { 58 | "Computer": "Filesystem.dest", 59 | "CreationUtcTime": "Filesystem.file_create_time", 60 | "Image": "Filesystem.process_path", 61 | "ProcessGuid": "Filesystem.process_guid", 62 | "ProcessId": "Filesystem.process_id", 63 | "TargetFilename": "Filesystem.file_path", 64 | } 65 | 66 | splunk_web_proxy_cim_mapping = { 67 | "c-uri": "Web.url", 68 | "c-uri-query": "Web.uri_query", 69 | "c-uri-stem": "Web.uri_path", 70 | "c-useragent": "Web.http_user_agent", 71 | "cs-method": "Web.http_method", 72 | "cs-host": "Web.dest", 73 | "cs-referrer": "Web.http_referrer", 74 | "src_ip": "Web.src", 75 | "dst_ip": "Web.dest_ip", 76 | } 77 | 78 | def splunk_windows_pipeline(): 79 | return ProcessingPipeline( 80 | name="Splunk Windows log source conditions", 81 | allowed_backends={"splunk"}, 82 | priority=20, 83 | items=generate_windows_logsource_items("source", "WinEventLog:{source}") 84 | + [ 85 | ProcessingItem( # Field mappings 86 | identifier="splunk_windows_field_mapping", 87 | transformation=FieldMappingTransformation( 88 | { 89 | "EventID": "EventCode", 90 | } 91 | ), 92 | ) 93 | ], 94 | ) 95 | 96 | 97 | def splunk_windows_sysmon_acceleration_keywords(): 98 | return ProcessingPipeline( 99 | name="Splunk Windows Sysmon search acceleration keywords", 100 | allowed_backends={"splunk"}, 101 | priority=25, 102 | items=[ 103 | ProcessingItem( # Some optimizations searching for characteristic keyword for specific log sources 104 | identifier="splunk_windows_sysmon_process_creation", 105 | transformation=AddConditionTransformation( 106 | { 107 | None: keyword, 108 | } 109 | ), 110 | rule_conditions=[ 111 | LogsourceCondition( 112 | category=sysmon_category, 113 | product="windows", 114 | service="sysmon", 115 | ) 116 | ], 117 | ) 118 | for sysmon_category, keyword in windows_sysmon_acceleration_keywords.items() 119 | ], 120 | ) 121 | 122 | 123 | def splunk_cim_data_model(): 124 | return ProcessingPipeline( 125 | name="Splunk CIM Data Model Mapping", 126 | allowed_backends={"splunk"}, 127 | priority=20, 128 | items=[ 129 | ProcessingItem( 130 | identifier="splunk_dm_mapping_sysmon_process_creation_unsupported_fields", 131 | transformation=DetectionItemFailureTransformation( 132 | "The Splunk Data Model Sigma backend supports only the following fields for process_creation log source: " 133 | + ",".join(splunk_sysmon_process_creation_cim_mapping.keys()) 134 | ), 135 | rule_conditions=[ 136 | logsource_windows_process_creation(), 137 | logsource_linux_process_creation(), 138 | ], 139 | rule_condition_linking=any, 140 | field_name_conditions=[ 141 | ExcludeFieldCondition( 142 | fields=splunk_sysmon_process_creation_cim_mapping.keys() 143 | ) 144 | ], 145 | ), 146 | ProcessingItem( 147 | identifier="splunk_dm_mapping_sysmon_process_creation", 148 | transformation=FieldMappingTransformation( 149 | splunk_sysmon_process_creation_cim_mapping 150 | ), 151 | rule_conditions=[ 152 | logsource_windows_process_creation(), 153 | logsource_linux_process_creation(), 154 | ], 155 | rule_condition_linking=any, 156 | ), 157 | ProcessingItem( 158 | identifier="splunk_dm_fields_sysmon_process_creation", 159 | transformation=SetStateTransformation( 160 | "fields", splunk_sysmon_process_creation_cim_mapping.values() 161 | ), 162 | rule_conditions=[ 163 | logsource_windows_process_creation(), 164 | logsource_linux_process_creation(), 165 | ], 166 | rule_condition_linking=any, 167 | ), 168 | ProcessingItem( 169 | identifier="splunk_dm_sysmon_process_creation_data_model_set", 170 | transformation=SetStateTransformation( 171 | "data_model_set", "Endpoint.Processes" 172 | ), 173 | rule_conditions=[ 174 | logsource_windows_process_creation(), 175 | logsource_linux_process_creation(), 176 | ], 177 | rule_condition_linking=any, 178 | ), 179 | ProcessingItem( 180 | identifier="splunk_dm_mapping_sysmon_registry_unsupported_fields", 181 | transformation=DetectionItemFailureTransformation( 182 | "The Splunk Data Model Sigma backend supports only the following fields for registry log source: " 183 | + ",".join(splunk_windows_registry_cim_mapping.keys()) 184 | ), 185 | rule_conditions=[ 186 | logsource_windows_registry_add(), 187 | logsource_windows_registry_delete(), 188 | logsource_windows_registry_event(), 189 | logsource_windows_registry_set(), 190 | ], 191 | rule_condition_linking=any, 192 | field_name_conditions=[ 193 | ExcludeFieldCondition( 194 | fields=splunk_windows_registry_cim_mapping.keys() 195 | ) 196 | ], 197 | ), 198 | ProcessingItem( 199 | identifier="splunk_dm_mapping_sysmon_registry", 200 | transformation=FieldMappingTransformation( 201 | splunk_windows_registry_cim_mapping 202 | ), 203 | rule_conditions=[ 204 | logsource_windows_registry_add(), 205 | logsource_windows_registry_delete(), 206 | logsource_windows_registry_event(), 207 | logsource_windows_registry_set(), 208 | ], 209 | rule_condition_linking=any, 210 | ), 211 | ProcessingItem( 212 | identifier="splunk_dm_fields_sysmon_registry", 213 | transformation=SetStateTransformation( 214 | "fields", splunk_windows_registry_cim_mapping.values() 215 | ), 216 | rule_conditions=[ 217 | logsource_windows_registry_add(), 218 | logsource_windows_registry_delete(), 219 | logsource_windows_registry_event(), 220 | logsource_windows_registry_set(), 221 | ], 222 | rule_condition_linking=any, 223 | ), 224 | ProcessingItem( 225 | identifier="splunk_dm_sysmon_registry_data_model_set", 226 | transformation=SetStateTransformation( 227 | "data_model_set", "Endpoint.Registry" 228 | ), 229 | rule_conditions=[ 230 | logsource_windows_registry_add(), 231 | logsource_windows_registry_delete(), 232 | logsource_windows_registry_event(), 233 | logsource_windows_registry_set(), 234 | ], 235 | rule_condition_linking=any, 236 | ), 237 | ProcessingItem( 238 | identifier="splunk_dm_mapping_sysmon_file_event_unsupported_fields", 239 | transformation=DetectionItemFailureTransformation( 240 | "The Splunk Data Model Sigma backend supports only the following fields for file_event log source: " 241 | + ",".join(splunk_windows_file_event_cim_mapping.keys()) 242 | ), 243 | rule_conditions=[ 244 | logsource_windows_file_event(), 245 | ], 246 | field_name_conditions=[ 247 | ExcludeFieldCondition( 248 | fields=splunk_windows_file_event_cim_mapping.keys() 249 | ) 250 | ], 251 | ), 252 | ProcessingItem( 253 | identifier="splunk_dm_mapping_sysmon_file_event", 254 | transformation=FieldMappingTransformation( 255 | splunk_windows_file_event_cim_mapping 256 | ), 257 | rule_conditions=[ 258 | logsource_windows_file_event(), 259 | ], 260 | ), 261 | ProcessingItem( 262 | identifier="splunk_dm_fields_sysmon_file_event", 263 | transformation=SetStateTransformation( 264 | "fields", splunk_windows_file_event_cim_mapping.values() 265 | ), 266 | rule_conditions=[ 267 | logsource_windows_file_event(), 268 | ], 269 | ), 270 | ProcessingItem( 271 | identifier="splunk_dm_mapping_sysmon_file_event_data_model_set", 272 | transformation=SetStateTransformation( 273 | "data_model_set", "Endpoint.Filesystem" 274 | ), 275 | rule_conditions=[ 276 | logsource_windows_file_event(), 277 | ], 278 | ), 279 | ProcessingItem( 280 | identifier="splunk_dm_mapping_web_proxy_unsupported_fields", 281 | transformation=DetectionItemFailureTransformation( 282 | "The Splunk Data Model Sigma backend supports only the following fields for web proxy log source: " 283 | + ",".join(splunk_web_proxy_cim_mapping.keys()) 284 | ), 285 | rule_conditions=[ 286 | LogsourceCondition(category="proxy"), 287 | ], 288 | field_name_conditions=[ 289 | ExcludeFieldCondition( 290 | fields=splunk_web_proxy_cim_mapping.keys() 291 | ) 292 | ], 293 | ), 294 | ProcessingItem( 295 | identifier="splunk_dm_mapping_web_proxy", 296 | transformation=FieldMappingTransformation( 297 | splunk_web_proxy_cim_mapping 298 | ), 299 | rule_conditions=[ 300 | LogsourceCondition(category="proxy"), 301 | ], 302 | ), 303 | ProcessingItem( 304 | identifier="splunk_dm_fields_web_proxy", 305 | transformation=SetStateTransformation( 306 | "fields", splunk_web_proxy_cim_mapping.values() 307 | ), 308 | rule_conditions=[ 309 | LogsourceCondition(category="proxy"), 310 | ], 311 | ), 312 | ProcessingItem( 313 | identifier="splunk_dm_mapping_web_proxy_data_model_set", 314 | transformation=SetStateTransformation( 315 | "data_model_set", "Web.Proxy" 316 | ), 317 | rule_conditions=[ 318 | LogsourceCondition(category="proxy"), 319 | ], 320 | ), 321 | ProcessingItem( 322 | identifier="splunk_dm_mapping_log_source_not_supported", 323 | rule_condition_linking=any, 324 | transformation=RuleFailureTransformation( 325 | "Rule type not yet supported by the Splunk data model CIM pipeline!" 326 | ), 327 | rule_condition_negation=True, 328 | rule_conditions=[ 329 | RuleProcessingItemAppliedCondition( 330 | "splunk_dm_mapping_sysmon_process_creation" 331 | ), 332 | RuleProcessingItemAppliedCondition( 333 | "splunk_dm_mapping_sysmon_registry" 334 | ), 335 | RuleProcessingItemAppliedCondition( 336 | "splunk_dm_mapping_sysmon_file_event" 337 | ), 338 | RuleProcessingItemAppliedCondition( 339 | "splunk_dm_mapping_web_proxy" 340 | ), 341 | ], 342 | ), 343 | ], 344 | ) 345 | -------------------------------------------------------------------------------- /tests/test_backend_splunk.py: -------------------------------------------------------------------------------- 1 | from sigma.exceptions import SigmaFeatureNotSupportedByBackendError 2 | import pytest 3 | from sigma.backends.splunk import SplunkBackend 4 | from sigma.collection import SigmaCollection 5 | from sigma.processing.pipeline import ProcessingPipeline 6 | from sigma.pipelines.splunk import splunk_cim_data_model 7 | 8 | 9 | @pytest.fixture 10 | def splunk_backend(): 11 | return SplunkBackend() 12 | 13 | 14 | @pytest.fixture 15 | def splunk_custom_backend(): 16 | return SplunkBackend( 17 | query_settings=lambda x: {"custom.query.key": x.title}, 18 | output_settings={"custom.key": "customvalue"}, 19 | ) 20 | 21 | 22 | def test_splunk_and_expression(splunk_backend: SplunkBackend): 23 | rule = SigmaCollection.from_yaml( 24 | """ 25 | title: Test 26 | status: test 27 | logsource: 28 | category: test_category 29 | product: test_product 30 | detection: 31 | sel: 32 | fieldA: valueA 33 | fieldB: valueB 34 | condition: sel 35 | """ 36 | ) 37 | 38 | assert splunk_backend.convert(rule) == ['fieldA="valueA" fieldB="valueB"'] 39 | 40 | 41 | def test_splunk_or_expression(splunk_backend: SplunkBackend): 42 | rule = SigmaCollection.from_yaml( 43 | """ 44 | title: Test 45 | status: test 46 | logsource: 47 | category: test_category 48 | product: test_product 49 | detection: 50 | sel1: 51 | fieldA: valueA 52 | sel2: 53 | fieldB: valueB 54 | condition: 1 of sel* 55 | """ 56 | ) 57 | assert splunk_backend.convert(rule) == ['fieldA="valueA" OR fieldB="valueB"'] 58 | 59 | 60 | def test_splunk_and_or_expression(splunk_backend: SplunkBackend): 61 | rule = SigmaCollection.from_yaml( 62 | """ 63 | title: Test 64 | status: test 65 | logsource: 66 | category: test_category 67 | product: test_product 68 | detection: 69 | sel: 70 | fieldA: 71 | - valueA1 72 | - valueA2 73 | fieldB: 74 | - valueB1 75 | - valueB2 76 | condition: sel 77 | """ 78 | ) 79 | assert splunk_backend.convert(rule) == [ 80 | 'fieldA IN ("valueA1", "valueA2") fieldB IN ("valueB1", "valueB2")' 81 | ] 82 | 83 | 84 | def test_splunk_or_and_expression(splunk_backend: SplunkBackend): 85 | rule = SigmaCollection.from_yaml( 86 | """ 87 | title: Test 88 | status: test 89 | logsource: 90 | category: test_category 91 | product: test_product 92 | detection: 93 | sel1: 94 | fieldA: valueA1 95 | fieldB: valueB1 96 | sel2: 97 | fieldA: valueA2 98 | fieldB: valueB2 99 | condition: 1 of sel* 100 | """ 101 | ) 102 | assert splunk_backend.convert(rule) == [ 103 | '(fieldA="valueA1" fieldB="valueB1") OR (fieldA="valueA2" fieldB="valueB2")' 104 | ] 105 | 106 | 107 | def test_splunk_in_expression(splunk_backend: SplunkBackend): 108 | assert ( 109 | splunk_backend.convert( 110 | SigmaCollection.from_yaml( 111 | """ 112 | title: Test 113 | status: test 114 | logsource: 115 | category: test_category 116 | product: test_product 117 | detection: 118 | sel: 119 | fieldA: 120 | - valueA 121 | - valueB 122 | - valueC* 123 | condition: sel 124 | """ 125 | ) 126 | ) 127 | == ['fieldA IN ("valueA", "valueB", "valueC*")'] 128 | ) 129 | 130 | 131 | def test_splunk_field_name_with_whitespace(splunk_backend: SplunkBackend): 132 | assert ( 133 | splunk_backend.convert( 134 | SigmaCollection.from_yaml( 135 | """ 136 | title: Test 137 | status: test 138 | logsource: 139 | category: test_category 140 | product: test_product 141 | detection: 142 | sel: 143 | field name: valueA 144 | condition: sel 145 | """ 146 | ) 147 | ) 148 | == ['"field name"="valueA"'] 149 | ) 150 | 151 | 152 | def test_splunk_regex_query(splunk_backend: SplunkBackend): 153 | assert ( 154 | splunk_backend.convert( 155 | SigmaCollection.from_yaml( 156 | """ 157 | title: Test 158 | status: test 159 | logsource: 160 | category: test_category 161 | product: test_product 162 | detection: 163 | sel: 164 | fieldA|re: foo.*bar 165 | fieldB: foo 166 | fieldC: bar 167 | condition: sel 168 | """ 169 | ) 170 | ) 171 | == ['fieldB="foo" fieldC="bar"\n| regex fieldA="foo.*bar"'] 172 | ) 173 | 174 | 175 | def test_splunk_regex_query_implicit_or(splunk_backend: SplunkBackend): 176 | assert ( 177 | splunk_backend.convert( 178 | SigmaCollection.from_yaml( 179 | """ 180 | title: Test 181 | status: test 182 | logsource: 183 | category: test_category 184 | product: test_product 185 | detection: 186 | sel: 187 | fieldA|re: 188 | - foo.*bar 189 | - boo.*foo 190 | fieldB: foo 191 | fieldC: bar 192 | condition: sel 193 | """ 194 | ) 195 | ) 196 | == [ 197 | '\n| rex field=fieldA "(?foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=fieldA "(?boo.*foo)"\n| eval fieldACondition2=if(isnotnull(fieldAMatch2), "true", "false")\n| search fieldACondition="true" OR fieldACondition2="true" fieldB="foo" fieldC="bar"' 198 | ] 199 | ) 200 | 201 | 202 | def test_splunk_regex_query_explicit_or(splunk_backend: SplunkBackend): 203 | 204 | assert ( 205 | splunk_backend.convert( 206 | SigmaCollection.from_yaml( 207 | """ 208 | title: Test 209 | status: test 210 | logsource: 211 | category: test_category 212 | product: test_product 213 | detection: 214 | sel1: 215 | fieldA|re: foo.*bar 216 | sel2: 217 | fieldB|re: boo.*foo 218 | condition: sel1 or sel2 219 | """ 220 | ) 221 | ) 222 | == [ 223 | '\n| rex field=fieldA "(?foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=fieldB "(?boo.*foo)"\n| eval fieldBCondition=if(isnotnull(fieldBMatch), "true", "false")\n| search fieldACondition="true" OR fieldBCondition="true"' 224 | ] 225 | ) 226 | 227 | 228 | def test_splunk_regex_query_explicit_or_with_nested_fields(): 229 | 230 | pipeline = ProcessingPipeline.from_yaml( 231 | """ 232 | name: Test 233 | priority: 100 234 | transformations: 235 | - id: field_mapping 236 | type: field_name_mapping 237 | mapping: 238 | fieldA: Event.EventData.fieldA 239 | fieldB: Event.EventData.fieldB 240 | """ 241 | ) 242 | splunk_backend = SplunkBackend(pipeline) 243 | 244 | collection = SigmaCollection.from_yaml( 245 | """ 246 | title: Test 247 | status: test 248 | logsource: 249 | category: test_category 250 | product: test_product 251 | detection: 252 | sel1: 253 | fieldA|re: foo.*bar 254 | sel2: 255 | fieldB|re: boo.*foo 256 | condition: sel1 or sel2 257 | """ 258 | ) 259 | 260 | assert splunk_backend.convert(collection) == [ 261 | '\n| rex field=Event.EventData.fieldA "(?foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=Event.EventData.fieldB "(?boo.*foo)"\n| eval fieldBCondition=if(isnotnull(fieldBMatch), "true", "false")\n| search fieldACondition="true" OR fieldBCondition="true"' 262 | ] 263 | 264 | 265 | def test_splunk_single_regex_query(splunk_backend: SplunkBackend): 266 | assert ( 267 | splunk_backend.convert( 268 | SigmaCollection.from_yaml( 269 | """ 270 | title: Test 271 | status: test 272 | logsource: 273 | category: test_category 274 | product: test_product 275 | detection: 276 | sel: 277 | fieldA|re: foo.*bar 278 | condition: sel 279 | """ 280 | ) 281 | ) 282 | == ['*\n| regex fieldA="foo.*bar"'] 283 | ) 284 | 285 | 286 | def test_splunk_cidr_query(splunk_backend: SplunkBackend): 287 | assert ( 288 | splunk_backend.convert( 289 | SigmaCollection.from_yaml( 290 | """ 291 | title: Test 292 | status: test 293 | logsource: 294 | category: test_category 295 | product: test_product 296 | detection: 297 | sel: 298 | fieldA|cidr: 192.168.0.0/16 299 | fieldB: foo 300 | fieldC: bar 301 | condition: sel 302 | """ 303 | ) 304 | ) 305 | == ['fieldA="192.168.0.0/16" fieldB="foo" fieldC="bar"'] 306 | ) 307 | 308 | 309 | def test_splunk_cidr_or(splunk_backend: SplunkBackend): 310 | assert ( 311 | splunk_backend.convert( 312 | SigmaCollection.from_yaml( 313 | """ 314 | title: Test 315 | status: test 316 | logsource: 317 | category: test_category 318 | product: test_product 319 | detection: 320 | sel: 321 | fieldA|cidr: 322 | - 192.168.0.0/16 323 | - 10.0.0.0/8 324 | fieldB: foo 325 | fieldC: bar 326 | condition: sel 327 | """ 328 | ) 329 | ) 330 | == ['fieldA="192.168.0.0/16" OR fieldA="10.0.0.0/8" fieldB="foo" fieldC="bar"'] 331 | ) 332 | 333 | 334 | def test_splunk_fieldref_query(splunk_backend: SplunkBackend): 335 | assert ( 336 | splunk_backend.convert( 337 | SigmaCollection.from_yaml( 338 | """ 339 | title: Test 340 | status: test 341 | logsource: 342 | category: test_category 343 | product: test_product 344 | detection: 345 | sel: 346 | fieldA|fieldref: fieldD 347 | fieldB: foo 348 | fieldC: bar 349 | condition: sel 350 | """ 351 | ) 352 | ) 353 | == ["fieldB=\"foo\" fieldC=\"bar\"\n| where 'fieldA'='fieldD'"] 354 | ) 355 | 356 | 357 | def test_splunk_fieldref_or(splunk_backend: SplunkBackend): 358 | with pytest.raises(SigmaFeatureNotSupportedByBackendError, match="ORing FieldRef"): 359 | splunk_backend.convert( 360 | SigmaCollection.from_yaml( 361 | """ 362 | title: Test 363 | status: test 364 | logsource: 365 | category: test_category 366 | product: test_product 367 | detection: 368 | sel: 369 | fieldA|fieldref: 370 | - fieldD 371 | - fieldE 372 | fieldB: foo 373 | fieldC: bar 374 | condition: sel 375 | """ 376 | ) 377 | ) 378 | 379 | def test_splunk_exists(splunk_backend: SplunkBackend): 380 | assert ( 381 | splunk_backend.convert( 382 | SigmaCollection.from_yaml( 383 | """ 384 | title: Test 385 | status: test 386 | logsource: 387 | category: test_category 388 | product: test_product 389 | detection: 390 | sel: 391 | fieldA|exists: yes 392 | fieldB|exists: no 393 | condition: sel 394 | """ 395 | ) 396 | ) 397 | == ['fieldA=* NOT fieldB=*'] 398 | ) 399 | 400 | 401 | def test_splunk_fields_output(splunk_backend: SplunkBackend): 402 | rule = SigmaCollection.from_yaml( 403 | """ 404 | title: Test 405 | status: test 406 | logsource: 407 | category: test_category 408 | product: test_product 409 | fields: 410 | - fieldA 411 | detection: 412 | sel: 413 | fieldA: valueA 414 | condition: sel 415 | """ 416 | ) 417 | 418 | assert splunk_backend.convert(rule) == ['fieldA="valueA" | table fieldA'] 419 | 420 | 421 | def test_splunk_savedsearch_output(splunk_backend: SplunkBackend): 422 | rules = """ 423 | title: Test 1 424 | description: | 425 | this is a description 426 | across two lines 427 | status: test 428 | logsource: 429 | category: test_category 430 | product: test_product 431 | fields: 432 | - fieldA 433 | detection: 434 | sel: 435 | fieldA|re: foo.*bar 436 | fieldB: foo 437 | fieldC: bar 438 | condition: sel 439 | --- 440 | title: Test 2 441 | status: test 442 | logsource: 443 | category: test_category 444 | product: test_product 445 | fields: 446 | - fieldA 447 | - fieldB 448 | detection: 449 | sel: 450 | fieldA: foo 451 | fieldB: bar 452 | condition: sel 453 | """ 454 | assert ( 455 | splunk_backend.convert(SigmaCollection.from_yaml(rules), "savedsearches") 456 | == """ 457 | [default] 458 | dispatch.earliest_time = -30d 459 | dispatch.latest_time = now 460 | 461 | [Test 1] 462 | description = this is a description \\ 463 | across two lines 464 | search = fieldB="foo" fieldC="bar" \\ 465 | | regex fieldA="foo.*bar" \\ 466 | | table fieldA 467 | 468 | [Test 2] 469 | description = 470 | search = fieldA="foo" fieldB="bar" \\ 471 | | table fieldA,fieldB""" 472 | ) 473 | 474 | 475 | def test_splunk_savedsearch_output_custom(splunk_custom_backend: SplunkBackend): 476 | rules = """ 477 | title: Test 1 478 | description: | 479 | this is a description 480 | across two lines 481 | status: test 482 | logsource: 483 | category: test_category 484 | product: test_product 485 | fields: 486 | - fieldA 487 | detection: 488 | sel: 489 | fieldA|re: foo.*bar 490 | fieldB: foo 491 | fieldC: bar 492 | condition: sel 493 | --- 494 | title: Test 2 495 | status: test 496 | logsource: 497 | category: test_category 498 | product: test_product 499 | fields: 500 | - fieldA 501 | - fieldB 502 | detection: 503 | sel: 504 | fieldA: foo 505 | fieldB: bar 506 | condition: sel 507 | """ 508 | assert ( 509 | splunk_custom_backend.convert(SigmaCollection.from_yaml(rules), "savedsearches") 510 | == """ 511 | [default] 512 | dispatch.earliest_time = -30d 513 | dispatch.latest_time = now 514 | custom.key = customvalue 515 | 516 | [Test 1] 517 | custom.query.key = Test 1 518 | description = this is a description \\ 519 | across two lines 520 | search = fieldB="foo" fieldC="bar" \\ 521 | | regex fieldA="foo.*bar" \\ 522 | | table fieldA 523 | 524 | [Test 2] 525 | custom.query.key = Test 2 526 | description = 527 | search = fieldA="foo" fieldB="bar" \\ 528 | | table fieldA,fieldB""" 529 | ) 530 | 531 | 532 | def test_splunk_data_model_process_creation(): 533 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 534 | rule = """ 535 | title: Test 536 | status: test 537 | logsource: 538 | category: process_creation 539 | product: windows 540 | detection: 541 | sel: 542 | CommandLine: test 543 | condition: sel 544 | """ 545 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 546 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes where 547 | Processes.process="test" by Processes.process Processes.dest Processes.process_current_directory Processes.process_path Processes.process_integrity_level Processes.original_file_name Processes.parent_process 548 | Processes.parent_process_path Processes.parent_process_guid Processes.parent_process_id Processes.process_guid Processes.process_id Processes.user 549 | | `drop_dm_object_name(Processes)` 550 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 551 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 552 | """.replace( 553 | "\n", " " 554 | ) 555 | ] 556 | 557 | 558 | def test_splunk_data_model_registry_add(): 559 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 560 | rule = """ 561 | title: Test 562 | status: test 563 | logsource: 564 | category: registry_add 565 | product: windows 566 | detection: 567 | sel: 568 | TargetObject: test 569 | condition: sel 570 | """ 571 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 572 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Registry where 573 | Registry.registry_key_name="test" by Registry.dest Registry.registry_value_data Registry.action Registry.process_path Registry.process_guid Registry.process_id Registry.registry_key_name 574 | | `drop_dm_object_name(Registry)` 575 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 576 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 577 | """.replace( 578 | "\n", " " 579 | ) 580 | ] 581 | 582 | 583 | def test_splunk_data_model_registry_delete(): 584 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 585 | rule = """ 586 | title: Test 587 | status: test 588 | logsource: 589 | category: registry_delete 590 | product: windows 591 | detection: 592 | sel: 593 | TargetObject: test 594 | condition: sel 595 | """ 596 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 597 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Registry where 598 | Registry.registry_key_name="test" by Registry.dest Registry.registry_value_data Registry.action Registry.process_path Registry.process_guid Registry.process_id Registry.registry_key_name 599 | | `drop_dm_object_name(Registry)` 600 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 601 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 602 | """.replace( 603 | "\n", " " 604 | ) 605 | ] 606 | 607 | 608 | def test_splunk_data_model_registry_event(): 609 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 610 | rule = """ 611 | title: Test 612 | status: test 613 | logsource: 614 | category: registry_event 615 | product: windows 616 | detection: 617 | sel: 618 | TargetObject: test 619 | condition: sel 620 | """ 621 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 622 | """ 623 | | tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Registry where 624 | Registry.registry_key_name="test" by Registry.dest Registry.registry_value_data Registry.action Registry.process_path Registry.process_guid Registry.process_id Registry.registry_key_name 625 | | `drop_dm_object_name(Registry)` 626 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 627 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 628 | """.replace( 629 | "\n", "" 630 | ) 631 | ] 632 | 633 | 634 | def test_splunk_data_model_registry_event(): 635 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 636 | rule = """ 637 | title: Test 638 | status: test 639 | logsource: 640 | category: registry_event 641 | product: windows 642 | detection: 643 | sel: 644 | TargetObject: test 645 | condition: sel 646 | """ 647 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 648 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Registry where 649 | Registry.registry_key_name="test" by Registry.dest Registry.registry_value_data Registry.action Registry.process_path Registry.process_guid Registry.process_id Registry.registry_key_name 650 | | `drop_dm_object_name(Registry)` 651 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 652 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 653 | """.replace( 654 | "\n", " " 655 | ) 656 | ] 657 | 658 | 659 | def test_splunk_data_model_registry_set(): 660 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 661 | rule = """ 662 | title: Test 663 | status: test 664 | logsource: 665 | category: registry_set 666 | product: windows 667 | detection: 668 | sel: 669 | TargetObject: test 670 | condition: sel 671 | """ 672 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 673 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Registry where 674 | Registry.registry_key_name="test" by Registry.dest Registry.registry_value_data Registry.action Registry.process_path Registry.process_guid Registry.process_id Registry.registry_key_name 675 | | `drop_dm_object_name(Registry)` 676 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 677 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 678 | """.replace( 679 | "\n", " " 680 | ) 681 | ] 682 | 683 | 684 | def test_splunk_data_model_file_event(): 685 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 686 | rule = """ 687 | title: Test 688 | status: test 689 | logsource: 690 | category: file_event 691 | product: windows 692 | detection: 693 | sel: 694 | TargetFilename: test 695 | condition: sel 696 | """ 697 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 698 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Filesystem where 699 | Filesystem.file_path="test" by Filesystem.dest Filesystem.file_create_time Filesystem.process_path Filesystem.process_guid Filesystem.process_id Filesystem.file_path 700 | | `drop_dm_object_name(Filesystem)` 701 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 702 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 703 | """.replace( 704 | "\n", " " 705 | ) 706 | ] 707 | 708 | 709 | def test_splunk_data_model_process_creation_linux(): 710 | splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model()) 711 | rule = """ 712 | title: Test 713 | status: test 714 | logsource: 715 | category: process_creation 716 | product: linux 717 | detection: 718 | sel: 719 | CommandLine: test 720 | condition: sel 721 | """ 722 | assert splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") == [ 723 | """| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes where 724 | Processes.process="test" by Processes.process Processes.dest Processes.process_current_directory Processes.process_path Processes.process_integrity_level Processes.original_file_name Processes.parent_process 725 | Processes.parent_process_path Processes.parent_process_guid Processes.parent_process_id Processes.process_guid Processes.process_id Processes.user 726 | | `drop_dm_object_name(Processes)` 727 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) 728 | | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) 729 | """.replace( 730 | "\n", " " 731 | ) 732 | ] 733 | 734 | 735 | def test_splunk_data_model_no_data_model_specified(): 736 | splunk_backend = SplunkBackend() 737 | rule = """ 738 | title: Test 739 | status: test 740 | logsource: 741 | product: windows 742 | service: security 743 | detection: 744 | sel: 745 | CommandLine: test 746 | condition: sel 747 | """ 748 | with pytest.raises( 749 | SigmaFeatureNotSupportedByBackendError, match="No data model specified" 750 | ): 751 | splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model") 752 | -------------------------------------------------------------------------------- /tests/test_backend_splunk_correlations.py: -------------------------------------------------------------------------------- 1 | from test_backend_splunk import splunk_backend 2 | from sigma.collection import SigmaCollection 3 | 4 | def test_event_count_correlation_rule_stats_query(splunk_backend): 5 | correlation_rule = SigmaCollection.from_yaml( 6 | """ 7 | title: Base rule 8 | name: base_rule 9 | status: test 10 | logsource: 11 | category: test 12 | detection: 13 | selection: 14 | fieldA: value1 15 | fieldB: value2 16 | condition: selection 17 | --- 18 | title: Multiple occurrences of base event 19 | status: test 20 | correlation: 21 | type: event_count 22 | rules: 23 | - base_rule 24 | group-by: 25 | - fieldC 26 | - fieldD 27 | timespan: 15m 28 | condition: 29 | gte: 10 30 | """ 31 | ) 32 | assert splunk_backend.convert(correlation_rule) == [ 33 | """fieldA="value1" fieldB="value2" 34 | 35 | | bin _time span=15m 36 | | stats count as event_count by _time fieldC fieldD 37 | 38 | | search event_count >= 10""" 39 | ] 40 | 41 | def test_value_count_correlation_rule_stats_query(splunk_backend): 42 | correlation_rule = SigmaCollection.from_yaml( 43 | """ 44 | title: Base rule 45 | name: base_rule 46 | status: test 47 | logsource: 48 | category: test 49 | detection: 50 | selection: 51 | fieldA: value1 52 | fieldB: value2 53 | condition: selection 54 | --- 55 | title: Multiple occurrences of base event 56 | status: test 57 | correlation: 58 | type: value_count 59 | rules: 60 | - base_rule 61 | group-by: 62 | - fieldC 63 | timespan: 15m 64 | condition: 65 | lt: 10 66 | field: fieldD 67 | """ 68 | ) 69 | assert splunk_backend.convert(correlation_rule) == [ 70 | """fieldA="value1" fieldB="value2" 71 | 72 | | bin _time span=15m 73 | | stats dc(fieldD) as value_count by _time fieldC 74 | 75 | | search value_count < 10""" 76 | ] 77 | 78 | def test_temporal_correlation_rule_stats_query(splunk_backend): 79 | correlation_rule = SigmaCollection.from_yaml( 80 | """ 81 | title: Base rule 1 82 | name: base_rule_1 83 | status: test 84 | logsource: 85 | category: test 86 | detection: 87 | selection: 88 | fieldA: value1 89 | fieldB: value2 90 | condition: selection 91 | --- 92 | title: Base rule 2 93 | name: base_rule_2 94 | status: test 95 | logsource: 96 | category: test 97 | detection: 98 | selection: 99 | fieldA: value3 100 | fieldB: value4 101 | condition: selection 102 | --- 103 | title: Temporal correlation rule 104 | status: test 105 | correlation: 106 | type: temporal 107 | rules: 108 | - base_rule_1 109 | - base_rule_2 110 | aliases: 111 | field: 112 | base_rule_1: fieldC 113 | base_rule_2: fieldD 114 | group-by: 115 | - fieldC 116 | timespan: 15m 117 | """ 118 | ) 119 | assert splunk_backend.convert(correlation_rule) == [ 120 | """| multisearch 121 | [ search fieldA="value1" fieldB="value2" | eval event_type="base_rule_1" | rename fieldC as field ] 122 | [ search fieldA="value3" fieldB="value4" | eval event_type="base_rule_2" | rename fieldD as field ] 123 | 124 | | bin _time span=15m 125 | | stats dc(event_type) as event_type_count by _time fieldC 126 | 127 | | search event_type_count >= 2"""] -------------------------------------------------------------------------------- /tests/test_splunk_pipelines.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sigma.collection import SigmaCollection 3 | from sigma.backends.splunk import SplunkBackend 4 | from sigma.pipelines.splunk import ( 5 | splunk_windows_pipeline, 6 | splunk_windows_sysmon_acceleration_keywords, 7 | splunk_cim_data_model, 8 | ) 9 | from sigma.pipelines.common import windows_logsource_mapping 10 | from sigma.exceptions import SigmaTransformationError 11 | 12 | 13 | @pytest.mark.parametrize(("service", "source"), windows_logsource_mapping.items()) 14 | def test_splunk_windows_pipeline_simple(service, source): 15 | if isinstance(source, str): 16 | assert ( 17 | SplunkBackend(processing_pipeline=splunk_windows_pipeline()).convert( 18 | SigmaCollection.from_yaml( 19 | f""" 20 | title: Test 21 | status: test 22 | logsource: 23 | product: windows 24 | service: {service} 25 | detection: 26 | sel: 27 | EventID: 123 28 | field: value 29 | condition: sel 30 | """ 31 | ) 32 | ) 33 | == [f'source="WinEventLog:{source}" EventCode=123 field="value"'] 34 | ) 35 | else: 36 | assert ( 37 | SplunkBackend(processing_pipeline=splunk_windows_pipeline()).convert( 38 | SigmaCollection.from_yaml( 39 | f""" 40 | title: Test 41 | status: test 42 | logsource: 43 | product: windows 44 | service: {service} 45 | detection: 46 | sel: 47 | EventID: 123 48 | field: value 49 | condition: sel 50 | """ 51 | ) 52 | ) 53 | == [ 54 | "source IN (" 55 | + ", ".join((f'"WinEventLog:{source_item}"' for source_item in source)) 56 | + ') EventCode=123 field="value"' 57 | ] 58 | ) 59 | 60 | 61 | def test_splunk_sysmon_process_creation_keyword_acceleration(): 62 | assert ( 63 | SplunkBackend( 64 | processing_pipeline=splunk_windows_pipeline() 65 | + splunk_windows_sysmon_acceleration_keywords() 66 | ) 67 | .convert( 68 | SigmaCollection.from_yaml( 69 | f""" 70 | title: Test 71 | status: test 72 | logsource: 73 | product: windows 74 | service: sysmon 75 | category: process_creation 76 | detection: 77 | sel: 78 | field: value 79 | condition: sel 80 | """ 81 | ) 82 | )[0] 83 | .startswith('"ParentProcessGuid"') 84 | ) 85 | 86 | 87 | def test_splunk_sysmon_file_creation_keyword_acceleration(): 88 | assert ( 89 | SplunkBackend( 90 | processing_pipeline=splunk_windows_pipeline() 91 | + splunk_windows_sysmon_acceleration_keywords() 92 | ) 93 | .convert( 94 | SigmaCollection.from_yaml( 95 | f""" 96 | title: Test 97 | status: test 98 | logsource: 99 | product: windows 100 | service: sysmon 101 | category: file_event 102 | detection: 103 | sel: 104 | field: value 105 | condition: sel 106 | """ 107 | ) 108 | )[0] 109 | .startswith('"TargetFilename"') 110 | ) 111 | 112 | 113 | def test_splunk_process_creation_dm(): 114 | assert ( 115 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 116 | SigmaCollection.from_yaml( 117 | f""" 118 | title: Test 119 | status: test 120 | logsource: 121 | category: process_creation 122 | product: windows 123 | detection: 124 | sel: 125 | CommandLine: test 126 | CurrentDirectory: test 127 | Image: test 128 | IntegrityLevel: test 129 | OriginalFileName: test 130 | ParentCommandLine: test 131 | ParentImage: test 132 | ParentProcessGuid: test 133 | ParentProcessId: test 134 | ProcessGuid: test 135 | ProcessId: test 136 | User: test 137 | condition: sel 138 | """ 139 | ) 140 | ) 141 | == [ 142 | f'Processes.process="test" Processes.process_current_directory="test" Processes.process_path="test" Processes.process_integrity_level="test" Processes.original_file_name="test" Processes.parent_process="test" Processes.parent_process_path="test" Processes.parent_process_guid="test" Processes.parent_process_id="test" Processes.process_guid="test" Processes.process_id="test" Processes.user="test"' 143 | ] 144 | ) 145 | 146 | 147 | def test_splunk_process_creation_dm_unsupported_fields(): 148 | with pytest.raises(SigmaTransformationError): 149 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 150 | SigmaCollection.from_yaml( 151 | f""" 152 | title: Test 153 | status: test 154 | logsource: 155 | category: process_creation 156 | product: windows 157 | detection: 158 | sel: 159 | imphash: 123456 160 | condition: sel 161 | """ 162 | ) 163 | ) 164 | 165 | 166 | def test_splunk_registry_add_dm(): 167 | assert ( 168 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 169 | SigmaCollection.from_yaml( 170 | f""" 171 | title: Test 172 | status: test 173 | logsource: 174 | category: registry_add 175 | product: windows 176 | detection: 177 | sel: 178 | Computer: test 179 | Details: test 180 | EventType: test 181 | Image: test 182 | ProcessGuid: test 183 | ProcessId: test 184 | TargetObject: test 185 | condition: sel 186 | """ 187 | ) 188 | ) 189 | == [ 190 | f"""Registry.dest=\"test\" Registry.registry_value_data=\"test\" Registry.action=\"test\" 191 | Registry.process_path=\"test\" Registry.process_guid=\"test\" Registry.process_id=\"test\" 192 | Registry.registry_key_name=\"test\"""".replace( 193 | "\n", " " 194 | ) 195 | ] 196 | ) 197 | 198 | 199 | def test_splunk_registry_delete_dm(): 200 | assert ( 201 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 202 | SigmaCollection.from_yaml( 203 | f""" 204 | title: Test 205 | status: test 206 | logsource: 207 | category: registry_delete 208 | product: windows 209 | detection: 210 | sel: 211 | Computer: test 212 | Details: test 213 | EventType: test 214 | Image: test 215 | ProcessGuid: test 216 | ProcessId: test 217 | TargetObject: test 218 | condition: sel 219 | """ 220 | ) 221 | ) 222 | == [ 223 | f"""Registry.dest=\"test\" Registry.registry_value_data=\"test\" Registry.action=\"test\" 224 | Registry.process_path=\"test\" Registry.process_guid=\"test\" Registry.process_id=\"test\" 225 | Registry.registry_key_name=\"test\"""".replace( 226 | "\n", " " 227 | ) 228 | ] 229 | ) 230 | 231 | 232 | def test_splunk_registry_event_dm(): 233 | assert ( 234 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 235 | SigmaCollection.from_yaml( 236 | f""" 237 | title: Test 238 | status: test 239 | logsource: 240 | category: registry_event 241 | product: windows 242 | detection: 243 | sel: 244 | Computer: test 245 | Details: test 246 | EventType: test 247 | Image: test 248 | ProcessGuid: test 249 | ProcessId: test 250 | TargetObject: test 251 | condition: sel 252 | """ 253 | ) 254 | ) 255 | == [ 256 | f"""Registry.dest=\"test\" Registry.registry_value_data=\"test\" Registry.action=\"test\" 257 | Registry.process_path=\"test\" Registry.process_guid=\"test\" Registry.process_id=\"test\" 258 | Registry.registry_key_name=\"test\"""".replace( 259 | "\n", " " 260 | ) 261 | ] 262 | ) 263 | 264 | 265 | def test_splunk_registry_set_dm(): 266 | assert ( 267 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 268 | SigmaCollection.from_yaml( 269 | f""" 270 | title: Test 271 | status: test 272 | logsource: 273 | category: registry_set 274 | product: windows 275 | detection: 276 | sel: 277 | Computer: test 278 | Details: test 279 | EventType: test 280 | Image: test 281 | ProcessGuid: test 282 | ProcessId: test 283 | TargetObject: test 284 | condition: sel 285 | """ 286 | ) 287 | ) 288 | == [ 289 | f"""Registry.dest=\"test\" Registry.registry_value_data=\"test\" Registry.action=\"test\" 290 | Registry.process_path=\"test\" Registry.process_guid=\"test\" Registry.process_id=\"test\" 291 | Registry.registry_key_name=\"test\"""".replace( 292 | "\n", " " 293 | ) 294 | ] 295 | ) 296 | 297 | 298 | def test_splunk_registry_dm_unsupported_fields(): 299 | with pytest.raises(SigmaTransformationError): 300 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 301 | SigmaCollection.from_yaml( 302 | f""" 303 | title: Test 304 | status: test 305 | logsource: 306 | category: registry_add 307 | product: windows 308 | detection: 309 | sel: 310 | NewName: test 311 | condition: sel 312 | """ 313 | ) 314 | ) 315 | 316 | 317 | def test_splunk_file_event_dm(): 318 | assert ( 319 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 320 | SigmaCollection.from_yaml( 321 | f""" 322 | title: Test 323 | status: test 324 | logsource: 325 | category: file_event 326 | product: windows 327 | detection: 328 | sel: 329 | Computer: test 330 | CreationUtcTime: test 331 | Image: test 332 | ProcessGuid: test 333 | ProcessId: test 334 | TargetFilename: test 335 | condition: sel 336 | """ 337 | ) 338 | ) 339 | == [ 340 | f"""Filesystem.dest=\"test\" Filesystem.file_create_time=\"test\" Filesystem.process_path=\"test\" 341 | Filesystem.process_guid=\"test\" Filesystem.process_id=\"test\" Filesystem.file_path=\"test\"""".replace( 342 | "\n", " " 343 | ) 344 | ] 345 | ) 346 | 347 | 348 | def test_splunk_file_event_dm_unsupported_fields(): 349 | with pytest.raises(SigmaTransformationError): 350 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 351 | SigmaCollection.from_yaml( 352 | f""" 353 | title: Test 354 | status: test 355 | logsource: 356 | category: file_event 357 | product: windows 358 | detection: 359 | sel: 360 | field: test 361 | condition: sel 362 | """ 363 | ) 364 | ) 365 | 366 | 367 | def test_splunk_dm_unsupported_logsource(): 368 | with pytest.raises(SigmaTransformationError): 369 | SplunkBackend(processing_pipeline=splunk_cim_data_model()).convert( 370 | SigmaCollection.from_yaml( 371 | f""" 372 | title: Test 373 | status: test 374 | logsource: 375 | category: image_load 376 | product: windows 377 | detection: 378 | sel: 379 | Image: test 380 | condition: sel 381 | """ 382 | ) 383 | ) 384 | --------------------------------------------------------------------------------