├── .github ├── pyinstaller │ ├── icon.ico │ └── pyinstaller.spec └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── alembic ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 25527d692d46_resize_meta_value_column.py │ ├── 709085f65102_added_rulesets.py │ └── 892f53ea8054_init.py ├── poetry.lock ├── pyproject.toml ├── resources └── config.toml └── yaramanager ├── __init__.py ├── commands ├── __init__.py ├── add.py ├── cli.py ├── config.py ├── db.py ├── delete.py ├── edit.py ├── export.py ├── get.py ├── list.py ├── new.py ├── parse.py ├── read.py ├── ruleset.py ├── scan.py ├── search.py ├── stats.py ├── tags.py └── version.py ├── config.py ├── db ├── __init__.py ├── base.py ├── base_class.py └── session.py ├── main.py ├── models ├── __init__.py ├── meta.py ├── rule.py ├── ruleset.py ├── string.py ├── tables.py ├── tag.py └── yarabuilder.py └── utils ├── __init__.py ├── output.py ├── platform.py └── utils.py /.github/pyinstaller/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3c7/yaramanager/ed72b6c9e7059e691b9ad1a800b2e02429599fd7/.github/pyinstaller/icon.ico -------------------------------------------------------------------------------- /.github/pyinstaller/pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | import io 3 | import subprocess 4 | 5 | with io.open("yaramanager/__init__.py", "a") as fh: 6 | commit = subprocess.check_output(["git", "describe", "--always", "--tags", "--long"]).decode("utf-8").strip().split("-")[-1][1:] 7 | fh.write(f"commit = \"{commit}\"\n") 8 | 9 | a = Analysis( 10 | ['../../yaramanager/main.py'], 11 | pathex=['yaramanager'], 12 | binaries=[], 13 | datas=[ 14 | # Include alembic files for database schema migrations 15 | ('../../alembic/*', 'alembic'), 16 | ('../../alembic/versions/*', 'alembic/versions'), 17 | # Include resources directory 18 | ('../../resources/*', 'resources') 19 | ], 20 | hiddenimports=[], 21 | hookspath=[], 22 | runtime_hooks=[], 23 | excludes=[], 24 | win_no_prefer_redirects=False, 25 | win_private_assemblies=False, 26 | noarchive=False 27 | ) 28 | pyz = PYZ(a.pure, a.zipped_data) 29 | exe = EXE( 30 | pyz, 31 | a.scripts, 32 | a.binaries, 33 | a.zipfiles, 34 | a.datas, 35 | [], 36 | name='ym', 37 | icon="icon.ico", 38 | debug=False, 39 | bootloader_ignore_signals=False, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | runtime_tmpdir=None, 44 | console=True 45 | ) 46 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [edited, published] 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | build: 11 | name: Build package for ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | include: 16 | - os: ubuntu-18.04 17 | artifact_name: ym 18 | asset_name: linux 19 | - os: windows-2019 20 | artifact_name: ym.exe 21 | asset_name: windows 22 | - os: macos-10.15 23 | artifact_name: ym 24 | asset_name: macos 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v2 28 | - name: Set up Python 3.9 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: 3.9 32 | - name: Install Poetry 33 | run: pip install 'poetry==1.1.5' 34 | # Export dependencies from the pyproject file into the requirements.txt format. 35 | # Skip hashes as some nested dependencies do not specify versions. 36 | - name: Export dependencies (without hashes) 37 | run: poetry export --dev --without-hashes > requirements.txt 38 | - name: Install dependencies 39 | run: pip install -r requirements.txt 40 | - name: Build standalone executable 41 | run: pyinstaller .github/pyinstaller/pyinstaller.spec 42 | - name: Check if built executable does run 43 | run: dist/ym version 44 | - uses: actions/upload-artifact@v2 45 | with: 46 | name: ${{ matrix.asset_name }} 47 | path: dist/${{ matrix.artifact_name }} 48 | 49 | zip: 50 | name: zip ${{ matrix.asset_name }} 51 | runs-on: ubuntu-20.04 52 | needs: build 53 | strategy: 54 | matrix: 55 | include: 56 | - asset_name: linux 57 | artifact_name: ym 58 | - asset_name: windows 59 | artifact_name: ym.exe 60 | - asset_name: macos 61 | artifact_name: ym 62 | steps: 63 | - name: Download ${{ matrix.asset_name }} 64 | uses: actions/download-artifact@v2 65 | with: 66 | name: ${{ matrix.asset_name }} 67 | - name: Set executable flag 68 | run: chmod +x ${{ matrix.artifact_name }} 69 | - name: Set zip name 70 | run: echo "zip_name=yaramanager-${GITHUB_REF#refs/tags/}-${{ matrix.asset_name }}.zip" >> $GITHUB_ENV 71 | - name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }} 72 | run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }} 73 | - name: Upload ${{ env.zip_name }} to GH Release 74 | uses: svenstaro/upload-release-action@v2 75 | with: 76 | repo_token: ${{ secrets.GITHUB_TOKEN }} 77 | file: ${{ env.zip_name }} 78 | tag: ${{ github.ref }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.db 3 | venv 4 | .idea 5 | build 6 | dist 7 | alembic_local.ini 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nils Kuhnert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![License:MIT](https://img.shields.io/github/license/3c7/yaramanager?style=flat-square&color=blue) 4 | ![Version](https://img.shields.io/pypi/v/yaramanager?style=flat-square&color=blue) 5 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/yaramanager?color=blue&style=flat-square) 6 | [![Awesome Yara](https://img.shields.io/static/v1?label=awesome&message=yara&style=flat-square&color=ff69b4&logo=awesome-lists)](https://github.com/InQuest/awesome-yara) 7 | 8 |
9 | 10 | # Yara Manager 11 | A simple program to manage your yara ruleset in a database. By default sqlite will be used, but using MySQL/MariaDB or 12 | Postgres is also possible. 13 | 14 | ## Todos 15 | - [ ] Implement backup and sharing possibilities 16 | 17 | ## Installation 18 | Install it using pip: 19 | ```shell 20 | pip install yaramanager 21 | ``` 22 | Or grab one of the prebuilt binaries from the release page. 23 | 24 | **If you want to use other databases than SQLite, you need to install the specific extra dependencies:** 25 | 26 | ```shell 27 | pip install yaramanager[mysql] 28 | pip install yaramanager[pgsql] 29 | ``` 30 | 31 | ## Configuration 32 | Yara Manager creates a fresh config if none exists. If you update from an older version, please pay attention to freshly 33 | added [config options](resources/config.toml). You can reset you configuration using `ym config reset`, however, this 34 | will also overwrite any custom changes you made. 35 | 36 | ```toml 37 | ## Editor 38 | # editor contains the command used to start the editor. Note that this must be a list of the command and the needed 39 | # parameters, e.g. `editor = ["codium", "-w"]`. 40 | editor = [ "codium", "-w" ] 41 | ``` 42 | The most important configuration to change is probably your editor. The default configuration uses `codium -w` for 43 | opening rules. You can use e.g. `EDITOR=vim DISABLE_STATUS=1 ym config edit` to open you config in Vim (and you can type 44 | `:wq` to save your changes and quit :P). After changing the editor path, you are good to go! The following asciinema 45 | shows how to quickly overwrite the editor set in the config: 46 | 47 | [![Asciinema: Temporarily overwrite the used editor.](https://asciinema.org/a/auX5tjpeUiHCnsfCrO0MEPRY9.svg)](https://asciinema.org/a/auX5tjpeUiHCnsfCrO0MEPRY9) 48 | 49 | ```toml 50 | # Databases 51 | # A list of databases. Every database needs to define a driver and a path, such as 52 | # 53 | # [[yaramanager.db.databases]] 54 | # driver = "sqlite" 55 | # path = "/home/user/.config/yaramanager/data.db" 56 | [[yaramanager.db.databases]] 57 | driver = "sqlite" 58 | path = "/home/3c7/.config/yaramanager/myrules.db" 59 | ``` 60 | If you want to use multiple databases (which is pretty useful if you use rules from different sources or with different 61 | classifications), you can add them to the config file, too. 62 | 63 | In order to use MySQL/MariaDB or Postgres, you need to specify the specific database driver, e.g.: 64 | 65 | ```toml 66 | [[yaramanager.db.databases]] 67 | driver = "mysql+pymysql" 68 | path = "user:password@127.0.0.1/database" 69 | [[yaramanager.db.databases]] 70 | driver = "postgresql+psycopg2" 71 | path = "user:password@127.0.0.1/database" 72 | ``` 73 | 74 | ## Features 75 | ### General usage 76 | ``` 77 | $ ym 78 | Usage: ym [OPTIONS] COMMAND [ARGS]... 79 | 80 | ym - yaramanager. Use the commands shown below to manage your yara 81 | ruleset. By default, the manager uses codium as editor. You can change 82 | that in the config file or using EDITOR environment variable. When using 83 | editors in the console, you might want to disable the status display using 84 | DISABLE_STATUS. 85 | 86 | Options: 87 | --help Show this message and exit. 88 | 89 | Commands: 90 | add Add a new rule to the database. 91 | config Review and change yaramanager configuration. 92 | db Manage your databases 93 | del Delete a rule by its ID or name. 94 | edit Edits a rule with your default editor. 95 | export Export rules from the database. 96 | get Get rules from the database. 97 | help Displays help about commands 98 | list Lists rules available in DB. 99 | new Create a new rule using you preferred editor. 100 | parse Parses rule files. 101 | read Read rules from stdin. 102 | ruleset Manage your rulesets 103 | scan Scan files using your rulesets. 104 | search Searches through your rules. 105 | stats Prints stats about the database contents. 106 | tags Show tags and the number of tagged rules 107 | version Displays the current version. 108 | ``` 109 | 110 | ### Yara Manager Showcase 111 | [![Asciiname: Yara Manager showcase](https://asciinema.org/a/8QbXQoBEeJIwVcf2mWqiRTNBj.svg)](https://asciinema.org/a/8QbXQoBEeJIwVcf2mWqiRTNBj) 112 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | # script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date 15 | # within the migration file as well as the filename. 16 | # string value is passed to dateutil.tz.gettz() 17 | # leave blank for localtime 18 | # timezone = 19 | 20 | # max length of characters to apply to the 21 | # "slug" field 22 | # truncate_slug_length = 40 23 | 24 | # set to 'true' to run the environment during 25 | # the 'revision' command, regardless of autogenerate 26 | # revision_environment = false 27 | 28 | # set to 'true' to allow .pyc and .pyo files without 29 | # a source .py file to be detected as revisions in the 30 | # versions/ directory 31 | # sourceless = false 32 | 33 | # version location specification; this defaults 34 | # to alembic/versions. When using multiple version 35 | # directories, initial revisions must be specified with --version-path 36 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 37 | 38 | # the output encoding used when revision files 39 | # are written from script.py.mako 40 | # output_encoding = utf-8 41 | 42 | # sqlalchemy.url = sqlite:///dev.db 43 | 44 | 45 | [post_write_hooks] 46 | # post_write_hooks defines scripts or Python functions that are run 47 | # on newly generated revision scripts. See the documentation for further 48 | # detail and examples 49 | 50 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 51 | # hooks=black 52 | # black.type=console_scripts 53 | # black.entrypoint=black 54 | # black.options=-l 79 55 | 56 | # Logging configuration 57 | [loggers] 58 | keys = root,sqlalchemy,alembic 59 | 60 | [handlers] 61 | keys = console 62 | 63 | [formatters] 64 | keys = generic 65 | 66 | [logger_root] 67 | level = WARN 68 | handlers = console 69 | qualname = 70 | 71 | [logger_sqlalchemy] 72 | level = WARN 73 | handlers = 74 | qualname = sqlalchemy.engine 75 | 76 | [logger_alembic] 77 | level = INFO 78 | handlers = 79 | qualname = alembic 80 | 81 | [handler_console] 82 | class = StreamHandler 83 | args = (sys.stderr,) 84 | level = NOTSET 85 | formatter = generic 86 | 87 | [formatter_generic] 88 | format = %(levelname)-5.5s [%(name)s] %(message)s 89 | datefmt = %H:%M:%S 90 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | from yaramanager.db.base import Base 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | target_metadata = Base.metadata 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | 27 | 28 | def run_migrations_offline(): 29 | """Run migrations in 'offline' mode. 30 | 31 | This configures the context with just a URL 32 | and not an Engine, though an Engine is acceptable 33 | here as well. By skipping the Engine creation 34 | we don't even need a DBAPI to be available. 35 | 36 | Calls to context.execute() here emit the given string to the 37 | script output. 38 | 39 | """ 40 | url = config.get_main_option("sqlalchemy.url") 41 | context.configure( 42 | url=url, 43 | target_metadata=target_metadata, 44 | literal_binds=True, 45 | dialect_opts={"paramstyle": "named"}, 46 | ) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | def run_migrations_online(): 53 | """Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine 56 | and associate a connection with the context. 57 | 58 | """ 59 | connectable = engine_from_config( 60 | config.get_section(config.config_ini_section), 61 | prefix="sqlalchemy.", 62 | poolclass=pool.NullPool, 63 | ) 64 | 65 | with connectable.connect() as connection: 66 | context.configure( 67 | connection=connection, target_metadata=target_metadata 68 | ) 69 | 70 | with context.begin_transaction(): 71 | context.run_migrations() 72 | 73 | 74 | if context.is_offline_mode(): 75 | run_migrations_offline() 76 | else: 77 | run_migrations_online() 78 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/25527d692d46_resize_meta_value_column.py: -------------------------------------------------------------------------------- 1 | """Resize meta value column 2 | 3 | Revision ID: 25527d692d46 4 | Revises: 709085f65102 5 | Create Date: 2022-02-21 22:11:03.306330 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '25527d692d46' 13 | down_revision = '709085f65102' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | with op.batch_alter_table("meta") as bo: 20 | bo.alter_column("value", type_=sa.String(4096)) 21 | 22 | 23 | def downgrade(): 24 | with op.batch_alter_table("meta") as bo: 25 | bo.alter_column("value", type_=sa.String(255)) 26 | -------------------------------------------------------------------------------- /alembic/versions/709085f65102_added_rulesets.py: -------------------------------------------------------------------------------- 1 | """Added rulesets 2 | 3 | Revision ID: 709085f65102 4 | Revises: 892f53ea8054 5 | Create Date: 2021-04-04 23:50:25.845214 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '709085f65102' 13 | down_revision = '892f53ea8054' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table( 21 | 'ruleset', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=True), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_index(op.f('ix_ruleset_id'), 'ruleset', ['id'], unique=False) 27 | op.create_index(op.f('ix_ruleset_name'), 'ruleset', ['name'], unique=False) 28 | 29 | with op.batch_alter_table('rule') as bo: 30 | bo.add_column(sa.Column('ruleset_id', sa.Integer(), nullable=True)) 31 | bo.create_foreign_key('rule_ruleset_fk', 'ruleset', ['ruleset_id'], ['id']) 32 | #op.create_foreign_key(None, 'rule', 'ruleset', ['ruleset_id'], ['id']) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | with op.batch_alter_table('rule') as bo: 39 | bo.drop_constraint('rule_ruleset_fk', type_='foreignkey') 40 | bo.drop_column('ruleset_id') 41 | #op.drop_constraint(None, 'rule', type_='foreignkey') 42 | #op.drop_column('rule', 'ruleset_id') 43 | op.drop_index(op.f('ix_ruleset_name'), table_name='ruleset') 44 | op.drop_index(op.f('ix_ruleset_id'), table_name='ruleset') 45 | op.drop_table('ruleset') 46 | # ### end Alembic commands ### 47 | -------------------------------------------------------------------------------- /alembic/versions/892f53ea8054_init.py: -------------------------------------------------------------------------------- 1 | """Init 2 | 3 | Revision ID: 892f53ea8054 4 | Revises: 5 | Create Date: 2021-03-29 21:38:23.372145 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '892f53ea8054' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('rule', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=True), 24 | sa.Column('condition', sa.Text(), nullable=True), 25 | sa.Column('imports', sa.Integer(), nullable=True), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | op.create_index(op.f('ix_rule_id'), 'rule', ['id'], unique=False) 29 | op.create_index(op.f('ix_rule_name'), 'rule', ['name'], unique=False) 30 | op.create_table('tag', 31 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 32 | sa.Column('name', sa.String(length=255), nullable=True), 33 | sa.PrimaryKeyConstraint('id') 34 | ) 35 | op.create_index(op.f('ix_tag_id'), 'tag', ['id'], unique=False) 36 | op.create_index(op.f('ix_tag_name'), 'tag', ['name'], unique=False) 37 | op.create_table('meta', 38 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 39 | sa.Column('key', sa.String(length=255), nullable=True), 40 | sa.Column('value', sa.String(length=255), nullable=True), 41 | sa.Column('order', sa.Integer(), nullable=True), 42 | sa.Column('rule_id', sa.Integer(), nullable=True), 43 | sa.ForeignKeyConstraint(['rule_id'], ['rule.id'], ), 44 | sa.PrimaryKeyConstraint('id') 45 | ) 46 | op.create_index(op.f('ix_meta_id'), 'meta', ['id'], unique=False) 47 | op.create_index(op.f('ix_meta_key'), 'meta', ['key'], unique=False) 48 | op.create_index(op.f('ix_meta_value'), 'meta', ['value'], unique=False) 49 | op.create_table('string', 50 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 51 | sa.Column('name', sa.String(length=255), nullable=True), 52 | sa.Column('type', sa.String(length=5), nullable=True), 53 | sa.Column('value', sa.Text(), nullable=True), 54 | sa.Column('modifiers', sa.Integer(), nullable=True), 55 | sa.Column('order', sa.Integer(), nullable=True), 56 | sa.Column('rule_id', sa.Integer(), nullable=True), 57 | sa.ForeignKeyConstraint(['rule_id'], ['rule.id'], ), 58 | sa.PrimaryKeyConstraint('id') 59 | ) 60 | op.create_index(op.f('ix_string_id'), 'string', ['id'], unique=False) 61 | op.create_index(op.f('ix_string_name'), 'string', ['name'], unique=False) 62 | op.create_table('tags_rules', 63 | sa.Column('tag_id', sa.Integer(), nullable=True), 64 | sa.Column('rule_id', sa.Integer(), nullable=True), 65 | sa.ForeignKeyConstraint(['rule_id'], ['rule.id'], ), 66 | sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ) 67 | ) 68 | # ### end Alembic commands ### 69 | 70 | 71 | def downgrade(): 72 | # ### commands auto generated by Alembic - please adjust! ### 73 | op.drop_table('tags_rules') 74 | op.drop_index(op.f('ix_string_name'), table_name='string') 75 | op.drop_index(op.f('ix_string_id'), table_name='string') 76 | op.drop_table('string') 77 | op.drop_index(op.f('ix_meta_value'), table_name='meta') 78 | op.drop_index(op.f('ix_meta_key'), table_name='meta') 79 | op.drop_index(op.f('ix_meta_id'), table_name='meta') 80 | op.drop_table('meta') 81 | op.drop_index(op.f('ix_tag_name'), table_name='tag') 82 | op.drop_index(op.f('ix_tag_id'), table_name='tag') 83 | op.drop_table('tag') 84 | op.drop_index(op.f('ix_rule_name'), table_name='rule') 85 | op.drop_index(op.f('ix_rule_id'), table_name='rule') 86 | op.drop_table('rule') 87 | # ### end Alembic commands ### 88 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alembic" 3 | version = "1.8.1" 4 | description = "A database migration tool for SQLAlchemy." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.7" 8 | 9 | [package.dependencies] 10 | importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} 11 | importlib-resources = {version = "*", markers = "python_version < \"3.9\""} 12 | Mako = "*" 13 | SQLAlchemy = ">=1.3.0" 14 | 15 | [package.extras] 16 | tz = ["python-dateutil"] 17 | 18 | [[package]] 19 | name = "altgraph" 20 | version = "0.17.3" 21 | description = "Python graph (network) package" 22 | category = "dev" 23 | optional = false 24 | python-versions = "*" 25 | 26 | [[package]] 27 | name = "certifi" 28 | version = "2022.9.24" 29 | description = "Python package for providing Mozilla's CA Bundle." 30 | category = "main" 31 | optional = false 32 | python-versions = ">=3.6" 33 | 34 | [[package]] 35 | name = "charset-normalizer" 36 | version = "2.1.1" 37 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 38 | category = "main" 39 | optional = false 40 | python-versions = ">=3.6.0" 41 | 42 | [package.extras] 43 | unicode-backport = ["unicodedata2"] 44 | 45 | [[package]] 46 | name = "click" 47 | version = "8.1.3" 48 | description = "Composable command line interface toolkit" 49 | category = "main" 50 | optional = false 51 | python-versions = ">=3.7" 52 | 53 | [package.dependencies] 54 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 55 | 56 | [[package]] 57 | name = "colorama" 58 | version = "0.4.6" 59 | description = "Cross-platform colored terminal text." 60 | category = "main" 61 | optional = false 62 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 63 | 64 | [[package]] 65 | name = "commonmark" 66 | version = "0.9.1" 67 | description = "Python parser for the CommonMark Markdown spec" 68 | category = "main" 69 | optional = false 70 | python-versions = "*" 71 | 72 | [package.extras] 73 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 74 | 75 | [[package]] 76 | name = "future" 77 | version = "0.18.2" 78 | description = "Clean single-source support for Python 3 and 2" 79 | category = "dev" 80 | optional = false 81 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 82 | 83 | [[package]] 84 | name = "greenlet" 85 | version = "2.0.0" 86 | description = "Lightweight in-process concurrent programming" 87 | category = "main" 88 | optional = false 89 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 90 | 91 | [package.extras] 92 | docs = ["Sphinx", "docutils (<0.18)"] 93 | test = ["faulthandler", "objgraph"] 94 | 95 | [[package]] 96 | name = "idna" 97 | version = "3.4" 98 | description = "Internationalized Domain Names in Applications (IDNA)" 99 | category = "main" 100 | optional = false 101 | python-versions = ">=3.5" 102 | 103 | [[package]] 104 | name = "importlib-metadata" 105 | version = "5.0.0" 106 | description = "Read metadata from Python packages" 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.7" 110 | 111 | [package.dependencies] 112 | zipp = ">=0.5" 113 | 114 | [package.extras] 115 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 116 | perf = ["ipython"] 117 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 118 | 119 | [[package]] 120 | name = "importlib-resources" 121 | version = "5.10.0" 122 | description = "Read resources from Python packages" 123 | category = "main" 124 | optional = false 125 | python-versions = ">=3.7" 126 | 127 | [package.dependencies] 128 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 129 | 130 | [package.extras] 131 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 132 | testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 133 | 134 | [[package]] 135 | name = "macholib" 136 | version = "1.16.2" 137 | description = "Mach-O header analysis and editing" 138 | category = "dev" 139 | optional = false 140 | python-versions = "*" 141 | 142 | [package.dependencies] 143 | altgraph = ">=0.17" 144 | 145 | [[package]] 146 | name = "mako" 147 | version = "1.2.3" 148 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 149 | category = "main" 150 | optional = false 151 | python-versions = ">=3.7" 152 | 153 | [package.dependencies] 154 | MarkupSafe = ">=0.9.2" 155 | 156 | [package.extras] 157 | babel = ["Babel"] 158 | lingua = ["lingua"] 159 | testing = ["pytest"] 160 | 161 | [[package]] 162 | name = "markupsafe" 163 | version = "2.1.1" 164 | description = "Safely add untrusted strings to HTML/XML markup." 165 | category = "main" 166 | optional = false 167 | python-versions = ">=3.7" 168 | 169 | [[package]] 170 | name = "pefile" 171 | version = "2022.5.30" 172 | description = "Python PE parsing module" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=3.6.0" 176 | 177 | [package.dependencies] 178 | future = "*" 179 | 180 | [[package]] 181 | name = "ply" 182 | version = "3.11" 183 | description = "Python Lex & Yacc" 184 | category = "main" 185 | optional = false 186 | python-versions = "*" 187 | 188 | [[package]] 189 | name = "plyara" 190 | version = "2.1.1" 191 | description = "Parse YARA rules." 192 | category = "main" 193 | optional = false 194 | python-versions = "*" 195 | 196 | [package.dependencies] 197 | ply = ">=3.11" 198 | 199 | [[package]] 200 | name = "psycopg2" 201 | version = "2.9.5" 202 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 203 | category = "main" 204 | optional = true 205 | python-versions = ">=3.6" 206 | 207 | [[package]] 208 | name = "pygments" 209 | version = "2.13.0" 210 | description = "Pygments is a syntax highlighting package written in Python." 211 | category = "main" 212 | optional = false 213 | python-versions = ">=3.6" 214 | 215 | [package.extras] 216 | plugins = ["importlib-metadata"] 217 | 218 | [[package]] 219 | name = "pyinstaller" 220 | version = "4.10" 221 | description = "PyInstaller bundles a Python application and all its dependencies into a single package." 222 | category = "dev" 223 | optional = false 224 | python-versions = "<3.11,>=3.6" 225 | 226 | [package.dependencies] 227 | altgraph = "*" 228 | macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} 229 | pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} 230 | pyinstaller-hooks-contrib = ">=2020.6" 231 | pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} 232 | setuptools = "*" 233 | 234 | [package.extras] 235 | encryption = ["tinyaes (>=1.0.0)"] 236 | hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] 237 | 238 | [[package]] 239 | name = "pyinstaller-hooks-contrib" 240 | version = "2022.11" 241 | description = "Community maintained hooks for PyInstaller" 242 | category = "dev" 243 | optional = false 244 | python-versions = ">=3.7" 245 | 246 | [[package]] 247 | name = "pymysql" 248 | version = "1.0.2" 249 | description = "Pure Python MySQL Driver" 250 | category = "main" 251 | optional = true 252 | python-versions = ">=3.6" 253 | 254 | [package.extras] 255 | ed25519 = ["PyNaCl (>=1.4.0)"] 256 | rsa = ["cryptography"] 257 | 258 | [[package]] 259 | name = "pywin32-ctypes" 260 | version = "0.2.0" 261 | description = "" 262 | category = "dev" 263 | optional = false 264 | python-versions = "*" 265 | 266 | [[package]] 267 | name = "requests" 268 | version = "2.28.1" 269 | description = "Python HTTP for Humans." 270 | category = "main" 271 | optional = false 272 | python-versions = ">=3.7, <4" 273 | 274 | [package.dependencies] 275 | certifi = ">=2017.4.17" 276 | charset-normalizer = ">=2,<3" 277 | idna = ">=2.5,<4" 278 | urllib3 = ">=1.21.1,<1.27" 279 | 280 | [package.extras] 281 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 282 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 283 | 284 | [[package]] 285 | name = "rich" 286 | version = "11.2.0" 287 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 288 | category = "main" 289 | optional = false 290 | python-versions = ">=3.6.2,<4.0.0" 291 | 292 | [package.dependencies] 293 | colorama = ">=0.4.0,<0.5.0" 294 | commonmark = ">=0.9.0,<0.10.0" 295 | pygments = ">=2.6.0,<3.0.0" 296 | 297 | [package.extras] 298 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 299 | 300 | [[package]] 301 | name = "setuptools" 302 | version = "65.5.0" 303 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 304 | category = "dev" 305 | optional = false 306 | python-versions = ">=3.7" 307 | 308 | [package.extras] 309 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 310 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 311 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 312 | 313 | [[package]] 314 | name = "sqlalchemy" 315 | version = "1.4.42" 316 | description = "Database Abstraction Library" 317 | category = "main" 318 | optional = false 319 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 320 | 321 | [package.dependencies] 322 | greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} 323 | 324 | [package.extras] 325 | aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] 326 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 327 | asyncio = ["greenlet (!=0.4.17)"] 328 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] 329 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] 330 | mssql = ["pyodbc"] 331 | mssql-pymssql = ["pymssql"] 332 | mssql-pyodbc = ["pyodbc"] 333 | mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] 334 | mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] 335 | mysql-connector = ["mysql-connector-python"] 336 | oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] 337 | postgresql = ["psycopg2 (>=2.7)"] 338 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 339 | postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] 340 | postgresql-psycopg2binary = ["psycopg2-binary"] 341 | postgresql-psycopg2cffi = ["psycopg2cffi"] 342 | pymysql = ["pymysql", "pymysql (<1)"] 343 | sqlcipher = ["sqlcipher3_binary"] 344 | 345 | [[package]] 346 | name = "toml" 347 | version = "0.10.2" 348 | description = "Python Library for Tom's Obvious, Minimal Language" 349 | category = "main" 350 | optional = false 351 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 352 | 353 | [[package]] 354 | name = "urllib3" 355 | version = "1.26.12" 356 | description = "HTTP library with thread-safe connection pooling, file post, and more." 357 | category = "main" 358 | optional = false 359 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 360 | 361 | [package.extras] 362 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 363 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 364 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 365 | 366 | [[package]] 367 | name = "yara-python" 368 | version = "4.2.3" 369 | description = "Python interface for YARA" 370 | category = "main" 371 | optional = false 372 | python-versions = "*" 373 | 374 | [[package]] 375 | name = "yarabuilder" 376 | version = "0.0.6" 377 | description = "A package to build YARA rules using Python" 378 | category = "main" 379 | optional = false 380 | python-versions = ">=3.6" 381 | 382 | [[package]] 383 | name = "zipp" 384 | version = "3.10.0" 385 | description = "Backport of pathlib-compatible object wrapper for zip files" 386 | category = "main" 387 | optional = false 388 | python-versions = ">=3.7" 389 | 390 | [package.extras] 391 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 392 | testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 393 | 394 | [extras] 395 | mysql = ["PyMySQL"] 396 | pgsql = ["psycopg2"] 397 | 398 | [metadata] 399 | lock-version = "1.1" 400 | python-versions = "^3.8,<3.11" 401 | content-hash = "9247d2841b6f22287eeb6a3bc5003b7c6100ebacc732ac37f41e846ebd6905d7" 402 | 403 | [metadata.files] 404 | alembic = [ 405 | {file = "alembic-1.8.1-py3-none-any.whl", hash = "sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4"}, 406 | {file = "alembic-1.8.1.tar.gz", hash = "sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa"}, 407 | ] 408 | altgraph = [ 409 | {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, 410 | {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, 411 | ] 412 | certifi = [ 413 | {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, 414 | {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, 415 | ] 416 | charset-normalizer = [ 417 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 418 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 419 | ] 420 | click = [ 421 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 422 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 423 | ] 424 | colorama = [ 425 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 426 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 427 | ] 428 | commonmark = [ 429 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 430 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 431 | ] 432 | future = [ 433 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 434 | ] 435 | greenlet = [ 436 | {file = "greenlet-2.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4be4dedbd2fa9b7c35627f322d6d3139cb125bc18d5ef2f40237990850ea446f"}, 437 | {file = "greenlet-2.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:75c022803de010294366f3608d4bba3e346693b1b7427b79d57e3d924ed03838"}, 438 | {file = "greenlet-2.0.0-cp27-cp27m-win32.whl", hash = "sha256:4a1953465b7651073cffde74ed7d121e602ef9a9740d09ee137b01879ac15a2f"}, 439 | {file = "greenlet-2.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a65205e6778142528978b4acca76888e7e7f0be261e395664e49a5c21baa2141"}, 440 | {file = "greenlet-2.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d71feebf5c8041c80dfda76427e14e3ca00bca042481bd3e9612a9d57b2cbbf7"}, 441 | {file = "greenlet-2.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f7edbd2957f72aea357241fe42ffc712a8e9b8c2c42f24e2ef5d97b255f66172"}, 442 | {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79687c48e7f564be40c46b3afea6d141b8d66ffc2bc6147e026d491c6827954a"}, 443 | {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a245898ec5e9ca0bc87a63e4e222cc633dc4d1f1a0769c34a625ad67edb9f9de"}, 444 | {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adcf45221f253b3a681c99da46fa6ac33596fa94c2f30c54368f7ee1c4563a39"}, 445 | {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3dc294afebf2acfd029373dbf3d01d36fd8d6888a03f5a006e2d690f66b153d9"}, 446 | {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1cfeae4dda32eb5c64df05d347c4496abfa57ad16a90082798a2bba143c6c854"}, 447 | {file = "greenlet-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:d58d4b4dc82e2d21ebb7dd7d3a6d370693b2236a1407fe3988dc1d4ea07575f9"}, 448 | {file = "greenlet-2.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0d7efab8418c1fb3ea00c4abb89e7b0179a952d0d53ad5fcff798ca7440f8e8"}, 449 | {file = "greenlet-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8a10e14238407be3978fa6d190eb3724f9d766655fefc0134fd5482f1fb0108"}, 450 | {file = "greenlet-2.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:98b848a0b75e76b446dc71fdbac712d9078d96bb1c1607f049562dde1f8801e1"}, 451 | {file = "greenlet-2.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:8e8dbad9b4f4c3e37898914cfccb7c4f00dbe3146333cfe52a1a3103cc2ff97c"}, 452 | {file = "greenlet-2.0.0-cp35-cp35m-win32.whl", hash = "sha256:069a8a557541a04518dc3beb9a78637e4e6b286814849a2ecfac529eaa78562b"}, 453 | {file = "greenlet-2.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:cc211c2ff5d3b2ba8d557a71e3b4f0f0a2020067515143a9516ea43884271192"}, 454 | {file = "greenlet-2.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:d4e7642366e638f45d70c5111590a56fbd0ffb7f474af20c6c67c01270bcf5cf"}, 455 | {file = "greenlet-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e7a0dca752b4e3395890ab4085c3ec3838d73714261914c01b53ed7ea23b5867"}, 456 | {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8c67ecda450ad4eac7837057f5deb96effa836dacaf04747710ccf8eeb73092"}, 457 | {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cc1abaf47cfcfdc9ac0bdff173cebab22cd54e9e3490135a4a9302d0ff3b163"}, 458 | {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efdbbbf7b6c8d5be52977afa65b9bb7b658bab570543280e76c0fabc647175ed"}, 459 | {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7acaa51355d5b9549d474dc71be6846ee9a8f2cb82f4936e5efa7a50bbeb94ad"}, 460 | {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2be628bca0395610da08921f9376dd14317f37256d41078f5c618358467681e1"}, 461 | {file = "greenlet-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:eca9c0473de053dcc92156dd62c38c3578628b536c7f0cd66e655e211c14ac32"}, 462 | {file = "greenlet-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9a4a9fea68fd98814999d91ea585e49ed68d7e199a70bef13a857439f60a4609"}, 463 | {file = "greenlet-2.0.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:6b28420ae290bfbf5d827f976abccc2f74f0a3f5e4fb69b66acf98f1cbe95e7e"}, 464 | {file = "greenlet-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2b8e1c939b363292ecc93999fb1ad53ffc5d0aac8e933e4362b62365241edda5"}, 465 | {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c5ddadfe40e903c6217ed2b95a79f49e942bb98527547cc339fc7e43a424aad"}, 466 | {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e5ead803b11b60b347e08e0f37234d9a595f44a6420026e47bcaf94190c3cd6"}, 467 | {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b89b78ffb516c2921aa180c2794082666e26680eef05996b91f46127da24d964"}, 468 | {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:939963d0137ec92540d95b68b7f795e8dbadce0a1fca53e3e7ef8ddc18ee47cb"}, 469 | {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c1e93ef863810fba75faf418f0861dbf59bfe01a7b5d0a91d39603df58d3d3fa"}, 470 | {file = "greenlet-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:6fd342126d825b76bf5b49717a7c682e31ed1114906cdec7f5a0c2ff1bc737a7"}, 471 | {file = "greenlet-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5392ddb893e7fba237b988f846c4a80576557cc08664d56dc1a69c5c02bdc80c"}, 472 | {file = "greenlet-2.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b4fd73b62c1038e7ee938b1de328eaa918f76aa69c812beda3aff8a165494201"}, 473 | {file = "greenlet-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:0ba0f2e5c4a8f141952411e356dba05d6fe0c38325ee0e4f2d0c6f4c2c3263d5"}, 474 | {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8bacecee0c9348ab7c95df810e12585e9e8c331dfc1e22da4ed0bd635a5f483"}, 475 | {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:341053e0a96d512315c27c34fad4672c4573caf9eb98310c39e7747645c88d8b"}, 476 | {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fcdd8ae391ffabb3b672397b58a9737aaff6b8cae0836e8db8ff386fcea802"}, 477 | {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c3aa7d3bc545162a6676445709b24a2a375284dc5e2f2432d58b80827c2bd91c"}, 478 | {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d8dca31a39dd9f25641559b8cdf9066168c682dfcfbe0f797f03e4c9718a63a"}, 479 | {file = "greenlet-2.0.0-cp38-cp38-win32.whl", hash = "sha256:aa2b371c3633e694d043d6cec7376cb0031c6f67029f37eef40bda105fd58753"}, 480 | {file = "greenlet-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:0fa2a66fdf0d09929e79f786ad61529d4e752f452466f7ddaa5d03caf77a603d"}, 481 | {file = "greenlet-2.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:e7ec3f2465ba9b7d25895307abe1c1c101a257c54b9ea1522bbcbe8ca8793735"}, 482 | {file = "greenlet-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:99e9851e40150504474915605649edcde259a4cd9bce2fcdeb4cf33ad0b5c293"}, 483 | {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20bf68672ae14ef2e2e6d3ac1f308834db1d0b920b3b0674eef48b2dce0498dd"}, 484 | {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30198bccd774f9b6b1ba7564a0d02a79dd1fe926cfeb4107856fe16c9dfb441c"}, 485 | {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d65d7d1ff64fb300127d2ffd27db909de4d21712a5dde59a3ad241fb65ee83d7"}, 486 | {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5d396a5457458460b0c28f738fc8ab2738ee61b00c3f845c7047a333acd96c"}, 487 | {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09f00f9938eb5ae1fe203558b56081feb0ca34a2895f8374cd01129ddf4d111c"}, 488 | {file = "greenlet-2.0.0-cp39-cp39-win32.whl", hash = "sha256:089e123d80dbc6f61fff1ff0eae547b02c343d50968832716a7b0a33bea5f792"}, 489 | {file = "greenlet-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc283f99a4815ef70cad537110e3e03abcef56ab7d005ba9a8c6ec33054ce9c0"}, 490 | {file = "greenlet-2.0.0.tar.gz", hash = "sha256:6c66f0da8049ee3c126b762768179820d4c0ae0ca46ae489039e4da2fae39a52"}, 491 | ] 492 | idna = [ 493 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 494 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 495 | ] 496 | importlib-metadata = [ 497 | {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, 498 | {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, 499 | ] 500 | importlib-resources = [ 501 | {file = "importlib_resources-5.10.0-py3-none-any.whl", hash = "sha256:ee17ec648f85480d523596ce49eae8ead87d5631ae1551f913c0100b5edd3437"}, 502 | {file = "importlib_resources-5.10.0.tar.gz", hash = "sha256:c01b1b94210d9849f286b86bb51bcea7cd56dde0600d8db721d7b81330711668"}, 503 | ] 504 | macholib = [ 505 | {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, 506 | {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, 507 | ] 508 | mako = [ 509 | {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, 510 | {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, 511 | ] 512 | markupsafe = [ 513 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 514 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 515 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 516 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 517 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 518 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 519 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 520 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 521 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 522 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 523 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 524 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 525 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 526 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 527 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 528 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 529 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 530 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 531 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 532 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 533 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 534 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 535 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 536 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 537 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 538 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 539 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 540 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 541 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 542 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 543 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 544 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 545 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 546 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 547 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 548 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 549 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 550 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 551 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 552 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 553 | ] 554 | pefile = [ 555 | {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, 556 | ] 557 | ply = [ 558 | {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, 559 | {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, 560 | ] 561 | plyara = [ 562 | {file = "plyara-2.1.1-py3-none-any.whl", hash = "sha256:d6d65380e39c7afb83dd55f885063235622cee168eb45e79d87f002641a95b7c"}, 563 | {file = "plyara-2.1.1.tar.gz", hash = "sha256:9f5fcb4fc6b944a92ec2979dcc54d9cf3a02b166c99bfef9e72d534c7ab91e6d"}, 564 | ] 565 | psycopg2 = [ 566 | {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, 567 | {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, 568 | {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, 569 | {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, 570 | {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, 571 | {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, 572 | {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, 573 | {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, 574 | {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, 575 | {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, 576 | {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, 577 | ] 578 | pygments = [ 579 | {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, 580 | {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, 581 | ] 582 | pyinstaller = [ 583 | {file = "pyinstaller-4.10-py3-none-macosx_10_13_universal2.whl", hash = "sha256:15557cd1a79d182967f0a5040750e6902e13ebd6cab41e3ed84d7b28a306357b"}, 584 | {file = "pyinstaller-4.10-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f2166ff2cd95eefb0d377ae8d1071f186fa25edd410ede65b376162d5ec41909"}, 585 | {file = "pyinstaller-4.10-py3-none-manylinux2014_i686.whl", hash = "sha256:7d94518ba1f8e9a8577345312276891ad7d6cd9785e453e9951b35647e2c7078"}, 586 | {file = "pyinstaller-4.10-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:70c71e827f4b34602cbc7a0947a067b662c1cbdc4db51832e13b97cca3c54dd7"}, 587 | {file = "pyinstaller-4.10-py3-none-manylinux2014_s390x.whl", hash = "sha256:05c21117b84199272ebd355b556af4714f6e79245e1c435d6f16653786d7d17e"}, 588 | {file = "pyinstaller-4.10-py3-none-manylinux2014_x86_64.whl", hash = "sha256:714c4dcc319a41416744d1e30c6317405dfaed80d2adc45f8bfa70dc7367e664"}, 589 | {file = "pyinstaller-4.10-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:581620bdcd32f01e89b13231256b807bb090e7eadf40c81c864ec402afa4758a"}, 590 | {file = "pyinstaller-4.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d4f79c0a774451f12baca4e476376418f011fa3039dde8fd172ea2aa8ff67bad"}, 591 | {file = "pyinstaller-4.10-py3-none-win32.whl", hash = "sha256:cfed0b3a43e73550a43a094610328109564710b9514afa093ef7199d072cae87"}, 592 | {file = "pyinstaller-4.10-py3-none-win_amd64.whl", hash = "sha256:0dcaf6557cdb2da763c46e06e95a94a7634ab03fb09d91bc77988b01ee05c907"}, 593 | {file = "pyinstaller-4.10.tar.gz", hash = "sha256:7749c868d2e2dc84df7d6f65437226183c8a366f3a99bb2737785625c3a3cca1"}, 594 | ] 595 | pyinstaller-hooks-contrib = [ 596 | {file = "pyinstaller-hooks-contrib-2022.11.tar.gz", hash = "sha256:2e1870350bb9ef2e09c1c1bb30347eb3185c5ef38c040ed04190d6d0b4b5df62"}, 597 | {file = "pyinstaller_hooks_contrib-2022.11-py2.py3-none-any.whl", hash = "sha256:8db4064d9c127940455ec363bebbf74228be21cdafb44f7ea4f44cf434d9bcaa"}, 598 | ] 599 | pymysql = [ 600 | {file = "PyMySQL-1.0.2-py3-none-any.whl", hash = "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641"}, 601 | {file = "PyMySQL-1.0.2.tar.gz", hash = "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"}, 602 | ] 603 | pywin32-ctypes = [ 604 | {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, 605 | {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, 606 | ] 607 | requests = [ 608 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 609 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 610 | ] 611 | rich = [ 612 | {file = "rich-11.2.0-py3-none-any.whl", hash = "sha256:d5f49ad91fb343efcae45a2b2df04a9755e863e50413623ab8c9e74f05aee52b"}, 613 | {file = "rich-11.2.0.tar.gz", hash = "sha256:1a6266a5738115017bb64a66c59c717e7aa047b3ae49a011ede4abdeffc6536e"}, 614 | ] 615 | setuptools = [ 616 | {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, 617 | {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, 618 | ] 619 | sqlalchemy = [ 620 | {file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"}, 621 | {file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"}, 622 | {file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"}, 623 | {file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"}, 624 | {file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"}, 625 | {file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"}, 626 | {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"}, 627 | {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"}, 628 | {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"}, 629 | {file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"}, 630 | {file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"}, 631 | {file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"}, 632 | {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"}, 633 | {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"}, 634 | {file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"}, 635 | {file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"}, 636 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"}, 637 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"}, 638 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"}, 639 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"}, 640 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"}, 641 | {file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"}, 642 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"}, 643 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"}, 644 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"}, 645 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"}, 646 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"}, 647 | {file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"}, 648 | {file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"}, 649 | {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"}, 650 | {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"}, 651 | {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"}, 652 | {file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"}, 653 | {file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"}, 654 | {file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"}, 655 | {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"}, 656 | {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"}, 657 | {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"}, 658 | {file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"}, 659 | {file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"}, 660 | {file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"}, 661 | ] 662 | toml = [ 663 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 664 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 665 | ] 666 | urllib3 = [ 667 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 668 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 669 | ] 670 | yara-python = [ 671 | {file = "yara-python-4.2.3.tar.gz", hash = "sha256:31f6f6f2fdca4c5ddfeed7cc6d29afad6af7dc259dde284df2d7ea5ae15ee69a"}, 672 | {file = "yara_python-4.2.3-cp310-cp310-win32.whl", hash = "sha256:d7543ff7eb7e21a815d7bc1bf2fcca24cab3548184d3257c58916628f3c89b37"}, 673 | {file = "yara_python-4.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ec1042017b3c4e12d2999ed6a33d7807013ae16c487048464b98dabfe3a7b0"}, 674 | {file = "yara_python-4.2.3-cp35-cp35m-win32.whl", hash = "sha256:47a7de4d7dae04e5e146b79f26b7f0b7e9430cb92459b85db7f4341843974aac"}, 675 | {file = "yara_python-4.2.3-cp35-cp35m-win_amd64.whl", hash = "sha256:113f430c5189519e13fc07d8493e3f29a1464a9be06eb7ba1b76451da620a391"}, 676 | {file = "yara_python-4.2.3-cp36-cp36m-win32.whl", hash = "sha256:98040aa88d242632c75be87ac3a9958eb407ca30e85b513b9e22807af82ab1c8"}, 677 | {file = "yara_python-4.2.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aaad4cd4495b7605cb4e039473710ee87a151082171c4bb720086adbe548fb36"}, 678 | {file = "yara_python-4.2.3-cp37-cp37m-win32.whl", hash = "sha256:bef2f079acd459b852c0634f72cd41058766110d8900573b2d55be12d35d55db"}, 679 | {file = "yara_python-4.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:feac02291a584b846615aa9265f01fa458ec7e7087317ffa679bbb1a2baec85b"}, 680 | {file = "yara_python-4.2.3-cp38-cp38-win32.whl", hash = "sha256:90db22a471b512d1adb49cec97a1356a1cf7791beeb0acab74c3187f6e8679a6"}, 681 | {file = "yara_python-4.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:9f17e0572c49906d0b2a8f6ac20fcb46f17820a7408a5511744a844df4b2ec61"}, 682 | {file = "yara_python-4.2.3-cp39-cp39-win32.whl", hash = "sha256:d6c2de71b368da053599d734c031389815a70df4b667d6dc386d1335689717d6"}, 683 | {file = "yara_python-4.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:87125ede7fbc18ae65aab550f1a36f4ebf73bb828c5d7a3dd2bb99176f0faa15"}, 684 | ] 685 | yarabuilder = [ 686 | {file = "yarabuilder-0.0.6-py3-none-any.whl", hash = "sha256:86214036f6d4b20fee93f1cdaba7596e46b6e67ac611e711393fcc61b2f15a8c"}, 687 | {file = "yarabuilder-0.0.6.tar.gz", hash = "sha256:9354f78879a9cf8c7162736969b6298c4f6467af295332addf00ce91afb4734e"}, 688 | ] 689 | zipp = [ 690 | {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, 691 | {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, 692 | ] 693 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "yaramanager" 3 | version = "0.2.1" 4 | description = "CLI tool to manage your yara rules" 5 | authors = ["3c7 <3c7@posteo.de>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/3c7/yaramanager" 9 | repository = "https://github.com/3c7/yaramanager" 10 | keywords = ["yara", "rule", "manage"] 11 | classifiers = [ 12 | "Environment :: Console", 13 | "Development Status :: 3 - Alpha", 14 | "Intended Audience :: Information Technology", 15 | "Intended Audience :: Science/Research", 16 | "Intended Audience :: System Administrators", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Topic :: Security", 25 | "Topic :: Utilities" 26 | ] 27 | include = [ 28 | "LICENSE", 29 | "alembic/*", 30 | "alembic/versions/*", 31 | "resources/*" 32 | ] 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.8,<3.11" 36 | SQLAlchemy = "^1.4.2" 37 | plyara = "^2.1.1" 38 | yarabuilder = "^0.0.6" 39 | rich = "^11.2.0" 40 | alembic = "^1.5.8" 41 | click = "^8.0.4" 42 | toml = "^0.10.2" 43 | requests = "^2.25.1" 44 | yara-python = "^4.1.3" 45 | PyMySQL = {version = "^1.0.2", optional = true} 46 | psycopg2 = {version = "^2.9.3", optional = true} 47 | 48 | [tool.poetry.dev-dependencies] 49 | pyinstaller = "^4.9" 50 | 51 | [tool.poetry.extras] 52 | mysql = ["PyMySQL"] 53 | pgsql = ["psycopg2"] 54 | 55 | [build-system] 56 | requires = ["poetry-core>=1.0.0"] 57 | build-backend = "poetry.core.masonry.api" 58 | 59 | [tool.poetry.scripts] 60 | yaramanager = "yaramanager.commands.cli:cli" 61 | ym = "yaramanager.commands.cli:cli" 62 | -------------------------------------------------------------------------------- /resources/config.toml: -------------------------------------------------------------------------------- 1 | # This is the yaramanager configuration file. 2 | [yaramanager] 3 | ## Editor 4 | # editor contains the command used to start the editor. Note that this must be a list of the command and the needed 5 | # parameters, e.g. `editor = ["codium", "-w"]`. 6 | editor = [ "codium", "-w" ] 7 | ## Debug 8 | # Enables or disables debug output. 9 | debug = false 10 | ## Ruleset Meta Key 11 | # ruleset_meta_key contains the key which should be used for ruleset detection. 12 | ruleset_meta_key = "ruleset" 13 | 14 | ## Template 15 | # Template used for creating new rules. 16 | template = """ 17 | rule apt_ZZ_RuleTemplate : apt zz { 18 | meta: 19 | author = "My Name" 20 | description = "This is a rule template that will be used for creating new rules." 21 | tlp = "white" 22 | ruleset = "Template 1" 23 | 24 | strings: 25 | $x1 = "Must1" ascii 26 | $x2 = "Must2" ascii 27 | $x3 = "Must3" wide 28 | 29 | $s1 = "Should1" ascii 30 | $s2 = "Should2" ascii 31 | $s3 = "Should3" ascii 32 | $s4 = "Should4" ascii 33 | $s5 = "Should5" ascii 34 | condition: 35 | uint16(0) == 0x5a4d and ( 36 | all of ($x*) and 2 of ($s*) or 37 | all of ($s*) 38 | ) 39 | } 40 | """ 41 | 42 | ## DB 43 | # The DB section contains the database configuration. 44 | [yaramanager.db] 45 | # Selected 46 | # The currently selected db from the list of databases below 47 | selected = 0 48 | # Databases 49 | # A list of databases. Every database needs to define a driver and a path, such as 50 | # 51 | # [[yaramanager.db.databases]] 52 | # driver = "sqlite" 53 | # path = "/home/user/.config/yaramanager/data.db" 54 | # [[yaramanager.db.databases]] 55 | # driver = "mysql+pymysql" 56 | # path = "user:password@127.0.0.1/database" 57 | # [[yaramanager.db.databases]] 58 | # driver = "postgresql+psycopg2" 59 | # path = "user:password@127.0.0.1/database" 60 | 61 | # {init_database} 62 | 63 | ## Meta 64 | # This section contains all meta keys that should be printed in table views. The format is: 65 | # Heading = "meta key" 66 | [yaramanager.meta] 67 | Author = "author" 68 | TLP = "tlp" 69 | Created = "created" 70 | Modified = "modified" 71 | 72 | ## Ensure 73 | # Rule attributes to ensure (ym list --ensure, -e). ensure_meta contains a list of meta attributes, ensure_tag just 74 | # requires tags set. 75 | [yaramanager.ensure] 76 | ensure_meta = [ "author", "tlp", "description" ] 77 | ensure_tag = true 78 | -------------------------------------------------------------------------------- /yaramanager/__init__.py: -------------------------------------------------------------------------------- 1 | version = "v0.2.1" 2 | alembic_version = "25527d692d46" 3 | -------------------------------------------------------------------------------- /yaramanager/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .cli import cli -------------------------------------------------------------------------------- /yaramanager/commands/add.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import click 5 | from rich.progress import Progress 6 | 7 | from yaramanager.db.base import Rule 8 | from yaramanager.db.session import get_session 9 | from yaramanager.utils.utils import parse_rule_file, plyara_obj_to_rule 10 | 11 | 12 | @click.command(help="Add a new rule to the database.") 13 | @click.option("--ruleset", "-R", is_flag=True, default=False, 14 | help="Creates a ruleset of rules given in a single file based on the filename.") 15 | @click.argument("paths", type=click.Path(exists=True, dir_okay=False), nargs=-1) 16 | def add(ruleset: bool, paths: List[str]): 17 | session = get_session() 18 | with Progress() as progress: 19 | t1 = progress.add_task("Processing rule files...", total=len(paths)) 20 | for rule_path in paths: 21 | progress.console.print(f"Processing {os.path.basename(rule_path)}...") 22 | plyara_list = parse_rule_file(rule_path) 23 | for plyara_obj in plyara_list: 24 | r = plyara_obj_to_rule( 25 | plyara_obj, 26 | session, 27 | overwrite_ruleset=os.path.basename(rule_path).split(".yar")[0] if ruleset else None 28 | ) 29 | available_rules_count = session.query(Rule.id).filter(Rule.name == r.name).count() 30 | if available_rules_count > 1: 31 | progress.console.print(f"Rule {r.name} already {available_rules_count - 1} time(s) in the db. " 32 | f"You should rename the new rule!", style="bold red") 33 | session.add(r) 34 | progress.update(t1, advance=1) 35 | session.commit() 36 | -------------------------------------------------------------------------------- /yaramanager/commands/cli.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import click 4 | from rich.console import Console 5 | 6 | from .add import add 7 | from .config import config 8 | from .db import db 9 | from .delete import delete 10 | from .edit import edit 11 | from .export import export 12 | from .get import get 13 | from .list import list 14 | from .new import new 15 | from .parse import parse 16 | from .read import read 17 | from .ruleset import ruleset 18 | from .scan import scan 19 | from .search import search 20 | from .stats import stats 21 | from .tags import tags 22 | from .version import version 23 | 24 | 25 | @click.group( 26 | help="ym - yaramanager. Use the commands shown below to manage your yara ruleset. By default, the manager " 27 | "uses codium as editor. You can change that in the config file or using EDITOR environment variable. " 28 | "When using editors in the console, you might want to disable the status display using DISABLE_STATUS. You can " 29 | "overwrite the general ym path with YM_PATH and the config path with YM_CONFIG." 30 | ) 31 | def cli(): 32 | pass 33 | 34 | 35 | cli.add_command(add) 36 | cli.add_command(config) 37 | cli.add_command(db) 38 | cli.add_command(delete) 39 | cli.add_command(edit) 40 | cli.add_command(export) 41 | cli.add_command(get) 42 | cli.add_command(list) 43 | cli.add_command(new) 44 | cli.add_command(parse) 45 | cli.add_command(read) 46 | cli.add_command(ruleset) 47 | cli.add_command(scan) 48 | cli.add_command(search) 49 | cli.add_command(stats) 50 | cli.add_command(tags) 51 | cli.add_command(version) 52 | 53 | 54 | @cli.command(help="Displays help about commands") 55 | @click.argument("cmds", nargs=-1) 56 | def help(cmds: List[str]): 57 | c, ec = Console(), Console(stderr=True, style="bold red") 58 | ctx = click.get_current_context() 59 | if not cmds or len(cmds) == 0: 60 | print(cli.get_help(ctx)) 61 | command = cli 62 | for cmd in cmds: 63 | if not isinstance(command, click.Group): 64 | ec.print("Command not found.") 65 | exit(-1) 66 | command = command.commands.get(cmd, None) 67 | if not command: 68 | ec.print("Command not found.") 69 | exit(-1) 70 | with click.Context(command) as ctx: 71 | c.print(ctx.get_help()) 72 | -------------------------------------------------------------------------------- /yaramanager/commands/config.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import click 4 | from rich.console import Console 5 | from rich.prompt import Confirm 6 | from rich.syntax import Syntax 7 | 8 | from yaramanager.config import load_config, config_file, write_initial_config 9 | from yaramanager.utils.utils import open_file 10 | 11 | CONFIG = load_config() 12 | 13 | 14 | @click.group(help="Review and change yaramanager configuration.") 15 | def config(): 16 | pass 17 | 18 | 19 | @config.command(help="Get single config entry by key.") 20 | @click.argument("key") 21 | def get(key): 22 | c, ec = Console(), Console(stderr=True, style="bold red") 23 | if key in CONFIG.keys(): 24 | c.print(CONFIG[key]) 25 | else: 26 | ec.print("Config key not found") 27 | 28 | 29 | @config.command(help=f"Edit your config with an external editor. The config file can be found here: {config_file}. " 30 | f"If you don't use codium as default editor, you need to change the according key in the config " 31 | f"or use the environment variable EDITOR.") 32 | def edit(): 33 | open_file(config_file, status="Config file opened in external editor...") 34 | 35 | 36 | @config.command(help="Prints the current config to stdout.") 37 | def dump(): 38 | c = Console() 39 | with io.open(config_file) as fh: 40 | syntax = Syntax(fh.read(), "toml", background_color="default") 41 | c.print(syntax) 42 | 43 | 44 | @config.command(help="Resets the configuration.") 45 | def reset(): 46 | confirm = Confirm.ask("Do you really want to reset the config?") 47 | if confirm: 48 | write_initial_config() 49 | -------------------------------------------------------------------------------- /yaramanager/commands/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from alembic import command 5 | from alembic.config import Config 6 | from rich.console import Console 7 | from rich.prompt import Confirm 8 | 9 | from yaramanager.config import load_config, change_db 10 | from yaramanager.db.session import get_path 11 | from yaramanager.utils.output import debug_print 12 | 13 | 14 | @click.group(help="Manage your databases") 15 | def db(): 16 | pass 17 | 18 | 19 | @db.command(help="Returns info about the selected database.") 20 | def get(): 21 | c = Console() 22 | config = load_config() 23 | c.print(f"Selected database: {config['db']['databases'][config['db']['selected']]['path']}", highlight=False) 24 | 25 | 26 | @db.command(help="Changes database.") 27 | @click.argument("db_num", type=int) 28 | def set(db_num): 29 | c, ec = Console(), Console(stderr=True, style="bold red") 30 | config = load_config() 31 | if db_num > len(config["db"]["databases"]) - 1: 32 | ec.print("Number of DB to chose is higher than number of available databases.") 33 | exit(-1) 34 | change_db(db_num) 35 | 36 | 37 | @db.command(help="Database initialization or schema upgrade.") 38 | def upgrade(): 39 | c = Console() 40 | db_path = get_path() 41 | base_path = os.path.abspath(os.path.join(__file__, "..", "..", "..")) 42 | script_path = os.path.join(base_path, "alembic") 43 | config_path = os.path.join(base_path, "alembic", "alembic.ini") 44 | debug_print(f"Accessing alembic files in {script_path} and {config_path}.", c) 45 | do_upgrade = Confirm.ask(f"Upgrade database {db_path}?") 46 | if do_upgrade: 47 | c.print(f"Using scripts in {script_path} to migrate...") 48 | a_cfg = Config(config_path) 49 | a_cfg.set_main_option("script_location", script_path) 50 | a_cfg.set_main_option("sqlalchemy.url", db_path) 51 | command.upgrade(a_cfg, "head") 52 | -------------------------------------------------------------------------------- /yaramanager/commands/delete.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import Union 4 | 5 | import click 6 | from rich.prompt import Confirm 7 | 8 | from yaramanager.db.base import Rule 9 | from yaramanager.db.session import get_session 10 | 11 | 12 | @click.command("del", help="Delete a rule by its ID or name. Can delete multiple rules using the name.") 13 | @click.argument("identifier") 14 | def delete(identifier: Union[int, str]): 15 | session = get_session() 16 | rule = session.query(Rule) 17 | if isinstance(identifier, int) or re.fullmatch(r"^\d+$", identifier): 18 | rule = rule.filter(Rule.id == int(identifier)) 19 | else: 20 | rule = rule.filter(Rule.name.like(f"%{identifier}%")) 21 | rule = rule.all() 22 | rule_names = ", ".join([r.name for r in rule]) 23 | confirmed = Confirm.ask(f"Do you really want to delete the following rules: {rule_names}") 24 | if confirmed: 25 | for r in rule: 26 | session.delete(r) 27 | session.commit() 28 | -------------------------------------------------------------------------------- /yaramanager/commands/edit.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sys import stderr 3 | from typing import Union 4 | 5 | import click 6 | from rich.console import Console 7 | from yarabuilder import YaraBuilder 8 | 9 | from yaramanager.db.session import get_session 10 | from yaramanager.utils.utils import ( 11 | get_md5, 12 | write_ruleset_to_tmp_file, 13 | get_rule_by_identifier, 14 | read_rulefile, 15 | plyara_object_to_meta, 16 | plyara_object_to_strings, 17 | plyara_object_to_condition, 18 | plyara_object_to_imports, 19 | plyara_object_to_tags, 20 | open_file, 21 | plyara_object_to_ruleset 22 | ) 23 | 24 | 25 | @click.command(help="Edits a rule with your default editor. " 26 | "Identifier can be part of a rule name or the specific ID.") 27 | @click.argument("identifier") 28 | def edit(identifier: Union[int, str]): 29 | c, ec = Console(), Console(file=stderr, style="bold red") 30 | session = get_session() 31 | rule = get_rule_by_identifier(identifier, session) 32 | if len(rule) > 1: 33 | ec.print(f"Found more than one rule.") 34 | exit(-1) 35 | rule = rule[0] 36 | yb = YaraBuilder() 37 | rule.add_to_yarabuilder(yb) 38 | path, _ = write_ruleset_to_tmp_file(yb) 39 | hash = get_md5(path) 40 | open_file(path, f"{rule.name} opened in external editor...") 41 | edit_hash = get_md5(path) 42 | 43 | if hash == edit_hash: 44 | c.print(f"No change detected...") 45 | else: 46 | c.print(f"Change detected, updating rule...") 47 | edited_rule = read_rulefile(path) 48 | if not 0 < len(edited_rule) < 2: 49 | ec.print("Edited rule file must contain exactly one yara rule.") 50 | exit(-1) 51 | rule.name = edited_rule[0].get("rule_name", "Unnamed rule") 52 | rule.meta = plyara_object_to_meta(edited_rule[0]) 53 | rule.imports = plyara_object_to_imports(edited_rule[0]) 54 | rule.strings = plyara_object_to_strings(edited_rule[0]) 55 | rule.tags = plyara_object_to_tags(edited_rule[0], session) 56 | rule.condition = plyara_object_to_condition(edited_rule[0]) 57 | rule.ruleset = plyara_object_to_ruleset(edited_rule[0], session) 58 | session.commit() 59 | os.remove(path) 60 | -------------------------------------------------------------------------------- /yaramanager/commands/export.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Tuple 3 | 4 | import click 5 | from rich.console import Console 6 | 7 | from yaramanager.db.session import get_session 8 | from yaramanager.models.yarabuilder import YaraBuilder 9 | from yaramanager.utils.utils import filter_rules_by_name_and_tag 10 | 11 | 12 | @click.command(help="Export rules from the database. The set of rules can be filtered through commandline options. " 13 | "Rules will be written in to separate files if -s not given.") 14 | @click.option("-n", "--name", help="Rule name must include [NAME].") 15 | @click.option("--tag", "-t", multiple=True, help="Only export rules with given tag.") 16 | @click.option("--exclude-tag", "-T", multiple=True, help="Exclude rules with given tag.") 17 | @click.option("-s", "--single", is_flag=True, help="Write set of rules into a single yara file.") 18 | @click.option("-c", "--compiled", is_flag=True, help="Write compiled ruleset into a single file.") 19 | @click.argument("path", type=click.Path(dir_okay=True, file_okay=True, writable=True)) 20 | def export(name: str, tag: Tuple[str], exclude_tag: Tuple[str], single: bool, compiled: bool, path: str): 21 | c, ec = Console(), Console(stderr=True, style="bold red") 22 | session = get_session() 23 | rules, count = filter_rules_by_name_and_tag(name, tag, exclude_tag, session) 24 | path = Path(path) 25 | 26 | if count == 0: 27 | ec.print(f"Found no matching rules.") 28 | exit(-1) 29 | 30 | if single and path.is_dir(): 31 | ec.print(f"Given path ({path}) is a directory.") 32 | exit(-1) 33 | 34 | yb = YaraBuilder() 35 | for rule in rules: 36 | rule.add_to_yarabuilder(yb) 37 | 38 | yb.write_rules_to_file(path, single_file=single, compiled=compiled) 39 | c.print(f"Wrote {len(rules)} rules.") 40 | -------------------------------------------------------------------------------- /yaramanager/commands/get.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import click 4 | from rich.console import Console 5 | from yarabuilder import YaraBuilder 6 | 7 | from yaramanager.utils.utils import get_rule_by_identifier 8 | 9 | 10 | @click.command(help="Get rules from the database.") 11 | @click.argument("identifier") 12 | @click.option("-o", "--output", help="Write output to file.") 13 | def get(identifier, output): 14 | c = Console() 15 | rules = get_rule_by_identifier(identifier) 16 | if len(rules) == 0: 17 | c.print("Query returned empty list of Rules.") 18 | exit(-1) 19 | 20 | yb = YaraBuilder() 21 | _ = [rule.add_to_yarabuilder(yb) for rule in rules] 22 | 23 | if output and len(output) > 0: 24 | with io.open(output, "w") as fh: 25 | fh.write(yb.build_rules()) 26 | exit(0) 27 | 28 | # Simple print because rich.Console adjusts to terminal size and might cut something or mess with the format for 29 | # readability 30 | print(yb.build_rules()) 31 | -------------------------------------------------------------------------------- /yaramanager/commands/list.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | from typing import Tuple 3 | 4 | import click 5 | from rich.console import Console 6 | 7 | from yaramanager.db.session import get_session 8 | from yaramanager.utils.utils import ( 9 | rules_to_table, 10 | filter_rules_by_name_and_tag, 11 | rules_to_highlighted_string, 12 | get_ruleset_by_identifier 13 | ) 14 | 15 | 16 | @click.command(help="Lists rules available in DB. Default output is in a table, but raw output can be enabled.") 17 | @click.option("--tag", "-t", multiple=True, help="Only display rules with given tag.") 18 | @click.option("--exclude-tag", "-T", multiple=True, help="Exclude rules with given tag.") 19 | @click.option("--raw", "-r", is_flag=True, help="Print rules to stdout.") 20 | @click.option("--name", "-n", help="Only display rules containing [NAME].") 21 | @click.option("--ensure", "-e", is_flag=True, help="Ensure meta fields and tags.") 22 | @click.option("--assign", "-a", help="Assign listed rules to ruleset. This has to be either a legitimate Ruleset id or " 23 | "a Ruleset name.") 24 | def list(tag: Tuple[str], exclude_tag: Tuple[str], raw: bool, name: str, ensure: bool, assign: str): 25 | c, ec = Console(), Console(stderr=True, style="bold red") 26 | session = get_session() 27 | rules, count = filter_rules_by_name_and_tag(name, tag, exclude_tag, session, not raw) 28 | 29 | if count == 0: 30 | ec.print(f"Query returned empty list of rules.") 31 | exit(-1) 32 | 33 | if assign and len(assign) > 0: 34 | ruleset = get_ruleset_by_identifier(assign, session) 35 | if not ruleset: 36 | ec.print("Ruleset not found.") 37 | exit(-1) 38 | 39 | for rule in rules: 40 | rule.ruleset = ruleset 41 | session.commit() 42 | if raw: 43 | c.print(rules_to_highlighted_string(rules)) 44 | else: 45 | c.print(rules_to_table(rules, ensure=ensure)) 46 | -------------------------------------------------------------------------------- /yaramanager/commands/new.py: -------------------------------------------------------------------------------- 1 | import click 2 | from yaramanager.db.session import get_session 3 | from yaramanager.utils.utils import open_temp_file_with_template, read_rulefile, plyara_obj_to_rule 4 | from rich.console import Console 5 | from os import remove 6 | 7 | @click.command(help="Create a new rule using you preferred editor.") 8 | def new(): 9 | c = Console() 10 | session = get_session() 11 | path = open_temp_file_with_template() 12 | plyara_list = read_rulefile(path) 13 | for plyara_object in plyara_list: 14 | rule = plyara_obj_to_rule(plyara_object, session) 15 | session.add(rule) 16 | c.print(f"Rule {rule.name} added to database.") 17 | session.commit() 18 | remove(path) 19 | -------------------------------------------------------------------------------- /yaramanager/commands/parse.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | 3 | import click 4 | 5 | from yaramanager.utils.utils import print_rule, parse_rule_file 6 | 7 | 8 | @click.command(help="Parses rule files. This is mainly used for development and debugging purposes.", deprecated=True) 9 | @click.option("--raw", "-r", is_flag=True, help="Print plyara output instead of yara.") 10 | @click.argument("path") 11 | def parse(raw: bool, path: str): 12 | if not raw: 13 | print(print_rule(parse_rule_file(path))) 14 | else: 15 | pprint(parse_rule_file(path)) 16 | -------------------------------------------------------------------------------- /yaramanager/commands/read.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | from rich.console import Console 5 | 6 | from yaramanager.db.session import get_session 7 | from yaramanager.utils.utils import parse_rule, plyara_obj_to_rule 8 | 9 | 10 | @click.command(help="Read rules from stdin.") 11 | def read(): 12 | c = Console() 13 | session = get_session() 14 | stdin = "" 15 | for line in sys.stdin: 16 | stdin += line 17 | plyara_list = parse_rule(stdin) 18 | for plyara_obj in plyara_list: 19 | rule = plyara_obj_to_rule(plyara_obj, session) 20 | session.add(rule) 21 | c.print(f"Added rule {rule.name} from stdin.") 22 | session.commit() 23 | -------------------------------------------------------------------------------- /yaramanager/commands/ruleset.py: -------------------------------------------------------------------------------- 1 | import io 2 | from pathlib import Path 3 | 4 | import click 5 | from rich.console import Console 6 | from rich.table import Table 7 | 8 | from yaramanager.db.base import Ruleset 9 | from yaramanager.db.session import get_session 10 | from yaramanager.models.yarabuilder import YaraBuilder 11 | from yaramanager.utils.utils import get_ruleset_by_identifier, rules_to_table 12 | 13 | 14 | @click.group(help="Manage your rulesets") 15 | def ruleset(): 16 | pass 17 | 18 | 19 | @ruleset.command("list", help="Print a list of your rulests.") 20 | def ruleset_list(): 21 | c = Console() 22 | session = get_session() 23 | rulesets = session.query(Ruleset).all() 24 | if len(rulesets) == 0: 25 | c.print("No rulesets found.") 26 | exit(-1) 27 | 28 | t = Table() 29 | t.add_column("ID") 30 | t.add_column("Name") 31 | t.add_column("Number of rules") 32 | for ruleset in rulesets: 33 | t.add_row( 34 | str(ruleset.id), 35 | ruleset.name, 36 | str(len(ruleset.rules)) 37 | ) 38 | c.print(t) 39 | 40 | 41 | @ruleset.command(help="Get all rules assigned to a ruleset.") 42 | @click.option("-r", "--raw", is_flag=True, help="Print rules to stdout") 43 | @click.argument("identifier") 44 | def get(raw: bool, identifier: str): 45 | c = Console() 46 | session = get_session() 47 | ruleset = get_ruleset_by_identifier(identifier, session) 48 | if not ruleset: 49 | c.print("Ruleset not found.") 50 | exit(-1) 51 | 52 | if not raw: 53 | c.print(rules_to_table(ruleset.rules)) 54 | else: 55 | yb = YaraBuilder() 56 | for rule in ruleset.rules: 57 | rule.add_to_yarabuilder(yb) 58 | c.print(yb.build_rules()) 59 | 60 | 61 | @ruleset.command(help="Export all rules assigned to a ruleset. " 62 | "Without -s and -c set, all rules are written as separate files.") 63 | @click.argument("identifier") 64 | @click.option("-s", "--single", is_flag=True, help="Write set of rules into a single yara file.") 65 | @click.option("-c", "--compiled", is_flag=True, help="Write compiled ruleset into a single file.") 66 | @click.argument("path", type=click.Path(dir_okay=True, file_okay=True, writable=True)) 67 | def export(identifier: str, single: bool, compiled: bool, path: str): 68 | c = Console() 69 | session = get_session() 70 | ruleset = get_ruleset_by_identifier(identifier, session) 71 | if not ruleset: 72 | c.print("Ruleset not found.") 73 | exit(-1) 74 | 75 | path = Path(path) 76 | if len(ruleset.rules) == 0: 77 | c.print(f"Found no matching rules.") 78 | exit(-1) 79 | 80 | yb = YaraBuilder() 81 | for rule in ruleset.rules: 82 | try: 83 | rule.add_to_yarabuilder(yb) 84 | except ValueError as e: 85 | ruleset.rules.remove(rule) 86 | yb.yara_rules.popitem() 87 | c.print(f"Error:{rule.name} not exported: {e}") 88 | 89 | yb.write_rules_to_file(path, single_file=single, compiled=compiled) 90 | c.print(f"Wrote {len(ruleset.rules)} rules.") 91 | 92 | 93 | @ruleset.command(help="Create a new ruleset.") 94 | @click.argument("name") 95 | def create(name: str): 96 | c, ec = Console(), Console(stderr=True, style="bold red") 97 | session = get_session() 98 | ruleset = session.query(Ruleset).filter(Ruleset.name == name).first() 99 | if ruleset: 100 | ec.print("Ruleset with that name already exists.") 101 | exit(-1) 102 | 103 | ruleset = Ruleset( 104 | name=name 105 | ) 106 | session.add(ruleset) 107 | session.commit() 108 | c.print("New ruleset added.") 109 | -------------------------------------------------------------------------------- /yaramanager/commands/scan.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Tuple 3 | 4 | import click 5 | import yara 6 | from rich.console import Console, Text 7 | from rich.progress import Progress 8 | from yarabuilder import YaraBuilder 9 | 10 | from yaramanager.db.session import get_session 11 | from yaramanager.utils.utils import filter_rules_by_name_and_tag, write_ruleset_to_tmp_file 12 | 13 | 14 | @click.command(help="Scan files using your rulesets. Please be aware that this program should not be used anywhere " 15 | "where performance matters. This is dead slow, single thread scanning. Useful for fast sample " 16 | "classifications and checking your rule coverage, but not for scanning large filesets.") 17 | @click.option("--tag", "-t", multiple=True, help="Only use rules with given tag.") 18 | @click.option("--exclude-tag", "-T", multiple=True, help="Exclude rules with given tag.") 19 | @click.option("-n", "--name", help="Only use rules contain [NAME] in name.") 20 | @click.option("--timeout", default=0, type=int, help="Set timeout in seconds.") 21 | @click.option("-p", "--no-progress", is_flag=True, help="Disable progress bar.") 22 | @click.option("-c", "--csv", is_flag=True, help="CSV like output. Use together with -p!") 23 | @click.option("-r", "--recursive", is_flag=True, help="Scans directories recursively.") 24 | @click.argument("paths", type=click.Path(exists=True, file_okay=True, dir_okay=True), nargs=-1) 25 | def scan(tag: Tuple[str], exclude_tag: Tuple[str], name: str, timeout: int, no_progress: bool, csv: bool, 26 | recursive: bool, paths: List[str]): 27 | c, ec = Console(), Console(style="bold red", stderr=True) 28 | if len(paths) == 0: 29 | with click.Context(scan) as ctx: 30 | c.print(scan.get_help(ctx)) 31 | exit(-1) 32 | session = get_session() 33 | rules, count = filter_rules_by_name_and_tag(name, tag, exclude_tag, session) 34 | if count == 0: 35 | ec.print("No rules matching your criteria.") 36 | exit(-1) 37 | 38 | yb = YaraBuilder() 39 | for rule in rules: 40 | rule.add_to_yarabuilder(yb) 41 | ruleset_path, _ = write_ruleset_to_tmp_file(yb) 42 | 43 | # Initialize external parameter filename as empty string. This will be filled during matching files. 44 | ruleset_compiled = yara.compile(ruleset_path, externals={ 45 | "filename": "" 46 | }) 47 | if not no_progress: 48 | c.print(f"Using ruleset {ruleset_path} for scanning. Check the rule file in case any error shows up.") 49 | if recursive: 50 | list_of_files = [] 51 | for path in paths: 52 | list_of_files.extend(get_dir_recursive(path)) 53 | paths = list_of_files 54 | with Progress() if not no_progress else c as prog: 55 | if isinstance(prog, Progress): 56 | t1 = prog.add_task("Scanning...", total=len(paths)) 57 | for path in paths: 58 | if isinstance(prog, Progress): 59 | prog.update(t1, advance=1, description=f"Scanning {path}...") 60 | if os.path.isdir(path): 61 | continue 62 | try: 63 | # Check if file matches. This also adds the basename as filename parameter. 64 | matches = ruleset_compiled.match(path, timeout=timeout, externals={ 65 | "filename": os.path.basename(path) 66 | }) 67 | except yara.TimeoutError: 68 | prog.print(Text("Timed out!", style="bold red")) 69 | if isinstance(prog, Progress): 70 | prog.update(t1, description="Timed out!") 71 | exit(-1) 72 | for match in matches: 73 | if csv: 74 | prog.print(f'"{match.rule}","{",".join(match.tags)}","{path}"') 75 | else: 76 | prog.print(f"{match.rule} ({', '.join(match.tags)}): {path}", highlight=not no_progress) 77 | if isinstance(prog, Progress): 78 | prog.update(t1, description="Finished scanning!") 79 | os.remove(ruleset_path) 80 | 81 | 82 | def get_dir_recursive(path: str) -> List[str]: 83 | """Grabbs all files from a given path recursively.""" 84 | files = [] 85 | for dir_entry in os.scandir(path): 86 | if dir_entry.is_dir(follow_symlinks=True): 87 | files.extend(get_dir_recursive(dir_entry)) 88 | else: 89 | files.append(dir_entry.path) 90 | return files 91 | -------------------------------------------------------------------------------- /yaramanager/commands/search.py: -------------------------------------------------------------------------------- 1 | import click 2 | from rich.console import Console 3 | from rich.syntax import Syntax 4 | from rich.table import Table 5 | from sqlalchemy.sql import and_, or_ 6 | from yarabuilder import YaraBuilder 7 | 8 | from yaramanager.db.base import Meta, Rule, String 9 | from yaramanager.db.session import get_session 10 | from yaramanager.utils.utils import rules_to_table, rules_to_highlighted_string 11 | 12 | 13 | @click.group(help="Searches through your rules.") 14 | def search(): 15 | pass 16 | 17 | 18 | @search.command(help="Wilcard search in rule names and descriptions.") 19 | @click.option("-r", "--raw", is_flag=True, help="Output rules instead of the string table.") 20 | @click.argument("search_term") 21 | def rule(raw: bool, search_term: str): 22 | c = Console() 23 | session = get_session() 24 | rules = session.query(Rule).select_from(Meta).join(Rule.meta).filter( 25 | or_( 26 | Rule.name.like(f"%{search_term}%"), 27 | and_( 28 | Meta.key.like("description"), 29 | Meta.value.like(f"%{search_term}%") 30 | ) 31 | ) 32 | ).all() 33 | if not raw: 34 | table = rules_to_table(rules) 35 | c.print(table) 36 | else: 37 | c.print(rules_to_highlighted_string(rules)) 38 | 39 | 40 | @search.command(help="Search for strings. You can use SQL wildcard syntax (%).") 41 | @click.option("-r", "--raw", is_flag=True, help="Output rules instead of the string table.") 42 | @click.argument("query_string") 43 | def string(raw, query_string): 44 | c = Console() 45 | session = get_session() 46 | strings = session.query(String).filter(String.type == "text").filter(String.value.like(query_string)).all() 47 | if not raw: 48 | c.print(f"Found {len(strings)} strings.") 49 | t = Table() 50 | t.add_column("String") 51 | t.add_column("ID") 52 | t.add_column("Rule") 53 | t.add_column("Tags") 54 | for string in strings: 55 | t.add_row( 56 | string.value, 57 | str(string.rule.id), 58 | string.rule.name, 59 | ", ".join([tag.name for tag in string.rule.tags]) 60 | ) 61 | c.print(t) 62 | else: 63 | yb = YaraBuilder() 64 | for string in strings: 65 | if string.rule.name not in yb.yara_rules.keys(): 66 | string.rule.add_to_yarabuilder(yb) 67 | syntax = Syntax(yb.build_rules(), "python", background_color="default") 68 | c.print(syntax) 69 | -------------------------------------------------------------------------------- /yaramanager/commands/stats.py: -------------------------------------------------------------------------------- 1 | import click 2 | from rich.console import Console 3 | import os 4 | from yaramanager.db.base import Rule, String, Meta, Tag 5 | from yaramanager.db.session import get_session 6 | from yaramanager.config import load_config 7 | 8 | 9 | @click.command(help="Prints stats about the database contents.") 10 | def stats(): 11 | c = Console() 12 | config = load_config() 13 | db = config.get_current_db() 14 | session = get_session() 15 | rule_count = session.query(Rule).count() 16 | string_count = session.query(String).count() 17 | meta_count = session.query(Meta).count() 18 | tag_count = session.query(Tag).count() 19 | c.print(f"Number of rules:\t{rule_count}") 20 | c.print(f"Number of strings:\t{string_count}") 21 | c.print(f"Number of meta fields:\t{meta_count}") 22 | c.print(f"Number of tags:\t\t{tag_count}") 23 | c.print() 24 | 25 | if db["driver"] == "sqlite": 26 | c.print(f"Database size: \t\t{os.path.getsize(db['path'])/1024/1024:.2}MB") 27 | -------------------------------------------------------------------------------- /yaramanager/commands/tags.py: -------------------------------------------------------------------------------- 1 | import click 2 | from rich.console import Console 3 | from rich.table import Table 4 | 5 | from yaramanager.db.base import Tag 6 | from yaramanager.db.session import get_session 7 | 8 | 9 | @click.command(help="Show tags and the number of tagged rules") 10 | @click.option("-r", "--reverse", is_flag=True, help="Reverse the order.") 11 | @click.option("-l", "--limit", type=int, help="Limit amount of rows.") 12 | def tags(reverse, limit): 13 | c, ec = Console(), Console(stderr=True, style="bold red") 14 | session = get_session() 15 | tags = session.query(Tag).all() 16 | if len(tags) == 0: 17 | ec.print("No tags available.") 18 | exit(-1) 19 | 20 | sorted_tags = [] 21 | for tag in tags: 22 | sorted_tags.append((tag.name, len(tag.rules))) 23 | 24 | sorted_tags.sort(key=lambda x: x[1], reverse=(not reverse)) 25 | table = Table() 26 | table.add_column("Tag") 27 | table.add_column("Rule count") 28 | for tag in sorted_tags[:limit]: 29 | table.add_row(tag[0], str(tag[1])) 30 | c.print(table) 31 | -------------------------------------------------------------------------------- /yaramanager/commands/version.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import click 4 | import requests 5 | from rich.console import Console 6 | 7 | from yaramanager import version as ver 8 | 9 | try: 10 | from yaramanager import commit 11 | 12 | IS_BINARY = True 13 | except ImportError: 14 | IS_BINARY = False 15 | 16 | 17 | @click.command(help="Displays the current version.") 18 | @click.option("-c", "--check", is_flag=True, help="Checks version via Github API.") 19 | def version(check): 20 | c = Console(highlight=False) 21 | github_ver = None 22 | if check: 23 | github_ver = get_latest_release_tag() 24 | 25 | c.print(f"YaraManager {ver}") 26 | if IS_BINARY: 27 | c.print(f"Built from Git commit {commit}.") 28 | 29 | if check: 30 | if github_ver: 31 | if ver != github_ver: 32 | c.print( 33 | f"Your version of YaraManager is out of date. Most recent version is {github_ver}.", 34 | style="yellow" 35 | ) 36 | else: 37 | c.print("Your version of YaraManager is up to date.", style="green") 38 | else: 39 | c.print("Could not get most recent version from Github.") 40 | 41 | c.print(f"https://github.com/3c7/yaramanager/releases/tag/{github_ver or ver}") 42 | 43 | 44 | def get_latest_release_tag() -> Union[str, None]: 45 | res = requests.get("https://api.github.com/repos/3c7/yaramanager/releases") 46 | if res.status_code != 200: 47 | return None 48 | 49 | releases = res.json() 50 | for release in releases: 51 | if not release["draft"] and not release["prerelease"]: 52 | return release["tag_name"] 53 | return None 54 | -------------------------------------------------------------------------------- /yaramanager/config.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import sys 5 | from collections import OrderedDict 6 | from typing import Dict 7 | 8 | import toml 9 | from rich.console import Console 10 | 11 | from yaramanager.utils.platform import get_user_path, get_config_path 12 | 13 | config_dir = get_user_path() 14 | config_file = get_config_path() 15 | 16 | 17 | class Config(OrderedDict): 18 | instance = None 19 | 20 | @staticmethod 21 | def get_instance(): 22 | if not Config.instance: 23 | Config.instance = Config() 24 | return Config.instance 25 | 26 | def get_current_db(self) -> Dict: 27 | return self["db"]["databases"][self["db"]["selected"]] 28 | 29 | def __init__(self): 30 | ec = Console(stderr=True, style="bold yellow") 31 | if not os.path.exists(config_dir): 32 | os.mkdir(config_dir) 33 | 34 | if not os.path.isdir(config_dir): 35 | ec.print(f"Error: File found as config directory path.") 36 | 37 | if not os.path.exists(config_file): 38 | ec.print(f"Creating initial config file.") 39 | write_initial_config() 40 | 41 | if os.path.getsize(config_file) == 0: 42 | ec.print(f"Config file ({config_file}) is empty. Applying initial config.") 43 | write_initial_config() 44 | 45 | with io.open(config_file, "r") as fh: 46 | config_data = toml.loads(fh.read())["yaramanager"] 47 | super().__init__(self, **config_data) 48 | 49 | 50 | def load_config() -> Config: 51 | return Config.get_instance() 52 | 53 | 54 | def create_initial_config() -> str: 55 | """Reads initial config from resources directory.""" 56 | config_toml = "" 57 | db_path = os.path.join(config_dir, 'data.db') 58 | if sys.platform == "win32": 59 | db_path = db_path.replace("\\", "\\\\") 60 | with io.open(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "resources", "config.toml"))) as fh: 61 | for line in fh.readlines(): 62 | config_toml += line.replace("# {init_database}", ( 63 | f"[[yaramanager.db.databases]]\ndriver = \"sqlite\"\n" 64 | f"path = \"{db_path}\"" 65 | )) 66 | return config_toml 67 | 68 | 69 | def write_initial_config() -> None: 70 | """Writes fresh config to config file""" 71 | config_toml = create_initial_config() 72 | with io.open(config_file, "w") as fh: 73 | fh.write(config_toml) 74 | 75 | 76 | def change_db(db_num: int) -> None: 77 | """Changes selected db through regex replace.""" 78 | with io.open(config_file) as fh: 79 | data = fh.read() 80 | data = re.sub(r"selected = [0-9]+", f"selected = {db_num}", data) 81 | with io.open(config_file, "w") as fh: 82 | fh.write(data) 83 | -------------------------------------------------------------------------------- /yaramanager/db/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /yaramanager/db/base.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | from yaramanager.db.base_class import Base 3 | # noinspection PyUnresolvedReferences 4 | from yaramanager.models.rule import Rule 5 | # noinspection PyUnresolvedReferences 6 | from yaramanager.models.meta import Meta 7 | # noinspection PyUnresolvedReferences 8 | from yaramanager.models.string import String 9 | # noinspection PyUnresolvedReferences 10 | from yaramanager.models.tag import Tag 11 | # noinspection PyUnresolvedReferences 12 | from yaramanager.models.ruleset import Ruleset 13 | -------------------------------------------------------------------------------- /yaramanager/db/base_class.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sqlalchemy.orm import as_declarative, declared_attr 4 | 5 | 6 | @as_declarative() 7 | class Base: 8 | id: Any 9 | __name__: str 10 | 11 | @declared_attr 12 | def __tablename__(self) -> str: 13 | """This automatically generates a table name""" 14 | return self.__name__.lower() 15 | -------------------------------------------------------------------------------- /yaramanager/db/session.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from rich.console import Console 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.engine import Engine 6 | from sqlalchemy.orm import sessionmaker, Session 7 | from sqlalchemy.sql import text 8 | 9 | from yaramanager.config import load_config 10 | from yaramanager import alembic_version 11 | from yaramanager.utils.output import debug_print 12 | 13 | 14 | def get_session() -> Session: 15 | ec = Console(stderr=True, style="bold red") 16 | config = load_config() 17 | db = config.get_current_db() 18 | driver = db["driver"] 19 | path = db["path"] 20 | if driver == "sqlite" and not (os.path.exists(path) or os.path.getsize(path) == 0): 21 | ec.print("Database not initialized.") 22 | exit(-1) 23 | engine = get_engine() 24 | s_maker = sessionmaker(autocommit=False, bind=engine) 25 | db_alembic_version = get_alembic_version(s_maker()) 26 | if db_alembic_version != alembic_version: 27 | ec.print(f"Database schema not up to date (is {db_alembic_version}, but should be {alembic_version}). " 28 | f"Please run ym db upgrade.") 29 | exit(-1) 30 | return s_maker() 31 | 32 | 33 | def get_engine() -> Engine: 34 | db_path = get_path() 35 | debug_print(f"Connecting to database {db_path}.") 36 | return create_engine(db_path) 37 | 38 | 39 | def get_path() -> str: 40 | config = load_config() 41 | db = config.get_current_db() 42 | driver = db["driver"] 43 | path = db["path"] 44 | if driver == "sqlite": 45 | driver += ":///" 46 | else: 47 | driver += "://" 48 | return f"{driver}{path}" 49 | 50 | 51 | def get_alembic_version(session: Session) -> str: 52 | result = session.execute(text("SELECT version_num FROM alembic_version LIMIT 1;")) 53 | for row in result: 54 | debug_print(f"Alembic version of database is {row[0]}.") 55 | return row[0] 56 | -------------------------------------------------------------------------------- /yaramanager/main.py: -------------------------------------------------------------------------------- 1 | from yaramanager.commands import cli 2 | 3 | # Import modules that are needed 4 | import sqlalchemy.sql.default_comparator 5 | import logging 6 | import logging.config 7 | 8 | 9 | if __name__ == "__main__": 10 | cli() 11 | -------------------------------------------------------------------------------- /yaramanager/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3c7/yaramanager/ed72b6c9e7059e691b9ad1a800b2e02429599fd7/yaramanager/models/__init__.py -------------------------------------------------------------------------------- /yaramanager/models/meta.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from yaramanager.db.base_class import Base 5 | 6 | 7 | class Meta(Base): 8 | id = Column(Integer, primary_key=True, index=True, autoincrement=True) 9 | key = Column(String(255), index=True) 10 | value = Column(String(4096), index=True) 11 | order = Column(Integer) 12 | rule_id = Column(Integer, ForeignKey("rule.id")) 13 | rule = relationship("Rule", back_populates="meta") 14 | 15 | def __repr__(self): 16 | if self.rule: 17 | return f"" 18 | return f"" 19 | -------------------------------------------------------------------------------- /yaramanager/models/rule.py: -------------------------------------------------------------------------------- 1 | from sys import stderr 2 | from typing import Any, List 3 | 4 | from sqlalchemy import Column, String, Integer, Text, ForeignKey 5 | from sqlalchemy.orm import relationship 6 | from yarabuilder import YaraBuilder 7 | 8 | from yaramanager.db.base_class import Base 9 | from yaramanager.models.tables import tags_rules 10 | from yaramanager.config import load_config 11 | 12 | 13 | class Rule(Base): 14 | id = Column(Integer, primary_key=True, index=True, autoincrement=True) 15 | name = Column(String(255), index=True) 16 | meta = relationship("Meta", back_populates="rule", cascade="all, delete, delete-orphan") 17 | strings = relationship("String", back_populates="rule", cascade="all, delete, delete-orphan") 18 | condition = Column(Text) 19 | imports = Column(Integer) 20 | tags = relationship("Tag", back_populates="rules", secondary=tags_rules) 21 | ruleset_id = Column(Integer, ForeignKey("ruleset.id")) 22 | ruleset = relationship("Ruleset", back_populates="rules") 23 | 24 | @property 25 | def import_list(self) -> List[str]: 26 | imports = self.imports 27 | for imp in ["pe", "elf", "math", "hash", "vt"]: 28 | if imports & 0x1 > 0: 29 | yield imp 30 | imports = imports >> 1 31 | 32 | def __repr__(self): 33 | return f"" 34 | 35 | def _get_meta(self, key: str) -> Any: 36 | for meta in self.meta: 37 | if meta.key == key: 38 | return meta 39 | return None 40 | 41 | def get_meta_value(self, key: str, default: str) -> str: 42 | meta = self._get_meta(key) 43 | return meta.value if meta else default 44 | 45 | def __str__(self): 46 | yb = YaraBuilder() 47 | self.add_to_yarabuilder(yb) 48 | return yb.build_rule(self.name) 49 | 50 | def add_to_yarabuilder(self, yb: YaraBuilder) -> None: 51 | """Add the rule object to a given YaraBuilder instance 52 | 53 | >>> rule = Rule(...) 54 | >>> yb = YaraBuilder() 55 | >>> rule.add_to_yarabuilder(yb) 56 | >>> print(yb.build_rules()) 57 | """ 58 | yb.create_rule(self.name) 59 | key = load_config().get("ruleset_meta_key", "ruleset") 60 | if self.ruleset: 61 | yb.add_meta(self.name, key, self.ruleset.name) 62 | for meta in self.meta: 63 | yb.add_meta( 64 | self.name, 65 | meta.key, 66 | meta.value 67 | ) 68 | for string in self.strings: 69 | s_name = string.name[1:] 70 | if string.type == "text": 71 | yb.add_text_string( 72 | self.name, 73 | string.value, 74 | s_name, 75 | string.modifier_list 76 | ) 77 | elif string.type == "byte": 78 | yb.add_hex_string( 79 | self.name, 80 | string.value, 81 | s_name, 82 | string.modifier_list # Todo: Check hex string modifiers - this list should always be empty? 83 | ) 84 | elif string.type == "regex": 85 | yb.add_regex_string( 86 | self.name, 87 | string.value, 88 | s_name, 89 | string.modifier_list 90 | ) 91 | else: 92 | print(f"ERROR: Unknown string type \"{string.type}\".", file=stderr) 93 | for tag in self.tags: 94 | yb.add_tag(self.name, tag.name) 95 | for imp in self.import_list: 96 | yb.add_import(self.name, imp) 97 | yb.add_condition(self.name, self.condition.strip()) 98 | -------------------------------------------------------------------------------- /yaramanager/models/ruleset.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from yaramanager.db.base_class import Base 5 | 6 | 7 | class Ruleset(Base): 8 | id = Column(Integer, primary_key=True, autoincrement=True, index=True) 9 | name = Column(String(255), index=True) 10 | rules = relationship("Rule", back_populates="ruleset") 11 | -------------------------------------------------------------------------------- /yaramanager/models/string.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import Column, Integer, String as SA_String, ForeignKey, Text 4 | from sqlalchemy.orm import relationship 5 | 6 | from yaramanager.db.base_class import Base 7 | 8 | 9 | class String(Base): 10 | id = Column(Integer, primary_key=True, index=True, autoincrement=True) 11 | name = Column(SA_String(255), index=True) 12 | # Types are: text, hex, regex 13 | type = Column(SA_String(5)) 14 | value = Column(Text) 15 | modifiers = Column(Integer) 16 | order = Column(Integer) 17 | 18 | rule_id = Column(Integer, ForeignKey("rule.id")) 19 | rule = relationship("Rule", back_populates="strings") 20 | 21 | def is_ascii(self): 22 | return self.modifiers & 0x1 > 0 23 | 24 | def is_wide(self): 25 | return self.modifiers & 0x2 > 0 26 | 27 | def is_xor(self): 28 | return self.modifiers & 0x4 > 0 29 | 30 | def is_base64(self): 31 | return self.modifiers & 0x8 > 0 32 | 33 | def is_base64_wide(self): 34 | return self.modifiers & 0x10 > 0 35 | 36 | def is_nocase(self): 37 | return self.modifiers & 0x20 > 0 38 | 39 | def is_fullword(self): 40 | return self.modifiers & 0x40 > 0 41 | 42 | @property 43 | def modifier_list(self) -> List[str]: 44 | m = self.modifiers 45 | l = [] 46 | for mod in ["ascii", "wide", "xor", "base64", "base64wide", "nocase", "fullword"]: 47 | if m & 0x1: 48 | l.append(mod) 49 | m = m >> 1 50 | return l 51 | 52 | def __repr__(self): 53 | if self.rule: 54 | return f"" 55 | return f"" 56 | -------------------------------------------------------------------------------- /yaramanager/models/tables.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Table, Column, Integer, ForeignKey 2 | 3 | from yaramanager.db.base_class import Base 4 | 5 | tags_rules = Table( 6 | "tags_rules", 7 | Base.metadata, 8 | Column("tag_id", Integer, ForeignKey("tag.id")), 9 | Column("rule_id", Integer, ForeignKey("rule.id")) 10 | ) 11 | -------------------------------------------------------------------------------- /yaramanager/models/tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from yaramanager.db.base_class import Base 5 | from yaramanager.models.tables import tags_rules 6 | 7 | 8 | class Tag(Base): 9 | id = Column(Integer, primary_key=True, autoincrement=True, index=True) 10 | name = Column(String(255), index=True) 11 | rules = relationship("Rule", back_populates="tags", secondary=tags_rules) 12 | 13 | def __repr__(self): 14 | return f"" 15 | -------------------------------------------------------------------------------- /yaramanager/models/yarabuilder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import yara 5 | import yarabuilder 6 | 7 | from yaramanager.utils.utils import write_ruleset_to_tmp_file 8 | 9 | 10 | class YaraBuilder(yarabuilder.YaraBuilder): 11 | def write_rules_to_file(self, path: Union[str, Path], single_file: bool = False, compiled: bool = False): 12 | """Write yarabuilder defined ruleset to a file. single_file and compiled can be used to define how the file is 13 | written, either as a single file containing all rules, as a compiled yara rule or as a directory containing all 14 | rules as separate files.""" 15 | if isinstance(path, str): 16 | path = Path(path) 17 | # Write rules as separate files 18 | if not single_file and not compiled: 19 | if not path.is_dir(): 20 | raise NotADirectoryError() 21 | 22 | for rule_name in self.yara_rules.keys(): 23 | with open(path.joinpath(rule_name + ".yar"), "w") as fh: 24 | fh.write(self.build_rule(rule_name)) 25 | 26 | # Write rules as single file 27 | else: 28 | if path.is_dir(): 29 | raise IsADirectoryError() 30 | 31 | if single_file: 32 | if path.suffix != ".yar": 33 | path = Path(str(path) + ".yar") 34 | with open(path, "w") as fh: 35 | fh.write(self.build_rules()) 36 | 37 | if compiled: 38 | if path.suffix == ".yar": 39 | path = Path(str(path)[:-4] + ".yac") 40 | elif path.suffix != ".yac": 41 | path = Path(str(path) + ".yac") 42 | tmp_path, size = write_ruleset_to_tmp_file(self) 43 | compiled: yara.Rules = yara.compile(tmp_path) 44 | compiled.save(str(path)) 45 | -------------------------------------------------------------------------------- /yaramanager/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3c7/yaramanager/ed72b6c9e7059e691b9ad1a800b2e02429599fd7/yaramanager/utils/__init__.py -------------------------------------------------------------------------------- /yaramanager/utils/output.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from rich.console import Console 4 | 5 | from yaramanager.config import load_config 6 | 7 | 8 | def debug_print(msg: str, c: Optional[Console] = None) -> None: 9 | debug = load_config().get("debug", False) 10 | if not debug: 11 | return 12 | if not c: 13 | # Use stderr so piped output is still valid 14 | c = Console(stderr=True) 15 | 16 | c.print("[cyan]DEBUG[reset]: " + msg) 17 | -------------------------------------------------------------------------------- /yaramanager/utils/platform.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sys import platform 3 | 4 | from rich.console import Console 5 | 6 | 7 | def is_linux() -> bool: 8 | return platform == "linux" 9 | 10 | 11 | def is_win() -> bool: 12 | return platform == "win32" 13 | 14 | 15 | def is_darwin() -> bool: 16 | return platform == "darwin" 17 | 18 | 19 | def get_user_path() -> str: 20 | """Returns yaramanager path in user dir.""" 21 | u_path = None 22 | u_env = os.getenv("YM_PATH", None) 23 | if u_env: 24 | return u_env 25 | if is_linux() or is_darwin(): 26 | u_path = os.path.abspath(os.path.join(os.getenv("HOME"), ".config", "yaramanager")) 27 | elif is_win(): 28 | u_path = os.path.abspath(os.path.join(os.getenv("APPDATA"), "yaramanager")) 29 | else: 30 | c = Console(stderr=True, style="bold red") 31 | c.print(f"Unknown platform: {platform}.") 32 | exit(-1) 33 | 34 | if not os.path.exists(u_path): 35 | os.mkdir(u_path) 36 | 37 | return u_path 38 | 39 | 40 | def get_config_path() -> str: 41 | """Return path to config.toml""" 42 | c_env = os.getenv("YM_CONFIG", None) 43 | if c_env: 44 | return c_env 45 | c_path = os.path.join(get_user_path(), "config.toml") 46 | return c_path 47 | -------------------------------------------------------------------------------- /yaramanager/utils/utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import subprocess 5 | from hashlib import md5 6 | from sys import stderr, exit 7 | from tempfile import mkstemp 8 | from typing import Dict, List, Union, Tuple, Optional 9 | 10 | from plyara import Plyara 11 | from rich.console import Console 12 | from rich.syntax import Syntax 13 | from rich.table import Table 14 | from sqlalchemy.orm import Session, noload 15 | from yarabuilder import YaraBuilder 16 | 17 | from yaramanager.config import load_config 18 | from yaramanager.db.base import Rule, Meta, String, Tag, Ruleset 19 | from yaramanager.db.session import get_session 20 | 21 | 22 | def read_rulefile(path: str) -> List[Dict]: 23 | """ 24 | Reads a file given through `path` and returns the plyara parsed list. 25 | 26 | >>> list_of_rules = read_rulefile(path) 27 | """ 28 | with io.open(path, "r") as handle: 29 | raw = handle.read() 30 | return Plyara().parse_string(raw) 31 | 32 | 33 | def plyara_obj_to_rule(obj: Dict, session: Session, overwrite_ruleset: str = None) -> Rule: 34 | """ 35 | Converts a yara rule dictionary representation into a Rule object. 36 | 37 | >>> r = plyara_obj_to_rule(...) 38 | >>> r.__repr__() 39 | 40 | """ 41 | r = Rule() 42 | r.name = obj.get("rule_name", "Unnamed rule") 43 | r.meta = plyara_object_to_meta(obj) 44 | r.strings = plyara_object_to_strings(obj) 45 | r.imports = plyara_object_to_imports(obj) 46 | r.tags = plyara_object_to_tags(obj, session) 47 | r.condition = plyara_object_to_condition(obj) 48 | if not overwrite_ruleset: 49 | ruleset = plyara_object_to_ruleset(obj, session) 50 | else: 51 | ruleset = get_ruleset_by_name( 52 | name=overwrite_ruleset, 53 | session=session, 54 | create=True 55 | ) 56 | r.ruleset = ruleset 57 | return r 58 | 59 | 60 | def plyara_object_to_meta(obj: Dict) -> List[Meta]: 61 | """Returns a list of initialized Meta objects based on a plyara object.""" 62 | meta: List[Meta] = [] 63 | for idx, m_dict in enumerate(obj.get("metadata", [])): 64 | for k, v in m_dict.items(): 65 | if k.lower() == "ruleset": 66 | continue 67 | m = Meta( 68 | key=k, 69 | value=v, 70 | order=idx 71 | ) 72 | meta.append(m) 73 | return meta 74 | 75 | 76 | def plyara_object_to_strings(obj: Dict) -> List[String]: 77 | """Returns a list of initialized String objects from a plyara object.""" 78 | strings: List[String] = [] 79 | for idx, ply_string in enumerate(obj.get("strings", [])): 80 | s = String( 81 | name=ply_string["name"], 82 | value=ply_string["value"], 83 | order=idx, 84 | type=ply_string["type"] 85 | ) 86 | s.modifiers = 0 87 | for mod in ply_string.get("modifiers", []): 88 | if mod == "ascii": 89 | s.modifiers = s.modifiers | 0x1 90 | elif mod == "wide": 91 | s.modifiers = s.modifiers | 0x2 92 | elif mod == "xor": 93 | s.modifiers = s.modifiers | 0x4 94 | elif mod == "base64": 95 | s.modifiers = s.modifiers | 0x8 96 | elif mod == "base64wide": 97 | s.modifiers = s.modifiers | 0x10 98 | elif mod == "nocase": 99 | s.modifiers = s.modifiers | 0x20 100 | elif mod == "fullword": 101 | s.modifiers = s.modifiers | 0x40 102 | strings.append(s) 103 | return strings 104 | 105 | 106 | def plyara_object_to_imports(obj: Dict) -> int: 107 | """Returns an integer representing imported yara modules.""" 108 | imports = 0 109 | conditions = plyara_object_to_condition(obj) 110 | for imp in obj.get("imports", []): 111 | if imp == "pe" and "pe." in conditions: 112 | imports = imports | 0x1 113 | elif imp == "elf" and "elf." in conditions: 114 | imports = imports | 0x2 115 | elif imp == "math" and "math." in conditions: 116 | imports = imports | 0x4 117 | elif imp == "hash" and "hash." in conditions: 118 | imports = imports | 0x8 119 | elif imp == "vt" and "vt." in conditions: 120 | imports = imports | 0x10 121 | return imports 122 | 123 | 124 | def plyara_object_to_tags(obj: Dict, session: Optional[Session] = None) -> List[Tag]: 125 | """Returns a list of initialized Tag objects based on a plyara dict""" 126 | tags: List[Tag] = [] 127 | if not session: 128 | session = get_session() 129 | 130 | for tag in obj.get("tags", []): 131 | t = session.query(Tag).filter(Tag.name == tag).first() 132 | if t: 133 | tags.append(t) 134 | else: 135 | t = Tag( 136 | name=tag 137 | ) 138 | tags.append(t) 139 | return tags 140 | 141 | 142 | def plyara_object_to_condition(obj: Dict) -> str: 143 | """Returns condition string from plyara object""" 144 | return obj["raw_condition"].split(":", 1)[1].strip() 145 | 146 | 147 | def plyara_object_to_ruleset(obj: Dict, session: Session) -> Union[Ruleset, None]: 148 | """Returns ruleset object, if ruleset is given as meta tag, or None""" 149 | key = load_config().get("ruleset_meta_key", "ruleset") 150 | for m_dict in obj.get("metadata", []): 151 | for k, v in m_dict.items(): 152 | if k != key: 153 | continue 154 | 155 | return get_ruleset_by_name( 156 | name=v, 157 | session=session, 158 | create=True 159 | ) 160 | return None 161 | 162 | 163 | def get_ruleset_by_name(name: str, session: Session, create: bool = False) -> Union[Ruleset, None]: 164 | """Get ruleset by name and return it or return None. If `create` is given, a not existing ruleset will be 165 | created.""" 166 | r = session.query(Ruleset).filter(Ruleset.name == name).first() 167 | if not r and create: 168 | return Ruleset(name=name) 169 | return r 170 | 171 | 172 | def parse_rule(rule: str) -> Union[Dict, List]: 173 | ply = Plyara() 174 | return ply.parse_string(rule) 175 | 176 | 177 | def parse_rule_file(path: str) -> Union[Dict, List]: 178 | with io.open(path) as fh: 179 | return parse_rule(fh.read()) 180 | 181 | 182 | def print_rule(rules: Union[Dict, List]) -> str: 183 | yb = YaraBuilder() 184 | if isinstance(rules, dict): 185 | rules = [rules] 186 | for rule in rules: 187 | rn = rule["rule_name"] 188 | yb.create_rule(rn) 189 | for mdata in rule.get("metadata", []): 190 | for k, v in mdata.items(): 191 | yb.add_meta(rn, k, v) 192 | for tag in rule["tags"]: 193 | yb.add_tag(rn, tag) 194 | for yara_string in rule["strings"]: 195 | s_type = yara_string["type"] 196 | s_name = yara_string["name"][1:] 197 | s_val = yara_string["value"] 198 | s_mod = yara_string.get("modifiers", []) 199 | if s_type == "text": 200 | yb.add_text_string(rn, s_val, s_name, s_mod) 201 | elif s_type == "regex": 202 | yb.add_regex_string(rn, s_val, s_name, s_mod) 203 | elif s_type == "byte": 204 | yb.add_hex_string(rn, s_val[1:-1].strip(), s_name) 205 | yb.add_condition(rn, " ".join(rule["condition_terms"])) 206 | return yb.build_rules() 207 | 208 | 209 | def get_md5(path: str): 210 | """Creates md5 hash of a file. Used for monitoring file changes during rule editing.""" 211 | hasher = md5() 212 | with io.open(path, "rb") as fh: 213 | hasher.update(fh.read()) 214 | return hasher.hexdigest() 215 | 216 | 217 | def write_ruleset_to_tmp_file(yb: YaraBuilder) -> Tuple[str, int]: 218 | """Writes a ruleset defined by yarabuilder to a temporary file and returns the path.""" 219 | fd_temp, path = mkstemp(suffix=".yar") 220 | bytes = write_ruleset_to_file(yb, fd_temp) 221 | return path, bytes 222 | 223 | 224 | def write_ruleset_to_file(yb: YaraBuilder, file: Union[int, str]) -> int: 225 | """Write a ruleset defined by yarabuilder to a given filedescriptor or filepath.""" 226 | text = yb.build_rules() 227 | if isinstance(file, int): 228 | with os.fdopen(file, "w") as fh: 229 | b = fh.write(text) 230 | else: 231 | with io.open(file, "w") as fh: 232 | b = fh.write(text) 233 | if b <= 0: 234 | ec = Console(file=stderr) 235 | ec.print(f"ERR: Number of bytes written should be greater 0 but was {b}.") 236 | return b 237 | 238 | 239 | def open_file(path: str, status: Optional[str] = None): 240 | c, ec = Console(), Console(stderr=True, style="bold red") 241 | config = load_config() 242 | env_editor = os.getenv("EDITOR", None) 243 | env_disable_status = os.getenv("DISABLE_STATUS", None) 244 | if env_editor: 245 | command = env_editor.split(" ") 246 | else: 247 | command = config.get("editor", None) 248 | if not command: 249 | ec.print("Editor not given. Please set editor config value or use EDITOR environment variable.") 250 | exit(-1) 251 | command.append(path) 252 | if env_disable_status: 253 | subprocess.call(command) 254 | else: 255 | if not status: 256 | status = f"{path} opened in external editor..." 257 | with c.status(status): 258 | subprocess.call(command) 259 | 260 | 261 | def open_temp_file_with_template(): 262 | ec = Console(stderr=True, style="bold red") 263 | config = load_config() 264 | fd_temp, path = mkstemp(".yar") 265 | with os.fdopen(fd_temp, "w") as fh: 266 | try: 267 | fh.write(config["template"]) 268 | except KeyError: 269 | ec.print("Template is missing. Please set template variable in config.") 270 | exit(-1) 271 | open_file(path) 272 | return path 273 | 274 | 275 | def get_rule_by_identifier(identifier: Union[str, int], session: Optional[Session] = None) -> List[Rule]: 276 | if not session: 277 | session = get_session() 278 | rules = session.query(Rule) 279 | if isinstance(identifier, int) or re.fullmatch(r"^\d+$", identifier): 280 | rules = rules.filter(Rule.id == int(identifier)) 281 | else: 282 | rules = rules.filter(Rule.name.like(f"%{identifier}%")) 283 | return rules.all() 284 | 285 | 286 | def get_ruleset_by_identifier(identifier: Union[str, int], session: Optional[Session] = None) -> Ruleset: 287 | if not session: 288 | session = get_session() 289 | 290 | ruleset = session.query(Ruleset) 291 | if isinstance(identifier, int) or re.fullmatch(r"^\d+$", identifier): 292 | return ruleset.get(int(identifier)) 293 | else: 294 | return ruleset.filter(Ruleset.name.like(identifier)).first() 295 | 296 | 297 | def rules_to_table(rules: List[Rule], ensure: Optional[bool] = False) -> Table: 298 | """Creates a rich.table.Table object from s list of rules.""" 299 | config = load_config() 300 | meta_columns = config.get("meta", {}) 301 | ensure_meta = config.get("ensure", {}).get("ensure_meta", []) 302 | ensure_tags = config.get("ensure", {}).get("ensure_tag", True) 303 | table = Table() 304 | table.add_column("ID") 305 | table.add_column("Name") 306 | table.add_column("Ruleset") 307 | if ensure and ensure_tags: 308 | table.add_column("Tags [yellow]:warning:") 309 | else: 310 | table.add_column("Tags") 311 | for column in meta_columns.keys(): 312 | c_header = column + " [yellow]:warning:" if meta_columns[column] in ensure_meta and ensure else column 313 | table.add_column(c_header) 314 | 315 | for rule in rules: 316 | if ensure and ensure_tags and len(rule.tags) == 0: 317 | tags = "[yellow]:warning:" 318 | else: 319 | tags = ", ".join([tag.name for tag in rule.tags]) 320 | 321 | row = [ 322 | str(rule.id), 323 | rule.name, 324 | rule.ruleset.name if rule.ruleset else "None", 325 | tags 326 | ] 327 | for column in meta_columns.values(): 328 | m_value = rule.get_meta_value(column, default="None") 329 | if ensure and column in ensure_meta and m_value == "None": 330 | row.append("[yellow]:warning:") 331 | else: 332 | row.append(rule.get_meta_value(column, default="None")) 333 | table.add_row(*row) 334 | return table 335 | 336 | 337 | def rules_to_highlighted_string(rules: List[Rule]): 338 | yb = YaraBuilder() 339 | for rule in rules: 340 | if rule.name not in yb.yara_rules.keys(): 341 | rule.add_to_yarabuilder(yb) 342 | # As there is no yara lexer available in pygments, we're usign python here. 343 | return Syntax(yb.build_rules(), "python", background_color="default") 344 | 345 | 346 | def filter_rules_by_name_and_tag(name: str, tag: Tuple[str], exclude_tag: Tuple[str], 347 | session: Optional[Session] = None, 348 | no_strings: Optional[bool] = False) -> Tuple[List[Rule], int]: 349 | if not session: 350 | session = get_session() 351 | 352 | rules = session.query(Rule) 353 | if no_strings: 354 | rules = rules.options(noload(Rule.strings)) 355 | if tag and len(tag) > 0: 356 | rules = rules.select_from(Tag).join(Rule.tags).filter(Tag.name.in_(tag)) 357 | if exclude_tag and len(exclude_tag) > 0: 358 | # Select rules which have one of the excluded tags and make sure the previously selected rules are not in there. 359 | rules = rules.filter(~Rule.id.in_( 360 | session.query(Rule) 361 | .select_from(Tag) 362 | .join(Rule.tags) 363 | .filter(Tag.name.in_(exclude_tag)) 364 | .with_entities(Rule.id) 365 | )) 366 | if name and len(name) > 0: 367 | rules = rules.filter(Rule.name.like(f"%{name}%")) 368 | count = rules.count() 369 | 370 | if count == 0: 371 | return [], count 372 | else: 373 | rules = rules.all() 374 | return rules, len(rules) 375 | --------------------------------------------------------------------------------