diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..da29c73 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..a27924b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '21 10 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..8656bbf --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,50 @@ +name: Python application + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + + steps: + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python ${{matrix.python-version}} + uses: actions/setup-python@v5 + with: + python-version: ${{matrix.python-version}} + + - name: Install python 3.13 specific dependencies + if: ${{ matrix.python-version == '3.13' }} + run: | + sudo apt-get install -y build-essential python3-dev libfuzzy-dev + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip poetry + poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport -E url -E email -E brotli -vvv + + - name: Test with nosetests + run: | + poetry run pytest --cov=pymisp tests/test_*.py + poetry run mypy . + + - name: Test with nosetests with orjson + run: | + pip3 install orjson + poetry run pytest --cov=pymisp tests/test_*.py + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e3238b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +on: + release: + types: + - published + +name: release + +jobs: + pypi-publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pymisp + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Install Poetry + run: python -m pip install --upgrade pip poetry + - name: Build artifacts + run: poetry build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 97626ac..63594a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,21 @@ *.swp *.pem *.pyc +docs/build/ examples/keys.py examples/cudeso.py examples/feed-generator/output/*\.json examples/feed-generator/output/hashes\.csv examples/feed-generator/settings\.py +examples/feed_generator/output/*\.json +examples/feed_generator/output/hashes\.csv +examples/feed_generator/settings\.py +tests/reportlab_testoutputs/*\.pdf build/* dist/* pymisp.egg-info/* +.coverage .idea +tests/keys.py + diff --git a/.gitmodules b/.gitmodules index a1fe528..d0e9062 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "pymisp/data/misp-objects"] path = pymisp/data/misp-objects url = https://github.com/MISP/misp-objects -[submodule "pymisp/tools/pdf_fonts"] - path = pymisp/tools/pdf_fonts - url = https://github.com/MISP/pdf_fonts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f71108b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +exclude: "tests/data" +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: [--py38-plus] diff --git a/.readthedocs.yml b/.readthedocs.yml index f53317b..6df72a3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,14 +1,14 @@ version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3" python: - version: 3.6 install: - method: pip path: . extra_requirements: - docs -build: - image: latest - formats: all diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9bbfd22..0000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: python - -cache: pip - -addons: - apt: - sources: [ 'ubuntu-toolchain-r-test' ] - packages: - - libstdc++6 - - libfuzzy-dev - -python: - - "2.7" - - "3.5-dev" - - "3.6" - - "3.6-dev" - -install: - - pip install pipenv - - pipenv install --dev - - pushd tests - - git clone https://github.com/viper-framework/viper-test-files.git - - popd - -script: - - pipenv run nosetests --with-coverage --cover-package=pymisp,tests --cover-tests tests/test_*.py - -after_success: - - pipenv run codecov - - pipenv run coveralls diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a375d49..c678ed6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,12 +2,3651 @@ Changelog ========= -%%version%% (unreleased) ------------------------- +v2.5.2 (2024-11-18) +------------------- + +New +~~~ +- Publish to PyPi on release. [Raphaël Vinot] Changes ~~~~~~~ -- Build all formats for the documentation. [Raphaël Vinot] +- Skip PyMISP version check. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- Bump version, deps, templates. [Raphaël Vinot] +- Bump version, test for GH action release. [Raphaël Vinot] +- Drop python 3.8, add python 3.13. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Remove fonts from submodules, on-demand download if needed. [Raphaël + Vinot] + +Fix +~~~ +- Avoid exception on dev releases. [Raphaël Vinot] +- Template versions in tests. [Raphaël Vinot] +- [AnalystData] A quick and simple typing fix. [Christian Studer] + +Other +~~~~~ +- Build(deps): bump codecov/codecov-action from 4 to 5. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Update pytest.yml for python 3.13. [Raphaël Vinot] + + +v2.5.1 (2024-10-17) +------------------- + +New +~~~ +- Onion-address type. [Raphaël Vinot] + +Changes +~~~~~~~ +- Re-bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Skip trying to install doc in python 3.9. [Raphaël Vinot] + + +v2.5.0 (2024-10-04) +------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [tests] misp_instance_version_master now uses the 2.5 branch. + [iglocska] + +Fix +~~~ +- Make mypy happy. [Raphaël Vinot] + + +v2.4.198 (2024-09-13) +--------------------- + +Changes +~~~~~~~ +- Re-Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Only include the changelog in the sdist package. [Raphaël Vinot] + + Related #1295 +- [data] describeTypes.json updated. [Alexandre Dulaunoy] + +Other +~~~~~ +- Openioc.py is not a script, but had exec bit. [Sebastian Wagner] + + the file openioc can only be used as module and as part of a package, + has no instructions for direct execution and is therefor not a script + for direct execution + + this removes the executable bit from the file + + +v2.4.197 (2024-09-02) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version, templates. [Raphaël Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] + +Fix +~~~ +- Avoid printing huge log when a request fails. [Raphaël Vinot] + + fix #1286 + + +v2.4.196 (2024-08-21) +--------------------- + +New +~~~ +- Add pre-commit file. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Remove broken config. [Raphaël Vinot] + + +v2.4.195 (2024-07-27) +--------------------- + +New +~~~ +- Add delete role, test suite for roles. [Raphaël Vinot] +- Test publish & search. [Raphaël Vinot] +- Add delete role, test suite for roles. [Raphaël Vinot] +- Test publish & search. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump Changelog (issue with template) [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [publish tests] further debugging. [iglocska] +- [publish test] check if the publishing actually worked as intended. + [iglocska] +- [tests] speculative fix for the published search. [iglocska] + + - locally it seems to work as intended, curious what is going on here +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [publish tests] further debugging. [iglocska] +- [publish test] check if the publishing actually worked as intended. + [iglocska] +- [tests] speculative fix for the published search. [iglocska] + + - locally it seems to work as intended, curious what is going on here +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Bump objects (invalid template) [Raphaël Vinot] +- Do not let a user pass a full dict as tagname. [Raphaël Vinot] +- [publish tests] fixed invalid setting name for disabling background + processing. [iglocska] +- [publish test] invalid path for the publishing outcome in the + response. [iglocska] +- [publish test] fixed. [iglocska] + + - was incorrect as it triggered a background processed publishing, which can take time +- Do not let a user pass a full dict as tagname. [Raphaël Vinot] +- Do not let a user pass a full dict as tagname. [Raphaël Vinot] +- [publish tests] fixed invalid setting name for disabling background + processing. [iglocska] +- [publish test] invalid path for the publishing outcome in the + response. [iglocska] +- [publish test] fixed. [iglocska] + + - was incorrect as it triggered a background processed publishing, which can take time + +Other +~~~~~ +- Re-naming variables to make tests happy. [Tobias Mainka] +- Added support to add or update a MISP role. [Tobias Mainka] +- Update tests. [Raphaël Vinot] +- Build(deps): bump certifi from 2024.6.2 to 2024.7.4. [dependabot[bot]] + + Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4. + - [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04) + + --- + updated-dependencies: + - dependency-name: certifi + dependency-type: indirect + ... +- MANIFEST.in does not seem to have an effect any longer. [Ulrik Haugen] +- Include docs, examples and tests in sdist. [Ulrik Haugen] +- Re-naming variables to make tests happy. [Tobias Mainka] +- Added support to add or update a MISP role. [Tobias Mainka] +- Update tests. [Raphaël Vinot] +- Build(deps): bump certifi from 2024.6.2 to 2024.7.4. [dependabot[bot]] + + Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4. + - [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04) + + --- + updated-dependencies: + - dependency-name: certifi + dependency-type: indirect + ... +- Feat: Adds methods to get attribute by id/uuid. [Sura De Silva] + + +v2.4.194 (2024-06-21) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Make a response in the tests a MISPUser obj. [Raphaël Vinot] +- Tests failing du to missing error. [Raphaël Vinot] + + +v2.4.193 (2024-06-06) +--------------------- + +New +~~~ +- [analyst-data] Added initial support of analyst data concept and + functions - WiP. [Sami Mokaddem] + +Changes +~~~~~~~ +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- A bit more refactoring. [Raphaël Vinot] +- Use from_dict in the mixin to initialize the objects. [Raphaël Vinot] +- [analyst-data] Added improvements, API endpoints and tests. [Sami + Mokaddem] +- [analyst-data] Make sure to include note_type_name. [Sami Mokaddem] +- Make mypy happy, change inheritance. [Raphaël Vinot] +- Allow orgc context for search_galaxy_clusters. [Jeroen Pinoy] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [analyst-data] Continued implementation of analyst-data support. [Sami + Mokaddem] +- Allow orgc context for search_galaxy_clusters. [Jeroen Pinoy] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Get the tests to pass. [Raphaël Vinot] +- Properly load AnalystData from dict. [Raphaël Vinot] +- More changes to get the tests to pass. [Raphaël Vinot] +- [event-report] Make sure to generate an UUID. [Sami Mokaddem] +- Pass kwargs to abstract. [Raphaël Vinot] + +Other +~~~~~ +- Chg; Bump changelog. [Raphaël Vinot] +- Chg; Bump version. [Raphaël Vinot] +- Add test case. [Vincenzo] +- Add attach galaxy cluster method. [Vincenzo] + + +v2.4.190 (2024-04-18) +--------------------- + +Changes +~~~~~~~ +- Bump object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Bump deps, require python 3.9+ for doc. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [data] describeTypes file updated. [Alexandre Dulaunoy] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- [internal] Correct way to convert bytes to string if orjson exists. + [Jakub Onderka] + + +v2.4.188 (2024-03-22) +--------------------- + +New +~~~ +- Support X-MISP-AUTH Header. [Raphaël Vinot] + + Also, improve HTTP headers init + + Fix #1179 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version, templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Strip API key before setting it. [Raphaël Vinot] +- Python 3.8 support & typing. [Raphaël Vinot] +- Typing for Python < 3.10. [Raphaël Vinot] +- Avoid issue when payload ist a list. [Raphaël Vinot] + + +v2.4.187 (2024-03-07) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump templates, version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump extract-msg. [Raphaël Vinot] + + +v2.4.186 (2024-02-27) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Correct FileObject import. [Johannes Bader] + + The FileObject import has been moved outside the try-except-block + related to lief, as the import is needed regardless whether lief + is available or not. +- Disable WL when calling the disable method, not toggle. [Raphaël + Vinot] + + Fix #1159 + +Other +~~~~~ +- Build(deps): bump urllib3 from 2.2.0 to 2.2.1. [dependabot[bot]] + + Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.0 to 2.2.1. + - [Release notes](https://github.com/urllib3/urllib3/releases) + - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) + - [Commits](https://github.com/urllib3/urllib3/compare/2.2.0...2.2.1) + + --- + updated-dependencies: + - dependency-name: urllib3 + dependency-type: direct:production + update-type: version-update:semver-patch + ... + + +v2.4.185 (2024-02-16) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] + + +v2.4.184.3 (2024-02-12) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Properly get body from message, without headers. [Raphaël Vinot] +- Remove from __all__ entries that shouldn't be there. [Raphaël Vinot] + +Other +~~~~~ +- Build(deps-dev): bump jupyter-lsp from 2.2.1 to 2.2.2. + [dependabot[bot]] + + Bumps [jupyter-lsp](https://github.com/jupyter-lsp/jupyterlab-lsp) from 2.2.1 to 2.2.2. + - [Release notes](https://github.com/jupyter-lsp/jupyterlab-lsp/releases) + - [Changelog](https://github.com/jupyter-lsp/jupyterlab-lsp/blob/main/CHANGELOG.md) + - [Commits](https://github.com/jupyter-lsp/jupyterlab-lsp/commits) + + --- + updated-dependencies: + - dependency-name: jupyter-lsp + dependency-type: indirect + ... + + +v2.4.184.2 (2024-02-06) +----------------------- + +Changes +~~~~~~~ +- Add changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Re-add ExpandedPyMISP, with a warning. [Raphaël Vinot] + +Fix +~~~ +- Do not throw a warning every time one import pymisp... [Raphaël Vinot] + +Other +~~~~~ +- Build(deps): bump codecov/codecov-action from 3 to 4. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.184.1 (2024-02-06) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Re-add ExpandedPyMISP, with a warning. [Raphaël Vinot] + + +v2.4.184 (2024-02-02) +--------------------- + +New +~~~ +- Enable support for python 3.12. [Raphaël Vinot] +- Relationship_type in tag. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/9483 +- [internal] Add support for orjson. [Jakub Onderka] + + orjson is much faster library for decoding and encoding JSON formats + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version, objects. [Raphaël Vinot] +- Remove IntEnum. [Raphaël Vinot] +- Add even more debug for gha. [Raphaël Vinot] +- Add some debug for gha. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add more strict typing, not done yet. [Raphaël Vinot] +- Add a bunch more typing. [Raphaël Vinot] +- Use typing info of lief. [Raphaël Vinot] +- First batch of changes for strict typing. [Raphaël Vinot] +- Update typing to please lief. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [internal] Simplify code. [Jakub Onderka] +- [internal] User faster method to convert bytes to str. [Jakub Onderka] +- New annotations in tests. [Raphaël Vinot] +- Initial changes to use new annotations. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps, try to install with python 3.12. [Raphaël Vinot] +- Make the publish_timestamp a string, as per specs. [Raphaël Vinot] +- [internal] Update poetry.lock. [Jakub Onderka] + +Fix +~~~ +- Revert typing changes. [Raphaël Vinot] +- More responses athat are lists. [Raphaël Vinot] +- Another call that cn be a list or a dict. [Raphaël Vinot] +- Do not cast enum. [Raphaël Vinot] +- More fixes to support responses from MISP. [Raphaël Vinot] +- Handle list responses properly. [Raphaël Vinot] +- Import FileObject as needed. [Raphaël Vinot] +- Also skip docs from mypy. [Raphaël Vinot] +- Run mypy on what I want. [Raphaël Vinot] +- Compatibility with python 3.8. [Raphaël Vinot] +- Python < 3.10 support on typing, for good. [Raphaël Vinot] +- Python < 3.10 support on typing. [Raphaël Vinot] +- Rollback tests on python 3.12 as lief is not supported yet. [Raphaël + Vinot] +- Add missing wheel. [Raphaël Vinot] +- Make publish_timestamp a string in tests. [Raphaël Vinot] +- [internal] README typos. [Jakub Onderka] + +Other +~~~~~ +- Revert "fix: More responses athat are lists" [Raphaël Vinot] + + This reverts commit 709a10c64c0513b515f25c3ecfb9eb577b55084b. +- Build(deps): bump jinja2 from 3.1.2 to 3.1.3. [dependabot[bot]] + + Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3. + - [Release notes](https://github.com/pallets/jinja/releases) + - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) + - [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3) + + --- + updated-dependencies: + - dependency-name: jinja2 + dependency-type: indirect + ... + + +v2.4.183 (2024-01-04) +--------------------- + +New +~~~ +- Documentation to install PyMISP on offline machine. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Remove jsonschema from dependencies. [Raphaël Vinot] +- Encrypt malicious js. [Raphaël Vinot] + +Other +~~~~~ +- Fix api ssl verify typing. [Steven] +- Add HTTPS Adapter. [Steven] + + Add the ability to provide a custom HTTPS adapter to the PyMISP class. With M2Crypto and m2requests, this can enable mutual TLS with hardware tokens. + + +v2.4.182 (2023-12-14) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Avoid exception when the malware file name contains a "|" [Raphaël + Vinot] + +Other +~~~~~ +- Build(deps): bump github/codeql-action from 2 to 3. [dependabot[bot]] + + Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. + - [Release notes](https://github.com/github/codeql-action/releases) + - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/github/codeql-action/compare/v2...v3) + + --- + updated-dependencies: + - dependency-name: github/codeql-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump actions/setup-python from 4 to 5. [dependabot[bot]] + + Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. + - [Release notes](https://github.com/actions/setup-python/releases) + - [Commits](https://github.com/actions/setup-python/compare/v4...v5) + + --- + updated-dependencies: + - dependency-name: actions/setup-python + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.179 (2023-11-23) +--------------------- + +Changes +~~~~~~~ +- Bump version, changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [misp-objects] Bumped latest version. [Christian Studer] + +Fix +~~~ +- Eml and msg are in sync again. [Raphaël Vinot] +- Update calls to getStringStream to the public method. [Raphaël Vinot] +- Avoid confusing error when an auth key is limited to an IP. [Raphaël + Vinot] + + Fix #1099 + + +v2.4.178 (2023-10-24) +--------------------- + +New +~~~ +- Run tests on python 3.12 too. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, make __version__ dynamic. [Raphaël Vinot] +- Bump deps, allow older jsonschema for compatibility. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Make mypy happy. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Disable search logs tests for now. [Raphaël Vinot] +- Disable fastmode, reenable fetching files. [Raphaël Vinot] +- Try to speedup tests by not importing galaxies, taxos, ... [Raphaël + Vinot] +- Do not clone repo from test. [Raphaël Vinot] + +Fix +~~~ +- Make other fieldnames in CSV also valid... [Raphaël Vinot] +- Make fieldnames actually valid. [Raphaël Vinot] +- Remove CI for python 3.12, waiting for pydeep wheels. [Raphaël Vinot] +- Allow object-relation names with uppercase characters defined in the + templates. [Raphaël Vinot] +- Check if path exists in tests. [Raphaël Vinot] + +Other +~~~~~ +- Ch: Bump deps. [Raphaël Vinot] + + +v2.4.176 (2023-09-15) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps, objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Avoid exception when data is an empty iterator. [Raphaël Vinot] + + Fix #1053 + +Other +~~~~~ +- Revert "build(deps): bump codecov/codecov-action from 3 to 4" [Raphaël + Vinot] + + This reverts commit b7bb6b74317b70613ed42ea234eaafb00da6e5c6. +- Build(deps): bump codecov/codecov-action from 3 to 4. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump actions/checkout from 3 to 4. [dependabot[bot]] + + Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. + - [Release notes](https://github.com/actions/checkout/releases) + - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/checkout/compare/v3...v4) + + --- + updated-dependencies: + - dependency-name: actions/checkout + dependency-type: direct:production + update-type: version-update:semver-major + ... + + +v2.4.175 (2023-08-23) +--------------------- + +Changes +~~~~~~~ +- Bump objects, missed that. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps, readthedocs config. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Update Sharing group info from full object. [Raphaël Vinot] + + Fix #1049 +- Changes in msg-extract strip a character. [Raphaël Vinot] + + +v2.4.174 (2023-07-31) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, templates. [Raphaël Vinot] +- Bump deps, fix code accordingly. [Raphaël Vinot] + +Fix +~~~ +- Push code changes related to deps upgrade... [Raphaël Vinot] + +Other +~~~~~ +- Git: Bump deps. [Raphaël Vinot] + + +v2.4.173 (2023-07-10) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Maybe fixing a CakePHP issue. [Raphaël Vinot] + + Maybe fixing #1014 +- Use proper endpoint to unpublish event. [Raphaël Vinot] + + Fix #1012 + +Other +~~~~~ +- Feat: introduce setter for galaxies. [Sura De Silva] + + +v2.4.172 (2023-06-08) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [misp-objects] Bumped latest version with updated templates. + [Christian Studer] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Proper changelog bump. [Raphaël Vinot] +- Properly bump version. [Raphaël Vinot] + +Other +~~~~~ +- Build(deps-dev): bump jupyterlab from 3.6.3 to 4.0.0. + [dependabot[bot]] + + Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 3.6.3 to 4.0.0. + - [Release notes](https://github.com/jupyterlab/jupyterlab/releases) + - [Changelog](https://github.com/jupyterlab/jupyterlab/blob/master/CHANGELOG.md) + - [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/vdom@3.6.3...@jupyterlab/lsp@4.0.0) + + --- + updated-dependencies: + - dependency-name: jupyterlab + dependency-type: direct:development + update-type: version-update:semver-major + ... +- Update settings.default.py - tags not tag. [Alexandre Dulaunoy] + + tags is now an array + + +v2.4.171 (2023-05-16) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Remove old setup files, bump deps. [Raphaël Vinot] + +Fix +~~~ +- Extra print breaking the CI on MISP side. [Raphaël Vinot] +- Properly use lief on a file. [Raphaël Vinot] + +Other +~~~~~ +- Allow search by 'event_tags' and improve the handling of galaxy + clusters. [Stefano Ortolani] + + Changes: + - Add 'event_tags' parameter when searching for events + - Add new method to search for galaxies by value + - Add new parameter to fetch cluster information when retrieving clusters + - Add new parameter to hard-delete object references +- Using underscore name 'description_file' in setup.cfg. [Erhan] + + Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead. By 2023-Sep-26, you need to update your project and remove deprecated calls or your builds will no longer be supported. + + See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details. + + +v2.4.170.2 (2023-05-04) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + + +v2.4.170.1 (2023-04-19) +----------------------- + +Changes +~~~~~~~ +- Disable fail fast in GHA. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Update lief code to v0.13. [Raphaël Vinot] + + +v2.4.170 (2023-04-12) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Other +~~~~~ +- Add: support breakOnDuplicate option for attributes:add() [Luciano + Righetti] +- Update reportlab_generator.py. [CarlosLoureiro] + + +v2.4.169.3 (2023-03-27) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Invalid check if taxo is enabled. [Raphaël Vinot] + + +v2.4.169.2 (2023-03-17) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Include event reports by default in feed. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Use proper parameter to trigger the request in search_galaxy_clusters. + [Raphaël Vinot] +- Use POST in search galaxy cluster. [Raphaël Vinot] + +Other +~~~~~ +- Rename include_event_reports kwarg to with_event_reports, in-line with + other kwarg naming. [UFOSmuggler] +- Add kwarg to allow the inclusion of event reports into to_feed(), + honour with_distribution and valid_distributions kwargs. [UFOSmuggler] + + +v2.4.169.1 (2023-03-14) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add greynoise-ip object. [Raphaël Vinot] + + Fix #951 + + +v2.4.169 (2023-03-10) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Add local key in MISPTag. [Raphaël Vinot] + + Related #947 +- Use pytest for the tests. [Raphaël Vinot] + + +v2.4.168.1 (2023-02-28) +----------------------- + +New +~~~ +- [doc] added the Jupyter notebook used in a.7-rest-api-extensive- + restsearch. [Alexandre Dulaunoy] + +Changes +~~~~~~~ +- Bump changelog, version. [Raphaël Vinot] +- Bump templates, again. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Bump deps, templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Properly handle missing parameter in CSV importer. [Raphaël Vinot] + + Fix #931 +- Undefined variable in event delegation. [Raphaël Vinot] +- Remove reference to old pydeep. [Raphaël Vinot] + + Fix #914 + + +v2.4.168 (2023-01-23) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + + +v2.4.167.2 (2023-01-17) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] + +Fix +~~~ +- Set relationship_type default in MISPTag to empty string. [Raphaël + Vinot] +- Another typo in readme. [Raphaël Vinot] +- Typo in readme. [Raphaël Vinot] + + +v2.4.167.1 (2023-01-16) +----------------------- + +New +~~~ +- Add relationship_type in Tag entries for feeds. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump requests. [Raphaël Vinot] +- Bump pyzmq. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump python version used by read the docs. [Raphaël Vinot] +- Bump warning to inform user that python 3.10 wil be required in 12 + months. [Raphaël Vinot] +- Bump minimal PyMISP version to 3.8. [Raphaël Vinot] +- Re-bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Update whl files. [Raphaël Vinot] +- Nvm, readthedocs requires python 3.8 at most. [Raphaël Vinot] + + +v2.4.167 (2022-12-22) +--------------------- + +Changes +~~~~~~~ +- Bump objects. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump dependencies, move to poetry 1.3. [Raphaël Vinot] +- Bump certifi. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Re-order classes. [Raphaël Vinot] + +Other +~~~~~ +- Creation fo "add_attributes_from_csv.py" [Julien Mongenet] + + The file aims to ingest a formated CSV file containing attributes for MISP ingestion. +- Graceful handling of tagging when name attribute is missing. [Sura De + Silva] +- Add: Galaxy test sample. [Christian Studer] +- Add: Added very straight forward tests to make sure the galaxy + clusters are properly defined. [Christian Studer] +- Add: Added the `Galaxy` field to MISPAttribute using the MISPGalaxy + class. [Christian Studer] + + - Including an `add_galaxy` method similar to the + one used for events + - `attribute.galaxies` gives the list of attached + galaxy clusters + + +v2.4.166 (2022-11-28) +--------------------- + +New +~~~ +- Basic support for listing, enabling and disabling decaying models. + [Raphaël Vinot] +- [tests] Test for local tags. [Raphaël Vinot] + +Changes +~~~~~~~ +- Re-bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump deps, version. [Raphaël Vinot] +- [types] added azure-application-id. [iglocska] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- [describetypes] updated with the latest output from MISP. [iglocska] +- [types] added missing type value. [iglocska] + + +v2.4.165.1 (2022-11-10) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Properly bump version. [Raphaël Vinot] + +Other +~~~~~ +- Update __init__.py. [Marcelo Chaves] + + Regardless of running the latest PyMISP version, the message below is presented: + ``` + The version of PyMISP recommended by the MISP instance (2.4.165) is newer than the one you're using now (2.4.162.1). Please upgrade PyMISP. + ``` + + +v2.4.165 (2022-11-09) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump mypy. [Raphaël Vinot] +- Add links to doc. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Issue with EMailObject. [Raphaël Vinot] + + +v2.4.162.2 (2022-11-02) +----------------------- + +New +~~~ +- Add in ability to set a taxonomies required status. [Tom King] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump lief (CVEs), version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- [tests] fix the list name test following latest warning-list updates. + [Alexandre Dulaunoy] +- Bump deps. [Raphaël Vinot] +- Add dependabot. [Raphaël Vinot] + +Other +~~~~~ +- Revert "chg: [tests] fix the list name test following latest warning- + list" [Alexandre Dulaunoy] + + This reverts commit be3715595bcf08d497303198fefdf91c735b3fb2. +- Build(deps): bump actions/setup-python from 2 to 4. [dependabot[bot]] + + Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. + - [Release notes](https://github.com/actions/setup-python/releases) + - [Commits](https://github.com/actions/setup-python/compare/v2...v4) + + --- + updated-dependencies: + - dependency-name: actions/setup-python + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump actions/checkout from 2 to 3. [dependabot[bot]] + + Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. + - [Release notes](https://github.com/actions/checkout/releases) + - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/checkout/compare/v2...v3) + + --- + updated-dependencies: + - dependency-name: actions/checkout + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Build(deps): bump codecov/codecov-action from 1 to 3. + [dependabot[bot]] + + Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 3. + - [Release notes](https://github.com/codecov/codecov-action/releases) + - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) + - [Commits](https://github.com/codecov/codecov-action/compare/v1...v3) + + --- + updated-dependencies: + - dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major + ... +- Create codeql-analysis.yml. [Raphaël Vinot] + + +v2.4.162.1 (2022-10-02) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps and version. [Raphaël Vinot] + + Fix LIEF vuln. +- Bump deps, objects. [Raphaël Vinot] + +Fix +~~~ +- Change DNS warning list test. [Raphaël Vinot] + + +v2.4.162 (2022-09-09) +--------------------- + +New +~~~ +- Pass arbitrary headers to a PyMISP request. [Raphaël Vinot] +- Allow to force the timestamps in to_dict/to_json, even if a change was + made. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add in sort/desc for sorting results and limit/page for pagination. + [Tom King] +- Improve documentation for add_attribute. [Raphaël Vinot] + +Fix +~~~ +- Missing place to update version. [Raphaël Vinot] + + +v2.4.160.1 (2022-08-09) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Make keepalive configuration linux only. [Raphaël Vinot] + + Bump deps + + +v2.4.160 (2022-08-05) +--------------------- + +New +~~~ +- Enable TCP keepalive. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump deps. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Improve warning on invalid template, bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Make mypy happy. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add in test case. [Tom King] +- Add ability to filter by sharing group for RestSearch for MISP >= + v2.4.158. [Tom King] + +Fix +~~~ +- Delete sharing group after deleting the event. [Raphaël Vinot] +- Give more time to MISP to publish the events before searching. + [Raphaël Vinot] +- Improper json check on non-json responses. [Raphaël Vinot] + + Fix #854 +- Mark all attributes in a soft deleted object as soft deleted too. + [Raphaël Vinot] + + Bump misp-objects, deps. +- Make flake8 happy. [Raphaël Vinot] +- Properly convert MSG to EML. [Raphaël Vinot] +- Update lock file. [Raphaël Vinot] +- [feed] fixes bug when template_uuid does not exist. [Christophe + Vandeplas] + +Other +~~~~~ +- Update api.py. [Derekt2] +- Fix typo in logging message. [Philipp Hauswirth] +- Fig: [feed] fixes bugs during export with old data. [Christophe + Vandeplas] +- Update pyproject.toml. [Steven] + + Add publicsuffixlist optional package for URL Object, which has a more current list than pyfaup +- Fix multiple_space warning. [malvidin] +- Option to include more URLObject attributes Add publicsuffixlist faup + for URLObject Windows support URLObject with PSLFaup prefers IP to + host/domain. [malvidin] +- Ensure that keys are sorted in the returned `_to_feed()` dictionary. + [Yun Zheng Hu] + + This allows for better deterministic feed output generation. + + +v2.4.159 (2022-05-30) +--------------------- + +New +~~~ +- [example:copyTagsFromAttributesToEvent] Added script to copy tags from + attributes to the event level. [Sami Mokaddem] + +Changes +~~~~~~~ +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Massive bump deps for python 3.7. [Raphaël Vinot] + + +v2.4.157 (2022-03-24) +--------------------- + +Changes +~~~~~~~ +- Bump object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps, objects. [Raphaël Vinot] +- [tests] reverted. [Alexandre Dulaunoy] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- [tests] subversion are supported. [Alexandre Dulaunoy] + +Fix +~~~ +- [tests] check if the version is a substring as PyMISP might contain + sub version. [Alexandre Dulaunoy] + + +v2.4.155.1 (2022-03-03) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump required python version for doc. [Raphaël Vinot] +- Remove python 3.6 from metadata. [Raphaël Vinot] + +Fix +~~~ +- Incorrect call when requesting a new API key. [Raphaël Vinot] + + +v2.4.155 (2022-03-03) +--------------------- + +New +~~~ +- Get_new_authkey for a user. [Raphaël Vinot] +- [dep] Use pydeep2 instead of pydeep. [Jakub Onderka] + +Changes +~~~~~~~ +- Re-bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump new minimal python version to 3.7. [Raphaël Vinot] +- Perl dependencies not longer required. [Jakub Onderka] +- Simplify submodules checkout. [Jakub Onderka] +- Use https for link to documentation. [Jakub Onderka] +- Bump deps. [Raphaël Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- [FIPS] no clean way to support OpenSSL hashlib interface for FIPS. + [Alexandre Dulaunoy] +- [FIPS] falling back on older version of Python not having + usedforsecurity. [Alexandre Dulaunoy] +- [FIPS] in some cases, the `usedforsecurity` is not used. So fail if + the FIPS compliance is required and then the `usedforsecurity` is + disabled. [Alexandre Dulaunoy] +- [feeds] FIPS: when MD5 hashes are generated for fast-lookup it's not + for security. [Alexandre Dulaunoy] + + hashlib provides an option to tell if the hash is used for security or + not. By default, it's set to True. For the feed cache generation, it's + not. Then usedforsecurity=False +- Bump deps. [Raphaël Vinot] +- Bump deps, objects. [Raphaël Vinot] + +Fix +~~~ +- Libfuzzy-dev is not longer required. [Jakub Onderka] +- [mispevent] cannot type. [Alexandre Dulaunoy] +- Make mypy happy. [Raphaël Vinot] + +Other +~~~~~ +- Create add_filetype_object_from_csv.py. [Félix Herrenschmidt] +- Add feed option for local tag exclusion #817. [deku] + + +v2.4.152 (2021-12-22) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps, object templates. [Raphaël Vinot] +- Bump objects templates. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Lief doesn't supports python 3.10. [Raphaël Vinot] +- Debug poetry install, freezes on the GHA. [Raphaël Vinot] +- Bump deps, use pytest. [Raphaël Vinot] +- [feed-generator] support for distribution and sharing groups. + [Christophe Vandeplas] + +Fix +~~~ +- Update live tests to support proper format of SGs. [Raphaël Vinot] +- [sharinggroups] Fixes wrong model for SharingGroupOrg. [Christophe + Vandeplas] +- [feed-generator] code style fixes. [Christophe Vandeplas] +- [feed-generator] keeping function compatibility. [Christophe + Vandeplas] +- [feed-generator] fix missing except type. [Christophe Vandeplas] + + +v2.4.151 (2021-11-19) +--------------------- + +New +~~~ +- Add Blind Carbon Copy (bcc) headers. [Sami Tainio] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- [feed-generator] Make the feature to exlude attribute type more + generic. [Sami Mokaddem] +- [feed-generator] Added exclude malware samples option. [Sami Mokaddem] +- Bump deps, chardet is required by pyfaup. [Raphaël Vinot] +- Removed a whitespace. [Sami Tainio] +- Keep strict and generate attributes when needed. [Raphaël Vinot] +- Slight changes regarding timezones. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Unified constructors. [Thomas Dupuy] +- Slight changes regarding timezones. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [types] remove the duplicate. [Alexandre Dulaunoy] +- [describeTypes] remove duplicate filename-pattern. [Alexandre + Dulaunoy] +- [misp-objects] updated. [Alexandre Dulaunoy] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- Bump many dependencies. [Raphaël Vinot] +- Add in test case for get_sharing_group and validate orgs are present. + [Tom King] +- Improve sharing groups, bring back organsations included and ability + to get specific SG. [Tom King] +- Add in test case for searching against orgs and users. [Tom King] +- Add ability to search against orgs and users by freetext search (both) + or organisation (users) [Tom King] +- [test] Check if all category types exists. [Jakub Onderka] +- Bump changelog. [Raphaël Vinot] +- [py] Typo. [Steve Clement] +- [describeTypes] updated to include ssh-fingerprint. [Alexandre + Dulaunoy] + +Fix +~~~ +- [feed-generator] Revert back the event initial search to use the index + endpoint instead of RestSearch. [Sami Mokaddem] + + Relying on RestSearch was offering more flexibility than index in terms of filtering options, + however, it might introduce a significant overhead potentially leading to timeout. +- PyMISP.get_user_setting method. [Jakub Onderka] +- [tests] Remove debug prints. [Jakub Onderka] +- Fix final nosetest. [Tom King] +- Fix nosetests. [Tom King] +- [types] Update types to use `filename-pattern` type. [Jakub Onderka] +- [test] Remove debug print. [Jakub Onderka] +- [test] Correct error messages for blocked event. [Jakub Onderka] +- Missing import in __init__ [Raphaël Vinot] + + Fix #796 +- [tests] Fixed stix test. [chrisr3d] +- [py] Typo. [Steve Clement] + +Other +~~~~~ +- Update README.md. [Raphaël Vinot] + + +v2.4.148.1 (2021-09-30) +----------------------- + +New +~~~ +- Add few keys to email object creator. [Raphaël Vinot] + + Fix #787 +- Test cases for edit objects and upload stix. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [doc] Minor fixes, note and typo. [Steve Clement] +- Bump deps. [Raphaël Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- Update tutorial for custom objects. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump live tests. [Raphaël Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- [types] updated types/categories mapping. [Christophe Vandeplas] +- Remove test files. [Raphaël Vinot] +- Automatically pull the malwares repo when running + tests/testlive_comprehensive.py. [Raphaël Vinot] +- Remove submodules with malware. [Raphaël Vinot] +- Add test for updating a objects from a custom template. [Raphaël + Vinot] +- Re-bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Message_from_bytes really dislikes newline at the beginning of a mail. + [Raphaël Vinot] +- Skip IPs in Received header. [Raphaël Vinot] +- Name is passed to super. [Raphaël Vinot] +- Do not create empty manifest, json load dislikes it. [Raphaël Vinot] +- Initial round of cleanup on redis feed generator. [Raphaël Vinot] +- Upload of STIX document with non-ascii characters. [Raphaël Vinot] + + Due to: https://github.com/psf/requests/issues/5560 + + TL;DR: a variable of type str passed to data in a POST request will be + silently re-encoded to ISO-8859-1, making MISP barf on the other side. +- Remove outdated deps from setup.py. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/7729 + +Other +~~~~~ +- Remove unicode to ascii parts. [Sami Tainio] +- Fix #787 and add Unicode to ASCII function. [Sami Tainio] + + Fix #787 + - Uses regex to pick up the hostnames/domains from the "Received: from" headers. + + Unicode to ASCII function + - Spam messages more often than not contain junk text as unicode characters in the headers. The "from" and "subject" headers being the most common ones. Before this change the script would error on such emails or sometimes replace the unicode characters with questionmarks "?". + - Function takes argument as an input and then encodes it in ascii while ignoring any malformed data. It then returns an ASCII string without the unicode characters. + - Currently implemented for "from" and "subject" handling. +- Update README.md. [Raphaël Vinot] + + Not using travis anymore. + + +v2.4.148 (2021-08-05) +--------------------- + +New +~~~ +- Method `sharing_group_exists` [Jakub Onderka] +- Method `update_sharing_group` [Jakub Onderka] +- Save one REST call when initialize PyMISP class. [Jakub Onderka] +- Method `organisation_exists` [Jakub Onderka] +- Method `sharing_group_exists` [Jakub Onderka] +- Method `update_sharing_group` [Jakub Onderka] +- `to_dict` method supports `json_format` parameter. [Jakub Onderka] +- Method `organisation_exists` [Jakub Onderka] +- Method `sharing_group_exists` [Jakub Onderka] +- Method `update_sharing_group` [Jakub Onderka] +- Save one REST call when initialize PyMISP class. [Jakub Onderka] +- Method `organisation_exists` [Jakub Onderka] +- Method `sharing_group_exists` [Jakub Onderka] +- Method `update_sharing_group` [Jakub Onderka] +- Exclude decayed attributes in search. [Raphaël Vinot] + + Fix #753 + +Changes +~~~~~~~ +- Bump objects template. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Remove duplicates tests. [Raphaël Vinot] +- [testlive_comprehensive] correct path to access sharing group + releasability after edit. [iglocska] +- Properly validate update_sharing_group without pythonify. [Raphaël + Vinot] +- Bump missing dep. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [testlive_comprehensive] correct path to access sharing group + releasability after edit. [iglocska] +- [authkey test] removed from testlive_comprehensive. [iglocska] + + - the default now enables advanced authkeys making the retriaval of keys impossible after the user creation +- Do not load schema for event when not necessary. [Jakub Onderka] +- Bump deps. [Raphaël Vinot] +- `get_taxonomy` supports namespace. [Jakub Onderka] +- Properly validate update_sharing_group without pythonify. [Raphaël + Vinot] +- Bump missing dep. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [testlive_comprehensive] correct path to access sharing group + releasability after edit. [iglocska] +- [authkey test] removed from testlive_comprehensive. [iglocska] + + - the default now enables advanced authkeys making the retriaval of keys impossible after the user creation +- Do not load schema for event when not necessary. [Jakub Onderka] +- Bump deps. [Raphaël Vinot] +- `get_taxonomy` supports namespace. [Jakub Onderka] +- Properly validate update_sharing_group without pythonify. [Raphaël + Vinot] +- Bump missing dep. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [testlive_comprehensive] correct path to access sharing group + releasability after edit. [iglocska] +- [authkey test] removed from testlive_comprehensive. [iglocska] + + - the default now enables advanced authkeys making the retriaval of keys impossible after the user creation +- Do not load schema for event when not necessary. [Jakub Onderka] +- Bump deps. [Raphaël Vinot] +- `get_taxonomy` supports namespace. [Jakub Onderka] +- Update mypy, change accordingly. [Raphaël Vinot] + +Fix +~~~ +- Typo in key name. [Raphaël Vinot] +- [test] test_sharing_groups. [Jakub Onderka] +- [test] test_sharing_groups again. [Jakub Onderka] +- [test] test_sharing_groups. [Jakub Onderka] +- Typo in key name. [Raphaël Vinot] +- [test] test_sharing_groups again. [Jakub Onderka] +- [test] test_sharing_groups. [Jakub Onderka] +- [test] test_sharing_groups again. [Jakub Onderka] +- [test] test_sharing_groups. [Jakub Onderka] +- Flake8 stuff. [Raphaël Vinot] +- Revert rename, fix mypy. [Raphaël Vinot] +- Properly handle the case MISP is in a sub redirect. [Raphaël Vinot] + + Fix #757 + +Other +~~~~~ +- Revert "chg: Remove legacy stix converter." [iglocska] + + This reverts commit 94ce4a367bbde9284a6f29e6e6152c91de386879. + + - breaks misp-stix converter, reverting it for now, let's find a way to deprecate this without outright removing it +- Revert "chg: Remove legacy stix converter." [iglocska] + + This reverts commit 94ce4a367bbde9284a6f29e6e6152c91de386879. + + - breaks misp-stix converter, reverting it for now, let's find a way to deprecate this without outright removing it +- Revert "chg: Remove legacy stix converter." [iglocska] + + This reverts commit 94ce4a367bbde9284a6f29e6e6152c91de386879. + + - breaks misp-stix converter, reverting it for now, let's find a way to deprecate this without outright removing it + + +v2.4.144 (2021-06-07) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump object templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Other +~~~~~ +- Fix misp API response content parsing. [Silvian I] + + +v2.4.143 (2021-05-14) +--------------------- + +New +~~~ +- Method to get the raw object template. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump objects templates. [Raphaël Vinot] + +Fix +~~~ +- First-seen and last-seen on attributes and objects were not checked + for sanity. [Raphaël Vinot] +- Remove search_all example, use search instead. [Raphaël Vinot] + + +v2.4.142 (2021-04-26) +--------------------- + +New +~~~ +- Support for correlation exclusion list. [Raphaël Vinot] + + Fix #732 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Fix test suite. [Raphaël Vinot] +- Bump objects templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add comment for controller attribute in search. [Raphaël Vinot] + +Fix +~~~ +- Enable/disable feeds. [Raphaël Vinot] +- Mistake in mypy config. [Raphaël Vinot] +- Exclude data from mypy. [Raphaël Vinot] + + +v2.4.141.1 (2021-04-02) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Re-bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] + +Other +~~~~~ +- Fix bump version, deps, templates. [Raphaël Vinot] +- Update README.md. [Raphaël Vinot] + + +v2.4.141 (2021-04-01) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Get_uuid_or_id_from_abstract_misp accepts dict. [Raphaël Vinot] +- Remove references to ExpandedPyMISP. [Raphaël Vinot] + + Fix #721 +- Follow best practices and remove the logging handler. [Raphaël Vinot] +- Strip NULL string from value. [Raphaël Vinot] + + https://github.com/MISP/PyMISP/issues/678 +- Bump deps. [Raphaël Vinot] +- Raise exception on missing template in CSVLoader. [Raphaël Vinot] +- Bump templates. [Raphaël Vinot] +- Re-bump objects. [Raphaël Vinot] +- Bump object templates. [Raphaël Vinot] +- Add test case, fix mypy. [Raphaël Vinot] +- Take simple_value as value in MISPObject.add_attribute. [Raphaël + Vinot] + +Fix +~~~ +- Use get_uuid_or_id_from_abstract_misp in tag methods. [Raphaël Vinot] + + Fix #725 +- Skip nameless sections in ELF. [Raphaël Vinot] +- Make reportlab tests optional if missing dep. [Raphaël Vinot] +- Enable taxonomy failed if global pythonify is on. [Raphaël Vinot] +- Properly pass content-type. [Raphaël Vinot] +- Re-enable support for uploading STIX 1 documents. [Raphaël Vinot] + + Fix #711 + + +v2.4.140 (2021-03-03) +--------------------- + +New +~~~ +- Soft delete object in MISPEvent. [Raphaël Vinot] + + Fix #706 +- Add in ability to add a new cluster relation. [Tom King] +- MISP Galaxy 2.0 capability. [Tom King] +- Soft delete object in MISPEvent. [Raphaël Vinot] + + Fix #706 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump object templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [describetypes] updated. [Alexandre Dulaunoy] +- Bump objects templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump tests for galaxy cluster. [Raphaël Vinot] +- Improve Pydoc on search method's timestamp parameter. [Raphaël Vinot] + + Fix #708 +- Bump poetry file. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [data] describeTypes updated. [Alexandre Dulaunoy] +- Add deprecation warning for Python < 3.8. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Don't parse the meta key into cluster elements on a MISPEvent, but + allow users to manually perform this action. [Tom King] +- Add in nosetests for MISP Galaxy functions, check default key as a + dict attribute not MISPAbstract attribute. [Tom King] +- Add in more Galaxy 2.0 functions and code cleanup. [Tom King] +- Add in add_cluster function and ability to search clusters within a + galaxy. [Tom King] +- Remove legacy stix converter. [Raphaël Vinot] +- Improve Pydoc on search method's timestamp parameter. [Raphaël Vinot] + + Fix #708 +- Bump poetry file. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- [data] describeTypes updated. [Alexandre Dulaunoy] +- Add deprecation warning for Python < 3.8. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + +Fix +~~~ +- Typo in tests. [Raphaël Vinot] +- Make mypy happy in python 3.6 and 3.7. [Raphaël Vinot] +- Cosmetic changes, fix mypy. [Raphaël Vinot] +- Support text search again. [Raphaël Vinot] + + Fix #705 +- Do not add the serial-number twice. [Raphaël Vinot] +- Skip PE section if name is none AND size is 0. [Raphaël Vinot] +- Urllib3.__version__ may not have a patch number. [Raphaël Vinot] + + fix https://github.com/MISP/PyMISP/issues/698 +- Fix mispevent edit test by including default and distribution keys on + a GalaxyCluster. [Tom King] +- Support text search again. [Raphaël Vinot] + + Fix #705 +- Do not add the serial-number twice. [Raphaël Vinot] +- Skip PE section if name is none AND size is 0. [Raphaël Vinot] +- Urllib3.__version__ may not have a patch number. [Raphaël Vinot] + + fix https://github.com/MISP/PyMISP/issues/698 + +Other +~~~~~ +- Removed unused import. [Nick] +- Supress ssl warnings. [Nick] +- Re-added error checking for defaults. [Nick] +- Deleted all references to org as it's unneeded. [Nick] +- Re-added brackets. [Nick] +- Multiple updates to proofpoint example. [Nick] + + - Added additionally necessary keys to keys.py.example + - Added error check for unset keys + - Used built-in HTTP Basic Auth for requests instead of manually-created header + - Removed setting of orgc as that's pulled from the MISP key being used + - +- Removed cast of str to str. [Nick] +- Added check for invalid creds. [Nick] + + Without the added check, the script will error out on line 29 since the key doesn't exist in the dict. This at least gives a reason. +- Removed unused import. [Nick] +- Supress ssl warnings. [Nick] +- Re-added error checking for defaults. [Nick] +- Deleted all references to org as it's unneeded. [Nick] +- Re-added brackets. [Nick] +- Multiple updates to proofpoint example. [Nick] + + - Added additionally necessary keys to keys.py.example + - Added error check for unset keys + - Used built-in HTTP Basic Auth for requests instead of manually-created header + - Removed setting of orgc as that's pulled from the MISP key being used + - +- Removed cast of str to str. [Nick] +- Added check for invalid creds. [Nick] + + Without the added check, the script will error out on line 29 since the key doesn't exist in the dict. This at least gives a reason. + + +v2.4.138 (2021-02-08) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] + + +v2.4.137.4 (2021-02-04) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Add kw_params to tags. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump template ID in test case. [Raphaël Vinot] + + +v2.4.137.3 (2021-02-02) +----------------------- + +Changes +~~~~~~~ +- Bump version. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Fix and improve optional dependencies. [Raphaël Vinot] +- Make brotli optional. [Raphaël Vinot] + + +v2.4.137.2 (2021-02-01) +----------------------- + +New +~~~ +- Add in ability to create/update/delete MISP Event Reports. [Tom King] +- Hard delete flag for objects. [Raphaël Vinot] +- Fail if a duplicate object is added to an event. [Raphaël Vinot] +- Support brotli compression. [Jakub Onderka] +- Hard delete flag for objects. [Raphaël Vinot] +- Fail if a duplicate object is added to an event. [Raphaël Vinot] +- Add in ability to create/update/delete MISP Event Reports. [Tom King] +- Add in ability to create/update/delete MISP Event Reports. [Tom King] +- Hard delete flag for objects. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add brotli support in the dependencies. [Raphaël Vinot] +- Make mypy happy. [Raphaël Vinot] +- Make clear that to_json returns str. [Raphaël Vinot] +- Disable correlation on malware-sample for FileObject. [Raphaël Vinot] +- Bump objects templates. [Raphaël Vinot] +- Add missing autodoc. [Raphaël Vinot] + + fix #693 +- Add in delete function for a MISP Object. [Tom King] +- Fix return of delete_event_report. [Raphaël Vinot] +- Remove critical warning if lief is not installed. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/6908 +- Bump deps. [Raphaël Vinot] +- Allow response of delete to be pythonify, add in nosetest. [Tom King] +- Add ability to get event reports from the Event ID. [Tom King] +- Remove travis file, GH Actions is better. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Remove critical warning if lief is not installed. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/6908 +- Add test case fir add_attribute and enforceWarninglist=True. [Raphaël + Vinot] +- Add testcase with breakOnDuplicate in a MISPObject. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add test case for page/limit in logs search. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Improve docstring for get_event. [Raphaël Vinot] + + fix #686 +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Show size when the json is not loadable. [Raphaël Vinot] +- Add authenticode support in generate_file_objects. [Raphaël Vinot] +- Use lief 0.11.0, generate authenticode entries. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps, add 3.9 in GH. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps, objects templates. [Raphaël Vinot] +- Make clear that to_json returns str. [Raphaël Vinot] +- Disable correlation on malware-sample for FileObject. [Raphaël Vinot] +- Bump objects templates. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add missing autodoc. [Raphaël Vinot] + + fix #693 +- Add in delete function for a MISP Object. [Tom King] +- Bump deps. [Raphaël Vinot] +- Fix return of delete_event_report. [Raphaël Vinot] +- Remove travis file, GH Actions is better. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Remove critical warning if lief is not installed. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/6908 +- Add test case fir add_attribute and enforceWarninglist=True. [Raphaël + Vinot] +- Add testcase with breakOnDuplicate in a MISPObject. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add test case for page/limit in logs search. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Improve docstring for get_event. [Raphaël Vinot] + + fix #686 +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Show size when the json is not loadable. [Raphaël Vinot] +- Add authenticode support in generate_file_objects. [Raphaël Vinot] +- Use lief 0.11.0, generate authenticode entries. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps, add 3.9 in GH. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps, objects templates. [Raphaël Vinot] +- Allow response of delete to be pythonify, add in nosetest. [Tom King] +- Add ability to get event reports from the Event ID. [Tom King] +- Remove travis file, GH Actions is better. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Remove critical warning if lief is not installed. [Raphaël Vinot] + + Fix https://github.com/MISP/MISP/issues/6908 +- Add test case fir add_attribute and enforceWarninglist=True. [Raphaël + Vinot] +- Add testcase with breakOnDuplicate in a MISPObject. [Raphaël Vinot] + +Fix +~~~ +- Flake error. [Raphaël Vinot] +- Update testlive accordingly. [Raphaël Vinot] +- Better warning if lief is outdated. [Raphaël Vinot] +- Call the AbstractMISP.from_dict at the end of the function to ensure + the edited flag remains false. [Tom King] +- Better warning if lief is outdated. [Raphaël Vinot] +- Update minimal dependency for lief in setup.py. [Raphaël Vinot] +- [dev mode only] force older jedi to avoid ipython exception. [Raphaël + Vinot] +- Add python 3.9 in GH Actions. [Raphaël Vinot] +- Update testlive accordingly. [Raphaël Vinot] +- Better warning if lief is outdated. [Raphaël Vinot] +- Update minimal dependency for lief in setup.py. [Raphaël Vinot] +- [dev mode only] force older jedi to avoid ipython exception. [Raphaël + Vinot] +- Add python 3.9 in GH Actions. [Raphaël Vinot] +- Call the AbstractMISP.from_dict at the end of the function to ensure + the edited flag remains false. [Tom King] + + +v2.4.137.1 (2021-01-21) +----------------------- + +New +~~~ +- Fail if a duplicate object is added to an event. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add test case for page/limit in logs search. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Improve docstring for get_event. [Raphaël Vinot] + + fix #686 +- Bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Better warning if lief is outdated. [Raphaël Vinot] +- Update minimal dependency for lief in setup.py. [Raphaël Vinot] + + +v2.4.137 (2021-01-20) +--------------------- + +New +~~~ +- Allow to pass an object template to MISPObject.__init__ [Raphaël + Vinot] + + MISPObject part of #6670 + +Changes +~~~~~~~ +- Bump version. [Raphaël Vinot] +- Show size when the json is not loadable. [Raphaël Vinot] +- Add authenticode support in generate_file_objects. [Raphaël Vinot] +- Use lief 0.11.0, generate authenticode entries. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump deps, add 3.9 in GH. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump deps, objects templates. [Raphaël Vinot] +- Add controller argument to get_csv script. [Raphaël Vinot] +- [test] file object template are now 24. [Alexandre Dulaunoy] +- [test] file object template is now at version 24. [Alexandre Dulaunoy] +- [misp-objects] updated. [Alexandre Dulaunoy] +- [type] favicon-mmh3 is the murmur3 hash of a favicon as used in + Shodan. [Alexandre Dulaunoy] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- Clarify misp_objects_template_custom. [Raphaël Vinot] +- Add docstring for misp_objects_template_custom. [Raphaël Vinot] +- Trigger GH actions on PR. [Raphaël Vinot] +- Improve documentation of MISPAttribute.malware_binary. [Raphaël Vinot] +- Remove trailing space. [Raphaël Vinot] +- On-demand decryption of malware-binary, speeds up pythonify. [Raphaël + Vinot] +- Force a few packages versions. [Raphaël Vinot] + +Fix +~~~ +- [dev mode only] force older jedi to avoid ipython exception. [Raphaël + Vinot] +- Add python 3.9 in GH Actions. [Raphaël Vinot] +- Do not fail if extract_msg is missing. [Raphaël Vinot] +- Properly decode the body depending on the encoding of the email. + [Raphaël Vinot] + + Fix #671 +- Properly match IO in load event. [Raphaël Vinot] +- Typing on recent mypy. [Raphaël Vinot] +- Typing edge case. [Raphaël Vinot] +- Add attribute dict as proposal. [Raphaël Vinot] + +Other +~~~~~ +- Noticed that test data mail_5.msg was malformatted. Replaced with + working test msg. [seamus tuohy] +- Updated emailobject. [seamus tuohy] + + Email object no longer requires extra php libraries for install. + Tests have been expanded to improve coverage. + RTF encapsulated HTML and Plain Text will now be de-encapsulated. + The raw MSG binary will now be included in the extracted email object. +- Adding check if "from" is in the "received" header row. [nighttardis] +- Update `vmray_automation` to stay compatible with the changes made to + `vmray_import` MISP modules. [Jens Thom] +- Update mispevent.py. [Raphaël Vinot] + + +v2.4.135.3 (2020-11-24) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Improve typing. [Raphaël Vinot] +- Improve add_attribute with a list. [Raphaël Vinot] + +Fix +~~~ +- Do not fail on PyMISP import when mail-parser is not present. [Raphaël + Vinot] + + +v2.4.135.2 (2020-11-24) +----------------------- + +New +~~~ +- Add Github workflow. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Improve error message if a type in missing. [Raphaël Vinot] +- [type] process-state added. [Alexandre Dulaunoy] +- Bump misp-objects. [Raphaël Vinot] +- [misp-objects] updated. [Alexandre Dulaunoy] +- Add path to CSV sample files. [Raphaël Vinot] +- [types] jarm-fingerprint added. [Alexandre Dulaunoy] + +Fix +~~~ +- Remove python 3.9 from action (lief not supported yet) [Raphaël Vinot] +- Initialize submodules in gh action. [Raphaël Vinot] +- Make mail-parser really optional. [Raphaël Vinot] + + +v2.4.135.1 (2020-11-24) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version, travis install. [Raphaël Vinot] +- Make mail-parser an optional dependency. [Raphaël Vinot] + + +v2.4.135 (2020-11-23) +--------------------- + +New +~~~ +- Test parsing just email header. [Jakub Onderka] +- Test parsing outlook message format. [Jakub Onderka] +- Add tests for EmailObject. [Jakub Onderka] +- Refactored emailobject generator. [Jakub Onderka] +- Export display name from email. [Jakub Onderka] +- Parse date from email. [Jakub Onderka] +- Method to check attribute and object existence. [Jakub Onderka] +- Allow to get just event metadata after add_event and edit_event. + [Jakub Onderka] +- Method to check event existence. [Jakub Onderka] +- Add method to search for tags. [Raphaël Vinot] + + fix #648 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Add search info field with "\" [Raphaël Vinot] +- Improve documentation of search_index. [Raphaël Vinot] +- Improve error handling for Outlook emails. [Raphaël Vinot] +- Bump object templates. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Update gitignore. [Raphaël Vinot] + + fix #613 +- Do not split a string into a list in complex query builder. [Raphaël + Vinot] + + fix #597 +- Force enable debug in test, test update tags. [Raphaël Vinot] +- Use REST search for the tags. [Raphaël Vinot] + + Related to comments on a1326f2cf2bcfd6e285188e0661b12076fe92747 +- Add typing meta. [Raphaël Vinot] + +Fix +~~~ +- [emailobject] Correctly parse multiple addresses. [Jakub Onderka] +- Test suite for exists calls. [Raphaël Vinot] +- Path for event creating and editing. [Jakub Onderka] +- Object_uuid could be None. [Raphaël Vinot] + + Fix #640 +- Last_seen has to be after first_seen, and it should habe been failing + before. [Raphaël Vinot] +- Missing f-string marker. [Raphaël Vinot] +- Fix: Docstring improvment based on @chrisinmtown's feedback. [Raphaël + Vinot] + +Other +~~~~~ +- We can now upload stix object directly. File is not necessary. [Remy + Dewailly] +- We can now upload stix object directly. File is not necessary. [Remy + Dewailly] + + +v2.4.134 (2020-11-02) +--------------------- + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Keep connection alive between requests. [Jakub Onderka] +- Bump deps. [Raphaël Vinot] +- Format docstrings in mispevent.py. [Lott, Christopher (cl778h)] + + Add ":param " prefix to parameters to improve ReadTheDocs output. + Fix some minor typos in docstrings. +- Bump deps. [Raphaël Vinot] +- Bump deps. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Remove duplicate check if debug logging is enabled. [Jakub Onderka] +- Do now fail on requests returning plain text. [Raphaël Vinot] + + Fix #639 + +Other +~~~~~ +- Revert "Update .travis.yml" [Raphaël Vinot] + + lief isn't compatible with python 3.9 + + This reverts commit e10843fa33c9a08b7da4ef24cbce457be53a7459. +- Update .travis.yml. [Raphaël Vinot] + + Add python 3.9 +- Drop `encoding=` in Python 3.9. [Friedrich Lindenberg] + + +v2.4.133 (2020-10-16) +--------------------- + +New +~~~ +- [attribute type] telfhash added. [Alexandre Dulaunoy] +- [add_gitlab_user] new gitlab user fetch script to MISP object. + [Alexandre Dulaunoy] + + usage: add_gitlab_user.py [-h] -e EVENT [-f] -u USERNAME [-l LINK] + + Fetch GitLab user details and add it in object in MISP + + optional arguments: + -h, --help show this help message and exit + -e EVENT, --event EVENT + Event ID to update + -f, --force-template-update + -u USERNAME, --username USERNAME + GitLab username to add + -l LINK, --link LINK Url to access the GitLab instance, Default is + www.gitlab.com. +- [example] add_github_user example - WiP. [Alexandre Dulaunoy] + + usage: add_github_user.py [-h] -e EVENT [-f] -u USERNAME + + Fetch GitHub user details and add it in object in MISP + + optional arguments: + -h, --help show this help message and exit + -e EVENT, --event EVENT + Event ID to update + -f, --force-template-update + -u USERNAME, --username USERNAME + GitHub username to add +- Method to get the new version of the templates. [Raphaël Vinot] +- Delete tags via update_attribute, search by sharing group. [Tom King] + +Changes +~~~~~~~ +- Bump object templates. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump test cases. [Raphaël Vinot] +- [type] updated. [Alexandre Dulaunoy] +- Bump file obj version in tests. [Raphaël Vinot] +- [data] misp-objects updated. [Alexandre Dulaunoy] +- Bump build system to poetry 1.1. [Raphaël Vinot] +- [type] new type added. [Alexandre Dulaunoy] +- [add_github_user] add ssh keys of the user in the MISP object. + [Alexandre Dulaunoy] +- [add_github_user] more fields added from the GitHub API. [Alexandre + Dulaunoy] +- Bump deps, objects. [Raphaël Vinot] +- Add test for delete=True in get_event. [Raphaël Vinot] +- [add_github_user] add following to the MISP object. [Alexandre + Dulaunoy] +- Bump dependencies. [Raphaël Vinot] +- Pass a list to add_attributes. [Raphaël Vinot] +- Use MISPObject instead of GenericObjectGenerator. [Raphaël Vinot] +- [doc] add a reference to the license. [Alexandre Dulaunoy] +- Add docstrings and extend conf.py for RTD. [Lott, Christopher + (cl778h)] + + Add minimal docstrings to public methods so ReadTheDocs will display them. + Add autodoc mock import for lief so RTD can generate HTML for tools. + + This fixes issue #626 +- Remove PyMISPExpanded from the docs. [Raphaël Vinot] +- Add comments to ELF, PE, and MachO object generators. [Raphaël Vinot] +- Improve error message, add comments, rename whitelist->allowedlist. + [Raphaël Vinot] +- Remove SG search for search() func as this doesn't support SG + searching, but the index does. [Tom King] + +Fix +~~~ +- Test on macosx. [Raphaël Vinot] + + Fix #630 +- Do not modify default_attributes_parameters in MISPObject. [Raphaël + Vinot] +- Wrong call to pymisp.search_index. [Raphaël Vinot] +- Few outdated calls in the tutorial. [Raphaël Vinot] +- Make flake8 happy. [Raphaël Vinot] +- Merge SG params to allow search. [Tom King] + +Other +~~~~~ +- Fix PyMISP repo URL. [garanews] + + MISP/PyMISP vs CIRCL/PyMISP +- Fix typo. [garanews] + + fix typo +- Attempt to decode utf-8-sig encoded emails. [seamus tuohy] + + eml files downloaded from Windows Online security on some Windows 11 + systems are automatically encoded in UTF with a byte order mark (BOM) + at the front of the file. This will cause the email parser to fail. + + This is a somewhat isolated problem. It only will affects a small + subset of Windows users who download and re-upload eml files. But, + this small subset of users is the target user-base for the MISP + email module: low expertiese users who wish to quickly share + high-value indicators on an ad-hoc basis. + + While this fix could be tacked onto the MISP email module instead of + here, I beleive that this fix is more appropriate in the PyMISP object + code. As the "email" object parser this object should be built to + parse all manner of emails that it may encounter. This includes common + malformations such as this one and, even horrors such as, the .msg + format. This commit adds a generically named "attempt_decoding" + function which can be expanded to address all manner of sins that + are encountered in the future. + + +v2.4.131 (2020-09-08) +--------------------- + +New +~~~ +- [test] Validate tag removal. [Raphaël Vinot] +- [describeTypes] sha3 added. [Alexandre Dulaunoy] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- [describeTypes] updated. [Alexandre Dulaunoy] +- [describeTypes] updated. [Alexandre Dulaunoy] +- Bump objects. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump file template version. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Rename blacklist -> blocklist. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] + + +v2.4.130 (2020-08-20) +--------------------- + +New +~~~ +- Blacklist methods. [Raphaël Vinot] +- Add list of missing calls. [Raphaël Vinot] +- Add test_obj_references_export. [louis] +- Add MISPObject.standalone property. [louis] + + Setting MISPObject.standalone updates MISPObject._standalone and + add/removes "ObjectReference" from AbstractMISP.__not_jsonable using + update_not_jsonable/_remove_from_not_jsonable. +- Add AbstractMISP._remove_from_not_jsonable. [louis] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump types. [Raphaël Vinot] +- [testlive_comprehensive] Updated generic tagging method to match + changes in MISP. [mokaddem] +- Cleanup blocklist methods. [Raphaël Vinot] +- Remove outdated example. [Raphaël Vinot] + + Fix #611 +- New test_get_non_exists_event. [Jakub Onderka] +- Bump dependencies. [Raphaël Vinot] +- Enable more tests. [Raphaël Vinot] +- Make get_object return a not standalone object. [louis] +- Remove standalone default value from MISPObject children c'tor. + [louis] + + MISPObject.__init__ sets standalone=True by default, so there is no + need to do it in its child classes. +- Make MISPObject standalone by default. [louis] + + standalone defaults to True in MISPObject.__init__, and is set to False + when the object is added to an event. +- Add MISPObject._standalone type. [louis] + +Fix +~~~ +- Bump file template version. [Raphaël Vinot] +- Test_get_non_exists_event. [Jakub Onderka] +- IP removed from the public DNS list. [Raphaël Vinot] +- Example using deprecated calls. [Raphaël Vinot] + + fix #602 +- Add STIX XML output for the search. [Raphaël Vinot] + + Use stix-xml as return_format. + + Fix #600 https://github.com/MISP/MISP/issues/5618 +- Dummy event example. [Raphaël Vinot] + + Fix #598 + +Other +~~~~~ +- Exclude section correlation .rsrc and zero-filled. [deku] +- Linting/Add missing whitespace. [Paal Braathen] +- Remove explicit loglevel checking. [Paal Braathen] +- Remove explicit traceback printing. [Paal Braathen] +- Master branch has been renamed to main. [Arcuri Davide] +- Update README.md. [Raphaël Vinot] + + fix: #599 + + +v2.4.128 (2020-06-22) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Add a few test cases. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] + + +v2.4.127.1 (2020-06-19) +----------------------- + +New +~~~ +- Optionally include deleted attributes/objects in feed. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Add test case for get event deleted. [Raphaël Vinot] +- Add test case for search deleted. [Raphaël Vinot] +- Update comments for search. [Raphaël Vinot] + +Fix +~~~ +- Keep deleted key in MISPObject and MISPObjectAttribute. [Raphaël + Vinot] + + +v2.4.127 (2020-06-16) +--------------------- + +New +~~~ +- Add helper and test case for GitVulnFinderObject. [Raphaël Vinot] +- Add git-commit-id type. [Raphaël Vinot] +- Add deleted in field export. [Raphaël Vinot] + + Fix #586 +- Timeout for connection/request, fixes #584. [Christophe Vandeplas] + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Rename master -> main. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Rename branches master -> main. [Raphaël Vinot] +- Remove extra parameter in change_user_password. [Raphaël Vinot] + +Fix +~~~ +- Do not fail if the attribute value is not a string. [Raphaël Vinot] +- Properly strip value in MISPObject.add_attribute, take 2. [Raphaël + Vinot] + + Fix #546 +- Properly strip value in MISPObject.add_attribute. [Raphaël Vinot] + + Fix #546 +- Deleted is not always required in the feed export. [Raphaël Vinot] +- Make mypy happy. [Raphaël Vinot] +- Fixes bug in timeout change. [Christophe Vandeplas] +- Fixes bug in timeout change. [Christophe Vandeplas] +- Fixes bug in timeout change. [Christophe Vandeplas] +- Fixes bug in timeout change. [Christophe Vandeplas] +- Fixes bug in timeout change. [Christophe Vandeplas] + + hail to Rafiot +- Fixes bug in timeout change. [Christophe Vandeplas] +- Fixes bug in timeout change. [Christophe Vandeplas] + +Other +~~~~~ +- Previously file object was reporting the libmagic description of a + file instead of the mimetype. According to [MISP + DataModels](https://www.misp-project.org/datamodels/#types) ``` mime- + type: A media type (also MIME type and content type) is a two-part + identifier for file formats and format contents transmitted on the + Internet ``` more precisely defined in + [RFC2045](https://tools.ietf.org/html/rfc2045) and others. [Troy Ross] + + The description returned by libmagic is more useful than the generic mime-type, + but I did not find a place to put the description in the current data model. +- Fix end of line encoding of examples/cytomic_orion.py. [Sebastian + Wagner] + + +v2.4.126 (2020-05-18) +--------------------- + +New +~~~ +- Test search with timestamp. [Raphaël Vinot] +- Add testcase for updating partial event. [Raphaël Vinot] +- Add pyfaup as optional dependency. [Raphaël Vinot] +- [dev] add microblog object tool. [VVX7] +- Very simple test case for rest search on objects. [Raphaël Vinot] +- Self registration, object level search (initial) [Raphaël Vinot] +- [dev] add flag to get extended misp event. [VVX7] +- [dev] add flag to get extended misp event. [VVX7] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-object. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Add test for feed partial update. [Raphaël Vinot] +- Strip empty parameters in build_complex_query. [Raphaël Vinot] + + Fix #577 +- Simplify delete_attribute. [Raphaël Vinot] +- Bump travis install. [Raphaël Vinot] +- Add comment in microblog object. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- [dev] clean up how keys are accessed in self._parameters. [VVX7] +- [dev] use isinstance() type check. [VVX7] +- [dev] fix abstract generator import. add logger. [VVX7] +- [dev] change type() == list. [VVX7] +- Bump misp-objects. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- [dev] remove duplicate line. [VVX7] +- [dev] add extend_event() test. chg typo in get_event() [VVX7] +- Re-Bump CHANGELOG. [Raphaël Vinot] + +Fix +~~~ +- Settings is not required in MISPFeed. [Raphaël Vinot] +- Properly skip timestamp in __iter__ when needed. [Raphaël Vinot] +- Catch exception when liblua-5.3 is not present. [Raphaël Vinot] +- Make flake8 happy. [Raphaël Vinot] +- Properly load feeds, fix undefined variable. [Raphaël Vinot] +- Make flake8 happy. [Raphaël Vinot] +- Remove extra print. [Raphaël Vinot] +- Typo, add test for extended event. [Raphaël Vinot] + +Other +~~~~~ +- Update docstring in api.py. [Bernhard E. Reiter] + + * remove typo in ssl parameter docstring. + * Add hint that other certs (which are not in the default CAs, but also are not self signed in a strict sense) can also use the CA_BUNDLE function of the ssl parameter. + + +v2.4.125 (2020-04-30) +--------------------- + +New +~~~ +- Extended option on get event. [Raphaël Vinot] + + Related to #567 + +Changes +~~~~~~~ +- Bump version in pyproject. [Raphaël Vinot] +- Bump CHANGELOG. [Raphaël Vinot] +- Bump objects, deps. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Remove old suricata script, keep reference to old code. [Raphaël + Vinot] + +Fix +~~~ +- Enable autoalert on admin user. [Raphaël Vinot] +- [abstract] Forces file to be read with utf8 encoding. [mokaddem] +- Properly handle timezone in tests. [Raphaël Vinot] + +Other +~~~~~ +- Update up.py. [Raphaël Vinot] + + Fix #563 +- Fixed __query_virustotal return type. [DocArmoryTech] + + __query_virustotal returned a Response object and not the json expected; modified so that report_json is returned instead of report. + + +v2.4.124 (2020-03-30) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Add option to aggregare by country. [Raphaël Vinot] +- [CSSE COVID] Publish the event immediately. [Raphaël Vinot] +- Add changelog and readme in the package. [Raphaël Vinot] +- Bump version in pyproject. [Raphaël Vinot] + +Fix +~~~ +- Strip every string in AbstractMISP. [Raphaël Vinot] + + fix #546 +- Incorrect expectation of attribute value to be a str - take 2. + [Raphaël Vinot] + + Related #553 +- Incorrect expectation of attribute value to be a str. [Raphaël Vinot] + + Fix #553 + +Other +~~~~~ +- Dos2unix examples/stats_report.py. [Sebastian Wagner] +- Cytomic Orion API access. [Koen Van Impe] +- Add organisations from CSV. [Koen Van Impe] +- Minor updates to vmray_automation for travis. [Koen Van Impe] +- VMRay Automation with ExpandedPyMISP. [Koen Van Impe] + + +v2.4.123 (2020-03-10) +--------------------- + +New +~~~ +- Add import script for dxy data. [Raphaël Vinot] +- Csse covid19 daily report importer. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- JSON files are UTF8. [Raphaël Vinot] + + Bump dev deps, update comment +- Add tag, set distribution, add file and source (CSSE importer) + [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] + + +v2.4.122 (2020-02-26) +--------------------- + +New +~~~ +- Add uuid by default in MISPEvent, add F/L seen in feed output. + [Raphaël Vinot] +- Admin script to setup a sync server. [Raphaël Vinot] +- Add feed generation example in notebook. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Comments were still referencing pipenv. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump dep. [Raphaël Vinot] +- Fix typo in readme. [Raphaël Vinot] +- Use bionic on travis. [Raphaël Vinot] +- Add poetry support. [Raphaël Vinot] + +Fix +~~~ +- Test cases & template version. [Raphaël Vinot] +- Mypy, more typing. [Raphaël Vinot] +- Do not skip data in add_attribute methods. [Raphaël Vinot] +- Remove references to the old API. [Raphaël Vinot] + +Other +~~~~~ +- Use poetry everywhere, fix readme. [Raphaël Vinot] + + +v2.4.121.1 (2020-02-07) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] + +Fix +~~~ +- Make lief optional again. [Raphaël Vinot] + + fix #538 + + +v2.4.121 (2020-02-06) +--------------------- + +New +~~~ +- Add includeDecayScore to rest search. [VVX7] +- Support for first_seen/last_seen. [Raphaël Vinot] + + Cleaner import of datetime +- [attributes] chrome-extension-id added. [Alexandre Dulaunoy] + +Changes +~~~~~~~ +- Bump version. [Raphaël Vinot] +- Do not install neo by default. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- More flexible when an event is in a weird state. [Raphaël Vinot] +- Str to int, properly load SharingGroup. [Raphaël Vinot] + + Fix #535 +- Bump deps, add pep8 test. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Support dict in tag/untag. [Raphaël Vinot] +- Test update last seen. [Raphaël Vinot] +- Add test cases in feed. [Raphaël Vinot] +- Add test cases. [Raphaël Vinot] +- Normalize to_datetime conversion. [Raphaël Vinot] +- Trustar example uses objects. [Raphaël Vinot] +- Add lief in the generic requirements. [Raphaël Vinot] +- Refactorize typing, validate. [Raphaël Vinot] + +Fix +~~~ +- Bump objects. [Raphaël Vinot] +- Issue with readme. [Raphaël Vinot] +- Remove debugging. [Raphaël Vinot] +- [*-seen] Consider that `-` can also be in the date component while + parsing. [mokaddem] +- First seen was after last seen, trigerring the exception. [Raphaël + Vinot] +- Tests failing if local tz was not CET. [Raphaël Vinot] +- Syntax and typos. [Raphaël Vinot] +- Bugs introduced by last commit. [Raphaël Vinot] + +Other +~~~~~ +- Doc: fix Search-FullOverview.ipynb code example. [Bernhard E. Reiter] +- Chore: delete old examples. [Manabu Niseki] + + Delete examples which use deprecated/deleted methods +- Scrape trustar intel platform reports and create misp events. + [th3jiv3r] +- Configuration for trustar integration. [th3jiv3r] +- Fixed trailing lines. [turtlefac3] +- Fixed trailing lines. [turtlefac3] +- Custom integration written in python to scrape Proofpoint VAP API for + metrics of top Very Attacked Persons and create MISP events. + [turtlefac3] +- Fix typos on FullOverview.ipynb. [Bernhard E. Reiter] + + +v2.4.120 (2020-01-17) +--------------------- + +New +~~~ +- [attribute type] kusto-query attribute type. [Alexandre Dulaunoy] + + Kusto query is the query language for the Kusto services in Azure used + to search large dataset. It's used in Windows Defender ATP Hunting-Queries + and also Azure Sentinel (Cloud-native SIEM). +- Remove python < 3.6 support. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump Changelog. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump dependencies, add debug. [Raphaël Vinot] +- Upate dummy events creator. [Raphaël Vinot] +- Add tests on more version of Python. [Raphaël Vinot] +- Search with the STIX output returns a json STIX. [Raphaël Vinot] + + Was XML before. +- Bump dependencies. [Raphaël Vinot] +- Add more typing information. [Raphaël Vinot] +- Add typing markup. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Bump Dependencies. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] + +Fix +~~~ +- Bump template_version in test cases. [Raphaël Vinot] +- Add missing variable in dummy creator. [Raphaël Vinot] +- Et2misp was python2 only. [Raphaël Vinot] +- Feed generator was broken. [Raphaël Vinot] + + Fix #506 +- Event without hashable attribute. [Raphaël Vinot] + + Related #506 + +Other +~~~~~ +- Update api.py. [AaronK] + + minor typo, can;t help it noticing those. sorry, +- Fixed TODO, added quarantineFolder/quarantineRule from + messagesBlocked, added some error handling to prevent empty attributes + from trying to be added. [th3jiv3r] +- Scrape proofpoint tap api for messages blocked/delivered & clicks + blocked/permitted and create misp events. [th3jiv3r] +- Add variable for proofpoint tap api auth. [th3jiv3r] +- Update README.md. [AaronK] + + minor typo +- Define the number of entries to output. [AndreC10002] + + Allow for defining in the settings.py file the number of entries to output +- Update generate.py. [AndreC10002] +- Cleanup of code and 'quick-n-dirty' sanitizing of tags. [Koen Van + Impe] +- Sync. [Koen Van Impe] +- Update README.md. [Raphaël Vinot] + + +v2.4.119.1 (2019-12-17) +----------------------- + +New +~~~ +- URLObject (requires pyfaup) [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Version bump. [Raphaël Vinot] +- Bump test files. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Debug travis error message. [Raphaël Vinot] +- [types] eppn type added. [Alexandre Dulaunoy] +- Fix typo. [Raphaël Vinot] +- Move scrippsco2 feed generator to a sub directory. [Raphaël Vinot] +- Update documentation. [Raphaël Vinot] + + Fix #396 +- Bump objects. [Raphaël Vinot] + +Fix +~~~ +- Properly test custom objects. [Raphaël Vinot] +- Adding a sighting takes a little bit of time. [Raphaël Vinot] +- Test case on reference. [Raphaël Vinot] +- Add missing fields to event & attribute for the feed output. [Raphaël + Vinot] +- Make sure the publish timestamp is bumped on update. [Raphaël Vinot] + + +v2.4.119 (2019-12-02) +--------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] + +Fix +~~~ +- Bump lief to 0.10.1. [Raphaël Vinot] +- Update tests. [Raphaël Vinot] +- Raise PyMISPError instead of Exception. [Raphaël Vinot] +- Rename feed_meta_generator so it clearly fails with python<3.6. + [Raphaël Vinot] + + +v2.4.117.3 (2019-11-25) +----------------------- + +New +~~~ +- Script to generate the metadata of a feed out of a directory. [Raphaël + Vinot] +- Add to_feed export to MISPEvent. [Raphaël Vinot] +- Validate object templates. [Raphaël Vinot] + + fix https://github.com/MISP/misp-objects/issues/199 +- Test cases for restricted tags. [Raphaël Vinot] + + Fix #483 +- Get Database Schema Diagnostic. [Raphaël Vinot] + + Fix #492 + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Require stable version of lief again. [Raphaël Vinot] +- Few more improvements on the feed export. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Make the feed generator more generic. [Raphaël Vinot] +- Use New version of PyMISP in the feed generator. [Raphaël Vinot] +- Bump misp-object. [Raphaël Vinot] +- Allow to sort and indent the json output for objects. [Raphaël Vinot] +- Bump objects. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- [test] feed test updated as botvrij is now TLS by default. [Alexandre + Dulaunoy] + +Fix +~~~ +- Improve stability of feed output. [Raphaël Vinot] +- Do not unitialize the uuid in MISPEvent. [Raphaël Vinot] +- Bump url template version in test cases. [Raphaël Vinot] +- Python 2.7 tests. [Raphaël Vinot] +- Print the full json blob in debug mode. [Raphaël Vinot] + + Related https://github.com/MISP/PyMISP/issues/462 + +Other +~~~~~ +- Cch: Bump misp-objects. [Raphaël Vinot] + + +v2.4.117.2 (2019-10-30) +----------------------- + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] + +Fix +~~~ +- Avoid exception on legacy MISP. [Raphaël Vinot] + + +v2.4.117.1 (2019-10-30) +----------------------- + +New +~~~ +- Add support for UserSettings. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Use default category from template. [Raphaël Vinot] + + Fix #477 +- Skip usersettings tests when emails are disabled. [Raphaël Vinot] + +Fix +~~~ +- [examples] typo uuid. [Jean-Louis Huynen] + + give me a hoodie. +- Prevents exception when lief is not installed. [Christophe Vandeplas] +- Python <3.4 should work again.... [Raphaël Vinot] + + Fix #482 +- Remote_describe_types response was invalid. [Raphaël Vinot] +- Missing file in last commit. [Raphaël Vinot] +- Remove overwrite of remote_describe_types. [Raphaël Vinot] + +Other +~~~~~ +- Added example for checking sync servers. [wotschel] +- Corrected docstring. [Shortfinga] +- Include to_ids and replace newlines in title. [Koen Van Impe] +- Update aping.py. [ater49] + + Just fixing a typo +- Remove unused MISPFileCache from PyMISP class. [Marc Hoersken] + + +v2.4.117 (2019-10-10) +--------------------- + +New +~~~ +- Better handling of delete(d) attributes. [Raphaël Vinot] + + * Hard delete on attribute + * Get the deleted attributes within an event + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Test if json exists in cached method. [Raphaël Vinot] +- Decode datetime without dateutils if possible. [Raphaël Vinot] +- Add support for rapidjson, refactoring and code cleanup. [Raphaël + Vinot] +- Cleanups. [Raphaël Vinot] +- Cleanups and improvements. [Raphaël Vinot] +- [types] updated to the latest version. [Christophe Vandeplas] + + now using the gen_misp_types_categories using jq +- [describeTypes] updated to the latest version. [Alexandre Dulaunoy] +- Bump dependencies. [Raphaël Vinot] +- Add missing return formats in restsearch, bump objects. [Raphaël + Vinot] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- Update search examples. [Raphaël Vinot] +- Update main notebook. [Raphaël Vinot] +- [test] remove attribute field which was not foreseen in 2.4 branch. + [Alexandre Dulaunoy] +- Fix travis tests due to sighting_timestamp. [Raphaël Vinot] +- Use default for warnings. [Raphaël Vinot] + + fix: #453 +- Dump dependencies, update tests. [Raphaël Vinot] +- Bump readme. [Raphaël Vinot] +- Update upload malware/attachment example script. [Raphaël Vinot] + + Fix #447 + + Make data at attibute level more generic with getter/setter methods + +Fix +~~~ +- [Python2] Use LRU cache decorator, fix call to describe_types in + PyMISP. [Raphaël Vinot] +- Python2 SyntaxError... [Raphaël Vinot] +- Objects helpers were broken, do not overwrite describe_types. [Raphaël + Vinot] +- Support for legacy python versions. [Raphaël Vinot] + + 90 days and counting, folks. +- Cache object templates at AbstractMISP level. [Raphaël Vinot] + + Related #468 and #471 +- Cache describeTypes at AbstractMISP level. [Raphaël Vinot] +- Big speed improvment when loading MISPEvent. [Raphaël Vinot] + + 1. `properties` is a list comprehension + 2. Massively reduce the amount of calls to `properties` +- Python 2.7 support. [Raphaël Vinot] + + I want a cookie. + +Other +~~~~~ +- Use classmethod instead of staticmethod and avoid hard-coded + reference. [Marc Hoersken] +- Cache JSON definitions in memory LFU cache provided by cachetools. + [Marc Hoersken] + + - Path and modified time of JSON file are used as the cache key + - Global state is hidden away inside a root-class for re-use + - Maximum size is 150 considering the number of JSON definitions + + During my tests the memory usage of the test suites was halved. +- Fix mixed whitespace in the travis helper script files. [Marc + Hoersken] +- Remove explicit clonce as the viper-test-files are now a Git + submodule. [Marc Hoersken] +- Add viper-test-files repository as Git submodule. [Marc Hoersken] +- Update .gitignore to exclude files produced during tests. [Marc + Hoersken] +- Code cleanup. [Koen Van Impe] +- Update type and code cleanup. [Koen Van Impe] +- List all the sightings - show_sightings.py. [Koen Van Impe] +- Disable to_ids based on false positive sightings reporting. [Koen Van + Impe] +- Adds support to add local tags. [Antoine Cailliau] + + Requires https://github.com/MISP/MISP/pull/5215 to be merged first. +- Minor grammar errors. [Miroslav Stampar] +- Make client_certs out of the box friendly. [Campbell McKenzie] + + +v2.4.114 (2019-08-30) +--------------------- + +New +~~~ +- [Community] Request access. [Raphaël Vinot] +- Initial support for communities. [Raphaël Vinot] +- Contact event reporter. [Raphaël Vinot] +- Delegate Event. [Raphaël Vinot] + + And more test cases + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump Changelog. [Raphaël Vinot] +- Temp disable tests for request_community_access. [Raphaël Vinot] +- Disable test for now. [Raphaël Vinot] +- Bump Changelog. [Raphaël Vinot] +- Bump Dependencies. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Re-enable a few test cases. [Raphaël Vinot] +- Make sure delegation is enabled while testing. [Raphaël Vinot] +- [tests] Check the type of the response. [Raphaël Vinot] +- New local key in Org/Orgc. [Raphaël Vinot] +- [tests] Do not run in fast mode by default. [Raphaël Vinot] +- Better handling of sightings. [Raphaël Vinot] +- [Travis] Add more debug. [Raphaël Vinot] +- Add test related to travis. [Raphaël Vinot] + +Fix +~~~ +- Event delegation was incorrect. [Raphaël Vinot] +- Automatically skip empty string in add_attribute at object level. + [Raphaël Vinot] + + Fix #439 + + Re-enable test cases. +- [Travis] User cannot create tag, Travis was right. [Raphaël Vinot] +- Invalid tests in last commit. [Raphaël Vinot] +- [Travis] Slight changes to help debug on Travis. [Raphaël Vinot] + +Other +~~~~~ +- Bump Changelog. [Raphaël Vinot] + + +v2.4.113 (2019-08-16) +--------------------- + +New +~~~ +- Helpers & testcases for syncing. [Raphaël Vinot] +- Preliminaty setup for testing syncing. [Raphaël Vinot] +- Add few tests for admin tasks. [Raphaël Vinot] +- Update MISP, test sync server. [Raphaël Vinot] +- Properly support attribute/add of multiple attributes (2.4.113+) + [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Improve test cases. [Raphaël Vinot] +- Update and improve live testing. [Raphaël Vinot] +- Add tests cases for sync, bump describeTypes. [Raphaël Vinot] +- Return empty list instead of None. [Pierre-Jean Grenier] + + In all cases but one, the 3rd returned object is a (potentially empty) list. +- Some more code cleanup. [Raphaël Vinot] +- Code cleanup. [Raphaël Vinot] +- Enable more tests. [Raphaël Vinot] +- #4891 was fixed. [Raphaël Vinot] +- Bump describeTypes. [Raphaël Vinot] + +Fix +~~~ +- Fallback to propose attribute update. [Raphaël Vinot] +- Properly __repr__ MISPUser. [Raphaël Vinot] +- Move __not_jsonable *inside* the __init__ [Raphaël Vinot] + + Turns out, if you modify a variable defined outside the __init__, + every instances (and inherited classes) of that class will be impacted by it. +- Exception when posting multiple attributes on attributes/add. [Raphaël + Vinot] + + Fix #433 + + Few cleanups in code. + +Other +~~~~~ +- Include date_from & date_to in subject and report content. [Koen Van + Impe] +- Allow statistics date_from date_to. [Koen Van Impe] + + - date_from + date_to + - move misp object creation after argument parser +- Allow to supply mail options as arguments on command line. [Koen Van + Impe] +- Fix stats_report example to use ExpandedPyMISP. [Maxime Thiebaut] + + The stats_report example relied on deprecated functions making it crash. + This has been fixed by upgrading to ExpandedPyMISP. Further checks have + been introduced to ensure used dictionnary keys do exist as the example + also crashed on clean MISP instances due to empty responses. + + +v2.4.112 (2019-08-02) +--------------------- + +New +~~~ +- [Search] Add a few new options in rest search. [Raphaël Vinot] +- Allow to change the template on an object on-the-fly. [Raphaël Vinot] +- [example] Script to load datasets from Scripps CO2. [Raphaël Vinot] +- Get_objects_by_name in MISPEvent. [Raphaël Vinot] + + new: Convert datetime objects to python datetime. + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump Changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- [tests] Few improvements. [Raphaël Vinot] +- [tests] Add new test cases. [Raphaël Vinot] +- Rename relationship included-in -> includes. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- [deps] Bump. [Raphaël Vinot] +- [examples] pythonify properly when needed. [Raphaël Vinot] +- [tests] Toggle pythonify in create_massive_dummy_events. [Raphaël + Vinot] + +Fix +~~~ +- Inconsistency in MISPEvent, reenable tests. [Raphaël Vinot] +- Some test cases need more love. [Raphaël Vinot] +- PyTaxonomies is not compatible with python<3.6. [Raphaël Vinot] +- Rename filename. [Raphaël Vinot] +- [deprecation] Wrong deprecation message. [Raphaël Vinot] + + Also, deprecated method was broken. + + Fix #424 + +Other +~~~~~ +- Add: New attribute type weakness. [chrisr3d] +- Fix missing f in f-string. [Paal Braathen] +- Wrong variable. [Georges Toth] +- Remove unused line. [kovacsbalu] +- Fix tag help text Minor pycodestyle. [kovacsbalu] + + +v2.4.111.2 (2019-07-22) +----------------------- + +New +~~~ +- [Sightings] Delete method. [Raphaël Vinot] + + Fix #230 +- [tests] non-exportable tags. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump verison. [Raphaël Vinot] +- Make pythonify=False default everywhere. [Raphaël Vinot] + + Add a method to toggle pythonify globally +- [tests] Update stats. [Raphaël Vinot] +- [tests] Remove travis exceptions. [Raphaël Vinot] + +Fix +~~~ +- [tests] Path to test file. [Raphaël Vinot] + + Fix #423 +- [objects] Allow the value of an attribute to be 0. [Raphaël Vinot] +- [tests] Disable one of the test cases for now. [Raphaël Vinot] +- [tests] By default, the workflow taxonomy isn't enabled. [Raphaël + Vinot] +- Properly handle fallbacks add/update/delete attributes. [Raphaël + Vinot] +- [add_attribute] Only create a proposal when needed. [Raphaël Vinot] + +Other +~~~~~ +- Fix for issue 420. [github-pba] + + +v2.4.111.1 (2019-07-18) +----------------------- + +New +~~~ +- Add option to locally expand malware samples with LIEF. [Raphaël + Vinot] + +Changes +~~~~~~~ +- Bump Changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Remove legacy tests. [Raphaël Vinot] +- Improve deprecation message on PyMISP. [Raphaël Vinot] +- [describeTypes] updated to add community-id. [Alexandre Dulaunoy] +- Bump examples to python3. [Raphaël Vinot] +- Reorganise ExpandedPyMISP methods, normalise the parameters. [Raphaël + Vinot] +- Deprecate everything in PyMISP. [Raphaël Vinot] + +Fix +~~~ +- Python < 3.6 support. [Raphaël Vinot] + +Other +~~~~~ +- Create statistical reports for MISP. [Koen Van Impe] + + PyMISP script to run every x-days to get an overview of new + events/attributes ; MISP-Galaxies ; MITRE ; Tags + + Output of report is on screen or sent via e-mail ; all stats attached + as CSV + + +v2.4.111 (2019-07-12) +--------------------- + +New +~~~ +- Introduce ability to create a sharing group. [Tom King] +- Allow to pass delimiter & quotechar to the CSV loader. [Raphaël Vinot] +- [example] Added edit_organisation examples. [Steve Clement] +- Method to POST a STIX file to MISP and create a new event. [Raphaël + Vinot] +- Object generator for ssh authorized_keys files. [Raphaël Vinot] +- Allow custom user-agent. [Christophe Vandeplas] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump version. [Raphaël Vinot] +- Bumb misp-objects. [Raphaël Vinot] +- [tests] WTF upload_sample on travis. [Raphaël Vinot] +- [tests] Add custom error message on upload_sample - fix last commit. + [Raphaël Vinot] +- [tests] Add custom error message on upload_sample. [Raphaël Vinot] +- Remove roaming as it can't be set in this request. [Tom King] +- Allow for deletion of security group. [Tom King] +- Bump dependencies. [Raphaël Vinot] +- [last] You can now paginate over multiple results in the last example + command. [Alexandre Dulaunoy] + + You can do stuff like this: + + python3 last.py -l 48h -m 10 -p 2 | jq .[].Event.info + + which means the last 10 events on second page which are between a + time range of 0 and 48 hours. +- [tests] now deleted flag is returning only the deleted values (to be + consistent) [Alexandre Dulaunoy] +- [misp-objects] updated to the latest version. [Alexandre Dulaunoy] +- Bump deps (lief 0.10 dev) [Raphaël Vinot] +- Use pydeep from pypi, add test. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Bump Pipfile for python 3.7. [Raphaël Vinot] + +Fix +~~~ +- Skip attribute in object when value is empty, skip empty objects. + [Raphaël Vinot] +- [perms] Added try/except for various permission conditions, also + create the output dir if not exist fix: [try/except] Catch Ctrl-c + keyboard interrupt fix: [style] isort imports. [Steve Clement] +- [direct_call] Allows the response type to be something else than a + JSON (e.g. csv). [mokaddem] +- [feed generator] Added missing fields. [iglocska] +- Properly fix deprecation warning. [Raphaël Vinot] + + fix #390 +- Travis & python2. [Raphaël Vinot] +- Last commit foobar. [Raphaël Vinot] +- Install lief on python < 3.7 with pipenv. [Raphaël Vinot] + +Other +~~~~~ +- [openioc] changed default mapping for + RouteEntryItem/Destination/string. [0x3c7] +- [openioc] Changed mapping for RouteEntryItem/Destination/string to + domain instead of url because UrlHistoryItem/URL is mostly used for + urls. [0x3c7] +- Fixes other mapping to other types. [0x3c7] +- [openioc] Allow the use of types in openioc content tags. [0x3c7] +- Sync sightings between MISP servers. [Koen Van Impe] + + Sync sightings between MISP servers + Sync from multiple clients to one authoritative MISP instance. + To be run from cron + (blog docu coming) +- Added includeWarninglistHits as a possible filter for the event level + restsearch. [Jeroen Pinoy] +- Resolve issue with change_sharing_group which do not update event + successfully. [hrifflet] +- Use misp_verifycert flag. [Koen Van Impe] +- Take 'to_ids' setting in account and PEP8 checks. [Koen Van Impe] + + - Include check if 'to_ids' is included in the data returned from the + import module + - PEP8 checks +- Automation script that links vmray_submit and vmray_import. [Koen Van + Impe] + + Import finished VMRay tasks ; add attributes to event + Makes use of the 'incomplete' workflow taxonomy + Needs to be put in a cronjob to run in the background +- Update PyMISP_tutorial.ipynb. [Carlos Borges] + + The function to collect event_id and put it into a list isn't looking into each MISPAttribute. + Just updated the script to look it. + + +v2.4.106 (2019-04-24) +--------------------- + +New +~~~ +- Test cases for attributes and proposals. [Raphaël Vinot] +- Improve python3.6+ lib. [Raphaël Vinot] +- Add_attributes method in MISPObject (for multiple attributes) [Raphaël + Vinot] +- Method to set the default role. [Raphaël Vinot] +- Default to "me" in the get_user method, update ExpandedPyMISP. + [Raphaël Vinot] + + Fix #377 +- Add get_object to ExpandedPyMISP. [Raphaël Vinot] + + Fix #372 +- Test cases for CSV loader, add cleaner methods in ExpandedPyMISP. + [Raphaël Vinot] +- Add CSV loader. [Raphaël Vinot] + + Fix #376 +- Helper to create MISP Objects for regcheck.org.uk. [Raphaël Vinot] +- Test for ACLs in testlive. [Raphaël Vinot] +- Test for manual calls to add_object and add_object_reference. [Raphaël + Vinot] +- Test update object in event. [Raphaël Vinot] + +Changes +~~~~~~~ +- Bump changelog. [Raphaël Vinot] +- Bump Objects. [Raphaël Vinot] +- Bump version, Bump changelog. [Raphaël Vinot] +- Add python 3.7 support for pipenv users. [Raphaël Vinot] +- Allow to pass a eml as string to EmailObject. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] + + Fix CVE-2019-11324 (urllib3) +- Bump dependencies. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Allow to pass an AbstractMISP to add_reference. [Raphaël Vinot] + + Fix #379 +- Rework notebooks. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Display an error on failure in testlive. [Raphaël Vinot] +- Add tests for disable_tag. [Raphaël Vinot] +- Bump misp-objects. [Raphaël Vinot] +- Reorganize some tests. [Raphaël Vinot] +- Orders of tests in make_bool. [Raphaël Vinot] +- Bump dependencies. [Raphaël Vinot] +- Initial set of refactoring on PDF generator. [Raphaël Vinot] +- Add i8n for pdfexport, without all the fonts in the main repo. + [Raphaël Vinot] + +Fix +~~~ +- Bump Test files because of new template version. [Raphaël Vinot] +- Build on readthedocs. [Raphaël Vinot] +- [typo] Fixed a small typo I noticed in the docs. [Steve Clement] +- Add missing files for testing (CSV loader) [Raphaël Vinot] +- Properly test query ACLs. [Raphaël Vinot] +- Update all json submodules at one place in testlive. [Raphaël Vinot] +- Disable some tests for the run on travis. [Raphaël Vinot] +- [exportpdf] Doc update. [Falconieri] +- [exportpdf] Coding Style. [Falconieri] +- Improper handling of to_ids passed as integer in MISPEvent. [Raphaël + Vinot] + + Fix #364 +- Do not fail when importing the reportlab file. [Raphaël Vinot] +- PDF Export requires python 3.6+. [Raphaël Vinot] +- Do not run PDF Export tests on python < 3.6. [Raphaël Vinot] +- [exportpdf] Custom path for fonts and font package. [Falconieri] +- Allow to use global variables HTTP_PROXY and HTTPS_PROXY again. + [Raphaël Vinot] + + Fix #365 +- Slight changes in new .change_disable_correlation method. [Raphaël + Vinot] +- Get_object_template_id was broken. Add test case. [Raphaël Vinot] + + Fix #361 + +Other +~~~~~ +- New Add test for ASNObject. [Raphaël Vinot] +- Update README.md. [Steve Clement] + + Added number of monthly PyPi downloads +- Add: [exportpdf] documentation added about exportPDF. [Falconieri] +- Fix for "'NoneType' object has no attribute 'setdefault'" [Jacco + Ligthart] +- Fix a type on function name. [l3m0ntr33] +- Add new function + PyMISP.change_disablecorrelation(attribute_uuid,disable_correlation) + to be able to enable/disable correlation on attributes. [hrifflet] v2.4.103 (2019-03-01) @@ -21,6 +3660,7 @@ New Changes ~~~~~~~ +- Build all formats for the documentation. [Raphaël Vinot] - Bump version. [Raphaël Vinot] - [jupyter] remove all the response key (as response is removing it) [Alexandre Dulaunoy] @@ -86,6 +3726,7 @@ Fix Other ~~~~~ +- Re-bump changelog. [Raphaël Vinot] - - Set my misp-objects… [Steve Clement] - Add : [exportpdf] Objects handling, tests cases, test files. [Falconieri] @@ -2075,5 +5716,3 @@ v1.1.2 (2015-08-05) - Json export is not supported everywhere. [Raphaël Vinot] - Some testing. [Raphaël Vinot] - Initial commit. [Raphaël Vinot] - - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 8c33af0..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -graft docs -graft examples -graft tests -include CHANGELOG.txt -include LICENSE -include pymisp/data/*.json -include pymisp/data/misp-objects/*.json -include pymisp/data/misp-objects/objects/*/definition.json -include pymisp/data/misp-objects/relationships/definition.json -include README.md diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 1a384db..0000000 --- a/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -nose = "*" -coveralls = "*" -codecov = "*" -requests-mock = "*" - -[packages] -pymisp = {editable = true,extras = ["fileobjects", "neo", "openioc", "virustotal", "pdfexport"],path = "."} -pydeep = {editable = true,git = "https://github.com/kbandla/pydeep.git"} -pymispwarninglists = {editable = true,git = "https://github.com/MISP/PyMISPWarningLists.git"} - -[requires] -python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index bea23b3..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,489 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "c95b6920af9d48d6e38e0456394f752479064c9f3091cf3e6b93e751de21cfad" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.6" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "asn1crypto": { - "hashes": [ - "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", - "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" - ], - "version": "==0.24.0" - }, - "attrs": { - "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" - ], - "version": "==19.1.0" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858", - "sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348", - "sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718" - ], - "version": "==4.7.1" - }, - "certifi": { - "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" - ], - "version": "==2019.3.9" - }, - "cffi": { - "hashes": [ - "sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f", - "sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11", - "sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d", - "sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891", - "sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf", - "sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c", - "sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed", - "sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b", - "sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a", - "sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585", - "sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea", - "sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f", - "sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33", - "sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145", - "sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a", - "sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3", - "sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f", - "sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd", - "sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804", - "sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d", - "sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92", - "sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f", - "sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84", - "sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb", - "sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7", - "sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7", - "sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35", - "sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889" - ], - "version": "==1.12.2" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "version": "==7.0" - }, - "colorama": { - "hashes": [ - "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", - "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" - ], - "version": "==0.4.1" - }, - "cryptography": { - "hashes": [ - "sha256:066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1", - "sha256:210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705", - "sha256:26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6", - "sha256:2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1", - "sha256:2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8", - "sha256:409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151", - "sha256:45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d", - "sha256:48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659", - "sha256:6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537", - "sha256:6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e", - "sha256:8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb", - "sha256:9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c", - "sha256:9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9", - "sha256:9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5", - "sha256:acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad", - "sha256:c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a", - "sha256:d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460", - "sha256:d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd", - "sha256:e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6" - ], - "version": "==2.6.1" - }, - "decorator": { - "hashes": [ - "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", - "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" - ], - "version": "==4.4.0" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "ipaddress": { - "hashes": [ - "sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", - "sha256:b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c" - ], - "version": "==1.0.22" - }, - "jsonschema": { - "hashes": [ - "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", - "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" - ], - "version": "==3.0.1" - }, - "lief": { - "hashes": [ - "sha256:c95974006a6b8a767eea8b35e6c63e2b20939730063ac472894b53ab9855a0b5" - ], - "version": "==0.9.0" - }, - "neobolt": { - "hashes": [ - "sha256:3324f2b319e84acb82e37a81ef75f3f7ce71c149387daf900589377db48bed2a" - ], - "version": "==1.7.4" - }, - "neotime": { - "hashes": [ - "sha256:4e0477ba0f24e004de2fa79a3236de2bd941f20de0b5db8d976c52a86d7363eb" - ], - "version": "==1.7.4" - }, - "pillow": { - "hashes": [ - "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", - "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", - "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", - "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", - "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", - "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", - "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", - "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", - "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", - "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", - "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", - "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", - "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", - "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", - "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", - "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", - "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", - "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", - "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", - "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", - "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", - "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", - "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", - "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", - "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", - "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", - "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", - "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", - "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", - "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e" - ], - "version": "==5.4.1" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", - "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", - "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" - ], - "version": "==2.0.9" - }, - "py2neo": { - "hashes": [ - "sha256:c25d24a1504bbfaf61e862e29953f17ad67a4810d55531b1436ad0c7664d85fd" - ], - "version": "==4.2.0" - }, - "pycparser": { - "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" - ], - "version": "==2.19" - }, - "pydeep": { - "editable": true, - "git": "https://github.com/kbandla/pydeep.git", - "ref": "bc0d33bff4b45718b4c5f2c79d4715d92a427eda" - }, - "pygments": { - "hashes": [ - "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", - "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" - ], - "version": "==2.3.1" - }, - "pymisp": { - "editable": true, - "extras": [ - "fileobjects", - "neo", - "openioc", - "virustotal", - "pdfexport" - ], - "path": "." - }, - "pymispwarninglists": { - "editable": true, - "git": "https://github.com/MISP/PyMISPWarningLists.git", - "ref": "d512ca91ae0635407754933099d6f3dd654dbcfe" - }, - "pyopenssl": { - "hashes": [ - "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", - "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" - ], - "version": "==19.0.0" - }, - "pyrsistent": { - "hashes": [ - "sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2" - ], - "version": "==0.14.11" - }, - "python-dateutil": { - "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" - ], - "version": "==2.8.0" - }, - "python-magic": { - "hashes": [ - "sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375", - "sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5" - ], - "version": "==0.4.15" - }, - "pytz": { - "hashes": [ - "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", - "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" - ], - "version": "==2018.9" - }, - "reportlab": { - "hashes": [ - "sha256:069f684cd0aaa518a27dc9124aed29cee8998e21ddf19604e53214ec8462bdd7", - "sha256:09b68ec01d86b4b120456b3f3202570ec96f57624e3a4fc36f3829323391daa4", - "sha256:0c32be9a406172c29ea20ff55a709ccac1e7fb09f15aba67cb7b455fd1d3dbe0", - "sha256:233196cf25e97cfe7c452524ea29d9a4909f1cb66599299233be1efaaaa7a7a3", - "sha256:2b5e4533f3e5b962835a5ce44467e66d1ecc822761d1b508077b5087a06be338", - "sha256:2e860bcdace5a558356802a92ae8658d7e5fdaa00ded82e83a3f2987c562cb66", - "sha256:3546029e63a9a9dc24ee38959eb417678c2425b96cd27b31e09e216dafc94666", - "sha256:4452b93f9c73b6b70311e7d69082d64da81b38e91bfb4766397630092e6da6fd", - "sha256:528c74a1c6527d1859c2c7a64a94a1cba485b00175162ea23699ae58a1e94939", - "sha256:6116e750f98018febc08dfee6df20446cf954adbcfa378d2c703d56c8864aff3", - "sha256:6b2b3580c647d75ef129172cb3da648cdb24566987b0b59c5ebb80ab770748d6", - "sha256:727b5f2bed08552d143fc99649b1863c773729f580a416844f9d9967bb0a1ae8", - "sha256:74c24a3ec0a3d4f8acb13a07192f45bdb54a1cc3c2286241677e7e8bcd5011fa", - "sha256:98ccd2f8b4f8636db05f3f14db0b471ad6bb4b66ae0dc9052c4822b3bd5d6a7d", - "sha256:a5905aa567946bc938b489a7249c7890c3fd3c9b7b5680dece5bc551c2ddbe0d", - "sha256:acbb7f676b8586b770719e9683eda951fdb38eb7970d46fcbf3cdda88d912a64", - "sha256:b5e30f865add48cf880f1c363eb505b97f2f7baaa88c155f87a335a76515a3e5", - "sha256:be2a7c33a2c28bbd3f453ffe4f0e5200b88c803a097f4cf52d69c6b53fad7a8f", - "sha256:c356bb600f59ac64955813d6497a08bfd5d0c451cb5829b61e3913d0ac084e26", - "sha256:c7ec4ae2393beab584921b1287a04e94fd98c28315e348362d89b85f4b464546", - "sha256:d476edc831bb3e9ebd04d1403abaf3ea57b3e4c2276c91a54fdfb6efbd3f9d97", - "sha256:db059e1a0691c872784062421ec51848539eb4f5210142682e61059a5ca7cc55", - "sha256:dd423a6753509ab14a0ac1b5be39d219c8f8d3781cce3deb4f45eda31969b5e8", - "sha256:ed9b7c0d71ce6fe2b31c6cde530ad8238632b876a5d599218739bda142a77f7c", - "sha256:f0a2465af4006f97b05e1f1546d67d3a3213d414894bf28be7f87f550a7f4a55", - "sha256:f20bfe26e57e8e1f575a9e0325be04dd3562db9f247ffdd73b5d4df6dec53bc2", - "sha256:f3463f2cb40a1b515ac0133ba859eca58f53b56760da9abb27ed684c565f853c", - "sha256:facc3c9748ab1525fb8401a1223bce4f24f0d6aa1a9db86c55db75777ccf40f9" - ], - "version": "==3.5.13" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "version": "==2.21.0" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "soupsieve": { - "hashes": [ - "sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b", - "sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26" - ], - "version": "==1.8" - }, - "urllib3": { - "extras": [ - "secure" - ], - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - }, - "validators": { - "hashes": [ - "sha256:68e4b74889aac1270d83636cb1dbcce3d2271e291ab14023cf95e7dbfbbce09d" - ], - "version": "==0.12.4" - }, - "wcwidth": { - "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" - ], - "version": "==0.1.7" - } - }, - "develop": { - "certifi": { - "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" - ], - "version": "==2019.3.9" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "codecov": { - "hashes": [ - "sha256:8ed8b7c6791010d359baed66f84f061bba5bd41174bf324c31311e8737602788", - "sha256:ae00d68e18d8a20e9c3288ba3875ae03db3a8e892115bf9b83ef20507732bed4" - ], - "index": "pypi", - "version": "==2.0.15" - }, - "coverage": { - "hashes": [ - "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", - "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", - "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", - "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", - "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", - "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", - "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", - "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", - "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", - "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", - "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", - "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", - "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", - "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", - "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", - "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", - "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", - "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", - "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", - "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", - "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", - "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", - "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", - "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", - "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", - "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", - "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", - "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", - "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", - "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", - "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" - ], - "version": "==4.5.3" - }, - "coveralls": { - "hashes": [ - "sha256:baa26648430d5c2225ab12d7e2067f75597a4b967034bba7e3d5ab7501d207a1", - "sha256:ff9b7823b15070f26f654837bb02a201d006baaf2083e0514ffd3b34a3ffed81" - ], - "index": "pypi", - "version": "==1.7.0" - }, - "docopt": { - "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" - ], - "version": "==0.6.2" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "nose": { - "hashes": [ - "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", - "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", - "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" - ], - "index": "pypi", - "version": "==1.3.7" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "version": "==2.21.0" - }, - "requests-mock": { - "hashes": [ - "sha256:7a5fa99db5e3a2a961b6f20ed40ee6baeff73503cf0a553cc4d679409e6170fb", - "sha256:8ca0628dc66d3f212878932fd741b02aa197ad53fd2228164800a169a4a826af" - ], - "index": "pypi", - "version": "==1.5.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "urllib3": { - "extras": [ - "secure" - ], - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - } -} diff --git a/README.md b/README.md index 12e6a1e..89da00f 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,58 @@ -README -====== - -[![Documentation Status](https://readthedocs.org/projects/pymisp/badge/?version=latest)](http://pymisp.readthedocs.io/?badge=latest) -[![Build Status](https://travis-ci.org/MISP/PyMISP.svg?branch=master)](https://travis-ci.org/MISP/PyMISP) -[![Coverage Status](https://coveralls.io/repos/github/MISP/PyMISP/badge.svg?branch=master)](https://coveralls.io/github/MISP/PyMISP?branch=master) -[![Python 3.6](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/release/python-360/) -[![PyPi version](https://img.shields.io/pypi/v/pymisp.svg)](https://pypi.python.org/pypi/pymisp/) -[![Number of PyPI downloads](https://pypip.in/d/pymisp/badge.png)](https://pypi.python.org/pypi/pymisp/) +**IMPORTANT NOTE**: This library will require **at least** Python 3.10 starting the 1st of January 2024. If you have legacy versions of python, please use the latest PyMISP version that will be released in December 2023, and consider updating your system(s). Anything released within the last 2 years will do, starting with Ubuntu 22.04. # PyMISP - Python Library to access MISP +[![Documentation Status](https://readthedocs.org/projects/pymisp/badge/?version=latest)](http://pymisp.readthedocs.io/?badge=latest) +[![Coverage Status](https://coveralls.io/repos/github/MISP/PyMISP/badge.svg?branch=main)](https://coveralls.io/github/MISP/PyMISP?branch=main) +[![Python 3.8](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/) +[![PyPi version](https://img.shields.io/pypi/v/pymisp.svg)](https://pypi.python.org/pypi/pymisp/) +[![Number of PyPI downloads](https://img.shields.io/pypi/dm/pymisp.svg)](https://pypi.python.org/pypi/pymisp/) + PyMISP is a Python library to access [MISP](https://github.com/MISP/MISP) platforms via their REST API. PyMISP allows you to fetch events, add or update events/attributes, add or update samples or search for attributes. -## Requirements - - * [requests](http://docs.python-requests.org) - ## Install from pip +**It is strongly recommended to use a virtual environment** + +If you want to know more about virtual environments, [python has you covered](https://docs.python.org/3/tutorial/venv.html) + +Only basic dependencies: ``` pip3 install pymisp ``` -## Install the latest version from repo +And there are a few optional dependencies: +* fileobjects: to create PE/ELF/Mach-o objects +* openioc: to import files in OpenIOC format (not really maintained) +* virustotal: to query VirusTotal and generate the appropriate objects +* docs: to generate te documentation +* pdfexport: to generate PDF reports out of MISP events +* url: to generate URL objects out of URLs with Pyfaup +* email: to generate MISP Email objects +* brotli: to use the brotli compression when interacting with a MISP instance + +Example: + +``` +pip3 install pymisp[virustotal,email] +``` + +## Install the latest version from repo from development purposes + +**Note**: poetry is required; e.g., "pip3 install poetry" ``` git clone https://github.com/MISP/PyMISP.git && cd PyMISP git submodule update --init -pip3 install -I .[fileobjects,neo,openioc,virustotal] +poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport -E email ``` -## Installing it with virtualenv - -It is recommended to use virtualenv to not polute your OS python envirenment. -``` -pip3 install virtualenv -git clone https://github.com/MISP/PyMISP.git && cd PyMISP -python3 -m venv ./ -source venv/bin/activate -git submodule update --init -pip3 install -I .[fileobjects,neo,openioc,virustotal] -``` - -## Running the tests +### Running the tests ```bash -pip3 install -U nose pip setuptools coveralls codecov requests-mock -pip3 install git+https://github.com/kbandla/pydeep.git - -git clone https://github.com/viper-framework/viper-test-files.git tests/viper-test-files -nosetests-3.4 --with-coverage --cover-package=pymisp,tests --cover-tests tests/test_*.py +poetry run pytest --cov=pymisp tests/test_*.py ``` If you have a MISP instance to test against, you can also run the live ones: @@ -59,7 +60,7 @@ If you have a MISP instance to test against, you can also run the live ones: **Note**: You need to update the key in `tests/testlive_comprehensive.py` to the automation key of your admin account. ```bash -nosetests-3.4 --with-coverage --cover-package=pymisp,tests --cover-tests tests/testlive_comprehensive.py +poetry run pytest --cov=pymisp tests/testlive_comprehensive.py ``` ## Samples and how to use PyMISP @@ -89,7 +90,7 @@ python3 last.py -l 45m # 45 minutes ## Debugging -You have two options there: +You have two options here: 1. Pass `debug=True` to `PyMISP` and it will enable logging.DEBUG to stderr on the whole module @@ -100,7 +101,7 @@ You have two options there: import logging logger = logging.getLogger('pymisp') -# Configure it as you whish, for example, enable DEBUG mode: +# Configure it as you wish, for example, enable DEBUG mode: logger.setLevel(logging.DEBUG) ``` @@ -114,31 +115,37 @@ logger = logging.getLogger('pymisp') logging.basicConfig(level=logging.DEBUG, filename="debug.log", filemode='w', format=pymisp.FORMAT) ``` +## Test cases + +1. The content of `mispevent.py` is tested on every commit +2. The test cases that require a running MISP instance can be run the following way: + + +```bash +# From poetry + +pytest --cov=pymisp tests/test_*.py tests/testlive_comprehensive.py:TestComprehensive.[test_name] +``` + ## Documentation -[PyMISP API documentation is available](https://media.readthedocs.org/pdf/pymisp/latest/pymisp.pdf). - -Documentation can be generated with epydoc: - -``` -epydoc --url https://github.com/MISP/PyMISP --graph all --name PyMISP --pdf pymisp -o doc -``` +The documentation is available [here](https://pymisp.readthedocs.io/en/latest/). ### Jupyter notebook -A series of [Jupyter notebooks for PyMISP tutorial](https://github.com/MISP/PyMISP/tree/master/docs/tutorial) are available in the repository. +A series of [Jupyter notebooks for PyMISP tutorial](https://github.com/MISP/PyMISP/tree/main/docs/tutorial) are available in the repository. ## Everything is a Mutable Mapping ... or at least everything that can be imported/exported from/to a json blob -`AbstractMISP` is the master class, and inherit `collections.MutableMapping` which means +`AbstractMISP` is the master class, and inherits from `collections.MutableMapping` which means the class can be represented as a python dictionary. The abstraction assumes every property that should not be seen in the dictionary is prepended with a `_`, or its name is added to the private list `__not_jsonable` (accessible through `update_not_jsonable` and `set_not_jsonable`. -This master class has helpers that will make it easy to load, and export, to, and from, a json string. +This master class has helpers that make it easy to load, and export to, and from, a json string. `MISPEvent`, `MISPAttribute`, `MISPObjectReference`, `MISPObjectAttribute`, and `MISPObject` are subclasses of AbstractMISP, which mean that they can be handled as python dictionaries. @@ -147,6 +154,40 @@ are subclasses of AbstractMISP, which mean that they can be handled as python di Creating a new MISP object generator should be done using a pre-defined template and inherit `AbstractMISPObjectGenerator`. -Your new MISPObject generator need to generate attributes, and add them as class properties using `add_attribute`. +Your new MISPObject generator must generate attributes and add them as class properties using `add_attribute`. When the object is sent to MISP, all the class properties will be exported to the JSON export. + +## Installing PyMISP on a machine with no internet access + +This is done using poetry and you need to have this repository cloned on your machine. +The commands below have to be run from inside the cloned directory. + + +1. From a machine with access to the internet, get the dependencies: + +```bash +mkdir offline +poetry export --all-extras > offline/requirements.txt +poetry run pip download -r offline/requirements.txt -d offline/packages/ +``` + +2. Prepare the PyMISP Package + +```bash +poetry build +mv dist/*.whl offline/packages/ +``` + +3. Copy the content of `offline/packages/` to the machine with no internet access. + +4. Install the packages: + +```bash +python -m pip install --no-index --no-deps packages/*.whl +``` + +# License + +PyMISP is distributed under an [open source license](./LICENSE). A simplified 2-BSD license. + diff --git a/docs/source/conf.py b/docs/source/conf.py index 10a7cea..27823aa 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # PyMISP documentation build configuration file, created by # sphinx-quickstart on Fri Aug 26 11:39:17 2016. @@ -39,6 +38,8 @@ extensions = [ 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', + 'sphinx.ext.imgconverter', + 'recommonmark', ] napoleon_google_docstring = False @@ -75,9 +76,9 @@ author = 'Raphaël Vinot' # built documents. # # The short X.Y version. -version = 'master' +version = 'main' # The full version, including alpha/beta/rc tags. -release = 'master' +release = 'main' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -131,6 +132,9 @@ pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True +# lief is a bit difficult to install +autodoc_mock_imports = ["lief"] + # -- Options for HTML output ---------------------------------------------- @@ -439,7 +443,3 @@ epub_exclude_files = ['search.html'] # If false, no index is generated. # # epub_use_index = True - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/source/index.rst b/docs/source/index.rst index f519501..886b516 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,7 @@ Welcome to PyMISP's documentation! Contents: .. toctree:: - :maxdepth: 4 + :maxdepth: 2 README modules diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 39843e4..655b432 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,5 +1,5 @@ -pymisp -====== +pymisp - Classes +================ .. toctree:: :maxdepth: 4 @@ -14,12 +14,6 @@ PyMISP .. autoclass:: PyMISP :members: -PyMISPExpanded (Python 3.6+ only) ---------------------------------- - -.. autoclass:: ExpandedPyMISP - :members: - MISPAbstract ------------ @@ -39,6 +33,20 @@ MISPEvent :members: :inherited-members: +MISPEventBlocklist +------------------ + +.. autoclass:: MISPEventBlocklist + :members: + :inherited-members: + +MISPEventDelegation +------------------- + +.. autoclass:: MISPEventDelegation + :members: + :inherited-members: + MISPAttribute ------------- @@ -67,6 +75,13 @@ MISPObjectReference :members: :inherited-members: +MISPObjectTemplate +------------------ + +.. autoclass:: MISPObjectTemplate + :members: + :inherited-members: + MISPTag ------- @@ -81,6 +96,12 @@ MISPUser :members: :inherited-members: +MISPUserSetting +--------------- + +.. autoclass:: MISPUserSetting + :members: + :inherited-members: MISPOrganisation ---------------- @@ -89,3 +110,87 @@ MISPOrganisation :members: :inherited-members: +MISPOrganisationBlocklist +------------------------- + +.. autoclass:: MISPOrganisationBlocklist + :members: + :inherited-members: + +MISPFeed +-------- + +.. autoclass:: MISPFeed + :members: + :inherited-members: + +MISPInbox +--------- + +.. autoclass:: MISPInbox + :members: + :inherited-members: + +MISPLog +------- + +.. autoclass:: MISPLog + :members: + :inherited-members: + +MISPNoticelist +-------------- + +.. autoclass:: MISPNoticelist + :members: + :inherited-members: + +MISPRole +-------- + +.. autoclass:: MISPRole + :members: + :inherited-members: + +MISPServer +---------- + +.. autoclass:: MISPServer + :members: + :inherited-members: + +MISPShadowAttribute +------------------- + +.. autoclass:: MISPShadowAttribute + :members: + :inherited-members: + +MISPSharingGroup +---------------- + +.. autoclass:: MISPSharingGroup + :members: + :inherited-members: + +MISPSighting +------------ + +.. autoclass:: MISPSighting + :members: + :inherited-members: + +MISPTaxonomy +------------ + +.. autoclass:: MISPTaxonomy + :members: + :inherited-members: + +MISPWarninglist +--------------- + +.. autoclass:: MISPWarninglist + :members: + :inherited-members: + diff --git a/docs/tutorial/PyMISP Objects.ipynb b/docs/tutorial/FullOverview.ipynb similarity index 55% rename from docs/tutorial/PyMISP Objects.ipynb rename to docs/tutorial/FullOverview.ipynb index 2d39774..08027ad 100644 --- a/docs/tutorial/PyMISP Objects.ipynb +++ b/docs/tutorial/FullOverview.ipynb @@ -53,41 +53,17 @@ "```" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Getting the API key (automatically generated on the trainig VM)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "api_file = Path('apikey')\n", - "if api_file.exists():\n", - " misp_url = 'http://127.0.0.1'\n", - " misp_verifycert = False\n", - " with open(api_file) as f:\n", - " misp_key = f.read().strip()\n", - " print(misp_key)" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Using the PyMISP objects\n", "\n", - "This page aims to give recommandations about how to efficiently use the `pymisp` library.\n", + "This page aims to give recommendations about how to efficiently use the `pymisp` library.\n", "\n", "It is strongly recommended (read \"don't do anything else, please\") to use the library this way and never, ever modify the python dictionary you get by loading the json blob you receive from the server.\n", "\n", - "This library is made in a way to hide as much as the complexity as possible and we're happy to improve it is there is someting missing." + "This library is made in a way to hide as much as the complexity as possible and we're happy to improve it is there is something missing." ] }, { @@ -145,7 +121,7 @@ "## Set the Event date\n", "\n", "\n", - "The date can be in many different formats. This helper makes sure it normalises it in a way that will be understood by your MISP instance." + "The date can be in many different formats. This helper makes sure it normalizes it in a way that will be understood by your MISP instance." ] }, { @@ -169,7 +145,7 @@ "event.set_date(d)\n", "print(event.date)\n", "\n", - "# datetime.datetime => MISP expects a day, so the hour will be droped.\n", + "# datetime.datetime => MISP expects a day, so the hour will be dropped.\n", "from datetime import datetime\n", "d = datetime.now()\n", "print(type(d))\n", @@ -183,7 +159,7 @@ "source": [ "## Add Attribute to event\n", "\n", - "More usefull things: adding attributes to an event.\n", + "More useful things: adding attributes to an event.\n", "\n", "Attributes have a bunch of parameters you can pass (if you feel like it). If you don't pass them, they'll be automatically set depending on their sane defaults.\n", "\n", @@ -216,7 +192,7 @@ "source": [ "## Set parameters (inline)\n", "\n", - "This is the was to pass other parameters" + "This is the way to pass other parameters" ] }, { @@ -272,13 +248,22 @@ "print(attribute_second.to_json())" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(event.to_json())" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Soft delete attribute\n", "\n", - "**Important note**: the default approach to *delete* on MISP is to do a soft delete (meaning the attribue is not displayed on the default view on MISP). The reason we do it this way is that it allows to push *delete* updates to instances we synchronize with.\n", + "**Important note**: the default approach to *delete* on MISP is to do a soft delete (meaning the attribute is not displayed on the default view on MISP). The reason we do it this way is that it allows to push *delete* updates to instances we synchronize with.\n", "\n", "The delete method will set the default parameter of the attribute to `True`." ] @@ -312,6 +297,57 @@ "print(event.published)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MISPAttribute" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attr_type = 'ip-dst'\n", + "value = '1.1.1.1'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPAttribute\n", + "\n", + "# Attribute data already defined\n", + "attribute = MISPAttribute()\n", + "attribute.type = attr_type\n", + "attribute.value = value\n", + "print(attribute)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# An attribute can also be loaded directly from a JSON\n", + "json = '''{\n", + " \"type\": \"ip-dst\",\n", + " \"value\": \"127.0.0.1\",\n", + " \"category\": \"Network activity\",\n", + " \"to_ids\": false\n", + " }'''\n", + "\n", + "attribute = MISPAttribute()\n", + "attribute.from_json(json)\n", + "print(attribute)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -354,7 +390,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## One-liner to add an object to a MISPEvent\n", + "## Short version to add an object to a MISPEvent\n", "\n", "You can also add the object directly in a misp event this way" ] @@ -374,6 +410,10 @@ "misp_object.add_attribute('ip', value='149.13.33.14')\n", "misp_object.add_attribute('first-seen', value='2018-04-11')\n", "misp_object.add_attribute('last-seen', value='2018-06-11')\n", + "\n", + "misp_object.add_attributes('ip', {'value': '10.8.8.8', 'to_ids': False}, '10.9.8.8')\n", + "\n", + "\n", "misp_object.add_reference(obj_attr.uuid, 'related-to', 'Expanded with passive DNS entry')\n", "\n", "print(event.to_json())\n" @@ -383,7 +423,41 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Helpers for MISPObjects \n", + "## New first/last seen" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPObject\n", + "\n", + "misp_object = event.add_object(name='domain-ip', comment='My Fancy new object, in one line')\n", + "\n", + "obj_attr = misp_object.add_attribute('domain', value='circl.lu')\n", + "obj_attr.add_tag('tlp:green')\n", + "misp_object.add_attribute('ip', value='149.13.33.14')\n", + "\n", + "misp_object.first_seen = '2018-04-11'\n", + "misp_object.last_seen = '2018-06-11T23:27:40.23356+07:00'\n", + "\n", + "print(misp_object.last_seen)\n", + "\n", + "misp_object.add_attributes('ip', {'value': '10.8.8.8', 'to_ids': False}, '10.9.8.8')\n", + "\n", + "\n", + "misp_object.add_reference(obj_attr.uuid, 'related-to', 'Expanded with passive DNS entry')\n", + "\n", + "print(event.to_json(indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers for MISPObjects \n", "\n", "For some objects, we have helpers in order to make your life easier. The most relevant example is the file object: when you have a file to push on MISP, there are plenty of indicators you can extract at once, and it is pretty simple to automate, so we made it a oneliner.\n", "\n", @@ -424,9 +498,76 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Generic helper\n", + "### Excel support \n", "\n", - "This helper is meant to be used when you alreadu have a script that does the mapping between your own code, and the MISPObject template." + "(okay, CSV, but that's the same thing, right?)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash \n", + "\n", + "cat ../../tests/csv_testfiles/valid_fieldnames.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash \n", + "\n", + "cat ../../tests/csv_testfiles/invalid_fieldnames.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp.tools import CSVLoader\n", + "from pymisp import MISPEvent\n", + "from pathlib import Path\n", + "\n", + "csv1 = CSVLoader(template_name='file', csv_path=Path('../../tests/csv_testfiles/valid_fieldnames.csv'))\n", + "event = MISPEvent()\n", + "event.info = 'Test event from CSV loader'\n", + "for o in csv1.load():\n", + " event.add_object(**o)\n", + "\n", + "print(event.to_json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "event = MISPEvent()\n", + "event.info = 'Test event from CSV loader'\n", + "csv2 = CSVLoader(template_name='file', csv_path=Path('../../tests/csv_testfiles/invalid_fieldnames.csv'),\n", + " fieldnames=['SHA1', 'fileName', 'size-in-bytes'], has_fieldnames=True)\n", + "\n", + "for o in csv2.load():\n", + " event.add_object(**o)\n", + " \n", + "print(event.to_json())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generic helper\n", + "\n", + "This helper is meant to be used when you already have a script that does the mapping between your own code, and the MISPObject template." ] }, { @@ -449,6 +590,13 @@ "print(misp_object.to_json())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## User defined objects" + ] + }, { "cell_type": "code", "execution_count": null, @@ -474,6 +622,7 @@ "outputs": [], "source": [ "from pymisp.tools import GenericObjectGenerator\n", + "from uuid import uuid4\n", "\n", "attributeAsDict = [{'MyCoolAttribute': {'value': 'critical thing', 'type': 'text'}}, \n", " {'MyCoolerAttribute': {'value': 'even worse', 'type': 'text'}}]\n", @@ -481,6 +630,11 @@ "\n", "misp_object = GenericObjectGenerator('my-cool-template')\n", "misp_object.generate_attributes(attributeAsDict)\n", + "# The parameters below are required if no template is provided.\n", + "misp_object.template_uuid = uuid4()\n", + "misp_object.templade_id = 1\n", + "misp_object.description = \"foo\"\n", + "setattr(misp_object, 'meta-category', 'bar')\n", "\n", "print(misp_object.to_json())" ] @@ -489,7 +643,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Use locally defined objet templates\n", + "## Use locally defined object templates\n", "\n", "**Important**: The path you pass as parameter for `misp_objects_path_custom` needs to contain a directory equals to the value of the parameter `name` (same structure as the content of the `misp-object` repository)\n" ] @@ -540,7 +694,7 @@ "source": [ "## Use lief to extract indicators out of binaries\n", "\n", - "An other cool helper: one liner to whom you can pass the path to a binary, if it is supported by `lief` (PE/ELF/Mach-o), you get the the file object, a PE, ELF, or Mach-o object, and the relevant sections.\n", + "An other cool helper: one liner to whom you can pass the path to a binary, if it is supported by `lief` (PE/ELF/Mach-o), you get the file object, a PE, ELF, or Mach-o object, and the relevant sections.\n", "\n", "If it is anything else, it will just generate the the file object.\n" ] @@ -600,6 +754,78 @@ "print(event.to_json())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Generate a feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPEvent, MISPOrganisation\n", + "from pymisp.tools import feed_meta_generator\n", + "from pathlib import Path\n", + "import json\n", + "\n", + "out_dir = Path('feed_test')\n", + "out_dir.mkdir(exist_ok=True)\n", + "\n", + "org = MISPOrganisation()\n", + "org.name = \"Test Org\"\n", + "org.uuid = \"972360d2-2c96-4004-937c-ba010d03f925\"\n", + "\n", + "event = MISPEvent()\n", + "\n", + "event.info = 'This is my new MISP event for a feed'\n", + "event.distribution = 1\n", + "event.Orgc = org\n", + "event.add_attribute('ip-dst', \"8.8.8.8\")\n", + "\n", + "feed_event = event.to_feed()\n", + "\n", + "with (out_dir / f'{event.uuid}.json').open('w') as f:\n", + " json.dump(feed_event, f)\n", + "\n", + "\n", + "feed_meta_generator(out_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!ls feed_test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!cat feed_test/manifest.json\n", + "\n", + "!echo ''\n", + "\n", + "!cat feed_test/hashes.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!rm feed_test/*" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -632,7 +858,7 @@ "source": [ "## Edit, removes the timestamp when exporting\n", "\n", - "If you tried to edit an event manually, and never got the updates on the instance, it is probably because the timestamps weren't updated/removed. Or you removed them all, and adding a single tag was makting every attributes as new.\n", + "If you tried to edit an event manually, and never got the updates on the instance, it is probably because the timestamps weren't updated/removed. Or you removed them all, and adding a single tag was making every attributes as new.\n", "\n", "PyMISP got you covered." ] @@ -647,98 +873,6 @@ "print(existing_event.attributes[0].to_json())" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Full example" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pymisp import MISPEvent, MISPObject\n", - "from pymisp import PyMISP\n", - "\n", - "event = MISPEvent()\n", - "event.info = 'This is my new MISP event' # Required\n", - "event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config\n", - "event.threat_level_id = 2 # Optional, defaults to MISP.default_event_threat_level in MISP config\n", - "event.analysis = 1 # Optional, defaults to 0 (initial analysis)\n", - "\n", - "mispObject = MISPObject('file')\n", - "mispObject.add_attribute('filename', type='filename',\n", - " value='filename.exe',\n", - " Tag=[{'name': 'tlp:amber'}])\n", - "\n", - "event.add_object(mispObject)\n", - "\n", - "# The URL of the MISP instance to connect to\n", - "misp_url = 'http://127.0.0.1:8080'\n", - "# Can be found in the MISP web interface under \n", - "# http://+MISP_URL+/users/view/me -> Authkey\n", - "misp_key = 'xe5okWNY2OB3O9ljR6t2cJPNsv4u1VZB0C1mKwtB'\n", - "# Should PyMISP verify the MISP certificate\n", - "misp_verifycert = False\n", - "\n", - "misp = PyMISP(misp_url, misp_key, misp_verifycert)\n", - "res = misp.add_event(event)\n", - "existing_event = MISPEvent()\n", - "existing_event.load(res)\n", - "mispObject = MISPObject('file')\n", - "mispObject.add_attribute('filename', type='filename',\n", - " value='filename2.exe',\n", - " Tag=[{'name': 'tlp:white'}])\n", - "\n", - "existing_event.add_object(mispObject)\n", - "print(existing_event.to_json())\n", - "\n", - "res = misp.update(existing_event)\n", - "existing_event = MISPEvent()\n", - "existing_event.load(res)\n", - "print(existing_event.to_json())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pymisp import MISPEvent, MISPObject\n", - "from pymisp import PyMISP\n", - "\n", - "event = MISPEvent()\n", - "\n", - "event.info = 'This is my new MISP event' # Required\n", - "event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config\n", - "event.threat_level_id = 2 # Optional, defaults to MISP.default_event_threat_level in MISP config\n", - "event.analysis = 1 # Optional, defaults to 0 (initial analysis)\n", - "\n", - "mispObject = MISPObject('file')\n", - "mispObject.add_attribute('filename', type='filename',\n", - " value='filename.exe',\n", - " Tag=[{'name':'tlp:amber'}]) \n", - "event.add_object(mispObject)\n", - "\n", - "# The URL of the MISP instance to connect to\n", - "misp_url = 'http://127.0.0.1:8080'\n", - "# Can be found in the MISP web interface under \n", - "# http://+MISP_URL+/users/view/me -> Authkey\n", - "misp_key = 'yB8DMS8LkfYYpcVX8bN2v7xwDZDMp4bpW0sNqNGj'\n", - "# Should PyMISP verify the MISP certificate\n", - "misp_verifycert = False\n", - "\n", - "misp = PyMISP(misp_url, misp_key, misp_verifycert)\n", - "res = misp.add_event(event)\n", - "existing_event = MISPEvent()\n", - "existing_event.load(res)\n", - "print(existing_event.to_json())" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -769,6 +903,586 @@ "\n", "print(event.to_json())\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting the API key (automatically generated on the training VM)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "api_file = Path('apikey')\n", + "if api_file.exists():\n", + " misp_url = 'http://127.0.0.1'\n", + " misp_verifycert = False\n", + " with open(api_file) as f:\n", + " misp_key = f.read().strip()\n", + " print(misp_key)\n", + "else:\n", + " print(\"Unable to find the api key\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize variables if you run the notebook locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The URL of the MISP instance to connect to\n", + "misp_url = 'https://127.0.0.1:8443/'\n", + "# Can be found in the MISP web interface under \n", + "# http://+MISP_URL+/users/view/me -> Authkey\n", + "misp_key = 'd6OmdDFvU3Seau3UjwvHS1y3tFQbaRNhJhDX0tjh'\n", + "# Should PyMISP verify the MISP certificate\n", + "misp_verifycert = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import PyMISP\n", + "\n", + "misp = PyMISP(misp_url, misp_key, misp_verifycert)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Full example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## New API\n", + "\n", + "Returns MISPEvent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPEvent, MISPObject\n", + "\n", + "event = MISPEvent()\n", + "event.info = 'This is my new MISP event' # Required\n", + "event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config\n", + "event.threat_level_id = 2 # Optional, defaults to MISP.default_event_threat_level in MISP config\n", + "event.analysis = 1 # Optional, defaults to 0 (initial analysis)\n", + "\n", + "mispObject = MISPObject('file')\n", + "mispObject.add_attribute('filename', type='filename',\n", + " value='filename.exe',\n", + " Tag=[{'name': 'tlp:amber'}])\n", + "\n", + "event.add_object(mispObject)\n", + "\n", + "print(misp)\n", + "existing_event = misp.add_event(event, pythonify=True)\n", + "print(existing_event)\n", + "mispObject = MISPObject('file')\n", + "mispObject.add_attribute('filename', type='filename',\n", + " value='filename2.exe',\n", + " Tag=[{'name': 'tlp:white'}])\n", + "\n", + "existing_event.add_object(mispObject)\n", + "print(existing_event.to_json())\n", + "\n", + "res = misp.update_event(existing_event)\n", + "existing_event = MISPEvent()\n", + "existing_event.load(res)\n", + "print(existing_event.to_json())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Interacting with a MISP instance" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating An Event" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Directly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.toggle_global_pythonify() # Returns PyMISP objects whenever possible, allows to skip pythonify\n", + "\n", + "event = misp.add_event({'distribution': 1, \"threat_level_id\": 1, \"analysis\": 1, 'info':\"Event from notebook\"})\n", + "print(\"Event id: %s\" % event.id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the MISPEvent constructor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPEvent\n", + "\n", + "event_obj = MISPEvent()\n", + "event_obj.distribution = 1\n", + "event_obj.threat_level_id = 1\n", + "event_obj.analysis = 1\n", + "event_obj.info = \"Event from notebook 2\"\n", + "event = misp.add_event(event_obj, pythonify=True)\n", + "event_id = event.id\n", + "print(\"Event id: %s\" % event_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fetching an event" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "event_id = 9" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch by ID\n", + "event = misp.get_event(event_id)\n", + "print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch by ID\n", + "event = misp_old.get_event(event_id)\n", + "print(event)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add an attribute to an event" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Directly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attr_type = \"ip-src\"\n", + "value = \"8.8.8.8\"\n", + "category = \"Network activity\"\n", + "to_ids = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Cleaner way" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "value = \"9.8.8.8\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPAttribute\n", + "\n", + "# Attribute data already defined\n", + "attribute = MISPAttribute()\n", + "attribute.type = attr_type\n", + "attribute.value = value\n", + "attribute.category = category\n", + "attribute.to_ids = to_ids\n", + "\n", + "attribute_to_change = misp.add_attribute(event_id, attribute, pythonify=True)\n", + "print(attribute_to_change.id, attribute_to_change)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Propose new Attribute" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPAttribute\n", + "\n", + "attr_type = \"ip-src\"\n", + "value = \"10.8.8.8\"\n", + "category = \"Network activity\"\n", + "to_ids = False\n", + "\n", + "# Attribute data already defined\n", + "attribute = MISPAttribute()\n", + "attribute.type = attr_type\n", + "attribute.value = value\n", + "attribute.category = category\n", + "attribute.to_ids = to_ids\n", + "\n", + "proposal = misp.add_attribute_proposal(event_id, attribute)\n", + "print(proposal.id, proposal)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Other things on proposals" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "proposal = misp.get_attribute_proposal(1)\n", + "print(proposal.to_json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "proposal = misp.accept_attribute_proposal(1)\n", + "print(proposal)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "proposal = misp.discard_attribute_proposal(2)\n", + "print(proposal)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Propose change to attribute" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPShadowAttribute\n", + "\n", + "proposal = MISPShadowAttribute()\n", + "proposal.type = 'ip-dst'\n", + "proposal.category = 'External analysis'\n", + "proposal.to_ids = False\n", + "\n", + "attribute = misp.update_attribute_proposal(attribute_to_change.id, proposal)\n", + "print(attribute.to_json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attribute = misp.update_attribute_proposal(attribute_to_change.id, {'to_ids': False, 'comment': \"This is crap\"})\n", + "print(attribute.to_json())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Update existing event" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import MISPAttribute, MISPObject\n", + "\n", + "attr_type = \"ip-src\"\n", + "value = \"20.8.8.8\"\n", + "category = \"Network activity\"\n", + "to_ids = False\n", + "\n", + "# Attribute data already defined\n", + "attribute = MISPAttribute()\n", + "attribute.type = attr_type\n", + "attribute.value = value\n", + "attribute.category = category\n", + "attribute.to_ids = to_ids\n", + "\n", + "# New Python 3.6 API\n", + "event = misp.get(event_id)\n", + "\n", + "## Add the attribute to the event\n", + "event.add_attribute(**attribute)\n", + "event.add_attribute(type='domain', value='circl.lu', disable_correlation=True)\n", + "\n", + "mispObject = MISPObject('file')\n", + "mispObject.add_attribute('filename', type='filename',\n", + " value='filename2.exe',\n", + " Tag=[{'name': 'tlp:white'}])\n", + "\n", + "event.add_object(mispObject)\n", + "\n", + "## Push the updated event to MISP\n", + "event_dict = misp.update_event(event)\n", + "print(event_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sightings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.sighting(value=event.attributes[1].value)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.sighting_list(event.attributes[1].id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Direct call, no validation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.direct_call('attributes/add/9', {'type': 'ip-dst', 'value': '8.11.8.8'})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.direct_call('events')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Admin Stuff" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.sharing_groups()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## User" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.users()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.add_user({'email': 'bar@foo.de'})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Organisations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.organisations()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Roles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.roles()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Feeds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.feeds(pythonify=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "misp.cache_feeds_all()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -787,7 +1501,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.9.5" } }, "nbformat": 4, diff --git a/docs/tutorial/Search-NG.ipynb b/docs/tutorial/Search-FullOverview.ipynb similarity index 60% rename from docs/tutorial/Search-NG.ipynb rename to docs/tutorial/Search-FullOverview.ipynb index 0e02a01..85d5909 100644 --- a/docs/tutorial/Search-NG.ipynb +++ b/docs/tutorial/Search-FullOverview.ipynb @@ -7,10 +7,10 @@ "outputs": [], "source": [ "# The URL of the MISP instance to connect to\n", - "misp_url = 'http://127.0.0.1:8080'\n", + "misp_url = 'https://127.0.0.1:8443'\n", "# Can be found in the MISP web interface under ||\n", "# http://+MISP_URL+/users/view/me -> Authkey\n", - "misp_key = 'LBelWqKY9SQyG0huZzAMqiEBl6FODxpgRRXMsZFu'\n", + "misp_key = 'd6OmdDFvU3Seau3UjwvHS1y3tFQbaRNhJhDX0tjh'\n", "# Should PyMISP verify the MISP certificate\n", "misp_verifycert = False" ] @@ -52,9 +52,9 @@ "metadata": {}, "outputs": [], "source": [ - "from pymisp import ExpandedPyMISP\n", + "from pymisp import PyMISP\n", "\n", - "misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=False)" + "misp = PyMISP(misp_url, misp_key, misp_verifycert, debug=False)" ] }, { @@ -70,7 +70,7 @@ "source": [ "## Search unpublished events\n", "\n", - "**WARNING**: By default, the search query will only return all the events listed on teh index page" + "**WARNING**: By default, the search query will only return all the events listed on the index page" ] }, { @@ -79,7 +79,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(published=False)\n", + "r = misp.search(published=False, metadata=True)\n", "print(r)" ] }, @@ -96,7 +96,16 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(eventid=[17217, 1717, 1721, 17218])" + "r = misp.search(eventid=[1,2,3], metadata=True, pythonify=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r" ] }, { @@ -112,7 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(tags=['tlp:white'], pythonify=True)\n", + "r = misp.search(tags=['tlp:white'], metadata=True, pythonify=True)\n", "for e in r:\n", " print(e)" ] @@ -123,7 +132,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(tag='TODO:VT-ENRICHMENT', published=False)" + "print('No attributes are in the event', r[0].attributes)" ] }, { @@ -132,7 +141,16 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(tag=['!TODO:VT-ENRICHMENT', 'tlp:white'], published=False) # ! means \"not this tag\"" + "r = misp.search(tags='TODO:VT-ENRICHMENT', published=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(tags=['!TODO:VT-ENRICHMENT', 'tlp:white'], metadata=True, published=False) # ! means \"not this tag\"" ] }, { @@ -148,7 +166,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(eventinfo='circl')" + "r = misp.search(eventinfo='circl', metadata=True)" ] }, { @@ -164,7 +182,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(org='CIRCL')" + "r = misp.search(org='CIRCL', metadata=True)" ] }, { @@ -180,7 +198,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search_index(timestamp='1h')" + "r = misp.search(timestamp='1h', metadata=True)" ] }, { @@ -227,6 +245,28 @@ "complex_query = misp.build_complex_query(or_parameters=['uibo.lembit@mail.ee', '103.195.185.222'])" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(complex_query)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "complex_query = misp.build_complex_query(or_parameters=['59.157.4.2', 'hotfixmsupload.com', '8.8.8.8'])\n", + "events = misp.search(value=complex_query, pythonify=True)\n", + "\n", + "for e in events:\n", + " print(e)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -318,6 +358,24 @@ "r = misp.search(value='8.8.8.8', withAttachments=True) # Return attachments" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(tags=['%tlp:amber%'], pythonify=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(r[0].tags)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -331,7 +389,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = misp.search(controller='attributes', value='8.8.8.9')" + "r = misp.search(controller='attributes', value='8.8.8.8')" ] }, { @@ -349,14 +407,7 @@ "metadata": {}, "outputs": [], "source": [ - "r" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Because reason" + "print(r)" ] }, { @@ -365,22 +416,146 @@ "metadata": {}, "outputs": [], "source": [ - "tag_to_remove = 'foo'\n", + "# Search attributes (specified in controller) where the attribute type is 'ip-src'\n", + "# And the to_ids flag is set\n", + "attributes = misp.search(controller='attributes', type_attribute='ip-src', to_ids=0, pythonify=True)\n", "\n", - "events = misp.search(tags=tag_to_remove, pythonify=True)\n", + "event_ids = set()\n", + "for attr in attributes:\n", + " event_ids.add(attr.event_id)\n", "\n", - "for event in events:\n", - " for tag in event.tags:\n", - " if tag.name == tag_to_remove:\n", - " print(f'Got {tag_to_remove} in {event.info}')\n", - " misp.untag(event.uuid, tag_to_remove)\n", - " break\n", - " for attribute in event.attributes:\n", - " for tag in attribute.tags:\n", - " if tag.name == tag_to_remove:\n", - " print(f'Got {tag_to_remove} in {attribute.value}')\n", - " misp.untag(attribute.uuid, tag_to_remove)\n", - " break" + "# Fetch all related events\n", + "for event_id in event_ids:\n", + " event = misp.get_event(event_id)\n", + " print(event.info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Last *published* attributes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attributes = misp.search(controller='attributes', publish_timestamp='1d', pythonify=True)\n", + "\n", + "for attribute in attributes:\n", + " print(attribute.event_id, attribute)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attributes = misp.search(controller='attributes', publish_timestamp=['2d', '1h'], pythonify=True)\n", + "\n", + "for a in attributes:\n", + " print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Last *updated* attributes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "ts = int(datetime.now().timestamp())\n", + "\n", + "attributes = misp.search(controller='attributes', timestamp=ts - 36000, pythonify=True)\n", + "\n", + "for a in attributes:\n", + " print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Orther output formats\n", + "\n", + "**Warning**: For that to work, the matching event has to be published" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(controller='attributes', value='8.8.8.8', return_format='csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(controller='events', value='9.8.8.8', return_format='snort')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(controller='events', value='9.8.8.8', return_format='suricata')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(controller='events', value='9.8.8.8', return_format='stix')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = misp.search(controller='events', value='9.8.8.8', return_format='stix2')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "print(r)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Search in logs" ] }, { @@ -410,17 +585,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "log = misp.search_logs(model='Tag', title=tag_to_remove)[0]\n", - "roles = misp.get_roles_list()\n", - "for r in roles:\n", - " if r['Role']['name'] == 'User':\n", - " new_role = r['Role']['id']\n", - " break\n", - "user = misp.get_user(log['Log']['user_id'])\n", - "user['User']['role_id'] = new_role\n", - "misp.edit_user(user['User']['id'], **user['User'])" - ] + "source": [] } ], "metadata": { @@ -439,7 +604,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.5" } }, "nbformat": 4, diff --git a/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb b/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb new file mode 100644 index 0000000..7e0fbc4 --- /dev/null +++ b/docs/tutorial/a.7-rest-api-extensive-restsearch.ipynb @@ -0,0 +1,1614 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Extracting data from MISP using PyMISP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recovering the API KEY" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- Go to `Global Actions` then `My Profile`\n", + "- Access the `/users/view/me` URL" + ] + }, + { + "cell_type": "code", + "execution_count": 491, + "metadata": {}, + "outputs": [], + "source": [ + "from pymisp import PyMISP\n", + "import urllib3\n", + "urllib3.disable_warnings()\n", + "\n", + "misp_url = 'https://localhost:8443/'\n", + "misp_key = 'GqfuZo444EFlylND0XaKZsEXgWgkPgguUZ6KVRuq'\n", + "# Should PyMISP verify the MISP certificate\n", + "misp_verifycert = False\n", + "\n", + "misp = PyMISP(misp_url, misp_key, misp_verifycert)" + ] + }, + { + "cell_type": "code", + "execution_count": 492, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "from pprint import pprint\n", + "import base64\n", + "import subprocess" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retreiving an Event" + ] + }, + { + "cell_type": "code", + "execution_count": 493, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "r1 = misp.get_event('7907c4a9-a15c-4c60-a1b4-1d214cf8cf41', pythonify=True)\n", + "print(r1)\n", + "r2 = misp.get_event(2, pythonify=False)\n", + "print(type(r2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Searching the Event index" + ] + }, + { + "cell_type": "code", + "execution_count": 494, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7907c4a9-a15c-4c60-a1b4-1d214cf8cf41\n" + ] + } + ], + "source": [ + "r = misp.search_index(pythonify=True)\n", + "print(r[1].uuid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Only published Events" + ] + }, + { + "cell_type": "code", + "execution_count": 495, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ ip-port), ]\n", + "List of tags: 18\n", + "\tThird Attribute [, ]\n" + ] + } + ], + "source": [ + "r1 = misp.search(controller='attributes', tags='tlp:red', pythonify=True)\n", + "print('Simple tag:', len(r1))\n", + "print('\\tFirst Attribute', r1[0].Tag)\n", + "\n", + "r2 = misp.search(controller='attributes', tags=['PAP:RED', 'tlp:red'], pythonify=True)\n", + "print('List of tags:', len(r2))\n", + "print('\\tThird Attribute', r2[2].Tag)" + ] + }, + { + "cell_type": "code", + "execution_count": 502, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wildcard: 22\n", + "\tTags of all Attributes: [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]\n", + "\n", + "Open question: Why do we have Attributes despite them not having the correct tag attached?\n", + "\n" + ] + } + ], + "source": [ + "r3 = misp.search(controller='attributes', tags=['misp-galaxy:target-information=%'], pythonify=True)\n", + "print('Wildcard:', len(r3))\n", + "print('\\tTags of all Attributes:', [attr.Tag for attr in r3])\n", + "print()\n", + "print(base64.b64decode('T3BlbiBxdWVzdGlvbjogV2h5IGRvIHdlIGhhdmUgQXR0cmlidXRlcyBkZXNwaXRlIHRoZW0gbm90IGhhdmluZyB0aGUgY29ycmVjdCB0YWcgYXR0YWNoZWQ/Cg==').decode())" + ] + }, + { + "cell_type": "code", + "execution_count": 503, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All unique Event tags: {'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:target-information=\"Germany\"', 'misp-galaxy:target-information=\"Luxembourg\"'}\n" + ] + } + ], + "source": [ + "allEventTags = [\n", + " [tag.name for tag in misp.get_event(attr.event_id, pythonify=True).Tag if tag.name.startswith('misp-galaxy:target-information=')]\n", + " for attr in r3\n", + "]\n", + "allUniqueEventTag = set()\n", + "for tags in allEventTags:\n", + " for tag in tags:\n", + " allUniqueEventTag.add(tag)\n", + "print('All unique Event tags:', allUniqueEventTag)" + ] + }, + { + "cell_type": "code", + "execution_count": 504, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Negation: 17\n", + "All unique Event tags: {'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:target-information=\"Germany\"'}\n" + ] + } + ], + "source": [ + "r4 = misp.search(\n", + " controller='attributes',\n", + " tags=['misp-galaxy:target-information=%', '!misp-galaxy:target-information=\"Luxembourg\"'],\n", + " pythonify=True)\n", + "print('Negation:', len(r4))\n", + "\n", + "\n", + "# Showing unique Event tags\n", + "allEventTags = [\n", + " [tag.name for tag in misp.get_event(attr.event_id, pythonify=True).Tag if tag.name.startswith('misp-galaxy:target-information=')]\n", + " for attr in r4\n", + "]\n", + "allUniqueEventTag = set()\n", + "for tags in allEventTags:\n", + " for tag in tags:\n", + " allUniqueEventTag.add(tag)\n", + "print('All unique Event tags:', allUniqueEventTag)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Want to also have the Event tags included**?" + ] + }, + { + "cell_type": "code", + "execution_count": 505, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tags of first attibute: []\n", + "Tags of first attibute: ['tlp:white', 'osint:lifetime=\"perpetual\"', 'osint:certainty=\"50\"', 'workflow:state=\"draft\"', 'misp-galaxy:threat-actor=\"APT 29\"', 'smo:sync', 'misp-galaxy:target-information=\"Canada\"', 'misp-galaxy:target-information=\"China\"', 'misp-galaxy:sector=\"Defense\"', 'misp-galaxy:sector=\"Infrastructure\"', 'misp-galaxy:malpedia=\"Kobalos\"', 'misp-galaxy:mitre-attack-pattern=\"SSH - T1021.004\"', 'misp-galaxy:mitre-attack-pattern=\"Software - T1592.002\"']\n" + ] + } + ], + "source": [ + "r5 = misp.search(\n", + " controller='attributes',\n", + " tags='misp-galaxy:target-information=%',\n", + " pythonify=True)\n", + "print('Tags of first attibute:', [tag.name for tag in r5[0].Tag])\n", + "\n", + "r6 = misp.search(\n", + " controller='attributes',\n", + " tags='misp-galaxy:target-information=%',\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('Tags of first attibute:', [tag.name for tag in r6[0].Tag])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Complex query**" + ] + }, + { + "cell_type": "code", + "execution_count": 506, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Or: 1056\n", + "[['tlp:amber'], ['tlp:amber'], ['tlp:amber'], ['tlp:amber'], ['tlp:amber']]\n", + "\n", + "And: 5\n", + "[['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber'],\n", + " ['adversary:infrastructure-type=\"c2\"', 'tlp:amber']]\n" + ] + } + ], + "source": [ + "complex_query = misp.build_complex_query(or_parameters=['tlp:amber', 'adversary:infrastructure-type=\"c2\"'])\n", + "r7 = misp.search(\n", + " controller='attributes',\n", + " tags=complex_query,\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('Or:', len(r7))\n", + "pprint([\n", + " [tag.name for tag in attr.Tag if (tag.name == 'tlp:amber' or tag.name == 'adversary:infrastructure-type=\"c2\"')] for attr in r7[:5]\n", + "])\n", + "print()\n", + "\n", + "complex_query = misp.build_complex_query(and_parameters=['tlp:amber', 'adversary:infrastructure-type=\"c2\"'])\n", + "r8 = misp.search(\n", + " controller='attributes',\n", + " tags=complex_query,\n", + " includeEventTags=True,\n", + " pythonify=True)\n", + "print('And:', len(r8))\n", + "pprint([\n", + " [tag.name for tag in attr.Tag if (tag.name == 'tlp:amber' or tag.name == 'adversary:infrastructure-type=\"c2\"')] for attr in r8\n", + "])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Searching on GalaxyCluster metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 507, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Events: 2\n", + "[['misp-galaxy:target-information=\"Canada\"',\n", + " 'misp-galaxy:target-information=\"China\"'],\n", + " ['misp-galaxy:target-information=\"Luxembourg\"']]\n" + ] + } + ], + "source": [ + "body = {\n", + " 'galaxy.member-of': 'NATO',\n", + " 'galaxy.official-languages': 'French',\n", + "}\n", + "\n", + "events = misp.direct_call('/events/restSearch', body)\n", + "print('Events: ', len(events))\n", + "pprint([\n", + " [tag['name'] for tag in event['Event']['Tag'] if tag['name'].startswith('misp-galaxy:target-information')] for event in events\n", + "])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Note 1**: The `galaxy.*` instructions are not supported by PyMISP\n", + "- **Note 2**: Each `galaxy.*` instructions are **AND**ed and are applied for the same cluster\n", + " - Cannot combine from different clusters\n", + " - Combining `Galaxy.official-languages` and `Galaxy.synonyms` would likely gives no result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Searching on creator Organisation metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 508, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Organisation nationality: {'admin_org': '', 'CIRCL': '', 'ORGNAME': '', 'Training': 'Luxembourg'}\n", + "Events: 4\n", + "Org for each Event: ['Training', 'Training', 'Training', 'Training']\n" + ] + } + ], + "source": [ + "all_orgs = misp.organisations()\n", + "print('Organisation nationality:', {org['Organisation']['name']: org['Organisation']['nationality'] for org in all_orgs})\n", + "\n", + "body = {\n", + " 'org.nationality': ['Luxembourg'],\n", + " 'org.sector': ['financial'],\n", + "}\n", + "\n", + "events = misp.direct_call('/events/restSearch', body)\n", + "print('Events: ', len(events))\n", + "print('Org for each Event:', [event['Event']['Orgc']['name'] for event in events])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Note 1**: The `org.*` instructions are not supported by PyMISP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ReturnFormat" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**CSV**" + ] + }, + { + "cell_type": "code", + "execution_count": 509, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "uuid,event_id,category,type,value,comment,to_ids,date,object_relation,attribute_tag,object_uuid,object_name,object_meta_category\n", + "\"724d5417-41e6-40a5-b368-bdfbe652302a\",2,\"Network activity\",\"ip-dst\",\"4.3.2.1\",\"Hello all!\",0,1639127173,\"\",\"\",\"\",\"\",\"\"\n", + "\"ba8e1a5a-6bb6-4ae5-9872-0a01b6b05cad\",2,\"Network activity\",\"ip-dst\",\"5.3.1.2\",\"\",1,1639060465,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"8c16cf20-d5bd-4ed3-b243-98c00c16e591\",2,\"Network activity\",\"ip-dst\",\"23.1.4.2\",\"\",1,1639126626,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"25a7bbb0-31f6-4525-94c0-89af86030201\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"f3eb2f37-d08d-4dbb-be0c-346ac508693f\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"f0a002d8-38a5-40f9-9a62-7e975cc8f987\",16,\"Network activity\",\"ip-dst\",\"127.0.0.1\",\"\",1,1645191487,\"ip-dst\",\"\",\"\",\"\",\"\"\n", + "\"61bfb8e3-20e3-4f37-905d-9d4e14f2564a\",20,\"Network activity\",\"ip-dst\",\"8.231.77.176\",\"\",1,1665471239,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"exploit-distribution-point\"\"\",\"\",\"\",\"\"\n", + "\"1ac08260-a5d6-4bee-bdcd-1525685ea07d\",20,\"Network activity\",\"ip-dst\",\"226.140.183.77\",\"\",1,1665471204,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"c2\"\"\",\"\",\"\",\"\"\n", + "\"78ce291d-241b-4162-8d6b-6a85964a31b8\",20,\"Network activity\",\"ip-dst\",\"2efe:65b4:7533:4f5f:1081:995:ff87:348f\",\"\",1,1665471204,\"ip\",\"PAP:RED,adversary:infrastructure-type=\"\"c2\"\"\",\"\",\"\",\"\"\n", + "\"b760f7a7-0d96-4b47-86b2-d5524cd2eff0\",26,\"Network activity\",\"ip-dst\",\"8.8.8.8\",\"\",1,1663321650,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"9023deba-1ba0-4ab3-a0bf-64a2d5c90520\",29,\"Network activity\",\"ip-dst\",\"81.177.170.166\",\"\",1,1665472920,\"ip\",\"adversary:infrastructure-type=\"\"c2\"\",misp-galaxy:mitre-attack-pattern=\"\"Botnet - T1583.005\"\"\",\"\",\"\",\"\"\n", + "\"c9d681ad-4087-4847-8f93-aef2e54452f2\",42,\"Network activity\",\"ip-dst\",\"2.2.2.2\",\"\",0,1671095982,\"ip\",\"\",\"\",\"\",\"\"\n", + "\"60950f6a-b3bf-4a0a-b901-43308e2f761a\",2,\"Network activity\",\"ip-src\",\"1.2.3.4\",\"\",0,1639060409,\"\",\"\",\"\",\"\",\"\"\n", + "\"f2a6eb8c-7a3e-4524-8036-1b90cb18fe75\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.0\",\"today\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"93bc9e55-20e9-4be1-b3e5-057e56a3b82e\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.1\",\"today - 1 days\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"f7771a53-fbdf-4980-822d-9a2339ce9076\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.2\",\"today - 2 days\",1,1622184577,\"\",\"\",\"\",\"\",\"\"\n", + "\"4972022a-26fd-4270-b614-506a9c951be6\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.3\",\"today - 3 days\",1,1622184578,\"\",\"admiralty-scale:information-credibility=\"\"1\"\",admiralty-scale:source-reliability=\"\"a\"\"\",\"\",\"\",\"\"\n", + "\"c661cd4b-0474-48eb-b4ed-eb02f6b569ea\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.4\",\"today - 4 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"42f68239-a794-492c-8fed-7520677824b0\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.5\",\"today - 5 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"d6404ba7-c847-49b8-8748-3029ce62e2b0\",7,\"Payload delivery\",\"ip-src\",\"149.23.54.6\",\"today - 6 days\",1,1622184578,\"\",\"\",\"\",\"\",\"\"\n", + "\"f04de340-ec63-471e-b5a2-66c3fe0676b6\",9,\"Network activity\",\"ip-src\",\"5.4.2.1\",\"\",0,1650956697,\"\",\"misp-galaxy:mitre-course-of-action=\"\"Access Token Manipulation Mitigation - T1134\"\"\",\"\",\"\",\"\"\n", + "\"7bb5432f-3d67-4d59-8a43-04e57e0dcc3f\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"b663b3b3-92af-41bf-a18f-8582bd0983b1\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"0ee4a946-d826-4884-aa28-e1b9da8cbbcb\",16,\"Network activity\",\"ip-src\",\"127.0.0.1\",\"\",1,1645191487,\"ip-src\",\"\",\"\",\"\",\"\"\n", + "\"1f4b0f6b-6cf9-47bf-acd4-f15b33e7d588\",21,\"Network activity\",\"ip-src\",\"185.194.93.14\",\"Attribute #281 enriched by dns.\",0,1668077578,\"\",\"\",\"\",\"\",\"\"\n", + "\"9f7f2d28-bcc8-466e-847f-3cf2a1ec4070\",21,\"Network activity\",\"ip-src\",\"31.22.121.122\",\"Attribute #291 enriched by dns.\",0,1663922175,\"\",\"\",\"\",\"\",\"\"\n", + "\"8153e053-c7c3-4a34-ae1c-b5cd3c80ba06\",22,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1659602097,\"\",\"\",\"\",\"\",\"\"\n", + "\"a57f70a2-70dd-4ea4-b879-fbcd03d465df\",24,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1662025545,\"\",\"another:tag\",\"\",\"\",\"\"\n", + "\"af044e10-5549-4018-bc6b-162cde1a1016\",21,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661517935,\"\",\"\",\"\",\"\",\"\"\n", + "\"fbb12142-0f82-4430-b0bc-2b1f9e26af67\",23,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661518277,\"\",\"\",\"\",\"\",\"\"\n", + "\"a783c55f-ac52-44b4-8be1-74d52bc2c4c3\",17,\"Network activity\",\"ip-src\",\"8.231.77.176\",\"\",0,1661517997,\"\",\"\",\"\",\"\",\"\"\n", + "\"90f6fd39-a426-43b3-9157-0c48bf0710fb\",22,\"Network activity\",\"ip-src\",\"31.22.121.122\",\"\",0,1661762437,\"\",\"\",\"\",\"\",\"\"\n", + "\"bc0a1ba5-d337-42b3-81fe-9d4b75a17bec\",26,\"Network activity\",\"ip-src\",\"185.194.93.14\",\"\",0,1663137408,\"\",\"\",\"\",\"\",\"\"\n", + "\"93931645-c86c-4dcf-aa4e-591edab44c4e\",26,\"Network activity\",\"ip-src\",\"8.8.8.8\",\"\",1,1663320641,\"\",\"\",\"\",\"\",\"\"\n", + "\n", + "\n" + ] + } + ], + "source": [ + "r1 = misp.search(\n", + " controller='attributes',\n", + " type_attribute=['ip-src', 'ip-dst'],\n", + " return_format='csv')\n", + "print(r1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Aggregated context** with `context-markdown`, `context` and `attack`" + ] + }, + { + "cell_type": "code", + "execution_count": 510, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Aggregated context data\n", + "## Tags and Taxonomies\n", + "#### admiralty-scale\n", + "*The Admiralty Scale or Ranking (also called the NATO System) is used to rank the reliability of a source and the credibility of an information. Reference based on FM 2-22.3 (FM 34-52) HUMAN INTELLIGENCE COLLECTOR OPERATIONS and NATO documents.*\n", + "- admiralty-scale:information-credibility="1"\n", + "\n", + " - **information-credibility**: Information Credibility\n", + " - **1**: Confirmed by other sources\n", + "- admiralty-scale:information-credibility="2"\n", + "\n", + " - **information-credibility**: Information Credibility\n", + " - **2**: Probably true\n", + "- admiralty-scale:source-reliability="a"\n", + "\n", + " - **source-reliability**: Source Reliability\n", + " - **a**: Completely reliable\n", + "#### economical-impact\n", + "*Economical impact is a taxonomy to describe the financial impact as positive or negative gain to the tagged information (e.g. data exfiltration loss, a positive gain for an adversary).*\n", + "- economical-impact:loss="less-than-1B-euro"\n", + "\n", + " - **loss**: Loss\n", + " - **less-than-1B-euro**: Less than 1 billion EUR\n", + "#### osint\n", + "*Open Source Intelligence - Classification (MISP taxonomies)*\n", + "- osint:certainty="50"\n", + "\n", + " - **certainty**: Certainty of the elements mentioned in this Open Source Intelligence\n", + " - **50**: Chances about even (probability equals 0.50 - 50%)\n", + "- osint:lifetime="perpetual"\n", + "\n", + " - **lifetime**: Lifetime of the information as Open Source Intelligence\n", + " - **perpetual**: Perpetual\n", + "#### tlp\n", + "*The Traffic Light Protocol - or short: TLP - was designed with the objective to create a favorable classification scheme for sharing sensitive information while keeping the control over its distribution at the same time.*\n", + "- tlp:red\n", + "\n", + " - **red**: (TLP:RED) Information exclusively and directly given to (a group of) individual recipients. Sharing outside is not legitimate.\n", + "- tlp:white\n", + "\n", + " - **white**: (TLP:WHITE) Information can be shared publicly in accordance with the law.\n", + "#### workflow\n", + "*Workflow support language is a common language to support intelligence analysts to perform their analysis on data and information.*\n", + "- workflow:state="draft"\n", + "\n", + " - **state**: State\n", + " - **draft**: Draft means the information tagged can be released as a preliminary version or outline\n", + "## Galaxy Clusters\n", + "#### Misinformation Pattern\n", + "*AM!TT Tactic*\n", + "- *[Adapt existing narratives](https://localhost:8443/galaxy_clusters/view/2712)*\n", + "Adapting existing narratives to current operational goals is the tactical sweet-spot for an effective misinformation campaign. Leveraging existing narratives is not only more effective, it requires substantially less resourcing, as the promotion of new master narratives operates on a much larger sca...\n", + "#### Malpedia\n", + "*Malware galaxy based on Malpedia archive.*\n", + "- *[Kobalos](https://localhost:8443/galaxy_clusters/view/4530)*\n", + "\n", + "#### Attack Pattern\n", + "*ATT&CK Tactic*\n", + "- *[SSH - T1021.004](https://localhost:8443/galaxy_clusters/view/9691)*\n", + "Adversaries may use [Valid Accounts](https://attack.mitre.org/techniques/T1078) to log into remote machines using Secure Shell (SSH). The adversary may then perform actions as the logged-on user.\n", + "\n", + "SSH is a protocol that allows authorized users to open remote shells on other computers. Many Linux and...\n", + "- *[Software - T1592.002](https://localhost:8443/galaxy_clusters/view/9721)*\n", + "Adversaries may gather information about the victim's host software that can be used during targeting. Information about installed software may include a variety of details such as types and versions on specific hosts, as well as the presence of additional components that might be indicative of...\n", + "#### Course of Action\n", + "*ATT&CK Mitigation*\n", + "- *[Access Token Manipulation Mitigation - T1134](https://localhost:8443/galaxy_clusters/view/8213)*\n", + "Access tokens are an integral part of the security system within Windows and cannot be turned off. However, an attacker must already have administrator level access on the local system to make full use of this technique; be sure to restrict users and accounts to the least privileges they require to ...\n", + "#### Sector\n", + "*Activity sectors*\n", + "- *[Defense](https://localhost:8443/galaxy_clusters/view/2762)*\n", + "\n", + "- *[Infrastructure](https://localhost:8443/galaxy_clusters/view/2780)*\n", + "\n", + "#### Target Information\n", + "*Description of targets of threat actors.*\n", + "- *[Canada](https://localhost:8443/galaxy_clusters/view/1994)*\n", + "\n", + "- *[China](https://localhost:8443/galaxy_clusters/view/2000)*\n", + "\n", + "#### Threat Actor\n", + "*Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.*\n", + "- *[APT 29](https://localhost:8443/galaxy_clusters/view/7251)*\n", + "A 2015 report by F-Secure describe APT29 as: 'The Dukes are a well-resourced, highly dedicated and organized cyberespionage group that we believe has been working for the Russian Federation since at least 2008 to collect intelligence in support of foreign and security policy decision-making. Th...\n" + ] + } + ], + "source": [ + "# Get the context of Events that were created by organisations from the financial sector\n", + "\n", + "body = {\n", + " 'returnFormat': 'context-markdown',\n", + " 'org.sector': ['financial'],\n", + "}\n", + "\n", + "r2 = misp.direct_call('/events/restSearch', body)\n", + "print(r2)" + ] + }, + { + "cell_type": "code", + "execution_count": 511, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the context of Events that had the threat actor APT-29 attached\n", + "\n", + "body = {\n", + " 'returnFormat': 'context',\n", + " 'tags': ['misp-galaxy:threat-actor=\\\"APT 29\\\"'],\n", + " 'staticHtml': 1, # If you want a JS-free HTML\n", + "}\n", + "\n", + "r2 = misp.direct_call('/events/restSearch', body)\n", + "with open('/tmp/attackOutput.html', 'w') as f:\n", + " f.write(r2)\n", + " # subprocess.run(['google-chrome', '--incognito', '/tmp/attackOutput.html'])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Be carefull with the amount of data you ask, use `pagination` if needed\n", + "\n", + "- `limit`: Specify the amount of data to be returned\n", + "- `page`: Specify the start of the rolling window. Is **not** zero-indexed\n", + "\n", + "If the size of the returned data is larger than the memory enveloppe you might get a different behavior based on your MISP setting:\n", + "- Nothing returned. Allowed memeory by PHP process exausted\n", + "- Data returned but slow. MISP will concatenante the returned data in a temporary file on disk\n", + " - This behavior is only applicable for `/*/restSearch` endpoints" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r1 = misp.search(controller='attributes', pythonify=True)\n", + "print('Amount of Attributes', len(r1))\n", + "\n", + "r2 = misp.search(\n", + " controller='attributes',\n", + " page=1,\n", + " limit=5,\n", + " pythonify=True)\n", + "print('Amount of paginated Attributes', len(r2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Searching for Sightings" + ] + }, + { + "cell_type": "code", + "execution_count": 513, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924035',\n", + " 'event_id': '40',\n", + " 'id': '12',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '65bd7539-29eb-46eb-bf7b-4c02473062c7',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924430',\n", + " 'event_id': '40',\n", + " 'id': '13',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '10857410-0033-4457-8a1d-c8331ee55d72',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924454',\n", + " 'event_id': '40',\n", + " 'id': '14',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': '1639fe60-0458-40f3-961b-7dc14eee9a7b',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924455',\n", + " 'event_id': '40',\n", + " 'id': '15',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': 'ee54ec70-3597-4455-bce9-c889202d533e',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1670924456',\n", + " 'event_id': '40',\n", + " 'id': '16',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '1',\n", + " 'uuid': '2c1cf4d1-a6ce-474b-8878-0251ee2b6bc5',\n", + " 'value': '398324'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1448',\n", + " 'date_sighting': '1671027299',\n", + " 'event_id': '41',\n", + " 'id': '17',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '39dff1d2-7082-48a9-8d30-ce29d412879b',\n", + " 'value': 'testtest'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1448',\n", + " 'date_sighting': '1671027301',\n", + " 'event_id': '41',\n", + " 'id': '18',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '84a8e7d0-715b-453f-8cdb-07db0c208185',\n", + " 'value': 'testtest'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '77',\n", + " 'date_sighting': '1671027307',\n", + " 'event_id': '9',\n", + " 'id': '19',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '264e4a25-e072-46e5-8460-b8df72e3115c',\n", + " 'value': '5.4.2.1'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '77',\n", + " 'date_sighting': '1671027308',\n", + " 'event_id': '9',\n", + " 'id': '20',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'b9f15aeb-54ea-44e5-90b8-22a418b973df',\n", + " 'value': '5.4.2.1'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '243',\n", + " 'date_sighting': '1671027309',\n", + " 'event_id': '9',\n", + " 'id': '21',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '4ef355f8-1cd3-476c-bccf-90a23b4eebfe',\n", + " 'value': 'test'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029412',\n", + " 'event_id': '29',\n", + " 'id': '22',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'f0e76bec-2e04-4e88-a976-df831257c856',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029413',\n", + " 'event_id': '29',\n", + " 'id': '23',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': '803bb696-ae86-4a04-9793-5f54a45c99b7',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1342',\n", + " 'date_sighting': '1671029414',\n", + " 'event_id': '29',\n", + " 'id': '24',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'fd8c4c0f-ebbb-4294-ade1-57493f1edc9a',\n", + " 'value': 'malware.exe|70f3bc193dfa56b78f3e6e4f800f701f'}},\n", + " {'Sighting': {'Organisation': {'id': '1',\n", + " 'name': 'ORGNAME',\n", + " 'uuid': 'c5de83b4-36ba-49d6-9530-2a315caeece6'},\n", + " 'attribute_id': '1441',\n", + " 'date_sighting': '1671030274',\n", + " 'event_id': '40',\n", + " 'id': '25',\n", + " 'org_id': '1',\n", + " 'source': '',\n", + " 'type': '0',\n", + " 'uuid': 'c84dd497-ad48-4b82-8203-6135a9a924fc',\n", + " 'value': '398324'}}]\n" + ] + } + ], + "source": [ + "body = {\n", + " 'last': '7d'\n", + "}\n", + "\n", + "sightings = misp.direct_call('/sightings/restSearch', body)\n", + "pprint(sightings)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sightings over time" + ] + }, + { + "cell_type": "code", + "execution_count": 512, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 514, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idattribute_idevent_idorg_iddate_sightinguuidsourcetypevalueOrganisationone
01214414012022-12-13 09:33:5565bd7539-29eb-46eb-bf7b-4c02473062c70398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
11314414012022-12-13 09:40:3010857410-0033-4457-8a1d-c8331ee55d720398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
21414414012022-12-13 09:40:541639fe60-0458-40f3-961b-7dc14eee9a7b1398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
31514414012022-12-13 09:40:55ee54ec70-3597-4455-bce9-c889202d533e1398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
41614414012022-12-13 09:40:562c1cf4d1-a6ce-474b-8878-0251ee2b6bc51398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
51714484112022-12-14 14:14:5939dff1d2-7082-48a9-8d30-ce29d412879b0testtest{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
61814484112022-12-14 14:15:0184a8e7d0-715b-453f-8cdb-07db0c2081850testtest{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
71977912022-12-14 14:15:07264e4a25-e072-46e5-8460-b8df72e3115c05.4.2.1{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
82077912022-12-14 14:15:08b9f15aeb-54ea-44e5-90b8-22a418b973df05.4.2.1{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
921243912022-12-14 14:15:094ef355f8-1cd3-476c-bccf-90a23b4eebfe0test{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
102213422912022-12-14 14:50:12f0e76bec-2e04-4e88-a976-df831257c8560malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
112313422912022-12-14 14:50:13803bb696-ae86-4a04-9793-5f54a45c99b70malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
122413422912022-12-14 14:50:14fd8c4c0f-ebbb-4294-ade1-57493f1edc9a0malware.exe|70f3bc193dfa56b78f3e6e4f800f701f{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
132514414012022-12-14 15:04:34c84dd497-ad48-4b82-8203-6135a9a924fc0398324{'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2...1
\n", + "
" + ], + "text/plain": [ + " id attribute_id event_id org_id date_sighting \\\n", + "0 12 1441 40 1 2022-12-13 09:33:55 \n", + "1 13 1441 40 1 2022-12-13 09:40:30 \n", + "2 14 1441 40 1 2022-12-13 09:40:54 \n", + "3 15 1441 40 1 2022-12-13 09:40:55 \n", + "4 16 1441 40 1 2022-12-13 09:40:56 \n", + "5 17 1448 41 1 2022-12-14 14:14:59 \n", + "6 18 1448 41 1 2022-12-14 14:15:01 \n", + "7 19 77 9 1 2022-12-14 14:15:07 \n", + "8 20 77 9 1 2022-12-14 14:15:08 \n", + "9 21 243 9 1 2022-12-14 14:15:09 \n", + "10 22 1342 29 1 2022-12-14 14:50:12 \n", + "11 23 1342 29 1 2022-12-14 14:50:13 \n", + "12 24 1342 29 1 2022-12-14 14:50:14 \n", + "13 25 1441 40 1 2022-12-14 15:04:34 \n", + "\n", + " uuid source type \\\n", + "0 65bd7539-29eb-46eb-bf7b-4c02473062c7 0 \n", + "1 10857410-0033-4457-8a1d-c8331ee55d72 0 \n", + "2 1639fe60-0458-40f3-961b-7dc14eee9a7b 1 \n", + "3 ee54ec70-3597-4455-bce9-c889202d533e 1 \n", + "4 2c1cf4d1-a6ce-474b-8878-0251ee2b6bc5 1 \n", + "5 39dff1d2-7082-48a9-8d30-ce29d412879b 0 \n", + "6 84a8e7d0-715b-453f-8cdb-07db0c208185 0 \n", + "7 264e4a25-e072-46e5-8460-b8df72e3115c 0 \n", + "8 b9f15aeb-54ea-44e5-90b8-22a418b973df 0 \n", + "9 4ef355f8-1cd3-476c-bccf-90a23b4eebfe 0 \n", + "10 f0e76bec-2e04-4e88-a976-df831257c856 0 \n", + "11 803bb696-ae86-4a04-9793-5f54a45c99b7 0 \n", + "12 fd8c4c0f-ebbb-4294-ade1-57493f1edc9a 0 \n", + "13 c84dd497-ad48-4b82-8203-6135a9a924fc 0 \n", + "\n", + " value \\\n", + "0 398324 \n", + "1 398324 \n", + "2 398324 \n", + "3 398324 \n", + "4 398324 \n", + "5 testtest \n", + "6 testtest \n", + "7 5.4.2.1 \n", + "8 5.4.2.1 \n", + "9 test \n", + "10 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "11 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "12 malware.exe|70f3bc193dfa56b78f3e6e4f800f701f \n", + "13 398324 \n", + "\n", + " Organisation one \n", + "0 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "1 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "2 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "3 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "4 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "5 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "6 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "7 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "8 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "9 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "10 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "11 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "12 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 \n", + "13 {'id': '1', 'uuid': 'c5de83b4-36ba-49d6-9530-2... 1 " + ] + }, + "execution_count": 514, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Converting our data to Panda DataFrame\n", + "sighting_rearranged = [sighting['Sighting'] for sighting in sightings]\n", + "df = pd.DataFrame.from_dict(sighting_rearranged)\n", + "df[\"date_sighting\"] = pd.to_datetime(df[\"date_sighting\"], unit='s')\n", + "df['one'] = 1\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('Min and Max:', df['date_sighting'].min(), df['date_sighting'].max())\n", + "print('Time delta:', df['date_sighting'].max() - df['date_sighting'].min())\n", + "print('Unique Event IDs:', df.event_id.unique())" + ] + }, + { + "cell_type": "code", + "execution_count": 515, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1441 6\n", + "1342 3\n", + "1448 2\n", + "77 2\n", + "243 1\n", + "Name: attribute_id, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 515, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGyCAYAAAC4Io22AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAhQUlEQVR4nO3de1TUdf7H8dcAMmICKt6DVdrKBG/lLTLdLNNF87RtlrmWZrtdNS+UJZqarYa15SHbMu3mntZba2tlpXbZzDpo3kItjxomiGLaYjKKOQLz+f3hz1lJUQc/wzDD83HOnNMM32He9mGYJ9/5zozDGGMEAABgQVigBwAAAKGDsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1EVV9gx6PRwUFBYqOjpbD4ajqmwcAAJVgjNHhw4fVvHlzhYVVvF+iysOioKBACQkJVX2zAADAgvz8fMXHx1f49SoPi+joaEknBouJianqmwcAAJXgcrmUkJDgfRyvSJWHxcmnP2JiYggLAACCzLkOY+DgTQAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArPE5LPbu3as777xTcXFxioqKUtu2bbV+/Xp/zAYAAIKMT58V8vPPP6tbt27q2bOnli1bpkaNGun7779X/fr1/TUfAAAIIj6FxTPPPKOEhAS9+eab3ssSExOtDwUAAIKTT0+FvP/+++rUqZNuu+02NW7cWFdeeaVeffXVs17H7XbL5XKVOwEAgNDk0x6LH374QbNmzVJaWprGjx+vdevWaeTIkYqMjNTQoUPPeJ2MjAxNmTLFyrDnq+W4D6v09vwld3q/QI8AAIBPHMYYc74bR0ZGqlOnTsrKyvJeNnLkSK1bt06rV68+43Xcbrfcbrf3vMvlUkJCgoqKihQTE3MBo1eMsAAAwC6Xy6XY2NhzPn779FRIs2bNlJSUVO6y1q1ba/fu3RVex+l0KiYmptwJAACEJp/Colu3btq+fXu5y3bs2KEWLVpYHQoAAAQnn8JizJgxWrNmjZ5++mnl5ORo/vz5mjNnjoYPH+6v+QAAQBDxKSw6d+6sJUuWaMGCBWrTpo3++te/KjMzU4MHD/bXfAAAIIj49KoQSbrpppt00003+WMWAAAQ5PisEAAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGt8Cosnn3xSDoej3OmKK67w12wAACDIRPh6heTkZH366af/+wYRPn8LAAAQonyugoiICDVt2tQfswAAgCDn8zEW33//vZo3b65LLrlEgwcP1u7du8+6vdvtlsvlKncCAAChyaew6Nq1q+bOnavly5dr1qxZ2rVrl7p3767Dhw9XeJ2MjAzFxsZ6TwkJCRc8NAAAqJ4cxhhT2SsfOnRILVq00IwZM/TnP//5jNu43W653W7veZfLpYSEBBUVFSkmJqayN31WLcd96JfvW9Vyp/cL9AgAAEg68fgdGxt7zsfvCzrysl69err88suVk5NT4TZOp1NOp/NCbgYAAASJC3ofiyNHjmjnzp1q1qyZrXkAAEAQ8yksHn30UX3xxRfKzc1VVlaWbrnlFoWHh2vQoEH+mg8AAAQRn54K2bNnjwYNGqTCwkI1atRI1157rdasWaNGjRr5az4AABBEfAqLhQsX+msOAAAQAvisEAAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhzQWExffp0ORwOjR492tI4AAAgmFU6LNatW6fZs2erXbt2NucBAABBrFJhceTIEQ0ePFivvvqq6tevb3smAAAQpCoVFsOHD1e/fv3Uq1cv2/MAAIAgFuHrFRYuXKiNGzdq3bp157W92+2W2+32nne5XL7eJAAACBI+7bHIz8/XqFGjNG/ePNWuXfu8rpORkaHY2FjvKSEhoVKDAgCA6s9hjDHnu/G7776rW265ReHh4d7LysrK5HA4FBYWJrfbXe5r0pn3WCQkJKioqEgxMTEW/gmnaznuQ79836qWO71foEcAAEDSicfv2NjYcz5++/RUyA033KAtW7aUu2zYsGG64oor9Pjjj58WFZLkdDrldDp9uRkAABCkfAqL6OhotWnTptxlF110keLi4k67HAAA1Dy88yYAALDG51eF/NrKlSstjAEAAEIBeywAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACs8SksZs2apXbt2ikmJkYxMTFKSUnRsmXL/DUbAAAIMj6FRXx8vKZPn64NGzZo/fr1uv7663XzzTfru+++89d8AAAgiET4snH//v3LnZ82bZpmzZqlNWvWKDk52epgAAAg+PgUFqcqKyvTv/71LxUXFyslJaXC7dxut9xut/e8y+Wq7E0CAIBqzuew2LJli1JSUnTs2DHVrVtXS5YsUVJSUoXbZ2RkaMqUKRc0JIJby3EfBnqEC5Y7vV+gRwCAoODzq0JatWql7Oxsff3113rwwQc1dOhQbd26tcLt09PTVVRU5D3l5+df0MAAAKD68nmPRWRkpC699FJJUseOHbVu3Tq98MILmj179hm3dzqdcjqdFzYlAAAIChf8PhYej6fcMRQAAKDm8mmPRXp6ulJTU/Wb3/xGhw8f1vz587Vy5UqtWLHCX/MBAIAg4lNYHDhwQEOGDNG+ffsUGxurdu3aacWKFbrxxhv9NR8AAAgiPoXF66+/7q85AABACOCzQgAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKzxKSwyMjLUuXNnRUdHq3HjxvrDH/6g7du3+2s2AAAQZHwKiy+++ELDhw/XmjVr9Mknn6ikpES9e/dWcXGxv+YDAABBJMKXjZcvX17u/Ny5c9W4cWNt2LBBPXr0sDoYAAAIPj6Fxa8VFRVJkho0aFDhNm63W26323ve5XJdyE0CAIBqrNJh4fF4NHr0aHXr1k1t2rSpcLuMjAxNmTKlsjcDwKKW4z4M9AgXLHd6v0CPYEUorIUUOusBeyr9qpDhw4fr22+/1cKFC8+6XXp6uoqKiryn/Pz8yt4kAACo5iq1x2LEiBH64IMPtGrVKsXHx591W6fTKafTWanhAABAcPEpLIwxevjhh7VkyRKtXLlSiYmJ/poLAAAEIZ/CYvjw4Zo/f77ee+89RUdH68cff5QkxcbGKioqyi8DAgCA4OHTMRazZs1SUVGRrrvuOjVr1sx7WrRokb/mAwAAQcTnp0IAAAAqwmeFAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBqfw2LVqlXq37+/mjdvLofDoXfffdcPYwEAgGDkc1gUFxerffv2eumll/wxDwAACGIRvl4hNTVVqamp/pgFAAAEOY6xAAAA1vi8x8JXbrdbbrfbe97lcvn7JgEAQID4PSwyMjI0ZcoUf98MAACV0nLch4EewYrc6f0CPYKkKngqJD09XUVFRd5Tfn6+v28SAAAEiN/3WDidTjmdTn/fDAAAqAZ8DosjR44oJyfHe37Xrl3Kzs5WgwYN9Jvf/MbqcAAAILj4HBbr169Xz549vefT0tIkSUOHDtXcuXOtDQYAAIKPz2Fx3XXXyRjjj1kAAECQ430sAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArKlUWLz00ktq2bKlateura5du2rt2rW25wIAAEHI57BYtGiR0tLSNHnyZG3cuFHt27dXnz59dODAAX/MBwAAgojPYTFjxgzde++9GjZsmJKSkvTKK6+oTp06euONN/wxHwAACCIRvmx8/PhxbdiwQenp6d7LwsLC1KtXL61evfqM13G73XK73d7zRUVFkiSXy1WZec+Lx33Ub9+7Kvnz/1FVCoX1YC2qD9aiegmF9WAtfPv+xpizb2h8sHfvXiPJZGVllbt87NixpkuXLme8zuTJk40kTpw4ceLEiVMInPLz88/aCj7tsaiM9PR0paWlec97PB4dPHhQcXFxcjgc/r55v3C5XEpISFB+fr5iYmICPU6NxlpUL6xH9cFaVB+hshbGGB0+fFjNmzc/63Y+hUXDhg0VHh6u/fv3l7t8//79atq06Rmv43Q65XQ6y11Wr149X2622oqJiQnqH5JQwlpUL6xH9cFaVB+hsBaxsbHn3MangzcjIyPVsWNHffbZZ97LPB6PPvvsM6WkpPg+IQAACCk+PxWSlpamoUOHqlOnTurSpYsyMzNVXFysYcOG+WM+AAAQRHwOi4EDB+qnn37SpEmT9OOPP6pDhw5avny5mjRp4o/5qiWn06nJkyef9hQPqh5rUb2wHtUHa1F91LS1cJhzvm4EAADg/PBZIQAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAIESd+gGQQFUhLFAj8Krq6sXj8QR6hJC3fft2TZgwQSUlJYEeBecQar+f/P4hZDWRx+NRWBjNFkjHjh3T8ePHvZ9V43A4ZIwJ2g++C3YFBQXatm2bCgsLddtttyksLIz7iR9t3rxZXbt2ldvtVs+ePdWvX79Aj4T/l5ubqyVLlujIkSNq1aqVbr/99pD7vcQbZFmSk5OjefPmaezYsapTpw6/NANo69ateuKJJ7Rjxw61aNFC/fv31wMPPBDosWqsLVu2aMCAAYqMjFReXp7atWunr776KtBjhaxNmzYpJSVF99xzjwoLCxUeHq45c+YoKioq5B7Ags3mzZuVmpqq1q1bq7i4WLm5ucrMzNTAgQMDPZpVPPJZkJOTo27duunFF1/UxIkTdfToUe9fZKhaW7duVY8ePdSsWTM99NBDio2N1bx587Rx48ZAj1Yj7dq1S3369NHgwYP1/vvva9myZcrLy9P69esDPVpI2rhxo7p37660tDT9/e9/19VXX62lS5eqoKDAu9cOgbFjxw717dtXQ4YM0YoVK/T222+rbdu2OnLkSKBHs449FheoqKhId999tyIiIpSYmKhVq1YpJSVF06ZNY89FFSssLNStt96qDh06KDMzU5J08OBBXXXVVRo+fLjGjh0b2AFroNmzZ+vf//63li5dqsjISJWWlurGG2/UQw89pEOHDql///5q2rRpoMcMCS6XS82bN9d9992nGTNmSJJKSkp0zTXXKCkpSXPnzmWPRYCUlJTogQceUGlpqV5//XVFRJw4CuH2229XVFSU6tevrxYtWmjMmDEBntQOHvEuUN26dZWUlKQBAwZo6tSp6tevn1avXq3x48efcc8FHec/eXl5atKkiW699VZJUmlpqRo0aKC+ffuqsLBQkliLKpafn69t27YpMjJSkpSZmamsrCxlZmZq+vTpuuqqq7x7L1iPytuzZ492796tjRs3eqPCGKOwsDD17t1bGzZs0H//+1/v5ahatWrV0iOPPKJ7773XGxUZGRlavHixysrK9Msvv+ixxx4LnadsDS5YSUmJ97+PHj1qpkyZYrp27WpGjRpljh49aowx5tixY4Ear8bYv3+/WbBggfe8x+MxxhjzwAMPmGHDhgVqrBpt8+bNpmnTpua3v/2tue2220ytWrXMihUrTFFRkTHGmJ49e5oePXoEeMrgtmXLFpOQkGDS0tKMMcaUlpYaY/73879//34THR1tnnrqqYDNWFOd+thwquzsbJOSkmI+/PBD72Vvv/22qVu3rvn222+rajy/YY9FJRQUFGjfvn3e8ycLtLS0VFFRUXr88cfVt29fff311xo/frwOHTqk4cOHe/+Shj0FBQUqKCiQJDVu3Fh33HGHJJ32CpBTX8//zDPPaOrUqVU7aA3x6/tGq1at9Omnn2rUqFG6/PLLdffdd6t3797ej4/u06ePSkpKdPz48UCNHNQ2bdqkrl27KiIiQvPnz9eBAwcUHh4uSXI4HCorK1Pjxo11//33a/ny5dq9e3eAJ645tm/frieeeEI5OTmnfa19+/ZavHix+vbt673M4/EoMTFRzZo1q8ox/YKw8FFeXp4SEhJ05513eh/QToqIiJDH45HT6dTjjz+u1NRUrV27Vl26dNGiRYt4jt+yk2tx1113ae/eveW+dmpUNGzYUNHR0ZKk8ePHa9KkSerfv3+VzloTnOm+ERkZqeTkZD388MM6duyY9uzZI0nesNi5c6eaN2/O7vlKOPnqj9GjR2vt2rWKi4vTq6++KmOM9//nycjo3bu3tmzZwkHMVcAYo19++UV33XWXnn32WT3//PPKz8/3fr20tFSSTguIDRs2qEWLFqpVq1aVzusXAd1fEoQ2bdpkWrRoYRo1amRSUlJMQUGB92sndz2e3P1VVFRk2rZta+rXr282b94ckHlD2fmshTHGjB071owYMcI89dRTpnbt2mb9+vWBGDfk/Xo99u7dW+7rS5cuNcnJyWbSpElm5cqVJi0tzcTFxYXErt+qtmnTJuN0Os348eONMcaUlZWZAQMGmM6dO3u3OfU+YIwxqamppnv37qasrOy0r8G+8ePHm2HDhpmoqCgzaNAgs2vXrjNut2/fPjNhwgRTr169kHmcICx84PF4zM6dO03fvn3Ntm3bTFJSkunWrZspLCw0xhizbds277Zut9uMHj3a1KlTJ2R+WKoTX9ZizJgxxuFwmIsuuoio8JNzrccPP/xgXC6XGTdunImPjzeXXnqp6dKli8nOzg7w5MFp7dq1ZuLEicaYE1FhzImf+djYWPPyyy+f8TpLliwxOTk5VTZjTXVyPUaNGmVeeukl89133xmn02mGDBliiouLzd/+9jeTm5trjDFm9erV5i9/+Ytp2bKl+eabbwI4tV283LQSevXqpRkzZqh27drq16+f4uPj1aRJE0nS66+/rosuukiSNHLkSA0dOlQdO3YM5Lgh7Wxr8dprr6lu3bqaOXOmZs6cqffee0/JyckBnji0VbQeHo9Hb7/9to4ePaqff/5ZR48eVaNGjVSvXr1AjxwSjDFyuVy6++67FRkZqfnz5yssLIyXlwbQ8uXLtXjxYr322mtat26dunfvrmbNmqmkpERffvmlEhMTlZubq+zsbLVv316JiYmBHtmeAIdNUPF4PKa0tNRcf/315pVXXjHGnHi6IzY21oSFhZmPP/44wBPWHGVlZee9Fvn5+SY/Pz9Qo9YI57pvrFixIsAT1gzvvPOOcTgc5quvvgr0KDXSqU8xffbZZ6ZVq1beVwampqaasLAwk5qaavbt23fG64QKDt48i4MHD+qnn34qd1l4eLh69uypY8eOSZJGjBghp9Op+Ph4Pf30096D02CXMUZlZWXe82FhYedci5MHTMXHxys+Pj4gc4cqX+8bGRkZ3DeqwE033aQbb7xRs2bN0i+//BLocWqE4uJiHT58WC6Xq9weotatW+uyyy5TVFSU7rnnHm3ZskVvvPGGvvzyS91///3e+0Mo7lUiLCrwww8/qHPnznrxxRe9R7if/AFo3LixsrKyNGTIEH388cf6/PPPlZWVpc2bN+vee+8t9wCIC7djxw6NGTNGN998s5566invm11JZ1+L++67z3sENuzhvlF9RUZGqmfPnlq6dKmKiooCPU7I27p1q/74xz/qd7/7nVq3bq158+Z5v9a4cWMdPnxYzZs310cffaQlS5Zo6NCh+uijj/T111+H9Dsy8+mmFfjkk0+0a9cuffDBB6pdu7buuece71sPJyUlacKECWrQoIE++ugjJSUlSZK++eYbHT9+3PsSL1y4LVu2qFevXurRo4fi4+M1bdo0GWM0efJkSVJycrImTpyoevXqnXEtTr7HCOzhvlE9mf9/75b7779fixcv9u45gn+c/FyiIUOGqFOnTtqwYYOGDRum5ORkdejQQcYYde/eXQ6HQ88//7yuuuoqlZWVqXv37srNzVXt2rUD/U/wGw7erMDmzZs1Y8YMXXbZZXr55Zf14IMPasSIEd6DzebMmaNrrrlGbdq0CeygIWzXrl26/vrrNWjQID399NOSpClTpujAgQPKzMxUrVq1VFZWptmzZ+vaa69Vu3btAjxxzcB9o3ozxujo0aPeg8hh38GDBzVo0CBdccUVeuGFF7yX9+zZU23bttXMmTMlST/++KOMMae9Z4X51Rv4hRr+nKuAMUZZWVl68803VVZWpjlz5ig6Olr/+c9/1KVLF02YMCHQI4a0srIyvfPOO0pNTdW4ceO8l+/Zs0ffffedunXrpg4dOmjgwIF66KGHAjhpzcN9o3pzOBxEhZ+VlJTo0KFDGjBggCR5P2wyMTFRBw8e9F5W0QfshXJUSIRFhU6+/CcvL0+TJk1SVFSUJkyYoIiICB7IqkB4eLjuuOMO7dmzRzExMZKkqVOn6s0339S4cePUpEkTvfXWW9q5c6eSk5P5hMwqxH0DNV2TJk30z3/+U5dddpmkE38IhYWF6eKLL1ZeXp4keY+hOHLkiOrWrRuwWQMhdI8e8UFFB5QdP35cq1atknTifd/Dw8MVFRWlzZs3n/Z23rDj1LWIj4/X1VdfLenER6IXFhbqgw8+0NSpU/Xwww/rH//4hz7//HNlZ2cHaNrQx30DOLOTUeHxeLxvw22M0YEDB7zbZGRkaM6cOTXuIPIaHxY7duxQZmZmuQ9OKikpkSR17dpVYWFhGjlypJYtW6bs7GyNHDlSTz75pBYuXMgR7padaS1OiouL07Rp0/T73/9exhh5PB6Vlpbqyiuv1MUXXxyAaUMf9w3g3MLCwsp91s3JPRWTJk3ShAkTdMMNN9S4g8hr1r/2V3JycpSSkqKff/5ZhYWFSktLU8OGDb312apVKw0ZMkRNmzbV+++/r8TERKWnpys8PFz9+/fnCHeLKloL6X8HOkVFRUk68fykw+HQwoULVatWLZ4G8QPuG8D5O/k7KiIiQgkJCXruuef07LPPav369Wrfvn2gx6tyNfZVIcXFxRo5cqQ8Ho86d+6sESNG6NFHH9Vjjz3mfUDbsWOH3nrrLd16663q0KGD9wAd2HU+a3GqrVu3asGCBZo5c6a+/PJLXg1iGfcNoHKmTZumiRMnKiYmRp9++qk6deoU6JECosbusQgLC1PHjh0VFxengQMHqmHDhrrjjjskyfsL9PLLL1d6errq1KkjKfSP5A2U81mLk3bv3q0nnnhC27Zt06pVq4gKP+C+AVROnz59NHHiRGVlZXnfw6UmqrF7LKQTf5md+rKsRYsWadCgQXrkkUf02GOPqVGjRvJ4PMrLywutD4iphs62FuPGjVNcXJzKyspUWFio48ePSxJv0+1H3DeAyvn1facmqrF7LCR5F//kS4UGDhwoY4z+9Kc/yeFwaPTo0XruueeUl5ent956y/vXGew737XYtWuXFixYENLvWlcdcN8AKqemR4VUw/dYnMoYI2OMwsLCtGjRIt1111265JJLtHPnTq1bt04dOnQI9Ig1xtnWYu3atbryyisDPWKNwn0DgC8Ii1Oc/F/hcDh0ww03KDs7WytXrlTbtm0DPFnNw1pUL6wHgPNVo58K+TWHw6GysjKNHTvW+8ZL/OIMDNaiemE9AJwvXh92BsnJydq4cSOvOKgGWIvqhfUAcC48FXIGof7Jc8GEtaheWA8A50JYAAAAa3gqBAAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAa/4PFusAbA7eHkYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Grouping by Attribute value\n", + "value_count = df['attribute_id'].value_counts()\n", + "print(value_count)\n", + "value_count.plot(kind='bar', rot=45)" + ] + }, + { + "cell_type": "code", + "execution_count": 516, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 9\n", + "1 5\n", + "Name: date_sighting, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 516, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGdCAYAAABO2DpVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAQG0lEQVR4nO3dX2jVdR/A8c9UOlnMPWrNFFdKBOafytKihCiKIizyJgoMzCCiZmZC5C4shtQSQgYl9geqXaTWjRU9ZIgwRUoqrciLNKmnRmEaxWYLTuL2XDw02KOrfvNzth19veB3cb7+fvw+QafefH/neGp6e3t7AwAgwajhHgAAOH0ICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgzZihvmFPT0/8+OOPUVtbGzU1NUN9ewBgEHp7e+Po0aMxZcqUGDVq4H2JIQ+LH3/8MRoaGob6tgBAgo6Ojpg6deqAfz7kYVFbWxsR/xts3LhxQ317AGAQurq6oqGhoe//4wMZ8rD48/HHuHHjhAUAVJm/+xiDD28CAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQZsh/Nv1MNm3Vv4d7BIbQf55dONwjAAw5OxYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkKRQWx48fj9WrV8f06dNj7NixcfHFF8eaNWuit7e3UvMBAFVkTJGT165dGxs2bIi2traYNWtWfPrpp7F06dKoq6uL5cuXV2pGAKBKFAqLDz/8MO68885YuHBhRERMmzYtNm3aFB9//HFFhgMAqkuhRyHXXXddbN++PQ4cOBAREV988UXs2rUrbrvttgGvKZfL0dXV1e8AAE5PhXYsVq1aFV1dXTFjxowYPXp0HD9+PJ5++ulYvHjxgNe0tLREc3PzKQ8KAIx8hXYs3nrrrXjjjTdi48aNsXfv3mhra4vnnnsu2traBrymqakpOjs7+46Ojo5THhoAGJkK7Vg8/vjjsWrVqrjnnnsiImLOnDnx3XffRUtLSyxZsuSk15RKpSiVSqc+KQAw4hXasfj9999j1Kj+l4wePTp6enpShwIAqlOhHYs77rgjnn766bjwwgtj1qxZ8dlnn8W6devi/vvvr9R8AEAVKRQWzz//fKxevToefvjhOHz4cEyZMiUefPDBePLJJys1HwBQRQqFRW1tbbS2tkZra2uFxgEAqpnfCgEA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0hQOix9++CHuvffemDhxYowdOzbmzJkTn376aSVmAwCqzJgiJ//666+xYMGCuPHGG+P999+P888/P77++usYP358peYDAKpIobBYu3ZtNDQ0xGuvvda3Nn369PShAIDqVOhRyLvvvhvz5s2Lu+66K+rr62Pu3LnxyiuvVGo2AKDKFAqLb775JjZs2BCXXHJJfPDBB/HQQw/F8uXLo62tbcBryuVydHV19TsAgNNToUchPT09MW/evHjmmWciImLu3Lmxb9++ePHFF2PJkiUnvaalpSWam5tPfVIAYMQrtGMxefLkmDlzZr+1Sy+9NL7//vsBr2lqaorOzs6+o6OjY3CTAgAjXqEdiwULFsT+/fv7rR04cCAuuuiiAa8plUpRKpUGNx0AUFUK7Vg89thjsXv37njmmWfi4MGDsXHjxnj55ZejsbGxUvMBAFWkUFjMnz8/tmzZEps2bYrZs2fHmjVrorW1NRYvXlyp+QCAKlLoUUhExO233x633357JWYBAKqc3woBANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANIICwAgjbAAANKMGe4BAE4H01b9e7hHYAj959mFwz3CiGXHAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDSnFBbPPvts1NTUxIoVK5LGAQCq2aDD4pNPPomXXnopLrvsssx5AIAqNqiw+O2332Lx4sXxyiuvxPjx47NnAgCq1KDCorGxMRYuXBg333zz355bLpejq6ur3wEAnJ7GFL1g8+bNsXfv3vjkk0/+0fktLS3R3NxceDAAoPoU2rHo6OiIRx99NN544404++yz/9E1TU1N0dnZ2Xd0dHQMalAAYOQrtGOxZ8+eOHz4cFx55ZV9a8ePH4+dO3fGCy+8EOVyOUaPHt3vmlKpFKVSKWdaAGBEKxQWN910U3z55Zf91pYuXRozZsyIJ5544oSoAADOLIXCora2NmbPnt1v7dxzz42JEyeesA4AnHn8zZsAQJrC3wr5f+3t7QljAACnAzsWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAECaQmHR0tIS8+fPj9ra2qivr49FixbF/v37KzUbAFBlCoXFjh07orGxMXbv3h3btm2LY8eOxS233BLd3d2Vmg8AqCJjipy8devWfq9ff/31qK+vjz179sT111+fOhgAUH0KhcX/6+zsjIiICRMmDHhOuVyOcrnc97qrq+tUbgkAjGCD/vBmT09PrFixIhYsWBCzZ88e8LyWlpaoq6vrOxoaGgZ7SwBghBt0WDQ2Nsa+ffti8+bNf3leU1NTdHZ29h0dHR2DvSUAMMIN6lHIsmXL4r333oudO3fG1KlT//LcUqkUpVJpUMMBANWlUFj09vbGI488Elu2bIn29vaYPn16peYCAKpQobBobGyMjRs3xjvvvBO1tbVx6NChiIioq6uLsWPHVmRAAKB6FPqMxYYNG6KzszNuuOGGmDx5ct/x5ptvVmo+AKCKFH4UAgAwEL8VAgCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkERYAQBphAQCkGVRYrF+/PqZNmxZnn312XHPNNfHxxx9nzwUAVKHCYfHmm2/GypUr46mnnoq9e/fG5ZdfHrfeemscPny4EvMBAFWkcFisW7cuHnjggVi6dGnMnDkzXnzxxTjnnHPi1VdfrcR8AEAVGVPk5D/++CP27NkTTU1NfWujRo2Km2++OT766KOTXlMul6NcLve97uzsjIiIrq6uwcxb1XrKvw/3CAyhM/Hf8TOZ9/eZ5Ux8f//5z9zb2/uX5xUKi59//jmOHz8ekyZN6rc+adKk+Oqrr056TUtLSzQ3N5+w3tDQUOTWUHXqWod7AqBSzuT399GjR6Ourm7APy8UFoPR1NQUK1eu7Hvd09MTv/zyS0ycODFqamoqfXuGWVdXVzQ0NERHR0eMGzduuMcBEnl/n1l6e3vj6NGjMWXKlL88r1BYnHfeeTF69Oj46aef+q3/9NNPccEFF5z0mlKpFKVSqd/av/71ryK35TQwbtw4/+GB05T395njr3Yq/lTow5tnnXVWXHXVVbF9+/a+tZ6enti+fXtce+21xScEAE4rhR+FrFy5MpYsWRLz5s2Lq6++OlpbW6O7uzuWLl1aifkAgCpSOCzuvvvuOHLkSDz55JNx6NChuOKKK2Lr1q0nfKATIv73KOypp5464XEYUP28vzmZmt6/+94IAMA/5LdCAIA0wgIASCMsAIA0wgIASCMsSNfS0hLz58+P2traqK+vj0WLFsX+/fuHeywgyc6dO+OOO+6IKVOmRE1NTbz99tvDPRIjiLAg3Y4dO6KxsTF2794d27Zti2PHjsUtt9wS3d3dwz0akKC7uzsuv/zyWL9+/XCPwgjk66ZU3JEjR6K+vj527NgR119//XCPAySqqamJLVu2xKJFi4Z7FEYIOxZUXGdnZ0RETJgwYZgnAaDShAUV1dPTEytWrIgFCxbE7Nmzh3scACqs4j+bzpmtsbEx9u3bF7t27RruUQAYAsKCilm2bFm89957sXPnzpg6depwjwPAEBAWpOvt7Y1HHnkktmzZEu3t7TF9+vThHgmAISIsSNfY2BgbN26Md955J2pra+PQoUMREVFXVxdjx44d5umAU/Xbb7/FwYMH+15/++238fnnn8eECRPiwgsvHMbJGAl83ZR0NTU1J11/7bXX4r777hvaYYB07e3tceONN56wvmTJknj99deHfiBGFGEBAKTxdVMAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADS/BfCDdsjZARtJAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Grouping by weekday (0-indexed)\n", + "amount_per_weekday = df['date_sighting'].dt.weekday.value_counts()\n", + "print(amount_per_weekday)\n", + "amount_per_weekday.plot(kind='bar', rot=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 517, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "date_sighting\n", + "9 5\n", + "14 8\n", + "15 1\n", + "Name: one, dtype: int64\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 517, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGxCAYAAAA+tv8YAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAeUklEQVR4nO3dfZBV9XnA8ee6KxeU3VUQBHQRRAsCASMYRnyJVhS2hNFOmraWmAXUUYsaJBjZcTQySBaaxMFm7NJkUkB8QROrJhpDhQnYaIiAwUhSUSzKqiS0FHcBk9Xu3v6RceOW17v8lt0Ln8/MmfGce849z05O9Ms5d7mZXC6XCwCABI5p7wEAgCOHsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSKD/cJm5qa4r333ouSkpLIZDKH+/QAQCvkcrnYuXNn9OnTJ445Zt/3JQ57WLz33ntRXl5+uE8LACRQW1sbp5566j5fP+xhUVJSEhF/HKy0tPRwnx4AaIX6+vooLy9v/u/4vhz2sPj48UdpaamwAIACc6CPMfjwJgCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSySssGhsb484774z+/ftHly5dYsCAATF79uzI5XJtNR8AUEDy+q6QefPmRU1NTSxevDiGDBkSa9eujcmTJ0dZWVnccsstbTUjAFAg8gqLF198Ma644ooYP358RET069cvHnnkkXjppZfaZDgAoLDk9Shk9OjRsWLFinj99dcjIuKVV16Jn/3sZ1FRUdEmwwEAhSWvOxYzZ86M+vr6GDRoUBQVFUVjY2PMmTMnJk6cuM9jGhoaoqGhoXm9vr6+9dMCAB1aXmHx2GOPxUMPPRQPP/xwDBkyJNavXx/Tpk2LPn36RGVl5V6Pqa6ujlmzZiUZFo5m/WY+094jHDHemju+vUeAI1Yml8evdJSXl8fMmTNj6tSpzdvuueeeePDBB+O1117b6zF7u2NRXl4edXV1UVpaegijw9FFWKQjLCB/9fX1UVZWdsD/fud1x+KDDz6IY45p+bGMoqKiaGpq2ucx2Ww2stlsPqcBAApUXmExYcKEmDNnTvTt2zeGDBkSv/zlL+Pee++NKVOmtNV8AEABySssvv3tb8edd94Zf//3fx/btm2LPn36xPXXXx933XVXW80HABSQvMKipKQk5s+fH/Pnz2+jcQCAQua7QgCAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGTyCot+/fpFJpPZY5k6dWpbzQcAFJDifHZes2ZNNDY2Nq9v2LAhLrvssvjCF76QfDAAoPDkFRY9evRosT537twYMGBAfPazn006FABQmFr9GYsPP/wwHnzwwZgyZUpkMpmUMwEABSqvOxaf9OSTT8b7778fkyZN2u9+DQ0N0dDQ0LxeX1/f2lMCAB1cq+9YfO9734uKioro06fPfverrq6OsrKy5qW8vLy1pwQAOrhWhcXbb78dy5cvj2uvvfaA+1ZVVUVdXV3zUltb25pTAgAFoFWPQhYuXBg9e/aM8ePHH3DfbDYb2Wy2NacBAApM3ncsmpqaYuHChVFZWRnFxa3+iAYAcATKOyyWL18eW7ZsiSlTprTFPABAAcv7lsPll18euVyuLWYBAAqc7woBAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSyTss3n333fjiF78Y3bt3jy5dusSnPvWpWLt2bVvMBgAUmOJ8dt6xY0ecf/75cckll8Szzz4bPXr0iDfeeCNOPPHEtpoPACggeYXFvHnzory8PBYuXNi8rX///smHAgAKU16PQn74wx/GyJEj4wtf+EL07NkzPv3pT8d3v/vdtpoNACgweYXFf/7nf0ZNTU2ceeaZsWzZsrjxxhvjlltuicWLF+/zmIaGhqivr2+xAABHprwehTQ1NcXIkSPj61//ekREfPrTn44NGzbEggULorKycq/HVFdXx6xZsw59UgCgw8vrjkXv3r1j8ODBLbadddZZsWXLln0eU1VVFXV1dc1LbW1t6yYFADq8vO5YnH/++bFx48YW215//fU47bTT9nlMNpuNbDbbuukAgIKS1x2LW2+9NVavXh1f//rXY9OmTfHwww/Hd77znZg6dWpbzQcAFJC8wuLcc8+NJ554Ih555JEYOnRozJ49O+bPnx8TJ05sq/kAgAKS16OQiIjPfe5z8bnPfa4tZgEACpzvCgEAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJLJKyzuvvvuyGQyLZZBgwa11WwAQIEpzveAIUOGxPLly//0BsV5vwUAcITKuwqKi4ujV69ebTELAFDg8v6MxRtvvBF9+vSJ008/PSZOnBhbtmxpi7kAgAKU1x2LUaNGxaJFi2LgwIGxdevWmDVrVlx44YWxYcOGKCkp2esxDQ0N0dDQ0LxeX19/aBMDAB1WXmFRUVHR/M/Dhg2LUaNGxWmnnRaPPfZYXHPNNXs9prq6OmbNmnVoU7aDfjOfae8RjhhvzR3f3iMAcJgc0q+bnnDCCfFnf/ZnsWnTpn3uU1VVFXV1dc1LbW3toZwSAOjADiksdu3aFW+++Wb07t17n/tks9koLS1tsQAAR6a8wmLGjBmxatWqeOutt+LFF1+Mv/zLv4yioqK46qqr2mo+AKCA5PUZi3feeSeuuuqq2L59e/To0SMuuOCCWL16dfTo0aOt5gMACkheYbF06dK2mgMAOAL4rhAAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACCZQwqLuXPnRiaTiWnTpiUaBwAoZK0OizVr1sQ///M/x7Bhw1LOAwAUsFaFxa5du2LixInx3e9+N0488cTUMwEABapVYTF16tQYP358jBkzJvU8AEABK873gKVLl8bLL78ca9asOaj9GxoaoqGhoXm9vr4+31MCAAUirzsWtbW18eUvfzkeeuih6Ny580EdU11dHWVlZc1LeXl5qwYFADq+vMJi3bp1sW3btjjnnHOiuLg4iouLY9WqVfGP//iPUVxcHI2NjXscU1VVFXV1dc1LbW1tsuEBgI4lr0chl156abz66qsttk2ePDkGDRoUt99+exQVFe1xTDabjWw2e2hTAgAFIa+wKCkpiaFDh7bYdvzxx0f37t332A4AHH38zZsAQDJ5/1bI/7dy5coEYwAARwJ3LACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGTyCouampoYNmxYlJaWRmlpaZx33nnx7LPPttVsAECBySssTj311Jg7d26sW7cu1q5dG3/+538eV1xxRfz6179uq/kAgAJSnM/OEyZMaLE+Z86cqKmpidWrV8eQIUOSDgYAFJ68wuKTGhsb4/vf/37s3r07zjvvvJQzAQAFKu+wePXVV+O8886LP/zhD9G1a9d44oknYvDgwfvcv6GhIRoaGprX6+vrWzcpANDh5f1bIQMHDoz169fHL37xi7jxxhujsrIyfvOb3+xz/+rq6igrK2teysvLD2lgAKDjyjssOnXqFGeccUaMGDEiqqurY/jw4XHfffftc/+qqqqoq6trXmpraw9pYACg42r1Zyw+1tTU1OJRx/+XzWYjm80e6mkAgAKQV1hUVVVFRUVF9O3bN3bu3BkPP/xwrFy5MpYtW9ZW8wEABSSvsNi2bVt86Utfiq1bt0ZZWVkMGzYsli1bFpdddllbzQcAFJC8wuJ73/teW80BABwBfFcIAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQTF5hUV1dHeeee26UlJREz54948orr4yNGze21WwAQIHJKyxWrVoVU6dOjdWrV8dzzz0XH330UVx++eWxe/futpoPACggxfns/JOf/KTF+qJFi6Jnz56xbt26uOiii5IOBgAUnkP6jEVdXV1ERHTr1i3JMABAYcvrjsUnNTU1xbRp0+L888+PoUOH7nO/hoaGaGhoaF6vr69v7SkBgA6u1Xcspk6dGhs2bIilS5fud7/q6uooKytrXsrLy1t7SgCgg2tVWNx0003x9NNPx09/+tM49dRT97tvVVVV1NXVNS+1tbWtGhQA6PjyehSSy+Xi5ptvjieeeCJWrlwZ/fv3P+Ax2Ww2stlsqwcEAApHXmExderUePjhh+Opp56KkpKS+O1vfxsREWVlZdGlS5c2GRAAKBx5PQqpqamJurq6uPjii6N3797Ny6OPPtpW8wEABSTvRyEAAPviu0IAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBk8g6L559/PiZMmBB9+vSJTCYTTz75ZBuMBQAUorzDYvfu3TF8+PC4//7722IeAKCAFed7QEVFRVRUVLTFLABAgfMZCwAgmbzvWOSroaEhGhoamtfr6+vb+pQAQDtp87Corq6OWbNmtfVpAGgH/WY+094jHBHemju+vUdIps0fhVRVVUVdXV3zUltb29anBADaSZvfschms5HNZtv6NABAB5B3WOzatSs2bdrUvL558+ZYv359dOvWLfr27Zt0OACgsOQdFmvXro1LLrmkeX369OkREVFZWRmLFi1KNhgAUHjyDouLL744crlcW8wCABQ4f48FAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQjLAAAJIRFgBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERYAQDLCAgBIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAywgIASEZYAADJCAsAIBlhAQAkIywAgGSEBQCQTKvC4v77749+/fpF586dY9SoUfHSSy+lngsAKEB5h8Wjjz4a06dPj6997Wvx8ssvx/Dhw2Ps2LGxbdu2tpgPACggeYfFvffeG9ddd11Mnjw5Bg8eHAsWLIjjjjsu/uVf/qUt5gMACkheYfHhhx/GunXrYsyYMX96g2OOiTFjxsTPf/7z5MMBAIWlOJ+d//u//zsaGxvj5JNPbrH95JNPjtdee22vxzQ0NERDQ0Pzel1dXURE1NfX5zvrYdXU8EF7j3DE6Oj/WxcK12Q6rsl0XJdpFMI1+fGMuVxuv/vlFRatUV1dHbNmzdpje3l5eVufmg6ibH57TwAtuSbpaArpmty5c2eUlZXt8/W8wuKkk06KoqKi+N3vftdi++9+97vo1avXXo+pqqqK6dOnN683NTXF//zP/0T37t0jk8nkc3o+ob6+PsrLy6O2tjZKS0vbexyICNclHY9rMp1cLhc7d+6MPn367He/vMKiU6dOMWLEiFixYkVceeWVEfHHUFixYkXcdNNNez0mm81GNpttse2EE07I57TsR2lpqf+z0OG4LuloXJNp7O9OxcfyfhQyffr0qKysjJEjR8ZnPvOZmD9/fuzevTsmT57cqiEBgCNH3mHxN3/zN/Ff//Vfcdddd8Vvf/vbOPvss+MnP/nJHh/oBACOPq368OZNN920z0cfHB7ZbDa+9rWv7fGYCdqT65KOxjV5+GVyB/q9EQCAg+RLyACAZIQFAJCMsAAAkhEWBWjnzp0xbdq0OO2006JLly4xevToWLNmTXuPxVHi+eefjwkTJkSfPn0ik8nEk08+uc99b7jhhshkMjF//vzDNh9HpwNdl5MmTYpMJtNiGTduXPsMe4QTFgXo2muvjeeeey6WLFkSr776alx++eUxZsyYePfdd9t7NI4Cu3fvjuHDh8f999+/3/2eeOKJWL169QH/lj5I4WCuy3HjxsXWrVubl0ceeeQwTnj0aPPvCiGt3//+9/H444/HU089FRdddFFERNx9993xox/9KGpqauKee+5p5wk50lVUVERFRcV+93n33Xfj5ptvjmXLlsX48eMP02QczQ7musxms/v8+gnScceiwPzv//5vNDY2RufOnVts79KlS/zsZz9rp6ngT5qamuLqq6+O2267LYYMGdLe40CzlStXRs+ePWPgwIFx4403xvbt29t7pCOSsCgwJSUlcd5558Xs2bPjvffei8bGxnjwwQfj5z//eWzdurW9x4OYN29eFBcXxy233NLeo0CzcePGxQMPPBArVqyIefPmxapVq6KioiIaGxvbe7QjjkchBWjJkiUxZcqUOOWUU6KoqCjOOeecuOqqq2LdunXtPRpHuXXr1sV9990XL7/8sm8vpkP527/92+Z//tSnPhXDhg2LAQMGxMqVK+PSSy9tx8mOPO5YFKABAwbEqlWrYteuXVFbWxsvvfRSfPTRR3H66ae392gc5f793/89tm3bFn379o3i4uIoLi6Ot99+O77yla9Ev3792ns8aHb66afHSSedFJs2bWrvUY447lgUsOOPPz6OP/742LFjRyxbtiz+4R/+ob1H4ih39dVXx5gxY1psGzt2bFx99dW+AZkO5Z133ont27dH796923uUI46wKEDLli2LXC4XAwcOjE2bNsVtt90WgwYN8i9uDotdu3a1+FPe5s2bY/369dGtW7fo27dvdO/evcX+xx57bPTq1SsGDhx4uEflKLK/67Jbt24xa9as+PznPx+9evWKN998M7761a/GGWecEWPHjm3HqY9MwqIA1dXVRVVVVbzzzjvRrVu3+PznPx9z5syJY489tr1H4yiwdu3auOSSS5rXp0+fHhERlZWVsWjRonaaiqPd/q7Lmpqa+NWvfhWLFy+O999/P/r06ROXX355zJ4927eetgHfbgoAJOPDmwBAMsICAEhGWAAAyQgLACAZYQEAJCMsAIBkhAUAkIywAACSERZQQC6++OKYNm1ae4+xV2+99VZkMplYv379QR+zaNGiOOGEEw7LuYDDQ1jAEWrlypWRyWTi/fffPyznKy8vj61bt8bQoUOTvu+kSZPiyiuvPCznAg6d7woBkigqKopevXodcecC8uOOBXRQu3fvji996UvRtWvX6N27d3zrW99q8fqSJUti5MiRUVJSEr169Yq/+7u/i23btkXEHx8VfPyFTCeeeGJkMpmYNGlSREQ0NTVFdXV19O/fP7p06RLDhw+PH/zgBwc1044dO2LixInRo0eP6NKlS5x55pmxcOHC5nP+/8cTP/zhD+PMM8+Mzp07xyWXXBKLFy/e612UZcuWxVlnnRVdu3aNcePGxdatWyMi4u67747FixfHU089FZlMJjKZTKxcuXKPc318d2bFihUxcuTIOO6442L06NGxcePGFue55557omfPnlFSUhLXXnttzJw5M84+++yD+tmBgyMsoIO67bbbYtWqVfHUU0/Fv/3bv8XKlSvj5Zdfbn79o48+itmzZ8crr7wSTz75ZLz11lvN8VBeXh6PP/54RERs3Lgxtm7dGvfdd19ERFRXV8cDDzwQCxYsiF//+tdx6623xhe/+MVYtWrVAWe688474ze/+U08++yz8R//8R9RU1MTJ5100l733bx5c/zVX/1VXHnllfHKK6/E9ddfH3fcccce+33wwQfxzW9+M5YsWRLPP/98bNmyJWbMmBERETNmzIi//uu/bo6NrVu3xujRo/c53x133BHf+ta3Yu3atVFcXBxTpkxpfu2hhx6KOXPmxLx582LdunXRt2/fqKmpOeDPDOQpB3Q4O3fuzHXq1Cn32GOPNW/bvn17rkuXLrkvf/nLez1mzZo1uYjI7dy5M5fL5XI//elPcxGR27FjR/M+f/jDH3LHHXdc7sUXX2xx7DXXXJO76qqrDjjXhAkTcpMnT97ra5s3b85FRO6Xv/xlLpfL5W6//fbc0KFDW+xzxx13tJhp4cKFuYjIbdq0qXmf+++/P3fyySc3r1dWVuauuOKK/Z7r4591+fLlzfs888wzuYjI/f73v8/lcrncqFGjclOnTm3xPueff35u+PDhB/y5gYPnjgV0QG+++WZ8+OGHMWrUqOZt3bp1i4EDBzavr1u3LiZMmBB9+/aNkpKS+OxnPxsREVu2bNnn+27atCk++OCDuOyyy6Jr167NywMPPBBvvvnmAee68cYbY+nSpXH22WfHV7/61XjxxRf3ue/GjRvj3HPPbbHtM5/5zB77HXfccTFgwIDm9d69ezc/0snXsGHDWrxPRDS/18aNG/c4/97mAQ6ND29CAdq9e3eMHTs2xo4dGw899FD06NEjtmzZEmPHjo0PP/xwn8ft2rUrIiKeeeaZOOWUU1q8ls1mD3jeioqKePvtt+PHP/5xPPfcc3HppZfG1KlT45vf/Garf5Zjjz22xXomk4lcLnfI75XJZCLij58pAQ4fdyygAxowYEAce+yx8Ytf/KJ5244dO+L111+PiIjXXnsttm/fHnPnzo0LL7wwBg0atMef8jt16hQREY2Njc3bBg8eHNlsNrZs2RJnnHFGi6W8vPygZuvRo0dUVlbGgw8+GPPnz4/vfOc7e91v4MCBsXbt2hbb1qxZc1Dn+P8/xyd/htYaOHDgHudvzTzA/rljAR1Q165d45prronbbrstunfvHj179ow77rgjjjnmj38W6Nu3b3Tq1Cm+/e1vxw033BAbNmyI2bNnt3iP0047LTKZTDz99NPxF3/xF9GlS5coKSmJGTNmxK233hpNTU1xwQUXRF1dXbzwwgtRWloalZWV+53rrrvuihEjRsSQIUOioaEhnn766TjrrLP2uu/1118f9957b9x+++1xzTXXxPr162PRokUR8ae7CQejX79+sWzZsti4cWN07949ysrKDvrYT7r55pvjuuuui5EjR8bo0aPj0UcfjV/96ldx+umnt+r9gL1zxwI6qG984xtx4YUXxoQJE2LMmDFxwQUXxIgRIyLij3cNFi1aFN///vdj8ODBMXfu3D0eR5xyyikxa9asmDlzZpx88slx0003RUTE7Nmz484774zq6uo466yzYty4cfHMM89E//79DzhTp06doqqqKoYNGxYXXXRRFBUVxdKlS/e6b//+/eMHP/hB/Ou//msMGzYsampqmn8r5GAeu3zsuuuui4EDB8bIkSOjR48e8cILLxz0sZ80ceLEqKqqihkzZsQ555wTmzdvjkmTJkXnzp1b9X7A3mVyrX2YCZCnOXPmxIIFC6K2tra9R4mIiMsuuyx69eoVS5Ysae9R4IjhUQjQZv7pn/4pzj333OjevXu88MIL8Y1vfKP5zsnh9sEHH8SCBQti7NixUVRUFI888kgsX748nnvuuXaZB45UHoUAzW644YYWv4b6yeWGG27I+/3eeOONuOKKK2Lw4MExe/bs+MpXvhJ33313+sEPQiaTiR//+Mdx0UUXxYgRI+JHP/pRPP744zFmzJh2mQeOVB6FAM22bdsW9fX1e32ttLQ0evbseZgnAgqNsAAAkvEoBABIRlgAAMkICwAgGWEBACQjLACAZIQFAJCMsAAAkhEWAEAy/weQUvStHTG7NQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "amount_per_weekday_for_each_attribute = df.groupby([df['date_sighting'].dt.hour])['one'].sum()\n", + "print(amount_per_weekday_for_each_attribute)\n", + "amount_per_weekday_for_each_attribute.plot(kind='bar', rot=0)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 ('venv': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "99e19f785595e5572f3a0434505ffd496bc893a60c3b4501be593ee9ddcf6bde" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorial/PyMISP_tutorial.ipynb b/docs/tutorial/old/PyMISP_tutorial.ipynb similarity index 98% rename from docs/tutorial/PyMISP_tutorial.ipynb rename to docs/tutorial/old/PyMISP_tutorial.ipynb index c12a217..37cc3f4 100644 --- a/docs/tutorial/PyMISP_tutorial.ipynb +++ b/docs/tutorial/old/PyMISP_tutorial.ipynb @@ -358,9 +358,10 @@ "# And the to_ids flag is set\n", "attributes = misp.search(controller='attributes', type_attribute='ip-src', to_ids=0, pythonify=True)\n", "\n", + "# Collect all event_id matching the searched attribute\n", "event_ids = set()\n", "for attr in attributes:\n", - " event_ids.add(event_id)\n", + " event_ids.add(attr.event_id)\n", "\n", "# Fetch all related events\n", "for event_id in event_ids:\n", @@ -499,7 +500,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/docs/tutorial/Search.ipynb b/docs/tutorial/old/Search.ipynb similarity index 99% rename from docs/tutorial/Search.ipynb rename to docs/tutorial/old/Search.ipynb index 9244e4c..cd2f8af 100644 --- a/docs/tutorial/Search.ipynb +++ b/docs/tutorial/old/Search.ipynb @@ -70,7 +70,7 @@ "source": [ "## Search unpublished events\n", "\n", - "**WARNING**: By default, the search query will only return all the events listed on teh index page" + "**WARNING**: By default, the search query will only return all the events listed on the index page" ] }, { @@ -457,7 +457,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/docs/tutorial/Usage-NG.ipynb b/docs/tutorial/old/Usage-NG.ipynb similarity index 99% rename from docs/tutorial/Usage-NG.ipynb rename to docs/tutorial/old/Usage-NG.ipynb index c3a4eac..41704e3 100644 --- a/docs/tutorial/Usage-NG.ipynb +++ b/docs/tutorial/old/Usage-NG.ipynb @@ -480,7 +480,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/add_attributes_from_csv.py b/examples/add_attributes_from_csv.py new file mode 100644 index 0000000..e05bb1b --- /dev/null +++ b/examples/add_attributes_from_csv.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import csv +from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPAttribute +from keys import misp_url, misp_key, misp_verifycert +from requests.packages.urllib3.exceptions import InsecureRequestWarning +import argparse +import urllib3 +import requests +requests.packages.urllib3.disable_warnings() + + +""" + +Sample usage: + +python3 add_filetype_object_from_csv.py -e -f .csv + + +Attribute CSV file (aach line is an entry): + +value;category;type;comment;to_ids;first_seen;last_seen;tag1;tag2 +test.pdf;Payload delivery;filename;Email attachment;0;1970-01-01;1970-01-01;tlp:green;ransomware +127.0.0.1;Network activity;ip-dst;C2 server;1;;;tlp:white; + +value = IOC's value +category = its MISP category (https://www.circl.lu/doc/misp/categories-and-types/) +type = its MISP type (https://www.circl.lu/doc/misp/categories-and-types/) +comment = IOC's description +to_ids = Boolean expected (0 = IDS flag not checked // 1 = IDS flag checked) +first_seen = First seen date, if any (left empty if not) +last_seen = Last seen date, if any (left empty if not) +tag = IOC tag, if any + +""" + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Add attributes to a MISP event from a semi-colon formated csv file') + parser.add_argument("-e", "--event_uuid", required=True, help="Event UUID to update") + parser.add_argument("-f", "--attr_file", required=True, help="Attribute CSV file path") + args = parser.parse_args() + + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + + f = open(args.attr_file, newline='') + csv_reader = csv.reader(f, delimiter=";") + + for line in csv_reader: + value = line[0] + category = line[1] + type = line[2] + comment = line[3] + ids = line[4] + fseen = line[5] + lseen = line[6] + tags = line[7:] + + misp_attribute = MISPAttribute() + misp_attribute.value = str(value) + misp_attribute.category = str(category) + misp_attribute.type = str(type) + misp_attribute.comment = str(comment) + misp_attribute.to_ids = str(ids) + if fseen != '': + misp_attribute.first_seen = str(fseen) + if lseen != '': + misp_attribute.last_seen = str(lseen) + for x in tags: + misp_attribute.add_tag(x) + r = pymisp.add_attribute(args.event_uuid, misp_attribute) + print(line) + print("\nAttributes successfully saved :)") diff --git a/examples/add_email_object.py b/examples/add_email_object.py index 1ff1c87..5f190ae 100755 --- a/examples/add_email_object.py +++ b/examples/add_email_object.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from pymisp import PyMISP from pymisp.tools import EMailObject import traceback -from keys import misp_url, misp_key, misp_verifycert +from keys import misp_url, misp_key, misp_verifycert # type: ignore import glob import argparse @@ -20,12 +19,11 @@ if __name__ == '__main__': for f in glob.glob(args.path): try: eo = EMailObject(f) - except Exception as e: + except Exception: traceback.print_exc() continue if eo: - template_id = pymisp.get_object_template_id(eo.template_uuid) - response = pymisp.add_object(args.event, template_id, eo) + response = pymisp.add_object(args.event, eo, pythonify=True) for ref in eo.ObjectReference: r = pymisp.add_object_reference(ref) diff --git a/examples/add_fail2ban_object.py b/examples/add_fail2ban_object.py index 225eed8..0ea3858 100755 --- a/examples/add_fail2ban_object.py +++ b/examples/add_fail2ban_object.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pymisp import PyMISP, MISPEvent +from pymisp import ExpandedPyMISP, MISPEvent from pymisp.tools import Fail2BanObject import argparse from base64 import b64decode @@ -43,23 +43,23 @@ if __name__ == '__main__': parser.add_argument("-d", "--disable_new", action='store_true', default=False, help="Do not create a new Event.") args = parser.parse_args() - pymisp = PyMISP(misp_url, misp_key, misp_verifycert, debug=True) + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=True) event_id = -1 me = None if args.force_new: me = create_new_event() else: - response = pymisp.search_index(tag=args.tag, timestamp='1h') - if response['response']: + response = pymisp.search_index(tags=args.tag, timestamp='1h', pythonify=True) + if response: if args.disable_new: - event_id = response['response'][0]['id'] + event_id = response[0].id else: - last_event_date = parse(response['response'][0]['date']).date() - nb_attr = response['response'][0]['attribute_count'] + last_event_date = parse(response[0].date).date() + nb_attr = response[0].attribute_count if last_event_date < date.today() or int(nb_attr) > 1000: me = create_new_event() else: - event_id = response['response'][0]['id'] + event_id = response[0].id else: me = create_new_event() @@ -83,5 +83,4 @@ if __name__ == '__main__': me.add_object(f2b) pymisp.add_event(me) elif event_id: - template_id = pymisp.get_object_template_id(f2b.template_uuid) - a = pymisp.add_object(event_id, template_id, f2b) + a = pymisp.add_object(event_id, f2b) diff --git a/examples/add_feed.py b/examples/add_feed.py index 94d0d04..aed6d07 100755 --- a/examples/add_feed.py +++ b/examples/add_feed.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPFeed from keys import misp_url, misp_key, misp_verifycert import argparse @@ -14,6 +14,12 @@ if __name__ == '__main__': parser.add_argument("-p", "--provider", required=True, help="Provider name") args = parser.parse_args() - pm = PyMISP(misp_url, misp_key, misp_verifycert, debug=True) - response = pm.add_feed(args.format, args.url, args.name, args.input, args.provider) - print(response) + pm = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=True) + feed = MISPFeed() + feed.format = args.format + feed.url = args.url + feed.name = args.name + feed.input = args.input + feed.provider = args.provider + response = pm.add_feed(feed, pythonify=True) + print(response.to_json()) diff --git a/examples/add_file_object.py b/examples/add_file_object.py index cfa8dc9..72abf37 100755 --- a/examples/add_file_object.py +++ b/examples/add_file_object.py @@ -19,23 +19,29 @@ if __name__ == '__main__': for f in glob.glob(args.path): try: fo, peo, seos = make_binary_objects(f) - except Exception as e: + except Exception: traceback.print_exc() continue if seos: for s in seos: - template_id = pymisp.get_object_template_id(s.template_uuid) - r = pymisp.add_object(args.event, template_id, s) + r = pymisp.add_object(args.event, s) if peo: - template_id = pymisp.get_object_template_id(peo.template_uuid) - r = pymisp.add_object(args.event, template_id, peo) + if hasattr(peo, 'certificates') and hasattr(peo, 'signers'): + # special authenticode case for PE objects + for c in peo.certificates: + pymisp.add_object(args.event, c, pythonify=True) + for s in peo.signers: + pymisp.add_object(args.event, s, pythonify=True) + del peo.certificates + del peo.signers + del peo.sections + r = pymisp.add_object(args.event, peo, pythonify=True) for ref in peo.ObjectReference: r = pymisp.add_object_reference(ref) if fo: - template_id = pymisp.get_object_template_id(fo.template_uuid) - response = pymisp.add_object(args.event, template_id, fo) + response = pymisp.add_object(args.event, fo, pythonify=True) for ref in fo.ObjectReference: r = pymisp.add_object_reference(ref) diff --git a/examples/add_filetype_object_from_csv.py b/examples/add_filetype_object_from_csv.py new file mode 100644 index 0000000..7468b7e --- /dev/null +++ b/examples/add_filetype_object_from_csv.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import csv +from pymisp import ExpandedPyMISP, MISPObject +from keys import misp_url, misp_key, misp_verifycert +import argparse + + +""" + +Sample usage: + +python3 ./add_filetype_object_from_csv.py -e 77bcc9f4-21a8-4252-9353-f4615d6121e3 -f ./attributes.csv + + +Attribute csv file (2 lines. Each line will be a file MISP Object): + +test.pdf;6ff19f8b680df260883d61d7c00db14a8bc57aa0;ea307d60ad0bd1df83ab5119df0bf638;b6c9903c9c38400345ad21faa2df50211d8878c96079c43ae64f35b17c9f74a1 +test2.xml;0dcef3d68f43e2badb0bfe3d47fd19633264cd1d;15f453625882f6123e239c9ce2b0fe24;b064514fcc52a769e064c4d61ce0c554fbc81e446af31dddac810879a5ca5b17 + +""" + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Create a file type MISP Object starting from attributes in a csv file') + parser.add_argument("-e", "--event_uuid", required=True, help="Event UUID to update") + parser.add_argument("-f", "--attr_file", required=True, help="Attribute CSV file path") + args = parser.parse_args() + + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + + f = open(args.attr_file, newline='') + csv_reader = csv.reader(f, delimiter=";") + + for line in csv_reader: + filename = line[0] + sha1 = line[1] + md5 = line[2] + sha256 = line[3] + + misp_object = MISPObject(name='file', filename=filename) + obj1 = misp_object.add_attribute("filename", value = filename) + obj1.add_tag('tlp:green') + obj2 = misp_object.add_attribute("sha1", value = sha1) + obj2.add_tag('tlp:amber') + obj3 = misp_object.add_attribute("md5", value = md5) + obj3.add_tag('tlp:amber') + obj4 = misp_object.add_attribute("sha256", value = sha256) + obj4.add_tag('tlp:amber') + r = pymisp.add_object(args.event_uuid, misp_object) + print(line) + print("\nObjects created :)") diff --git a/examples/add_generic_object.py b/examples/add_generic_object.py index 86a7675..ecaae0f 100755 --- a/examples/add_generic_object.py +++ b/examples/add_generic_object.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import json -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from pymisp.tools import GenericObjectGenerator from keys import misp_url, misp_key, misp_verifycert import argparse @@ -19,21 +19,8 @@ if __name__ == '__main__': parser.add_argument("-l", "--attr_list", required=True, help="List of attributes") args = parser.parse_args() - pymisp = PyMISP(misp_url, misp_key, misp_verifycert) - template = pymisp.get_object_templates_list() - if 'response' in template.keys(): - template = template['response'] - try: - template_ids = [x['ObjectTemplate']['id'] for x in template if x['ObjectTemplate']['name'] == args.type] - if len(template_ids) > 0: - template_id = template_ids[0] - else: - raise IndexError - except IndexError: - valid_types = ", ".join([x['ObjectTemplate']['name'] for x in template]) - print ("Template for type %s not found! Valid types are: %s" % (args.type, valid_types)) - exit() + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) misp_object = GenericObjectGenerator(args.type.replace("|", "-")) misp_object.generate_attributes(json.loads(args.attr_list)) - r = pymisp.add_object(args.event, template_id, misp_object) + r = pymisp.add_object(args.event, misp_object) diff --git a/examples/add_github_user.py b/examples/add_github_user.py new file mode 100755 index 0000000..07b8abb --- /dev/null +++ b/examples/add_github_user.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import PyMISP +from pymisp import MISPObject +from pymisp.tools import update_objects +from keys import misp_url, misp_key, misp_verifycert +import argparse +import requests +import sys + + +""" + +usage: add_github_user.py [-h] -e EVENT [-f] -u USERNAME + +Fetch GitHub user details and add it in object in MISP + +optional arguments: + -h, --help show this help message and exit + -e EVENT, --event EVENT + Event ID to update + -f, --force-template-update + -u USERNAME, --username USERNAME + GitHub username to add +""" + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Fetch GitHub user details and add it in object in MISP') + parser.add_argument("-e", "--event", required=True, help="Event ID to update") + parser.add_argument("-f", "--force-template-update", required=False, action="store_true") + parser.add_argument("-u", "--username", required=True, help="GitHub username to add") + args = parser.parse_args() + + r = requests.get("https://api.github.com/users/{}".format(args.username)) + if r.status_code != 200: + sys.exit("HTTP return is {} and not 200 as expected".format(r.status_code)) + if args.force_template_update: + print("Updating MISP Object templates...") + update_objects() + pymisp = PyMISP(misp_url, misp_key, misp_verifycert) + + misp_object = MISPObject(name="github-user") + github_user = r.json() + rfollowers = requests.get(github_user['followers_url']) + followers = rfollowers.json() + rfollowing = requests.get("https://api.github.com/users/{}/following".format(args.username)) + followings = rfollowing.json() + rkeys = requests.get("https://api.github.com/users/{}/keys".format(args.username)) + keys = rkeys.json() + misp_object.add_attributes("follower", *[follower['login'] for follower in followers]) + misp_object.add_attributes("following", *[following['login'] for following in followings]) + misp_object.add_attributes("ssh-public-key", *[sshkey['key'] for sshkey in keys]) + misp_object.add_attribute('bio', github_user['bio']) + misp_object.add_attribute('link', github_user['html_url']) + misp_object.add_attribute('user-fullname', github_user['name']) + misp_object.add_attribute('username', github_user['login']) + misp_object.add_attribute('twitter_username', github_user['twitter_username']) + misp_object.add_attribute('location', github_user['location']) + misp_object.add_attribute('company', github_user['company']) + misp_object.add_attribute('public_gists', github_user['public_gists']) + misp_object.add_attribute('public_repos', github_user['public_repos']) + misp_object.add_attribute('blog', github_user['blog']) + misp_object.add_attribute('node_id', github_user['node_id']) + retcode = pymisp.add_object(args.event, misp_object) diff --git a/examples/add_gitlab_user.py b/examples/add_gitlab_user.py new file mode 100755 index 0000000..a88cd08 --- /dev/null +++ b/examples/add_gitlab_user.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import PyMISP +from pymisp import MISPObject +from pymisp.tools import update_objects +from keys import misp_url, misp_key, misp_verifycert +import argparse +import requests +import sys + +""" +usage: add_gitlab_user.py [-h] -e EVENT [-f] -u USERNAME [-l LINK] + +Fetch GitLab user details and add it in object in MISP + +optional arguments: + -h, --help show this help message and exit + -e EVENT, --event EVENT + Event ID to update + -f, --force-template-update + -u USERNAME, --username USERNAME + GitLab username to add + -l LINK, --link LINK Url to access the GitLab instance, Default is + www.gitlab.com. +""" + +default_url = "http://www.gitlab.com/" + +parser = argparse.ArgumentParser(description='Fetch GitLab user details and add it in object in MISP') +parser.add_argument("-e", "--event", required=True, help="Event ID to update") +parser.add_argument("-f", "--force-template-update", required=False, action="store_true") +parser.add_argument("-u", "--username", required=True, help="GitLab username to add") +parser.add_argument("-l", "--link", required=False, help="Url to access the GitLab instance, Default is www.gitlab.com.", default=default_url) +args = parser.parse_args() + + +r = requests.get("{}api/v4/users?username={}".format(args.link, args.username)) +if r.status_code != 200: + sys.exit("HTTP return is {} and not 200 as expected".format(r.status_code)) +if args.force_template_update: + print("Updating MISP Object templates...") + update_objects() + +gitlab_user = r.json()[0] +pymisp = PyMISP(misp_url, misp_key, misp_verifycert) +print(gitlab_user) + +misp_object = MISPObject(name="gitlab-user") +misp_object.add_attribute('username', gitlab_user['username']) +misp_object.add_attribute('id', gitlab_user['id']) +misp_object.add_attribute('name', gitlab_user['name']) +misp_object.add_attribute('state', gitlab_user['state']) +misp_object.add_attribute('avatar_url', gitlab_user['avatar_url']) +misp_object.add_attribute('web_url', gitlab_user['web_url']) +retcode = pymisp.add_object(args.event, misp_object) diff --git a/examples/add_named_attribute.py b/examples/add_named_attribute.py index ac494fd..4bbcd97 100755 --- a/examples/add_named_attribute.py +++ b/examples/add_named_attribute.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse @@ -12,9 +12,6 @@ except NameError: pass -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', debug=True) - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Add an attribute to an event') parser.add_argument("-e", "--event", help="The id, uuid or json of the event to update.") @@ -22,7 +19,7 @@ if __name__ == '__main__': parser.add_argument("-v", "--value", help="The value of the attribute") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - event = misp.add_named_attribute(args.event, args.type, args.value) + event = misp.add_attribute(args.event, {'type': args.type, 'value': args.value}, pythonify=True) print(event) diff --git a/examples/add_organisations.py b/examples/add_organisations.py new file mode 100644 index 0000000..e8bc57a --- /dev/null +++ b/examples/add_organisations.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import ExpandedPyMISP, MISPOrganisation, MISPSharingGroup +from keys import misp_url, misp_key, misp_verifycert +import argparse +import csv + + +# Suppress those "Unverified HTTPS request is being made" +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Add organizations from a CSV file') + parser.add_argument("-c", "--csv-import", required=True, help="The CSV file containing the organizations. Format 'orgname,nationality,sector,type,contacts,uuid,local,sharingroup_uuid'") + args = parser.parse_args() + + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + + # CSV format + # orgname,nationality,sector,type,contacts,uuid,local,sharingroup + with open(args.csv_import) as csv_file: + count_orgs = 0 + csv_reader = csv.reader(csv_file, delimiter=',') + for row in csv_reader: + + org = MISPOrganisation() + org.name = row[0] + print("Process {}".format(org.name)) + org.nationality = row[1] + org.sector = row[2] + org.type = row[3] + org.contacts = row[4] + org.uuid = row[5] + org.local = row[6] + + add_org = misp.add_organisation(org, pythonify=True) + + if 'errors' in add_org: + print(add_org['errors']) + else: + count_orgs = count_orgs + 1 + org_uuid = add_org.uuid + + if org_uuid: + sharinggroup = MISPSharingGroup() + sharinggroup_uuid = row[7] + + if sharinggroup_uuid: + sharinggroup.uuid = sharinggroup_uuid + add_sharing = misp.add_org_to_sharing_group(sharinggroup, org) + else: + print("Organisation {} not added to sharing group, missing sharing group uuid".format(org.name)) + + print("Import finished, {} organisations added".format(count_orgs)) diff --git a/examples/add_sbsignature.py b/examples/add_sbsignature.py deleted file mode 100644 index 5a03068..0000000 --- a/examples/add_sbsignature.py +++ /dev/null @@ -1,16 +0,0 @@ -import json -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -from pymisp.tools import SBSignatureObject - -pymisp = PyMISP(misp_url, misp_key, misp_verifycert) -a = json.loads('{"signatures":[{"new_data":[],"confidence":100,"families":[],"severity":1,"weight":0,"description":"AttemptstoconnecttoadeadIP:Port(2uniquetimes)","alert":false,"references":[],"data":[{"IP":"95.101.39.58:80(Europe)"},{"IP":"192.35.177.64:80(UnitedStates)"}],"name":"dead_connect"},{"new_data":[],"confidence":30,"families":[],"severity":2,"weight":1,"description":"PerformssomeHTTPrequests","alert":false,"references":[],"data":[{"url":"http://cert.int-x3.letsencrypt.org/"},{"url":"http://apps.identrust.com/roots/dstrootcax3.p7c"}],"name":"network_http"},{"new_data":[],"confidence":100,"families":[],"severity":2,"weight":1,"description":"Theofficefilehasaunconventionalcodepage:ANSICyrillic;Cyrillic(Windows)","alert":false,"references":[],"data":[],"name":"office_code_page"}]}') -a = [(x['name'], x['description']) for x in a["signatures"]] - - -b = SBSignatureObject(a) - - -template_id = [x['ObjectTemplate']['id'] for x in pymisp.get_object_templates_list() if x['ObjectTemplate']['name'] == 'sb-signature'][0] - -pymisp.add_object(234111, template_id, b) diff --git a/examples/add_ssh_authorized_keys.py b/examples/add_ssh_authorized_keys.py new file mode 100755 index 0000000..68d818c --- /dev/null +++ b/examples/add_ssh_authorized_keys.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pymisp import ExpandedPyMISP +from pymisp.tools import SSHAuthorizedKeysObject +import traceback +from keys import misp_url, misp_key, misp_verifycert +import glob +import argparse + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Extract indicators out of authorized_keys file.') + parser.add_argument("-e", "--event", required=True, help="Event ID to update.") + parser.add_argument("-p", "--path", required=True, help="Path to process (expanded using glob).") + args = parser.parse_args() + + pymisp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=True) + + for f in glob.glob(args.path): + try: + auth_keys = SSHAuthorizedKeysObject(f) + except Exception: + traceback.print_exc() + continue + + response = pymisp.add_object(args.event, auth_keys, pythonify=True) + for ref in auth_keys.ObjectReference: + r = pymisp.add_object_reference(ref) diff --git a/examples/add_user.py b/examples/add_user.py index f18e7c4..c50b29a 100755 --- a/examples/add_user.py +++ b/examples/add_user.py @@ -1,20 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPUser from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Add a new user by setting the mandory fields.') parser.add_argument("-e", "--email", required=True, help="Email linked to the account.") @@ -22,6 +12,11 @@ if __name__ == '__main__': parser.add_argument("-r", "--role_id", required=True, help="Role linked to the user.") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, 'json') - print (misp.add_user(args.email, args.org_id, args.role_id)) + user = MISPUser() + user.email = args.email + user.org_id = args.org_id + user.role_id = args.role_id + + print(misp.add_user(user, pythonify=True)) diff --git a/examples/add_user_json.py b/examples/add_user_json.py deleted file mode 100755 index 759b26f..0000000 --- a/examples/add_user_json.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Add the user described in the given json. If no file is provided, returns a json listing all the fields used to describe a user.') - parser.add_argument("-f", "--json_file", help="The name of the json file describing the user you want to create.") - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - if args.json_file is None: - print (misp.get_add_user_fields_list()) - else: - print(misp.add_user_json(args.json_file)) diff --git a/examples/addtag.py b/examples/addtag.py deleted file mode 100755 index 5cecf28..0000000 --- a/examples/addtag.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse -import os -import json - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - - result = m.get_event(event) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Get an event from a MISP instance.') - parser.add_argument("-e", "--event", required=True, help="Event ID to get.") - parser.add_argument("-a", "--attribute", help="Attribute ID to modify. A little dirty for now, argument need to be included in event") - parser.add_argument("-t", "--tag", required=True, type=int, help="Attribute ID to modify.") - parser.add_argument("-m", "--modify_attribute", action='store_true', help="If set, the tag will be add to the attribute, otherwise to the event.") - - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - event = misp.get_event(args.event) - if args.modify_attribute: - for temp in event['Event']['Attribute']: - if temp['id'] == args.attribute: - attribute = temp - break - - misp.add_tag(attribute, args.tag, attribute=True) - else: - misp.add_tag(event['Event'], args.tag) diff --git a/examples/addtag2.py b/examples/addtag2.py index fa9b89b..321210a 100755 --- a/examples/addtag2.py +++ b/examples/addtag2.py @@ -3,21 +3,18 @@ from pymisp import PyMISP from keys import misp_url, misp_key, misp_verifycert import argparse -import os -import json def init(url, key): return PyMISP(url, key, misp_verifycert, 'json') - result = m.get_event(event) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Tag something.') parser.add_argument("-u", "--uuid", help="UUID to tag.") parser.add_argument("-e", "--event", help="Event ID to tag.") parser.add_argument("-a", "--attribute", help="Attribute ID to tag") - parser.add_argument("-t", "--tag", required=True, help="Attribute ID to modify.") + parser.add_argument("-t", "--tag", required=True, help="Tag ID.") args = parser.parse_args() if not args.event and not args.uuid and not args.attribute: @@ -26,12 +23,9 @@ if __name__ == '__main__': misp = init(misp_url, misp_key) - event = misp.get_event(args.event) - if args.event and not args.attribute: result = misp.search(eventid=args.event) - data = result['response'] - for event in data: + for event in result: uuid = event['Event']['uuid'] if args.attribute: @@ -39,8 +33,7 @@ if __name__ == '__main__': print("Please provide event ID also") exit() result = misp.search(eventid=args.event) - data = result['response'] - for event in data: + for event in result: for attribute in event['Event']['Attribute']: if attribute["id"] == args.attribute: uuid = attribute["uuid"] @@ -48,5 +41,5 @@ if __name__ == '__main__': if args.uuid: uuid = args.uuid - print("UUID tagged: %s"%uuid) + print("UUID tagged: %s" % uuid) misp.tag(uuid, args.tag) diff --git a/examples/admin/setup_sync.py b/examples/admin/setup_sync.py new file mode 100644 index 0000000..14e7374 --- /dev/null +++ b/examples/admin/setup_sync.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pymisp import PyMISP +import sys +import json + + +# NOTE: the user of the API key *need to be a sync user* +remote_url = 'https://misp.remote' +remote_api_key = 'REMOTE KEY FOR SYNC USER' +remote_verify = True + +# NOTE: the user of the API key *need to be an admin* +own_url = 'https://misp.own' +own_api_key = 'OWN KEY FOR ADMIN USER' +own_verify = True + + +remote_misp = PyMISP(url=remote_url, key=remote_api_key, ssl=remote_verify) +sync_config = remote_misp.get_sync_config() + +if 'errors' in sync_config: + print('Sumething went wrong:') + print(json.dumps(sync_config, indent=2)) + sys.exit(1) +else: + print('Sucessfully got a sync config:') + print(json.dumps(sync_config, indent=2)) + +own_misp = PyMISP(url=own_url, key=own_api_key, ssl=own_verify) +response = own_misp.import_server(sync_config) + +if 'errors' in response: + print('Sumething went wrong:') + print(json.dumps(response, indent=2)) + sys.exit(1) +else: + print('Sucessfully added the sync config:') + print(json.dumps(response, indent=2)) diff --git a/examples/cache_all.py b/examples/cache_all.py index 00e3eea..4a3fa02 100755 --- a/examples/cache_all.py +++ b/examples/cache_all.py @@ -2,13 +2,9 @@ # -*- coding: utf-8 -*- from keys import misp_url, misp_key, misp_verifycert -from pymisp import PyMISP - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') +from pymisp import ExpandedPyMISP if __name__ == '__main__': - misp = init(misp_url, misp_key) - misp.cache_all_feeds() \ No newline at end of file + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + misp.cache_all_feeds() diff --git a/examples/climate/scrippsco2.py b/examples/climate/scrippsco2.py new file mode 100644 index 0000000..08cdc86 --- /dev/null +++ b/examples/climate/scrippsco2.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import datetime +from dateutil.parser import parse +import csv +from pathlib import Path +import json +from uuid import uuid4 +import requests + +from pymisp import MISPEvent, MISPObject, MISPTag, MISPOrganisation +from pymisp.tools import feed_meta_generator + + +class Scrippts: + + def __init__(self, output_dir: str= 'output', org_name: str='CIRCL', + org_uuid: str='55f6ea5e-2c60-40e5-964f-47a8950d210f'): + self.misp_org = MISPOrganisation() + self.misp_org.name = org_name + self.misp_org.uuid = org_uuid + + self.output_dir = Path(output_dir) + self.output_dir.mkdir(exist_ok=True) + + self.data_dir = self.output_dir / 'data' + self.data_dir.mkdir(exist_ok=True) + + self.scrippts_meta_file = self.output_dir / '.meta_scrippts' + self.scrippts_meta = {} + if self.scrippts_meta_file.exists(): + # Format: ,.json + with self.scrippts_meta_file.open() as f: + reader = csv.reader(f) + for row in reader: + self.scrippts_meta[row[0]] = row[1] + else: + self.scrippts_meta_file.touch() + + def geolocation_alt(self) -> MISPObject: + # Alert, NWT, Canada + location = MISPObject('geolocation', standalone=False) + location.add_attribute('latitude', 82.3) + location.add_attribute('longitude', 62.3) + location.add_attribute('altitude', 210) + location.add_attribute('text', 'Alert, NWT, Canada') + return location + + def tag_alt(self) -> MISPTag: + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:ALT' + return tag + + def geolocation_ptb(self): + # Point Barrow, Alaska + location = MISPObject('geolocation') + location.add_attribute('latitude', 71.3) + location.add_attribute('longitude', 156.6) + location.add_attribute('altitude', 11) + location.add_attribute('text', 'Point Barrow, Alaska') + return location + + def tag_ptb(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:PTB' + return tag + + def geolocation_stp(self) -> MISPObject: + # Station P + location = MISPObject('geolocation') + location.add_attribute('latitude', 50) + location.add_attribute('longitude', 145) + location.add_attribute('altitude', 0) + location.add_attribute('text', 'Station P') + return location + + def tag_stp(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:STP' + return tag + + def geolocation_ljo(self) -> MISPObject: + # La Jolla Pier, California + location = MISPObject('geolocation') + location.add_attribute('latitude', 32.9) + location.add_attribute('longitude', 117.3) + location.add_attribute('altitude', 10) + location.add_attribute('text', 'La Jolla Pier, California') + return location + + def tag_ljo(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:LJO' + return tag + + def geolocation_bcs(self) -> MISPObject: + # Baja California Sur, Mexico + location = MISPObject('geolocation') + location.add_attribute('latitude', 23.3) + location.add_attribute('longitude', 110.2) + location.add_attribute('altitude', 4) + location.add_attribute('text', 'Baja California Sur, Mexico') + return location + + def tag_bcs(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:BCS' + return tag + + def geolocation_mlo(self) -> MISPObject: + # Mauna Loa Observatory, Hawaii + location = MISPObject('geolocation') + location.add_attribute('latitude', 19.5) + location.add_attribute('longitude', 155.6) + location.add_attribute('altitude', 3397) + location.add_attribute('text', 'Mauna Loa Observatory, Hawaii') + return location + + def tag_mlo(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:MLO' + return tag + + def geolocation_kum(self) -> MISPObject: + # Cape Kumukahi, Hawaii + location = MISPObject('geolocation') + location.add_attribute('latitude', 19.5) + location.add_attribute('longitude', 154.8) + location.add_attribute('altitude', 3) + location.add_attribute('text', 'Cape Kumukahi, Hawaii') + return location + + def tag_kum(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:KUM' + return tag + + def geolocation_chr(self): + # Christmas Island, Fanning Island + location = MISPObject('geolocation') + location.add_attribute('latitude', 2) + location.add_attribute('longitude', 157.3) + location.add_attribute('altitude', 2) + location.add_attribute('text', 'Christmas Island, Fanning Island') + return location + + def tag_chr(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:CHR' + return tag + + def geolocation_sam(self): + # American Samoa + location = MISPObject('geolocation') + location.add_attribute('latitude', 14.2) + location.add_attribute('longitude', 170.6) + location.add_attribute('altitude', 30) + location.add_attribute('text', 'American Samoa') + return location + + def tag_sam(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:SAM' + return tag + + def geolocation_ker(self): + # Kermadec Islands, Raoul Island + location = MISPObject('geolocation') + location.add_attribute('latitude', 29.2) + location.add_attribute('longitude', 177.9) + location.add_attribute('altitude', 2) + location.add_attribute('text', 'Kermadec Islands, Raoul Island') + return location + + def tag_ker(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:KER' + return tag + + def geolocation_nzd(self): + # Baring Head, New Zealand + location = MISPObject('geolocation') + location.add_attribute('latitude', 41.4) + location.add_attribute('longitude', 174.9) + location.add_attribute('altitude', 85) + location.add_attribute('text', 'Baring Head, New Zealand') + return location + + def tag_nzd(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:NZD' + return tag + + def geolocation_psa(self): + # Palmer Station, Antarctica + location = MISPObject('geolocation') + location.add_attribute('latitude', 64.9) + location.add_attribute('longitude', 64) + location.add_attribute('altitude', 10) + location.add_attribute('text', 'Palmer Station, Antarctica') + return location + + def tag_psa(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:PSA' + return tag + + def geolocation_spo(self): + # South Pole + location = MISPObject('geolocation') + location.add_attribute('latitude', 90) + location.add_attribute('longitude', 0) + location.add_attribute('altitude', 2810) + location.add_attribute('text', 'South Pole') + return location + + def tag_spo(self): + tag = MISPTag() + tag.name = 'scrippsco2-sampling-stations:SPO' + return tag + + def fetch(self, url): + filepath = self.data_dir / Path(url).name + r = requests.get(url) + if r.status_code != 200 or r.text[0] != '"': + print(url) + return False + with filepath.open('w') as f: + f.write(r.text) + return filepath + + def import_all(self, stations_short_names, interval, data_type): + object_creator = getattr(self, f'{interval}_flask_{data_type}') + if data_type == 'co2': + base_url = 'https://scrippsco2.ucsd.edu/assets/data/atmospheric/stations/flask_co2/' + elif data_type in ['c13', 'o18']: + base_url = 'https://scrippsco2.ucsd.edu/assets/data/atmospheric/stations/flask_isotopic/' + for station in stations_short_names: + url = f'{base_url}/{interval}/{interval}_flask_{data_type}_{station}.csv' + infofield = f'[{station.upper()}] {interval} average atmospheric {data_type} concentrations' + filepath = self.fetch(url) + if not filepath: + continue + if infofield in self.scrippts_meta: + event = MISPEvent() + event.load_file(str(self.output_dir / self.scrippts_meta[infofield])) + location = event.get_objects_by_name('geolocation')[0] + update = True + else: + event = MISPEvent() + event.uuid = str(uuid4()) + event.info = infofield + event.Orgc = self.misp_org + event.add_tag(getattr(self, f'tag_{station}')()) + location = getattr(self, f'geolocation_{station}')() + event.add_object(location) + event.add_attribute('link', f'https://scrippsco2.ucsd.edu/data/atmospheric_co2/{station}') + update = False + with self.scrippts_meta_file.open('a') as f: + writer = csv.writer(f) + writer.writerow([infofield, f'{event.uuid}.json']) + + object_creator(event, location, filepath, update) + if update: + # Bump the publish timestamp + event.publish_timestamp = datetime.datetime.timestamp(datetime.datetime.now()) + feed_output = event.to_feed(with_meta=False) + with (self.output_dir / f'{event.uuid}.json').open('w') as f: + # json.dump(feed_output, f, indent=2, sort_keys=True) # For testing + json.dump(feed_output, f) + + def import_monthly_co2_all(self): + to_import = ['alt', 'ptb', 'stp', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd'] + self.import_all(to_import, 'monthly', 'co2') + + def import_monthly_c13_all(self): + to_import = ['alt', 'ptb', 'stp', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd', 'psa', 'spo'] + self.import_all(to_import, 'monthly', 'c13') + + def import_monthly_o18_all(self): + to_import = ['alt', 'ptb', 'stp', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd', 'spo'] + self.import_all(to_import, 'monthly', 'o18') + + def import_daily_co2_all(self): + to_import = ['alt', 'ptb', 'stp', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd'] + self.import_all(to_import, 'daily', 'co2') + + def import_daily_c13_all(self): + to_import = ['alt', 'ptb', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd', 'spo'] + self.import_all(to_import, 'daily', 'c13') + + def import_daily_o18_all(self): + to_import = ['alt', 'ptb', 'ljo', 'bcs', 'mlo', 'kum', 'chr', 'sam', 'ker', 'nzd', 'spo'] + self.import_all(to_import, 'daily', 'o18') + + def split_data_comment(self, csv_file, update, event): + comment = '' + data = [] + with csv_file.open() as f: + for line in f: + if line[0] == '"': + if update: + continue + if '----------' in line: + event.add_attribute('comment', comment, disable_correlation=True) + comment = '' + continue + comment += line[1:-1].strip() + else: + data.append(line) + if not update: + event.add_attribute('comment', comment, disable_correlation=True) + return data + + def monthly_flask_co2(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-co2-monthly'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + if not row[0].isdigit(): + # This file has fucked up headers + continue + sample_date = parse(f'{row[0]}-{row[1]}-16T00:00:00') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-co2-monthly', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('monthly-co2', float(row[4])) + obj.add_attribute('monthly-co2-seasonal-adjustment', float(row[5])) + obj.add_attribute('monthly-co2-smoothed', float(row[6])) + obj.add_attribute('monthly-co2-smoothed-seasonal-adjustment', float(row[7])) + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + def monthly_flask_c13(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-c13-monthly'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + if not row[0].isdigit(): + # This file has fucked up headers + continue + sample_date = parse(f'{row[0]}-{row[1]}-16T00:00:00') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-c13-monthly', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('monthly-c13', float(row[4])) + obj.add_attribute('monthly-c13-seasonal-adjustment', float(row[5])) + obj.add_attribute('monthly-c13-smoothed', float(row[6])) + obj.add_attribute('monthly-c13-smoothed-seasonal-adjustment', float(row[7])) + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + def monthly_flask_o18(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-o18-monthly'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + if not row[0].isdigit(): + # This file has fucked up headers + continue + sample_date = parse(f'{row[0]}-{row[1]}-16T00:00:00') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-o18-monthly', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('monthly-o18', float(row[4])) + obj.add_attribute('monthly-o18-seasonal-adjustment', float(row[5])) + obj.add_attribute('monthly-o18-smoothed', float(row[6])) + obj.add_attribute('monthly-o18-smoothed-seasonal-adjustment', float(row[7])) + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + def daily_flask_co2(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-co2-daily'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + sample_date = parse(f'{row[0]}-{row[1]}') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-co2-daily', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('number-flask', int(row[4])) + obj.add_attribute('flag', int(row[5])) + attr = obj.add_attribute('co2-value', float(row[6])) + attr.add_tag(f'scrippsco2-fgc:{int(row[5])}') + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + def daily_flask_c13(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-c13-daily'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + sample_date = parse(f'{row[0]}-{row[1]}') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-c13-daily', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('number-flask', int(row[4])) + obj.add_attribute('flag', int(row[5])) + attr = obj.add_attribute('c13-value', float(row[6])) + attr.add_tag(f'scrippsco2-fgi:{int(row[5])}') + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + def daily_flask_o18(self, event, location, csv_file, update): + data = self.split_data_comment(csv_file, update, event) + + dates_already_imported = [] + if update: + # get all datetime from existing event + for obj in event.get_objects_by_name('scrippsco2-o18-daily'): + date_attribute = obj.get_attributes_by_relation('sample-datetime')[0] + dates_already_imported.append(date_attribute.value) + + reader = csv.reader(data) + for row in reader: + sample_date = parse(f'{row[0]}-{row[1]}') + if sample_date in dates_already_imported: + continue + obj = MISPObject('scrippsco2-o18-daily', standalone=False) + obj.add_attribute('sample-datetime', sample_date) + obj.add_attribute('sample-date-excel', float(row[2])) + obj.add_attribute('sample-date-fractional', float(row[3])) + obj.add_attribute('number-flask', int(row[4])) + obj.add_attribute('flag', int(row[5])) + attr = obj.add_attribute('o18-value', float(row[6])) + attr.add_tag(f'scrippsco2-fgi:{int(row[5])}') + obj.add_reference(location, 'sampling-location') + event.add_object(obj) + + +if __name__ == '__main__': + output_dir = 'scrippsco2_feed' + + i = Scrippts(output_dir=output_dir) + i.import_daily_co2_all() + i.import_daily_c13_all() + i.import_daily_o18_all() + i.import_monthly_co2_all() + i.import_monthly_c13_all() + i.import_monthly_o18_all() + + feed_meta_generator(Path(output_dir)) diff --git a/examples/copyTagsFromAttributesToEvent.py b/examples/copyTagsFromAttributesToEvent.py new file mode 100755 index 0000000..68eee9d --- /dev/null +++ b/examples/copyTagsFromAttributesToEvent.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import PyMISP +from keys import misp_url, misp_key, misp_verifycert +import argparse +import os + +SILENT = False + + +def getTagToApplyToEvent(event): + tags_to_apply = set() + + event_tags = { tag.name for tag in event.tags } + for galaxy in event.galaxies: + for cluster in galaxy.clusters: + event_tags.add(cluster.tag_name) + + for attribute in event.attributes: + for attribute_tag in attribute.tags: + if attribute_tag.name not in event_tags: + tags_to_apply.add(attribute_tag.name) + + return tags_to_apply + + +def TagEvent(event, tags_to_apply): + for tag in tags_to_apply: + event.add_tag(tag) + return event + + +def condPrint(text): + if not SILENT: + print(text) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Get an event from a MISP instance.') + parser.add_argument("-e", "--event", required=True, help="Event ID to get.") + parser.add_argument("-y", "--yes", required=False, default=False, action='store_true', help="Automatically accept prompt.") + parser.add_argument("-s", "--silent", required=False, default=False, action='store_true', help="No output to stdin.") + + args = parser.parse_args() + SILENT = args.silent + + misp = PyMISP(misp_url, misp_key, misp_verifycert) + + event = misp.get_event(args.event, pythonify=True) + tags_to_apply = getTagToApplyToEvent(event) + condPrint('Tag to apply at event level:') + for tag in tags_to_apply: + condPrint(f'- {tag}') + + confirmed = False + if args.yes: + confirmed = True + else: + confirm = input('Confirm [Y/n]: ') + confirmed = len(confirm) == 0 or confirm == 'Y' or confirm == 'y' + if confirmed: + event = TagEvent(event, tags_to_apply) + condPrint(f'Updating event {args.event}') + misp.update_event(event) + condPrint(f'Event {args.event} tagged with {len(tags_to_apply)} tags') + else: + condPrint('Operation cancelled') diff --git a/examples/covid19/import_csse_covid19_daily.py b/examples/covid19/import_csse_covid19_daily.py new file mode 100755 index 0000000..4d3561f --- /dev/null +++ b/examples/covid19/import_csse_covid19_daily.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +from csv import DictReader +from pymisp import MISPEvent, MISPOrganisation, PyMISP, MISPObject +from datetime import datetime +from dateutil.parser import parse +import json +from pymisp.tools import feed_meta_generator +from io import BytesIO +from collections import defaultdict + +make_feed = False + +aggregate_by_country = True + +path = Path('/home/raphael/gits/COVID-19/csse_covid_19_data/csse_covid_19_daily_reports/') + + +def get_country_region(row): + if 'Country/Region' in row: + return row['Country/Region'] + elif 'Country_Region' in row: + return row['Country_Region'] + else: + print(p, row.keys()) + raise Exception() + + +def get_last_update(row): + if 'Last_Update' in row: + return parse(row['Last_Update']) + elif 'Last Update' in row: + return parse(row['Last Update']) + else: + print(p, row.keys()) + raise Exception() + + +def add_detailed_object(obj, row): + if 'Province/State' in row: + if row['Province/State']: + obj.add_attribute('province-state', row['Province/State']) + elif '\ufeffProvince/State' in row: + if row['\ufeffProvince/State']: + obj.add_attribute('province-state', row['\ufeffProvince/State']) + elif 'Province_State' in row: + if row['Province_State']: + obj.add_attribute('province-state', row['Province_State']) + else: + print(p, row.keys()) + raise Exception() + + obj.add_attribute('country-region', get_country_region(row)) + + obj.add_attribute('update', get_last_update(row)) + + if 'Lat' in row: + obj.add_attribute('latitude', row['Lat']) + + if 'Long_' in row: + obj.add_attribute('longitude', row['Long_']) + elif 'Long' in row: + obj.add_attribute('longitude', row['Long']) + + if row['Confirmed']: + obj.add_attribute('confirmed', int(row['Confirmed'])) + if row['Deaths']: + obj.add_attribute('death', int(row['Deaths'])) + if row['Recovered']: + obj.add_attribute('recovered', int(row['Recovered'])) + if 'Active' in row and row['Active']: + obj.add_attribute('active', int(row['Active'])) + + +def country_aggregate(aggregate, row): + c = get_country_region(row) + if c not in aggregate: + aggregate[c] = defaultdict(active=0, death=0, recovered=0, confirmed=0, update=datetime.fromtimestamp(0)) + if row['Confirmed']: + aggregate[c]['confirmed'] += int(row['Confirmed']) + if row['Deaths']: + aggregate[c]['death'] += int(row['Deaths']) + if row['Recovered']: + aggregate[c]['recovered'] += int(row['Recovered']) + if 'Active' in row and row['Active']: + aggregate[c]['active'] += int(row['Active']) + + update = get_last_update(row) + if update > aggregate[c]['update']: + aggregate[c]['update'] = update + + +if make_feed: + org = MISPOrganisation() + org.name = 'CIRCL' + org.uuid = "55f6ea5e-2c60-40e5-964f-47a8950d210f" +else: + from covid_key import url, key + misp = PyMISP(url, key) + +for p in path.glob('**/*.csv'): + d = datetime.strptime(p.name[:-4], '%m-%d-%Y').date() + event = MISPEvent() + if aggregate_by_country: + event.info = f"[{d.isoformat()}] CSSE COVID-19 daily report" + else: + event.info = f"[{d.isoformat()}] CSSE COVID-19 detailed daily report" + event.date = d + event.distribution = 3 + event.add_tag('tlp:white') + if make_feed: + event.orgc = org + else: + e = misp.search(eventinfo=event.info, metadata=True, pythonify=True) + if e: + # Already added. + continue + event.add_attribute('attachment', p.name, data=BytesIO(p.open('rb').read())) + event.add_attribute('link', f'https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_daily_reports/{p.name}', comment='Source') + if aggregate_by_country: + aggregate = defaultdict() + with p.open() as f: + reader = DictReader(f) + for row in reader: + if aggregate_by_country: + country_aggregate(aggregate, row) + else: + obj = MISPObject(name='covid19-csse-daily-report') + add_detailed_object(obj, row) + event.add_object(obj) + + if aggregate_by_country: + for country, values in aggregate.items(): + obj = event.add_object(name='covid19-csse-daily-report', standalone=False) + obj.add_attribute('country-region', country) + obj.add_attribute('update', values['update']) + obj.add_attribute('confirmed', values['confirmed']) + obj.add_attribute('death', values['death']) + obj.add_attribute('recovered', values['recovered']) + obj.add_attribute('active', values['active']) + + if make_feed: + with (Path('output') / f'{event.uuid}.json').open('w') as _w: + json.dump(event.to_feed(), _w) + else: + event = misp.add_event(event) + misp.publish(event) + +if make_feed: + feed_meta_generator(Path('output')) diff --git a/examples/covid19/import_dxy_covid19_live.py b/examples/covid19/import_dxy_covid19_live.py new file mode 100755 index 0000000..2d85768 --- /dev/null +++ b/examples/covid19/import_dxy_covid19_live.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +from pymisp import MISPEvent, MISPOrganisation, PyMISP +from dateutil.parser import parse +import json +from pymisp.tools import feed_meta_generator +from io import BytesIO + +make_feed = False + +path = Path('/home/raphael/gits/covid-19-china/data') + + +if make_feed: + org = MISPOrganisation() + org.name = 'CIRCL' + org.uuid = "55f6ea5e-2c60-40e5-964f-47a8950d210f" +else: + from covid_key import url, key + misp = PyMISP(url, key) + +for p in path.glob('*_json/current_china.json'): + d = parse(p.parent.name[:-5]) + event = MISPEvent() + event.info = f"[{d.isoformat()}] DXY COVID-19 live report" + event.date = d + event.distribution = 3 + event.add_tag('tlp:white') + if make_feed: + event.orgc = org + else: + e = misp.search(eventinfo=event.info, metadata=True, pythonify=True) + if e: + # Already added. + continue + event.add_attribute('attachment', p.name, data=BytesIO(p.open('rb').read())) + with p.open() as f: + data = json.load(f) + for province in data: + obj_province = event.add_object(name='covid19-dxy-live-province', standalone=False) + obj_province.add_attribute('province', province['provinceName']) + obj_province.add_attribute('update', d) + if province['currentConfirmedCount']: + obj_province.add_attribute('current-confirmed', province['currentConfirmedCount']) + if province['confirmedCount']: + obj_province.add_attribute('total-confirmed', province['confirmedCount']) + if province['curedCount']: + obj_province.add_attribute('total-cured', province['curedCount']) + if province['deadCount']: + obj_province.add_attribute('total-death', province['deadCount']) + if province['comment']: + obj_province.add_attribute('comment', province['comment']) + + for city in province['cities']: + obj_city = event.add_object(name='covid19-dxy-live-city', standalone=False) + obj_city.add_attribute('city', city['cityName']) + obj_city.add_attribute('update', d) + if city['currentConfirmedCount']: + obj_city.add_attribute('current-confirmed', city['currentConfirmedCount']) + if city['confirmedCount']: + obj_city.add_attribute('total-confirmed', city['confirmedCount']) + if city['curedCount']: + obj_city.add_attribute('total-cured', city['curedCount']) + if city['deadCount']: + obj_city.add_attribute('total-death', city['deadCount']) + obj_city.add_reference(obj_province, 'part-of') + + if make_feed: + with (Path('output') / f'{event.uuid}.json').open('w') as _w: + json.dump(event.to_feed(), _w) + else: + misp.add_event(event) + +if make_feed: + feed_meta_generator(Path('output')) diff --git a/examples/create_events.py b/examples/create_events.py index 89eb398..1d8c2b4 100755 --- a/examples/create_events.py +++ b/examples/create_events.py @@ -1,19 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPEvent from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', debug=True) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Create an event on MISP.') @@ -23,7 +14,13 @@ if __name__ == '__main__': parser.add_argument("-t", "--threat", type=int, help="The threat level ID of the newly created event, if applicable. [1-4]") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - event = misp.new_event(args.distrib, args.threat, args.analysis, args.info) + event = MISPEvent() + event.distribution = args.distrib + event.threat_level_id = args.threat + event.analysis = args.analysis + event.info = args.info + + event = misp.add_event(event, pythonify=True) print(event) diff --git a/examples/cytomic_orion.py b/examples/cytomic_orion.py new file mode 100755 index 0000000..2874b6a --- /dev/null +++ b/examples/cytomic_orion.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' +Koen Van Impe + +Cytomic Automation +Put this script in crontab to run every /15 or /60 + */15 * * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/cytomic_orion.py + + +Fetches the configuration set in the Cytomic Orion enrichment module +- events : upload events tagged with the 'upload' tag, all the attributes supported by Cytomic Orion +- upload : upload attributes flagged with the 'upload' tag (only attributes supported by Cytomic Orion) +- delete : delete attributes flagged with the 'upload' tag (only attributes supported by Cytomic Orion) + +''' + +from pymisp import ExpandedPyMISP +from keys import misp_url, misp_key, misp_verifycert +import argparse +import os +import re +import sys +import requests +import json +import urllib3 + + +def get_token(token_url, clientid, clientsecret, scope, grant_type, username, password): + ''' + Get oAuth2 token + Configuration settings are fetched first from the MISP module configu + ''' + + try: + if scope and grant_type and username and password: + data = {'scope': scope, 'grant_type': grant_type, 'username': username, 'password': password} + + if token_url and clientid and clientsecret: + access_token_response = requests.post(token_url, data=data, verify=False, allow_redirects=False, auth=(clientid, clientsecret)) + tokens = json.loads(access_token_response.text) + if 'access_token' in tokens: + access_token = tokens['access_token'] + return access_token + else: + sys.exit('No token received') + else: + sys.exit('No token_url, clientid or clientsecret supplied') + else: + sys.exit('No scope, grant_type, username or password supplied') + except Exception: + sys.exit('Unable to connect to token_url') + + +def get_config(url, key, misp_verifycert): + ''' + Get the module config and the settings needed to access the API + Also contains the settings to do the query + ''' + try: + misp_headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': key} + req = requests.get(url + 'servers/serverSettings.json', verify=misp_verifycert, headers=misp_headers) + if req.status_code == 200: + req_json = req.json() + if 'finalSettings' in req_json: + finalSettings = req_json['finalSettings'] + + clientid = clientsecret = scope = username = password = grant_type = api_url = token_url = '' + module_enabled = False + scope = 'orion.api' + grant_type = 'password' + limit_upload_events = 50 + limit_upload_attributes = 50 + ttlDays = "1" + last_attributes = '5d' + post_threat_level_id = 2 + for el in finalSettings: + # Is the module enabled? + if el['setting'] == 'Plugin.Enrichment_cytomic_orion_enabled': + module_enabled = el['value'] + if module_enabled is False: + break + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_clientid': + clientid = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_clientsecret': + clientsecret = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_username': + username = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_password': + password = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_api_url': + api_url = el['value'].replace('\\/', '/') + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_token_url': + token_url = el['value'].replace('\\/', '/') + elif el['setting'] == 'MISP.baseurl': + misp_baseurl = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_upload_threat_level_id': + if el['value']: + try: + post_threat_level_id = int(el['value']) + except: + continue + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_upload_ttlDays': + if el['value']: + try: + ttlDays = "{last_days}".format(last_days=int(el['value'])) + except: + continue + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_upload_timeframe': + if el['value']: + try: + last_attributes = "{last_days}d".format(last_days=int(el['value'])) + except: + continue + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_upload_tag': + upload_tag = el['value'] + elif el['setting'] == 'Plugin.Enrichment_cytomic_orion_delete_tag': + delete_tag = el['value'] + elif el['setting'] == 'Plugin.Enrichment_limit_upload_events': + if el['value']: + try: + limit_upload_events = "{limit_upload_events}".format(limit_upload_events=int(el['value'])) + except: + continue + elif el['setting'] == 'Plugin.Enrichment_limit_upload_attributes': + if el['value']: + try: + limit_upload_attributes = "{limit_upload_attributes}".format(limit_upload_attributes=int(el['value'])) + except: + continue + else: + sys.exit('Did not receive a 200 code from MISP') + + if module_enabled and api_url and token_url and clientid and clientsecret and username and password and grant_type: + + return {'cytomic_policy': 'Detect', + 'upload_timeframe': last_attributes, + 'upload_tag': upload_tag, + 'delete_tag': delete_tag, + 'upload_ttlDays': ttlDays, + 'post_threat_level_id': post_threat_level_id, + 'clientid': clientid, + 'clientsecret': clientsecret, + 'scope': scope, + 'username': username, + 'password': password, + 'grant_type': grant_type, + 'api_url': api_url, + 'token_url': token_url, + 'misp_baseurl': misp_baseurl, + 'limit_upload_events': limit_upload_events, + 'limit_upload_attributes': limit_upload_attributes} + else: + sys.exit('Did not receive all the necessary configuration information from MISP') + + except Exception as e: + sys.exit('Unable to get module config from MISP') + + +class cytomicobject: + misp = None + lst_evtid = None + lst_attuuid = None + lst_attuuid_error = None + endpoint_ioc = None + api_call_headers = None + post_data = None + args = None + tag = None + limit_events = None + limit_attributes = None + atttype_misp = None + atttype_cytomic = None + attlabel_cytomic = None + att_types = { + "ip-dst": {"ip": "ipioc"}, + "ip-src": {"ip": "ipioc"}, + "url": {"url": "urlioc"}, + "md5": {"hash": "filehashioc"}, + "domain": {"domain": "domainioc"}, + "hostname": {"domain": "domainioc"}, + "domain|ip": {"domain": "domainioc"}, + "hostname|port": {"domain": "domainioc"} + } + debug = True + error = False + res = False + res_msg = None + + +def collect_events_ids(cytomicobj, moduleconfig): + # Get events that contain Cytomic tag. + try: + evt_result = cytomicobj.misp.search(controller='events', limit=cytomicobj.limit_events, tags=cytomicobj.tag, last=moduleconfig['upload_timeframe'], published=True, deleted=False, pythonify=True) + cytomicobj.lst_evtid = ['x', 'y'] + for evt in evt_result: + evt = cytomicobj.misp.get_event(event=evt['id'], pythonify=True) + if len(evt.tags) > 0: + for tg in evt.tags: + if tg.name == cytomicobj.tag: + if not cytomicobj.lst_evtid: + cytomicobj.lst_evtid = str(evt['id']) + else: + if not evt['id'] in cytomicobj.lst_evtid: + cytomicobj.lst_evtid.append(str(evt['id'])) + break + cytomicobj.lst_evtid.remove('x') + cytomicobj.lst_evtid.remove('y') + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to collect events ids') + + +def find_eventid(cytomicobj, evtid): + # Get events that contain Cytomic tag. + try: + cytomicobj.res = False + for id in cytomicobj.lst_evtid: + if id == evtid: + cytomicobj.res = True + break + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to collect events ids') + + +def print_result_events(cytomicobj): + try: + if cytomicobj.res_msg is not None: + for key, msg in cytomicobj.res_msg.items(): + if msg is not None: + print(key, msg) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to print result') + + +def set_postdata(cytomicobj, moduleconfig, attribute): + # Set JSON to send to the API. + try: + + if cytomicobj.args.upload or cytomicobj.args.events: + event = attribute['Event'] + event_title = event['info'] + event_id = event['id'] + threat_level_id = int(event['threat_level_id']) + if moduleconfig['post_threat_level_id'] <= threat_level_id: + + if cytomicobj.atttype_misp == 'domain|ip' or cytomicobj.atttype_misp == 'hostname|port': + post_value = attribute['value'].split('|')[0] + else: + post_value = attribute['value'] + + if cytomicobj.atttype_misp == 'url' and 'http' not in post_value: + pass + else: + if cytomicobj.post_data is None: + cytomicobj.post_data = [{cytomicobj.attlabel_cytomic: post_value, 'AdditionalData': '{} {}'.format(cytomicobj.atttype_misp, attribute['comment']).strip(), 'Source': 'Uploaded from MISP', 'Policy': moduleconfig['cytomic_policy'], 'Description': '{} - {}'.format(event_id, event_title).strip()}] + else: + if post_value not in str(cytomicobj.post_data): + cytomicobj.post_data.append({cytomicobj.attlabel_cytomic: post_value, 'AdditionalData': '{} {}'.format(cytomicobj.atttype_misp, attribute['comment']).strip(), 'Source': 'Uploaded from MISP', 'Policy': moduleconfig['cytomic_policy'], 'Description': '{} - {}'.format(event_id, event_title).strip()}) + else: + if cytomicobject.debug: + print('Event %s skipped because of lower threat level' % event_id) + else: + event = attribute['Event'] + threat_level_id = int(event['threat_level_id']) + if moduleconfig['post_threat_level_id'] <= threat_level_id: + if cytomicobj.atttype_misp == 'domain|ip' or cytomicobj.atttype_misp == 'hostname|port': + post_value = attribute['value'].split('|')[0] + else: + post_value = attribute['value'] + + if cytomicobj.atttype_misp == 'url' and 'http' not in post_value: + pass + else: + if cytomicobj.post_data is None: + cytomicobj.post_data = [{cytomicobj.attlabel_cytomic: post_value}] + else: + cytomicobj.post_data.append({cytomicobj.attlabel_cytomic: post_value}) + else: + if cytomicobject.debug: + print('Event %s skipped because of lower threat level' % event_id) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to process post-data') + + +def send_postdata(cytomicobj, evtid=None): + # Batch post to upload event attributes. + try: + if cytomicobj.post_data is not None: + if cytomicobj.debug: + print('POST: {} {}'.format(cytomicobj.endpoint_ioc, cytomicobj.post_data)) + result_post_endpoint_ioc = requests.post(cytomicobj.endpoint_ioc, headers=cytomicobj.api_call_headers, json=cytomicobj.post_data, verify=False) + json_result_post_endpoint_ioc = json.loads(result_post_endpoint_ioc.text) + print(result_post_endpoint_ioc) + if 'true' not in (result_post_endpoint_ioc.text): + cytomicobj.error = True + if evtid is not None: + if cytomicobj.res_msg['Event: ' + str(evtid)] is None: + cytomicobj.res_msg['Event: ' + str(evtid)] = '(Send POST data: errors uploading attributes, event NOT untagged). If the problem persists, please review the format of the value of the attributes is correct.' + else: + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.res_msg['Event: ' + str(evtid)] + ' (Send POST data -else: errors uploading attributes, event NOT untagged). If the problem persists, please review the format of the value of the attributes is correct.' + if cytomicobj.debug: + print('RESULT: {}'.format(json_result_post_endpoint_ioc)) + else: + if evtid is None: + cytomicobj.error = True + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to post attributes') + + +def process_attributes(cytomicobj, moduleconfig, evtid=None): + # Get attributes to process. + try: + for misptype, cytomictypes in cytomicobject.att_types.items(): + cytomicobj.atttype_misp = misptype + for cytomiclabel, cytomictype in cytomictypes.items(): + cytomicobj.attlabel_cytomic = cytomiclabel + cytomicobj.atttype_cytomic = cytomictype + cytomicobj.post_data = None + icont = 0 + if cytomicobj.args.upload or cytomicobj.args.events: + cytomicobj.endpoint_ioc = moduleconfig['api_url'] + '/iocs/' + cytomicobj.atttype_cytomic + '?ttlDays=' + str(moduleconfig['upload_ttlDays']) + else: + cytomicobj.endpoint_ioc = moduleconfig['api_url'] + '/iocs/eraser/' + cytomicobj.atttype_cytomic + + # Get attributes to upload/delete and prepare JSON + # If evtid is set; we're called from --events + if cytomicobject.debug: + print("\nSearching for attributes of type %s" % cytomicobj.atttype_misp) + + if evtid is None: + cytomicobj.error = False + attr_result = cytomicobj.misp.search(controller='attributes', last=moduleconfig['upload_timeframe'], limit=cytomicobj.limit_attributes, type_attribute=cytomicobj.atttype_misp, tag=cytomicobj.tag, published=True, deleted=False, includeProposals=False, include_context=True, to_ids=True) + else: + if cytomicobj.error: + break + # We don't search with tags; we have an event for which we want to upload all events + attr_result = cytomicobj.misp.search(controller='attributes', eventid=evtid, last=moduleconfig['upload_timeframe'], limit=cytomicobj.limit_attributes, type_attribute=cytomicobj.atttype_misp, published=True, deleted=False, includeProposals=False, include_context=True, to_ids=True) + + cytomicobj.lst_attuuid = ['x', 'y'] + + if len(attr_result['Attribute']) > 0: + for attribute in attr_result['Attribute']: + if evtid is not None: + if cytomicobj.error: + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.res_msg['Event: ' + str(evtid)] + ' (errors uploading attributes, event NOT untagged). If the problem persists, please review the format of the value of the attributes is correct.' + break + if icont >= cytomicobj.limit_attributes: + if not cytomicobj.error and cytomicobj.post_data is not None: + # Send data to Cytomic + send_postdata(cytomicobj, evtid) + if not cytomicobj.error: + if 'Event: ' + str(evtid) in cytomicobj.res_msg: + if cytomicobj.res_msg['Event: ' + str(evtid)] is None: + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.attlabel_cytomic + 's: ' + str(icont) + else: + cytomicobj.res_msg['Event: ' + str(evtid)] += ' | ' + cytomicobj.attlabel_cytomic + 's: ' + str(icont) + else: + if cytomicobject.debug: + print('Data sent (' + cytomicobj.attlabel_cytomic + '): ' + str(icont)) + + cytomicobj.post_data = None + if cytomicobj.error: + if evtid is not None: + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.res_msg['Event: ' + str(evtid)] + ' (errors uploading attributes, event NOT untagged). If the problem persists, please review the format of the value of the attributes is correct.' + break + icont = 0 + + if evtid is None: + event = attribute['Event'] + event_id = event['id'] + find_eventid(cytomicobj, str(event_id)) + if not cytomicobj.res: + if not cytomicobj.lst_attuuid: + cytomicobj.lst_attuuid = attribute['uuid'] + else: + if not attribute['uuid'] in cytomicobj.lst_attuuid: + cytomicobj.lst_attuuid.append(attribute['uuid']) + icont += 1 + # Prepare data to send + set_postdata(cytomicobj, moduleconfig, attribute) + else: + icont += 1 + # Prepare data to send + set_postdata(cytomicobj, moduleconfig, attribute) + + if not cytomicobj.error: + # Send data to Cytomic + send_postdata(cytomicobj, evtid) + + if not cytomicobj.error and cytomicobj.post_data is not None and icont > 0: + # Data sent; process response + if cytomicobj.res_msg is not None and 'Event: ' + str(evtid) in cytomicobj.res_msg: + if cytomicobj.res_msg['Event: ' + str(evtid)] is None: + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.attlabel_cytomic + 's: ' + str(icont) + else: + cytomicobj.res_msg['Event: ' + str(evtid)] += ' | ' + cytomicobj.attlabel_cytomic + 's: ' + str(icont) + else: + if cytomicobject.debug: + print('Data sent (' + cytomicobj.attlabel_cytomic + '): ' + str(icont)) + + if not cytomicobj.error: + cytomicobj.lst_attuuid.remove('x') + cytomicobj.lst_attuuid.remove('y') + # Untag attributes + untag_attributes(cytomicobj) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to get attributes') + + +def untag_event(evtid): + # Remove tag of the event being processed. + try: + cytomicobj.records = 0 + evt = cytomicobj.misp.get_event(event=evtid, pythonify=True) + if len(evt.tags) > 0: + for tg in evt.tags: + if tg.name == cytomicobj.tag: + cytomicobj.misp.untag(evt['uuid'], cytomicobj.tag) + cytomicobj.records += 1 + cytomicobj.res_msg['Event: ' + str(evtid)] = cytomicobj.res_msg['Event: ' + str(evtid)] + ' (event untagged)' + break + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to untag events') + + +def process_events(cytomicobj, moduleconfig): + # Get events that contain Cytomic tag. + try: + collect_events_ids(cytomicobj, moduleconfig) + total_attributes_sent = 0 + for evtid in cytomicobj.lst_evtid: + cytomicobj.error = False + if cytomicobj.res_msg is None: + cytomicobj.res_msg = {'Event: ' + str(evtid): None} + else: + cytomicobj.res_msg['Event: ' + str(evtid)] = None + if cytomicobject.debug: + print('Event id: ' + str(evtid)) + + # get attributes of each known type of the event / prepare data to send / send data to Cytomic + process_attributes(cytomicobj, moduleconfig, evtid) + if not cytomicobj.error: + untag_event(evtid) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to process events ids') + + +def untag_attributes(cytomicobj): + # Remove tag of attributes sent. + try: + icont = 0 + if len(cytomicobj.lst_attuuid) > 0: + for uuid in cytomicobj.lst_attuuid: + attr = cytomicobj.misp.get_attribute(attribute=uuid, pythonify=True) + if len(attr.tags) > 0: + for tg in attr.tags: + if tg.name == cytomicobj.tag: + cytomicobj.misp.untag(uuid, cytomicobj.tag) + icont += 1 + break + print('Attributes untagged (' + str(icont) + ')') + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to untag attributes') + + +def process_attributes_upload(cytomicobj, moduleconfig): + # get attributes of each known type / prepare data to send / send data to Cytomic + try: + collect_events_ids(cytomicobj, moduleconfig) + process_attributes(cytomicobj, moduleconfig) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to upload attributes to Cytomic') + + +def process_attributes_delete(cytomicobj, moduleconfig): + # get attributes of each known type / prepare data to send / send data to Cytomic + try: + collect_events_ids(cytomicobj, moduleconfig) + process_attributes(cytomicobj, moduleconfig) + except Exception: + cytomicobj.error = True + if cytomicobj.debug: + sys.exit('Unable to delete attributes in Cytomic') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Upload or delete indicators to Cytomic API') + group = parser.add_mutually_exclusive_group() + group.add_argument('--events', action='store_true', help='Upload events indicators') + group.add_argument('--upload', action='store_true', help='Upload indicators') + group.add_argument('--delete', action='store_true', help='Delete indicators') + args = parser.parse_args() + if not args.upload and not args.delete and not args.events: + sys.exit("No valid action for the API") + + if misp_verifycert is False: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + module_config = get_config(misp_url, misp_key, misp_verifycert) + cytomicobj = cytomicobject + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, debug=cytomicobject.debug) + + cytomicobj.misp = misp + cytomicobj.args = args + + access_token = get_token(module_config['token_url'], module_config['clientid'], module_config['clientsecret'], module_config['scope'], module_config['grant_type'], module_config['username'], module_config['password']) + cytomicobj.api_call_headers = {'Authorization': 'Bearer ' + access_token} + if cytomicobj.debug: + print('Received access token') + + if cytomicobj.args.events: + cytomicobj.tag = module_config['upload_tag'] + cytomicobj.limit_events = module_config['limit_upload_events'] + cytomicobj.limit_attributes = module_config['limit_upload_attributes'] + process_events(cytomicobj, module_config) + print_result_events(cytomicobj) + + elif cytomicobj.args.upload: + cytomicobj.tag = module_config['upload_tag'] + cytomicobj.limit_events = 0 + cytomicobj.limit_attributes = module_config['limit_upload_attributes'] + process_attributes_upload(cytomicobj, module_config) + + else: + cytomicobj.tag = module_config['delete_tag'] + cytomicobj.limit_events = 0 + cytomicobj.limit_attributes = module_config['limit_upload_attributes'] + process_attributes_delete(cytomicobj, module_config) diff --git a/examples/del.py b/examples/del.py index 24969d1..81dd774 100755 --- a/examples/del.py +++ b/examples/del.py @@ -1,26 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP -from keys import misp_url, misp_key,misp_verifycert +from pymisp import ExpandedPyMISP +from keys import misp_url, misp_key, misp_verifycert import argparse -# Usage for pipe masters: ./last.py -l 5h | jq . - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', debug=True) - - -def del_event(m, eventid): - result = m.delete_event(eventid) - print(result) - -def del_attr(m, attrid): - result = m.delete_attribute(attrid) - print(result) - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Delete an event from a MISP instance.') parser.add_argument("-e", "--event", help="Event ID to delete.") @@ -28,9 +13,10 @@ if __name__ == '__main__': args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) if args.event: - del_event(misp, args.event) + result = misp.delete_event(args.event) else: - del_attr(misp, args.attribute) + result = misp.delete_attribute(args.attribute) + print(result) diff --git a/examples/delete_user.py b/examples/delete_user.py index 9537558..c579cc4 100755 --- a/examples/delete_user.py +++ b/examples/delete_user.py @@ -1,25 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Delete the user with the given id. Keep in mind that disabling users (by setting the disabled flag via an edit) is always prefered to keep user associations to events intact.') + parser = argparse.ArgumentParser(description='Delete the user with the given id. Keep in mind that disabling users (by setting the disabled flag via an edit) is always preferred to keep user associations to events intact.') parser.add_argument("-i", "--user_id", help="The id of the user you want to delete.") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) print(misp.delete_user(args.user_id)) diff --git a/examples/edit_organisation.py b/examples/edit_organisation.py new file mode 100755 index 0000000..41bc024 --- /dev/null +++ b/examples/edit_organisation.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import ExpandedPyMISP, MISPOrganisation +from keys import misp_url, misp_key, misp_verifycert +import argparse + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Edit the email of the organisation designed by the organisation_id.') + parser.add_argument("-i", "--organisation_id", required=True, help="The name of the json file describing the organisation you want to modify.") + parser.add_argument("-e", "--email", help="Email linked to the organisation.") + args = parser.parse_args() + + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + + org = MISPOrganisation() + org.id = args.organisation_id + org.email = args.email + + print(misp.update_organisation(org, pythonify=True)) diff --git a/examples/edit_user.py b/examples/edit_user.py index e48090d..74440fd 100755 --- a/examples/edit_user.py +++ b/examples/edit_user.py @@ -1,19 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPUser from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') if __name__ == '__main__': parser = argparse.ArgumentParser(description='Edit the email of the user designed by the user_id.') @@ -21,6 +12,9 @@ if __name__ == '__main__': parser.add_argument("-e", "--email", help="Email linked to the account.") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) + user = MISPUser + user.id = args.user_id + user.email = args.email - print(misp.edit_user(args.user_id, email=args.email)) + print(misp.edit_user(user, pythonify=True)) diff --git a/examples/edit_user_json.py b/examples/edit_user_json.py deleted file mode 100755 index 6e1d276..0000000 --- a/examples/edit_user_json.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Edit the user designed by the user_id. If no file is provided, returns a json listing all the fields used to describe a user.') - parser.add_argument("-i", "--user_id", required=True, help="The name of the json file describing the user you want to modify.") - parser.add_argument("-f", "--json_file", help="The name of the json file describing your modifications.") - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - if args.json_file is None: - print (misp.get_edit_user_fields_list(args.user_id)) - else: - print(misp.edit_user_json(args.json_file, args.user_id)) diff --git a/examples/et2misp.py b/examples/et2misp.py deleted file mode 100755 index 2fa5f29..0000000 --- a/examples/et2misp.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copy Emerging Threats Block IPs list to several MISP events -# Because of the large size of the list the first run will take a minute -# Running it again will update the MISP events if changes are detected -# -# This script requires PyMISP 2.4.50 or later - -import sys, json, time, requests -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert - -et_url = 'https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt' -et_str = 'Emerging Threats ' - -def init_misp(): - global mymisp - mymisp = PyMISP(misp_url, misp_key, misp_verifycert) - -def load_misp_event(eid): - global et_attr - global et_drev - global et_event - et_attr = {} - et_drev = {} - - et_event = mymisp.get(eid) - echeck(et_event) - for a in et_event['Event']['Attribute']: - if a['category'] == 'Network activity': - et_attr[a['value']] = a['id'] - continue - if a['category'] == 'Internal reference': - et_drev = a; - -def init_et(): - global et_data - global et_rev - requests.packages.urllib3.disable_warnings() - s = requests.Session() - r = s.get(et_url) - if r.status_code != 200: - raise Exception('Error getting ET data: {}'.format(r.text)) - name = '' - et_data = {} - et_rev = 0 - for line in r.text.splitlines(): - if line.startswith('# Rev '): - et_rev = int(line[6:]) - continue - if line.startswith('#'): - name = line[1:].strip() - if et_rev and not et_data.get(name): - et_data[name] = {} - continue - l = line.rstrip() - if l: - et_data[name][l] = name - -def update_et_event(name): - if et_drev and et_rev and int(et_drev['value']) < et_rev: - # Copy MISP attributes to new dict - et_ips = dict.fromkeys(et_attr.keys()) - - # Weed out attributes still in ET data - for k,v in et_data[name].items(): - et_attr.pop(k, None) - - # Delete the leftover attributes from MISP - for k,v in et_attr.items(): - r = mymisp.delete_attribute(v) - if r.get('errors'): - print "Error deleting attribute {} ({}): {}\n".format(v,k,r['errors']) - - # Weed out ips already in the MISP event - for k,v in et_ips.items(): - et_data[name].pop(k, None) - - # Add new attributes to MISP event - ipdst = [] - for i,k in enumerate(et_data[name].items(), 1-len(et_data[name])): - ipdst.append(k[0]) - if i % 100 == 0: - r = mymisp.add_ipdst(et_event, ipdst) - echeck(r, et_event['Event']['id']) - ipdst = [] - - # Update revision number - et_drev['value'] = et_rev - et_drev.pop('timestamp', None) - attr = [] - attr.append(et_drev) - - # Publish updated MISP event - et_event['Event']['Attribute'] = attr - et_event['Event']['published'] = False - et_event['Event']['date'] = time.strftime('%Y-%m-%d') - r = mymisp.publish(et_event) - echeck(r, et_event['Event']['id']) - -def echeck(r, eid=None): - if r.get('errors'): - if eid: - print "Processing event {} failed: {}".format(eid, r['errors']) - else: - print r['errors'] - sys.exit(1) - -if __name__ == '__main__': - init_misp() - init_et() - - for et_type in set(et_data.keys()): - info = et_str + et_type - r = mymisp.search_index(eventinfo=info) - if r['response']: - eid=r['response'][0]['id'] - else: # event not found, create it - new_event = mymisp.new_event(info=info, distribution=3, threat_level_id=4, analysis=1) - echeck(new_event) - eid=new_event['Event']['id'] - r = mymisp.add_internal_text(new_event, 1, comment='Emerging Threats revision number') - echeck(r, eid) - load_misp_event(eid) - update_et_event(et_type) diff --git a/examples/events/create_massive_dummy_events.py b/examples/events/create_massive_dummy_events.py index 7829ad6..0b55757 100755 --- a/examples/events/create_massive_dummy_events.py +++ b/examples/events/create_massive_dummy_events.py @@ -4,9 +4,11 @@ from pymisp import ExpandedPyMISP try: from keys import url, key + verifycert = False except ImportError: - url = 'http://localhost:8080' - key = '8h0gHbhS0fv6JUOlTED0AznLXFbf83TYtQrCycqb' + url = 'https://localhost:8443' + key = 'd6OmdDFvU3Seau3UjwvHS1y3tFQbaRNhJhDX0tjh' + verifycert = False import argparse import tools @@ -17,7 +19,8 @@ if __name__ == '__main__': parser.add_argument("-a", "--attribute", type=int, help="Number of attributes per event (default 3000)") args = parser.parse_args() - misp = ExpandedPyMISP(url, key, True) + misp = ExpandedPyMISP(url, key, verifycert) + misp.toggle_global_pythonify() if args.limit is None: args.limit = 1 diff --git a/examples/events/tools.py b/examples/events/tools.py index 94f5d91..1bed7c4 100644 --- a/examples/events/tools.py +++ b/examples/events/tools.py @@ -4,7 +4,7 @@ import random from random import randint import string -from pymisp import MISPEvent +from pymisp import MISPEvent, MISPAttribute def randomStringGenerator(size, chars=string.ascii_lowercase + string.digits): @@ -15,32 +15,34 @@ def randomIpGenerator(): return str(randint(0, 255)) + '.' + str(randint(0, 255)) + '.' + str(randint(0, 255)) + '.' + str(randint(0, 255)) +def _attribute(category, type, value): + attribute = MISPAttribute() + attribute.category = category + attribute.type = type + attribute.value = value + return attribute + + def floodtxt(misp, event, maxlength=255): text = randomStringGenerator(randint(1, maxlength)) - textfunctions = [misp.add_internal_comment, misp.add_internal_text, misp.add_internal_other, misp.add_email_subject, misp.add_mutex, misp.add_filename] - textfunctions[randint(0, 5)](event, text) + choose_from = [('Internal reference', 'comment', text), ('Internal reference', 'text', text), + ('Internal reference', 'other', text), ('Network activity', 'email-subject', text), + ('Artifacts dropped', 'mutex', text), ('Artifacts dropped', 'filename', text)] + misp.add_attribute(event, _attribute(*random.choice(choose_from))) def floodip(misp, event): ip = randomIpGenerator() - ipfunctions = [misp.add_ipsrc, misp.add_ipdst] - ipfunctions[randint(0, 1)](event, ip) + choose_from = [('Network activity', 'ip-src', ip), ('Network activity', 'ip-dst', ip)] + misp.add_attribute(event, _attribute(*random.choice(choose_from))) def flooddomain(misp, event, maxlength=25): a = randomStringGenerator(randint(1, maxlength)) b = randomStringGenerator(randint(2, 3), chars=string.ascii_lowercase) domain = a + '.' + b - domainfunctions = [misp.add_hostname, misp.add_domain] - domainfunctions[randint(0, 1)](event, domain) - - -def flooddomainip(misp, event, maxlength=25): - a = randomStringGenerator(randint(1, maxlength)) - b = randomStringGenerator(randint(2, 3), chars=string.ascii_lowercase) - domain = a + '.' + b - ip = randomIpGenerator() - misp.add_domain_ip(event, domain, ip) + choose_from = [('Network activity', 'domain', domain), ('Network activity', 'hostname', domain)] + misp.add_attribute(event, _attribute(*random.choice(choose_from))) def floodemail(misp, event, maxlength=25): @@ -48,19 +50,15 @@ def floodemail(misp, event, maxlength=25): b = randomStringGenerator(randint(1, maxlength)) c = randomStringGenerator(randint(2, 3), chars=string.ascii_lowercase) email = a + '@' + b + '.' + c - emailfunctions = [misp.add_email_src, misp.add_email_dst] - emailfunctions[randint(0, 1)](event, email) - - -def floodattachment(misp, eventid, distribution, to_ids, category, comment, info, analysis, threat_level_id): - filename = randomStringGenerator(randint(1, 128)) - misp.upload_sample(filename, 'dummy', eventid, distribution, to_ids, category, comment, info, analysis, threat_level_id) + choose_from = [('Network activity', 'email-dst', email), ('Network activity', 'email-src', email)] + misp.add_attribute(event, _attribute(*random.choice(choose_from))) def create_dummy_event(misp): - event = misp.new_event(0, 4, 0, 'dummy event') - flooddomainip(misp, event) - floodattachment(misp, event['Event']['id'], event['Event']['distribution'], False, 'Payload delivery', '', event['Event']['info'], event['Event']['analysis'], event['Event']['threat_level_id']) + event = MISPEvent() + event.info = 'Dummy event' + event = misp.add_event(event, pythonify=True) + return event def create_massive_dummy_events(misp, nbattribute): @@ -68,12 +66,6 @@ def create_massive_dummy_events(misp, nbattribute): event.info = 'massive dummy event' event = misp.add_event(event) print(event) - eventid = event.id - distribution = '0' - functions = [floodtxt, floodip, flooddomain, flooddomainip, floodemail, floodattachment] + functions = [floodtxt, floodip, flooddomain, floodemail] for i in range(nbattribute): - choice = randint(0, 5) - if choice == 5: - floodattachment(misp, eventid, distribution, False, 'Payload delivery', '', event.info, event.analysis, event.threat_level_id) - else: - functions[choice](misp, event) + functions[random.randint(0, len(functions) - 1)](misp, event) diff --git a/examples/falsepositive_disabletoids.py b/examples/falsepositive_disabletoids.py new file mode 100755 index 0000000..c4a72d1 --- /dev/null +++ b/examples/falsepositive_disabletoids.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +Koen Van Impe + +Disable the to_ids flag of an attribute when there are to many false positives +Put this script in crontab to run every /15 or /60 + */5 * * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/falsepositive_disabletoids.py + +Do inline config in "main" + +''' + +from pymisp import ExpandedPyMISP, MISPEvent +from keys import misp_url, misp_key, misp_verifycert +from datetime import datetime +from datetime import date + +import datetime as dt +import smtplib +import mimetypes +from email.mime.multipart import MIMEMultipart +from email import encoders +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +import argparse + + +def init(url, key, verifycert): + ''' + Template to get MISP module started + ''' + return ExpandedPyMISP(url, key, verifycert, 'json') + + +if __name__ == '__main__': + + minimal_fp = 0 + threshold_to_ids = .50 + minimal_date_sighting_date = '1970-01-01 00:00:00' + + smtp_from = 'INSERT_FROM' + smtp_to = 'INSERT_TO' + smtp_server = 'localhost' + report_changes = '' + ts_format = '%Y-%m-%d %H:%M:%S' + + parser = argparse.ArgumentParser(description="Disable the to_ids flag of attributes with a certain number of false positives above a threshold.") + parser.add_argument('-m', '--mail', action='store_true', help='Mail the report') + parser.add_argument('-o', '--mailoptions', action='store', help='mailoptions: \'smtp_from=INSERT_FROM;smtp_to=INSERT_TO;smtp_server=localhost\'') + parser.add_argument('-b', '--minimal-fp', default=minimal_fp, type=int, help='Minimal number of false positive (default: %(default)s )') + parser.add_argument('-t', '--threshold', default=threshold_to_ids, type=float, help='Threshold false positive/true positive rate (default: %(default)s )') + parser.add_argument('-d', '--minimal-date-sighting', default=minimal_date_sighting_date, help='Minimal date for sighting (false positive / true positive) (default: %(default)s )') + + args = parser.parse_args() + misp = init(misp_url, misp_key, misp_verifycert) + + minimal_fp = int(args.minimal_fp) + threshold_to_ids = args.threshold + minimal_date_sighting_date = args.minimal_date_sighting + minimal_date_sighting = int(dt.datetime.strptime(minimal_date_sighting_date, '%Y-%m-%d %H:%M:%S').strftime("%s")) + + # Fetch all the attributes + result = misp.search('attributes', to_ids=1, include_sightings=1) + + if 'Attribute' in result: + for attribute in result['Attribute']: + true_positive = 0 + false_positive = 0 + compute_threshold = 0 + attribute_id = attribute['id'] + attribute_value = attribute['value'] + attribute_uuid = attribute['uuid'] + event_id = attribute['event_id'] + + # Only do something if there is a sighting + if 'Sighting' in attribute: + + for sighting in attribute['Sighting']: + if int(sighting['date_sighting']) > minimal_date_sighting: + if int(sighting['type']) == 0: + true_positive = true_positive + 1 + elif int(sighting['type']) == 1: + false_positive = false_positive + 1 + + if false_positive > minimal_fp: + compute_threshold = false_positive / (true_positive + false_positive) + + if compute_threshold >= threshold_to_ids: + # Fetch event title for report text + event_details = misp.get_event(event_id) + event_info = event_details['Event']['info'] + + misp.update_attribute( { 'uuid': attribute_uuid, 'to_ids': 0}) + + report_changes = report_changes + 'Disable to_ids for [%s] (%s) in event [%s] (%s) - FP: %s TP: %s \n' % (attribute_value, attribute_id, event_info, event_id, false_positive, true_positive) + + # Changing the attribute to_ids flag sets the event to unpublished + misp.publish(event_id) + + # Only send/print the report if it contains content + if report_changes: + if args.mail: + if args.mailoptions: + mailoptions = args.mailoptions.split(';') + for s in mailoptions: + if s.split('=')[0] == 'smtp_from': + smtp_from = s.split('=')[1] + if s.split('=')[0] == 'smtp_to': + smtp_to = s.split('=')[1] + if s.split('=')[0] == 'smtp_server': + smtp_server = s.split('=')[1] + + now = datetime.now() + current_date = now.strftime(ts_format) + report_changes_body = 'MISP Disable to_ids flags for %s on %s\n-------------------------------------------------------------------------------\n\n' % (misp_url, current_date) + report_changes_body = report_changes_body + 'Minimal number of false positives before considering threshold: %s\n' % (minimal_fp) + report_changes_body = report_changes_body + 'Threshold false positives/true positives to disable to_ids flag: %s\n' % (threshold_to_ids) + report_changes_body = report_changes_body + 'Minimal date for sighting false positives: %s\n\n' % (minimal_date_sighting_date) + report_changes_body = report_changes_body + report_changes + report_changes_body = report_changes_body + '\nEvents that have attributes with changed to_ids flag have been republished, without e-mail notification.' + report_changes_body = report_changes_body + '\n\nMISP Disable to_ids Finished\n' + + subject = 'Report of disable to_ids flag for false positives sightings of %s' % (current_date) + msg = MIMEMultipart() + msg['From'] = smtp_from + msg['To'] = smtp_to + msg['Subject'] = subject + + msg.attach(MIMEText(report_changes_body, 'text')) + print(report_changes_body) + server = smtplib.SMTP(smtp_server) + server.sendmail(smtp_from, smtp_to, msg.as_string()) + + else: + print(report_changes) diff --git a/examples/feed-generator-from-redis/ObjectConstructor/CowrieMISPObject.py b/examples/feed-generator-from-redis/ObjectConstructor/CowrieMISPObject.py index 1bf98ca..b69c153 100644 --- a/examples/feed-generator-from-redis/ObjectConstructor/CowrieMISPObject.py +++ b/examples/feed-generator-from-redis/ObjectConstructor/CowrieMISPObject.py @@ -8,12 +8,10 @@ from pymisp.tools.abstractgenerator import AbstractMISPObjectGenerator class CowrieMISPObject(AbstractMISPObjectGenerator): def __init__(self, dico_val, **kargs): self._dico_val = dico_val - self.name = "cowrie" - # Enforce attribute date with timestamp super(CowrieMISPObject, self).__init__('cowrie', - default_attributes_parameters={'timestamp': int(time.time())}, - **kargs) + default_attributes_parameters={'timestamp': int(time.time())}, + **kargs) self.generate_attributes() def generate_attributes(self): diff --git a/examples/feed-generator-from-redis/README.md b/examples/feed-generator-from-redis/README.md index 777f370..12b5163 100644 --- a/examples/feed-generator-from-redis/README.md +++ b/examples/feed-generator-from-redis/README.md @@ -9,10 +9,16 @@ ## Installation -```` +``` +# redis-server +sudo apt install redis-server + +# Check if redis is running +redis-cli ping + # Feed generator -git clone https://github.com/CIRCL/PyMISP -cd examples/feed-generator-from-redis +git clone https://github.com/MISP/PyMISP +cd PyMISP/examples/feed-generator-from-redis cp settings.default.py settings.py vi settings.py # adjust your settings @@ -66,7 +72,7 @@ python3 server.py >>> obj_data = { "session": "session_id", "username": "admin", "password": "admin", "protocol": "telnet" } >>> generator.add_object_to_event(obj_name, **obj_data) -# Immediatly write the event to the disk (Bypassing the default flushing behavior) +# Immediately write the event to the disk (Bypassing the default flushing behavior) >>> generator.flush_event() ``` diff --git a/examples/feed-generator-from-redis/fromredis.py b/examples/feed-generator-from-redis/fromredis.py index 26b2ee6..a82f5ce 100755 --- a/examples/feed-generator-from-redis/fromredis.py +++ b/examples/feed-generator-from-redis/fromredis.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import sys -import json import argparse import datetime +import json +import sys import time + import redis import settings - from generator import FeedGenerator @@ -60,7 +60,10 @@ class RedisToMISPFeed: except Exception as error: self.save_error_to_redis(error, data) - beautyful_sleep(5, self.format_last_action()) + try: + beautyful_sleep(5, self.format_last_action()) + except KeyboardInterrupt: + sys.exit(130) def pop(self, key): popped = self.serv.rpop(key) @@ -104,7 +107,7 @@ class RedisToMISPFeed: # Suffix not provided, try to add anyway if settings.fallback_MISP_type == 'attribute': new_key = key + self.SUFFIX_ATTR - # Add atribute type from the config + # Add attribute type from the config if 'type' not in data and settings.fallback_attribute_type: data['type'] = settings.fallback_attribute_type else: diff --git a/examples/feed-generator-from-redis/generator.py b/examples/feed-generator-from-redis/generator.py index 38a9d54..80aba3e 100755 --- a/examples/feed-generator-from-redis/generator.py +++ b/examples/feed-generator-from-redis/generator.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 -import sys +import datetime +import hashlib import json import os -import hashlib -import datetime +import sys import time -import uuid -from pymisp import MISPEvent +from pymisp import MISPEvent, MISPOrganisation import settings @@ -35,11 +34,6 @@ def get_system_templates(): return templates -def gen_uuid(): - """Generate a random UUID and returns its string representation""" - return str(uuid.uuid4()) - - class FeedGenerator: """Helper object to create MISP feed. @@ -127,29 +121,44 @@ class FeedGenerator: if ('|' in attr_type or attr_type == 'malware-sample'): split = attr_value.split('|') self.attributeHashes.append([ - hashlib.md5(str(split[0]).encode("utf-8")).hexdigest(), + hashlib.md5(str(split[0]).encode("utf-8"), usedforsecurity=False).hexdigest(), self.current_event_uuid ]) self.attributeHashes.append([ - hashlib.md5(str(split[1]).encode("utf-8")).hexdigest(), + hashlib.md5(str(split[1]).encode("utf-8"), usedforsecurity=False).hexdigest(), self.current_event_uuid ]) else: self.attributeHashes.append([ - hashlib.md5(str(attr_value).encode("utf-8")).hexdigest(), + hashlib.md5(str(attr_value).encode("utf-8"), usedforsecurity=False).hexdigest(), self.current_event_uuid ]) # Manifest def _init_manifest(self): + # check if outputdir exists and try to create it if not + if not os.path.exists(settings.outputdir): + try: + os.makedirs(settings.outputdir) + except PermissionError as error: + print(error) + print("Please fix the above error and try again.") + sys.exit(126) + # create an empty manifest - with open(os.path.join(settings.outputdir, 'manifest.json'), 'w'): - pass + try: + with open(os.path.join(settings.outputdir, 'manifest.json'), 'w') as f: + json.dump({}, f) + except PermissionError as error: + print(error) + print("Please fix the above error and try again.") + sys.exit(126) + # create new event and save manifest self.create_daily_event() def flush_event(self, new_event=None): - print('Writting event on disk'+' '*50) + print('Writing event on disk' + ' ' * 50) if new_event is not None: event_uuid = new_event['uuid'] event = new_event @@ -157,9 +166,8 @@ class FeedGenerator: event_uuid = self.current_event_uuid event = self.current_event - eventFile = open(os.path.join(settings.outputdir, event_uuid+'.json'), 'w') - eventFile.write(event.to_json()) - eventFile.close() + with open(os.path.join(settings.outputdir, event_uuid + '.json'), 'w') as eventFile: + json.dump(event.to_feed(), eventFile) self.save_hashes() @@ -182,27 +190,11 @@ class FeedGenerator: hashFile.write('{},{}\n'.format(element[0], element[1])) hashFile.close() self.attributeHashes = [] - print('Hash saved' + ' '*30) + print('Hash saved' + ' ' * 30) except Exception as e: print(e) sys.exit('Could not create the quick hash lookup file.') - def _addEventToManifest(self, event): - event_dict = event.to_dict()['Event'] - tags = [] - for eventTag in event_dict.get('EventTag', []): - tags.append({'name': eventTag['Tag']['name'], - 'colour': eventTag['Tag']['colour']}) - return { - 'Orgc': event_dict.get('Orgc', []), - 'Tag': tags, - 'info': event_dict['info'], - 'date': event_dict['date'], - 'analysis': event_dict['analysis'], - 'threat_level_id': event_dict['threat_level_id'], - 'timestamp': event_dict.get('timestamp', int(time.time())) - } - def get_last_event_from_manifest(self): """Retreive last event from the manifest. @@ -225,7 +217,7 @@ class FeedGenerator: # Sort by date then by event name dated_events.sort(key=lambda k: (k[0], k[2]), reverse=True) return dated_events[0] - except FileNotFoundError as e: + except FileNotFoundError: print('Manifest not found, generating a fresh one') self._init_manifest() return self.get_last_event_from_manifest() @@ -248,11 +240,9 @@ class FeedGenerator: return event def create_daily_event(self): - new_uuid = gen_uuid() today = str(datetime.date.today()) event_dict = { - 'uuid': new_uuid, - 'id': len(self.manifest)+1, + 'id': len(self.manifest) + 1, 'Tag': settings.Tag, 'info': self.daily_event_name.format(today), 'analysis': settings.analysis, # [0-2] @@ -264,14 +254,14 @@ class FeedGenerator: event.from_dict(**event_dict) # reference org - org_dict = {} - org_dict['name'] = settings.org_name - org_dict['uui'] = settings.org_uuid - event['Orgc'] = org_dict + org = MISPOrganisation() + org.name = settings.org_name + org.uuid = settings.org_uuid + event.Orgc = org # save event on disk self.flush_event(new_event=event) # add event to manifest - self.manifest[event['uuid']] = self._addEventToManifest(event) + self.manifest.update(event.manifest) self.save_manifest() return event diff --git a/examples/feed-generator/README.md b/examples/feed-generator/README.md index 6babeeb..27888d9 100644 --- a/examples/feed-generator/README.md +++ b/examples/feed-generator/README.md @@ -7,7 +7,7 @@ This python script can be used to generate a MISP feed based on an existing MISP ```` git clone https://github.com/MISP/PyMISP.git cd examples/feed-generator -cp settings-default.py settings.py +cp settings.default.py settings.py vi settings.py #adjust your settings python3 generate.py ```` diff --git a/examples/feed-generator/generate.py b/examples/feed-generator/generate.py index d90cfab..48c28e8 100755 --- a/examples/feed-generator/generate.py +++ b/examples/feed-generator/generate.py @@ -4,149 +4,55 @@ import sys import json import os -import hashlib -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from settings import url, key, ssl, outputdir, filters, valid_attribute_distribution_levels +try: + from settings import with_distribution +except ImportError: + with_distribution = False -objectsFields = { - 'Attribute': { - 'uuid', - 'value', - 'category', - 'type', - 'comment', - 'data', - 'timestamp', - 'to_ids', - 'object_relation' - }, - 'Event': { - 'uuid', - 'info', - 'threat_level_id', - 'analysis', - 'timestamp', - 'publish_timestamp', - 'published', - 'date' - }, - 'Object': { - 'name', - 'meta-category', - 'description', - 'template_uuid', - 'template_version', - 'uuid', - 'timestamp', - 'distribution', - 'sharing_group_id', - 'comment' - }, - 'ObjectReference': { - 'uuid', - 'timestamp', - 'relationship_type', - 'comment', - 'object_uuid', - 'referenced_uuid' - }, - 'Orgc': { - 'name', - 'uuid' - }, - 'Tag': { - 'name', - 'colour', - 'exportable' - } -} +try: + from settings import with_local_tags +except ImportError: + with_local_tags = True -objectsToSave = { - 'Orgc': {}, - 'Tag': {}, - 'Attribute': { - 'Tag': {} - }, - 'Object': { - 'Attribute': { - 'Tag': {} - }, - 'ObjectReference': {} - } -} +try: + from settings import include_deleted +except ImportError: + include_deleted = False + +try: + from settings import exclude_attribute_types +except ImportError: + exclude_attribute_types = [] valid_attribute_distributions = [] -attributeHashes = [] - def init(): # If we have an old settings.py file then this variable won't exist global valid_attribute_distributions try: - valid_attribute_distributions = valid_attribute_distribution_levels + valid_attribute_distributions = [int(v) for v in valid_attribute_distribution_levels] except Exception: - valid_attribute_distributions = ['0', '1', '2', '3', '4', '5'] - return PyMISP(url, key, ssl) + valid_attribute_distributions = [0, 1, 2, 3, 4, 5] + return ExpandedPyMISP(url, key, ssl) -def recursiveExtract(container, containerType, leaf, eventUuid): - temp = {} - if containerType in ['Attribute', 'Object']: - if (__blockByDistribution(container)): - return False - for field in objectsFields[containerType]: - if field in container: - temp[field] = container[field] - if (containerType == 'Attribute'): - global attributeHashes - if ('|' in container['type'] or container['type'] == 'malware-sample'): - split = container['value'].split('|') - attributeHashes.append([hashlib.md5(split[0].encode("utf-8")).hexdigest(), eventUuid]) - attributeHashes.append([hashlib.md5(split[1].encode("utf-8")).hexdigest(), eventUuid]) - else: - attributeHashes.append([hashlib.md5(container['value'].encode("utf-8")).hexdigest(), eventUuid]) - children = leaf.keys() - for childType in children: - childContainer = container.get(childType) - if (childContainer): - if (type(childContainer) is dict): - temp[childType] = recursiveExtract(childContainer, childType, leaf[childType], eventUuid) - else: - temp[childType] = [] - for element in childContainer: - processed = recursiveExtract(element, childType, leaf[childType], eventUuid) - if (processed): - temp[childType].append(processed) - return temp - - -def saveEvent(misp, uuid): - event = misp.get_event(uuid) - if not event.get('Event'): - print('Error while fetching event: {}'.format(event['message'])) - sys.exit('Could not create file for event ' + uuid + '.') - event['Event'] = recursiveExtract(event['Event'], 'Event', objectsToSave, event['Event']['uuid']) - event = json.dumps(event) - eventFile = open(os.path.join(outputdir, uuid + '.json'), 'w') - eventFile.write(event) - eventFile.close() - - -def __blockByDistribution(element): - if element['distribution'] not in valid_attribute_distributions: - return True - return False - - -def saveHashes(): - if not attributeHashes: - return False +def saveEvent(event): try: - hashFile = open(os.path.join(outputdir, 'hashes.csv'), 'w') - for element in attributeHashes: - hashFile.write('{},{}\n'.format(element[0], element[1])) - hashFile.close() + with open(os.path.join(outputdir, f'{event["Event"]["uuid"]}.json'), 'w') as f: + json.dump(event, f, indent=2) + except Exception as e: + print(e) + sys.exit('Could not create the event dump.') + + +def saveHashes(hashes): + try: + with open(os.path.join(outputdir, 'hashes.csv'), 'w') as hashFile: + for element in hashes: + hashFile.write('{},{}\n'.format(element[0], element[1])) except Exception as e: print(e) sys.exit('Could not create the quick hash lookup file.') @@ -162,41 +68,39 @@ def saveManifest(manifest): sys.exit('Could not create the manifest file.') -def __addEventToManifest(event): - tags = [] - for eventTag in event['EventTag']: - tags.append({'name': eventTag['Tag']['name'], - 'colour': eventTag['Tag']['colour']}) - return {'Orgc': event['Orgc'], - 'Tag': tags, - 'info': event['info'], - 'date': event['date'], - 'analysis': event['analysis'], - 'threat_level_id': event['threat_level_id'], - 'timestamp': event['timestamp'] - } - - if __name__ == '__main__': misp = init() try: - r = misp.get_index(filters) - events = r['response'] - print(events[0]) + events = misp.search_index(minimal=True, **filters, pythonify=False) except Exception as e: print(e) sys.exit("Invalid response received from MISP.") if len(events) == 0: sys.exit("No events returned.") manifest = {} + hashes = [] counter = 1 total = len(events) for event in events: - saveEvent(misp, event['uuid']) - manifest[event['uuid']] = __addEventToManifest(event) + try: + e = misp.get_event(event['uuid'], deleted=include_deleted, pythonify=True) + if exclude_attribute_types: + for i, attribute in enumerate(e.attributes): + if attribute.type in exclude_attribute_types: + e.attributes.pop(i) + e_feed = e.to_feed(valid_distributions=valid_attribute_distributions, with_meta=True, with_distribution=with_distribution, with_local_tags=with_local_tags) + except Exception as err: + print(err, event['uuid']) + continue + if not e_feed: + print(f'Invalid distribution {e.distribution}, skipping') + continue + hashes += [[h, e.uuid] for h in e_feed['Event'].pop('_hashes')] + manifest.update(e_feed['Event'].pop('_manifest')) + saveEvent(e_feed) print("Event " + str(counter) + "/" + str(total) + " exported.") counter += 1 saveManifest(manifest) print('Manifest saved.') - saveHashes() + saveHashes(hashes) print('Hashes saved. Feed creation completed.') diff --git a/examples/feed-generator/settings.default.py b/examples/feed-generator/settings.default.py index 384ab34..3b994e8 100755 --- a/examples/feed-generator/settings.default.py +++ b/examples/feed-generator/settings.default.py @@ -16,11 +16,13 @@ outputdir = 'output' # you can use on the event index, such as organisation, tags, etc. # It uses the same joining and condition rules as the API parameters # For example: -# filters = {'tag':'tlp:white|feed-export|!privint','org':'CIRCL', 'published':1} +# filters = {'tags':['tlp:white','feed-export','!privint'],'org':'CIRCL', 'published':1} # the above would generate a feed for all published events created by CIRCL, # tagged tlp:white and/or feed-export but exclude anything tagged privint filters = {'published':'true'} +# Include deleted attributes and objects in the events +include_deleted = False # By default all attributes will be included in the feed generation # Remove the levels that you do not wish to include in the feed @@ -37,3 +39,18 @@ filters = {'published':'true'} # 5: Inherit Event valid_attribute_distribution_levels = ['0', '1', '2', '3', '4', '5'] +# By default, all attribute passing the filtering rules will be exported. +# This setting can be used to filter out any attributes being of the type contained in the list. +# Warning: Keep in mind that if you propagate data (via synchronisation/feeds/...), recipients +# will not be able to get these attributes back unless their events get updated. +# For example: +# exclude_attribute_types = ['malware-sample'] +exclude_attribute_types = [] + +# Include the distribution and sharing group information (and names/UUIDs of organisations in those Sharing Groups) +# Set this to False if you want to discard the distribution metadata. That way all data will inherit the distribution +# the feed +with_distribution = False + +# Include the exportable local tags along with the global tags. The default is True. +with_local_tags = True diff --git a/examples/fetch_events_feed.py b/examples/fetch_events_feed.py index 3a3a8fe..92a1a7b 100755 --- a/examples/fetch_events_feed.py +++ b/examples/fetch_events_feed.py @@ -3,22 +3,13 @@ from keys import misp_url, misp_key, misp_verifycert import argparse -from pymisp import PyMISP - -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', debug=False) +from pymisp import ExpandedPyMISP if __name__ == '__main__': parser = argparse.ArgumentParser(description='Fetch all events from a feed.') parser.add_argument("-f", "--feed", required=True, help="feed's ID to be fetched.") args = parser.parse_args() - - misp = init(misp_url, misp_key) + + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) misp.fetch_feed(args.feed) diff --git a/examples/fetch_warninglist_hits.py b/examples/fetch_warninglist_hits.py new file mode 100644 index 0000000..12d3f62 --- /dev/null +++ b/examples/fetch_warninglist_hits.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pymisp import PyMISP +from keys import misp_url, misp_key +import argparse + + +def init(url, key): + return PyMISP(url, key) + + +def loop_attributes(elem): + if 'Attribute' in elem.keys(): + for attribute in elem['Attribute']: + if 'warnings' in attribute.keys(): + for warning in attribute['warnings']: + print("Value {} has a hit in warninglist with name '{}' and id '{}'".format(warning['value'], + warning[ + 'warninglist_name'], + warning[ + 'warninglist_id'])) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Print all warninglist hits for an event.') + parser.add_argument("eventid", type=str, help="The event id of the event to get info of") + args = parser.parse_args() + misp = init(misp_url, misp_key) + evt = misp.search('events', eventid=args.eventid, includeWarninglistHits=1)['response'][0]['Event'] + if 'warnings' in evt.keys(): + print('warnings in entire event:') + print(str(evt['warnings']) + '\n') + print('Warnings at attribute levels:') + loop_attributes(evt) + if 'Object' in evt.keys(): + for obj in evt['Object']: + loop_attributes(obj) diff --git a/examples/freetext.py b/examples/freetext.py index 63c0a65..fdadacc 100755 --- a/examples/freetext.py +++ b/examples/freetext.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse diff --git a/examples/generate_file_objects.py b/examples/generate_file_objects.py index 3269845..7a8dbbd 100755 --- a/examples/generate_file_objects.py +++ b/examples/generate_file_objects.py @@ -5,7 +5,7 @@ import argparse import json try: - from pymisp import MISPEncode + from pymisp import pymisp_json_default, AbstractMISP from pymisp.tools import make_binary_objects except ImportError: pass @@ -43,6 +43,15 @@ def make_objects(path): to_return['references'] += s.ObjectReference if peo: + if hasattr(peo, 'certificates') and hasattr(peo, 'signers'): + # special authenticode case for PE objects + for c in peo.certificates: + to_return['objects'].append(c) + for s in peo.signers: + to_return['objects'].append(s) + del peo.certificates + del peo.signers + del peo.sections to_return['objects'].append(peo) if peo.ObjectReference: to_return['references'] += peo.ObjectReference @@ -51,7 +60,8 @@ def make_objects(path): to_return['objects'].append(fo) if fo.ObjectReference: to_return['references'] += fo.ObjectReference - return json.dumps(to_return, cls=MISPEncode) + return json.dumps(to_return, default=pymisp_json_default) + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Extract indicators out of binaries and returns MISP objects.') @@ -59,6 +69,7 @@ if __name__ == '__main__': group.add_argument("-p", "--path", help="Path to process.") group.add_argument("-c", "--check", action='store_true', help="Check the dependencies.") args = parser.parse_args() + a = AbstractMISP() if args.check: print(check()) diff --git a/examples/generate_meta_feed.py b/examples/generate_meta_feed.py new file mode 100644 index 0000000..5618595 --- /dev/null +++ b/examples/generate_meta_feed.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pymisp.tools import feed_meta_generator +import argparse +from pathlib import Path + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Build meta files for feed') + parser.add_argument("--feed", required=True, help="Path to directory containing the feed.") + args = parser.parse_args() + + feed = Path(args.feed) + + feed_meta_generator(feed) diff --git a/examples/get.py b/examples/get.py index 80e5270..6ca3ce8 100755 --- a/examples/get.py +++ b/examples/get.py @@ -1,15 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse import os -import json -# Usage for pipe masters: ./last.py -l 5h | jq . - proxies = { 'http': 'http://127.0.0.1:8123', 'https': 'http://127.0.0.1:8123', @@ -18,18 +15,6 @@ proxies = { proxies = None -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', proxies=proxies) - - -def get_event(m, event, out=None): - result = m.get_event(event) - if out is None: - print(json.dumps(result) + '\n') - else: - with open(out, 'w') as f: - f.write(json.dumps(result) + '\n') - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get an event from a MISP instance.') @@ -42,6 +27,11 @@ if __name__ == '__main__': print('Output file already exists, abort.') exit(0) - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, proxies=proxies) - get_event(misp, args.event, args.output) + event = misp.get_event(args.event, pythonify=True) + if args.output: + with open(args.output, 'w') as f: + f.write(event.to_json()) + else: + print(event.to_json()) diff --git a/examples/get_attachment.py b/examples/get_attachment.py deleted file mode 100755 index f40f38d..0000000 --- a/examples/get_attachment.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Get an attachment.') - parser.add_argument("-a", "--attribute", type=int, help="Attribute ID to download.") - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - with open('foo', 'wb') as f: - out = misp.get_attachment(args.attribute) - if isinstance(out, dict): - # Fails - print(out) - else: - f.write(out) diff --git a/examples/get_csv.py b/examples/get_csv.py index 5921e53..d7b5b0a 100755 --- a/examples/get_csv.py +++ b/examples/get_csv.py @@ -9,6 +9,7 @@ from keys import misp_url, misp_key, misp_verifycert if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get MISP stuff as CSV.') + parser.add_argument("--controller", default='attributes', help="Attribute to use for the search (events, objects, attributes)") parser.add_argument("-e", "--event_id", help="Event ID to fetch. Without it, it will fetch the whole database.") parser.add_argument("-a", "--attribute", nargs='+', help="Attribute column names") parser.add_argument("-o", "--object_attribute", nargs='+', help="Object attribute column names") @@ -26,7 +27,7 @@ if __name__ == '__main__': if not attr: attr = None print(args.context) - response = pymisp.search(return_format='csv', eventid=args.event_id, requested_attributes=attr, + response = pymisp.search(return_format='csv', controller=args.controller, eventid=args.event_id, requested_attributes=attr, type_attribute=args.misp_types, include_context=args.context) if args.outfile: diff --git a/examples/ioc-2-misp/README.md b/examples/ioc_2_misp/README.md similarity index 100% rename from examples/ioc-2-misp/README.md rename to examples/ioc_2_misp/README.md diff --git a/examples/ioc-2-misp/ioc2misp.py b/examples/ioc_2_misp/ioc2misp.py similarity index 100% rename from examples/ioc-2-misp/ioc2misp.py rename to examples/ioc_2_misp/ioc2misp.py diff --git a/examples/ioc-2-misp/keys.py.sample b/examples/ioc_2_misp/keys.py.sample similarity index 100% rename from examples/ioc-2-misp/keys.py.sample rename to examples/ioc_2_misp/keys.py.sample diff --git a/examples/ioc-2-misp/taxonomy.csv b/examples/ioc_2_misp/taxonomy.csv similarity index 100% rename from examples/ioc-2-misp/taxonomy.csv rename to examples/ioc_2_misp/taxonomy.csv diff --git a/examples/keys.py.sample b/examples/keys.py.sample index 168b765..3c59bfe 100644 --- a/examples/keys.py.sample +++ b/examples/keys.py.sample @@ -4,3 +4,6 @@ misp_url = 'https:///' misp_key = 'Your MISP auth key' # The MISP auth key can be found on the MISP web interface under the automation section misp_verifycert = True +misp_client_cert = '' +proofpoint_sp = '' # Service Principal from TAP (https://threatinsight.proofpoint.com//settings/connected-applications) +proofpoint_secret = '' \ No newline at end of file diff --git a/examples/last.py b/examples/last.py index ed07be5..3fabf2c 100755 --- a/examples/last.py +++ b/examples/last.py @@ -1,43 +1,48 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert +try: + from keys import misp_client_cert +except ImportError: + misp_client_cert = '' import argparse import os -import json # Usage for pipe masters: ./last.py -l 5h | jq . - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - - -def download_last(m, last, out=None): - result = m.download_last(last) - if out is None: - if 'response' in result: - print(json.dumps(result['response'])) - else: - print('No results for that time period') - exit(0) - else: - with open(out, 'w') as f: - f.write(json.dumps(result['response'])) +# Usage in case of large data set and pivoting page by page: python3 last.py -l 48h -m 10 -p 2 | jq .[].Event.info if __name__ == '__main__': parser = argparse.ArgumentParser(description='Download latest events from a MISP instance.') parser.add_argument("-l", "--last", required=True, help="can be defined in days, hours, minutes (for example 5d or 12h or 30m).") + parser.add_argument("-m", "--limit", required=False, default="10", help="Add the limit of records to get (by default, the limit is set to 10)") + parser.add_argument("-p", "--page", required=False, default="1", help="Add the page to request to paginate over large dataset (by default page is set to 1)") parser.add_argument("-o", "--output", help="Output file") args = parser.parse_args() if args.output is not None and os.path.exists(args.output): - print('Output file already exists, abord.') + print('Output file already exists, aborted.') exit(0) - misp = init(misp_url, misp_key) + if misp_client_cert == '': + misp_client_cert = None + else: + misp_client_cert = (misp_client_cert) - download_last(misp, args.last, args.output) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert, cert=misp_client_cert) + result = misp.search(publish_timestamp=args.last, limit=args.limit, page=args.page, pythonify=True) + + if not result: + print('No results for that time period') + exit(0) + + if args.output: + with open(args.output, 'w') as f: + for r in result: + f.write(r.to_json() + '\n') + else: + for r in result: + print(r.to_json()) diff --git a/examples/load_csv.py b/examples/load_csv.py index 892dfbb..7732196 100755 --- a/examples/load_csv.py +++ b/examples/load_csv.py @@ -10,7 +10,7 @@ from pymisp import MISPEvent try: from keys import misp_url, misp_key, misp_verifycert - from pymisp import ExpandedPyMISP + from pymisp import PyMISP offline = False except ImportError as e: offline = True @@ -22,9 +22,14 @@ Example: load_csv.py -n file -p /tmp/foo.csv + CSV sample file: tests/csv_testfiles/valid_fieldnames.csv + + * If you want to force the fieldnames: load_csv.py -n file -p /tmp/foo.csv -f SHA1 fileName size-in-bytes + + CSV sample file: tests/csv_testfiles/invalid_fieldnames.csv ''' @@ -35,6 +40,8 @@ if __name__ == '__main__': parser.add_argument("-f", "--fieldnames", nargs='*', default=[], help="Fieldnames of the CSV, have to match the object-relation allowed in the template. If empty, the fieldnames of the CSV have to match the template.") parser.add_argument("-s", "--skip_fieldnames", action='store_true', help="Skip fieldnames in the CSV.") parser.add_argument("-d", "--dump", action='store_true', help="(Debug) Dump the object in the terminal.") + parser.add_argument("--delimiter", type=str, default=',', help="Delimiter between firlds in the CSV. Default: ','.") + parser.add_argument("--quotechar", type=str, default='"', help="Quote character of the fields in the CSV. Default: '\"'.") # Interact with MISP misp_group = parser.add_mutually_exclusive_group() @@ -48,7 +55,8 @@ if __name__ == '__main__': else: has_fieldnames = args.skip_fieldnames csv_loader = CSVLoader(template_name=args.object_name, csv_path=args.path, - fieldnames=args.fieldnames, has_fieldnames=has_fieldnames) + fieldnames=args.fieldnames, has_fieldnames=has_fieldnames, + delimiter=args.delimiter, quotechar=args.quotechar) objects = csv_loader.load() if args.dump: @@ -58,13 +66,13 @@ if __name__ == '__main__': if offline: print('You are in offline mode, quitting.') else: - misp = ExpandedPyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) + misp = PyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) if args.new_event: event = MISPEvent() event.info = args.new_event for o in objects: event.add_object(**o) - new_event = misp.add_event(event) + new_event = misp.add_event(event, pythonify=True) if isinstance(new_event, str): print(new_event) elif 'id' in new_event: @@ -72,9 +80,9 @@ if __name__ == '__main__': else: print('Something went wrong:') print(new_event) - else: + elif args.update_event: for o in objects: - new_object = misp.add_object(args.update_event, o) + new_object = misp.add_object(args.update_event, o, pythonify=True) if isinstance(new_object, str): print(new_object) elif new_object.attributes: @@ -82,3 +90,5 @@ if __name__ == '__main__': else: print('Something went wrong:') print(new_event) + else: + print('you need to pass either a event info field (flag -i), or the event ID you want to update (flag -u)') diff --git a/examples/proofpoint_tap.py b/examples/proofpoint_tap.py new file mode 100644 index 0000000..d76aa3f --- /dev/null +++ b/examples/proofpoint_tap.py @@ -0,0 +1,203 @@ +import requests +from requests.auth import HTTPBasicAuth +import json +from pymisp import ExpandedPyMISP, MISPEvent +from keys import misp_url, misp_key, misp_verifycert, proofpoint_sp, proofpoint_secret +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +if proofpoint_secret == '': + print('Set the proofpoint_secret in keys.py before running. Exiting...') + quit() + +# initialize PyMISP and set url for Panorama +misp = ExpandedPyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) + +urlSiem = "https://tap-api-v2.proofpoint.com/v2/siem/all" + +alertType = ("messagesDelivered", "messagesBlocked", "clicksPermitted", "clicksBlocked") + +# max query is 1h, and we want Proofpoint TAP api to return json +queryString = { + "sinceSeconds": "3600", + "format": "json" +} + + + +responseSiem = requests.request("GET", urlSiem, params=queryString, auth=HTTPBasicAuth(proofpoint_sp, proofpoint_secret)) +if 'Credentials authentication failed' in responseSiem.text: + print('Credentials invalid, please edit keys.py and try again') + quit() + +jsonDataSiem = json.loads(responseSiem.text) + +for alert in alertType: + for messages in jsonDataSiem[alert]: + # initialize and set MISPEvent() + event = MISPEvent() + if alert == "messagesDelivered" or alert == "messagesBlocked": + if alert == "messagesDelivered": + event.info = alert + event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config + event.threat_level_id = 2 # setting this to 0 breaks the integration + event.analysis = 0 # Optional, defaults to 0 (initial analysis) + else: + event.info = alert + event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config + event.threat_level_id = 2 # BLOCKED = LOW + event.analysis = 0 # Optional, defaults to 0 (initial analysis) + + recipient = event.add_attribute('email-dst', messages["recipient"][0]) + recipient.comment = 'recipient address' + + sender = event.add_attribute('email-src', messages["sender"]) + sender.comment = 'sender address' + + if messages["fromAddress"] is not None and messages["fromAddress"] != "" : + fromAddress = event.add_attribute('email-src-display-name', messages["fromAddress"]) + + headerFrom = event.add_attribute('email-header', messages["headerFrom"]) + headerFrom.comment = 'email header from' + + senderIP = event.add_attribute('ip-src', messages["senderIP"]) + senderIP.comment = 'sender IP' + + subject = event.add_attribute('email-subject', messages["subject"]) + subject.comment = 'email subject' + + if messages["quarantineFolder"] is not None and messages["quarantineFolder"] != "": + quarantineFolder = event.add_attribute('comment', messages["quarantineFolder"]) + quarantineFolder.comment = 'quarantine folder' + + if messages["quarantineRule"] is not None and messages["quarantineRule"] != "": + quarantineRule = event.add_attribute('comment', messages["quarantineRule"]) + quarantineRule.comment = 'quarantine rule' + + messageSize = event.add_attribute('size-in-bytes', messages["messageSize"]) + messageSize.comment = 'size of email in bytes' + + malwareScore = event.add_attribute('comment', messages["malwareScore"]) + malwareScore.comment = 'malware score' + + phishScore = event.add_attribute('comment', messages["phishScore"]) + phishScore.comment = 'phish score' + + spamScore = event.add_attribute('comment', messages["spamScore"]) + spamScore.comment = 'spam score' + + imposterScore = event.add_attribute('comment', messages["impostorScore"]) + imposterScore.comment = 'impostor score' + + completelyRewritten = event.add_attribute('comment', messages["completelyRewritten"]) + completelyRewritten.comment = 'proofpoint url defense' + + # grab the threat info for each message in TAP + for threatInfo in messages["threatsInfoMap"]: + threat_type = { + "url": "url", + "attachment": "email-attachment", + "message": "email-body" + } + + threat = event.add_attribute(threat_type.get(threatInfo["threatType"]), threatInfo["threat"]) + threat.comment = 'threat' + + threatUrl = event.add_attribute('link', threatInfo["threatUrl"]) + threatUrl.comment = 'link to threat in TAP' + + threatStatus = event.add_attribute('comment', threatInfo["threatStatus"]) + threatStatus.comment = "proofpoint's threat status" + + event.add_tag(threatInfo["classification"]) + + # get campaignID from each TAP alert and query campaign API + if threatInfo["campaignID"] is not None and threatInfo["campaignID"] != "": + urlCampaign = "https://tap-api-v2.proofpoint.com/v2/campaign/" + threatInfo["campaignID"] + responseCampaign = requests.request("GET", urlCampaign, auth=HTTPBasicAuth(proofpoint_sp, proofpoint_secret)) + + jsonDataCampaign = json.loads(responseCampaign.text) + + campaignType = ("actors", "families", "malware", "techniques") + + # loop through campaignType and grab tags to add to MISP event + for tagType in campaignType: + for tag in jsonDataCampaign[tagType]: + event.add_tag(tag['name']) + + # grab which policy route the message took + for policy in messages["policyRoutes"]: + policyRoute = event.add_attribute('comment', policy) + policyRoute.comment = 'email policy route' + + # was the threat in the body of the email or is it an attachment? + for parts in messages["messageParts"]: + disposition = event.add_attribute('comment', parts["disposition"]) + disposition.comment = 'email body or attachment' + + # sha256 hash of threat + if parts["sha256"] is not None and parts["sha256"] != "": + sha256 = event.add_attribute('sha256', parts["sha256"]) + sha256.comment = 'sha256 hash' + + # md5 hash of threat + if parts["md5"] is not None and parts["md5"] != "": + md5 = event.add_attribute('md5', parts["md5"]) + md5.comment = 'md5 hash' + + # filename of threat + if parts["filename"] is not None and parts["filename"] != "": + filename = event.add_attribute('filename', parts["filename"]) + filename.comment = 'filename' + + misp.add_event(event.to_json()) + + if alert == "clicksPermitted" or alert == "clicksBlocked": + if alert == "clicksPermitted": + print(alert + " is a permitted click") + event.info = alert + event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config + event.threat_level_id = 2 # setting this to 0 breaks the integration + event.analysis = 0 # Optional, defaults to 0 (initial analysis) + else: + print(alert + " is a blocked click") + event.info = alert + event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config + event.threat_level_id = 2 # BLOCKED = LOW + event.analysis = 0 # Optional, defaults to 0 (initial analysis) + + event.add_tag(messages["classification"]) + + campaignId = event.add_attribute('campaign-id', messages["campaignId"][0]) + campaignId.comment = 'campaignId' + + clickIP = event.add_attribute('ip-src', messages["clickIP"]) + clickIP.comment = 'clickIP' + + clickTime = event.add_attribute('datetime', messages["clickTime"]) + clickTime.comment = 'clicked threat' + + threatTime = event.add_attribute('datetime', messages["threatTime"]) + threatTime.comment = 'identified threat' + + GUID = event.add_attribute('comment', messages["GUID"]) + GUID.comment = 'PPS message ID' + + recipient = event.add_attribute('email-dst', messages["recipient"][0]) + recipient.comment = 'recipient address' + + sender = event.add_attribute('email-src', messages["sender"]) + sender.comment = 'sender address' + + senderIP = event.add_attribute('ip-src', messages["senderIP"]) + senderIP.comment = 'sender IP' + + threatURL = event.add_attribute('link', messages["threatURL"]) + threatURL.comment = 'link to threat in TAP' + + url = event.add_attribute('link', messages["url"]) + url.comment = 'malicious url clicked' + + userAgent = event.add_attribute('user-agent', messages["userAgent"]) + + misp.add_event(event.to_json()) diff --git a/examples/proofpoint_vap.py b/examples/proofpoint_vap.py new file mode 100644 index 0000000..6cd863a --- /dev/null +++ b/examples/proofpoint_vap.py @@ -0,0 +1,65 @@ +import requests +import json +from pymisp import ExpandedPyMISP, MISPEvent, MISPOrganisation +from keys import misp_url, misp_key, misp_verifycert, proofpoint_key + +# initialize PyMISP and set url for Panorama +misp = ExpandedPyMISP(url=misp_url, key=misp_key, ssl=misp_verifycert) + +urlVap = "https://tap-api-v2.proofpoint.com/v2/people/vap?window=30" # Window can be 14, 30, and 90 Days + +headers = { + 'Authorization': "Basic " + proofpoint_key +} + +responseVap = requests.request("GET", urlVap, headers=headers) + +jsonDataVap = json.loads(responseVap.text) + +for alert in jsonDataVap["users"]: + orgc = MISPOrganisation() + orgc.name = 'Proofpoint' + orgc.id = '#{ORGC.ID}' # organisation id + orgc.uuid = '#{ORGC.UUID}' # organisation uuid + # initialize and set MISPEvent() + event = MISPEvent() + event.Orgc = orgc + event.info = 'Very Attacked Person ' + jsonDataVap["interval"] + event.distribution = 0 # Optional, defaults to MISP.default_event_distribution in MISP config + event.threat_level_id = 2 # setting this to 0 breaks the integration + event.analysis = 0 # Optional, defaults to 0 (initial analysis) + + totalVapUsers = event.add_attribute('counter', jsonDataVap["totalVapUsers"], comment="Total VAP Users") + + averageAttackIndex = event.add_attribute('counter', jsonDataVap["averageAttackIndex"], comment="Average Attack Count") + + vapAttackIndexThreshold = event.add_attribute('counter', jsonDataVap["vapAttackIndexThreshold"], comment="Attack Threshold") + + emails = event.add_attribute('email-dst', alert["identity"]["emails"], comment="Email Destination") + + attack = event.add_attribute('counter', alert["threatStatistics"]["attackIndex"], comment="Attack Count") + + vip = event.add_attribute('other', str(alert["identity"]["vip"]), comment="VIP") + + guid = event.add_attribute('other', alert["identity"]["guid"], comment="GUID") + + if alert["identity"]["customerUserId"] is not None: + customerUserId = event.add_attribute('other', alert["identity"]["customerUserId"], comment="Customer User Id") + + if alert["identity"]["department"] is not None: + department = event.add_attribute(alert['other', "identity"]["department"], comment="Department") + + if alert["identity"]["location"] is not None: + location = event.add_attribute('other', alert["identity"]["location"], comment="Location") + + if alert["identity"]["name"] is not None: + + name = event.add_attribute('target-user', alert["identity"]["name"], comment="Name") + + if alert["identity"]["title"] is not None: + + title = event.add_attribute('other', alert["identity"]["title"], comment="Title") + + event.add_tag("VAP") + + misp.add_event(event.to_json()) diff --git a/examples/search_sighting.py b/examples/search_sighting.py index 8e517c7..67e9b4e 100644 --- a/examples/search_sighting.py +++ b/examples/search_sighting.py @@ -14,7 +14,7 @@ def init(url, key): def search_sighting(m, context, out=None, **kwargs): - result = m.sighting_search(context, **kwargs) + result = m.search_sightings(context, **kwargs) if out is None: print(json.dumps(result['response'])) else: diff --git a/examples/searchall.py b/examples/searchall.py deleted file mode 100755 index 6efe548..0000000 --- a/examples/searchall.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key,misp_verifycert -import argparse -import os -import json - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - - -def searchall(m, search, quiet, url, out=None): - result = m.search_all(search) - if quiet: - for e in result['response']: - print('{}{}{}\n'.format(url, '/events/view/', e['Event']['id'])) - elif out is None: - print(json.dumps(result['response'])) - else: - with open(out, 'w') as f: - f.write(json.dumps(result['response'])) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Get all the events matching a value.') - parser.add_argument("-s", "--search", required=True, help="String to search.") - parser.add_argument("-q", "--quiet", action='store_true', help="Only display URLs to MISP") - parser.add_argument("-o", "--output", help="Output file") - - args = parser.parse_args() - - if args.output is not None and os.path.exists(args.output): - print('Output file already exists, abord.') - exit(0) - - misp = init(misp_url, misp_key) - - searchall(misp, args.search, args.quiet, misp_url, args.output) diff --git a/examples/server_sync_check_conn.py b/examples/server_sync_check_conn.py new file mode 100755 index 0000000..8833236 --- /dev/null +++ b/examples/server_sync_check_conn.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import requests +import json + +# Suppress those "Unverified HTTPS request is being made" +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +from keys import misp_url, misp_key, misp_verifycert +proxies = { + +} + +''' +Checks if the connection to a sync server works +returns json object +''' + +def check_connection(connection_number): + + misp_headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': misp_key} + req = requests.get(misp_url + 'servers/testConnection/{}'.format(connection_number), verify=misp_verifycert, headers=misp_headers, proxies=proxies) + + result = json.loads(req.text) + return(result) + + +if __name__ == "__main__": + + result = check_connection(1) + print(result) diff --git a/examples/sharing_groups.py b/examples/sharing_groups.py index 005b432..dea34da 100755 --- a/examples/sharing_groups.py +++ b/examples/sharing_groups.py @@ -1,24 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get a list of the sharing groups from the MISP instance.') - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - sharing_groups = misp.get_sharing_groups() - print (sharing_groups) + sharing_groups = misp.sharing_groups(pythonify=True) + print(sharing_groups) diff --git a/examples/show_sightings.py b/examples/show_sightings.py new file mode 100644 index 0000000..dd2cbe4 --- /dev/null +++ b/examples/show_sightings.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +Koen Van Impe + +List all the sightings + +Put this script in crontab to run every day + 25 4 * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/show_sightings.py + +''' + +from pymisp import ExpandedPyMISP +from keys import misp_url, misp_key, misp_verifycert + +import sys +import time +from datetime import datetime +import smtplib +import mimetypes +from email.mime.multipart import MIMEMultipart +from email import encoders +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +import argparse +import string + +def init(url, key, verifycert): + ''' + Template to get MISP module started + ''' + return ExpandedPyMISP(url, key, verifycert, 'json') + + +def set_drift_timestamp(drift_timestamp, drift_timestamp_path): + ''' + Save the timestamp in a (local) file + ''' + try: + with open(drift_timestamp_path, 'w+') as f: + f.write(str(drift_timestamp)) + return True + except IOError: + sys.exit("Unable to write drift_timestamp %s to %s" % (drift_timestamp, drift_timestamp_path)) + return False + + +def get_drift_timestamp(drift_timestamp_path): + ''' + From when do we start with the sightings? + ''' + try: + with open(drift_timestamp_path) as f: + drift = f.read() + if drift: + drift = int(float(drift)) + else: + drift = 0 + except IOError: + drift = 0 + + return drift + + +def search_sightings(misp, from_timestamp, end_timestamp): + ''' + Search all the sightings + ''' + completed_sightings = [] + + try: + found_sightings = misp.search_sightings(date_from=from_timestamp, date_to=end_timestamp) + except Exception as e: + sys.exit('Unable to search for sightings') + + if found_sightings is not None: + for s in found_sightings: + if 'Sighting' in s: + sighting = s['Sighting'] + if 'attribute_id' in sighting: + attribute_id = sighting['attribute_id'] + + # Query the attribute and event to get the details + try: + attribute = misp.get_attribute(attribute_id) + except Exception as e: + print("Unable to fetch attribute") + continue + + if 'Attribute' in attribute and 'uuid' in attribute['Attribute']: + event_details = misp.get_event(attribute['Attribute']['event_id']) + event_info = event_details['Event']['info'] + attribute_uuid = attribute['Attribute']['uuid'] + to_ids = attribute['Attribute']['to_ids'] + completed_sightings.append({'attribute_uuid': attribute_uuid, 'date_sighting': sighting['date_sighting'], 'source': sighting['source'], 'type': sighting['type'], 'uuid': sighting['uuid'], 'event_id': attribute['Attribute']['event_id'], 'value': attribute['Attribute']['value'], 'attribute_id': attribute['Attribute']['id'], 'event_title': event_info, 'to_ids': to_ids}) + else: + continue + + return completed_sightings + + +if __name__ == '__main__': + smtp_from = 'INSERT_FROM' + smtp_to = 'INSERT_TO' + smtp_server = 'localhost' + report_sightings = '' + ts_format = '%Y-%m-%d %H:%M:%S' + drift_timestamp_path = '/home/mispuser/PyMISP/examples/show_sightings.drift' + + parser = argparse.ArgumentParser(description="Show all the sightings.") + parser.add_argument('-m', '--mail', action='store_true', help='Mail the report') + parser.add_argument('-o', '--mailoptions', action='store', help='mailoptions: \'smtp_from=INSERT_FROM;smtp_to=INSERT_TO;smtp_server=localhost\'') + + args = parser.parse_args() + misp = init(misp_url, misp_key, misp_verifycert) + + start_timestamp = get_drift_timestamp(drift_timestamp_path=drift_timestamp_path) + end_timestamp = time.time() + start_timestamp_s = datetime.fromtimestamp(start_timestamp).strftime(ts_format) + end_timestamp_s = datetime.fromtimestamp(end_timestamp).strftime(ts_format) + + # Get all attribute sightings + found_sightings = search_sightings(misp, start_timestamp, end_timestamp) + if found_sightings: + for s in found_sightings: + if int(s['type']) == 0: + s_type = 'TP' + else: + s_type = 'FP' + date_sighting = datetime.fromtimestamp(int(s['date_sighting'])).strftime(ts_format) + s_title = s['event_title'] + s_title = s_title.replace('\r','').replace('\n','').replace('\t','') + source = s['source'] + if not s['source']: + source = 'N/A' + report_sightings = report_sightings + '%s for [%s] (%s) in event [%s] (%s) on %s from %s (to_ids flag: %s) \n' % ( s_type, s['value'], s['attribute_id'], s_title, s['event_id'], date_sighting, source, s['to_ids']) + + set_drift_timestamp(end_timestamp, drift_timestamp_path) + else: + report_sightings = 'No sightings found' + + # Mail options + if args.mail: + if args.mailoptions: + mailoptions = args.mailoptions.split(';') + for s in mailoptions: + if s.split('=')[0] == 'smtp_from': + smtp_from = s.split('=')[1] + if s.split('=')[0] == 'smtp_to': + smtp_to = s.split('=')[1] + if s.split('=')[0] == 'smtp_server': + smtp_server = s.split('=')[1] + + report_sightings_body = 'MISP Sightings report for %s between %s and %s\n-------------------------------------------------------------------------------\n\n' % (misp_url, start_timestamp_s, end_timestamp_s) + report_sightings_body = report_sightings_body + report_sightings + subject = 'Report of sightings between %s and %s' % (start_timestamp_s, end_timestamp_s) + + msg = MIMEMultipart() + msg['From'] = smtp_from + msg['To'] = smtp_to + msg['Subject'] = subject + + msg.attach(MIMEText(report_sightings_body, 'text')) + server = smtplib.SMTP(smtp_server) + server.sendmail(smtp_from, smtp_to, msg.as_string()) + + else: + print(report_sightings) diff --git a/examples/sighting.py b/examples/sighting.py deleted file mode 100755 index d6c8323..0000000 --- a/examples/sighting.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Add sighting.') - parser.add_argument("-f", "--json_file", required=True, help="The name of the json file describing the attribute you want to add sighting to.") - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - misp.sighting_per_json(args.json_file) diff --git a/examples/situational-awareness/README.md b/examples/situational_awareness/README.md similarity index 89% rename from examples/situational-awareness/README.md rename to examples/situational_awareness/README.md index fb896c6..5a0e071 100644 --- a/examples/situational-awareness/README.md +++ b/examples/situational_awareness/README.md @@ -4,8 +4,8 @@ * It will also generate a html document with a table (attribute\_table.html) containing count for each type of attribute. * test\_attribute\_treemap.html is a quick page made to visualize both treemap and table at the same time. -* tags\_count.py is a script that count the number of occurences of every tags in a fetched sample of Events in a given period of time. -* tag\_search.py is a script that count the number of occurences of a given tag in a fetched sample of Events in a given period of time. +* tags\_count.py is a script that count the number of occurrences of every tags in a fetched sample of Events in a given period of time. +* tag\_search.py is a script that count the number of occurrences of a given tag in a fetched sample of Events in a given period of time. * Events will be fetched from _days_ days ago to today. * _begindate_ is the beginning of the studied period. If it is later than today, an error will be raised. * _enddate_ is the end of the studied period. If it is earlier than _begindate_, an error will be raised. diff --git a/examples/situational_awareness/__init__.py b/examples/situational_awareness/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/situational-awareness/attribute_treemap.py b/examples/situational_awareness/attribute_treemap.py similarity index 100% rename from examples/situational-awareness/attribute_treemap.py rename to examples/situational_awareness/attribute_treemap.py diff --git a/examples/situational-awareness/bokeh_tools.py b/examples/situational_awareness/bokeh_tools.py similarity index 100% rename from examples/situational-awareness/bokeh_tools.py rename to examples/situational_awareness/bokeh_tools.py diff --git a/examples/situational-awareness/date_tools.py b/examples/situational_awareness/date_tools.py similarity index 100% rename from examples/situational-awareness/date_tools.py rename to examples/situational_awareness/date_tools.py diff --git a/examples/situational-awareness/pygal_tools.py b/examples/situational_awareness/pygal_tools.py similarity index 100% rename from examples/situational-awareness/pygal_tools.py rename to examples/situational_awareness/pygal_tools.py diff --git a/examples/situational-awareness/style.css b/examples/situational_awareness/style.css similarity index 100% rename from examples/situational-awareness/style.css rename to examples/situational_awareness/style.css diff --git a/examples/situational-awareness/style2.css b/examples/situational_awareness/style2.css similarity index 100% rename from examples/situational-awareness/style2.css rename to examples/situational_awareness/style2.css diff --git a/examples/situational-awareness/tag_scatter.py b/examples/situational_awareness/tag_scatter.py similarity index 100% rename from examples/situational-awareness/tag_scatter.py rename to examples/situational_awareness/tag_scatter.py diff --git a/examples/situational-awareness/tag_search.py b/examples/situational_awareness/tag_search.py similarity index 100% rename from examples/situational-awareness/tag_search.py rename to examples/situational_awareness/tag_search.py diff --git a/examples/situational-awareness/tags_count.py b/examples/situational_awareness/tags_count.py similarity index 100% rename from examples/situational-awareness/tags_count.py rename to examples/situational_awareness/tags_count.py diff --git a/examples/situational-awareness/tags_to_graphs.py b/examples/situational_awareness/tags_to_graphs.py similarity index 100% rename from examples/situational-awareness/tags_to_graphs.py rename to examples/situational_awareness/tags_to_graphs.py diff --git a/examples/situational-awareness/test_attribute_treemap.html b/examples/situational_awareness/test_attribute_treemap.html similarity index 100% rename from examples/situational-awareness/test_attribute_treemap.html rename to examples/situational_awareness/test_attribute_treemap.html diff --git a/examples/situational-awareness/tools.py b/examples/situational_awareness/tools.py similarity index 100% rename from examples/situational-awareness/tools.py rename to examples/situational_awareness/tools.py diff --git a/examples/stats.py b/examples/stats.py deleted file mode 100755 index 41d6b28..0000000 --- a/examples/stats.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Output attributes statistics from a MISP instance.') - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - print (misp.get_attributes_statistics(misp, percentage=True)) - print (misp.get_attributes_statistics(context='category', percentage=True)) diff --git a/examples/stats_report.py b/examples/stats_report.py new file mode 100755 index 0000000..ef2b63c --- /dev/null +++ b/examples/stats_report.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +Koen Van Impe +Maxime Thiebaut + +Generate a report of your MISP statistics +Put this script in crontab to run every /15 or /60 + */5 * * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/stats_report.py -t 30d -m -v + +Do inline config in "main" + +''' + +from pymisp import ExpandedPyMISP +from keys import misp_url, misp_key, misp_verifycert +import argparse +import os +from datetime import datetime +from datetime import date +import time +import sys +import smtplib +import mimetypes +from email.mime.multipart import MIMEMultipart +from email import encoders +from email.mime.base import MIMEBase +from email.mime.text import MIMEText + +# Suppress those "Unverified HTTPS request is being made" +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def init(url, key, verifycert): + ''' + Template to get MISP module started + ''' + return ExpandedPyMISP(url, key, verifycert, 'json') + + +def get_data(misp, timeframe, date_from=None, date_to=None): + ''' + Get the event date to build our report + ''' + number_of_misp_events = 0 + number_of_attributes = 0 + number_of_attributes_to_ids = 0 + attr_type = {} + attr_category = {} + tags_type = {} + tags_tlp = {'tlp:white': 0, 'tlp:green': 0, 'tlp:amber': 0, 'tlp:red': 0} + tags_misp_galaxy_mitre = {} + tags_misp_galaxy = {} + tags_misp_galaxy_threat_actor = {} + galaxies = {} + galaxies_cluster = {} + threat_levels_counts = [0, 0, 0, 0] + analysis_completion_counts = [0, 0, 0] + report = {} + + try: + if date_from and date_to: + stats_event_response = misp.search(date_from=date_from, date_to=date_to) + else: + stats_event_response = misp.search(last=timeframe) + + # Number of new or updated events since timestamp + report['number_of_misp_events'] = len(stats_event_response) + report['misp_events'] = [] + + for event in stats_event_response: + event_data = event['Event'] + + timestamp = datetime.utcfromtimestamp(int(event_data['timestamp'])).strftime(ts_format) + publish_timestamp = datetime.utcfromtimestamp(int(event_data['publish_timestamp'])).strftime(ts_format) + + threat_level_id = int(event_data['threat_level_id']) - 1 + threat_levels_counts[threat_level_id] = threat_levels_counts[threat_level_id] + 1 + threat_level_id = threat_levels[threat_level_id] + + analysis_id = int(event_data['analysis']) + analysis_completion_counts[analysis_id] = analysis_completion_counts[analysis_id] + 1 + analysis = analysis_completion[analysis_id] + + report['misp_events'].append({'id': event_data['id'], 'title': event_data['info'].replace('\n', '').encode('utf-8'), 'date': event_data['date'], 'timestamp': timestamp, 'publish_timestamp': publish_timestamp, 'threat_level': threat_level_id, 'analysis_completion': analysis}) + + # Walk through the attributes + if 'Attribute' in event_data: + event_attr = event_data['Attribute'] + for attr in event_attr: + number_of_attributes = number_of_attributes + 1 + + type = attr['type'] + category = attr['category'] + to_ids = attr['to_ids'] + + if to_ids: + number_of_attributes_to_ids = number_of_attributes_to_ids + 1 + + if type in attr_type: + attr_type[type] = attr_type[type] + 1 + else: + attr_type[type] = 1 + + if category in attr_category: + attr_category[category] = attr_category[category] + 1 + else: + attr_category[category] = 1 + + # Process tags + if 'Tag' in event_data: + tags_attr = event_data['Tag'] + for tag in tags_attr: + tag_title = tag['name'] + + if tag_title.lower().replace(' ', '') in tags_tlp: + tags_tlp[tag_title.lower().replace(' ', '')] = tags_tlp[tag_title.lower().replace(' ', '')] + 1 + + if 'misp-galaxy:mitre-' in tag_title: + if tag_title in tags_misp_galaxy_mitre: + tags_misp_galaxy_mitre[tag_title] = tags_misp_galaxy_mitre[tag_title] + 1 + else: + tags_misp_galaxy_mitre[tag_title] = 1 + + if 'misp-galaxy:threat-actor=' in tag_title: + if tag_title in tags_misp_galaxy_threat_actor: + tags_misp_galaxy_threat_actor[tag_title] = tags_misp_galaxy_threat_actor[tag_title] + 1 + else: + tags_misp_galaxy_threat_actor[tag_title] = 1 + elif 'misp-galaxy:' in tag_title: + if tag_title in tags_misp_galaxy: + tags_misp_galaxy[tag_title] = tags_misp_galaxy[tag_title] + 1 + else: + tags_misp_galaxy[tag_title] = 1 + + if tag_title in tags_type: + tags_type[tag_title] = tags_type[tag_title] + 1 + else: + tags_type[tag_title] = 1 + + # Process the galaxies + if 'Galaxy' in event_data: + galaxy_attr = event_data['Galaxy'] + for galaxy in galaxy_attr: + galaxy_title = galaxy['type'] + + if galaxy_title in galaxies: + galaxies[galaxy_title] = galaxies[galaxy_title] + 1 + else: + galaxies[galaxy_title] = 1 + + for cluster in galaxy['GalaxyCluster']: + cluster_value = cluster['type'] + if cluster_value in galaxies_cluster: + galaxies_cluster[cluster_value] = galaxies_cluster[cluster_value] + 1 + else: + galaxies_cluster[cluster_value] = 1 + report['number_of_attributes'] = number_of_attributes + report['number_of_attributes_to_ids'] = number_of_attributes_to_ids + report['attr_type'] = attr_type + report['attr_category'] = attr_category + report['tags_type'] = tags_type + report['tags_tlp'] = tags_tlp + report['tags_misp_galaxy_mitre'] = tags_misp_galaxy_mitre + report['tags_misp_galaxy'] = tags_misp_galaxy + report['tags_misp_galaxy_threat_actor'] = tags_misp_galaxy_threat_actor + report['galaxies'] = galaxies + report['galaxies_cluster'] = galaxies_cluster + + # General MISP statistics + user_statistics = misp.users_statistics() + if user_statistics and 'errors' not in user_statistics: + report['user_statistics'] = user_statistics + + # Return the report data + return report + except Exception as e: + sys.exit('Unable to get statistics from MISP') + + +def build_report(report, timeframe, misp_url, sanitize_report=True): + ''' + Build the body of the report and optional attachments + ''' + attachments = {} + + now = datetime.now() + current_date = now.strftime(ts_format) + if timeframe: + report_body = "MISP Report %s for last %s on %s\n-------------------------------------------------------------------------------" % (current_date, timeframe, misp_url) + else: + report_body = "MISP Report %s from %s to %s on %s\n-------------------------------------------------------------------------------" % (current_date, date_from, date_to, misp_url) + + report_body = report_body + '\nNew or updated events: %s' % report['number_of_misp_events'] + report_body = report_body + '\nNew or updated attributes: %s' % report['number_of_attributes'] + report_body = report_body + '\nNew or updated attributes with IDS flag: %s' % report['number_of_attributes_to_ids'] + report_body = report_body + '\n' + if 'user_statistics' in report: + report_body = report_body + '\nTotal events: %s' % report['user_statistics']['stats']['event_count'] + report_body = report_body + '\nTotal attributes: %s' % report['user_statistics']['stats']['attribute_count'] + report_body = report_body + '\nTotal users: %s' % report['user_statistics']['stats']['user_count'] + report_body = report_body + '\nTotal orgs: %s' % report['user_statistics']['stats']['org_count'] + report_body = report_body + '\nTotal correlation: %s' % report['user_statistics']['stats']['correlation_count'] + report_body = report_body + '\nTotal proposals: %s' % report['user_statistics']['stats']['proposal_count'] + + report_body = report_body + '\n\n' + + if args.mispevent: + report_body = report_body + '\nNew or updated events\n-------------------------------------------------------------------------------' + attachments['misp_events'] = 'ID;Title;Date;Updated;Published;ThreatLevel;AnalysisStatus' + for el in report['misp_events']: + report_body = report_body + '\n #%s %s (%s) \t%s \n\t\t\t\t(Date: %s, Updated: %s, Published: %s)' % (el['id'], el['threat_level'], el['analysis_completion'], el['title'].decode('utf-8'), el['date'], el['timestamp'], el['publish_timestamp']) + attachments['misp_events'] = attachments['misp_events'] + '\n%s;%s;%s;%s;%s;%s;%s' % (el['id'], el['title'].decode('utf-8'), el['date'], el['timestamp'], el['publish_timestamp'], el['threat_level'], el['analysis_completion']) + + report_body, attachments['attr_category'] = add_report_body(report_body, 'New or updated attributes - Category', report['attr_category'], 'AttributeCategory;Qt') + report_body, attachments['attr_type'] = add_report_body(report_body, 'New or updated attributes - Type', report['attr_type'], 'AttributeType;Qt') + report_body, attachments['tags_tlp'] = add_report_body(report_body, 'TLP Codes', report['tags_tlp'], 'TLP;Qt') + report_body, attachments['tags_misp_galaxy'] = add_report_body(report_body, 'Tag MISP Galaxy', report['tags_misp_galaxy'], 'MISPGalaxy;Qt') + report_body, attachments['tags_misp_galaxy_mitre'] = add_report_body(report_body, 'Tag MISP Galaxy Mitre', report['tags_misp_galaxy_mitre'], 'MISPGalaxyMitre;Qt') + report_body, attachments['tags_misp_galaxy_threat_actor'] = add_report_body(report_body, 'Tag MISP Galaxy Threat Actor', report['tags_misp_galaxy_threat_actor'], 'MISPGalaxyThreatActor;Qt') + report_body, attachments['tags_type'] = add_report_body(report_body, 'Tags', report['tags_type'], 'Tag;Qt') + report_body, attachments['galaxies'] = add_report_body(report_body, 'Galaxies', report['galaxies'], 'Galaxies;Qt') + report_body, attachments['galaxies_cluster'] = add_report_body(report_body, 'Galaxies Cluster', report['galaxies_cluster'], 'Galaxies;Qt') + + if sanitize_report: + mitre_tactic = get_sanitized_report(report['tags_misp_galaxy_mitre'], 'ATT&CK Tactic') + mitre_group = get_sanitized_report(report['tags_misp_galaxy_mitre'], 'ATT&CK Group') + mitre_software = get_sanitized_report(report['tags_misp_galaxy_mitre'], 'ATT&CK Software') + threat_actor = get_sanitized_report(report['tags_misp_galaxy_threat_actor'], 'MISP Threat Actor') + misp_tag = get_sanitized_report(report['tags_type'], 'MISP Tags', False, True) + + report_body, attachments['mitre_tactics'] = add_report_body(report_body, 'MITRE ATT&CK Tactics (sanitized)', mitre_tactic, 'MITRETactics;Qt') + report_body, attachments['mitre_group'] = add_report_body(report_body, 'MITRE ATT&CK Group (sanitized)', mitre_group, 'MITREGroup;Qt') + report_body, attachments['mitre_software'] = add_report_body(report_body, 'MITRE ATT&CK Software (sanitized)', mitre_software, 'MITRESoftware;Qt') + report_body, attachments['threat_actor'] = add_report_body(report_body, 'MISP Threat Actor (sanitized)', threat_actor, 'MISPThreatActor;Qt') + report_body, attachments['misp_tag'] = add_report_body(report_body, 'Tags (sanitized)', misp_tag, 'MISPTags;Qt') + + report_body = report_body + "\n\nMISP Reporter Finished\n" + + return report_body, attachments + + +def add_report_body(report_body, subtitle, data_object, csv_title): + ''' + Add a section to the report body text + ''' + if report_body: + report_body = report_body + '\n\n' + report_body = report_body + '\n%s\n-------------------------------------------------------------------------------' % subtitle + data_object_s = sorted(data_object.items(), key=lambda kv: (kv[1], kv[0]), reverse=True) + csv_attachment = csv_title + for el in data_object_s: + report_body = report_body + "\n%s \t %s" % (el[0], el[1]) + csv_attachment = csv_attachment + '\n%s;%s' % (el[0], el[1]) + + return report_body, csv_attachment + + +def msg_attach(content, filename): + ''' + Return an message attachment object + ''' + part = MIMEBase('application', "octet-stream") + part.set_payload(content) + part.add_header('Content-Disposition', 'attachment; filename="%s"' % filename) + return part + + +def print_report(report_body, attachments, smtp_from, smtp_to, smtp_server, misp_url): + ''' + Print (or send) the report + ''' + if args.mail: + now = datetime.now() + current_date = now.strftime(ts_format) + + if timeframe: + subject = "MISP Report %s for last %s on %s" % (current_date, timeframe, misp_url) + else: + subject = "MISP Report %s from %s to %s on %s" % (current_date, date_from, date_to, misp_url) + + msg = MIMEMultipart() + msg['From'] = smtp_from + msg['To'] = smtp_to + msg['Subject'] = subject + + msg.attach(MIMEText(report_body, 'text')) + + if args.mispevent: + part = MIMEBase('application', "octet-stream") + part.set_payload(attachments['misp_events']) + part.add_header('Content-Disposition', 'attachment; filename="misp_events.csv"') + msg.attach(part) + + msg.attach(msg_attach(attachments['attr_type'], 'attr_type.csv')) + msg.attach(msg_attach(attachments['attr_category'], 'attr_category.csv')) + msg.attach(msg_attach(attachments['tags_tlp'], 'tags_tlp.csv')) + msg.attach(msg_attach(attachments['tags_misp_galaxy_mitre'], 'tags_misp_galaxy_mitre.csv')) + msg.attach(msg_attach(attachments['tags_misp_galaxy'], 'tags_misp_galaxy.csv')) + msg.attach(msg_attach(attachments['tags_misp_galaxy_threat_actor'], 'tags_misp_galaxy_threat_actor.csv')) + msg.attach(msg_attach(attachments['tags_type'], 'tags_type.csv')) + msg.attach(msg_attach(attachments['galaxies'], 'galaxies.csv')) + msg.attach(msg_attach(attachments['galaxies_cluster'], 'galaxies_cluster.csv')) + msg.attach(msg_attach(attachments['misp_tag'], 'misp_tag.csv')) + msg.attach(msg_attach(attachments['threat_actor'], 'threat_actor.csv')) + msg.attach(msg_attach(attachments['mitre_software'], 'mitre_software.csv')) + msg.attach(msg_attach(attachments['mitre_group'], 'mitre_group.csv')) + msg.attach(msg_attach(attachments['mitre_tactics'], 'mitre_tactics.csv')) + + server = smtplib.SMTP(smtp_server) + server.sendmail(smtp_from, smtp_to, msg.as_string()) + + else: + print(report_body) + + +def get_sanitized_report(dataset, sanitize_selector='ATT&CK Tactic', lower=False, add_not_sanitized=False): + ''' + Remove or bundle some of the tags + 'quick'n'dirty ; could also do this by using the galaxy/tags definition + ''' + # If you add the element completely then it gets removed by an empty string; this allows to filter out non-relevant items + sanitize_set = { + 'ATT&CK Tactic': ['misp-galaxy:mitre-enterprise-attack-pattern="', 'misp-galaxy:mitre-pre-attack-pattern="', 'misp-galaxy:mitre-mobile-attack-pattern="', 'misp-galaxy:mitre-attack-pattern="', 'misp-galaxy:mitre-enterprise-attack-attack-pattern="', 'misp-galaxy:mitre-pre-attack-attack-pattern="', 'misp-galaxy:mitre-enterprise-attack-attack-pattern="', 'misp-galaxy:mitre-mobile-attack-attack-pattern="'], + 'ATT&CK Group': ['misp-galaxy:mitre-enterprise-intrusion-set="', 'misp-galaxy:mitre-pre-intrusion-set="', 'misp-galaxy:mitre-mobile-intrusion-set="', 'misp-galaxy:mitre-intrusion-set="', 'misp-galaxy:mitre-enterprise-attack-intrusion-set="', 'misp-galaxy:mitre-pre-attack-intrusion-set="', 'misp-galaxy:mitre-mobile-attack-intrusion-set="'], + 'ATT&CK Software': ['misp-galaxy:mitre-enterprise-malware="', 'misp-galaxy:mitre-pre-malware="', 'misp-galaxy:mitre-mobile-malware="', 'misp-galaxy:mitre-malware="', 'misp-galaxy:mitre-enterprise-attack-tool="', 'misp-galaxy:mitre-enterprise-tool="', 'misp-galaxy:mitre-pre-tool="', 'misp-galaxy:mitre-mobile-tool="', 'misp-galaxy:mitre-tool="', 'misp-galaxy:mitre-enterprise-attack-malware="'], + 'MISP Threat Actor': ['misp-galaxy:threat-actor="'], + 'MISP Tags': ['circl:incident-classification="', 'osint:source-type="blog-post"', 'misp-galaxy:tool="', 'CERT-XLM:malicious-code="', 'circl:topic="', 'ddos:type="', 'ecsirt:fraud="', 'dnc:malware-type="', 'enisa:nefarious-activity-abuse="', 'europol-incident:information-gathering="', 'misp-galaxy:ransomware="', 'misp-galaxy:rat="', 'misp-galaxy:social-dark-patterns="', 'misp-galaxy:tool="', 'misp:threat-level="', 'ms-caro-malware:malware-platform=', 'ms-caro-malware:malware-type=', 'veris:security_incident="', 'veris:attribute:integrity:variety="', 'veris:actor:motive="', 'misp-galaxy:banker="', 'misp-galaxy:malpedia="', 'misp-galaxy:botnet="', 'malware_classification:malware-category="', 'TLP: white', 'TLP: Green', + 'inthreat:event-src="feed-osint"', 'tlp:white', 'tlp:amber', 'tlp:green', 'tlp:red', 'osint:source-type="blog-post"', 'Partner Feed', 'IBM XForce', 'type:OSINT', 'malware:', 'osint:lifetime="perpetual"', 'Actor:', 'osint:certainty="50"', 'Banker:', 'Group:', 'Threat:', + 'ncsc-nl-ndn:feed="selected"', 'misp-galaxy:microsoft-activity-group="', 'admiralty-scale:source-reliability="b"', 'admiralty-scale:source-reliability="a"', 'admiralty-scale:information-credibility="2"', 'admiralty-scale:information-credibility="3"', + 'feed:source="CESICAT"', 'osint:source-type="automatic-analysis"', 'workflow:state="complete"', 'osint:source-type="technical-report"', + 'csirt_case_classification:incident-category="', 'dnc:driveby-type="', 'veris:action:social:variety="', 'osint:source-type="', + 'osint:source-type="microblog-post"', 'ecsirt:malicious-code="', 'misp-galaxy:sector="', 'veris:action:variety=', 'label=', 'csirt_case_classification:incident-category="', 'admiralty-scale:source-reliability="c"', 'workflow:todo="review"', 'LDO-CERT:detection="toSIEM"', 'Threat tlp:White', 'Threat Type:', 'adversary:infrastructure-state="active"', 'cirl:incident-classification:', 'misp-galaxy:android="', 'dnc:infrastructure-type="', 'ecsirt:information-gathering="', 'ecsirt:intrusions="', 'dhs-ciip-sectors:DHS-critical-sectors="', 'malware_classification:obfuscation-technique="no-obfuscation"', + 'riskiq:threat-type="', 'veris:action:hacking:variety="', 'veris:action:social:target="', 'workflow:state="incomplete"', 'workflow:todo="add-tagging"', 'workflow:todo="add-context"', 'europol-incident:availability="', 'label=', 'misp-galaxy:stealer="', 'misp-galaxy:exploit-kit="', 'rsit:availability="', 'rsit:fraud="', 'ransomware:type="', 'veris:action:variety=', 'malware:', + 'ecsirt:abusive-content="']} + if sanitize_selector == 'MISP Tags': + sanitize_set['MISP Tags'] = sanitize_set['MISP Tags'] + sanitize_set['ATT&CK Tactic'] + sanitize_set['ATT&CK Group'] + sanitize_set['ATT&CK Software'] + sanitize_set['MISP Threat Actor'] + result_sanitize_set = {} + + if dataset: + for element in dataset: + sanited = False + for sanitize_el in sanitize_set[sanitize_selector]: + if sanitize_el in element: + sanited = True + new_el = element.replace(sanitize_el, '').replace('"', '').strip() + if lower: + new_el = new_el.lower() + result_sanitize_set[new_el] = dataset[element] + if add_not_sanitized and not sanited: + new_el = element.strip() + if lower: + new_el = new_el.lower() + result_sanitize_set[new_el] = dataset[element] + + return result_sanitize_set + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Generate a report of your MISP statistics.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-t', '--timeframe', action='store', help='Timeframe to include in the report') + group.add_argument('-f', '--date_from', action='store', help='Start date of query (YYYY-MM-DD)') + parser.add_argument('-u', '---date-to', action='store', help='End date of query (YYYY-MM-DD)') + parser.add_argument('-e', '--mispevent', action='store_true', help='Include MISP event titles') + parser.add_argument('-m', '--mail', action='store_true', help='Mail the report') + parser.add_argument('-o', '--mailoptions', action='store', help='mailoptions: \'smtp_from=INSERT_FROM;smtp_to=INSERT_TO;smtp_server=localhost\'') + + args = parser.parse_args() + misp = init(misp_url, misp_key, misp_verifycert) + + timeframe = args.timeframe + if not timeframe: + date_from = args.date_from + if not args.date_to: + today = date.today() + date_to = today.strftime("%Y-%m-%d") + else: + date_to = args.date_to + else: + date_from = None + date_to = None + + ts_format = '%Y-%m-%d %H:%M:%S' + threat_levels = ['High', 'Medium', 'Low', 'Undef'] + analysis_completion = ['Initial', 'Ongoing', 'Complete'] + smtp_from = 'INSERT_FROM' + smtp_to = 'INSERT_TO' + smtp_server = 'localhost' + + if args.mailoptions: + mailoptions = args.mailoptions.split(';') + for s in mailoptions: + if s.split('=')[0] == 'smtp_from': + smtp_from = s.split('=')[1] + if s.split('=')[0] == 'smtp_to': + smtp_to = s.split('=')[1] + if s.split('=')[0] == 'smtp_server': + smtp_server = s.split('=')[1] + + report = get_data(misp, timeframe, date_from, date_to) + if(report): + report_body, attachments = build_report(report, timeframe, misp_url) + print_report(report_body, attachments, smtp_from, smtp_to, smtp_server, misp_url) diff --git a/examples/suricata.py b/examples/suricata.py deleted file mode 100755 index 2526033..0000000 --- a/examples/suricata.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse - - -def init(url, key): - return PyMISP(url, key, misp_verifycert) - - -def fetch(m, all_events, event): - if all_events: - print(misp.download_all_suricata().text) - else: - print(misp.download_suricata_rule_event(event).text) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Download Suricata events.') - parser.add_argument("-a", "--all", action='store_true', help="Download all suricata rules available.") - parser.add_argument("-e", "--event", help="Download suricata rules from one event.") - - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - fetch(misp, args.all, args.event) diff --git a/examples/suricata_search/README.md b/examples/suricata_search/README.md index f0c6670..bbb7648 100644 --- a/examples/suricata_search/README.md +++ b/examples/suricata_search/README.md @@ -1,24 +1,3 @@ -# Description -Get all attributes, from a MISP (https://github.com/MISP) instance, that can be converted into Suricata rules, given a *parameter* and a *term* to search +This script was outdated and didn't work on the current version of PyMISP. -**requires** -* PyMISP (https://github.com/CIRCL/PyMISP/) -* python 2.7 or python3 (suggested) - - - # Usage - * **suricata_search.py -p tags -s 'APT' -o misp_ids.rules -t 5** - - search for 'APT' tag - - use 5 threads while generating IDS rules - - dump results to misp_ids.rules - - * **suricata_search.py -p tags -s 'APT' -o misp_ids.rules -ne 411 357 343** - - same as above, but skip events ID 411,357 and 343 - - * **suricata_search.py -p tags -s 'circl:incident-classification="malware", tlp:green' -o misp_ids.rules** - - search for multiple tags 'circl:incident-classification="malware", tlp:green' - - * **suricata_search.py -p categories -s 'Artifacts dropped' -t 20 -o artifacts_dropped.rules** - - search for category 'Artifacts dropped' - - use 20 threads while generating IDS rules - - dump results to artifacts_dropped.rules \ No newline at end of file +For reference, you can look at this repository: https://github.com/raw-data/pymisp-suricata_search diff --git a/examples/suricata_search/suricata_search.py b/examples/suricata_search/suricata_search.py deleted file mode 100755 index 9fd2ec1..0000000 --- a/examples/suricata_search/suricata_search.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -https://github.com/raw-data/pymisp-suricata_search - - 2017.06.28 start - 2017.07.03 fixed args.quiet and status msgs - -""" - -import argparse -import os -import queue -import sys -from threading import Thread, enumerate -from keys import misp_url, misp_key, misp_verifycert - -try: - from pymisp import PyMISP -except ImportError as err: - sys.stderr.write("ERROR: {}\n".format(err)) - sys.stderr.write("\t[try] with pip install pymisp\n") - sys.stderr.write("\t[try] with pip3 install pymisp\n") - sys.exit(1) - -HEADER = """ -#This part might still contain bugs, use and your own risk and report any issues. -# -# MISP export of IDS rules - optimized for suricata -# -# These NIDS rules contain some variables that need to exist in your configuration. -# Make sure you have set: -# -# $HOME_NET - Your internal network range -# $EXTERNAL_NET - The network considered as outside -# $SMTP_SERVERS - All your internal SMTP servers -# $HTTP_PORTS - The ports used to contain HTTP traffic (not required with suricata export) -# -""" - -# queue for events matching searched term/s -IDS_EVENTS = queue.Queue() - -# queue for downloaded Suricata rules -DOWNLOADED_RULES = queue.Queue() - -# Default number of threads to use -THREAD = 4 - -try: - input = raw_input -except NameError: - pass - - -def init(): - """ init connection to MISP """ - return PyMISP(misp_url, misp_key, misp_verifycert, 'json') - - -def search(misp, quiet, noevent, **kwargs): - """ Start search in MISP """ - - result = misp.search(**kwargs) - - # fetch all events matching **kwargs - track_events = 0 - skip_events = list() - for event in result['response']: - event_id = event["Event"].get("id") - track_events += 1 - - to_ids = False - for attribute in event["Event"]["Attribute"]: - to_ids_event = attribute["to_ids"] - if to_ids_event: - to_ids = True - break - - # if there is at least one eligible event to_ids, add event_id - if to_ids: - # check if the event_id is not blacklisted by the user - if isinstance(noevent, list): - if event_id not in noevent[0]: - to_ids_event = (event_id, misp) - IDS_EVENTS.put(to_ids_event) - else: - skip_events.append(event_id) - else: - to_ids_event = (event_id, misp) - IDS_EVENTS.put(to_ids_event) - - if not quiet: - print ("\t[i] matching events: {}".format(track_events)) - if len(skip_events) > 0: - print ("\t[i] skipped {0} events -> {1}".format(len(skip_events),skip_events)) - print ("\t[i] events selected for IDS export: {}".format(IDS_EVENTS.qsize())) - - -def collect_rules(thread): - """ Dispatch tasks to Suricata_processor worker """ - - for x in range(int(thread)): - th = Thread(target=suricata_processor, args=(IDS_EVENTS, )) - th.start() - - for x in enumerate(): - if x.name == "MainThread": - continue - x.join() - - -def suricata_processor(ids_events): - """ Trigger misp.download_suricata_rule_event """ - - while not ids_events.empty(): - event_id, misp = ids_events.get() - ids_rules = misp.download_suricata_rule_event(event_id).text - - for r in ids_rules.split("\n"): - # skip header - if not r.startswith("#"): - if len(r) > 0: DOWNLOADED_RULES.put(r) - - -def return_rules(output, quiet): - """ Return downloaded rules to user """ - - rules = set() - while not DOWNLOADED_RULES.empty(): - rules.add(DOWNLOADED_RULES.get()) - - if output is None: - - if not quiet: - print ("[+] Displaying rules") - - print (HEADER) - for r in rules: print (r) - print ("#") - - else: - - if not quiet: - print ("[+] Writing rules to {}".format(output)) - print ("[+] Generated {} rules".format(len(rules))) - - with open(output, 'w') as f: - f.write(HEADER) - f.write("\n".join(r for r in rules)) - f.write("\n"+"#") - - -def format_request(param, term, misp, quiet, output, thread, noevent): - """ Format request and start search """ - - kwargs = {param: term} - - if not quiet: - print ("[+] Searching for: {}".format(kwargs)) - - search(misp, quiet, noevent, **kwargs) - - # collect Suricata rules - collect_rules(thread) - - -if __name__ == "__main__": - - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - description='Get all attributes that can be converted into Suricata rules, given a parameter and a term to ' - 'search.', - epilog=''' - EXAMPLES: - suricata_search.py -p tags -s 'APT' -o misp_ids.rules -t 5 - suricata_search.py -p tags -s 'APT' -o misp_ids.rules -ne 411 357 343 - suricata_search.py -p tags -s 'tlp:green, OSINT' -o misp_ids.rules - suricata_search.py -p tags -s 'circl:incident-classification="malware", tlp:green' -o misp_ids.rules - suricata_search.py -p categories -s 'Artifacts dropped' -t 20 -o artifacts_dropped.rules - ''') - parser.add_argument("-p", "--param", required=True, help="Parameter to search (e.g. categories, tags, org, etc.).") - parser.add_argument("-s", "--search", required=True, help="Term/s to search.") - parser.add_argument("-q", "--quiet", action='store_true', help="No status messages") - parser.add_argument("-t", "--thread", required=False, help="Number of threads to use", default=THREAD) - parser.add_argument("-ne", "--noevent", nargs='*', required=False, dest='noevent', action='append', - help="Event/s ID to exclude during the search") - parser.add_argument("-o", "--output", help="Output file",required=False) - - args = parser.parse_args() - - if args.output is not None and os.path.exists(args.output) and not args.quiet: - try: - check = input("[!] Output file {} exists, do you want to continue [Y/n]? ".format(args.output)) - if check not in ["Y","y"]: - exit(0) - except KeyboardInterrupt: - sys.exit(0) - - if not args.quiet: - print ("[i] Connecting to MISP instance: {}".format(misp_url)) - print ("[i] Note: duplicated IDS rules will be removed") - - # Based on # of terms, format request - if "," in args.search: - for term in args.search.split(","): - term = term.strip() - misp = init() - format_request(args.param, term, misp, args.quiet, args.output, args.thread, args.noevent) - else: - misp = init() - format_request(args.param, args.search, misp, args.quiet, args.output, args.thread, args.noevent) - - # return collected rules - return_rules(args.output, args.quiet) diff --git a/examples/sync_sighting.py b/examples/sync_sighting.py new file mode 100755 index 0000000..7d25608 --- /dev/null +++ b/examples/sync_sighting.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +Koen Van Impe + +Sync sightings between MISP instances + +Put this script in crontab to run every /15 or /60 + */5 * * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/sync_sighting.py + +Uses a drift file to keep track of latest timestamp synced (config) +Install on "clients", these push the sightings back to authoritative MISP instance + +''' + +from pymisp import PyMISP +from keys import misp_url, misp_key, misp_verifycert +from keys import misp_authoritive_url, misp_authoritive_key, misp_authoritive_verifycert + +import sys +import time + + +def init(url, key, verifycert): + ''' + Template to get MISP module started + ''' + return PyMISP(url, key, verifycert, 'json') + + +def search_sightings(misp, timestamp, timestamp_now): + ''' + Search all the local sightings + Extend the sighting with the attribute UUID + ''' + completed_sightings = [] + + try: + found_sightings = misp.search_sightings(date_from=timestamp, date_to=timestamp_now) + except Exception as e: + sys.exit("Unable to search for sightings") + + if found_sightings is not None and 'response' in found_sightings: + for s in found_sightings['response']: + if 'Sighting' in s: + sighting = s['Sighting'] + if 'attribute_id' in sighting: + attribute_id = sighting['attribute_id'] + + # Query the attribute to get the uuid + # We need this to update the sighting on the other instance + try: + attribute = misp.get_attribute(attribute_id) + except Exception as e: + if module_DEBUG: + print("Unable to fetch attribute UUID for ID %s " % attribute_id) + continue + + if 'Attribute' in attribute and 'uuid' in attribute['Attribute']: + attribute_uuid = attribute['Attribute']['uuid'] + completed_sightings.append({'attribute_uuid': attribute_uuid, 'date_sighting': sighting['date_sighting'], 'source': sighting['source'], 'type': sighting['type'], 'uuid': sighting['uuid']}) + else: + if module_DEBUG: + print("No information returned for attribute ID %s " % attribute_id) + continue + + return completed_sightings + + +def sync_sightings(misp, misp_authoritive, found_sightings, verify_before_push, custom_sighting_text): + ''' + Walk through all the sightings + ''' + if found_sightings is not None: + for sighting in found_sightings: + attribute_uuid = sighting['attribute_uuid'] + date_sighting = sighting['date_sighting'] + source = sighting['source'] + if not source: + source = custom_sighting_text + type = sighting['type'] + + # Fail safe + if verify_before_push: + if sighting_exists(misp_authoritive, sighting): + continue + else: + continue + else: + push_sighting(misp_authoritive, attribute_uuid, date_sighting, source, type) + continue + return True + return False + + +def push_sighting(misp_authoritive, attribute_uuid, date_sighting, source, type): + ''' + Push sighting to the authoritative server + ''' + if attribute_uuid: + try: + misp_authoritive.sighting(uuid=attribute_uuid, source=source, type=type, timestamp=date_sighting) + if module_DEBUG: + print("Pushed sighting for %s on %s" % (attribute_uuid, date_sighting)) + return True + except Exception as e: + if module_DEBUG: + print("Unable to update attribute %s " % (attribute_uuid)) + return False + + +def sighting_exists(misp_authoritive, sighting): + ''' + Check if the sighting exists on the authoritative server + sightings/restSearch/attribute for uuid is not supported in MISP + + optionally to implement + ''' + return False + + +def set_drift_timestamp(drift_timestamp, drift_timestamp_path): + ''' + Save the timestamp in a (local) file + ''' + try: + with open(drift_timestamp_path, 'w+') as f: + f.write(str(drift_timestamp)) + return True + except IOError: + sys.exit("Unable to write drift_timestamp %s to %s" % (drift_timestamp, drift_timestamp_path)) + return False + + +def get_drift_timestamp(drift_timestamp_path): + ''' + From when do we start with the sightings? + ''' + try: + with open(drift_timestamp_path) as f: + drift = f.read() + if drift: + drift = int(float(drift)) + else: + drift = 0 + except IOError: + drift = 0 + + return drift + + +if __name__ == '__main__': + misp = init(misp_url, misp_key, misp_verifycert) + misp_authoritive = init(misp_authoritive_url, misp_authoritive_key, misp_authoritive_verifycert) + drift_timestamp_path = '/home/mispuser/PyMISP/examples/sync_sighting.drift' + + drift_timestamp = get_drift_timestamp(drift_timestamp_path=drift_timestamp_path) + timestamp_now = time.time() + module_DEBUG = True + + # Get all attribute sightings + found_sightings = search_sightings(misp, drift_timestamp, timestamp_now) + if found_sightings is not None and len(found_sightings) > 0: + if sync_sightings(misp, misp_authoritive, found_sightings, verify_before_push=False, custom_sighting_text="Custom Sighting"): + set_drift_timestamp(timestamp_now, drift_timestamp_path) + if module_DEBUG: + print("Sighting drift file updated to %s " % (timestamp_now)) + else: + sys.exit("Unable to sync sync_sightings") + else: + sys.exit("No sightings found") diff --git a/examples/tags.py b/examples/tags.py index b8f3f13..6bf3b95 100755 --- a/examples/tags.py +++ b/examples/tags.py @@ -1,16 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse import json -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', True) - - def get_tags(m): result = m.get_all_tags(True) r = result @@ -22,6 +18,8 @@ if __name__ == '__main__': args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - get_tags(misp) + tags = misp.tags(pythonify=True) + for tag in tags: + print(tag.to_json()) diff --git a/examples/tagstatistics.py b/examples/tagstatistics.py deleted file mode 100755 index 4f9fe76..0000000 --- a/examples/tagstatistics.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from pymisp import PyMISP -from keys import misp_url, misp_key, misp_verifycert -import argparse -import json - -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Get statistics from tags.') - parser.add_argument("-p", "--percentage", action='store_true', default=None, help="An optional field, if set, it will return the results in percentages, otherwise it returns exact count.") - parser.add_argument("-n", "--namesort", action='store_true', default=None, help="An optional field, if set, values are sort by the namespace, otherwise the sorting will happen on the value.") - args = parser.parse_args() - - misp = init(misp_url, misp_key) - - stats = misp.get_tags_statistics(args.percentage, args.namesort) - print(json.dumps(stats)) diff --git a/examples/trustar.conf b/examples/trustar.conf new file mode 100644 index 0000000..77cb4c4 --- /dev/null +++ b/examples/trustar.conf @@ -0,0 +1,14 @@ +[trustar] + +# endpoint that provides oauth token +auth_endpoint = https://api.trustar.co/oauth/token + +# base API URL access endpoint +api_endpoint = https://api.trustar.co/api/1.3 + +# Generate and copy your API key and secret on user API settings page on Station: https://station.trustar.co/settings/api +user_api_key = '#{API_KEY}' +user_api_secret = '#{API_SECRET}' + +# OPTIONAL: enter one or more comma-separate enclave IDs to submit to - get these from API settings page on Station +# enclave_ids = abcdef,1234f diff --git a/examples/trustar_misp.py b/examples/trustar_misp.py new file mode 100644 index 0000000..443ac31 --- /dev/null +++ b/examples/trustar_misp.py @@ -0,0 +1,59 @@ +from trustar import TruStar, datetime_to_millis +from datetime import datetime, timedelta +from keys import misp_url, misp_key, misp_verifycert +from pymisp import PyMISP, MISPEvent, MISPOrganisation, MISPObject + +# enclave_ids = '7a33144f-aef3-442b-87d4-dbf70d8afdb0' # RHISAC +enclave_ids = None + +time_interval = {'days': 30, 'hours': 0} + +distribution = None # Optional, defaults to MISP.default_event_distribution in MISP config +threat_level_id = None # Optional, defaults to MISP.default_event_threat_level in MISP config +analysis = None # Optional, defaults to 0 (initial analysis) + + + +tru = TruStar() + +misp = PyMISP(misp_url, misp_key, misp_verifycert) + +now = datetime.now() + +# date range for pulling reports is last 4 hours when script is run +to_time = datetime.now() +from_time = to_time - timedelta(**time_interval) + +# convert to millis since epoch +to_time = datetime_to_millis(to_time) +from_time = datetime_to_millis(from_time) + +if not enclave_ids: + reports = tru.get_reports(from_time=from_time, + to_time=to_time) +else: + reports = tru.get_reports(from_time=from_time, + to_time=to_time, + is_enclave=True, + enclave_ids=enclave_ids) + +# loop through each trustar report and create MISP events for each +for report in reports: + # initialize and set MISPEvent() + event = MISPEvent() + event.info = report.title + event.distribution = distribution + event.threat_level_id = threat_level_id + event.analysis = analysis + + # get tags for report + for tag in tru.get_enclave_tags(report.id): + event.add_tag(tag.name) + + obj = MISPObject('trustar_report', standalone=False, strict=True) + # get indicators for report + for indicator in tru.get_indicators_for_report(report.id): + obj.add_attribute(indicator.type, indicator.value) + event.add_object(obj) + # post each event to MISP via API + misp.add_event(event) diff --git a/examples/up.py b/examples/up.py index d056af4..31088ee 100755 --- a/examples/up.py +++ b/examples/up.py @@ -1,19 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPEvent from keys import misp_url, misp_key, misp_verifycert import argparse -from io import open - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json', debug=True) - -def up_event(m, event, content): - with open(content, 'r') as f: - result = m.update_event(event, f.read()) - print(result) if __name__ == '__main__': parser = argparse.ArgumentParser(description="Update a MISP event.") @@ -22,6 +13,9 @@ if __name__ == '__main__': args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - up_event(misp, args.event, args.input) + me = MISPEvent() + me.load_file(args.input) + + result = misp.update_event(me, args.event) diff --git a/examples/upload.py b/examples/upload.py index 4c6708d..447a9ea 100755 --- a/examples/upload.py +++ b/examples/upload.py @@ -1,43 +1,60 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP, MISPEvent, MISPAttribute from keys import misp_url, misp_key, misp_verifycert import argparse -import os -import glob - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') - - -def upload_files(m, eid, paths, distrib, ids, categ, comment, info, analysis, threat): - out = m.upload_samplelist(paths, eid, distrib, ids, categ, comment, info, analysis, threat) - print(out) +from pathlib import Path if __name__ == '__main__': parser = argparse.ArgumentParser(description='Send malware sample to MISP.') parser.add_argument("-u", "--upload", type=str, required=True, help="File or directory of files to upload.") - parser.add_argument("-e", "--event", type=int, help="Not supplying an event ID will cause MISP to create a single new event for all of the POSTed malware samples.") parser.add_argument("-d", "--distrib", type=int, help="The distribution setting used for the attributes and for the newly created event, if relevant. [0-3].") - parser.add_argument("-ids", action='store_true', help="You can flag all attributes created during the transaction to be marked as \"to_ids\" or not.") - parser.add_argument("-c", "--categ", help="The category that will be assigned to the uploaded samples. Valid options are: Payload delivery, Artifacts dropped, Payload Installation, External Analysis.") + parser.add_argument("-c", "--comment", type=str, help="Comment for the uploaded file(s).") + parser.add_argument('-m', '--is-malware', action='store_true', help='The file(s) to upload are malwares') + parser.add_argument('--expand', action='store_true', help='(Only if the file is a malware) Run lief expansion (creates objects)') + parser.add_argument("-e", "--event", type=int, default=None, help="Not supplying an event ID will cause MISP to create a single new event for all of the POSTed malware samples.") parser.add_argument("-i", "--info", help="Used to populate the event info field if no event ID supplied.") - parser.add_argument("-a", "--analysis", type=int, help="The analysis level of the newly created event, if applicatble. [0-2]") - parser.add_argument("-t", "--threat", type=int, help="The threat level ID of the newly created event, if applicatble. [1-4]") - parser.add_argument("-co", "--comment", type=str, help="Comment for the uploaded file(s).") args = parser.parse_args() - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) files = [] - if os.path.isfile(args.upload): - files = [args.upload] - elif os.path.isdir(args.upload): - files = [f for f in glob.iglob(os.path.join(args.upload + '*'))] + p = Path(args.upload) + if p.is_file(): + files = [p] + elif p.is_dir(): + files = [f for f in p.glob('**/*') if f.is_file()] else: - print('invalid file') + print('invalid upload path (must be file or dir)') exit(0) - upload_files(misp, args.event, files, args.distrib, args.ids, args.categ, args.comment, args.info, args.analysis, args.threat) + if args.is_malware: + arg_type = 'malware-sample' + else: + arg_type = 'attachment' + + # Create attributes + attributes = [] + for f in files: + a = MISPAttribute() + a.type = arg_type + a.value = f.name + a.data = f + a.comment = args.comment + a.distribution = args.distrib + if args.expand and arg_type == 'malware-sample': + a.expand = 'binary' + attributes.append(a) + + if args.event: + for a in attributes: + misp.add_attribute(args.event, a) + else: + m = MISPEvent() + m.info = args.info + m.distribution = args.distrib + m.attributes = attributes + if args.expand and arg_type == 'malware-sample': + m.run_expansions() + misp.add_event(m) diff --git a/examples/users_list.py b/examples/users_list.py index 606d210..d62c78e 100755 --- a/examples/users_list.py +++ b/examples/users_list.py @@ -1,24 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pymisp import PyMISP +from pymisp import ExpandedPyMISP from keys import misp_url, misp_key, misp_verifycert import argparse -# For python2 & 3 compat, a bit dirty, but it seems to be the least bad one -try: - input = raw_input -except NameError: - pass - - -def init(url, key): - return PyMISP(url, key, misp_verifycert, 'json') if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get a list of the sharing groups from the MISP instance.') - misp = init(misp_url, misp_key) + misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert) - users_list = misp.get_users_list() - print (users_list) + users_list = misp.users(pythonify=True) + print(users_list) diff --git a/examples/vmray_automation.py b/examples/vmray_automation.py new file mode 100644 index 0000000..b87ba31 --- /dev/null +++ b/examples/vmray_automation.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Jens Thom (VMRay), Koen Van Impe + +VMRay automatic import +Put this script in crontab to run every /15 or /60 + */5 * * * * mispuser /usr/bin/python3 /home/mispuser/PyMISP/examples/vmray_automation.py + +Calls "vmray_import" for all events that have an 'incomplete' VMray analysis + +Do inline config in "main". +If your MISP user is not an admin, you cannot use `get_config`, +use `overwrite_config` instead. +Example config: + config = { + "vmray_import_enabled": True, + "vmray_import_apikey": vmray_api_key, + "vmray_import_url": vmray_server, + "vmray_import_disable_tags": False, + "vmray_import_disable_misp_objects": False, + "vmray_import_ignore_analysis_finished": False, + "services_port": 6666, + "services_url": "http://localhost", + "Artifacts": "1", + "VTI": "1", + "IOCs": "1", + "Analysis Details": "1", + } +""" + +import logging +import urllib + +from typing import Any, Dict, List, Optional + +import requests + +from keys import misp_key, misp_url, misp_verifycert +from pymisp import ExpandedPyMISP + +# Suppress those "Unverified HTTPS request is being made" +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def is_url(url: str) -> bool: + try: + result = urllib.parse.urlparse(url) + return result.scheme and result.netloc + except ValueError: + return False + + +class VMRayAutomationException(Exception): + pass + + +class VMRayAutomation: + def __init__( + self, + misp_url: str, + misp_key: str, + verify_cert: bool = False, + debug: bool = False, + ) -> None: + # setup logging + log_level = logging.DEBUG if debug else logging.INFO + log_format = "%(asctime)s - %(name)s - %(levelname)8s - %(message)s" + + logging.basicConfig(level=log_level, format=log_format) + logging.getLogger("pymisp").setLevel(log_level) + self.logger = logging.getLogger(self.__class__.__name__) + + self.misp_url = misp_url.rstrip("/") + self.misp_key = misp_key + self.verifycert = verify_cert + self.misp = ExpandedPyMISP(misp_url, misp_key, ssl=verify_cert, debug=debug) + self.config = {} + self.tag_incomplete = 'workflow:state="incomplete"' + + @staticmethod + def _setting_enabled(value: bool) -> bool: + if not value: + raise VMRayAutomationException( + "VMRay import is disabled. " + "Please enable `vmray_import` in the MISP settings." + ) + + return True + + @staticmethod + def _setting_apikey(value: str) -> str: + if not value: + raise VMRayAutomationException( + "VMRay API key not set. Please set the API key in the MISP settings." + ) + + return value + + @staticmethod + def _setting_url(value: str) -> str: + if not value: + raise VMRayAutomationException( + "VMRay URL not set. Please set the URL in the MISP settings." + ) + + if not is_url(value): + raise VMRayAutomationException("Not a valid URL") + + return value + + @staticmethod + def _setting_disabled(value: str) -> bool: + return value.lower() in ["no", "false"] + + @staticmethod + def _services_port(value: int) -> bool: + if value == 0: + return 6666 + return value + + @staticmethod + def services_url(value: str) -> bool: + if not is_url(value): + raise VMRayAutomationException("Services URL is not valid.") + + return value + + @property + def vmray_settings(self) -> Dict[str, Any]: + return { + "vmray_import_enabled": self._setting_enabled, + "vmray_import_apikey": self._setting_apikey, + "vmray_import_url": self._setting_url, + "vmray_import_disable_tags": self._setting_disabled, + "vmray_import_disable_misp_objects": self._setting_disabled, + "vmray_import_ignore_analysis_finished": self._setting_disabled, + "services_port": self._services_port, + "services_url": self.services_url, + } + + def _get_misp_settings(self) -> List[Dict[str, Any]]: + misp_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": self.misp_key, + } + + response = requests.get( + f"{self.misp_url}/servers/serverSettings.json", + verify=self.verifycert, + headers=misp_headers, + ) + + if response.status_code == 200: + settings = response.json() + if "finalSettings" in settings: + return settings["finalSettings"] + + raise VMRayAutomationException("Could not get settings from MISP server.") + + def get_config(self) -> None: + self.logger.debug("Loading confing...") + # get settings from MISP server + settings = self._get_misp_settings() + for setting in settings: + config_name = setting["setting"].replace("Plugin.Import_", "") + if config_name in self.vmray_settings: + func = self.vmray_settings[config_name] + value = func(setting["value"]) + self.config[config_name] = value + + # set default `vmray_import` settings + self.config.setdefault("VTI", "1") + self.config.setdefault("IOCs", "1") + self.config.setdefault("Artifacts", "0") + self.config.setdefault("Analysis Details", "1") + + self.logger.info("Loading config: Done.") + + def overwrite_config(self, config: Dict[str, Any]) -> None: + self.config.update(config) + + def _get_sample_id(self, value: str) -> Optional[int]: + vmray_sample_id_text = "VMRay Sample ID: " + if not value.startswith(vmray_sample_id_text): + self.logger.warning("Invalid Sample ID: %s.", value) + return None + + return int(value.replace(vmray_sample_id_text, "")) + + def _call_vmray_import(self, sample_id: int, event_id: str) -> Dict[str, Any]: + url = f"{self.config['services_url']}:{self.config['services_port']}/query" + + config = {"Sample ID": sample_id} + for key, value in self.config.items(): + vmray_config_key = key.replace("vmray_import_", "") + config[vmray_config_key] = str(value) + + data = { + "module": "vmray_import", + "event_id": event_id, + "config": config, + "data": "", + } + + self.logger.debug("calling `vmray_import`: url=%s, config=%s", url, config) + response = requests.post(url, json=data) + if response.status_code != 200: + raise VMRayAutomationException( + f"MISP modules returned status code `{response.status_code}`" + ) + + json_response = response.json() + if "error" in json_response: + error = json_response["error"] + raise VMRayAutomationException(f"MISP modules returned error: {error}") + + return json_response + + def _add_event_attributes(self, event_id: int, attributes: Dict[str, Any]) -> None: + event = self.misp.get_event(event_id, pythonify=True) + for attr in attributes["Attribute"]: + event.add_attribute(**attr) + + self.misp.update_event(event) + + def _add_event_objects(self, event_id: int, objects: Dict[str, Any]) -> None: + event = self.misp.get_event(event_id, pythonify=True) + for obj in objects["Object"]: + event.add_object(**obj) + + if "Tag" in objects: + for tag in objects["Tag"]: + event.add_tag(tag["name"]) + + self.misp.update_event(event) + + def _add_misp_event(self, event_id: int, response: Dict[str, Any]) -> None: + if self.config["vmray_import_disable_misp_objects"]: + self._add_event_attributes(event_id, response["results"]) + else: + self._add_event_objects(event_id, response["results"]) + + def import_incomplete_analyses(self) -> None: + self.logger.info("Searching for attributes with tag='%s'", self.tag_incomplete) + result = self.misp.search("attributes", tags=self.tag_incomplete) + attributes = result["Attribute"] + + for attr in attributes: + event_id = int(attr["event_id"]) + self.logger.info("Processing event ID `%d`.", event_id) + + sample_id = self._get_sample_id(attr["value"]) + if not sample_id: + continue + + response = self._call_vmray_import(sample_id, event_id) + self._add_misp_event(event_id, response) + self.misp.untag(attr["uuid"], self.tag_incomplete) + + +def main(): + debug = False + config = { + "Artifacts": "0", + "VTI": "1", + "IOCs": "1", + "Analysis Details": "0", + "vmray_import_disable_misp_objects": False, + } + + automation = VMRayAutomation(misp_url, misp_key, misp_verifycert, debug) + automation.get_config() # only possible with admin user + automation.overwrite_config(config) + automation.import_incomplete_analyses() + + +if __name__ == "__main__": + main() diff --git a/examples/yara_dump.py b/examples/yara_dump.py index ed6bc85..11477d8 100755 --- a/examples/yara_dump.py +++ b/examples/yara_dump.py @@ -38,8 +38,8 @@ attr_cnt_duplicate = 0 attr_cnt_changed = 0 yara_rules = [] yara_rule_names = [] -if 'response' in result and 'Attribute' in result['response']: - for attribute in result['response']['Attribute']: +if result.get('Attribute'): + for attribute in result.get('Attribute'): value = attribute['value'] event_id = attribute['event_id'] attribute_id = attribute['id'] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..3f7aec0 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,15 @@ +[mypy] +strict = True +warn_return_any = False +show_error_context = True +pretty = True +exclude = tests/testlive_comprehensive.py|tests/testlive_sync.py|feed-generator|examples|pymisp/data|docs|pymisp/tools/openioc.py|pymisp/tools/reportlab_generator.py|tests/test_reportlab.py + +# Stuff to remove gradually +disallow_untyped_defs = False +disallow_untyped_calls = False +disable_error_code = arg-type,return-value,assignment,call-overload,union-attr + + +[mypy-docs.source.*] +ignore_errors = True diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3bd01f4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3526 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = true +python-versions = ">=3.10" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bleach" +version = "6.2.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.9" +files = [ + {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, + {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, +] + +[package.dependencies] +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.5)"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = true +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = true +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = true +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "colorclass" +version = "2.2.2" +description = "Colorful worry-free console applications for Linux, Mac OS X, and Windows." +optional = true +python-versions = ">=2.6" +files = [ + {file = "colorclass-2.2.2-py2.py3-none-any.whl", hash = "sha256:6f10c273a0ef7a1150b1120b6095cbdd68e5cf36dfd5d0fc957a2500bbf99a55"}, + {file = "colorclass-2.2.2.tar.gz", hash = "sha256:6d4fe287766166a98ca7bc6f6312daf04a0481b1eda43e7173484051c0ab4366"}, +] + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +optional = true +python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "compressed-rtf" +version = "1.0.6" +description = "Compressed Rich Text Format (RTF) compression and decompression package" +optional = true +python-versions = "*" +files = [ + {file = "compressed_rtf-1.0.6.tar.gz", hash = "sha256:c1c827f1d124d24608981a56e8b8691eb1f2a69a78ccad6440e7d92fde1781dd"}, +] + +[[package]] +name = "coverage" +version = "7.6.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, + {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, + {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, + {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, + {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, + {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, + {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, + {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, + {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, + {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, + {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, + {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, + {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, + {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, + {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, + {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "debugpy" +version = "1.8.8" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, + {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, + {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, + {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, + {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, + {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, + {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, + {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, + {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, + {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, + {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, + {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, + {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, + {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, + {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, + {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, + {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, + {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, + {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, + {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, + {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, + {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, + {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, + {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, + {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, + {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "deprecated" +version = "1.2.15" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, + {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = true +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "easygui" +version = "0.98.3" +description = "EasyGUI is a module for very simple, very easy GUI programming in Python. EasyGUI is different from other GUI generators in that EasyGUI is NOT event-driven. Instead, all GUI interactions are invoked by simple function calls." +optional = true +python-versions = "*" +files = [ + {file = "easygui-0.98.3-py2.py3-none-any.whl", hash = "sha256:33498710c68b5376b459cd3fc48d1d1f33822139eb3ed01defbc0528326da3ba"}, + {file = "easygui-0.98.3.tar.gz", hash = "sha256:d653ff79ee1f42f63b5a090f2f98ce02335d86ad8963b3ce2661805cafe99a04"}, +] + +[[package]] +name = "ebcdic" +version = "1.1.1" +description = "Additional EBCDIC codecs" +optional = true +python-versions = "*" +files = [ + {file = "ebcdic-1.1.1-py2.py3-none-any.whl", hash = "sha256:33b4cb729bc2d0bf46cc1847b0e5946897cb8d3f53520c5b9aa5fa98d7e735f1"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.1.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +files = [ + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "extract-msg" +version = "0.52.0" +description = "Extracts emails and attachments saved in Microsoft Outlook's .msg files" +optional = true +python-versions = ">=3.8" +files = [ + {file = "extract_msg-0.52.0-py3-none-any.whl", hash = "sha256:93c919846bac2a6034cf7d0dcf8e825d640b6ddb8539e42f7f1817869cd1eeaf"}, + {file = "extract_msg-0.52.0.tar.gz", hash = "sha256:c21c548c43e1f0cdce5616102d33e590e2b46fbdc9d04f21af4eb62dcbf296dd"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.11.1,<4.13" +compressed-rtf = ">=1.0.6,<2" +ebcdic = ">=1.1.1,<2" +olefile = "0.47" +red-black-tree-mod = "1.20" +RTFDE = ">=0.1.1,<0.2" +tzlocal = ">=4.2,<6" + +[package.extras] +all = ["extract-msg[encoding]", "extract-msg[image]", "extract-msg[mime]"] +encoding = ["chardet (>=3.0.0,<6)"] +image = ["Pillow (>=9.5.0,<10)"] +mime = ["python-magic (>=0.4.27,<0.5)"] +readthedocs = ["sphinx-rtd-theme"] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "ipython" +version = "8.29.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"}, + {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] +kernel = ["ipykernel"] +matplotlib = ["matplotlib"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json5" +version = "0.9.28" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "json5-0.9.28-py3-none-any.whl", hash = "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df"}, + {file = "json5-0.9.28.tar.gz", hash = "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e"}, +] + +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.10.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, + {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.5" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, + {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.14.2" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"}, + {file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = ">=21.1" +jinja2 = ">=3.0.3" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = ">=0.4.4" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = ">=5.0" +packaging = ">=22.0" +prometheus-client = ">=0.9" +pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = ">=1.7" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, + {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.3.1" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.3.1-py3-none-any.whl", hash = "sha256:2d9a1c305bc748e277819a17a5d5e22452e533e835f4237b2f30f3b0e491e01f"}, + {file = "jupyterlab-4.3.1.tar.gz", hash = "sha256:a4a338327556443521731d82f2a6ccf926df478914ca029616621704d47c3c65"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +httpx = ">=0.25.0" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +ipykernel = ">=6.5.0" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.27.1,<3" +notebook-shim = ">=0.2" +packaging = "*" +setuptools = ">=40.1.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.27.3" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4"}, + {file = "jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4"}, +] + +[package.dependencies] +babel = ">=2.10" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "lark" +version = "1.1.9" +description = "a modern parsing library" +optional = true +python-versions = ">=3.6" +files = [ + {file = "lark-1.1.9-py3-none-any.whl", hash = "sha256:a0dd3a87289f8ccbb325901e4222e723e7d745dbfc1803eaf5f3d2ace19cf2db"}, + {file = "lark-1.1.9.tar.gz", hash = "sha256:15fa5236490824c2c4aba0e22d2d6d823575dcaf4cdd1848e34b6ad836240fba"}, +] + +[package.extras] +atomic-cache = ["atomicwrites"] +interegular = ["interegular (>=0.3.1,<0.4.0)"] +nearley = ["js2py"] +regex = ["regex"] + +[[package]] +name = "lief" +version = "0.15.1" +description = "Library to instrument executable formats" +optional = true +python-versions = ">=3.8" +files = [ + {file = "lief-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a80246b96501b2b1d4927ceb3cb817eda9333ffa9e07101358929a6cffca5dae"}, + {file = "lief-0.15.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:84bf310710369544e2bb82f83d7fdab5b5ac422651184fde8bf9e35f14439691"}, + {file = "lief-0.15.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8fb58efb77358291109d2675d5459399c0794475b497992d0ecee18a4a46a207"}, + {file = "lief-0.15.1-cp310-cp310-manylinux_2_33_aarch64.whl", hash = "sha256:d5852a246361bbefa4c1d5930741765a2337638d65cfe30de1b7d61f9a54b865"}, + {file = "lief-0.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:12e53dc0253c303df386ae45487a2f0078026602b36d0e09e838ae1d4dbef958"}, + {file = "lief-0.15.1-cp310-cp310-win32.whl", hash = "sha256:38b9cee48f42c355359ad7e3ff18bf1ec95e518238e4e8fb25657a49169dbf4c"}, + {file = "lief-0.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ddf2ebd73766169594d631b35f84c49ef42871de552ad49f36002c60164d0aca"}, + {file = "lief-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20508c52de0dffcee3242253541609590167a3e56150cbacb506fdbb822206ef"}, + {file = "lief-0.15.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:0750c892fd3b7161a3c2279f25fe1844427610c3a5a4ae23f65674ced6f93ea5"}, + {file = "lief-0.15.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a8634ea79d6d9862297fadce025519ab25ff01fcadb333cf42967c6295f0d057"}, + {file = "lief-0.15.1-cp311-cp311-manylinux_2_33_aarch64.whl", hash = "sha256:1e11e046ad71fe8c81e1a8d1d207fe2b99c967d33ce79c3d3915cb8f5ecacf52"}, + {file = "lief-0.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:674b620cdf1d686f52450fd97c1056d4c92e55af8217ce85a1b2efaf5b32140b"}, + {file = "lief-0.15.1-cp311-cp311-win32.whl", hash = "sha256:dbdcd70fd23c90017705b7fe6c716f0a69c01d0d0ea7a2ff653d83dc4a61fefb"}, + {file = "lief-0.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b96a37bf11ca777ff305d85d957eabad2a92a6e577b6e2fb3ab79514e5a12e"}, + {file = "lief-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a96f17c2085ef38d12ad81427ae8a5d6ad76f0bc62a1e1f5fe384255cd2cc94"}, + {file = "lief-0.15.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:d780af1762022b8e01b613253af490afea3864fbd6b5a49c6de7cea8fde0443d"}, + {file = "lief-0.15.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d0f10d80202de9634a16786b53ba3a8f54ae8b9a9e124a964d83212444486087"}, + {file = "lief-0.15.1-cp312-cp312-manylinux_2_33_aarch64.whl", hash = "sha256:864f17ecf1736296e6d5fc38b11983f9d19a5e799f094e21e20d58bfb1b95b80"}, + {file = "lief-0.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2ec738bcafee8a569741f4a749f0596823b12f10713306c7d0cbbf85759f51c"}, + {file = "lief-0.15.1-cp312-cp312-win32.whl", hash = "sha256:db38619edf70e27fb3686b8c0f0bec63ad494ac88ab51660c5ecd2720b506e41"}, + {file = "lief-0.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:28bf0922de5fb74502a29cc47930d3a052df58dc23ab6519fa590e564f194a60"}, + {file = "lief-0.15.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0616e6048f269d262ff93d67c497ebff3c1d3965ffb9427b0f2b474764fd2e8c"}, + {file = "lief-0.15.1-cp313-cp313-manylinux_2_33_aarch64.whl", hash = "sha256:6a08b2e512a80040429febddc777768c949bcd53f6f580e902e41ec0d9d936b8"}, + {file = "lief-0.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fcd489ff80860bcc2b2689faa330a46b6d66f0ee3e0f6ef9e643e2b996128a06"}, + {file = "lief-0.15.1-cp313-cp313-win32.whl", hash = "sha256:0d10e5b22e86bbf2d1e3877b604ffd8860c852b6bc00fca681fe1432f5018fe9"}, + {file = "lief-0.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:5af7dcb9c3f44baaf60875df6ba9af6777db94776cc577ee86143bcce105ba2f"}, + {file = "lief-0.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9757ff0c7c3d6f66e5fdcc6a9df69680fad0dc2707d64a3428f0825dfce1a85"}, + {file = "lief-0.15.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:8ac3cd099be2580d0e15150b1d2f5095c38f150af89993ddf390d7897ee8135f"}, + {file = "lief-0.15.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4dedeab498c312a29b58f16b739895f65fa54b2a21b8d98b111e99ad3f7e30a8"}, + {file = "lief-0.15.1-cp38-cp38-manylinux_2_33_aarch64.whl", hash = "sha256:b9217578f7a45f667503b271da8481207fb4edda8d4a53e869fb922df6030484"}, + {file = "lief-0.15.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:82e6308ad8bd4bc7eadee3502ede13a5bb398725f25513a0396c8dba850f58a1"}, + {file = "lief-0.15.1-cp38-cp38-win32.whl", hash = "sha256:dde1c8f8ebe0ee9db4f2302c87ae3cacb9898dc412e0d7da07a8e4e834ac5158"}, + {file = "lief-0.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:a079a76bca23aa73c850ab5beb7598871a1bf44662658b952cead2b5ddd31bee"}, + {file = "lief-0.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:785a3aa14575f046ed9c8d44ea222ea14c697cd03b5331d1717b5b0cf4f72466"}, + {file = "lief-0.15.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:d7044553cf07c8a2ab6e21874f07585610d996ff911b9af71dc6085a89f59daa"}, + {file = "lief-0.15.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13285c3ff5ef6de2421d85684c954905af909db0ad3472e33c475e5f0f657dcf"}, + {file = "lief-0.15.1-cp39-cp39-manylinux_2_33_aarch64.whl", hash = "sha256:932f880ee8a130d663a97a9099516d8570b1b303af7816e70a02f9931d5ef4c2"}, + {file = "lief-0.15.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:de9453f94866e0f2c36b6bd878625880080e7e5800788f5cbc06a76debf283b9"}, + {file = "lief-0.15.1-cp39-cp39-win32.whl", hash = "sha256:4e47324736d6aa559421720758de4ce12d04fb56bdffa3dcc051fe8cdd42ed17"}, + {file = "lief-0.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:382a189514c0e6ebfb41e0db6106936c7ba94d8400651276add2899ff3570585"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, +] + +[[package]] +name = "msoffcrypto-tool" +version = "5.4.2" +description = "Python tool and library for decrypting and encrypting MS Office files using a password or other keys" +optional = true +python-versions = "<4.0,>=3.8" +files = [ + {file = "msoffcrypto_tool-5.4.2-py3-none-any.whl", hash = "sha256:274fe2181702d1e5a107ec1b68a4c9fea997a44972ae1cc9ae0cb4f6a50fef0e"}, + {file = "msoffcrypto_tool-5.4.2.tar.gz", hash = "sha256:44b545adba0407564a0cc3d6dde6ca36b7c0fdf352b85bca51618fa1d4817370"}, +] + +[package.dependencies] +cryptography = ">=39.0" +olefile = ">=0.46" + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nbclient" +version = "0.10.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.16.4" +description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, + {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["pyqtwebengine (>=5.15)"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.10.4" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[package.dependencies] +fastjsonschema = ">=2.15" +jsonschema = ">=2.6" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, + {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + +[[package]] +name = "olefile" +version = "0.47" +description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f"}, + {file = "olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "oletools" +version = "0.60.2" +description = "Python tools to analyze security characteristics of MS Office and OLE files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), for Malware Analysis and Incident Response #DFIR" +optional = true +python-versions = "*" +files = [ + {file = "oletools-0.60.2-py2.py3-none-any.whl", hash = "sha256:72ad8bd748fd0c4e7b5b4733af770d11543ebb2bf2697455f99f975fcd50cc96"}, + {file = "oletools-0.60.2.zip", hash = "sha256:ad452099f4695ffd8855113f453348200d195ee9fa341a09e197d66ee7e0b2c3"}, +] + +[package.dependencies] +colorclass = "*" +easygui = "*" +msoffcrypto-tool = {version = "*", markers = "platform_python_implementation != \"PyPy\" or python_version >= \"3\" and (platform_system != \"Windows\" and platform_system != \"Darwin\")"} +olefile = ">=0.46" +pcodedmp = ">=1.2.5" +pyparsing = ">=2.1.0,<4" + +[package.extras] +full = ["XLMMacroDeobfuscator"] + +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, + {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pcodedmp" +version = "1.2.6" +description = "A VBA p-code disassembler" +optional = true +python-versions = "*" +files = [ + {file = "pcodedmp-1.2.6-py2.py3-none-any.whl", hash = "sha256:4441f7c0ab4cbda27bd4668db3b14f36261d86e5059ce06c0828602cbe1c4278"}, + {file = "pcodedmp-1.2.6.tar.gz", hash = "sha256:025f8c809a126f45a082ffa820893e6a8d990d9d7ddb68694b5a9f0a6dbcd955"}, +] + +[package.dependencies] +oletools = ">=0.54" +win-unicode-console = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +optional = true +python-versions = ">=3.9" +files = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prometheus-client" +version = "0.21.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, + {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "6.1.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "publicsuffixlist" +version = "1.0.2.20241113" +description = "publicsuffixlist implement" +optional = true +python-versions = ">=3.5" +files = [ + {file = "publicsuffixlist-1.0.2.20241113-py2.py3-none-any.whl", hash = "sha256:61867573005d0ac817c75fa99b30a1b6df5731d4edf1ce62b60e8cb1a42079ad"}, + {file = "publicsuffixlist-1.0.2.20241113.tar.gz", hash = "sha256:5984340cbb0effa7ef316f65a821da7005b20d19c8e6245c3ab943b3a1248fb4"}, +] + +[package.extras] +readme = ["pandoc"] +update = ["requests"] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydeep2" +version = "0.5.1" +description = "Python bindings for ssdeep" +optional = true +python-versions = "*" +files = [ + {file = "pydeep2-0.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14b310b820d895a7354be7fd025de874892df249cbfb3ad8a524459e1511fd8"}, + {file = "pydeep2-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2283893e25826b547dd1e5c71a010e86ddfd7270e2f2b8c90973c1d7984c7eb7"}, + {file = "pydeep2-0.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f248e3161deb53d46a9368a7c164e36d83004faf2f11625d47a5cf23a6bdd2cb"}, + {file = "pydeep2-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a13fca9be89a9fa8d92a4f49d7b9191eef94555f8ddf030fb2be4c8c15ad618c"}, + {file = "pydeep2-0.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cb4757db97ac15ddf034c21cd6bab984f841586b6d53984e63c9a7803b2cd4"}, + {file = "pydeep2-0.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7809a1d6640bdbee68f075d53229d05229e11b4711f232728dd540f68e6483a4"}, + {file = "pydeep2-0.5.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fedc1c9660cb5d0b73ad0b5f1dbffe16990e6721cbfc6454571a4b9882d0ea4"}, + {file = "pydeep2-0.5.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca68f7d63e2ef510d410d20b223e8e97df41707fb50c4c526b6dd1d8698d9e6"}, + {file = "pydeep2-0.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:199d05d8b4b7544509a2ba4802ead4b41dfe7859e0ecea9d9be9e41939f11660"}, + {file = "pydeep2-0.5.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bf00de2fe1918e4d698fe8195a5c0a3a0c3050a2e3e15583748cfd20b427153"}, + {file = "pydeep2-0.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c65dc910d782fa2bc97e1b28a78d77c4bada037d14b63e3e75a1fa5918d642c5"}, + {file = "pydeep2-0.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef00ca5681a2c4ad5dc744db5f8ae5406d3f13121b38d84cc58dfb8fce4c3dc2"}, + {file = "pydeep2-0.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:add24d7aa0386b285fd3e99632719714efabeb13d7b03a015b7c64d1f588f815"}, + {file = "pydeep2-0.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2063cbb053e5ce684cc45fff3e72c063b26aa85e41e6435cab0c658ad9e3e1e"}, + {file = "pydeep2-0.5.1.tar.gz", hash = "sha256:44ce447e3253a69d3393f3cc53e3a87a48fe3ff9861793736a7bc218a1b95d77"}, +] + +[[package]] +name = "pyfaup" +version = "1.2" +description = "Python bindings for the faup library" +optional = true +python-versions = "*" +files = [ + {file = "pyfaup-1.2-py2.py3-none-any.whl", hash = "sha256:75f96f7da86ffb5402d3fcc2dbf98a511e792cf9100c159e34cdba8996ddc7f9"}, + {file = "pyfaup-1.2.tar.gz", hash = "sha256:5648bc3ebd80239aec927aedfc218c3a6ff36de636cc53822bfeb70b0869b1e7"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = true +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, + {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, +] + +[[package]] +name = "pywin32" +version = "308" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, +] + +[[package]] +name = "pywinpty" +version = "2.0.14" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f"}, + {file = "pywinpty-2.0.14-cp311-none-win_amd64.whl", hash = "sha256:cf2a43ac7065b3e0dc8510f8c1f13a75fb8fde805efa3b8cff7599a1ef497bc7"}, + {file = "pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737"}, + {file = "pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819"}, + {file = "pywinpty-2.0.14-cp39-none-win_amd64.whl", hash = "sha256:5725fd56f73c0531ec218663bd8c8ff5acc43c78962fab28564871b5fce053fd"}, + {file = "pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "recommonmark" +version = "0.7.1" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +optional = true +python-versions = "*" +files = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] + +[package.dependencies] +commonmark = ">=0.8.1" +docutils = ">=0.11" +sphinx = ">=1.3.1" + +[[package]] +name = "red-black-tree-mod" +version = "1.20" +description = "Flexible python implementation of red black trees" +optional = true +python-versions = "*" +files = [ + {file = "red-black-tree-mod-1.20.tar.gz", hash = "sha256:2448e6fc9cbf1be204c753f352c6ee49aa8156dbf1faa57dfc26bd7705077e0a"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "reportlab" +version = "4.2.5" +description = "The Reportlab Toolkit" +optional = true +python-versions = "<4,>=3.7" +files = [ + {file = "reportlab-4.2.5-py3-none-any.whl", hash = "sha256:eb2745525a982d9880babb991619e97ac3f661fae30571b7d50387026ca765ee"}, + {file = "reportlab-4.2.5.tar.gz", hash = "sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f"}, +] + +[package.dependencies] +chardet = "*" +pillow = ">=9.0.0" + +[package.extras] +accel = ["rl-accel (>=0.9.0,<1.1)"] +pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"] +renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-mock" +version = "1.12.1" +description = "Mock out responses from the requests package" +optional = false +python-versions = ">=3.5" +files = [ + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, +] + +[package.dependencies] +requests = ">=2.22,<3" + +[package.extras] +fixture = ["fixtures"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rpds-py" +version = "0.21.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "rpds_py-0.21.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590"}, + {file = "rpds_py-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664"}, + {file = "rpds_py-0.21.0-cp310-none-win32.whl", hash = "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682"}, + {file = "rpds_py-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5"}, + {file = "rpds_py-0.21.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95"}, + {file = "rpds_py-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8"}, + {file = "rpds_py-0.21.0-cp311-none-win32.whl", hash = "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a"}, + {file = "rpds_py-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e"}, + {file = "rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d"}, + {file = "rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11"}, + {file = "rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952"}, + {file = "rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd"}, + {file = "rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937"}, + {file = "rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976"}, + {file = "rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202"}, + {file = "rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e"}, + {file = "rpds_py-0.21.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928"}, + {file = "rpds_py-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed"}, + {file = "rpds_py-0.21.0-cp39-none-win32.whl", hash = "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8"}, + {file = "rpds_py-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed"}, +] + +[[package]] +name = "rtfde" +version = "0.1.2" +description = "A library for extracting HTML content from RTF encapsulated HTML as commonly found in the exchange MSG email format." +optional = true +python-versions = "~=3.8" +files = [ + {file = "RTFDE-0.1.2-py3-none-any.whl", hash = "sha256:f6d1450c99b04e930da130e8b419aa33b1f953623e1b94ad5c0f67f0362eb737"}, +] + +[package.dependencies] +lark = ">=1.1.8,<1.2.0" +oletools = ">=0.56" + +[package.extras] +dev = ["coverage (>=7.2.2,<7.3.0)", "lxml (>=4.6,<5.0)", "mypy (>=1.1,<2.0)", "pdoc3 (>=0.10.0,<0.11.0)"] +msg-parse = ["extract-msg (>=0.27,<1.0)"] + +[[package]] +name = "send2trash" +version = "1.8.3" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "setuptools" +version = "75.5.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, + {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = true +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +description = "Python documentation generator" +optional = true +python-versions = ">=3.10" +files = [ + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.5.0" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = true +python-versions = ">=3.10" +files = [ + {file = "sphinx_autodoc_typehints-2.5.0-py3-none-any.whl", hash = "sha256:53def4753239683835b19bfa8b68c021388bd48a096efcb02cdab508ece27363"}, + {file = "sphinx_autodoc_typehints-2.5.0.tar.gz", hash = "sha256:259e1026b218d563d72743f417fcc25906a9614897fe37f91bd8d7d58f748c3b"}, +] + +[package.dependencies] +sphinx = ">=8.0.2" + +[package.extras] +docs = ["furo (>=2024.8.6)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "defusedxml (>=0.7.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1.1)", "typing-extensions (>=4.12.2)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = true +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "terminado" +version = "0.18.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, + {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "tinycss2" +version = "1.4.0" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, + {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["pytest", "ruff"] + +[[package]] +name = "tomli" +version = "2.1.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "types-cffi" +version = "1.16.0.20240331" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, + {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, +] + +[package.dependencies] +types-setuptools = "*" + +[[package]] +name = "types-click" +version = "7.1.8" +description = "Typing stubs for click" +optional = false +python-versions = "*" +files = [ + {file = "types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092"}, + {file = "types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81"}, +] + +[[package]] +name = "types-flask" +version = "1.1.6" +description = "Typing stubs for Flask" +optional = false +python-versions = "*" +files = [ + {file = "types-Flask-1.1.6.tar.gz", hash = "sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf"}, + {file = "types_Flask-1.1.6-py3-none-any.whl", hash = "sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087"}, +] + +[package.dependencies] +types-click = "*" +types-Jinja2 = "*" +types-Werkzeug = "*" + +[[package]] +name = "types-jinja2" +version = "2.11.9" +description = "Typing stubs for Jinja2" +optional = false +python-versions = "*" +files = [ + {file = "types-Jinja2-2.11.9.tar.gz", hash = "sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81"}, + {file = "types_Jinja2-2.11.9-py3-none-any.whl", hash = "sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2"}, +] + +[package.dependencies] +types-MarkupSafe = "*" + +[[package]] +name = "types-markupsafe" +version = "1.1.10" +description = "Typing stubs for MarkupSafe" +optional = false +python-versions = "*" +files = [ + {file = "types-MarkupSafe-1.1.10.tar.gz", hash = "sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1"}, + {file = "types_MarkupSafe-1.1.10-py3-none-any.whl", hash = "sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5"}, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +description = "Typing stubs for pyOpenSSL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, + {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-cffi = "*" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241003" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, + {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +description = "Typing stubs for redis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, + {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-pyOpenSSL = "*" + +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, + {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-setuptools" +version = "75.5.0.20241116" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-setuptools-75.5.0.20241116.tar.gz", hash = "sha256:b6939ffdbc50ffdc0bcfbf14f7a6de1ddc5510906c1ca2bd62c23646e5798b1a"}, + {file = "types_setuptools-75.5.0.20241116-py3-none-any.whl", hash = "sha256:1144b2ab8fa986061f963391fdbde16df20582e3cc39c94340e71aa61cc7203f"}, +] + +[[package]] +name = "types-werkzeug" +version = "1.0.9" +description = "Typing stubs for Werkzeug" +optional = false +python-versions = "*" +files = [ + {file = "types-Werkzeug-1.0.9.tar.gz", hash = "sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c"}, + {file = "types_Werkzeug-1.0.9-py3-none-any.whl", hash = "sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = true +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = true +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.dependencies] +brotli = {version = ">=1.0.9", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"brotli\""} +brotlicffi = {version = ">=0.8.0", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"brotli\""} + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "validators" +version = "0.34.0" +description = "Python Data Validation for Humans™" +optional = true +python-versions = ">=3.8" +files = [ + {file = "validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321"}, + {file = "validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f"}, +] + +[package.extras] +crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.9" +files = [ + {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, + {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "win-unicode-console" +version = "0.5" +description = "Enable Unicode input and display when running Python from Windows console." +optional = true +python-versions = "*" +files = [ + {file = "win_unicode_console-0.5.zip", hash = "sha256:d4142d4d56d46f449d6f00536a73625a871cba040f0bc1a2e305a04578f07d1e"}, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[extras] +brotli = ["urllib3"] +docs = ["Sphinx", "docutils", "recommonmark", "sphinx-autodoc-typehints"] +email = ["RTFDE", "extract_msg", "oletools"] +fileobjects = ["lief", "pydeep2", "python-magic"] +openioc = ["beautifulsoup4"] +pdfexport = ["reportlab"] +url = ["pyfaup"] +virustotal = ["validators"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "87c16bdffa94baa7d0e2e5fe5326c3ecc92d10306e6a5d90d3cbc82ad807a480" diff --git a/pymisp/__init__.py b/pymisp/__init__.py index f0497a9..ca0868d 100644 --- a/pymisp/__init__.py +++ b/pymisp/__init__.py @@ -1,60 +1,83 @@ -__version__ = '2.4.103' -import logging -import functools -import warnings -import sys +from __future__ import annotations -FORMAT = "%(levelname)s [%(filename)s:%(lineno)s - %(funcName)s() ] %(message)s" -formatter = logging.Formatter(FORMAT) -default_handler = logging.StreamHandler() -default_handler.setFormatter(formatter) +import logging +import sys +import warnings + +import importlib.metadata logger = logging.getLogger(__name__) -logger.addHandler(default_handler) -logger.setLevel(logging.WARNING) + +__version__ = importlib.metadata.version("pymisp") -def deprecated(func): - '''This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used.''' +def warning_2024() -> None: + if sys.version_info < (3, 10): + warnings.warn(""" +As our baseline system is the latest Ubuntu LTS, and Ubuntu LTS 22.04 has Python 3.10 available, +we will officially deprecate python versions below 3.10 on January 1st 2024. +**Please update your codebase.**""", DeprecationWarning, stacklevel=3) - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.showwarning( - "Call to deprecated function {}.".format(func.__name__), - category=DeprecationWarning, - filename=func.__code__.co_filename, - lineno=func.__code__.co_firstlineno + 1 - ) - return func(*args, **kwargs) - return new_func + +everything_broken = '''Unknown error: the response is not in JSON. +Something is broken server-side, please send us everything that follows (careful with the auth key): +Request headers: +{} +Request body: +{} +Response (if any): +{}''' try: - from .exceptions import PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey, InvalidMISPObject, UnknownMISPObjectTemplate, PyMISPInvalidFormat, MISPServerError, PyMISPNotImplementedYet, PyMISPUnexpectedResponse, PyMISPEmptyResponse # noqa - from .api import PyMISP # noqa - from .abstract import AbstractMISP, MISPEncode, MISPTag, Distribution, ThreatLevel, Analysis # noqa - from .mispevent import MISPEvent, MISPAttribute, MISPObjectReference, MISPObjectAttribute, MISPObject, MISPUser, MISPOrganisation, MISPSighting, MISPLog # noqa + warning_2024() + from .exceptions import (PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey, # noqa + InvalidMISPObject, UnknownMISPObjectTemplate, PyMISPInvalidFormat, MISPServerError, PyMISPNotImplementedYet, PyMISPUnexpectedResponse, PyMISPEmptyResponse) + from .abstract import AbstractMISP, MISPEncode, pymisp_json_default, MISPTag, Distribution, ThreatLevel, Analysis # noqa + from .mispevent import (MISPEvent, MISPAttribute, MISPObjectReference, MISPObjectAttribute, MISPObject, MISPUser, # noqa + MISPOrganisation, MISPSighting, MISPLog, MISPShadowAttribute, MISPWarninglist, MISPTaxonomy, + MISPNoticelist, MISPObjectTemplate, MISPSharingGroup, MISPRole, MISPServer, MISPFeed, + MISPEventDelegation, MISPUserSetting, MISPInbox, MISPEventBlocklist, MISPOrganisationBlocklist, + MISPEventReport, MISPCorrelationExclusion, MISPDecayingModel, MISPGalaxy, MISPGalaxyCluster, + MISPGalaxyClusterElement, MISPGalaxyClusterRelation, MISPNote, MISPOpinion, MISPRelationship) + from .api import PyMISP, register_user # noqa + # NOTE: the direct imports to .tools are kept for backward compatibility but should be removed in the future from .tools import AbstractMISPObjectGenerator # noqa - from .tools import Neo4j # noqa - from .tools import stix # noqa from .tools import openioc # noqa - from .tools import load_warninglists # noqa from .tools import ext_lookups # noqa + from .tools import update_objects # noqa + from .tools import load_warninglists # noqa - if sys.version_info >= (3, 6): - # Let's not bother with old python - try: - from .tools import reportlab_generator # noqa - except ImportError: - # FIXME: The import should not raise an exception if reportlab isn't installed - pass - except NameError: - # FIXME: The import should not raise an exception if reportlab isn't installed - pass - if sys.version_info >= (3, 6): - from .aping import ExpandedPyMISP # noqa + try: + from .tools import reportlab_generator # noqa + except ImportError: + # FIXME: The import should not raise an exception if reportlab isn't installed + pass + except NameError: + # FIXME: The import should not raise an exception if reportlab isn't installed + pass logger.debug('pymisp loaded properly') except ImportError as e: - logger.warning('Unable to load pymisp properly: {}'.format(e)) + logger.warning(f'Unable to load pymisp properly: {e}') + + +class ExpandedPyMISP(PyMISP): + + def __init__(self, *args, **kwargs): + warnings.warn('This class is deprecated, use PyMISP instead', FutureWarning) + super().__init__(*args, **kwargs) + + +__all__ = ['PyMISP', 'register_user', 'AbstractMISP', 'MISPTag', + 'MISPEvent', 'MISPAttribute', 'MISPObjectReference', 'MISPObjectAttribute', + 'MISPObject', 'MISPUser', 'MISPOrganisation', 'MISPSighting', 'MISPLog', + 'MISPShadowAttribute', 'MISPWarninglist', 'MISPTaxonomy', 'MISPNoticelist', + 'MISPObjectTemplate', 'MISPSharingGroup', 'MISPRole', 'MISPServer', 'MISPFeed', + 'MISPEventDelegation', 'MISPUserSetting', 'MISPInbox', 'MISPEventBlocklist', + 'MISPOrganisationBlocklist', 'MISPEventReport', 'MISPCorrelationExclusion', + 'MISPDecayingModel', 'MISPGalaxy', 'MISPGalaxyCluster', 'MISPGalaxyClusterElement', + 'MISPGalaxyClusterRelation', 'MISPNote', 'MISPOpinion', 'MISPRelationship', + 'PyMISPError', 'NewEventError', 'NewAttributeError', + 'NoURL', 'NoKey', 'InvalidMISPObject', 'UnknownMISPObjectTemplate', 'PyMISPInvalidFormat', + 'Distribution', 'ThreatLevel', 'Analysis', 'ExpandedPyMISP' + ] diff --git a/pymisp/abstract.py b/pymisp/abstract.py index fedaac5..c6d5a38 100644 --- a/pymisp/abstract.py +++ b/pymisp/abstract.py @@ -1,36 +1,49 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 + +from __future__ import annotations -import sys -import datetime -import json -from json import JSONEncoder -import collections import logging +from datetime import date, datetime +from deprecated import deprecated # type: ignore +from json import JSONEncoder +from uuid import UUID +from abc import ABCMeta from enum import Enum +from typing import Any, Mapping +from collections.abc import MutableMapping +from functools import lru_cache +from pathlib import Path -from .exceptions import PyMISPInvalidFormat +try: + import orjson # type: ignore + from orjson import loads, dumps + HAS_ORJSON = True +except ImportError: + from json import loads, dumps + HAS_ORJSON = False +from .exceptions import PyMISPInvalidFormat, PyMISPError logger = logging.getLogger('pymisp') -if sys.version_info < (3, 0): - logger.warning("You're using python 2, it is strongly recommended to use python >=3.6") - # This is required because Python 2 is a pain. - from datetime import tzinfo, timedelta +resources_path = Path(__file__).parent / 'data' +misp_objects_path = resources_path / 'misp-objects' / 'objects' +with (resources_path / 'describeTypes.json').open('rb') as f: + describe_types: dict[str, Any] = loads(f.read())['result'] - class UTC(tzinfo): - """UTC""" - def utcoffset(self, dt): - return timedelta(0) +class MISPFileCache: + # cache up to 150 JSON structures in class attribute - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return timedelta(0) + @staticmethod + @lru_cache(maxsize=150) + def _load_json(path: Path) -> dict[str, Any] | None: + if not path.exists(): + return None + with path.open('rb') as f: + data = loads(f.read()) + return data class Distribution(Enum): @@ -55,62 +68,80 @@ class Analysis(Enum): completed = 2 -def _int_to_str(d): +def _int_to_str(d: dict[str, Any]) -> dict[str, Any]: # transform all integer back to string for k, v in d.items(): - if isinstance(v, (int, float)) and not isinstance(v, bool): + if isinstance(v, dict): + d[k] = _int_to_str(v) + elif isinstance(v, int) and not isinstance(v, bool): d[k] = str(v) return d +@deprecated(reason=" Use method default=pymisp_json_default instead of cls=MISPEncode", version='2.4.117', action='default') class MISPEncode(JSONEncoder): - - def default(self, obj): + def default(self, obj: Any) -> dict[str, Any] | str: if isinstance(obj, AbstractMISP): return obj.jsonable() - elif isinstance(obj, datetime.datetime): + elif isinstance(obj, (datetime, date)): return obj.isoformat() elif isinstance(obj, Enum): return obj.value + elif isinstance(obj, UUID): + return str(obj) return JSONEncoder.default(self, obj) -class AbstractMISP(collections.MutableMapping): +class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta): # type: ignore[type-arg] + __resources_path = resources_path + __misp_objects_path = misp_objects_path + __describe_types = describe_types - __not_jsonable = [] - - def __init__(self, **kwargs): - """Abstract class for all the MISP objects""" - super(AbstractMISP, self).__init__() - self.__edited = True # As we create a new object, we assume it is edited + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + """Abstract class for all the MISP objects. + NOTE: Every method in every classes inheriting this one are doing + changes in memory and do not modify data on a remote MISP instance. + To do so, you need to call the respective add_* or update_* + methods in PyMISP. + """ + super().__init__() + self.__edited: bool = True # As we create a new object, we assume it is edited + self.__not_jsonable: list[str] = [] + self._fields_for_feed: set[str] + self.__self_defined_describe_types: dict[str, Any] | None = None + self.uuid: str if kwargs.get('force_timestamps') is not None: # Ignore the edited objects and keep the timestamps. - self.__force_timestamps = True + self.__force_timestamps: bool = True else: self.__force_timestamps = False - # List of classes having tags - from .mispevent import MISPAttribute, MISPEvent - self.__has_tags = (MISPAttribute, MISPEvent) - if isinstance(self, self.__has_tags): - self.Tag = [] - setattr(AbstractMISP, 'add_tag', AbstractMISP.__add_tag) - setattr(AbstractMISP, 'tags', property(AbstractMISP.__get_tags, AbstractMISP.__set_tags)) + @property + def describe_types(self) -> dict[str, Any]: + if self.__self_defined_describe_types: + return self.__self_defined_describe_types + return self.__describe_types + + @describe_types.setter + def describe_types(self, describe_types: dict[str, Any]) -> None: + self.__self_defined_describe_types = describe_types @property - def properties(self): - """All the class public properties that will be dumped in the dictionary, and the JSON export. - Note: all the properties starting with a `_` (private), or listed in __not_jsonable will be skipped. - """ - to_return = [] - for prop, value in vars(self).items(): - if prop.startswith('_') or prop in self.__not_jsonable: - continue - to_return.append(prop) - return to_return + def resources_path(self) -> Path: + return self.__resources_path - def from_dict(self, **kwargs): + @property + def misp_objects_path(self) -> Path: + return self.__misp_objects_path + + @misp_objects_path.setter + def misp_objects_path(self, misp_objects_path: str | Path) -> None: + if isinstance(misp_objects_path, str): + misp_objects_path = Path(misp_objects_path) + self.__misp_objects_path = misp_objects_path + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] """Loading all the parameters as class properties, if they aren't `None`. This method aims to be called when all the properties requiring a special treatment are processed. @@ -123,31 +154,51 @@ class AbstractMISP(collections.MutableMapping): # We load an existing dictionary, marking it an not-edited self.__edited = False - def update_not_jsonable(self, *args): + def update_not_jsonable(self, *args) -> None: # type: ignore[no-untyped-def] """Add entries to the __not_jsonable list""" self.__not_jsonable += args - def set_not_jsonable(self, *args): + def set_not_jsonable(self, args: list[str]) -> None: """Set __not_jsonable to a new list""" self.__not_jsonable = args - def from_json(self, json_string): - """Load a JSON string""" - self.from_dict(**json.loads(json_string)) + def _remove_from_not_jsonable(self, *args) -> None: # type: ignore[no-untyped-def] + """Remove the entries that are in the __not_jsonable list""" + for entry in args: + try: + self.__not_jsonable.remove(entry) + except ValueError: + pass - def to_dict(self): - """Dump the lass to a dictionary. + def from_json(self, json_string: str) -> None: + """Load a JSON string""" + self.from_dict(**loads(json_string)) + + def to_dict(self, json_format: bool = False) -> dict[str, Any]: + """Dump the class to a dictionary. This method automatically removes the timestamp recursively in every object that has been edited is order to let MISP update the event accordingly.""" + is_edited = self.edited to_return = {} - for attribute in self.properties: - val = getattr(self, attribute, None) + for attribute, val in self.items(): if val is None: continue elif isinstance(val, list) and len(val) == 0: continue + elif isinstance(val, str): + val = val.strip() + elif json_format: + if isinstance(val, AbstractMISP): + val = val.to_json(True) + elif isinstance(val, (datetime, date)): + val = val.isoformat() + elif isinstance(val, Enum): + val = val.value + elif isinstance(val, UUID): + val = str(val) + if attribute == 'timestamp': - if not self.__force_timestamps and self.edited: + if not self.__force_timestamps and is_edited: # In order to be accepted by MISP, the timestamp of an object # needs to be either newer, or None. # If the current object is marked as edited, the easiest is to @@ -155,78 +206,132 @@ class AbstractMISP(collections.MutableMapping): continue else: val = self._datetime_to_timestamp(val) + if (attribute in ('first_seen', 'last_seen', 'datetime') + and isinstance(val, datetime) + and not val.tzinfo): + # Need to make sure the timezone is set. Otherwise, it will be processed as UTC on the server + val = val.astimezone() + to_return[attribute] = val to_return = _int_to_str(to_return) return to_return - def jsonable(self): + def jsonable(self) -> dict[str, Any]: """This method is used by the JSON encoder""" return self.to_dict() - def to_json(self): - """Dump recursively any class of type MISPAbstract to a json string""" - return json.dumps(self, cls=MISPEncode, sort_keys=True, indent=2) + def _to_feed(self) -> dict[str, Any]: + if not hasattr(self, '_fields_for_feed') or not self._fields_for_feed: + raise PyMISPError('Unable to export in the feed format, _fields_for_feed is missing.') + if hasattr(self, '_set_default') and callable(self._set_default): + self._set_default() + to_return = {} + for field in sorted(self._fields_for_feed): + if getattr(self, field, None) is not None: + if field in ['timestamp', 'publish_timestamp']: + to_return[field] = self._datetime_to_timestamp(getattr(self, field)) + elif isinstance(getattr(self, field), (datetime, date)): + to_return[field] = getattr(self, field).isoformat() + else: + to_return[field] = getattr(self, field) + else: + if field in ['data', 'first_seen', 'last_seen', 'deleted']: + # special fields + continue + raise PyMISPError(f'The field {field} is required in {self.__class__.__name__} when generating a feed.') + to_return = _int_to_str(to_return) + return to_return - def __getitem__(self, key): + def to_json(self, sort_keys: bool = False, indent: int | None = None) -> str: + """Dump recursively any class of type MISPAbstract to a json string""" + if HAS_ORJSON: + option = 0 + if sort_keys: + option |= orjson.OPT_SORT_KEYS + if indent: + option |= orjson.OPT_INDENT_2 + # orjson dumps method returns bytes instead of bytes, to keep compatibility with json + # we have to convert output to str + return dumps(self, default=pymisp_json_default, option=option).decode() + + return dumps(self, default=pymisp_json_default, sort_keys=sort_keys, indent=indent) + + def __getitem__(self, key: str) -> Any: try: - return getattr(self, key) + if key[0] != '_' and key not in self.__not_jsonable: + return self.__dict__[key] + raise KeyError except AttributeError: # Expected by pop and other dict-related methods raise KeyError - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: setattr(self, key, value) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: delattr(self, key) - def __iter__(self): - return iter(self.to_dict()) + def __iter__(self) -> Any: + '''When we call **self, skip keys: + * starting with _ + * in __not_jsonable + * timestamp if the object is edited *unless* it is forced + ''' + return iter({k: v for k, v in self.__dict__.items() + if not (k[0] == '_' + or k in self.__not_jsonable + or (not self.__force_timestamps and (k == 'timestamp' and self.__edited)))}) - def __len__(self): - return len(self.to_dict()) + def __len__(self) -> int: + return len([k for k in self.__dict__.keys() if not (k[0] == '_' or k in self.__not_jsonable)]) @property - def edited(self): + def force_timestamp(self) -> bool: + return self.__force_timestamps + + @force_timestamp.setter + def force_timestamp(self, force: bool) -> None: + self.__force_timestamps = force + + @property + def edited(self) -> bool: """Recursively check if an object has been edited and update the flag accordingly to the parent objects""" if self.__edited: return self.__edited - for p in self.properties: - if self.__edited: - break - val = getattr(self, p) + for p, val in self.items(): if isinstance(val, AbstractMISP) and val.edited: self.__edited = True + break elif isinstance(val, list) and all(isinstance(a, AbstractMISP) for a in val): if any(a.edited for a in val): self.__edited = True + break return self.__edited @edited.setter - def edited(self, val): + def edited(self, val: bool) -> None: """Set the edit flag""" if isinstance(val, bool): self.__edited = val else: - raise Exception('edited can only be True or False') + raise PyMISPError('edited can only be True or False') - def __setattr__(self, name, value): - if name in self.properties: + def __setattr__(self, name: str, value: Any) -> None: + if name[0] != '_' and not self.__edited and name in self: + # The private members don't matter + # If we already have a key with that name, we're modifying it. self.__edited = True - super(AbstractMISP, self).__setattr__(name, value) + super().__setattr__(name, value) - def _datetime_to_timestamp(self, d): - """Convert a datetime.datetime object to a timestamp (int)""" - if isinstance(d, (int, str)) or (sys.version_info < (3, 0) and isinstance(d, unicode)): + def _datetime_to_timestamp(self, d: int | float | str | datetime) -> int: + """Convert a datetime object to a timestamp (int)""" + if isinstance(d, (int, float, str)): # Assume we already have a timestamp return int(d) - if sys.version_info >= (3, 3): - return int(d.timestamp()) - else: - return int((d - datetime.datetime.fromtimestamp(0, UTC())).total_seconds()) + return int(d.timestamp()) - def __add_tag(self, tag=None, **kwargs): + def _add_tag(self, tag: str | MISPTag | Mapping[str, Any] | None = None, **kwargs): # type: ignore[no-untyped-def] """Add a tag to the attribute (by name or a MISPTag object)""" if isinstance(tag, str): misp_tag = MISPTag() @@ -240,23 +345,20 @@ class AbstractMISP(collections.MutableMapping): misp_tag = MISPTag() misp_tag.from_dict(**kwargs) else: - raise PyMISPInvalidFormat("The tag is in an invalid format (can be either string, MISPTag, or an expanded dict): {}".format(tag)) - if misp_tag not in self.tags: + raise PyMISPInvalidFormat(f"The tag is in an invalid format (can be either string, MISPTag, or an expanded dict): {tag}") + if misp_tag not in self.tags: # type: ignore self.Tag.append(misp_tag) self.edited = True + return misp_tag - def __get_tags(self): - """Returns a lost of tags associated to this Attribute""" - return self.Tag - - def __set_tags(self, tags): + def _set_tags(self, tags: list[MISPTag]) -> None: """Set a list of prepared MISPTag.""" if all(isinstance(x, MISPTag) for x in tags): self.Tag = tags else: raise PyMISPInvalidFormat('All the attributes have to be of type MISPTag.') - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, AbstractMISP): return self.to_dict() == other.to_dict() elif isinstance(other, dict): @@ -264,16 +366,58 @@ class AbstractMISP(collections.MutableMapping): else: return False + def __repr__(self) -> str: + return f'<{self.__class__.__name__} - please define me>' + class MISPTag(AbstractMISP): - def __init__(self): - super(MISPTag, self).__init__() - def from_dict(self, name, **kwargs): - self.name = name - super(MISPTag, self).from_dict(**kwargs) + _fields_for_feed: set[str] = {'name', 'colour', 'relationship_type', 'local'} - def __repr__(self): + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(**kwargs) + self.name: str + self.exportable: bool + self.local: bool + self.relationship_type: str | None + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if kwargs.get('Tag'): + kwargs = kwargs.get('Tag') # type: ignore[assignment] + super().from_dict(**kwargs) + + def _set_default(self) -> None: + if not hasattr(self, 'relationship_type'): + self.relationship_type = '' + if not hasattr(self, 'colour'): + self.colour = '#ffffff' + if not hasattr(self, 'local'): + self.local = False + + def _to_feed(self, with_local: bool = True) -> dict[str, Any]: + if hasattr(self, 'exportable') and not self.exportable: + return {} + if with_local is False and hasattr(self, 'local') and self.local: + return {} + return super()._to_feed() + + def delete(self) -> None: + self.deleted = True + self.edited = True + + def __repr__(self) -> str: if hasattr(self, 'name'): - return '<{self.__class__.__name__}(name={self.name})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return '<{self.__class__.__name__}(name={self.name})>'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)>' + + +# UUID, datetime, date and Enum is serialized by ORJSON by default +def pymisp_json_default(obj: AbstractMISP | datetime | date | Enum | UUID) -> dict[str, Any] | str: + if isinstance(obj, AbstractMISP): + return obj.jsonable() + elif isinstance(obj, (datetime, date)): + return obj.isoformat() + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, UUID): + return str(obj) diff --git a/pymisp/api.py b/pymisp/api.py index ea33254..0230bf6 100644 --- a/pymisp/api.py +++ b/pymisp/api.py @@ -1,2383 +1,4078 @@ +from __future__ import annotations -# -*- coding: utf-8 -*- - -"""Python API using the REST interface of MISP""" - -import copy -import sys -import json -import datetime -from dateutil.parser import parse -import os -import base64 -import re +from typing import TypeVar, Any, Mapping, Iterable, MutableMapping, Union, List, Dict +from datetime import date, datetime +import csv +from pathlib import Path import logging -from io import BytesIO, open -import zipfile +from urllib.parse import urljoin +import requests +from requests.auth import AuthBase +import re +from uuid import UUID +import warnings +import sys +import copy +from io import BytesIO, StringIO +from importlib.metadata import version -from . import __version__, deprecated -from .exceptions import PyMISPError, SearchError, NoURL, NoKey, PyMISPEmptyResponse -from .mispevent import MISPEvent, MISPAttribute, MISPUser, MISPOrganisation, MISPSighting, MISPFeed, MISPObject -from .abstract import AbstractMISP, MISPEncode +try: + # orjson is optional dependency that speedups parsing and encoding JSON + from orjson import loads, dumps # type: ignore + HAS_ORJSON = True +except ImportError: + from json import loads, dumps + HAS_ORJSON = False + +from . import __version__, everything_broken +from .exceptions import MISPServerError, PyMISPUnexpectedResponse, PyMISPError, NoURL, NoKey +from .mispevent import MISPEvent, MISPAttribute, MISPSighting, MISPLog, MISPObject, \ + MISPUser, MISPOrganisation, MISPShadowAttribute, MISPWarninglist, MISPTaxonomy, \ + MISPGalaxy, MISPNoticelist, MISPObjectReference, MISPObjectTemplate, MISPSharingGroup, \ + MISPRole, MISPServer, MISPFeed, MISPEventDelegation, MISPCommunity, MISPUserSetting, \ + MISPInbox, MISPEventBlocklist, MISPOrganisationBlocklist, MISPEventReport, \ + MISPGalaxyCluster, MISPGalaxyClusterRelation, MISPCorrelationExclusion, MISPDecayingModel, \ + MISPNote, MISPOpinion, MISPRelationship, AnalystDataBehaviorMixin +from .abstract import pymisp_json_default, MISPTag, AbstractMISP, describe_types + + +if sys.platform == 'linux': + # Enable TCP keepalive by default on every requests + import socket + from urllib3.connection import HTTPConnection + HTTPConnection.default_socket_options = HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), # enable keepalive + (socket.SOL_TCP, socket.TCP_KEEPIDLE, 30), # Start pinging after 30s of idle time + (socket.SOL_TCP, socket.TCP_KEEPINTVL, 10), # ping every 10s + (socket.SOL_TCP, socket.TCP_KEEPCNT, 6) # kill the connection if 6 ping fail (60s total) + ] + +try: + # cached_property exists since Python 3.8 + from functools import cached_property +except ImportError: + from functools import lru_cache + + def cached_property(func): # type: ignore + return property(lru_cache(func)) + +SearchType = TypeVar('SearchType', str, int) +# str: string to search / list: values to search (OR) / dict: {'OR': [list], 'NOT': [list], 'AND': [list]} +# NOTE: we cannot use new typing here until we drop Python 3.8 and 3.9 support +SearchParameterTypes = TypeVar('SearchParameterTypes', str, List[Union[str, int]], Dict[str, Union[str, int]]) + +ToIDSType = TypeVar('ToIDSType', str, int, bool) logger = logging.getLogger('pymisp') -try: - from urllib.parse import urljoin - # Least dirty way to support python 2 and 3 - basestring = str - unicode = str -except ImportError: - from urlparse import urljoin - logger.warning("You're using python 2, it is strongly recommended to use python >=3.6") -try: - import requests - HAVE_REQUESTS = True -except ImportError: - HAVE_REQUESTS = False +def get_uuid_or_id_from_abstract_misp(obj: AbstractMISP | int | str | UUID | dict[str, Any]) -> str | int: + """Extract the relevant ID accordingly to the given type passed as parameter""" + if isinstance(obj, UUID): + return str(obj) + if isinstance(obj, (int, str)): + return obj -if (3, 0) <= sys.version_info < (3, 6): - OLD_PY3 = True -else: - OLD_PY3 = False + if isinstance(obj, dict) and len(obj) == 1: + # We have an object in that format: {'Event': {'id': 2, ...}} + # We need to get the content of that dictionary + obj = obj[list(obj.keys())[0]] -try: - from requests_futures.sessions import FuturesSession - ASYNC_OK = True -except ImportError: - ASYNC_OK = False + if isinstance(obj, MISPShadowAttribute): + # A ShadowAttribute has the same UUID as the related Attribute, we *need* to use the ID + return obj['id'] + if isinstance(obj, MISPEventDelegation): + # An EventDelegation doesn't have a uuid, we *need* to use the ID + return obj['id'] -everything_broken = '''Unknown error: the response is not in JSON. -Something is broken server-side, please send us everything that follows (careful with the auth key): -Request headers: -{} -Request body: -{} -Response (if any): -{}''' + # For the blocklists, we want to return a specific key. + if isinstance(obj, MISPEventBlocklist): + return obj.event_uuid + if isinstance(obj, MISPOrganisationBlocklist): + return obj.org_uuid + + # at this point, we must have an AbstractMISP + if 'uuid' in obj: # type: ignore + return obj['uuid'] # type: ignore + return obj['id'] # type: ignore -class PyMISP(object): +def register_user(misp_url: str, email: str, + organisation: MISPOrganisation | int | str | UUID | None = None, + org_id: str | None = None, org_name: str | None = None, + message: str | None = None, custom_perms: str | None = None, + perm_sync: bool = False, perm_publish: bool = False, perm_admin: bool = False, + verify: bool = True) -> dict[str, Any] | list[dict[str, Any]]: + """Ask for the creation of an account for the user with the given email address""" + data = copy.deepcopy(locals()) + if organisation: + data['org_uuid'] = get_uuid_or_id_from_abstract_misp(data.pop('organisation')) + + url = urljoin(data.pop('misp_url'), 'users/register') + user_agent = f'PyMISP {__version__} - no login - Python {".".join(str(x) for x in sys.version_info[:2])}' + headers = { + 'Accept': 'application/json', + 'content-type': 'application/json', + 'User-Agent': user_agent} + r = requests.post(url, json=data, verify=data.pop('verify'), headers=headers) + return r.json() + + +def brotli_supported() -> bool: + """ + Returns whether Brotli compression is supported + """ + major: int + minor: int + patch: int + + # urllib >= 1.25.1 includes brotli support + version_splitted = version('urllib3').split('.') # noqa: F811 + if len(version_splitted) == 2: + major, minor = version_splitted # type: ignore + patch = 0 + else: + major, minor, patch = version_splitted # type: ignore + major, minor, patch = int(major), int(minor), int(patch) + urllib3_with_brotli = (major == 1 and ((minor == 25 and patch >= 1) or (minor >= 26))) or major >= 2 + + if not urllib3_with_brotli: + return False + + # pybrotli is an extra package required by urllib3 for brotli support + try: + import brotli # type: ignore # noqa + return True + except ImportError: + return False + + +class PyMISP: """Python API for MISP :param url: URL of the MISP instance you want to connect to :param key: API key of the user you want to use - :param ssl: can be True or False (to check ot not the validity of the certificate. Or a CA_BUNDLE in case of self signed certiifcate (the concatenation of all the \*.crt of the chain) - :param out_type: Type of object (json) NOTE: XML output isn't supported anymore, keeping the flag for compatibility reasons. + :param ssl: can be True or False (to check or to not check the validity of the certificate. Or a CA_BUNDLE in case of self signed or other certificate (the concatenation of all the crt of the chain) :param debug: Write all the debug information to stderr - :param proxies: Proxy dict as describes here: http://docs.python-requests.org/en/master/user/advanced/#proxies - :param cert: Client certificate, as described there: http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates - :param asynch: Use asynchronous processing where possible + :param proxies: Proxy dict, as described here: http://docs.python-requests.org/en/master/user/advanced/#proxies + :param cert: Client certificate, as described here: http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates :param auth: The auth parameter is passed directly to requests, as described here: http://docs.python-requests.org/en/master/user/authentication/ + :param tool: The software using PyMISP (string), used to set a unique user-agent + :param http_headers: Arbitrary headers to pass to all the requests. + :param https_adapter: Arbitrary HTTPS adapter for the requests session. + :param http_auth_header_name: The name of the HTTP header to use for the API key. Can be either "Authorization" or "X-MISP-AUTH". + :param timeout: Timeout, as described here: https://requests.readthedocs.io/en/master/user/advanced/#timeouts """ - def __init__(self, url, key, ssl=True, out_type='json', debug=None, proxies=None, cert=None, asynch=False, auth=None): + def __init__(self, url: str, key: str, ssl: bool | str = True, debug: bool = False, proxies: MutableMapping[str, str] | None = None, + cert: str | tuple[str, str] | None = None, auth: AuthBase | None = None, tool: str = '', + timeout: float | tuple[float, float] | None = None, + http_headers: dict[str, str] | None = None, + https_adapter: requests.adapters.BaseAdapter | None = None, + http_auth_header_name: str = 'Authorization' + ): + if not url: raise NoURL('Please provide the URL of your MISP instance.') if not key: raise NoKey('Please provide your authorization key.') - self.root_url = url - self.key = key - self.ssl = ssl - self.proxies = proxies - self.cert = cert - self.asynch = asynch - self.auth = auth - if asynch and not ASYNC_OK: - logger.critical("You turned on Async, but don't have requests_futures installed") - self.asynch = False + self.root_url: str = url + self.key: str = key.strip() + self.ssl: bool | str = ssl + self.proxies: MutableMapping[str, str] | None = proxies + self.cert: str | tuple[str, str] | None = cert + self.auth: AuthBase | None = auth + self.timeout: float | tuple[float, float] | None = timeout + self.__session = requests.Session() # use one session to keep connection between requests + if https_adapter is not None: + self.__session.mount('https://', https_adapter) + if brotli_supported(): + self.__session.headers['Accept-Encoding'] = ', '.join(('br', 'gzip', 'deflate')) - self.resources_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') - if out_type != 'json': - raise PyMISPError('The only output type supported by PyMISP is JSON. If you still rely on XML, use PyMISP v2.4.49') + if http_auth_header_name in ['Authorization', 'X-MISP-AUTH']: + self.__session.headers[http_auth_header_name] = self.key + else: + raise PyMISPError('http_auth_header_name should be either "Authorization" or "X-MISP-AUTH"') + + user_agent = f'PyMISP {__version__} - Python {".".join(str(x) for x in sys.version_info[:2])}' + if tool: + user_agent = f'{user_agent} - {tool}' + self.__session.headers['User-Agent'] = user_agent + + if http_headers: + self.__session.headers.update(http_headers) + + self.global_pythonify = False + + self.resources_path = Path(__file__).parent / 'data' if debug: logger.setLevel(logging.DEBUG) logger.info('To configure logging in your script, leave it to None and use the following: import logging; logging.getLogger(\'pymisp\').setLevel(logging.DEBUG)') try: # Make sure the MISP instance is working and the URL is valid - response = self.get_recommended_api_version() - if response.get('errors'): - logger.warning(response.get('errors')[0]) - elif not response.get('version'): - logger.warning("Unable to check the recommended PyMISP version (MISP <2.4.60), please upgrade.") + response = self.recommended_pymisp_version + if 'errors' in response: + logger.warning(response['errors'][0]) else: - pymisp_version_tup = tuple(int(x) for x in __version__.split('.')) - recommended_version_tup = tuple(int(x) for x in response['version'].split('.')) + pymisp_version_tup = tuple(int(x) for x in __version__.split('.')[:3]) + recommended_version_tup = tuple(int(x) for x in response['version'].split('.')[:3]) if recommended_version_tup < pymisp_version_tup[:3]: - logger.info("The version of PyMISP recommended by the MISP instance ({}) is older than the one you're using now ({}). If you have a problem, please upgrade the MISP instance or use an older PyMISP version.".format(response['version'], __version__)) + logger.info(f"The version of PyMISP recommended by the MISP instance ({response['version']}) is older than the one you're using now ({__version__}). If you have a problem, please upgrade the MISP instance or use an older PyMISP version.") elif pymisp_version_tup[:3] < recommended_version_tup: - logger.warning("The version of PyMISP recommended by the MISP instance ({}) is newer than the one you're using now ({}). Please upgrade PyMISP.".format(response['version'], __version__)) + logger.warning(f"The version of PyMISP recommended by the MISP instance ({response['version']}) is newer than the one you're using now ({__version__}). Please upgrade PyMISP.") + misp_version = self.misp_instance_version + if 'version' in misp_version: + self._misp_version = tuple(int(v) for v in misp_version['version'].split('.')) + + # Get the user information + self._current_user: MISPUser + self._current_role: MISPRole + self._current_user_settings: list[MISPUserSetting] + user_infos = self.get_user(pythonify=True, expanded=True) + if isinstance(user_infos, dict): + # There was an error during the get_user call + if e := user_infos.get('errors'): + raise PyMISPError(f'Unable to get the user settings: {e}') + raise PyMISPError(f'Unexpected error when initializing the connection: {user_infos}') + elif isinstance(user_infos, tuple) and len(user_infos) == 3: + self._current_user, self._current_role, self._current_user_settings = user_infos + else: + raise PyMISPError(f'Unexpected error when initializing the connection: {user_infos}') + except PyMISPError as e: + raise e except Exception as e: - raise PyMISPError('Unable to connect to MISP ({}). Please make sure the API key and the URL are correct (http/https is required): {}'.format(self.root_url, e)) + raise PyMISPError(f'Unable to connect to MISP ({self.root_url}). Please make sure the API key and the URL are correct (http/https is required): {e}') try: - self.describe_types = self.get_live_describe_types() + self.describe_types = self.describe_types_remote except Exception: - self.describe_types = self.get_local_describe_types() + self.describe_types = self.describe_types_local self.categories = self.describe_types['categories'] self.types = self.describe_types['types'] self.category_type_mapping = self.describe_types['category_type_mappings'] self.sane_default = self.describe_types['sane_defaults'] - def __repr__(self): - return '<{self.__class__.__name__}(url={self.root_url})'.format(self=self) + def remote_acl(self, debug_type: str = 'findMissingFunctionNames') -> dict[str, Any] | list[dict[str, Any]]: + """This should return an empty list, unless the ACL is outdated. - def get_live_query_acl(self): - """This should return an empty list, unless the ACL is outdated.""" - response = self._prepare_request('GET', urljoin(self.root_url, 'events/queryACL.json')) - return self._check_response(response) + :param debug_type: printAllFunctionNames, findMissingFunctionNames, or printRoleAccess + """ + response = self._prepare_request('GET', f'events/queryACL/{debug_type}') + return self._check_json_response(response) - def get_local_describe_types(self): - with open(os.path.join(self.resources_path, 'describeTypes.json'), 'rb') as f: - if OLD_PY3: - describe_types = json.loads(f.read().decode()) - else: - describe_types = json.load(f) - return describe_types['result'] - - def get_live_describe_types(self): - response = self._prepare_request('GET', urljoin(self.root_url, 'attributes/describeTypes.json')) - describe_types = self._check_response(response) - if describe_types.get('error'): - for e in describe_types.get('error'): - raise PyMISPError('Failed: {}'.format(e)) - describe_types = describe_types['result'] - if not describe_types.get('sane_defaults'): - raise PyMISPError('The MISP server your are trying to reach is outdated (<2.4.52). Please use PyMISP v2.4.51.1 (pip install -I PyMISP==v2.4.51.1) and/or contact your administrator.') + @property + def describe_types_local(self) -> dict[str, Any] | list[dict[str, Any]]: + '''Returns the content of describe types from the package''' return describe_types - def _prepare_request(self, request_type, url, data=None, - background_callback=None, output_type='json'): - if logger.isEnabledFor(logging.DEBUG): - logger.debug('{} - {}'.format(request_type, url)) - if data is not None: - logger.debug(data) - if data is None: - req = requests.Request(request_type, url) - else: - if not isinstance(data, str): - if isinstance(data, dict): - # Remove None values. - data = {k: v for k, v in data.items() if v is not None} - data = json.dumps(data) - req = requests.Request(request_type, url, data=data) - if self.asynch and background_callback is not None: - local_session = FuturesSession - else: - local_session = requests.Session - with local_session() as s: - req.auth = self.auth - prepped = s.prepare_request(req) - prepped.headers.update( - {'Authorization': self.key, - 'Accept': 'application/{}'.format(output_type), - 'content-type': 'application/{}'.format(output_type), - 'User-Agent': 'PyMISP {} - Python {}.{}.{}'.format(__version__, *sys.version_info)}) - if logger.isEnabledFor(logging.DEBUG): - logger.debug(prepped.headers) - settings = s.merge_environment_settings(req.url, proxies=self.proxies or {}, stream=None, verify=self.ssl, cert=self.cert) - if self.asynch and background_callback is not None: - return s.send(prepped, background_callback=background_callback, **settings) - else: - return s.send(prepped, **settings) + @property + def describe_types_remote(self) -> dict[str, Any] | list[dict[str, Any]]: + '''Returns the content of describe types from the remote instance''' + response = self._prepare_request('GET', 'attributes/describeTypes.json') + remote_describe_types = self._check_json_response(response) + return remote_describe_types['result'] - # ##################### - # ### Core helpers #### - # ##################### + @property + def recommended_pymisp_version(self) -> dict[str, Any] | list[dict[str, Any]]: + """Returns the recommended API version from the server""" + # Sine MISP 2.4.146 is recommended PyMISP version included in getVersion call + misp_version = self.misp_instance_version + if "pymisp_recommended_version" in misp_version: + return {"version": misp_version["pymisp_recommended_version"]} # Returns dict to keep BC - def flatten_error_messages(self, response): - """Dirty dirty method to normalize the error messages between the API calls. - Any response containing the a key 'error' or 'errors' failed at some point, - we make one single list out of it. + response = self._prepare_request('GET', 'servers/getPyMISPVersion.json') + return self._check_json_response(response) + + @property + def version(self) -> dict[str, Any] | list[dict[str, Any]]: + """Returns the version of PyMISP you're currently using""" + return {'version': __version__} + + @property + def pymisp_version_master(self) -> dict[str, Any] | list[dict[str, Any]]: + """PyMISP version as defined in the main repository""" + return self.pymisp_version_main + + @property + def pymisp_version_main(self) -> dict[str, Any] | list[dict[str, Any]]: + """Get the most recent version of PyMISP from github""" + r = requests.get('https://raw.githubusercontent.com/MISP/PyMISP/main/pyproject.toml') + if r.status_code == 200: + version = re.findall('version = "(.*)"', r.text) + return {'version': version[0]} + return {'error': 'Impossible to retrieve the version of the main branch.'} + + @cached_property + def misp_instance_version(self) -> dict[str, Any] | list[dict[str, Any]]: + """Returns the version of the instance.""" + response = self._prepare_request('GET', 'servers/getVersion') + return self._check_json_response(response) + + @property + def misp_instance_version_master(self) -> dict[str, Any] | list[dict[str, Any]]: + """Get the most recent version from github""" + r = requests.get('https://raw.githubusercontent.com/MISP/MISP/2.5/VERSION.json') + if r.status_code == 200: + master_version = loads(r.content) + return {'version': '{}.{}.{}'.format(master_version['major'], master_version['minor'], master_version['hotfix'])} + return {'error': 'Impossible to retrieve the version of the master branch.'} + + def update_misp(self) -> dict[str, Any] | list[dict[str, Any]]: + """Trigger a server update""" + response = self._prepare_request('POST', 'servers/update') + return self._check_json_response(response) + + def set_server_setting(self, setting: str, value: str | int | bool, force: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Set a setting on the MISP instance + + :param setting: server setting name + :param value: value to set + :param force: override value test """ - messages = [] - if response.get('error'): - if isinstance(response['error'], list): - for e in response['error']: - if isinstance(e, dict): - messages.append(e['error']['value'][0]) - else: - messages.append(e) - else: - messages.append(['error']) - elif response.get('errors'): - if isinstance(response['errors'], dict): - for where, errors in response['errors'].items(): - if isinstance(errors, dict): - for where, msg in errors.items(): - if isinstance(msg, list): - for m in msg: - messages.append('Error in {}: {}'.format(where, m)) - else: - messages.append('Error in {}: {}'.format(where, msg)) - else: - if isinstance(errors, list): - for e in errors: - if not e: - continue - if isinstance(e, basestring): - messages.append(e) - continue - for type_e, msgs in e.items(): - for m in msgs: - messages.append('Error in {}: {}'.format(where, m)) - else: - messages.append('{} ({})'.format(errors, where)) + data = {'value': value, 'force': force} + response = self._prepare_request('POST', f'servers/serverSettingsEdit/{setting}', data=data) + return self._check_json_response(response) - return messages + def get_server_setting(self, setting: str) -> dict[str, Any] | list[dict[str, Any]]: + """Get a setting from the MISP instance - def _check_response(self, response): - """Check if the response from the server is not an unexpected error""" - try: - json_response = response.json() - except ValueError: - # If the server didn't return a JSON blob, we've a problem. - if not len(response.text): - raise PyMISPEmptyResponse('The server returned an empty response. \n{}\n{}\n'.format(response.request.headers, response.request.body)) - raise PyMISPError(everything_broken.format(response.request.headers, response.request.body, response.text)) + :param setting: server setting name + """ + response = self._prepare_request('GET', f'servers/getSetting/{setting}') + return self._check_json_response(response) - errors = [] + def server_settings(self) -> dict[str, Any] | list[dict[str, Any]]: + """Get all the settings from the server""" + response = self._prepare_request('GET', 'servers/serverSettings') + return self._check_json_response(response) - if response.status_code >= 500: - errors.append('500 exception: {}'.format(json_response)) - logger.critical(everything_broken.format(response.request.headers, response.request.body, json_response)) + def restart_workers(self) -> dict[str, Any] | list[dict[str, Any]]: + """Restart all the workers""" + response = self._prepare_request('POST', 'servers/restartWorkers') + return self._check_json_response(response) - to_return = json_response - if isinstance(to_return, (list, str)): - # FIXME: This case look like a bug. - to_return = {'response': to_return} - else: - if to_return.get('error'): - if not isinstance(to_return['error'], list): - errors.append(to_return['error']) - else: - errors += to_return['error'] - if to_return.get('errors'): - if not isinstance(to_return['errors'], list): - errors.append(to_return['errors']) - else: - errors += to_return['errors'] + def db_schema_diagnostic(self) -> dict[str, Any] | list[dict[str, Any]]: + """Get the schema diagnostic""" + response = self._prepare_request('GET', 'servers/dbSchemaDiagnostic') + return self._check_json_response(response) - if 400 <= response.status_code < 500: - if not errors and to_return.get('message'): - errors.append(to_return['message']) - else: - errors.append(str(response.status_code)) - errors += self.flatten_error_messages(to_return) - if errors: - to_return['errors'] = errors - if logger.isEnabledFor(logging.DEBUG): - logger.debug(json.dumps(to_return, indent=4)) + def toggle_global_pythonify(self) -> None: + """Toggle the pythonify variable for the class""" + self.global_pythonify = not self.global_pythonify + + # ## BEGIN Event ## + + def events(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEvent] | list[dict[str, Any]]: + """Get all the events from the MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/getEvents + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'events/index') + events_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(events_r, dict): + return events_r + to_return = [] + for event in events_r: + e = MISPEvent() + e.from_dict(**event) + to_return.append(e) return to_return - def _one_or_more(self, value): - """Returns a list/tuple of one or more items, regardless of input.""" - return value if isinstance(value, (tuple, list)) else (value,) + def get_event(self, event: MISPEvent | int | str | UUID, + deleted: bool | int | list[int] = False, + extended: bool | int = False, + pythonify: bool = False) -> dict[str, Any] | MISPEvent: + """Get an event from a MISP instance. Includes collections like + Attribute, EventReport, Feed, Galaxy, Object, Tag, etc. so the + response size may be large : https://www.misp-project.org/openapi/#tag/Events/operation/getEventById - def _make_mispevent(self, event): - """Transform a Json MISP event into a MISPEvent""" - if not isinstance(event, MISPEvent): - e = MISPEvent(self.describe_types) - e.load(copy.copy(event)) + :param event: event to get + :param deleted: whether to include soft-deleted attributes + :param extended: whether to get extended events + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + data = {} + if deleted: + data['deleted'] = deleted + if extended: + data['extended'] = extended + if data: + r = self._prepare_request('POST', f'events/view/{event_id}', data=data) else: - e = event + r = self._prepare_request('GET', f'events/view/{event_id}') + event_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in event_r: + return event_r + e = MISPEvent() + e.load(event_r) return e - def _prepare_full_event(self, distribution, threat_level_id, analysis, info, date=None, published=False, orgc_id=None, org_id=None, sharing_group_id=None): - """Initialize a new MISPEvent from scratch""" - misp_event = MISPEvent(self.describe_types) - misp_event.from_dict(info=info, distribution=distribution, threat_level_id=threat_level_id, - analysis=analysis, date=date, orgc_id=orgc_id, org_id=org_id, sharing_group_id=sharing_group_id) - if published: - misp_event.publish() - return misp_event + def event_exists(self, event: MISPEvent | int | str | UUID) -> bool: + """Fast check if event exists. - def _prepare_full_attribute(self, category, type_value, value, to_ids, comment=None, distribution=None, **kwargs): - """Initialize a new MISPAttribute from scratch""" - misp_attribute = MISPAttribute(self.describe_types) - misp_attribute.from_dict(type=type_value, value=value, category=category, - to_ids=to_ids, comment=comment, distribution=distribution, **kwargs) - return misp_attribute - - def _valid_uuid(self, uuid): - """Test if uuid is valid - Will test against CakeText's RFC 4122, i.e - "the third group must start with a 4, - and the fourth group must start with 8, 9, a or b." - - :param uuid: an uuid + :param event: Event to check """ - regex = re.compile(r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', re.I) - match = regex.match(uuid) - return bool(match) + event_id = get_uuid_or_id_from_abstract_misp(event) + r = self._prepare_request('HEAD', f'events/view/{event_id}') + return self._check_head_response(r) - # ################################################ - # ############### Simple REST API ################ - # ################################################ + def add_event(self, event: MISPEvent, pythonify: bool = False, metadata: bool = False) -> dict[str, Any] | MISPEvent: + """Add a new event on a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/addEvent - def test_connection(self): - """Test the auth key""" - response = self.get_version() - if response.get('errors'): - raise PyMISPError(response.get('errors')[0]) - return True - - def get_index(self, filters=None): - """Return the index. - - Warning, there's a limit on the number of results + :param event: event to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param metadata: Return just event metadata after successful creating """ - url = urljoin(self.root_url, 'events/index') - if filters is None: - response = self._prepare_request('GET', url) - else: - response = self._prepare_request('POST', url, json.dumps(filters)) - return self._check_response(response) + r = self._prepare_request('POST', 'events/add' + ('/metadata:1' if metadata else ''), data=event) + new_event = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_event: + return new_event + e = MISPEvent() + e.load(new_event) + return e - def get_event(self, event_id): - """Get an event + def update_event(self, event: MISPEvent, event_id: int | None = None, pythonify: bool = False, + metadata: bool = False) -> dict[str, Any] | MISPEvent: + """Update an event on a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/editEvent - :param event_id: Event id to get + :param event: event to update + :param event_id: ID of event to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param metadata: Return just event metadata after successful update """ - url = urljoin(self.root_url, 'events/{}'.format(event_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_object(self, obj_id): - """Get an object - - :param obj_id: Object id to get - """ - url = urljoin(self.root_url, 'objects/view/{}'.format(obj_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_attribute(self, att_id): - """Get an attribute - - :param att_id: Attribute id to get - """ - url = urljoin(self.root_url, 'attributes/view/{}'.format(att_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def add_event(self, event): - """Add a new event - - :param event: Event as JSON object / string to add - """ - url = urljoin(self.root_url, 'events') - if isinstance(event, MISPEvent): - event = event.to_json() - elif not isinstance(event, basestring): - event = json.dumps(event) - response = self._prepare_request('POST', url, event) - return self._check_response(response) - - def update_attribute(self, attribute_id, attribute): - """Update an attribute - - :param attribute_id: Attribute id/uuid to update - :param attribute: Attribute as JSON object / string to add - """ - url = urljoin(self.root_url, 'attributes/{}'.format(attribute_id)) - if isinstance(attribute, MISPAttribute): - attribute = attribute.to_json() - elif not isinstance(attribute, basestring): - attribute = json.dumps(attribute) - response = self._prepare_request('POST', url, attribute) - return self._check_response(response) - - def update_event(self, event_id, event): - """Update an event - - :param event_id: Event id to update - :param event: Event as JSON object / string to add - """ - url = urljoin(self.root_url, 'events/{}'.format(event_id)) - if isinstance(event, MISPEvent): - event = event.to_json() - elif not isinstance(event, basestring): - event = json.dumps(event) - response = self._prepare_request('POST', url, event) - return self._check_response(response) - - def delete_event(self, event_id): - """Delete an event - - :param event_id: Event id to delete - """ - url = urljoin(self.root_url, 'events/{}'.format(event_id)) - response = self._prepare_request('DELETE', url) - return self._check_response(response) - - def delete_attribute(self, attribute_id, hard_delete=False): - """Delete an attribute by ID""" - if hard_delete: - url = urljoin(self.root_url, 'attributes/delete/{}/1'.format(attribute_id)) - else: - url = urljoin(self.root_url, 'attributes/delete/{}'.format(attribute_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def pushEventToZMQ(self, event_id): - """Force push an event on ZMQ""" - url = urljoin(self.root_url, 'events/pushEventToZMQ/{}.json'.format(event_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def direct_call(self, url, data=None): - '''Very lightweight call that posts a data blob (python dictionary or json string) on the URL''' - url = urljoin(self.root_url, url) - if not data: - response = self._prepare_request('GET', url) - else: - if isinstance(data, dict): - data = json.dumps(data) - response = self._prepare_request('POST', url, data) - return self._check_response(response) - - # ############################################## - # ############### Event handling ############### - # ############################################## - - def get(self, eid): - """Get an event by event ID""" - return self.get_event(eid) - - def update(self, event): - """Update an event by ID""" - e = self._make_mispevent(event) - if e.uuid: - eid = e.uuid - else: - eid = e.id - return self.update_event(eid, e) - - def fast_publish(self, event_id, alert=False): - """Does the same as the publish method, but just try to publish the event - even with one single HTTP GET. - The default is to not send a mail as it is assumed this method is called on update. - """ - if not alert: - url = urljoin(self.root_url, 'events/publish/{}'.format(event_id)) - else: - url = urljoin(self.root_url, 'events/alert/{}'.format(event_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def publish(self, event, alert=True): - """Publish event (with or without alert email) - :param event: pass event or event id (as string or int) to publish - :param alert: set to True by default (send alerting email) if False will not send alert - :return publish status - """ - if isinstance(event, int) or (isinstance(event, basestring) and event.isdigit()): - event_id = event - else: - full_event = self._make_mispevent(event) - if full_event.published: - return {'error': 'Already published'} - event_id = full_event.id - return self.fast_publish(event_id, alert) - - def change_threat_level(self, event, threat_level_id): - """Change the threat level of an event""" - e = self._make_mispevent(event) - e.threat_level_id = threat_level_id - return self.update(e) - - def change_analysis_status(self, event, analysis_status): - """Change the analysis status of an event""" - e = self._make_mispevent(event) - e.analysis = analysis_status - return self.update(e) - - def change_distribution(self, event, distribution): - """Change the distribution of an event""" - e = self._make_mispevent(event) - e.distribution = distribution - return self.update(e) - - def change_sharing_group(self, event, sharing_group_id): - """Change the sharing group of an event""" - e = self._make_mispevent(event) - e.distribution = 4 # Needs to be 'Sharing group' - e.sharing_group_id = sharing_group_id - return self.update(e) - - def new_event(self, distribution=None, threat_level_id=None, analysis=None, info=None, date=None, published=False, orgc_id=None, org_id=None, sharing_group_id=None): - """Create and add a new event""" - misp_event = self._prepare_full_event(distribution, threat_level_id, analysis, info, date, published, orgc_id, org_id, sharing_group_id) - return self.add_event(misp_event) - - def tag(self, uuid, tag): - """Tag an event or an attribute""" - if not self._valid_uuid(uuid): - raise PyMISPError('Invalid UUID') - url = urljoin(self.root_url, 'tags/attachTagToObject') - to_post = {'uuid': uuid, 'tag': tag} - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) - - def untag(self, uuid, tag): - """Untag an event or an attribute""" - if not self._valid_uuid(uuid): - raise PyMISPError('Invalid UUID') - url = urljoin(self.root_url, 'tags/removeTagFromObject') - to_post = {'uuid': uuid, 'tag': tag} - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) - - # ##### File attributes ##### - def _send_attributes(self, event, attributes, proposal=False): - """ - Helper to add new attributes to an existing event, identified by an event object or an event id - - - :param event: EventID (int) or Event to alter - :param attributes: One or more attribute to add - :param proposal: True or False based on whether the attributes should be proposed or directly save - :type event: MISPEvent, int - :type attributes: MISPAttribute, list - :type proposal: bool - :return: list of responses - :rtype: list - """ - event_id = self._extract_event_id(event) - responses = [] - if not event_id: - raise PyMISPError("Unable to find the ID of the event to update.") - if not attributes: - return [{'error': 'No attributes.'}] - - # Propals need to be posted in single requests - if proposal: - for a in attributes: - # proposal_add(...) returns a dict - responses.append(self.proposal_add(event_id, a)) - else: - url = urljoin(self.root_url, 'attributes/add/{}'.format(event_id)) - if isinstance(attributes, list): - if all(isinstance(a, AbstractMISP) for a in attributes): - data = attributes - else: - values = [] - for a in attributes: - values.append(a['value']) - attributes[0]['value'] = values - data = attributes[0].to_json() - else: - data = attributes.to_json() - # _prepare_request(...) returns a requests.Response Object - resp = self._prepare_request('POST', url, json.dumps(data, cls=MISPEncode)) - try: - responses.append(resp.json()) - except Exception: - # The response isn't a json object, appending the text. - responses.append(resp.text) - return responses - - def _extract_event_id(self, event): - """ - Extracts the eventId from a given MISPEvent - - :param event: MISPEvent to extract the id from - :type event: MISPEvent - :return: EventId - :rtype: int - """ - event_id = None - if isinstance(event, MISPEvent): - if hasattr(event, 'id'): - event_id = event.id - elif hasattr(event, 'uuid'): - event_id = event.uuid - elif isinstance(event, int) or (isinstance(event, str) and (event.isdigit() or self._valid_uuid(event))): - event_id = event - else: - if 'Event' in event: - e = event['Event'] - else: - e = event - if 'id' in e: - event_id = e['id'] - elif 'uuid' in e: - event_id = e['uuid'] - return event_id - - def add_named_attribute(self, event, type_value, value, category=None, to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add one or more attributes to an existing event""" - attributes = [] - for value in self._one_or_more(value): - attributes.append(self._prepare_full_attribute(category, type_value, value, to_ids, comment, distribution, **kwargs)) - return self._send_attributes(event, attributes, proposal) - - def add_hashes(self, event, category='Artifacts dropped', filename=None, md5=None, sha1=None, sha256=None, ssdeep=None, comment=None, to_ids=True, distribution=None, proposal=False, **kwargs): - """Add hashe(s) to an existing event""" - - attributes = [] - type_value = '{}' - value = '' - if filename: - type_value = 'filename|{}' - value = filename + '|' - if md5: - attributes.append(self._prepare_full_attribute(category, type_value.format('md5'), value + md5, to_ids, comment, distribution)) - if sha1: - attributes.append(self._prepare_full_attribute(category, type_value.format('sha1'), value + sha1, to_ids, comment, distribution)) - if sha256: - attributes.append(self._prepare_full_attribute(category, type_value.format('sha256'), value + sha256, to_ids, comment, distribution)) - if ssdeep: - attributes.append(self._prepare_full_attribute(category, type_value.format('ssdeep'), value + ssdeep, to_ids, comment, distribution)) - - return self._send_attributes(event, attributes, proposal) - - def av_detection_link(self, event, link, category='Antivirus detection', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add AV detection link(s)""" - return self.add_named_attribute(event, 'link', link, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_detection_name(self, event, name, category='Antivirus detection', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add AV detection name(s)""" - return self.add_named_attribute(event, 'text', name, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_filename(self, event, filename, category='Artifacts dropped', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add filename(s)""" - return self.add_named_attribute(event, 'filename', filename, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_attachment(self, event, attachment, category='Artifacts dropped', to_ids=False, comment=None, distribution=None, proposal=False, filename=None, **kwargs): - """Add an attachment to the MISP event - - :param event: The event to add an attachment to - :param attachment: Either a file handle or a path to a file - will be uploaded - :param filename: Explicitly defined attachment filename - """ - if isinstance(attachment, basestring) and os.path.isfile(attachment): - # We have a file to open - if filename is None: - filename = os.path.basename(attachment) - with open(attachment, "rb") as f: - fileData = f.read() - elif hasattr(attachment, "read"): - # It's a file handle - we can read it but it has no filename - fileData = attachment.read() - if filename is None: - filename = 'attachment' - elif isinstance(attachment, (tuple, list)): - # tuple/list (filename, pseudofile) - if filename is None: - filename = attachment[0] - if hasattr(attachment[1], "read"): - # Pseudo file - fileData = attachment[1].read() - else: - fileData = attachment[1] - else: - # Plain file content, no filename - if filename is None: - filename = 'attachment' - fileData = attachment - - if not isinstance(fileData, bytes): - fileData = fileData.encode() - - # by now we have a string for the file - # we just need to b64 encode it and send it on its way - # also, just decode it to utf-8 to avoid the b'string' format - encodedData = base64.b64encode(fileData).decode("utf-8") - - # Send it on its way - return self.add_named_attribute(event, 'attachment', filename, category, to_ids, comment, distribution, proposal, data=encodedData, **kwargs) - - def add_regkey(self, event, regkey, rvalue=None, category='Artifacts dropped', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a registry key""" - if rvalue: - type_value = 'regkey|value' - value = '{}|{}'.format(regkey, rvalue) - else: - type_value = 'regkey' - value = regkey - - attributes = [] - attributes.append(self._prepare_full_attribute(category, type_value, value, to_ids, comment, distribution)) - return self._send_attributes(event, attributes, proposal) - - def add_regkeys(self, event, regkeys_values, category='Artifacts dropped', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a registry keys""" - attributes = [] - - for regkey, rvalue in regkeys_values.items(): - if rvalue is not None: - type_value = 'regkey|value' - value = '{}|{}'.format(regkey, rvalue) - else: - type_value = 'regkey' - value = regkey - - attributes.append(self._prepare_full_attribute(category, type_value, value, to_ids, comment, distribution)) - return self._send_attributes(event, attributes, proposal) - - def add_pattern(self, event, pattern, in_file=True, in_memory=False, category='Artifacts dropped', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a pattern(s) in file or in memory""" - if not (in_file or in_memory): - raise PyMISPError('Invalid pattern type: please use in_memory=True or in_file=True') - itemtype = 'pattern-in-file' if in_file else 'pattern-in-memory' - return self.add_named_attribute(event, itemtype, pattern, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_pipe(self, event, named_pipe, category='Artifacts dropped', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add pipes(s)""" - def scrub(s): - if not s.startswith('\\.\\pipe\\'): - s = '\\.\\pipe\\{}'.format(s) - return s - attributes = list(map(scrub, self._one_or_more(named_pipe))) - return self.add_named_attribute(event, 'named pipe', attributes, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_mutex(self, event, mutex, category='Artifacts dropped', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add mutex(es)""" - def scrub(s): - if not s.startswith('\\BaseNamedObjects\\'): - s = '\\BaseNamedObjects\\{}'.format(s) - return s - attributes = list(map(scrub, self._one_or_more(mutex))) - return self.add_named_attribute(event, 'mutex', attributes, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_yara(self, event, yara, category='Payload delivery', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add yara rule(es)""" - return self.add_named_attribute(event, 'yara', yara, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Network attributes ##### - - def add_ipdst(self, event, ipdst, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add destination IP(s)""" - return self.add_named_attribute(event, 'ip-dst', ipdst, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_ipsrc(self, event, ipsrc, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add source IP(s)""" - return self.add_named_attribute(event, 'ip-src', ipsrc, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_hostname(self, event, hostname, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add hostname(s)""" - return self.add_named_attribute(event, 'hostname', hostname, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_domain(self, event, domain, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add domain(s)""" - return self.add_named_attribute(event, 'domain', domain, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_domain_ip(self, event, domain, ip, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add domain|ip""" - if isinstance(ip, str): - ip = [ip] - composed = list(map(lambda x: '%s|%s' % (domain, x), ip)) - return self.add_named_attribute(event, 'domain|ip', composed, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_domains_ips(self, event, domain_ips, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add multiple domain|ip""" - composed = list(map(lambda x: '%s|%s' % (x[0], x[1]), domain_ips.items())) - return self.add_named_attribute(event, 'domain|ip', composed, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_url(self, event, url, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add url(s)""" - return self.add_named_attribute(event, 'url', url, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_useragent(self, event, useragent, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add user agent(s)""" - return self.add_named_attribute(event, 'user-agent', useragent, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_traffic_pattern(self, event, pattern, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add pattern(s) in traffic""" - return self.add_named_attribute(event, 'pattern-in-traffic', pattern, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_snort(self, event, snort, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add SNORT rule(s)""" - return self.add_named_attribute(event, 'snort', snort, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_asn(self, event, asn, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add network ASN""" - return self.add_named_attribute(event, 'AS', asn, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_net_other(self, event, netother, category='Network activity', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a free text entry""" - return self.add_named_attribute(event, 'other', netother, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Email attributes ##### - - def add_email_src(self, event, email, category='Payload delivery', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a source email""" - return self.add_named_attribute(event, 'email-src', email, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_email_dst(self, event, email, category='Payload delivery', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add a destination email""" - return self.add_named_attribute(event, 'email-dst', email, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_email_subject(self, event, email, category='Payload delivery', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an email subject""" - return self.add_named_attribute(event, 'email-subject', email, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_email_attachment(self, event, email, category='Payload delivery', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an email atachment""" - return self.add_named_attribute(event, 'email-attachment', email, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_email_header(self, event, email, category='Payload delivery', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an email header""" - return self.add_named_attribute(event, 'email-header', email, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Target attributes ##### - - def add_target_email(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target email""" - return self.add_named_attribute(event, 'target-email', target, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_target_user(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target user""" - return self.add_named_attribute(event, 'target-user', target, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_target_machine(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target machine""" - return self.add_named_attribute(event, 'target-machine', target, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_target_org(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target organisation""" - return self.add_named_attribute(event, 'target-org', target, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_target_location(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target location""" - return self.add_named_attribute(event, 'target-location', target, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_target_external(self, event, target, category='Targeting data', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an target external""" - return self.add_named_attribute(event, 'target-external', target, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Attribution attributes ##### - - def add_threat_actor(self, event, target, category='Attribution', to_ids=True, comment=None, distribution=None, proposal=False, **kwargs): - """Add an threat actor""" - return self.add_named_attribute(event, 'threat-actor', target, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Internal reference attributes ##### - - def add_internal_link(self, event, reference, category='Internal reference', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add an internal link""" - return self.add_named_attribute(event, 'link', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_internal_comment(self, event, reference, category='Internal reference', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add an internal comment""" - return self.add_named_attribute(event, 'comment', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_internal_text(self, event, reference, category='Internal reference', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add an internal text""" - return self.add_named_attribute(event, 'text', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_internal_other(self, event, reference, category='Internal reference', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add an internal reference (type other)""" - return self.add_named_attribute(event, 'other', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - # ##### Other attributes ##### - - def add_other_comment(self, event, reference, category='Other', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add other comment""" - return self.add_named_attribute(event, 'comment', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_other_counter(self, event, reference, category='Other', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add other counter""" - return self.add_named_attribute(event, 'counter', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - def add_other_text(self, event, reference, category='Other', to_ids=False, comment=None, distribution=None, proposal=False, **kwargs): - """Add other text""" - return self.add_named_attribute(event, 'text', reference, category, to_ids, comment, distribution, proposal, **kwargs) - - # ################################################## - # ######### Upload samples through the API ######### - # ################################################## - - def _prepare_upload(self, event_id, distribution, to_ids, category, comment, info, - analysis, threat_level_id, advanced_extraction): - """Helper to prepare a sample to upload""" - to_post = {'request': {}} - - if event_id is not None: - try: - event_id = int(event_id) - except ValueError: - pass - if not isinstance(event_id, int): - # New event - misp_event = self._prepare_full_event(distribution, threat_level_id, analysis, info) - to_post['request']['distribution'] = misp_event.distribution - to_post['request']['info'] = misp_event.info - to_post['request']['analysis'] = misp_event.analysis - to_post['request']['threat_level_id'] = misp_event.threat_level_id - else: - if distribution is not None: - to_post['request']['distribution'] = distribution - - default_values = self.sane_default['malware-sample'] - if to_ids is None or not isinstance(to_ids, bool): - to_ids = bool(int(default_values['to_ids'])) - to_post['request']['to_ids'] = to_ids - - if category is None or category not in self.categories: - category = default_values['default_category'] - to_post['request']['category'] = category - - to_post['request']['comment'] = comment - to_post['request']['advanced'] = 1 if advanced_extraction else 0 - return to_post, event_id - - def _encode_file_to_upload(self, filepath_or_bytes): - """Helper to encode a file to upload""" - if isinstance(filepath_or_bytes, basestring): - if os.path.isfile(filepath_or_bytes): - with open(filepath_or_bytes, 'rb') as f: - binblob = f.read() - else: - binblob = filepath_or_bytes.encode() - else: - binblob = filepath_or_bytes - return base64.b64encode(binblob).decode() - - def upload_sample(self, filename, filepath_or_bytes, event_id, distribution=None, - to_ids=True, category=None, comment=None, info=None, - analysis=None, threat_level_id=None, advanced_extraction=False): - """Upload a sample""" - to_post, event_id = self._prepare_upload(event_id, distribution, to_ids, category, - comment, info, analysis, threat_level_id, - advanced_extraction) - to_post['request']['files'] = [{'filename': filename, 'data': self._encode_file_to_upload(filepath_or_bytes)}] - return self._upload_sample(to_post, event_id) - - def upload_samplelist(self, filepaths, event_id, distribution=None, - to_ids=True, category=None, comment=None, info=None, - analysis=None, threat_level_id=None, advanced_extraction=False): - """Upload a list of samples""" - to_post, event_id = self._prepare_upload(event_id, distribution, to_ids, category, - comment, info, analysis, threat_level_id, - advanced_extraction) - files = [] - for path in filepaths: - if not os.path.isfile(path): - continue - files.append({'filename': os.path.basename(path), 'data': self._encode_file_to_upload(path)}) - to_post['request']['files'] = files - return self._upload_sample(to_post, event_id) - - def _upload_sample(self, to_post, event_id=None): - """Helper to upload a sample""" if event_id is None: - url = urljoin(self.root_url, 'events/upload_sample') + eid = get_uuid_or_id_from_abstract_misp(event) else: - url = urljoin(self.root_url, 'events/upload_sample/{}'.format(event_id)) - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) + eid = get_uuid_or_id_from_abstract_misp(event_id) + r = self._prepare_request('POST', f'events/edit/{eid}' + ('/metadata:1' if metadata else ''), data=event) + updated_event = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_event: + return updated_event + e = MISPEvent() + e.load(updated_event) + return e - # ############################ - # ######## Proposals ######### - # ############################ + def delete_event(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an event from a MISP instance: https://www.misp-project.org/openapi/#tag/Events/operation/deleteEvent - def __query_proposal(self, path, id, attribute=None): - """Helper to prepare a query to handle proposals""" - url = urljoin(self.root_url, 'shadow_attributes/{}/{}'.format(path, id)) - if path in ['add', 'edit']: - query = {'request': {'ShadowAttribute': attribute}} - response = self._prepare_request('POST', url, json.dumps(query, cls=MISPEncode)) - elif path == 'view': - response = self._prepare_request('GET', url) - else: # accept or discard - response = self._prepare_request('POST', url) - return self._check_response(response) + :param event: event to delete + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + response = self._prepare_request('POST', f'events/delete/{event_id}') + return self._check_json_response(response) - def proposal_view(self, event_id=None, proposal_id=None): - """View a proposal""" - if proposal_id is not None and event_id is not None: - return {'error': 'You can only view an event ID or a proposal ID'} - if event_id is not None: - id = event_id + def publish(self, event: MISPEvent | int | str | UUID, alert: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Publish the event with one single HTTP POST: https://www.misp-project.org/openapi/#tag/Events/operation/publishEvent + + :param event: event to publish + :param alert: whether to send an email. The default is to not send a mail as it is assumed this method is called on update. + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + if alert: + response = self._prepare_request('POST', f'events/alert/{event_id}') else: - id = proposal_id - return self.__query_proposal('view', id) + response = self._prepare_request('POST', f'events/publish/{event_id}') + return self._check_json_response(response) - def proposal_add(self, event_id, attribute): - """Add a proposal""" - return self.__query_proposal('add', event_id, attribute) + def unpublish(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Unpublish the event with one single HTTP POST: https://www.misp-project.org/openapi/#tag/Events/operation/unpublishEvent - def proposal_edit(self, attribute_id, attribute): - """Edit a proposal""" - return self.__query_proposal('edit', attribute_id, attribute) + :param event: event to unpublish + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + response = self._prepare_request('POST', f'events/unpublish/{event_id}') + return self._check_json_response(response) - def proposal_accept(self, proposal_id): - """Accept a proposal""" - return self.__query_proposal('accept', proposal_id) + def contact_event_reporter(self, event: MISPEvent | int | str | UUID, message: str) -> dict[str, Any] | list[dict[str, Any]]: + """Send a message to the reporter of an event - def proposal_discard(self, proposal_id): - """Discard a proposal""" - return self.__query_proposal('discard', proposal_id) + :param event: event with reporter to contact + :param message: message to send + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + to_post = {'message': message} + response = self._prepare_request('POST', f'events/contact/{event_id}', data=to_post) + return self._check_json_response(response) - # ############################## - # ###### Attribute update ###### - # ############################## + # ## END Event ### - def change_toids(self, attribute_uuid, to_ids): - """Change the toids flag""" - if to_ids not in [0, 1]: - raise Exception('to_ids can only be 0 or 1') - query = {"to_ids": to_ids} - return self.__query('edit/{}'.format(attribute_uuid), query, controller='attributes') + # ## BEGIN Event Report ### - def change_comment(self, attribute_uuid, comment): - """Change the comment of attribute""" - query = {"comment": comment} - return self.__query('edit/{}'.format(attribute_uuid), query, controller='attributes') + def get_event_report(self, event_report: MISPEventReport | int | str | UUID, + pythonify: bool = False) -> dict[str, Any] | MISPEventReport: + """Get an event report from a MISP instance - def change_disable_correlation(self, attribute_uuid, disable_correlation): - """Change the disable_correlation flag""" - possible_values = [0, 1, False, True] - if disable_correlation not in possible_values: - raise Exception('disable_correlation can only be in {}'.format(', '.join(possible_values))) - query = {"disable_correlation": disable_correlation} - return self.__query('edit/{}'.format(attribute_uuid), query, controller='attributes') + :param event_report: event report to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + event_report_id = get_uuid_or_id_from_abstract_misp(event_report) + r = self._prepare_request('GET', f'eventReports/view/{event_report_id}') + event_report_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in event_report_r: + return event_report_r + er = MISPEventReport() + er.from_dict(**event_report_r) + return er - # ############################## - # ###### Attribute update ###### - # ############################## + def get_event_reports(self, event_id: int | str, + pythonify: bool = False) -> dict[str, Any] | list[MISPEventReport] | list[dict[str, Any]]: + """Get event report from a MISP instance that are attached to an event ID - def freetext(self, event_id, string, adhereToWarninglists=False, distribution=None, returnMetaAttributes=False): - """Pass a text to the freetext importer""" - query = {"value": string} + :param event_id: event id to get the event reports for + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. + """ + r = self._prepare_request('GET', f'eventReports/index/event_id:{event_id}') + event_reports = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(event_reports, dict): + return event_reports + to_return = [] + for event_report in event_reports: + er = MISPEventReport() + er.from_dict(**event_report) + to_return.append(er) + return to_return + + def add_event_report(self, event: MISPEvent | int | str | UUID, event_report: MISPEventReport, pythonify: bool = False) -> dict[str, Any] | MISPEventReport: + """Add an event report to an existing MISP event + + :param event: event to extend + :param event_report: event report to add. + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + r = self._prepare_request('POST', f'eventReports/add/{event_id}', data=event_report) + new_event_report = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_event_report: + return new_event_report + er = MISPEventReport() + er.from_dict(**new_event_report) + return er + + def update_event_report(self, event_report: MISPEventReport, event_report_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPEventReport: + """Update an event report on a MISP instance + + :param event_report: event report to update + :param event_report_id: event report ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if event_report_id is None: + erid = get_uuid_or_id_from_abstract_misp(event_report) + else: + erid = get_uuid_or_id_from_abstract_misp(event_report_id) + r = self._prepare_request('POST', f'eventReports/edit/{erid}', data=event_report) + updated_event_report = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_event_report: + return updated_event_report + er = MISPEventReport() + er.from_dict(**updated_event_report) + return er + + def delete_event_report(self, event_report: MISPEventReport | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an event report from a MISP instance + + :param event_report: event report to delete + :param hard: flag for hard delete + """ + event_report_id = get_uuid_or_id_from_abstract_misp(event_report) + request_url = f'eventReports/delete/{event_report_id}' + data = {} + if hard: + data['hard'] = 1 + r = self._prepare_request('POST', request_url, data=data) + return self._check_json_response(r) + # ## END Event Report ### + + # ## BEGIN Galaxy Cluster ### + def attach_galaxy_cluster(self, misp_entity: MISPEvent | MISPAttribute, galaxy_cluster: MISPGalaxyCluster | int | str, local: bool = False, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Attach a galaxy cluster to an event or an attribute + + :param misp_entity: a MISP Event or a MISP Attribute + :param galaxy_cluster: Galaxy cluster to attach + :param local: whether the object should be attached locally or not to the target + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if isinstance(misp_entity, MISPEvent): + attach_target_type = 'event' + elif isinstance(misp_entity, MISPAttribute): + attach_target_type = 'attribute' + else: + raise PyMISPError('The misp_entity must be MISPEvent or MISPAttribute') + + attach_target_id = misp_entity.id + local = 1 if local else 0 + + if isinstance(galaxy_cluster, MISPGalaxyCluster): + cluster_id = galaxy_cluster.id + elif isinstance(galaxy_cluster, (int, str)): + cluster_id = galaxy_cluster + else: + raise PyMISPError('The galaxy_cluster must be MISPGalaxyCluster or the id associated with the cluster (int or str)') + + to_post = {'Galaxy': {'target_id': cluster_id}} + url = f'galaxies/attachCluster/{attach_target_id}/{attach_target_type}/local:{local}' + + r = self._prepare_request('POST', url, data=to_post) + return self._check_json_response(r) + # ## END Galaxy Cluster ### + + # ## BEGIN Analyst Data ###a + def get_analyst_data(self, analyst_data: AnalystDataBehaviorMixin | int | str | UUID, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Get an analyst data from a MISP instance + + :param analyst_data: analyst data to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + r = self._prepare_request('GET', f'analyst_data/view/{analyst_data_type}/{analyst_data_id}') + analyst_data_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in analyst_data_r or analyst_data_type == 'all': + return analyst_data_r + er = type(analyst_data)() + er.from_dict(**analyst_data_r) + return er + + def add_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Add an analyst data to an existing MISP element + + :param analyst_data: analyst_data to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + object_uuid = analyst_data.object_uuid + object_type = analyst_data.object_type + r = self._prepare_request('POST', f'analyst_data/add/{analyst_data.analyst_data_object_type}/{object_uuid}/{object_type}', data=analyst_data) + new_analyst_data = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_analyst_data: + return new_analyst_data + er = type(analyst_data)() + er.from_dict(**new_analyst_data) + return er + + def update_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship, analyst_data_id: int | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPNote | MISPOpinion | MISPRelationship: + """Update an analyst data on a MISP instance + + :param analyst_data: analyst data to update + :param analyst_data_id: analyst data ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + if analyst_data_id is None: + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + r = self._prepare_request('POST', f'analyst_data/edit/{analyst_data_type}/{analyst_data_id}', data=analyst_data) + updated_analyst_data = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_analyst_data or analyst_data_type == 'all': + return updated_analyst_data + er = type(analyst_data)() + er.from_dict(**updated_analyst_data) + return er + + def delete_analyst_data(self, analyst_data: MISPNote | MISPOpinion | MISPRelationship | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an analyst data from a MISP instance + + :param analyst_data: analyst data to delete + """ + if isinstance(analyst_data, AnalystDataBehaviorMixin): + analyst_data_type = analyst_data.analyst_data_object_type + else: + analyst_data_type = 'all' + analyst_data_id = get_uuid_or_id_from_abstract_misp(analyst_data) + request_url = f'analyst_data/delete/{analyst_data_type}/{analyst_data_id}' + r = self._prepare_request('POST', request_url) + return self._check_json_response(r) + + # ## END Analyst Data ### + + # ## BEGIN Analyst Note ### + + def get_note(self, note: MISPNote, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Get a note from a MISP instance + + :param note: note to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(note, pythonify) + + def add_note(self, note: MISPNote, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Add a note to an existing MISP element + + :param note: note to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(note, pythonify) + + def update_note(self, note: MISPNote, note_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPNote: + """Update a note on a MISP instance + + :param note: note to update + :param note_id: note ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(note, note_id, pythonify) + + def delete_note(self, note: MISPNote | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a note from a MISP instance + + :param note: note delete + """ + return self.delete_analyst_data(note) + + # ## END Analyst Note ### + + # ## BEGIN Analyst Opinion ### + + def get_opinion(self, opinion: MISPOpinion, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Get an opinion from a MISP instance + + :param opinion: opinion to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(opinion, pythonify) + + def add_opinion(self, opinion: MISPOpinion, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Add an opinion to an existing MISP element + + :param opinion: opinion to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(opinion, pythonify) + + def update_opinion(self, opinion: MISPOpinion, opinion_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOpinion: + """Update an opinion on a MISP instance + + :param opinion: opinion to update + :param opinion_id: opinion ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(opinion, opinion_id, pythonify) + + def delete_opinion(self, opinion: MISPOpinion | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an opinion from a MISP instance + + :param opinion: opinion to delete + """ + return self.delete_analyst_data(opinion) + + # ## END Analyst Opinion ### + + # ## BEGIN Analyst Relationship ### + + def get_relationship(self, relationship: MISPRelationship, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Get a relationship from a MISP instance + + :param relationship: relationship to get + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + return self.get_analyst_data(relationship, pythonify) + + def add_relationship(self, relationship: MISPRelationship, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Add a relationship to an existing MISP element + + :param relationship: relationship to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.add_analyst_data(relationship, pythonify) + + def update_relationship(self, relationship: MISPRelationship, relationship_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPRelationship: + """Update a relationship on a MISP instance + + :param relationship: relationship to update + :param relationship_id: relationship ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + return self.update_analyst_data(relationship, relationship_id, pythonify) + + def delete_relationship(self, relationship: MISPRelationship | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a relationship from a MISP instance + + :param relationship: relationship to delete + """ + return self.delete_analyst_data(relationship) + + # ## END Analyst Relationship ### + + # ## BEGIN Object ### + + def get_object(self, misp_object: MISPObject | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPObject: + """Get an object from the remote MISP instance: https://www.misp-project.org/openapi/#tag/Objects/operation/getObjectById + + :param misp_object: object to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + object_id = get_uuid_or_id_from_abstract_misp(misp_object) + r = self._prepare_request('GET', f'objects/view/{object_id}') + misp_object_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in misp_object_r: + return misp_object_r + o = MISPObject(misp_object_r['Object']['name'], standalone=False) + o.from_dict(**misp_object_r) + return o + + def object_exists(self, misp_object: MISPObject | int | str | UUID) -> bool: + """Fast check if object exists. + + :param misp_object: Attribute to check + """ + object_id = get_uuid_or_id_from_abstract_misp(misp_object) + r = self._prepare_request('HEAD', f'objects/view/{object_id}') + return self._check_head_response(r) + + def add_object(self, event: MISPEvent | int | str | UUID, misp_object: MISPObject, pythonify: bool = False, break_on_duplicate: bool = False) -> dict[str, Any] | MISPObject: + """Add a MISP Object to an existing MISP event: https://www.misp-project.org/openapi/#tag/Objects/operation/addObject + + :param event: event to extend + :param misp_object: object to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param break_on_duplicate: if True, check and reject if this object's attributes match an existing object's attributes; may require much time + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + params = {'breakOnDuplicate': 1} if break_on_duplicate else {} + r = self._prepare_request('POST', f'objects/add/{event_id}', data=misp_object, kw_params=params) + new_object = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_object: + return new_object + o = MISPObject(new_object['Object']['name'], standalone=False) + o.from_dict(**new_object) + return o + + def update_object(self, misp_object: MISPObject, object_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPObject: + """Update an object on a MISP instance + + :param misp_object: object to update + :param object_id: ID of object to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if object_id is None: + oid = get_uuid_or_id_from_abstract_misp(misp_object) + else: + oid = get_uuid_or_id_from_abstract_misp(object_id) + r = self._prepare_request('POST', f'objects/edit/{oid}', data=misp_object) + updated_object = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_object: + return updated_object + o = MISPObject(updated_object['Object']['name'], standalone=False) + o.from_dict(**updated_object) + return o + + def delete_object(self, misp_object: MISPObject | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an object from a MISP instance: https://www.misp-project.org/openapi/#tag/Objects/operation/deleteObject + + :param misp_object: object to delete + :param hard: flag for hard delete + """ + object_id = get_uuid_or_id_from_abstract_misp(misp_object) + data = {} + if hard: + data['hard'] = 1 + r = self._prepare_request('POST', f'objects/delete/{object_id}', data=data) + return self._check_json_response(r) + + def add_object_reference(self, misp_object_reference: MISPObjectReference, pythonify: bool = False) -> dict[str, Any] | MISPObjectReference: + """Add a reference to an object + + :param misp_object_reference: object reference + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'objectReferences/add', misp_object_reference) + object_reference = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in object_reference: + return object_reference + ref = MISPObjectReference() + ref.from_dict(**object_reference) + return ref + + def delete_object_reference( + self, + object_reference: MISPObjectReference | int | str | UUID, + hard: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a reference to an object.""" + object_reference_id = get_uuid_or_id_from_abstract_misp(object_reference) + query_url = f"objectReferences/delete/{object_reference_id}" + if hard: + query_url += "/true" + response = self._prepare_request("POST", query_url) + return self._check_json_response(response) + + # Object templates + + def object_templates(self, pythonify: bool = False) -> dict[str, Any] | list[MISPObjectTemplate] | list[dict[str, Any]]: + """Get all the object templates + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'objectTemplates/index') + templates = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(templates, dict): + return templates + to_return = [] + for object_template in templates: + o = MISPObjectTemplate() + o.from_dict(**object_template) + to_return.append(o) + return to_return + + def get_object_template(self, object_template: MISPObjectTemplate | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPObjectTemplate: + """Gets the full object template + + :param object_template: template or ID to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + object_template_id = get_uuid_or_id_from_abstract_misp(object_template) + r = self._prepare_request('GET', f'objectTemplates/view/{object_template_id}') + object_template_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in object_template_r: + return object_template_r + t = MISPObjectTemplate() + t.from_dict(**object_template_r) + return t + + def get_raw_object_template(self, uuid_or_name: str) -> dict[str, Any] | list[dict[str, Any]]: + """Get a row template. It needs to be present on disk on the MISP instance you're connected to. + The response of this method can be passed to MISPObject(, misp_objects_template_custom=) + """ + r = self._prepare_request('GET', f'objectTemplates/getRaw/{uuid_or_name}') + return self._check_json_response(r) + + def update_object_templates(self) -> dict[str, Any] | list[dict[str, Any]]: + """Trigger an update of the object templates""" + response = self._prepare_request('POST', 'objectTemplates/update') + return self._check_json_response(response) + + # ## END Object ### + + # ## BEGIN Attribute ### + + def attributes(self, pythonify: bool = False) -> dict[str, Any] | list[MISPAttribute] | list[dict[str, Any]]: + """Get all the attributes from the MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/getAttributes + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'attributes/index') + attributes_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(attributes_r, dict): + return attributes_r + to_return = [] + for attribute in attributes_r: + a = MISPAttribute() + a.from_dict(**attribute) + to_return.append(a) + return to_return + + def get_attribute(self, attribute: MISPAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPAttribute: + """Get an attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/getAttributeById + + :param attribute: attribute to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + r = self._prepare_request('GET', f'attributes/view/{attribute_id}') + attribute_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in attribute_r: + return attribute_r + a = MISPAttribute() + a.from_dict(**attribute_r) + return a + + def attribute_exists(self, attribute: MISPAttribute | int | str | UUID) -> bool: + """Fast check if attribute exists. + + :param attribute: Attribute to check + """ + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + r = self._prepare_request('HEAD', f'attributes/view/{attribute_id}') + return self._check_head_response(r) + + def add_attribute(self, event: MISPEvent | int | str | UUID, attribute: MISPAttribute | Iterable[str], pythonify: bool = False, break_on_duplicate: bool = True) -> dict[str, Any] | MISPAttribute | MISPShadowAttribute: + """Add an attribute to an existing MISP event: https://www.misp-project.org/openapi/#tag/Attributes/operation/addAttribute + + :param event: event to extend + :param attribute: attribute or (MISP version 2.4.113+) list of attributes to add. + If a list is passed, the pythonified response is a dict with the following structure: + {'attributes': [MISPAttribute], 'errors': {errors by attributes}} + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param break_on_duplicate: if False, do not fail if the attribute already exists, updates existing attribute instead (timestamp will be always updated) + """ + params = {'breakOnDuplicate': 0} if break_on_duplicate is not True else {} + event_id = get_uuid_or_id_from_abstract_misp(event) + r = self._prepare_request('POST', f'attributes/add/{event_id}', data=attribute, kw_params=params) + new_attribute = self._check_json_response(r) + if isinstance(attribute, list): + # Multiple attributes were passed at once, the handling is totally different + if not (self.global_pythonify or pythonify): + return new_attribute + to_return: dict[str, list[MISPAttribute]] = {'attributes': []} + if 'errors' in new_attribute: + to_return['errors'] = new_attribute['errors'] + + if len(attribute) == 1: + # input list size 1 yields dict, not list of size 1 + if 'Attribute' in new_attribute: + a = MISPAttribute() + a.from_dict(**new_attribute['Attribute']) + to_return['attributes'].append(a) + else: + for new_attr in new_attribute['Attribute']: + a = MISPAttribute() + a.from_dict(**new_attr) + to_return['attributes'].append(a) + return to_return + + if ('errors' in new_attribute and new_attribute['errors'][0] == 403 + and new_attribute['errors'][1]['message'] == 'You do not have permission to do that.'): + # At this point, we assume the user tried to add an attribute on an event they don't own + # Re-try with a proposal + if isinstance(attribute, (MISPAttribute, dict)): + return self.add_attribute_proposal(event_id, attribute, pythonify) # type: ignore + if not (self.global_pythonify or pythonify) or 'errors' in new_attribute: + return new_attribute + a = MISPAttribute() + a.from_dict(**new_attribute) + return a + + def update_attribute(self, attribute: MISPAttribute, attribute_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPAttribute | MISPShadowAttribute: + """Update an attribute on a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/editAttribute + + :param attribute: attribute to update + :param attribute_id: attribute ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if attribute_id is None: + aid = get_uuid_or_id_from_abstract_misp(attribute) + else: + aid = get_uuid_or_id_from_abstract_misp(attribute_id) + r = self._prepare_request('POST', f'attributes/edit/{aid}', data=attribute) + updated_attribute = self._check_json_response(r) + if 'errors' in updated_attribute: + if (updated_attribute['errors'][0] == 403 + and updated_attribute['errors'][1]['message'] == 'You do not have permission to do that.'): + # At this point, we assume the user tried to update an attribute on an event they don't own + # Re-try with a proposal + return self.update_attribute_proposal(aid, attribute, pythonify) + if not (self.global_pythonify or pythonify) or 'errors' in updated_attribute: + return updated_attribute + a = MISPAttribute() + a.from_dict(**updated_attribute) + return a + + def delete_attribute(self, attribute: MISPAttribute | int | str | UUID, hard: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/deleteAttribute + + :param attribute: attribute to delete + :param hard: flag for hard delete + """ + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + data = {} + if hard: + data['hard'] = 1 + r = self._prepare_request('POST', f'attributes/delete/{attribute_id}', data=data) + response = self._check_json_response(r) + if ('errors' in response and response['errors'][0] == 403 + and response['errors'][1]['message'] == 'You do not have permission to do that.'): + # FIXME: https://github.com/MISP/MISP/issues/4913 + # At this point, we assume the user tried to delete an attribute on an event they don't own + # Re-try with a proposal + return self.delete_attribute_proposal(attribute_id) + return response + + def restore_attribute(self, attribute: MISPAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPAttribute: + """Restore a soft deleted attribute from a MISP instance: https://www.misp-project.org/openapi/#tag/Attributes/operation/restoreAttribute + + :param attribute: attribute to restore + """ + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + r = self._prepare_request('POST', f'attributes/restore/{attribute_id}') + response = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in response: + return response + a = MISPAttribute() + a.from_dict(**response) + return a + + # ## END Attribute ### + + # ## BEGIN Attribute Proposal ### + + def attribute_proposals(self, event: MISPEvent | int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPShadowAttribute] | list[dict[str, Any]]: + """Get all the attribute proposals + + :param event: event + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + if event: + event_id = get_uuid_or_id_from_abstract_misp(event) + r = self._prepare_request('GET', f'shadowAttributes/index/{event_id}') + else: + r = self._prepare_request('GET', 'shadowAttributes/index') + attribute_proposals = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(attribute_proposals, dict): + return attribute_proposals + to_return = [] + for attribute_proposal in attribute_proposals: + a = MISPShadowAttribute() + a.from_dict(**attribute_proposal) + to_return.append(a) + return to_return + + def get_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: + """Get an attribute proposal + + :param proposal: proposal to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + proposal_id = get_uuid_or_id_from_abstract_misp(proposal) + r = self._prepare_request('GET', f'shadowAttributes/view/{proposal_id}') + attribute_proposal = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in attribute_proposal: + return attribute_proposal + a = MISPShadowAttribute() + a.from_dict(**attribute_proposal) + return a + + # NOTE: the tree following method have a very specific meaning, look at the comments + + def add_attribute_proposal(self, event: MISPEvent | int | str | UUID, attribute: MISPAttribute, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: + """Propose a new attribute in an event + + :param event: event to receive new attribute + :param attribute: attribute to propose + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + r = self._prepare_request('POST', f'shadowAttributes/add/{event_id}', data=attribute) + new_attribute_proposal = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_attribute_proposal: + return new_attribute_proposal + a = MISPShadowAttribute() + a.from_dict(**new_attribute_proposal) + return a + + def update_attribute_proposal(self, initial_attribute: MISPAttribute | int | str | UUID, attribute: MISPAttribute, pythonify: bool = False) -> dict[str, Any] | MISPShadowAttribute: + """Propose a change for an attribute + + :param initial_attribute: attribute to change + :param attribute: attribute to propose + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + initial_attribute_id = get_uuid_or_id_from_abstract_misp(initial_attribute) + r = self._prepare_request('POST', f'shadowAttributes/edit/{initial_attribute_id}', data=attribute) + update_attribute_proposal = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in update_attribute_proposal: + return update_attribute_proposal + a = MISPShadowAttribute() + a.from_dict(**update_attribute_proposal) + return a + + def delete_attribute_proposal(self, attribute: MISPAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Propose the deletion of an attribute + + :param attribute: attribute to delete + """ + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + response = self._prepare_request('POST', f'shadowAttributes/delete/{attribute_id}') + return self._check_json_response(response) + + def accept_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Accept a proposal. You cannot modify an existing proposal, only accept/discard + + :param proposal: attribute proposal to accept + """ + proposal_id = get_uuid_or_id_from_abstract_misp(proposal) + response = self._prepare_request('POST', f'shadowAttributes/accept/{proposal_id}') + return self._check_json_response(response) + + def discard_attribute_proposal(self, proposal: MISPShadowAttribute | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Discard a proposal. You cannot modify an existing proposal, only accept/discard + + :param proposal: attribute proposal to discard + """ + proposal_id = get_uuid_or_id_from_abstract_misp(proposal) + response = self._prepare_request('POST', f'shadowAttributes/discard/{proposal_id}') + return self._check_json_response(response) + + # ## END Attribute Proposal ### + + # ## BEGIN Sighting ### + + def sightings(self, misp_entity: AbstractMISP | None = None, + org: MISPOrganisation | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | list[MISPSighting] | list[dict[str, Any]]: + """Get the list of sightings related to a MISPEvent or a MISPAttribute (depending on type of misp_entity): https://www.misp-project.org/openapi/#tag/Sightings/operation/getSightingsByEventId + + :param misp_entity: MISP entity + :param org: MISP organization + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + if isinstance(misp_entity, MISPEvent): + url = 'sightings/listSightings' + to_post = {'context': 'event', 'id': misp_entity.id} + elif isinstance(misp_entity, MISPAttribute): + url = 'sightings/listSightings' + to_post = {'context': 'attribute', 'id': misp_entity.id} + else: + url = 'sightings/index' + to_post = {} + + if org is not None: + org_id = get_uuid_or_id_from_abstract_misp(org) + to_post['org_id'] = org_id + + r = self._prepare_request('POST', url, data=to_post) + sightings = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(sightings, dict): + return sightings + to_return = [] + for sighting in sightings: + s = MISPSighting() + s.from_dict(**sighting) + to_return.append(s) + return to_return + + def add_sighting(self, sighting: MISPSighting | dict[str, Any], + attribute: MISPAttribute | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPSighting: + """Add a new sighting (globally, or to a specific attribute): https://www.misp-project.org/openapi/#tag/Sightings/operation/addSighting and https://www.misp-project.org/openapi/#tag/Sightings/operation/getSightingsByEventId + + :param sighting: sighting to add + :param attribute: specific attribute to modify with the sighting + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if attribute: + attribute_id = get_uuid_or_id_from_abstract_misp(attribute) + r = self._prepare_request('POST', f'sightings/add/{attribute_id}', data=sighting) + else: + # Either the ID/UUID is in the sighting, or we want to add a sighting on all the attributes with the given value + r = self._prepare_request('POST', 'sightings/add', data=sighting) + new_sighting = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_sighting: + return new_sighting + s = MISPSighting() + s.from_dict(**new_sighting) + return s + + def delete_sighting(self, sighting: MISPSighting | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a sighting from a MISP instance: https://www.misp-project.org/openapi/#tag/Sightings/operation/deleteSighting + + :param sighting: sighting to delete + """ + sighting_id = get_uuid_or_id_from_abstract_misp(sighting) + response = self._prepare_request('POST', f'sightings/delete/{sighting_id}') + return self._check_json_response(response) + + # ## END Sighting ### + + # ## BEGIN Tags ### + + def tags(self, pythonify: bool = False, **kw_params) -> dict[str, Any] | list[MISPTag]: # type: ignore[no-untyped-def] + """Get the list of existing tags: https://www.misp-project.org/openapi/#tag/Tags/operation/getTags + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'tags/index', kw_params=kw_params) + tags = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in tags: + return tags['Tag'] + to_return = [] + for tag in tags['Tag']: + t = MISPTag() + t.from_dict(**tag) + to_return.append(t) + return to_return + + def get_tag(self, tag: MISPTag | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPTag: + """Get a tag by id: https://www.misp-project.org/openapi/#tag/Tags/operation/getTagById + + :param tag: tag to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + tag_id = get_uuid_or_id_from_abstract_misp(tag) + r = self._prepare_request('GET', f'tags/view/{tag_id}') + tag_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in tag_r: + return tag_r + t = MISPTag() + t.from_dict(**tag_r) + return t + + def add_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: + """Add a new tag on a MISP instance: https://www.misp-project.org/openapi/#tag/Tags/operation/addTag + The user calling this method needs the Tag Editor permission. + It doesn't add a tag to an event, simply creates it on the MISP instance. + + :param tag: tag to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'tags/add', data=tag) + new_tag = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_tag: + return new_tag + t = MISPTag() + t.from_dict(**new_tag) + return t + + def enable_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: + """Enable a tag + + :param tag: tag to enable + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + tag.hide_tag = False + return self.update_tag(tag, pythonify=pythonify) + + def disable_tag(self, tag: MISPTag, pythonify: bool = False) -> dict[str, Any] | MISPTag: + """Disable a tag + + :param tag: tag to disable + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + tag.hide_tag = True + return self.update_tag(tag, pythonify=pythonify) + + def update_tag(self, tag: MISPTag, tag_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPTag: + """Edit only the provided parameters of a tag: https://www.misp-project.org/openapi/#tag/Tags/operation/editTag + + :param tag: tag to update + :aram tag_id: tag ID to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if tag_id is None: + tid = get_uuid_or_id_from_abstract_misp(tag) + else: + tid = get_uuid_or_id_from_abstract_misp(tag_id) + r = self._prepare_request('POST', f'tags/edit/{tid}', data=tag) + updated_tag = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_tag: + return updated_tag + t = MISPTag() + t.from_dict(**updated_tag) + return t + + def delete_tag(self, tag: MISPTag | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a tag from a MISP instance: https://www.misp-project.org/openapi/#tag/Tags/operation/deleteTag + + :param tag: tag to delete + """ + tag_id = get_uuid_or_id_from_abstract_misp(tag) + response = self._prepare_request('POST', f'tags/delete/{tag_id}') + return self._check_json_response(response) + + def search_tags(self, tagname: str, strict_tagname: bool = False, pythonify: bool = False) -> dict[str, Any] | list[MISPTag] | list[dict[str, Any]]: + """Search for tags by name: https://www.misp-project.org/openapi/#tag/Tags/operation/searchTag + + :param tag_name: Name to search, use % for substrings matches. + :param strict_tagname: only return tags matching exactly the tag name (so skipping synonyms and cluster's value) + """ + query = {'tagname': tagname, 'strict_tagname': strict_tagname} + response = self._prepare_request('POST', 'tags/search', data=query) + normalized_response = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): + return normalized_response + to_return: list[MISPTag] = [] + for tag in normalized_response: + t = MISPTag() + t.from_dict(**tag) + to_return.append(t) + return to_return + + # ## END Tags ### + + # ## BEGIN Taxonomies ### + + def taxonomies(self, pythonify: bool = False) -> dict[str, Any] | list[MISPTaxonomy] | list[dict[str, Any]]: + """Get all the taxonomies: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/getTaxonomies + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'taxonomies/index') + taxonomies = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(taxonomies, dict): + return taxonomies + to_return = [] + for taxonomy in taxonomies: + t = MISPTaxonomy() + t.from_dict(**taxonomy) + to_return.append(t) + return to_return + + def get_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPTaxonomy: + """Get a taxonomy by id or namespace from a MISP instance: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/getTaxonomyById + + :param taxonomy: taxonomy to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + r = self._prepare_request('GET', f'taxonomies/view/{taxonomy_id}') + taxonomy_r = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in taxonomy_r: + return taxonomy_r + t = MISPTaxonomy() + t.from_dict(**taxonomy_r) + return t + + def enable_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Enable a taxonomy: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/enableTaxonomy + + :param taxonomy: taxonomy to enable + """ + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + response = self._prepare_request('POST', f'taxonomies/enable/{taxonomy_id}') + return self._check_json_response(response) + + def disable_taxonomy(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Disable a taxonomy: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/disableTaxonomy + + :param taxonomy: taxonomy to disable + """ + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + self.disable_taxonomy_tags(taxonomy_id) + response = self._prepare_request('POST', f'taxonomies/disable/{taxonomy_id}') + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) + + def disable_taxonomy_tags(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Disable all the tags of a taxonomy + + :param taxonomy: taxonomy with tags to disable + """ + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + response = self._prepare_request('POST', f'taxonomies/disableTag/{taxonomy_id}') + return self._check_json_response(response) + + def enable_taxonomy_tags(self, taxonomy: MISPTaxonomy | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Enable all the tags of a taxonomy. NOTE: this is automatically done when you call enable_taxonomy + + :param taxonomy: taxonomy with tags to enable + """ + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + t = self.get_taxonomy(taxonomy_id) + if isinstance(t, MISPTaxonomy): + if not t.enabled: + # Can happen if global pythonify is enabled. + raise PyMISPError(f"The taxonomy {t.namespace} is not enabled.") + elif not t['Taxonomy']['enabled']: + raise PyMISPError(f"The taxonomy {t['Taxonomy']['namespace']} is not enabled.") + url = urljoin(self.root_url, f'taxonomies/addTag/{taxonomy_id}') + response = self._prepare_request('POST', url) + return self._check_json_response(response) + + def update_taxonomies(self) -> dict[str, Any] | list[dict[str, Any]]: + """Update all the taxonomies: https://www.misp-project.org/openapi/#tag/Taxonomies/operation/updateTaxonomies""" + response = self._prepare_request('POST', 'taxonomies/update') + return self._check_json_response(response) + + def set_taxonomy_required(self, taxonomy: MISPTaxonomy | int | str, required: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy) + url = urljoin(self.root_url, f'taxonomies/toggleRequired/{taxonomy_id}') + payload = { + "Taxonomy": { + "required": required + } + } + response = self._prepare_request('POST', url, data=payload) + return self._check_json_response(response) + + # ## END Taxonomies ### + + # ## BEGIN Warninglists ### + + def warninglists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPWarninglist]: + """Get all the warninglists: https://www.misp-project.org/openapi/#tag/Warninglists/operation/getWarninglists + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'warninglists/index') + warninglists = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in warninglists: + return warninglists['Warninglists'] + to_return = [] + for warninglist in warninglists['Warninglists']: + w = MISPWarninglist() + w.from_dict(**warninglist) + to_return.append(w) + return to_return + + def get_warninglist(self, warninglist: MISPWarninglist | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPWarninglist: + """Get a warninglist by id: https://www.misp-project.org/openapi/#tag/Warninglists/operation/getWarninglistById + + :param warninglist: warninglist to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + warninglist_id = get_uuid_or_id_from_abstract_misp(warninglist) + r = self._prepare_request('GET', f'warninglists/view/{warninglist_id}') + wl = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in wl: + return wl + w = MISPWarninglist() + w.from_dict(**wl) + return w + + def toggle_warninglist(self, warninglist_id: str | int | list[int] | None = None, warninglist_name: str | list[str] | None = None, force_enable: bool | None = None) -> dict[str, Any] | list[dict[str, Any]]: + '''Toggle (enable/disable) the status of a warninglist by id: https://www.misp-project.org/openapi/#tag/Warninglists/operation/toggleEnableWarninglist + + :param warninglist_id: ID of the WarningList + :param warninglist_name: name of the WarningList + :param force_enable: Force the warning list in the enabled state (does nothing if already enabled) - None means toggle. + ''' + if warninglist_id is None and warninglist_name is None: + raise PyMISPError('Either warninglist_id or warninglist_name is required.') + query: dict[str, list[str] | list[int] | bool] = {} + if warninglist_id is not None: + if isinstance(warninglist_id, list): + query['id'] = warninglist_id + else: + query['id'] = [warninglist_id] # type: ignore + if warninglist_name is not None: + if isinstance(warninglist_name, list): + query['name'] = warninglist_name + else: + query['name'] = [warninglist_name] + if force_enable is not None: + query['enabled'] = force_enable + response = self._prepare_request('POST', 'warninglists/toggleEnable', data=query) + return self._check_json_response(response) + + def enable_warninglist(self, warninglist: MISPWarninglist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Enable a warninglist + + :param warninglist: warninglist to enable + """ + warninglist_id = get_uuid_or_id_from_abstract_misp(warninglist) + return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=True) + + def disable_warninglist(self, warninglist: MISPWarninglist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Disable a warninglist + + :param warninglist: warninglist to disable + """ + warninglist_id = get_uuid_or_id_from_abstract_misp(warninglist) + return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=False) + + def values_in_warninglist(self, value: Iterable[str]) -> dict[str, Any] | list[dict[str, Any]]: + """Check if IOC values are in warninglist + + :param value: iterator with values to check + """ + response = self._prepare_request('POST', 'warninglists/checkValue', data=value) + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) + + def update_warninglists(self) -> dict[str, Any] | list[dict[str, Any]]: + """Update all the warninglists: https://www.misp-project.org/openapi/#tag/Warninglists/operation/updateWarninglists""" + response = self._prepare_request('POST', 'warninglists/update') + return self._check_json_response(response) + + # ## END Warninglists ### + + # ## BEGIN Noticelist ### + + def noticelists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPNoticelist] | list[dict[str, Any]]: + """Get all the noticelists: https://www.misp-project.org/openapi/#tag/Noticelists/operation/getNoticelists + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'noticelists/index') + noticelists = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(noticelists, dict): + return noticelists + to_return = [] + for noticelist in noticelists: + n = MISPNoticelist() + n.from_dict(**noticelist) + to_return.append(n) + return to_return + + def get_noticelist(self, noticelist: MISPNoticelist | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPNoticelist: + """Get a noticelist by id: https://www.misp-project.org/openapi/#tag/Noticelists/operation/getNoticelistById + + :param notistlist: Noticelist to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + noticelist_id = get_uuid_or_id_from_abstract_misp(noticelist) + r = self._prepare_request('GET', f'noticelists/view/{noticelist_id}') + noticelist_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in noticelist_j: + return noticelist_j + n = MISPNoticelist() + n.from_dict(**noticelist_j) + return n + + def enable_noticelist(self, noticelist: MISPNoticelist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Enable a noticelist by id: https://www.misp-project.org/openapi/#tag/Noticelists/operation/toggleEnableNoticelist + + :param noticelist: Noticelist to enable + """ + # FIXME: https://github.com/MISP/MISP/issues/4856 + # response = self._prepare_request('POST', f'noticelists/enable/{noticelist_id}') + noticelist_id = get_uuid_or_id_from_abstract_misp(noticelist) + response = self._prepare_request('POST', f'noticelists/enableNoticelist/{noticelist_id}/true') + return self._check_json_response(response) + + def disable_noticelist(self, noticelist: MISPNoticelist | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Disable a noticelist by id + + :param noticelist: Noticelist to disable + """ + # FIXME: https://github.com/MISP/MISP/issues/4856 + # response = self._prepare_request('POST', f'noticelists/disable/{noticelist_id}') + noticelist_id = get_uuid_or_id_from_abstract_misp(noticelist) + response = self._prepare_request('POST', f'noticelists/enableNoticelist/{noticelist_id}') + return self._check_json_response(response) + + def update_noticelists(self) -> dict[str, Any] | list[dict[str, Any]]: + """Update all the noticelists: https://www.misp-project.org/openapi/#tag/Noticelists/operation/updateNoticelists""" + response = self._prepare_request('POST', 'noticelists/update') + return self._check_json_response(response) + + # ## END Noticelist ### + + # ## BEGIN Correlation Exclusions ### + + def correlation_exclusions(self, pythonify: bool = False) -> dict[str, Any] | list[MISPCorrelationExclusion] | list[dict[str, Any]]: + """Get all the correlation exclusions + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'correlation_exclusions') + correlation_exclusions = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(correlation_exclusions, dict): + return correlation_exclusions + to_return = [] + for correlation_exclusion in correlation_exclusions: + c = MISPCorrelationExclusion() + c.from_dict(**correlation_exclusion) + to_return.append(c) + return to_return + + def get_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPCorrelationExclusion: + """Get a correlation exclusion by ID + + :param correlation_exclusion: Correlation exclusion to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + exclusion_id = get_uuid_or_id_from_abstract_misp(correlation_exclusion) + r = self._prepare_request('GET', f'correlation_exclusions/view/{exclusion_id}') + correlation_exclusion_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in correlation_exclusion_j: + return correlation_exclusion_j + c = MISPCorrelationExclusion() + c.from_dict(**correlation_exclusion_j) + return c + + def add_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion, pythonify: bool = False) -> dict[str, Any] | MISPCorrelationExclusion: + """Add a new correlation exclusion + + :param correlation_exclusion: correlation exclusion to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'correlation_exclusions/add', data=correlation_exclusion) + new_correlation_exclusion = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_correlation_exclusion: + return new_correlation_exclusion + c = MISPCorrelationExclusion() + c.from_dict(**new_correlation_exclusion) + return c + + def delete_correlation_exclusion(self, correlation_exclusion: MISPCorrelationExclusion | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a correlation exclusion + + :param correlation_exclusion: The MISPCorrelationExclusion you wish to delete from MISP + """ + exclusion_id = get_uuid_or_id_from_abstract_misp(correlation_exclusion) + r = self._prepare_request('POST', f'correlation_exclusions/delete/{exclusion_id}') + return self._check_json_response(r) + + def clean_correlation_exclusions(self) -> dict[str, Any] | list[dict[str, Any]]: + """Initiate correlation exclusions cleanup""" + r = self._prepare_request('POST', 'correlation_exclusions/clean') + return self._check_json_response(r) + + # ## END Correlation Exclusions ### + + # ## BEGIN Galaxy ### + + def galaxies( + self, + withCluster: bool = False, + pythonify: bool = False, + ) -> dict[str, Any] | list[MISPGalaxy] | list[dict[str, Any]]: + """Get all the galaxies: https://www.misp-project.org/openapi/#tag/Galaxies/operation/getGalaxies + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'galaxies/index') + galaxies = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(galaxies, dict): + return galaxies + to_return = [] + for galaxy in galaxies: + g = MISPGalaxy() + g.from_dict(**galaxy, withCluster=withCluster) + to_return.append(g) + return to_return + + def search_galaxy( + self, + value: str, + withCluster: bool = False, + pythonify: bool = False, + ) -> dict[str, Any] | list[MISPGalaxy] | list[dict[str, Any]]: + """Text search to find a matching galaxy name, namespace, description, or uuid.""" + r = self._prepare_request("POST", "galaxies", data={"value": value}) + galaxies = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(galaxies, dict): + return galaxies + to_return = [] + for galaxy in galaxies: + g = MISPGalaxy() + g.from_dict(**galaxy, withCluster=withCluster) + to_return.append(g) + return to_return + + def get_galaxy(self, galaxy: MISPGalaxy | int | str | UUID, withCluster: bool = False, pythonify: bool = False) -> dict[str, Any] | MISPGalaxy: + """Get a galaxy by id: https://www.misp-project.org/openapi/#tag/Galaxies/operation/getGalaxyById + + :param galaxy: galaxy to get + :param withCluster: Include the clusters associated with the galaxy + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + galaxy_id = get_uuid_or_id_from_abstract_misp(galaxy) + r = self._prepare_request('GET', f'galaxies/view/{galaxy_id}') + galaxy_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in galaxy_j: + return galaxy_j + g = MISPGalaxy() + g.from_dict(**galaxy_j, withCluster=withCluster) + return g + + def search_galaxy_clusters(self, galaxy: MISPGalaxy | int | str | UUID, context: str = "all", searchall: str | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPGalaxyCluster] | list[dict[str, Any]]: + """Searches the galaxy clusters within a specific galaxy: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/getGalaxyClusters and https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/getGalaxyClusterById + + :param galaxy: The MISPGalaxy you wish to search in + :param context: The context of how you want to search within the galaxy_ + :param searchall: The search you want to make against the galaxy and context + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + + galaxy_id = get_uuid_or_id_from_abstract_misp(galaxy) + allowed_context_types: list[str] = ["all", "default", "custom", "org", "orgc", "deleted"] + if context not in allowed_context_types: + raise PyMISPError(f"The context must be one of {', '.join(allowed_context_types)}") + kw_params = {"context": context} + if searchall: + kw_params["searchall"] = searchall + r = self._prepare_request('POST', f"galaxy_clusters/index/{galaxy_id}", data=kw_params) + clusters_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(clusters_j, dict): + return clusters_j + response = [] + for cluster in clusters_j: + c = MISPGalaxyCluster() + c.from_dict(**cluster) + response.append(c) + return response + + def update_galaxies(self) -> dict[str, Any] | list[dict[str, Any]]: + """Update all the galaxies: https://www.misp-project.org/openapi/#tag/Galaxies/operation/updateGalaxies""" + response = self._prepare_request('POST', 'galaxies/update') + return self._check_json_response(response) + + def get_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: + """Gets a specific galaxy cluster + + :param galaxy_cluster: The MISPGalaxyCluster you want to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + + cluster_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster) + r = self._prepare_request('GET', f'galaxy_clusters/view/{cluster_id}') + cluster_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in cluster_j: + return cluster_j + gc = MISPGalaxyCluster() + gc.from_dict(**cluster_j) + return gc + + def add_galaxy_cluster(self, galaxy: MISPGalaxy | str | UUID, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: + """Add a new galaxy cluster to a MISP Galaxy: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/addGalaxyCluster + + :param galaxy: A MISPGalaxy (or UUID) where you wish to add the galaxy cluster + :param galaxy_cluster: A MISPGalaxyCluster you wish to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + + if getattr(galaxy_cluster, "default", False): + # We can't add default galaxies + raise PyMISPError('You are not able add a default galaxy cluster') + galaxy_id = get_uuid_or_id_from_abstract_misp(galaxy) + r = self._prepare_request('POST', f'galaxy_clusters/add/{galaxy_id}', data=galaxy_cluster) + cluster_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in cluster_j: + return cluster_j + gc = MISPGalaxyCluster() + gc.from_dict(**cluster_j) + return gc + + def update_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: + """Update a custom galaxy cluster: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/editGalaxyCluster + + ;param galaxy_cluster: The MISPGalaxyCluster you wish to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + + if getattr(galaxy_cluster, "default", False): + # We can't edit default galaxies + raise PyMISPError('You are not able to update a default galaxy cluster') + cluster_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster) + r = self._prepare_request('POST', f'galaxy_clusters/edit/{cluster_id}', data=galaxy_cluster) + cluster_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in cluster_j: + return cluster_j + gc = MISPGalaxyCluster() + gc.from_dict(**cluster_j) + return gc + + def publish_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Publishes a galaxy cluster: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/publishGalaxyCluster + + :param galaxy_cluster: The galaxy cluster you wish to publish + """ + if isinstance(galaxy_cluster, MISPGalaxyCluster) and getattr(galaxy_cluster, "default", False): + raise PyMISPError('You are not able to publish a default galaxy cluster') + cluster_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster) + r = self._prepare_request('POST', f'galaxy_clusters/publish/{cluster_id}') + response = self._check_json_response(r) + return response + + def fork_galaxy_cluster(self, galaxy: MISPGalaxy | int | str | UUID, galaxy_cluster: MISPGalaxyCluster, pythonify: bool = False) -> dict[str, Any] | MISPGalaxyCluster: + """Forks an existing galaxy cluster, creating a new one with matching attributes + + :param galaxy: The galaxy (or galaxy ID) where the cluster you want to fork resides + :param galaxy_cluster: The galaxy cluster you wish to fork + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + + galaxy_id = get_uuid_or_id_from_abstract_misp(galaxy) + cluster_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster) + # Create a duplicate cluster from the cluster to fork + forked_galaxy_cluster = MISPGalaxyCluster() + forked_galaxy_cluster.from_dict(**galaxy_cluster) + # Set the UUID and version it extends from the existing galaxy cluster + forked_galaxy_cluster.extends_uuid = forked_galaxy_cluster.pop('uuid') + forked_galaxy_cluster.extends_version = forked_galaxy_cluster.pop('version') + r = self._prepare_request('POST', f'galaxy_clusters/add/{galaxy_id}/forkUUID:{cluster_id}', data=galaxy_cluster) + cluster_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in cluster_j: + return cluster_j + gc = MISPGalaxyCluster() + gc.from_dict(**cluster_j) + return gc + + def delete_galaxy_cluster(self, galaxy_cluster: MISPGalaxyCluster | int | str | UUID, hard: bool=False) -> dict[str, Any] | list[dict[str, Any]]: + """Deletes a galaxy cluster from MISP: https://www.misp-project.org/openapi/#tag/Galaxy-Clusters/operation/deleteGalaxyCluster + + :param galaxy_cluster: The MISPGalaxyCluster you wish to delete from MISP + :param hard: flag for hard delete + """ + + if isinstance(galaxy_cluster, MISPGalaxyCluster) and getattr(galaxy_cluster, "default", False): + raise PyMISPError('You are not able to delete a default galaxy cluster') + data = {} + if hard: + data['hard'] = 1 + cluster_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster) + r = self._prepare_request('POST', f'galaxy_clusters/delete/{cluster_id}', data=data) + return self._check_json_response(r) + + def add_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> dict[str, Any] | list[dict[str, Any]]: + """Add a galaxy cluster relation, cluster relation must include + cluster UUIDs in both directions + + :param galaxy_cluster_relation: The MISPGalaxyClusterRelation to add + """ + r = self._prepare_request('POST', 'galaxy_cluster_relations/add/', data=galaxy_cluster_relation) + cluster_rel_j = self._check_json_response(r) + return cluster_rel_j + + def update_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation) -> dict[str, Any] | list[dict[str, Any]]: + """Update a galaxy cluster relation + + :param galaxy_cluster_relation: The MISPGalaxyClusterRelation to update + """ + cluster_relation_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster_relation) + r = self._prepare_request('POST', f'galaxy_cluster_relations/edit/{cluster_relation_id}', data=galaxy_cluster_relation) + cluster_rel_j = self._check_json_response(r) + return cluster_rel_j + + def delete_galaxy_cluster_relation(self, galaxy_cluster_relation: MISPGalaxyClusterRelation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a galaxy cluster relation + + :param galaxy_cluster_relation: The MISPGalaxyClusterRelation to delete + """ + cluster_relation_id = get_uuid_or_id_from_abstract_misp(galaxy_cluster_relation) + r = self._prepare_request('POST', f'galaxy_cluster_relations/delete/{cluster_relation_id}') + cluster_rel_j = self._check_json_response(r) + return cluster_rel_j + + # ## END Galaxy ### + + # ## BEGIN Feed ### + + def feeds(self, pythonify: bool = False) -> dict[str, Any] | list[MISPFeed] | list[dict[str, Any]]: + """Get the list of existing feeds: https://www.misp-project.org/openapi/#tag/Feeds/operation/getFeeds + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'feeds/index') + feeds = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(feeds, dict): + return feeds + to_return = [] + for feed in feeds: + f = MISPFeed() + f.from_dict(**feed) + to_return.append(f) + return to_return + + def get_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Get a feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/getFeedById + + :param feed: feed to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + feed_id = get_uuid_or_id_from_abstract_misp(feed) + r = self._prepare_request('GET', f'feeds/view/{feed_id}') + feed_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in feed_j: + return feed_j + f = MISPFeed() + f.from_dict(**feed_j) + return f + + def add_feed(self, feed: MISPFeed, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Add a new feed on a MISP instance: https://www.misp-project.org/openapi/#tag/Feeds/operation/addFeed + + :param feed: feed to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + # FIXME: https://github.com/MISP/MISP/issues/4834 + r = self._prepare_request('POST', 'feeds/add', data={'Feed': feed}) + new_feed = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_feed: + return new_feed + f = MISPFeed() + f.from_dict(**new_feed) + return f + + def enable_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Enable a feed; fetching it will create event(s): https://www.misp-project.org/openapi/#tag/Feeds/operation/enableFeed + + :param feed: feed to enable + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if not isinstance(feed, MISPFeed): + feed_id = get_uuid_or_id_from_abstract_misp(feed) # In case we have a UUID + f = MISPFeed() + f.id = feed_id + else: + f = feed + f.enabled = True + return self.update_feed(feed=f, pythonify=pythonify) + + def disable_feed(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Disable a feed: https://www.misp-project.org/openapi/#tag/Feeds/operation/disableFeed + + :param feed: feed to disable + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if not isinstance(feed, MISPFeed): + feed_id = get_uuid_or_id_from_abstract_misp(feed) # In case we have a UUID + f = MISPFeed() + f.id = feed_id + else: + f = feed + f.enabled = False + return self.update_feed(feed=f, pythonify=pythonify) + + def enable_feed_cache(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Enable the caching of a feed + + :param feed: feed to enable caching + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if not isinstance(feed, MISPFeed): + feed_id = get_uuid_or_id_from_abstract_misp(feed) # In case we have a UUID + f = MISPFeed() + f.id = feed_id + else: + f = feed + f.caching_enabled = True + return self.update_feed(feed=f, pythonify=pythonify) + + def disable_feed_cache(self, feed: MISPFeed | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Disable the caching of a feed + + :param feed: feed to disable caching + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if not isinstance(feed, MISPFeed): + feed_id = get_uuid_or_id_from_abstract_misp(feed) # In case we have a UUID + f = MISPFeed() + f.id = feed_id + else: + f = feed + f.caching_enabled = False + return self.update_feed(feed=f, pythonify=pythonify) + + def update_feed(self, feed: MISPFeed, feed_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPFeed: + """Update a feed on a MISP instance + + :param feed: feed to update + :param feed_id: feed id + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if feed_id is None: + fid = get_uuid_or_id_from_abstract_misp(feed) + else: + fid = get_uuid_or_id_from_abstract_misp(feed_id) + # FIXME: https://github.com/MISP/MISP/issues/4834 + r = self._prepare_request('POST', f'feeds/edit/{fid}', data={'Feed': feed}) + updated_feed = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_feed: + return updated_feed + f = MISPFeed() + f.from_dict(**updated_feed) + return f + + def delete_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a feed from a MISP instance + + :param feed: feed to delete + """ + feed_id = get_uuid_or_id_from_abstract_misp(feed) + response = self._prepare_request('POST', f'feeds/delete/{feed_id}') + return self._check_json_response(response) + + def fetch_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Fetch one single feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/fetchFromFeed + + :param feed: feed to fetch + """ + feed_id = get_uuid_or_id_from_abstract_misp(feed) + response = self._prepare_request('GET', f'feeds/fetchFromFeed/{feed_id}') + return self._check_json_response(response) + + def cache_all_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: + """ Cache all the feeds: https://www.misp-project.org/openapi/#tag/Feeds/operation/cacheFeeds""" + response = self._prepare_request('GET', 'feeds/cacheFeeds/all') + return self._check_json_response(response) + + def cache_feed(self, feed: MISPFeed | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Cache a specific feed by id: https://www.misp-project.org/openapi/#tag/Feeds/operation/cacheFeeds + + :param feed: feed to cache + """ + feed_id = get_uuid_or_id_from_abstract_misp(feed) + response = self._prepare_request('GET', f'feeds/cacheFeeds/{feed_id}') + return self._check_json_response(response) + + def cache_freetext_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: + """Cache all the freetext feeds""" + response = self._prepare_request('GET', 'feeds/cacheFeeds/freetext') + return self._check_json_response(response) + + def cache_misp_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: + """Cache all the MISP feeds""" + response = self._prepare_request('GET', 'feeds/cacheFeeds/misp') + return self._check_json_response(response) + + def compare_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: + """Generate the comparison matrix for all the MISP feeds""" + response = self._prepare_request('GET', 'feeds/compareFeeds') + return self._check_json_response(response) + + def load_default_feeds(self) -> dict[str, Any] | list[dict[str, Any]]: + """Load all the default feeds.""" + response = self._prepare_request('POST', 'feeds/loadDefaultFeeds') + return self._check_json_response(response) + + # ## END Feed ### + + # ## BEGIN Server ### + + def servers(self, pythonify: bool = False) -> dict[str, Any] | list[MISPServer] | list[dict[str, Any]]: + """Get the existing servers the MISP instance can synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'servers/index') + servers = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(servers, dict): + return servers + to_return = [] + for server in servers: + s = MISPServer() + s.from_dict(**server) + to_return.append(s) + return to_return + + def get_sync_config(self, pythonify: bool = False) -> dict[str, Any] | MISPServer: + """Get the sync server config. + WARNING: This method only works if the user calling it is a sync user + + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('GET', 'servers/createSync') + server = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in server: + return server + s = MISPServer() + s.from_dict(**server) + return s + + def import_server(self, server: MISPServer, pythonify: bool = False) -> dict[str, Any] | MISPServer: + """Import a sync server config received from get_sync_config + + :param server: sync server config + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'servers/import', data=server) + server_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in server_j: + return server_j + s = MISPServer() + s.from_dict(**server_j) + return s + + def add_server(self, server: MISPServer, pythonify: bool = False) -> dict[str, Any] | MISPServer: + """Add a server to synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers + Note: You probably want to use PyMISP.get_sync_config and PyMISP.import_server instead + + :param server: sync server config + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'servers/add', data=server) + server_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in server_j: + return server_j + s = MISPServer() + s.from_dict(**server_j) + return s + + def update_server(self, server: MISPServer, server_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPServer: + """Update a server to synchronise with: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers + + :param server: sync server config + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if server_id is None: + sid = get_uuid_or_id_from_abstract_misp(server) + else: + sid = get_uuid_or_id_from_abstract_misp(server_id) + r = self._prepare_request('POST', f'servers/edit/{sid}', data=server) + updated_server = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_server: + return updated_server + s = MISPServer() + s.from_dict(**updated_server) + return s + + def delete_server(self, server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a sync server: https://www.misp-project.org/openapi/#tag/Servers/operation/getServers + + :param server: sync server config + """ + server_id = get_uuid_or_id_from_abstract_misp(server) + response = self._prepare_request('POST', f'servers/delete/{server_id}') + return self._check_json_response(response) + + def server_pull(self, server: MISPServer | int | str | UUID, event: MISPEvent | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Initialize a pull from a sync server, optionally limited to one event: https://www.misp-project.org/openapi/#tag/Servers/operation/pullServer + + :param server: sync server config + :param event: event + """ + server_id = get_uuid_or_id_from_abstract_misp(server) + if event: + event_id = get_uuid_or_id_from_abstract_misp(event) + url = f'servers/pull/{server_id}/{event_id}' + else: + url = f'servers/pull/{server_id}' + response = self._prepare_request('GET', url) + # FIXME: can we pythonify? + return self._check_json_response(response) + + def server_push(self, server: MISPServer | int | str | UUID, event: MISPEvent | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Initialize a push to a sync server, optionally limited to one event: https://www.misp-project.org/openapi/#tag/Servers/operation/pushServer + + :param server: sync server config + :param event: event + """ + server_id = get_uuid_or_id_from_abstract_misp(server) + if event: + event_id = get_uuid_or_id_from_abstract_misp(event) + url = f'servers/push/{server_id}/{event_id}' + else: + url = f'servers/push/{server_id}' + response = self._prepare_request('GET', url) + # FIXME: can we pythonify? + return self._check_json_response(response) + + def test_server(self, server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Test if a sync link is working as expected + + :param server: sync server config + """ + server_id = get_uuid_or_id_from_abstract_misp(server) + response = self._prepare_request('POST', f'servers/testConnection/{server_id}') + return self._check_json_response(response) + + # ## END Server ### + + # ## BEGIN Sharing group ### + + def sharing_groups(self, pythonify: bool = False) -> dict[str, Any] | list[MISPSharingGroup] | list[dict[str, Any]]: + """Get the existing sharing groups: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/getSharingGroup + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'sharingGroups/index') + sharing_groups = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(sharing_groups, dict): + return sharing_groups + to_return = [] + for sharing_group in sharing_groups: + s = MISPSharingGroup() + s.from_dict(**sharing_group) + to_return.append(s) + return to_return + + def get_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: + """Get a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/getSharingGroupById + + :param sharing_group: sharing group to find + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + r = self._prepare_request('GET', f'sharing_groups/view/{sharing_group_id}') + sharing_group_resp = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in sharing_group_resp: + return sharing_group_resp + s = MISPSharingGroup() + s.from_dict(**sharing_group_resp) + return s + + def add_sharing_group(self, sharing_group: MISPSharingGroup, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: + """Add a new sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addSharingGroup + + :param sharing_group: sharing group to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'sharingGroups/add', data=sharing_group) + sharing_group_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in sharing_group_j: + return sharing_group_j + s = MISPSharingGroup() + s.from_dict(**sharing_group_j) + return s + + def update_sharing_group(self, sharing_group: MISPSharingGroup | dict[str, Any], sharing_group_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPSharingGroup: + """Update sharing group parameters: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/editSharingGroup + + :param sharing_group: MISP Sharing Group + :param sharing_group_id Sharing group ID + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if sharing_group_id is None: + sid = get_uuid_or_id_from_abstract_misp(sharing_group) + else: + sid = get_uuid_or_id_from_abstract_misp(sharing_group_id) + sharing_group.pop('modified', None) # Quick fix for https://github.com/MISP/PyMISP/issues/1049 - remove when fixed in MISP. + r = self._prepare_request('POST', f'sharing_groups/edit/{sid}', data=sharing_group) + updated_sharing_group = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_sharing_group: + return updated_sharing_group + s = MISPSharingGroup() + s.from_dict(**updated_sharing_group) + return s + + def sharing_group_exists(self, sharing_group: MISPSharingGroup | int | str | UUID) -> bool: + """Fast check if sharing group exists. + + :param sharing_group: Sharing group to check + """ + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + r = self._prepare_request('HEAD', f'sharing_groups/view/{sharing_group_id}') + return self._check_head_response(r) + + def delete_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/deleteSharingGroup + + :param sharing_group: sharing group to delete + """ + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + response = self._prepare_request('POST', f'sharingGroups/delete/{sharing_group_id}') + return self._check_json_response(response) + + def add_org_to_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID, extend: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + '''Add an organisation to a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addOrganisationToSharingGroup + + :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID + :param organisation: Organisation's local instance ID, or Organisation's global UUID, or Organisation's name as known to the curent instance + :param extend: Allow the organisation to extend the group + ''' + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + to_jsonify = {'sg_id': sharing_group_id, 'org_id': organisation_id, 'extend': extend} + response = self._prepare_request('POST', 'sharingGroups/addOrg', data=to_jsonify) + return self._check_json_response(response) + + def remove_org_from_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + '''Remove an organisation from a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/removeOrganisationFromSharingGroup + + :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID + :param organisation: Organisation's local instance ID, or Organisation's global UUID, or Organisation's name as known to the curent instance + ''' + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + to_jsonify = {'sg_id': sharing_group_id, 'org_id': organisation_id} + response = self._prepare_request('POST', 'sharingGroups/removeOrg', data=to_jsonify) + return self._check_json_response(response) + + def add_server_to_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + server: MISPServer | int | str | UUID, all_orgs: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + '''Add a server to a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/addServerToSharingGroup + + :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID + :param server: Server's local instance ID, or URL of the Server, or Server's name as known to the curent instance + :param all_orgs: Add all the organisations of the server to the group + ''' + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + server_id = get_uuid_or_id_from_abstract_misp(server) + to_jsonify = {'sg_id': sharing_group_id, 'server_id': server_id, 'all_orgs': all_orgs} + response = self._prepare_request('POST', 'sharingGroups/addServer', data=to_jsonify) + return self._check_json_response(response) + + def remove_server_from_sharing_group(self, sharing_group: MISPSharingGroup | int | str | UUID, + server: MISPServer | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + '''Remove a server from a sharing group: https://www.misp-project.org/openapi/#tag/Sharing-Groups/operation/removeServerFromSharingGroup + + :param sharing_group: Sharing group's local instance ID, or Sharing group's global UUID + :param server: Server's local instance ID, or URL of the Server, or Server's name as known to the curent instance + ''' + sharing_group_id = get_uuid_or_id_from_abstract_misp(sharing_group) + server_id = get_uuid_or_id_from_abstract_misp(server) + to_jsonify = {'sg_id': sharing_group_id, 'server_id': server_id} + response = self._prepare_request('POST', 'sharingGroups/removeServer', data=to_jsonify) + return self._check_json_response(response) + + # ## END Sharing groups ### + + # ## BEGIN Organisation ### + + def organisations(self, scope: str="local", search: str | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPOrganisation] | list[dict[str, Any]]: + """Get all the organisations: https://www.misp-project.org/openapi/#tag/Organisations/operation/getOrganisations + + :param scope: scope of organizations to get + :param search: The search to make against the list of organisations + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + url_path = f'organisations/index/scope:{scope}' + if search: + url_path += f"/searchall:{search}" + + r = self._prepare_request('GET', url_path) + organisations = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(organisations, dict): + return organisations + to_return = [] + for organisation in organisations: + o = MISPOrganisation() + o.from_dict(**organisation) + to_return.append(o) + return to_return + + def get_organisation(self, organisation: MISPOrganisation | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: + """Get an organisation by id: https://www.misp-project.org/openapi/#tag/Organisations/operation/getOrganisationById + + :param organisation: organization to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + r = self._prepare_request('GET', f'organisations/view/{organisation_id}') + organisation_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in organisation_j: + return organisation_j + o = MISPOrganisation() + o.from_dict(**organisation_j) + return o + + def organisation_exists(self, organisation: MISPOrganisation | int | str | UUID) -> bool: + """Fast check if organisation exists. + + :param organisation: Organisation to check + """ + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + r = self._prepare_request('HEAD', f'organisations/view/{organisation_id}') + return self._check_head_response(r) + + def add_organisation(self, organisation: MISPOrganisation, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: + """Add an organisation: https://www.misp-project.org/openapi/#tag/Organisations/operation/addOrganisation + + :param organisation: organization to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'admin/organisations/add', data=organisation) + new_organisation = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in new_organisation: + return new_organisation + o = MISPOrganisation() + o.from_dict(**new_organisation) + return o + + def update_organisation(self, organisation: MISPOrganisation, organisation_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOrganisation: + """Update an organisation: https://www.misp-project.org/openapi/#tag/Organisations/operation/editOrganisation + + :param organisation: organization to update + :param organisation_id: id to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if organisation_id is None: + oid = get_uuid_or_id_from_abstract_misp(organisation) + else: + oid = get_uuid_or_id_from_abstract_misp(organisation_id) + r = self._prepare_request('POST', f'admin/organisations/edit/{oid}', data=organisation) + updated_organisation = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_organisation: + return updated_organisation + o = MISPOrganisation() + o.from_dict(**organisation) + return o + + def delete_organisation(self, organisation: MISPOrganisation | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete an organisation by id: https://www.misp-project.org/openapi/#tag/Organisations/operation/deleteOrganisation + + :param organisation: organization to delete + """ + # NOTE: MISP in inconsistent and currently require "delete" in the path and doesn't support HTTP DELETE + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + response = self._prepare_request('POST', f'admin/organisations/delete/{organisation_id}') + return self._check_json_response(response) + + # ## END Organisation ### + + # ## BEGIN User ### + + def users(self, search: str | None = None, organisation: int | None = None, pythonify: bool = False) -> dict[str, Any] | list[MISPUser] | list[dict[str, Any]]: + """Get all the users, or a filtered set of users: https://www.misp-project.org/openapi/#tag/Users/operation/getUsers + + :param search: The search to make against the list of users + :param organisation: The ID of an organisation to filter against + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + urlpath = 'admin/users/index' + if search: + urlpath += f'/value:{search}' + if organisation: + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + urlpath += f"/searchorg:{organisation_id}" + r = self._prepare_request('GET', urlpath) + users = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(users, dict): + return users + to_return = [] + for user in users: + u = MISPUser() + u.from_dict(**user) + to_return.append(u) + return to_return + + def get_user(self, user: MISPUser | int | str | UUID = 'me', pythonify: bool = False, expanded: bool = False) -> dict[str, Any] | MISPUser | tuple[MISPUser, MISPRole, list[MISPUserSetting]]: + """Get a user by id: https://www.misp-project.org/openapi/#tag/Users/operation/getUsers + + :param user: user to get; `me` means the owner of the API key doing the query + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param expanded: Also returns a MISPRole and a MISPUserSetting. Only taken in account if pythonify is True. + """ + user_id = get_uuid_or_id_from_abstract_misp(user) + r = self._prepare_request('GET', f'users/view/{user_id}') + user_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in user_j: + return user_j + u = MISPUser() + u.from_dict(**user_j) + if not expanded: + return u + else: + role = MISPRole() + role.from_dict(**user_j['Role']) + usersettings = [] + if user_j['UserSetting']: + for name, value in user_j['UserSetting'].items(): + us = MISPUserSetting() + us.from_dict(**{'name': name, 'value': value}) + usersettings.append(us) + return u, role, usersettings + + def get_new_authkey(self, user: MISPUser | int | str | UUID = 'me') -> str: + '''Get a new authorization key for a specific user, defaults to user doing the call: https://www.misp-project.org/openapi/#tag/AuthKeys/operation/addAuthKey + + :param user: The owner of the key + ''' + user_id = get_uuid_or_id_from_abstract_misp(user) + r = self._prepare_request('POST', f'/auth_keys/add/{user_id}', data={}) + authkey = self._check_json_response(r) + if 'AuthKey' in authkey and 'authkey_raw' in authkey['AuthKey']: + return authkey['AuthKey']['authkey_raw'] + else: + raise PyMISPUnexpectedResponse(f'Unable to get authkey: {authkey}') + + def add_user(self, user: MISPUser, pythonify: bool = False) -> dict[str, Any] | MISPUser: + """Add a new user: https://www.misp-project.org/openapi/#tag/Users/operation/addUser + + :param user: user to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'admin/users/add', data=user) + user_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in user_j: + return user_j + u = MISPUser() + u.from_dict(**user_j) + return u + + def update_user(self, user: MISPUser, user_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPUser: + """Update a user on a MISP instance: https://www.misp-project.org/openapi/#tag/Users/operation/editUser + + :param user: user to update + :param user_id: id to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if user_id is None: + uid = get_uuid_or_id_from_abstract_misp(user) + else: + uid = get_uuid_or_id_from_abstract_misp(user_id) + url = f'users/edit/{uid}' + if self._current_role.perm_admin or self._current_role.perm_site_admin: + url = f'admin/{url}' + r = self._prepare_request('POST', url, data=user) + updated_user = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_user: + return updated_user + e = MISPUser() + e.from_dict(**updated_user) + return e + + def delete_user(self, user: MISPUser | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a user by id: https://www.misp-project.org/openapi/#tag/Users/operation/deleteUser + + :param user: user to delete + """ + # NOTE: MISP in inconsistent and currently require "delete" in the path and doesn't support HTTP DELETE + user_id = get_uuid_or_id_from_abstract_misp(user) + response = self._prepare_request('POST', f'admin/users/delete/{user_id}') + return self._check_json_response(response) + + def change_user_password(self, new_password: str) -> dict[str, Any] | list[dict[str, Any]]: + """Change the password of the curent user: + + :param new_password: password to set + """ + response = self._prepare_request('POST', 'users/change_pw', data={'password': new_password}) + return self._check_json_response(response) + + def user_registrations(self, pythonify: bool = False) -> dict[str, Any] | list[MISPInbox] | list[dict[str, Any]]: + """Get all the user registrations + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'users/registrations/index') + registrations = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(registrations, dict): + return registrations + to_return = [] + for registration in registrations: + i = MISPInbox() + i.from_dict(**registration) + to_return.append(i) + return to_return + + def accept_user_registration(self, registration: MISPInbox | int | str | UUID, + organisation: MISPOrganisation | int | str | UUID | None = None, + role: MISPRole | int | str | None = None, + perm_sync: bool = False, perm_publish: bool = False, + perm_admin: bool = False, + unsafe_fallback: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Accept a user registration + + :param registration: the registration to accept + :param organisation: user organization + :param role: user role + :param perm_sync: indicator for sync + :param perm_publish: indicator for publish + :param perm_admin: indicator for admin + :param unsafe_fallback: indicator for unsafe fallback + """ + registration_id = get_uuid_or_id_from_abstract_misp(registration) + if role: + role_id = role_id = get_uuid_or_id_from_abstract_misp(role) + else: + for _r in self.roles(pythonify=True): + if not isinstance(_r, MISPRole): + continue + if _r.default_role: + role_id = get_uuid_or_id_from_abstract_misp(_r) + break + else: + raise PyMISPError('Unable to find default role') + + organisation_id = None + if organisation: + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + elif unsafe_fallback and isinstance(registration, MISPInbox): + if 'org_uuid' in registration.data: + org = self.get_organisation(registration.data['org_uuid'], pythonify=True) + if isinstance(org, MISPOrganisation): + organisation_id = org.id + + if unsafe_fallback and isinstance(registration, MISPInbox): + # Blindly use request from user, and instance defaults. + to_post = {'User': {'org_id': organisation_id, 'role_id': role_id, + 'perm_sync': registration.data['perm_sync'], + 'perm_publish': registration.data['perm_publish'], + 'perm_admin': registration.data['perm_admin']}} + else: + to_post = {'User': {'org_id': organisation_id, 'role_id': role_id, + 'perm_sync': perm_sync, 'perm_publish': perm_publish, + 'perm_admin': perm_admin}} + + r = self._prepare_request('POST', f'users/acceptRegistrations/{registration_id}', data=to_post) + return self._check_json_response(r) + + def discard_user_registration(self, registration: MISPInbox | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Discard a user registration + + :param registration: the registration to discard + """ + registration_id = get_uuid_or_id_from_abstract_misp(registration) + r = self._prepare_request('POST', f'users/discardRegistrations/{registration_id}') + return self._check_json_response(r) + + # ## END User ### + + # ## BEGIN Role ### + + def roles(self, pythonify: bool = False) -> dict[str, Any] | list[MISPRole] | list[dict[str, Any]]: + """Get the existing roles + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'roles/index') + roles = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(roles, dict): + return roles + to_return = [] + for role in roles: + nr = MISPRole() + nr.from_dict(**role) + to_return.append(nr) + return to_return + + def add_role(self, role: MISPRole, pythonify: bool = False) -> dict[str, Any] | MISPRole: + """Add a new role + + :param role: role to add + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + r = self._prepare_request('POST', 'admin/roles/add', data=role) + role_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in role_j: + return role_j + new_misp_role = MISPRole() + new_misp_role.from_dict(**role_j) + return new_misp_role + + def update_role(self, role: MISPRole, role_id: int | None = None, pythonify: bool = False) -> dict[str, Any] | MISPRole: + """Update a role on a MISP instance + + :param role: role to update + :param role_id: id to update + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if role_id is None: + uid = get_uuid_or_id_from_abstract_misp(role) + else: + uid = get_uuid_or_id_from_abstract_misp(role_id) + url = f'admin/roles/edit/{uid}' + r = self._prepare_request('POST', url, data=role) + updated_role = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in updated_role: + return updated_role + updated_misp_role = MISPRole() + updated_misp_role.from_dict(**updated_role) + return updated_misp_role + + def set_default_role(self, role: MISPRole | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Set a default role for the new user accounts + + :param role: the default role to set + """ + role_id = get_uuid_or_id_from_abstract_misp(role) + url = urljoin(self.root_url, f'admin/roles/set_default/{role_id}') + response = self._prepare_request('POST', url) + return self._check_json_response(response) + + def delete_role(self, role: MISPRole | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a role + + :param role: role to delete + """ + role_id = get_uuid_or_id_from_abstract_misp(role) + response = self._prepare_request('POST', f'admin/roles/delete/{role_id}') + return self._check_json_response(response) + + # ## END Role ### + + # ## BEGIN Decaying Models ### + + def update_decaying_models(self) -> dict[str, Any] | list[dict[str, Any]]: + """Update all the Decaying models""" + response = self._prepare_request('POST', 'decayingModel/update') + return self._check_json_response(response) + + def decaying_models(self, pythonify: bool = False) -> dict[str, Any] | list[MISPDecayingModel] | list[dict[str, Any]]: + """Get all the decaying models + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output + """ + r = self._prepare_request('GET', 'decayingModel/index') + models = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(models, dict): + return models + to_return = [] + for model in models: + n = MISPDecayingModel() + n.from_dict(**model) + to_return.append(n) + return to_return + + def enable_decaying_model(self, decaying_model: MISPDecayingModel | int | str) -> dict[str, Any] | list[dict[str, Any]]: + """Enable a decaying Model""" + if isinstance(decaying_model, MISPDecayingModel): + decaying_model_id = decaying_model.id + else: + decaying_model_id = int(decaying_model) + response = self._prepare_request('POST', f'decayingModel/enable/{decaying_model_id}') + return self._check_json_response(response) + + def disable_decaying_model(self, decaying_model: MISPDecayingModel | int | str) -> dict[str, Any] | list[dict[str, Any]]: + """Disable a decaying Model""" + if isinstance(decaying_model, MISPDecayingModel): + decaying_model_id = decaying_model.id + else: + decaying_model_id = int(decaying_model) + response = self._prepare_request('POST', f'decayingModel/disable/{decaying_model_id}') + return self._check_json_response(response) + + # ## END Decaying Models ### + + # ## BEGIN Search methods ### + + def search(self, controller: str = 'events', return_format: str = 'json', # type: ignore[no-untyped-def] + limit: int | None = None, page: int | None = None, + value: SearchParameterTypes | None = None, + type_attribute: SearchParameterTypes | None = None, + category: SearchParameterTypes | None = None, + org: SearchParameterTypes | None = None, + tags: SearchParameterTypes | None = None, + event_tags: SearchParameterTypes | None = None, + quick_filter: str | None = None, quickFilter: str | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + eventid: SearchType | None = None, + with_attachments: bool | None = None, withAttachments: bool | None = None, + metadata: bool | None = None, + uuid: str | None = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + last: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + published: bool | None = None, + enforce_warninglist: bool | None = None, enforceWarninglist: bool | None = None, + to_ids: ToIDSType | list[ToIDSType] | None = None, + deleted: str | None = None, + include_event_uuid: bool | None = None, includeEventUuid: bool | None = None, + include_event_tags: bool | None = None, includeEventTags: bool | None = None, + event_timestamp: datetime | date | int | str | float | None | None = None, + sg_reference_only: bool | None = None, + eventinfo: str | None = None, + searchall: bool | None = None, + requested_attributes: str | None = None, + include_context: bool | None = None, includeContext: bool | None = None, + headerless: bool | None = None, + include_sightings: bool | None = None, includeSightings: bool | None = None, + include_correlations: bool | None = None, includeCorrelations: bool | None = None, + include_decay_score: bool | None = None, includeDecayScore: bool | None = None, + object_name: str | None = None, + exclude_decayed: bool | None = None, + sharinggroup: int | list[int] | None = None, + pythonify: bool | None = False, + **kwargs) -> dict[str, Any] | str | list[MISPEvent | MISPAttribute | MISPObject] | list[dict[str, Any]]: + '''Search in the MISP instance + + :param controller: Controller to search on, it can be `events`, `objects`, `attributes`. The response will either be a list of events, objects, or attributes. + Reference documentation for each controller: + + * events: https://www.misp-project.org/openapi/#tag/Events/operation/restSearchEvents + * attributes: https://www.misp-project.org/openapi/#tag/Attributes/operation/restSearchAttributes + * objects: N/A + + :param return_format: Set the return format of the search (Currently supported: json, xml, openioc, suricata, snort - more formats are being moved to restSearch with the goal being that all searches happen through this API). Can be passed as the first parameter after restSearch or via the JSON payload. + :param limit: Limit the number of results returned, depending on the scope (for example 10 attributes or 10 full events). + :param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300). + :param value: Search for the given value in the attributes' value field. + :param type_attribute: The attribute type, any valid MISP attribute type is accepted. + :param category: The attribute category, any valid MISP attribute category is accepted. + :param org: Search by the creator organisation by supplying the organisation identifier. + :param tags: Tags to search or to exclude. You can pass a list, or the output of `build_complex_query` + :param event_tags: Tags to search or to exclude at the event level. You can pass a list, or the output of `build_complex_query` + :param quick_filter: The string passed to this field will ignore all of the other arguments. MISP will return an xml / json (depending on the header sent) of all events that have a sub-string match on value in the event info, event orgc, or any of the attribute value1 / value2 fields, or in the attribute comment. + :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. + :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. + :param eventid: The events that should be included / excluded from the search + :param with_attachments: If set, encodes the attachments / zipped malware samples as base64 in the data field within each attribute + :param metadata: Only the metadata (event, tags, relations) is returned, attributes and proposals are omitted. + :param uuid: Restrict the results by uuid. + :param publish_timestamp: Restrict the results by the last publish timestamp (newer than). + :param timestamp: Restrict the results by the timestamp (last edit). Any event with a timestamp newer than the given timestamp will be returned. In case you are dealing with /attributes as scope, the attribute's timestamp will be used for the lookup. The input can be a timestamp or a short-hand time description (7d or 24h for example). You can also pass a list with two values to set a time range (for example ["14d", "7d"]). + :param published: Set whether published or unpublished events should be returned. Do not set the parameter if you want both. + :param enforce_warninglist: Remove any attributes from the result that would cause a hit on a warninglist entry. + :param to_ids: By default all attributes are returned that match the other filter parameters, regardless of their to_ids setting. To restrict the returned data set to to_ids only attributes set this parameter to 1. 0 for the ones with to_ids set to False. + :param deleted: If this parameter is set to 1, it will only return soft-deleted attributes. ["0", "1"] will return the active ones as well as the soft-deleted ones. + :param include_event_uuid: Instead of just including the event ID, also include the event UUID in each of the attributes. + :param include_event_tags: Include the event level tags in each of the attributes. + :param event_timestamp: Only return attributes from events that have received a modification after the given timestamp. + :param sg_reference_only: If this flag is set, sharing group objects will not be included, instead only the sharing group ID is set. + :param eventinfo: Filter on the event's info field. + :param searchall: Search for a full or a substring (delimited by % for substrings) in the event info, event tags, attribute tags, attribute values or attribute comment fields. + :param requested_attributes: [CSV only] Select the fields that you wish to include in the CSV export. By setting event level fields additionally, includeContext is not required to get event metadata. + :param include_context: [Attribute only] Include the event data with each attribute. [CSV output] Add event level metadata in every line of the CSV. + :param headerless: [CSV Only] The CSV created when this setting is set to true will not contain the header row. + :param include_sightings: [JSON Only - Attribute] Include the sightings of the matching attributes. + :param include_decay_score: Include the decay score at attribute level. + :param include_correlations: [JSON Only - attribute] Include the correlations of the matching attributes. + :param object_name: [objects controller only] Search for objects with that name + :param exclude_decayed: [attributes controller only] Exclude the decayed attributes from the response + :param sharinggroup: Filter by sharing group ID(s) + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + + Deprecated: + + :param quickFilter: synonym for quick_filter + :param withAttachments: synonym for with_attachments + :param last: synonym for publish_timestamp + :param enforceWarninglist: synonym for enforce_warninglist + :param includeEventUuid: synonym for include_event_uuid + :param includeEventTags: synonym for include_event_tags + :param includeContext: synonym for include_context + + ''' + + return_formats = ('openioc', 'json', 'xml', 'suricata', 'snort', 'text', 'rpz', 'csv', 'cache', 'stix-xml', + 'stix', 'stix2', 'yara', 'yara-json', 'attack', 'attack-sightings', 'context', 'context-markdown') + + if controller not in ('events', 'attributes', 'objects'): + raise ValueError('controller has to be in {}'.format(', '.join(['events', 'attributes', 'objects']))) + + # Deprecated stuff / synonyms + if quickFilter is not None: + quick_filter = quickFilter + if withAttachments is not None: + with_attachments = withAttachments + if last is not None: + publish_timestamp = last + if enforceWarninglist is not None: + enforce_warninglist = enforceWarninglist + if includeEventUuid is not None: + include_event_uuid = includeEventUuid + if includeEventTags is not None: + include_event_tags = includeEventTags + if includeContext is not None: + include_context = includeContext + if includeDecayScore is not None: + include_decay_score = includeDecayScore + if includeCorrelations is not None: + include_correlations = includeCorrelations + if includeSightings is not None: + include_sightings = includeSightings + # Add all the parameters in kwargs are aimed at modules, or other 3rd party components, and cannot be sanitized. + # They are passed as-is. + query = kwargs + + if return_format not in return_formats: + raise ValueError('return_format has to be in {}'.format(', '.join(return_formats))) + if return_format == 'stix-xml': + query['returnFormat'] = 'stix' + else: + query['returnFormat'] = return_format + + query['page'] = page + query['limit'] = limit + query['value'] = value + query['type'] = type_attribute + query['category'] = category + query['org'] = org + query['tags'] = tags + query['event_tags'] = event_tags + query['quickFilter'] = quick_filter + query['from'] = self._make_timestamp(date_from) + query['to'] = self._make_timestamp(date_to) + query['eventid'] = eventid + query['withAttachments'] = self._make_misp_bool(with_attachments) + query['metadata'] = self._make_misp_bool(metadata) + query['uuid'] = uuid + if publish_timestamp is not None: + if isinstance(publish_timestamp, (list, tuple)): + query['publish_timestamp'] = (self._make_timestamp(publish_timestamp[0]), self._make_timestamp(publish_timestamp[1])) + else: + query['publish_timestamp'] = self._make_timestamp(publish_timestamp) + if timestamp is not None: + if isinstance(timestamp, (list, tuple)): + query['timestamp'] = (self._make_timestamp(timestamp[0]), self._make_timestamp(timestamp[1])) + else: + query['timestamp'] = self._make_timestamp(timestamp) + query['published'] = published + query['enforceWarninglist'] = self._make_misp_bool(enforce_warninglist) + if to_ids is not None: + if to_ids not in [0, 1, '0', '1']: + raise ValueError('to_ids has to be in 0 or 1') + query['to_ids'] = to_ids + query['deleted'] = deleted + query['includeEventUuid'] = self._make_misp_bool(include_event_uuid) + query['includeEventTags'] = self._make_misp_bool(include_event_tags) + if event_timestamp is not None: + if isinstance(event_timestamp, (list, tuple)): + query['event_timestamp'] = (self._make_timestamp(event_timestamp[0]), self._make_timestamp(event_timestamp[1])) + else: + query['event_timestamp'] = self._make_timestamp(event_timestamp) + query['sgReferenceOnly'] = self._make_misp_bool(sg_reference_only) + query['eventinfo'] = eventinfo + query['searchall'] = searchall + query['requested_attributes'] = requested_attributes + query['includeContext'] = self._make_misp_bool(include_context) + query['headerless'] = self._make_misp_bool(headerless) + query['includeSightings'] = self._make_misp_bool(include_sightings) + query['includeDecayScore'] = self._make_misp_bool(include_decay_score) + query['includeCorrelations'] = self._make_misp_bool(include_correlations) + query['object_name'] = object_name + query['excludeDecayed'] = self._make_misp_bool(exclude_decayed) + query['sharinggroup'] = sharinggroup + + url = urljoin(self.root_url, f'{controller}/restSearch') + if return_format == 'stix-xml': + response = self._prepare_request('POST', url, data=query, output_type='xml') + else: + response = self._prepare_request('POST', url, data=query) + + if return_format == 'csv': + normalized_response_text = self._check_response(response) + if (self.global_pythonify or pythonify) and not headerless: + return self._csv_to_dict(normalized_response_text) # type: ignore + else: + return normalized_response_text + elif return_format not in ['json', 'yara-json']: + return self._check_response(response) + + normalized_response: list[dict[str, Any]] | dict[str, Any] + if controller in ['events', 'objects']: + # This one is truly fucked: event returns a list, attributes doesn't. + normalized_response = self._check_json_response(response) + elif controller == 'attributes': + normalized_response = self._check_json_response(response) + + if 'errors' in normalized_response: + return normalized_response + + if return_format == 'json' and self.global_pythonify or pythonify: + # The response is in json, we can convert it to a list of pythonic MISP objects + to_return: list[MISPEvent | MISPAttribute | MISPObject] = [] + if controller == 'events': + if isinstance(normalized_response, dict): + return normalized_response + for e in normalized_response: + me = MISPEvent() + me.load(e) + to_return.append(me) + elif controller == 'attributes': + # FIXME: obvs, this is hurting my soul. We need something generic. + for a in normalized_response['Attribute']: # type: ignore[call-overload] + ma = MISPAttribute() + ma.from_dict(**a) + if 'Event' in ma: + me = MISPEvent() + me.from_dict(**ma.Event) + ma.Event = me + if 'RelatedAttribute' in ma: + related_attributes = [] + for ra in ma.RelatedAttribute: + r_attribute = MISPAttribute() + r_attribute.from_dict(**ra) + if 'Event' in r_attribute: + me = MISPEvent() + me.from_dict(**r_attribute.Event) + r_attribute.Event = me + related_attributes.append(r_attribute) + ma.RelatedAttribute = related_attributes + if 'Sighting' in ma: + sightings = [] + for sighting in ma.Sighting: + s = MISPSighting() + s.from_dict(**sighting) + sightings.append(s) + ma.Sighting = sightings + to_return.append(ma) + elif controller == 'objects': + if isinstance(normalized_response, dict): + return normalized_response + for o in normalized_response: + mo = MISPObject(o['Object']['name']) + mo.from_dict(**o) + to_return.append(mo) + return to_return + + return normalized_response + + def search_index(self, + all: str | None = None, + attribute: str | None = None, + email: str | None = None, + published: bool | None = None, + hasproposal: bool | None = None, + eventid: SearchType | None = None, + tags: SearchParameterTypes | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + eventinfo: str | None = None, + threatlevel: list[SearchType] | None = None, + distribution: list[SearchType] | None = None, + analysis: list[SearchType] | None = None, + org: SearchParameterTypes | None = None, + timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + sharinggroup: list[SearchType] | None = None, + minimal: bool | None = None, + sort: str | None = None, + desc: bool | None = None, + limit: int | None = None, + page: int | None = None, + pythonify: bool | None = None) -> dict[str, Any] | list[MISPEvent] | list[dict[str, Any]]: + """Search event metadata shown on the event index page. Using ! in front of a value + means NOT, except for parameters date_from, date_to and timestamp which cannot be negated. + Criteria are AND-ed together; values in lists are OR-ed together. Return matching events + with metadata but no attributes or objects; also see minimal parameter. + + :param all: Search for a full or a substring (delimited by % for substrings) in the + event info, event tags, attribute tags, attribute values or attribute comment fields. + :param attribute: Filter on attribute's value. + :param email: Filter on user's email. + :param published: Set whether published or unpublished events should be returned. + Do not set the parameter if you want both. + :param hasproposal: Filter for events containing proposal(s). + :param eventid: The events that should be included / excluded from the search + :param tags: Tags to search or to exclude. You can pass a list, or the output of + `build_complex_query` + :param date_from: Events with the date set to a date after the one specified. + This filter will use the date of the event. + :param date_to: Events with the date set to a date before the one specified. + This filter will use the date of the event. + :param eventinfo: Filter on the event's info field. + :param threatlevel: Threat level(s) (1,2,3,4) | list + :param distribution: Distribution level(s) (0,1,2,3) | list + :param analysis: Analysis level(s) (0,1,2) | list + :param org: Search by the creator organisation by supplying the organisation identifier. + :param timestamp: Restrict the results by the timestamp (last edit). Any event with a + timestamp newer than the given timestamp will be returned. In case you are dealing + with /attributes as scope, the attribute's timestamp will be used for the lookup. + :param publish_timestamp: Filter on event's publish timestamp. + :param sharinggroup: Restrict by a sharing group | list + :param minimal: Return only event ID, UUID, timestamp, sighting_timestamp and published. + :param sort: The field to sort the events by, such as 'id', 'date', 'attribute_count'. + :param desc: Whether to sort events ascending (default) or descending. + :param limit: Limit the number of events returned + :param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300). + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. + Warning: it might use a lot of RAM + """ + query = locals() + query.pop('self') + query.pop('pythonify') + if query.get('date_from'): + query['datefrom'] = self._make_timestamp(query.pop('date_from')) + if query.get('date_to'): + query['dateuntil'] = self._make_timestamp(query.pop('date_to')) + if isinstance(query.get('sharinggroup'), list): + query['sharinggroup'] = '|'.join([str(sg) for sg in query['sharinggroup']]) + if query.get('timestamp') is not None: + timestamp = query.pop('timestamp') + if isinstance(timestamp, (list, tuple)): + query['timestamp'] = (self._make_timestamp(timestamp[0]), self._make_timestamp(timestamp[1])) + else: + query['timestamp'] = self._make_timestamp(timestamp) + if query.get("sort"): + query["direction"] = "desc" if desc else "asc" + url = urljoin(self.root_url, 'events/index') + response = self._prepare_request('POST', url, data=query) + normalized_response = self._check_json_response(response) + + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): + return normalized_response + to_return = [] + for e_meta in normalized_response: + me = MISPEvent() + me.from_dict(**e_meta) + to_return.append(me) + return to_return + + def search_sightings(self, context: str | None = None, + context_id: SearchType | None = None, + type_sighting: str | None = None, + date_from: datetime | date | int | str | float | None | None = None, + date_to: datetime | date | int | str | float | None | None = None, + publish_timestamp: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + last: None | (datetime | date | int | str | float | None + | tuple[datetime | date | int | str | float | None, + datetime | date | int | str | float | None] + ) = None, + org: SearchType | None = None, + source: str | None = None, + include_attribute: bool | None = None, + include_event_meta: bool | None = None, + pythonify: bool | None = False + ) -> dict[str, Any] | list[dict[str, MISPEvent | MISPAttribute | MISPSighting]]: + '''Search sightings + + :param context: The context of the search. Can be either "attribute", "event", or nothing (will then match on events and attributes). + :param context_id: Only relevant if context is either "attribute" or "event". Then it is the relevant ID. + :param type_sighting: Type of sighting + :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. + :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. + :param publish_timestamp: Restrict the results by the last publish timestamp (newer than). + :param org: Search by the creator organisation by supplying the organisation identifier. + :param source: Source of the sighting + :param include_attribute: Include the attribute. + :param include_event_meta: Include the meta information of the event. + + Deprecated: + + :param last: synonym for publish_timestamp + + :Example: + + >>> misp.search_sightings(publish_timestamp='30d') # search sightings for the last 30 days on the instance + [ ... ] + >>> misp.search_sightings(context='attribute', context_id=6, include_attribute=True) # return list of sighting for attribute 6 along with the attribute itself + [ ... ] + >>> misp.search_sightings(context='event', context_id=17, include_event_meta=True, org=2) # return list of sighting for event 17 filtered with org id 2 + ''' + query: dict[str, Any] = {'returnFormat': 'json'} + if context is not None: + if context not in ['attribute', 'event']: + raise ValueError('context has to be in {}'.format(', '.join(['attribute', 'event']))) + url_path = f'sightings/restSearch/{context}' + else: + url_path = 'sightings/restSearch' + if isinstance(context_id, (MISPEvent, MISPAttribute)): + context_id = get_uuid_or_id_from_abstract_misp(context_id) + query['id'] = context_id + query['type'] = type_sighting + query['from'] = date_from + query['to'] = date_to + query['last'] = publish_timestamp + query['org_id'] = org + query['source'] = source + query['includeAttribute'] = include_attribute + query['includeEvent'] = include_event_meta + + url = urljoin(self.root_url, url_path) + response = self._prepare_request('POST', url, data=query) + normalized_response = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): + return normalized_response + + if self.global_pythonify or pythonify: + to_return = [] + for s in normalized_response: + entries: dict[str, MISPEvent | MISPAttribute | MISPSighting] = {} + s_data = s['Sighting'] + if include_event_meta: + e = s_data.pop('Event') + me = MISPEvent() + me.from_dict(**e) + entries['event'] = me + if include_attribute: + a = s_data.pop('Attribute') + ma = MISPAttribute() + ma.from_dict(**a) + entries['attribute'] = ma + ms = MISPSighting() + ms.from_dict(**s_data) + entries['sighting'] = ms + to_return.append(entries) + return to_return + return normalized_response + + def search_logs(self, limit: int | None = None, page: int | None = None, + log_id: int | None = None, title: str | None = None, + created: datetime | date | int | str | float | None | None = None, model: str | None = None, + action: str | None = None, user_id: int | None = None, + change: str | None = None, email: str | None = None, + org: str | None = None, description: str | None = None, + ip: str | None = None, pythonify: bool | None = False) -> dict[str, Any] | list[MISPLog] | list[dict[str, Any]]: + '''Search in logs + + Note: to run substring queries simply append/prepend/encapsulate the search term with % + + :param limit: Limit the number of results returned, depending on the scope (for example 10 attributes or 10 full events). + :param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300). + :param log_id: Log ID + :param title: Log Title + :param created: Creation timestamp + :param model: Model name that generated the log entry + :param action: The thing that was done + :param user_id: ID of the user doing the action + :param change: Change that occured + :param email: Email of the user + :param org: Organisation of the User doing the action + :param description: Description of the action + :param ip: Origination IP of the User doing the action + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + ''' + query = locals() + query.pop('self') + query.pop('pythonify') + if log_id is not None: + query['id'] = query.pop('log_id') + if created is not None and isinstance(created, datetime): + query['created'] = query.pop('created').timestamp() + + response = self._prepare_request('POST', 'admin/logs/index', data=query) + normalized_response = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): + return normalized_response + + to_return = [] + for log in normalized_response: + ml = MISPLog() + ml.from_dict(**log) + to_return.append(ml) + return to_return + + def search_feeds(self, value: SearchParameterTypes | None = None, pythonify: bool | None = False) -> dict[str, Any] | list[MISPFeed] | list[dict[str, Any]]: + '''Search in the feeds cached on the servers''' + response = self._prepare_request('POST', 'feeds/searchCaches', data={'value': value}) + normalized_response = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or isinstance(normalized_response, dict): + return normalized_response + to_return = [] + for feed in normalized_response: + f = MISPFeed() + f.from_dict(**feed) + to_return.append(f) + return to_return + + # ## END Search methods ### + + # ## BEGIN Communities ### + + def communities(self, pythonify: bool = False) -> dict[str, Any] | list[MISPCommunity] | list[dict[str, Any]]: + """Get all the communities + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'communities/index') + communities = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(communities, dict): + return communities + to_return = [] + for community in communities: + c = MISPCommunity() + c.from_dict(**community) + to_return.append(c) + return to_return + + def get_community(self, community: MISPCommunity | int | str | UUID, pythonify: bool = False) -> dict[str, Any] | MISPCommunity: + """Get a community by id from a MISP instance + + :param community: community to get + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + community_id = get_uuid_or_id_from_abstract_misp(community) + r = self._prepare_request('GET', f'communities/view/{community_id}') + community_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in community_j: + return community_j + c = MISPCommunity() + c.from_dict(**community_j) + return c + + def request_community_access(self, community: MISPCommunity | int | str | UUID, + requestor_email_address: str | None = None, + requestor_gpg_key: str | None = None, + requestor_organisation_name: str | None = None, + requestor_organisation_uuid: str | None = None, + requestor_organisation_description: str | None = None, + message: str | None = None, sync: bool = False, + anonymise_requestor_server: bool = False, + mock: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Request the access to a community + + :param community: community to request access + :param requestor_email_address: requestor email + :param requestor_gpg_key: requestor key + :param requestor_organisation_name: requestor org name + :param requestor_organisation_uuid: requestor org ID + :param requestor_organisation_description: requestor org desc + :param message: requestor message + :param sync: synchronize flag + :param anonymise_requestor_server: anonymise flag + :param mock: mock flag + """ + community_id = get_uuid_or_id_from_abstract_misp(community) + to_post = {'org_name': requestor_organisation_name, + 'org_uuid': requestor_organisation_uuid, + 'org_description': requestor_organisation_description, + 'email': requestor_email_address, 'gpgkey': requestor_gpg_key, + 'message': message, 'anonymise': anonymise_requestor_server, 'sync': sync, + 'mock': mock} + r = self._prepare_request('POST', f'communities/requestAccess/{community_id}', data=to_post) + return self._check_json_response(r) + + # ## END Communities ### + + # ## BEGIN Event Delegation ### + + def event_delegations(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEventDelegation] | list[dict[str, Any]]: + """Get all the event delegations + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'eventDelegations') + delegations = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(delegations, dict): + return delegations + to_return = [] + for delegation in delegations: + d = MISPEventDelegation() + d.from_dict(**delegation) + to_return.append(d) + return to_return + + def accept_event_delegation(self, delegation: MISPEventDelegation | int | str, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Accept the delegation of an event + + :param delegation: event delegation to accept + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + delegation_id = get_uuid_or_id_from_abstract_misp(delegation) + r = self._prepare_request('POST', f'eventDelegations/acceptDelegation/{delegation_id}') + return self._check_json_response(r) + + def discard_event_delegation(self, delegation: MISPEventDelegation | int | str, pythonify: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Discard the delegation of an event + + :param delegation: event delegation to discard + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + delegation_id = get_uuid_or_id_from_abstract_misp(delegation) + r = self._prepare_request('POST', f'eventDelegations/deleteDelegation/{delegation_id}') + return self._check_json_response(r) + + def delegate_event(self, event: MISPEvent | int | str | UUID | None = None, + organisation: MISPOrganisation | int | str | UUID | None = None, + event_delegation: MISPEventDelegation | None = None, + distribution: int = -1, message: str = '', pythonify: bool = False) -> dict[str, Any] | MISPEventDelegation: + """Delegate an event. Either event and organisation OR event_delegation are required + + :param event: event to delegate + :param organisation: organization + :param event_delegation: event delegation + :param distribution: distribution == -1 means recipient decides + :param message: message + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + if event and organisation: + event_id = get_uuid_or_id_from_abstract_misp(event) + organisation_id = get_uuid_or_id_from_abstract_misp(organisation) + data = {'event_id': event_id, 'org_id': organisation_id, 'distribution': distribution, 'message': message} + r = self._prepare_request('POST', f'eventDelegations/delegateEvent/{event_id}', data=data) + elif event_delegation: + r = self._prepare_request('POST', f'eventDelegations/delegateEvent/{event_delegation.event_id}', data=event_delegation) + else: + raise PyMISPError('Either event and organisation OR event_delegation are required.') + delegation_j = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or 'errors' in delegation_j: + return delegation_j + d = MISPEventDelegation() + d.from_dict(**delegation_j) + return d + + # ## END Event Delegation ### + + # ## BEGIN Others ### + + def push_event_to_ZMQ(self, event: MISPEvent | int | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Force push an event by id on ZMQ + + :param event: the event to push + """ + event_id = get_uuid_or_id_from_abstract_misp(event) + response = self._prepare_request('POST', f'events/pushEventToZMQ/{event_id}.json') + return self._check_json_response(response) + + def direct_call(self, url: str, data: dict[str, Any] | None = None, params: Mapping[str, Any] = {}, kw_params: Mapping[str, Any] = {}) -> Any: + """Very lightweight call that posts a data blob (python dictionary or json string) on the URL + + :param url: URL to post to + :param data: data to post + :param params: dict with parameters for request + :param kw_params: dict with keyword parameters for request + """ + if data is None: + response = self._prepare_request('GET', url, params=params, kw_params=kw_params) + else: + response = self._prepare_request('POST', url, data=data, params=params, kw_params=kw_params) + return self._check_response(response, lenient_response_type=True) + + def freetext(self, event: MISPEvent | int | str | UUID, string: str, adhereToWarninglists: bool | str = False, # type: ignore[no-untyped-def] + distribution: int | None = None, returnMetaAttributes: bool = False, pythonify: bool = False, **kwargs) -> dict[str, Any] | list[MISPAttribute] | list[dict[str, Any]]: + """Pass a text to the freetext importer + + :param event: event + :param string: query + :param adhereToWarninglists: flag + :param distribution: distribution == -1 means recipient decides + :param returnMetaAttributes: flag + :param pythonify: Returns a PyMISP Object instead of the plain json output + :param kwargs: kwargs passed to prepare_request + """ + + event_id = get_uuid_or_id_from_abstract_misp(event) + query: dict[str, Any] = {"value": string} wl_params = [False, True, 'soft'] - if adhereToWarninglists not in wl_params: - raise Exception('Invalid parameter, adhereToWarninglists Can only be {}'.format(', '.join(wl_params))) - if adhereToWarninglists: + if adhereToWarninglists in wl_params: query['adhereToWarninglists'] = adhereToWarninglists + else: + raise PyMISPError('Invalid parameter, adhereToWarninglists Can only be False, True, or soft') if distribution is not None: query['distribution'] = distribution if returnMetaAttributes: query['returnMetaAttributes'] = returnMetaAttributes - return self.__query('freeTextImport/{}'.format(event_id), query, controller='events') - - # ############################## - # ######## REST Search ######### - # ############################## - - def __query(self, path, query, controller='events', async_callback=None): - """Helper to prepare a search query""" - if query.get('error') is not None: - return query - if controller not in ['events', 'attributes', 'objects', 'sightings']: - raise ValueError('Invalid controller. Can only be {}'.format(', '.join(['events', 'attributes', 'objects', 'sightings']))) - url = urljoin(self.root_url, '{}/{}'.format(controller, path.lstrip('/'))) - - if ASYNC_OK and async_callback: - response = self._prepare_request('POST', url, json.dumps(query), async_callback) - else: - response = self._prepare_request('POST', url, json.dumps(query)) - return self._check_response(response) - - def search_index(self, published=None, eventid=None, tag=None, datefrom=None, - dateuntil=None, eventinfo=None, threatlevel=None, distribution=None, - analysis=None, attribute=None, org=None, async_callback=None, normalize=False, - timestamp=None, sharinggroup=None): - """Search only at the index level. Use ! infront of value as NOT, default OR - If using async, give a callback that takes 2 args, session and response: - basic usage is - pymisp.search_index(..., async_callback=lambda ses,resp: print(resp.json())) - - :param published: Published (0,1) - :param eventid: Evend ID(s) | str or list - :param tag: Tag(s) | str or list - :param datefrom: First date, in format YYYY-MM-DD - :param dateuntil: Last date, in format YYYY-MM-DD - :param eventinfo: Event info(s) to match | str or list - :param threatlevel: Threat level(s) (1,2,3,4) | str or list - :param distribution: Distribution level(s) (0,1,2,3) | str or list - :param analysis: Analysis level(s) (0,1,2) | str or list - :param org: Organisation(s) | str or list - :param async_callback: Function to call when the request returns (if running async) - :param normalize: Normalize output | True or False - :param timestamp: Interval since last update (in second, or 1d, 1h, ...) - :param sharinggroup: The sharing group value - """ - allowed = {'published': published, 'eventid': eventid, 'tag': tag, 'dateuntil': dateuntil, - 'datefrom': datefrom, 'eventinfo': eventinfo, 'threatlevel': threatlevel, - 'distribution': distribution, 'analysis': analysis, 'attribute': attribute, - 'org': org, 'timestamp': timestamp, 'sharinggroup': sharinggroup} - rule_levels = {'distribution': ["0", "1", "2", "3", "!0", "!1", "!2", "!3"], - 'threatlevel': ["1", "2", "3", "4", "!1", "!2", "!3", "!4"], - 'analysis': ["0", "1", "2", "!0", "!1", "!2"]} - buildup_url = "events/index" - - to_post = {} - for rule in allowed.keys(): - - if allowed.get(rule) is None: - continue - param = allowed[rule] - if isinstance(param, bool): - param = int(param) - if not isinstance(param, list): - param = [param] - # param = [x for x in map(str, param)] - if rule in rule_levels: - if not set(param).issubset(rule_levels[rule]): - raise SearchError('Values in your {} are invalid, has to be in {}'.format(rule, ', '.join(str(x) for x in rule_levels[rule]))) - to_post[rule] = '|'.join(str(x) for x in param) - url = urljoin(self.root_url, buildup_url) - - if self.asynch and async_callback: - response = self._prepare_request('POST', url, json.dumps(to_post), async_callback) - else: - response = self._prepare_request('POST', url, json.dumps(to_post)) - res = self._check_response(response) - if normalize: - to_return = {'response': []} - for elem in res['response']: - tmp = {'Event': elem} - to_return['response'].append(tmp) - res = to_return - return res - - def search_all(self, value): - """Search a value in the whole database""" - query = {'value': value, 'searchall': 1} - return self.__query('restSearch', query) - - def __prepare_rest_search(self, values, not_values): - """Prepare a search, generate the chain processed by the server - - :param values: Values to search - :param not_values: Values that should not be in the response - """ + r = self._prepare_request('POST', f'events/freeTextImport/{event_id}', data=query, **kwargs) + attributes = self._check_json_response(r) + if returnMetaAttributes or not (self.global_pythonify or pythonify) or isinstance(attributes, dict): + return attributes to_return = [] - if values is not None: - if isinstance(values, list): - to_return += values - else: - to_return.append(values) - if not_values is not None: - if isinstance(not_values, list): - to_return += ['!{}'.format(v) for v in not_values] - else: - to_return.append('!{}'.format(not_values)) + for attribute in attributes: + a = MISPAttribute() + a.from_dict(**attribute) + to_return.append(a) return to_return - def search(self, controller='events', async_callback=None, **kwargs): - """Search via the Rest API + def upload_stix(self, path: str | Path | BytesIO | StringIO | None = None, + data: str | bytes | None = None, version: str = '2') -> requests.Response: + """Upload a STIX file to MISP. - :param values: values to search for - :param not_values: values *not* to search for - :param type_attribute: Type of attribute - :param category: Category to search - :param org: Org reporting the event - :param tags: Tags to search for - :param not_tags: Tags *not* to search for - :param date_from: First date - :param date_to: Last date - :param last: Last published events (for example 5d or 12h or 30m) - :param eventid: Evend ID(s) | str or list - :param withAttachments: return events with or without the attachments - :param uuid: search by uuid - :param publish_timestamp: the publish timestamp - :param timestamp: the timestamp of the last modification. Can be a list (from->to) - :param enforceWarninglist: Enforce the warning lists - :param searchall: full text search on the database - :param metadata: return only metadata if True - :param published: return only published events - :param to_ids: return only the attributes with the to_ids flag set - :param deleted: also return the deleted attributes - :param event_timestamp: the timestamp of the last modification of the event (attributes controller only)). Can be a list (from->to) - :param includeProposals: return shadow attributes if True - :param async_callback: The function to run when results are returned + :param path: Path to the STIX on the disk (can be a path-like object, or a pseudofile) + :param data: stix object + :param version: Can be 1 or 2 """ - query = {} - # Event: array('value', 'type', 'category', 'org', 'tags', 'from', 'to', 'last', 'eventid', 'withAttachments', 'uuid', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'searchall', 'metadata', 'published'); - # Attribute: array('value', 'type', 'category', 'org', 'tags', 'from', 'to', 'last', 'eventid', 'withAttachments', 'uuid', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'to_ids', 'deleted'); - val = self.__prepare_rest_search(kwargs.pop('values', None), kwargs.pop('not_values', None)) - if val: - query['value'] = val - - query['type'] = kwargs.pop('type_attribute', None) - query['category'] = kwargs.pop('category', None) - query['org'] = kwargs.pop('org', None) - - tag = self.__prepare_rest_search(kwargs.pop('tags', None), kwargs.pop('not_tags', None)) - if tag: - query['tags'] = tag - - date_from = kwargs.pop('date_from', None) - if date_from: - if isinstance(date_from, datetime.date) or isinstance(date_from, datetime.datetime): - query['from'] = date_from.strftime('%Y-%m-%d') + to_post: str | bytes + if path is not None: + if isinstance(path, (str, Path)): + with open(path, 'rb') as f: + to_post = f.read() else: - query['from'] = date_from - - date_to = kwargs.pop('date_to', None) - if date_to: - if isinstance(date_to, datetime.date) or isinstance(date_to, datetime.datetime): - query['to'] = date_to.strftime('%Y-%m-%d') - else: - query['to'] = date_to - - query['last'] = kwargs.pop('last', None) - query['eventid'] = kwargs.pop('eventid', None) - query['withAttachments'] = kwargs.pop('withAttachments', None) - - uuid = kwargs.pop('uuid', None) - if uuid: - if self._valid_uuid(uuid): - query['uuid'] = uuid - else: - return {'error': 'You must enter a valid uuid.'} - - returnFormat = kwargs.pop('returnFormat', None) - if returnFormat: - if returnFormat in ['json', 'openioc', 'xml', 'suricata', 'snort', 'text', 'rpz', 'csv', 'cache', 'stix', 'stix2']: - query['returnFormat'] = returnFormat - else: - return {'error': 'You must enter a valid returnFormat - json, openioc, xml, suricata, snort, text, rpz, csv, stix, stix2 or cache'} + to_post = path.read() + elif data is not None: + to_post = data else: - query['returnFormat'] = 'json' + raise MISPServerError("please fill path or data parameter") - query['publish_timestamp'] = kwargs.pop('publish_timestamp', None) - query['timestamp'] = kwargs.pop('timestamp', None) - query['enforceWarninglist'] = kwargs.pop('enforceWarninglist', None) - query['to_ids'] = kwargs.pop('to_ids', None) - query['deleted'] = kwargs.pop('deleted', None) - query['published'] = kwargs.pop('published', None) + if str(version) == '1': + url = urljoin(self.root_url, 'events/upload_stix') + response = self._prepare_request('POST', url, data=to_post, output_type='xml', content_type='xml') + else: + url = urljoin(self.root_url, 'events/upload_stix/2') + response = self._prepare_request('POST', url, data=to_post) + return response - if controller == 'events': - # Event search only: - query['searchall'] = kwargs.pop('searchall', None) - query['metadata'] = kwargs.pop('metadata', None) - if controller == 'attributes': - query['event_timestamp'] = kwargs.pop('event_timestamp', None) - query['includeProposals'] = kwargs.pop('includeProposals', None) + # ## END Others ### - if kwargs: - logger.info('Some unknown parameters are in kwargs. appending as-is: {}'.format(', '.join(kwargs.keys()))) - # Add all other keys as-is. - query.update({k: v for k, v in kwargs.items()}) + # ## BEGIN Statistics ### - # Cleanup - query = {k: v for k, v in query.items() if v is not None} + def attributes_statistics(self, context: str = 'type', percentage: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Get attribute statistics from the MISP instance - # Create a session, make it async if and only if we have a callback - return self.__query('restSearch', query, controller, async_callback) - - def get_attachment(self, attribute_id): - """Get an attachement (not a malware sample) by attribute ID. - Returns the attachment as a bytestream, or a dictionary containing the error message. - - :param attribute_id: Attribute ID to fetched + :param context: "type" or "category" + :param percentage: get percentages """ - url = urljoin(self.root_url, 'attributes/download/{}'.format(attribute_id)) - response = self._prepare_request('GET', url) - try: - response.json() - # The query fails, response contains a json blob - return self._check_response(response) - except ValueError: - # content contains the attachment in binary - return response.content + # FIXME: https://github.com/MISP/MISP/issues/4874 + if context not in ['type', 'category']: + raise PyMISPError('context can only be "type" or "category"') + if percentage: + path = f'attributes/attributeStatistics/{context}/true' + else: + path = f'attributes/attributeStatistics/{context}' + response = self._prepare_request('GET', path) + return self._check_json_response(response) - def get_yara(self, event_id): - """Get the yara rules from an event""" - url = urljoin(self.root_url, 'attributes/restSearch') - to_post = {'request': {'eventid': event_id, 'type': 'yara'}} - response = self._prepare_request('POST', url, data=json.dumps(to_post)) - result = self._check_response(response) - if result.get('error') is not None: - return False, result.get('error') - if not result.get('response'): - return False, result.get('message') - rules = '\n\n'.join([a['value'] for a in result['response']['Attribute']]) - return True, rules + def tags_statistics(self, percentage: bool = False, name_sort: bool = False) -> dict[str, Any] | list[dict[str, Any]]: + """Get tag statistics from the MISP instance - def download_samples(self, sample_hash=None, event_id=None, all_samples=False, unzip=True): - """Download samples, by hash or event ID. If there are multiple samples in one event, use the all_samples switch - - :param sample_hash: hash of sample - :param event_id: ID of event - :param all_samples: download all samples - :param unzip: whether to unzip or keep zipped - :return: A tuple with (success, [[event_id, sample_hash, sample_as_bytesio], [event_id,...]]) - In case of legacy sample, the sample_hash will be replaced by the zip's filename + :param percentage: get percentages + :param name_sort: sort by name """ - url = urljoin(self.root_url, 'attributes/downloadSample') - to_post = {'request': {'hash': sample_hash, 'eventID': event_id, 'allSamples': all_samples}} - response = self._prepare_request('POST', url, data=json.dumps(to_post)) - result = self._check_response(response) - if result.get('error') is not None: - return False, result.get('error') - if not result.get('result'): - return False, result.get('message') - details = [] - for f in result['result']: - decoded = base64.b64decode(f['base64']) - zipped = BytesIO(decoded) - if unzip: - try: - archive = zipfile.ZipFile(zipped) - if f.get('md5') and f['md5'] in archive.namelist(): - # New format - unzipped = BytesIO(archive.open(f['md5'], pwd=b'infected').read()) - details.append([f['event_id'], f['md5'], unzipped]) - else: - # Old format - unzipped = BytesIO(archive.open(f['filename'], pwd=b'infected').read()) - details.append([f['event_id'], f['filename'], unzipped]) - except zipfile.BadZipfile: - # In case the sample isn't zipped - details.append([f['event_id'], f['filename'], zipped]) - else: - details.append([f['event_id'], "{0}.zip".format(f['filename']), zipped]) - return True, details + # FIXME: https://github.com/MISP/MISP/issues/4874 + # NOTE: https://github.com/MISP/MISP/issues/4879 + if percentage: + p = 'true' + else: + p = 'false' + if name_sort: + ns = 'true' + else: + ns = 'false' + response = self._prepare_request('GET', f'tags/tagStatistics/{p}/{ns}') + return self._check_json_response(response) - def download_last(self, last): - """Download the last published events. + def users_statistics(self, context: str = 'data') -> dict[str, Any] | list[dict[str, Any]]: + """Get user statistics from the MISP instance - :param last: can be defined in days, hours, minutes (for example 5d or 12h or 30m) + :param context: one of 'data', 'orgs', 'users', 'tags', 'attributehistogram', 'sightings', 'galaxyMatrix' """ - return self.search(last=last) - - def _string_to_timestamp(self, date_string): - pydate = parse(date_string) - if sys.version_info >= (3, 3): - # Sane python version - timestamp = pydate.timestamp() - else: - # Whatever - from datetime import timezone # Only for Python < 3.3 - timestamp = (pydate - datetime(1970, 1, 1, tzinfo=timezone.utc)).total_seconds() - return timestamp - - def get_events_last_modified(self, search_from, search_to=None): - """Download the last modified events. - - :param search_from: Beginning of the interval. Can be either a timestamp, or a date (2000-12-21) - :param search_to: End of the interval. Can be either a timestamp, or a date (2000-12-21) - """ - - search_from = self._string_to_timestamp(search_from) - - if search_to is not None: - search_to = self._string_to_timestamp(search_to) - to_search = [search_from, search_to] - else: - to_search = search_from - - return self.search(timestamp=to_search) - - # ########## Tags ########## - - def get_all_tags(self, quiet=False): - """Get all the tags used on the instance""" - url = urljoin(self.root_url, 'tags') - response = self._prepare_request('GET', url) - r = self._check_response(response) - if not quiet or r.get('errors'): - return r - else: - to_return = [] - for tag in r['Tag']: - to_return.append(tag['name']) - return to_return - - def new_tag(self, name=None, colour="#00ace6", exportable=False, hide_tag=False): - """Create a new tag""" - to_post = {'Tag': {'name': name, 'colour': colour, 'exportable': exportable, 'hide_tag': hide_tag}} - url = urljoin(self.root_url, 'tags/add') - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) - - # ########## Version ########## - - def get_api_version(self): - """Returns the current version of PyMISP installed on the system""" - return {'version': __version__} - - def get_api_version_master(self): - """Get the most recent version of PyMISP from github""" - r = requests.get('https://raw.githubusercontent.com/MISP/PyMISP/master/pymisp/__init__.py') - if r.status_code == 200: - version = re.findall("__version__ = '(.*)'", r.text) - return {'version': version[0]} - else: - return {'error': 'Impossible to retrieve the version of the master branch.'} - - def get_recommended_api_version(self): - """Returns the recommended API version from the server""" - url = urljoin(self.root_url, 'servers/getPyMISPVersion.json') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_version(self): - """Returns the version of the instance.""" - url = urljoin(self.root_url, 'servers/getVersion.json') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_version_master(self): - """Get the most recent version from github""" - r = requests.get('https://raw.githubusercontent.com/MISP/MISP/2.4/VERSION.json') - if r.status_code == 200: - master_version = json.loads(r.text) - return {'version': '{}.{}.{}'.format(master_version['major'], master_version['minor'], master_version['hotfix'])} - else: - return {'error': 'Impossible to retrieve the version of the master branch.'} - - # ############## Statistics ################## - - def get_attributes_statistics(self, context='type', percentage=None): - """Get attributes statistics from the MISP instance""" - if (context != 'category'): - context = 'type' - if percentage is not None: - url = urljoin(self.root_url, 'attributes/attributeStatistics/{}/{}'.format(context, percentage)) - else: - url = urljoin(self.root_url, 'attributes/attributeStatistics/{}'.format(context)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_tags_statistics(self, percentage=None, name_sort=None): - """Get tags statistics from the MISP instance""" - if percentage is not None: - percentage = 'true' - else: - percentage = 'false' - if name_sort is not None: - name_sort = 'true' - else: - name_sort = 'false' - url = urljoin(self.root_url, 'tags/tagStatistics/{}/{}'.format(percentage, name_sort)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_users_statistics(self, context='data'): - """Get users statistics from the MISP instance""" - availables_contexts = ['data', 'orgs', 'users', 'tags', 'attributehistogram', 'sightings', 'attackMatrix'] + availables_contexts = ['data', 'orgs', 'users', 'tags', 'attributehistogram', 'sightings', 'galaxyMatrix'] if context not in availables_contexts: - context = 'data' - url = urljoin(self.root_url, 'users/statistics/{}.json'.format(context)) - response = self._prepare_request('GET', url) - return self._check_response(response) + raise PyMISPError("context can only be {','.join(availables_contexts)}") + response = self._prepare_request('GET', f'users/statistics/{context}') + try: + return self._check_json_response(response) + except PyMISPError: + return self._check_json_response(response) - # ############## Sightings ################## + # ## END Statistics ### - def sighting_per_id(self, attribute_id): - """Add a sighting to an attribute (by attribute ID)""" - url = urljoin(self.root_url, 'sightings/add/{}'.format(attribute_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) + # ## BEGIN User Settings ### - def sighting_per_uuid(self, attribute_uuid): - """Add a sighting to an attribute (by attribute UUID)""" - url = urljoin(self.root_url, 'sightings/add/{}'.format(attribute_uuid)) - response = self._prepare_request('POST', url) - return self._check_response(response) + def user_settings(self, pythonify: bool = False) -> dict[str, Any] | list[MISPUserSetting] | list[dict[str, Any]]: + """Get all the user settings: https://www.misp-project.org/openapi/#tag/UserSettings/operation/getUserSettings - def set_sightings(self, sightings): - """Push a sighting (python dictionary or MISPSighting) or a list of sightings""" - if not isinstance(sightings, list): - sightings = [sightings] - for sighting in sightings: - if isinstance(sighting, MISPSighting): - to_post = sighting.to_json() - elif isinstance(sighting, dict): - to_post = json.dumps(sighting) - url = urljoin(self.root_url, 'sightings/add/') - response = self._prepare_request('POST', url, to_post) - return self._check_response(response) - - def sighting_per_json(self, json_file): - """Push a sighting (JSON file)""" - with open(json_file, 'rb') as f: - jdata = json.load(f) - return self.set_sightings(jdata) - - def sighting(self, value=None, uuid=None, id=None, source=None, type=None, timestamp=None, **kwargs): - """ Set a single sighting. - :value: Value of the attribute the sighting is related too. Pushing this object - will update the sighting count of each attriutes with thifs value on the instance - :uuid: UUID of the attribute to update - :id: ID of the attribute to update - :source: Source of the sighting - :type: Type of the sighting - :timestamp: Timestamp associated to the sighting + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM """ - s = MISPSighting() - s.from_dict(value=value, uuid=uuid, id=id, source=source, type=type, timestamp=timestamp, **kwargs) - return self.set_sightings(s) + r = self._prepare_request('GET', 'userSettings/index') + user_settings = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(user_settings, dict): + return user_settings + to_return = [] + for user_setting in user_settings: + u = MISPUserSetting() + u.from_dict(**user_setting) + to_return.append(u) + return to_return - def sighting_list(self, element_id, scope="attribute", org_id=False): - """Get the list of sighting. - :param element_id: could be an event id or attribute id - :type element_id: int - :param scope: could be attribute or event - :return: A json list of sighting corresponding to the search - :rtype: dict + def get_user_setting(self, user_setting: str, user: MISPUser | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPUserSetting: + """Get a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/getUserSettingById - :Example: - - >>> misp.sighting_list(4731) # default search on attribute - [ ... ] - >>> misp.sighting_list(42, event) # return list of sighting for event 42 - [ ... ] - >>> misp.sighting_list(element_id=42, org_id=2, scope=event) # return list of sighting for event 42 filtered with org id 2 + :param user_setting: name of user setting + :param user: user + :param pythonify: Returns a PyMISP Object instead of the plain json output """ - if isinstance(element_id, int) is False: - raise Exception('Invalid parameter, element_id must be a number') - if scope not in ["attribute", "event"]: - raise Exception('scope parameter must be "attribute" or "event"') - if org_id is not False: - if isinstance(org_id, int) is False: - raise Exception('Invalid parameter, org_id must be a number') + query: dict[str, Any] = {'setting': user_setting} + if user: + query['user_id'] = get_uuid_or_id_from_abstract_misp(user) + response = self._prepare_request('POST', 'userSettings/getSetting', data=query) + user_setting_j = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or 'errors' in user_setting_j: + return user_setting_j + u = MISPUserSetting() + u.from_dict(**user_setting_j) + return u + + def set_user_setting(self, user_setting: str, value: str | dict[str, Any], user: MISPUser | int | str | UUID | None = None, + pythonify: bool = False) -> dict[str, Any] | MISPUserSetting: + """Set a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/setUserSetting + + :param user_setting: name of user setting + :param value: value to set + :param user: user + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + query: dict[str, Any] = {'setting': user_setting} + if isinstance(value, dict): + value = str(dumps(value)) if HAS_ORJSON else dumps(value) + query['value'] = value + if user: + query['user_id'] = get_uuid_or_id_from_abstract_misp(user) + response = self._prepare_request('POST', 'userSettings/setSetting', data=query) + user_setting_j = self._check_json_response(response) + if not (self.global_pythonify or pythonify) or 'errors' in user_setting_j: + return user_setting_j + u = MISPUserSetting() + u.from_dict(**user_setting_j) + return u + + def delete_user_setting(self, user_setting: str, user: MISPUser | int | str | UUID | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a user setting: https://www.misp-project.org/openapi/#tag/UserSettings/operation/deleteUserSettingById + + :param user_setting: name of user setting + :param user: user + """ + query: dict[str, Any] = {'setting': user_setting} + if user: + query['user_id'] = get_uuid_or_id_from_abstract_misp(user) + response = self._prepare_request('POST', 'userSettings/delete', data=query) + return self._check_json_response(response) + + # ## END User Settings ### + + # ## BEGIN Blocklists ### + + def event_blocklists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPEventBlocklist] | list[dict[str, Any]]: + """Get all the blocklisted events + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'eventBlocklists/index') + event_blocklists = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(event_blocklists, dict): + return event_blocklists + to_return = [] + for event_blocklist in event_blocklists: + ebl = MISPEventBlocklist() + ebl.from_dict(**event_blocklist) + to_return.append(ebl) + return to_return + + def organisation_blocklists(self, pythonify: bool = False) -> dict[str, Any] | list[MISPOrganisationBlocklist] | list[dict[str, Any]]: + """Get all the blocklisted organisations + + :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM + """ + r = self._prepare_request('GET', 'orgBlocklists/index') + organisation_blocklists = self._check_json_response(r) + if not (self.global_pythonify or pythonify) or isinstance(organisation_blocklists, dict): + return organisation_blocklists + to_return = [] + for organisation_blocklist in organisation_blocklists: + obl = MISPOrganisationBlocklist() + obl.from_dict(**organisation_blocklist) + to_return.append(obl) + return to_return + + def _add_entries_to_blocklist(self, blocklist_type: str, uuids: str | list[str], **kwargs) -> dict[str, Any] | list[dict[str, Any]]: # type: ignore[no-untyped-def] + if blocklist_type == 'event': + url = 'eventBlocklists/add' + elif blocklist_type == 'organisation': + url = 'orgBlocklists/add' else: - org_id = "" - uri = 'sightings/listSightings/{}/{}/{}'.format(element_id, scope, org_id) - url = urljoin(self.root_url, uri) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def search_sightings(self, context='', async_callback=None, **kwargs): - """Search sightings via the REST API - :context: The context of the search, could be attribute, event or False - :param context_id: ID of the attribute or event if context is specified - :param type_sighting: Type of the sighting - :param date_from: From date - :param date_to: To date - :param publish_timestamp: Last published sighting (e.g. 5m, 3h, 7d) - :param org_id: The org_id - :param source: The source of the sighting - :param include_attribute: Should the result include attribute data - :param include_event: Should the result include event data - :param async_callback: The function to run when results are returned - - :Example: - - >>> misp.search_sightings(**{'publish_timestamp': '30d'}) # search sightings for the last 30 days on the instance - [ ... ] - >>> misp.search_sightings('attribute', context_id=6, include_attribute=1) # return list of sighting for attribute 6 along with the attribute itself - [ ... ] - >>> misp.search_sightings('event', **{'context_id': 17, 'include_event': 1, 'org_id': 2}) # return list of sighting for event 17 filtered with org id 2 - """ - if context not in ['', 'attribute', 'event']: - raise Exception('Context parameter must be empty, "attribute" or "event"') - query = {} - # Sighting: array('id', 'type', 'from', 'to', 'last', 'org_id', 'includeAttribute', 'includeEvent'); - query['returnFormat'] = kwargs.pop('returnFormat', 'json') - query['id'] = kwargs.pop('context_id', None) - query['type'] = kwargs.pop('type_sighting', None) - query['from'] = kwargs.pop('date_from', None) - query['to'] = kwargs.pop('date_to', None) - query['last'] = kwargs.pop('publish_timestamp', None) - query['org_id'] = kwargs.pop('org_id', None) - query['source'] = kwargs.pop('source', None) - query['includeAttribute'] = kwargs.pop('include_attribute', None) - query['includeEvent'] = kwargs.pop('include_event', None) - - # Cleanup - query = {k: v for k, v in query.items() if v is not None} - + raise PyMISPError('blocklist_type can only be "event" or "organisation"') + if isinstance(uuids, str): + uuids = [uuids] + data = {'uuids': uuids} if kwargs: - raise SearchError('Unused parameter: {}'.format(', '.join(kwargs.keys()))) + data.update({k: v for k, v in kwargs.items() if v}) + r = self._prepare_request('POST', url, data=data) + return self._check_json_response(r) - # Create a session, make it async if and only if we have a callback - controller = 'sightings' - return self.__query('restSearch/' + context, query, controller, async_callback) + def add_event_blocklist(self, uuids: str | list[str], comment: str | None = None, + event_info: str | None = None, event_orgc: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Add a new event in the blocklist - # ############## Sharing Groups ################## + :param uuids: UUIDs + :param comment: comment + :param event_info: event information + :param event_orgc: event organization + """ + return self._add_entries_to_blocklist('event', uuids=uuids, comment=comment, event_info=event_info, event_orgc=event_orgc) - def get_sharing_groups(self): - """Get the existing sharing groups""" - url = urljoin(self.root_url, 'sharing_groups.json') - response = self._prepare_request('GET', url) - return self._check_response(response) + def add_organisation_blocklist(self, uuids: str | list[str], comment: str | None = None, + org_name: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Add a new organisation in the blocklist - # ############## Users ################## + :param uuids: UUIDs + :param comment: comment + :param org_name: organization name + """ + return self._add_entries_to_blocklist('organisation', uuids=uuids, comment=comment, org_name=org_name) - def get_users_list(self): - return self._rest_list('admin/users') - - def get_user(self, user_id): - return self._rest_view('admin/users', user_id) - - def add_user(self, email, org_id, role_id, **kwargs): - new_user = MISPUser() - new_user.from_dict(email=email, org_id=org_id, role_id=role_id, **kwargs) - return self._rest_add('admin/users', new_user) - - def add_user_json(self, json_file): - with open(json_file, 'rb') as f: - jdata = json.load(f) - new_user = MISPUser() - new_user.from_dict(**jdata) - return self._rest_add('admin/users', new_user) - - def get_user_fields_list(self): - return self._rest_get_parameters('admin/users') - - def edit_user(self, user_id, **kwargs): - edit_user = MISPUser() - edit_user.from_dict(**kwargs) - return self._rest_edit('admin/users', edit_user, user_id) - - def edit_user_json(self, json_file, user_id): - with open(json_file, 'rb') as f: - jdata = json.load(f) - new_user = MISPUser() - new_user.from_dict(**jdata) - return self._rest_edit('admin/users', new_user, user_id) - - def delete_user(self, user_id): - return self._rest_delete('admin/users', user_id) - - # ############## Organisations ################## - - def get_organisations_list(self, scope="local"): - scope = scope.lower() - if scope not in ["local", "external", "all"]: - raise ValueError("Authorized fields are 'local','external' or 'all'") - return self._rest_list('organisations/index/scope:{}'.format(scope)) - - def get_organisation(self, organisation_id): - return self._rest_view('organisations', organisation_id) - - def add_organisation(self, name, **kwargs): - new_org = MISPOrganisation() - new_org.from_dict(name=name, **kwargs) - if 'local' in new_org: - if new_org.get('local') is False: - if 'uuid' not in new_org: - raise PyMISPError('A remote org MUST have a valid uuid') - return self._rest_add('admin/organisations', new_org) - - def add_organisation_json(self, json_file): - with open(json_file, 'rb') as f: - jdata = json.load(f) - new_org = MISPOrganisation() - new_org.from_dict(**jdata) - return self._rest_add('admin/organisations', new_org) - - def get_organisation_fields_list(self): - return self._rest_get_parameters('admin/organisations') - - def edit_organisation(self, org_id, **kwargs): - edit_org = MISPOrganisation() - edit_org.from_dict(**kwargs) - return self._rest_edit('admin/organisations', edit_org, org_id) - - def edit_organisation_json(self, json_file, org_id): - with open(json_file, 'rb') as f: - jdata = json.load(f) - edit_org = MISPOrganisation() - edit_org.from_dict(**jdata) - return self._rest_edit('admin/organisations', edit_org, org_id) - - def delete_organisation(self, org_id): - return self._rest_delete('admin/organisations', org_id) - - # ############## Servers ################## - - def _set_server_organisation(self, server, organisation): - if organisation is None: - raise PyMISPError('Need a valid organisation as argument, create it before if needed') - if 'Organisation' in organisation: - organisation = organisation.get('Organisation') - if 'local' not in organisation: - raise PyMISPError('Need a valid organisation as argument. "local" value have not been set in this organisation') - if 'id' not in organisation: - raise PyMISPError('Need a valid organisation as argument. "id" value doesn\'t exist in provided organisation') - - if organisation.get('local'): # Local organisation is '0' and remote organisation is '1'. These values are extracted from web interface of MISP - organisation_type = 0 + def _update_entries_in_blocklist(self, blocklist_type: str, uuid, **kwargs) -> dict[str, Any] | list[dict[str, Any]]: # type: ignore[no-untyped-def] + if blocklist_type == 'event': + url = f'eventBlocklists/edit/{uuid}' + elif blocklist_type == 'organisation': + url = f'orgBlocklists/edit/{uuid}' else: - organisation_type = 1 - server['organisation_type'] = organisation_type - server['json'] = json.dumps({'id': organisation['id']}) - return server + raise PyMISPError('blocklist_type can only be "event" or "organisation"') + data = {k: v for k, v in kwargs.items() if v} + r = self._prepare_request('POST', url, data=data) + return self._check_json_response(r) - def _set_server_parameters(self, url, name, authkey, organisation, internal, - push, pull, self_signed, push_rules, pull_rules, - submitted_cert, submitted_client_cert, delete_cert, - delete_client_cert): - server = {} - self._set_server_organisation(server, organisation) - if url is not None: - server['url'] = url - if name is not None: - server['name'] = name - if authkey is not None: - server['authkey'] = authkey - if internal is not None: - server['internal'] = internal - if push is not None: - server['push'] = push - if pull is not None: - server['pull'] = pull - if self_signed is not None: - server['self_signed'] = self_signed - if push_rules is not None: - server['push_rules'] = push_rules - if pull_rules is not None: - server['pull_rules'] = pull_rules - if submitted_cert is not None: - server['submitted_cert'] = submitted_cert - if submitted_client_cert is not None: - server['submitted_client_cert'] = submitted_client_cert - if delete_cert is not None: - server['delete_cert'] = delete_cert - if delete_client_cert is not None: - server['delete_client_cert'] = delete_client_cert - return server + def update_event_blocklist(self, event_blocklist: MISPEventBlocklist, event_blocklist_id: int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | MISPEventBlocklist: + """Update an event in the blocklist - def add_server(self, url, name, authkey, organisation, internal=None, push=False, - pull=False, self_signed=False, push_rules="", pull_rules="", - submitted_cert=None, submitted_client_cert=None): - new_server = self._set_server_parameters(url, name, authkey, organisation, internal, - push, pull, self_signed, push_rules, pull_rules, submitted_cert, - submitted_client_cert, None, None) - url = urljoin(self.root_url, 'servers/add') - response = self._prepare_request('POST', url, json.dumps(new_server)) - return self._check_response(response) - - def add_server_json(self, json_file): - with open(json_file, 'rb') as f: - jdata = json.load(f) - url = urljoin(self.root_url, 'servers/add') - response = self._prepare_request('POST', url, json.dumps(jdata)) - return self._check_response(response) - - def edit_server(self, server_id, url=None, name=None, authkey=None, organisation=None, internal=None, push=False, - pull=False, self_signed=False, push_rules="", pull_rules="", - submitted_cert=None, submitted_client_cert=None, delete_cert=None, delete_client_cert=None): - new_server = self._set_server_parameters(url, name, authkey, organisation, internal, - push, pull, self_signed, push_rules, pull_rules, submitted_cert, - submitted_client_cert, delete_cert, delete_client_cert) - url = urljoin(self.root_url, 'servers/edit/{}'.format(server_id)) - response = self._prepare_request('POST', url, json.dumps(new_server)) - return self._check_response(response) - - def edit_server_json(self, json_file, server_id): - with open(json_file, 'rb') as f: - jdata = json.load(f) - url = urljoin(self.root_url, 'servers/edit/{}'.format(server_id)) - response = self._prepare_request('POST', url, json.dumps(jdata)) - return self._check_response(response) - - def server_pull(self, server_id, event_id=None): - url = urljoin(self.root_url, 'servers/pull/{}'.format(server_id)) - if event_id is not None: - url += '/{}'.format(event_id) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def server_push(self, server_id, event_id=None): - url = urljoin(self.root_url, 'servers/push/{}'.format(server_id)) - if event_id is not None: - url += '/{}'.format(event_id) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def servers_index(self): - url = urljoin(self.root_url, 'servers/index') - response = self._prepare_request('GET', url) - return self._check_response(response) - - # ############## Roles ################## - - def get_roles_list(self): - """Get the list of existing roles""" - url = urljoin(self.root_url, 'roles') - response = self._prepare_request('GET', url) - return self._check_response(response) - - # ############## Tags ################## - - def get_tags_list(self): - """Get the list of existing tags.""" - url = urljoin(self.root_url, 'tags') - response = self._prepare_request('GET', url) - return self._check_response(response)['Tag'] - - def get_tag(self, tag_id): - """Get a tag by id.""" - url = urljoin(self.root_url, 'tags/view/{}'.format(tag_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def _set_tag_parameters(self, name, colour, exportable, hide_tag, org_id, count, user_id, numerical_value, - attribute_count, old_tag): - tag = old_tag - if name is not None: - tag['name'] = name - if colour is not None: - tag['colour'] = colour - if exportable is not None: - tag['exportable'] = exportable - if hide_tag is not None: - tag['hide_tag'] = hide_tag - if org_id is not None: - tag['org_id'] = org_id - if count is not None: - tag['count'] = count - if user_id is not None: - tag['user_id'] = user_id - if numerical_value is not None: - tag['numerical_value'] = numerical_value - if attribute_count is not None: - tag['attribute_count'] = attribute_count - - return {'Tag': tag} - - def edit_tag(self, tag_id, name=None, colour=None, exportable=None, hide_tag=None, org_id=None, count=None, - user_id=None, numerical_value=None, attribute_count=None): - """Edit only the provided parameters of a tag.""" - old_tag = self.get_tag(tag_id) - new_tag = self._set_tag_parameters(name, colour, exportable, hide_tag, org_id, count, user_id, - numerical_value, attribute_count, old_tag) - url = urljoin(self.root_url, 'tags/edit/{}'.format(tag_id)) - response = self._prepare_request('POST', url, json.dumps(new_tag)) - return self._check_response(response) - - def edit_tag_json(self, json_file, tag_id): - """Edit the tag using a json file.""" - with open(json_file, 'rb') as f: - jdata = json.load(f) - url = urljoin(self.root_url, 'tags/edit/{}'.format(tag_id)) - response = self._prepare_request('POST', url, json.dumps(jdata)) - return self._check_response(response) - - def enable_tag(self, tag_id): - """Enable a tag by id.""" - response = self.edit_tag(tag_id, hide_tag=False) - return response - - def disable_tag(self, tag_id): - """Disable a tag by id.""" - response = self.edit_tag(tag_id, hide_tag=True) - return response - - # ############## Taxonomies ################## - - def get_taxonomies_list(self): - """Get all the taxonomies.""" - url = urljoin(self.root_url, 'taxonomies') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_taxonomy(self, taxonomy_id): - """Get a taxonomy by id.""" - url = urljoin(self.root_url, 'taxonomies/view/{}'.format(taxonomy_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def update_taxonomies(self): - """Update all the taxonomies.""" - url = urljoin(self.root_url, 'taxonomies/update') - response = self._prepare_request('POST', url) - return self._check_response(response) - - def enable_taxonomy(self, taxonomy_id): - """Enable a taxonomy by id.""" - url = urljoin(self.root_url, 'taxonomies/enable/{}'.format(taxonomy_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def disable_taxonomy(self, taxonomy_id): - """Disable a taxonomy by id.""" - self.disable_taxonomy_tags(taxonomy_id) - url = urljoin(self.root_url, 'taxonomies/disable/{}'.format(taxonomy_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def get_taxonomy_tags_list(self, taxonomy_id): - """Get all the tags of a taxonomy by id.""" - url = urljoin(self.root_url, 'taxonomies/view/{}'.format(taxonomy_id)) - response = self._prepare_request('GET', url) - return self._check_response(response)["entries"] - - def enable_taxonomy_tags(self, taxonomy_id): - """Enable all the tags of a taxonomy by id.""" - enabled = self.get_taxonomy(taxonomy_id)['Taxonomy']['enabled'] - if enabled: - url = urljoin(self.root_url, 'taxonomies/addTag/{}'.format(taxonomy_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def disable_taxonomy_tags(self, taxonomy_id): - """Disable all the tags of a taxonomy by id.""" - url = urljoin(self.root_url, 'taxonomies/disableTag/{}'.format(taxonomy_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - # ############## WarningLists ################## - - def get_warninglists(self): - """Get all the warninglists.""" - url = urljoin(self.root_url, 'warninglists') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_warninglist(self, warninglist_id): - """Get a warninglist by id.""" - url = urljoin(self.root_url, 'warninglists/view/{}'.format(warninglist_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def update_warninglists(self): - """Update all the warninglists.""" - url = urljoin(self.root_url, 'warninglists/update') - response = self._prepare_request('POST', url) - return self._check_response(response) - - def toggle_warninglist(self, warninglist_id=None, warninglist_name=None, force_enable=None): - '''Toggle (enable/disable) the status of a warninglist by ID. - :param warninglist_id: ID of the WarningList - :param force_enable: Force the warning list in the enabled state (does nothing if already enabled) - ''' - if warninglist_id is None and warninglist_name is None: - raise Exception('Either warninglist_id or warninglist_name is required.') - query = {} - if warninglist_id is not None: - if not isinstance(warninglist_id, list): - warninglist_id = [warninglist_id] - query['id'] = warninglist_id - if warninglist_name is not None: - if not isinstance(warninglist_name, list): - warninglist_name = [warninglist_name] - query['name'] = warninglist_name - if force_enable is not None: - query['enabled'] = force_enable - url = urljoin(self.root_url, 'warninglists/toggleEnable') - response = self._prepare_request('POST', url, json.dumps(query)) - return self._check_response(response) - - def enable_warninglist(self, warninglist_id): - """Enable a warninglist by id.""" - return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=True) - - def disable_warninglist(self, warninglist_id): - """Disable a warninglist by id.""" - return self.toggle_warninglist(warninglist_id=warninglist_id, force_enable=False) - - def check_warninglist(self, value): - """Check if IOC values are in warninglist""" - url = urljoin(self.root_url, 'warninglists/checkValue') - response = self._prepare_request('POST', url, json.dumps(value)) - return self._check_response(response) - - # ############## NoticeLists ################## - - def get_noticelists(self): - """Get all the noticelists.""" - url = urljoin(self.root_url, 'noticelists') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_noticelist(self, noticelist_id): - """Get a noticelist by id.""" - url = urljoin(self.root_url, 'noticelists/view/{}'.format(noticelist_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def update_noticelists(self): - """Update all the noticelists.""" - url = urljoin(self.root_url, 'noticelists/update') - response = self._prepare_request('POST', url) - return self._check_response(response) - - def enable_noticelist(self, noticelist_id): - """Enable a noticelist by id.""" - url = urljoin(self.root_url, 'noticelists/enableNoticelist/{}/true'.format(noticelist_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - def disable_noticelist(self, noticelist_id): - """Disable a noticelist by id.""" - url = urljoin(self.root_url, 'noticelists/enableNoticelist/{}'.format(noticelist_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - # ############## Galaxies/Clusters ################## - - def get_galaxies(self): - """Get all the galaxies.""" - url = urljoin(self.root_url, 'galaxies') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_galaxy(self, galaxy_id): - """Get a galaxy by id.""" - url = urljoin(self.root_url, 'galaxies/view/{}'.format(galaxy_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def update_galaxies(self): - """Update all the galaxies.""" - url = urljoin(self.root_url, 'galaxies/update') - response = self._prepare_request('POST', url) - return self._check_response(response) - - # ############################################## - # ############### Non-JSON output ############## - # ############################################## - - # ############## Suricata ############## - - def download_all_suricata(self): - """Download all suricata rules events.""" - url = urljoin(self.root_url, 'events/nids/suricata/download') - response = self._prepare_request('GET', url, output_type='rules') - return response - - def download_suricata_rule_event(self, event_id): - """Download one suricata rule event. - - :param event_id: ID of the event to download (same as get) + :param event_blocklist: event block list + :param event_blocklist_id: event block lisd id + :param pythonify: Returns a PyMISP Object instead of the plain json output """ - url = urljoin(self.root_url, 'events/nids/suricata/download/{}'.format(event_id)) - response = self._prepare_request('GET', url, output_type='rules') - return response - - # ############## Text ############### - - def get_all_attributes_txt(self, type_attr, tags=False, eventId=False, allowNonIDS=False, date_from=False, date_to=False, last=False, enforceWarninglist=False, allowNotPublished=False): - """Get all attributes from a specific type as plain text. Only published and IDS flagged attributes are exported, except if stated otherwise.""" - url = urljoin(self.root_url, 'attributes/text/download/%s/%s/%s/%s/%s/%s/%s/%s/%s' % (type_attr, tags, eventId, allowNonIDS, date_from, date_to, last, enforceWarninglist, allowNotPublished)) - response = self._prepare_request('GET', url, output_type='txt') - return response - - # ############## STIX ############## - - def get_stix_event(self, event_id=None, with_attachments=False, from_date=False, to_date=False, tags=False): - """Get an event/events in STIX format""" - if tags: - if isinstance(tags, list): - tags = "&&".join(tags) - url = urljoin(self.root_url, "events/stix/download/{}/{}/{}/{}/{}".format( - event_id, with_attachments, tags, from_date, to_date)) - logger.debug("Getting STIX event from %s", url) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def get_stix(self, **kwargs): - return self.get_stix_event(**kwargs) - - def get_csv(self, eventid=None, attributes=[], object_attributes=[], misp_types=[], context=False, ignore=False, last=None): - """Get MISP values in CSV format - :param eventid: The event ID to query - :param attributes: The column names to export from normal attributes (i.e. uuid, value, type, ...) - :param object_attributes: The column names to export from attributes within objects (i.e. uuid, value, type, ...) - :param misp_types: MISP types to get (i.e. ip-src, hostname, ...) - :param context: Add event level context (event_info,event_member_org,event_source_org,event_distribution,event_threat_level_id,event_analysis,event_date,event_tag) - :param ignore: Returns the attributes even if the event isn't published, or the attribute doesn't have the to_ids flag set - """ - url = urljoin(self.root_url, 'events/csv/download') - to_post = {} - if eventid: - to_post['eventid'] = eventid - if attributes: - to_post['attributes'] = attributes - if object_attributes: - to_post['object_attributes'] = object_attributes - if misp_types: - for t in misp_types: - if t not in self.types: - logger.warning('{} is not a valid type'.format(t)) - to_post['type'] = misp_types - if context: - to_post['includeContext'] = True - if ignore: - to_post['ignore'] = True - if last: - to_post['last'] = last - if to_post: - response = self._prepare_request('POST', url, json.dumps(to_post), output_type='json') + if event_blocklist_id is None: + eblid = get_uuid_or_id_from_abstract_misp(event_blocklist) else: - response = self._prepare_request('POST', url, output_type='json') - return response.text + eblid = get_uuid_or_id_from_abstract_misp(event_blocklist_id) + updated_event_blocklist = self._update_entries_in_blocklist('event', eblid, **event_blocklist) + if not (self.global_pythonify or pythonify) or 'errors' in updated_event_blocklist: + return updated_event_blocklist + e = MISPEventBlocklist() + e.from_dict(**updated_event_blocklist) + return e - # ####################################### - # ######## RestResponse generic ######### - # ####################################### + def update_organisation_blocklist(self, organisation_blocklist: MISPOrganisationBlocklist, organisation_blocklist_id: int | str | UUID | None = None, pythonify: bool = False) -> dict[str, Any] | MISPOrganisationBlocklist: + """Update an organisation in the blocklist - def _rest_list(self, urlpath): - url = urljoin(self.root_url, urlpath) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def _rest_get_parameters(self, urlpath): - url = urljoin(self.root_url, '{}/add'.format(urlpath)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def _rest_view(self, urlpath, rest_id): - url = urljoin(self.root_url, '{}/view/{}'.format(urlpath, rest_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def _rest_add(self, urlpath, obj): - url = urljoin(self.root_url, '{}/add'.format(urlpath)) - response = self._prepare_request('POST', url, obj.to_json()) - return self._check_response(response) - - def _rest_edit(self, urlpath, obj, rest_id): - url = urljoin(self.root_url, '{}/edit/{}'.format(urlpath, rest_id)) - response = self._prepare_request('POST', url, obj.to_json()) - return self._check_response(response) - - def _rest_delete(self, urlpath, rest_id): - url = urljoin(self.root_url, '{}/delete/{}'.format(urlpath, rest_id)) - response = self._prepare_request('POST', url) - return self._check_response(response) - - # ########################### - # ######## Feed ######### - # ########################### - - def get_feeds_list(self): - """Get the content of all the feeds""" - return self._rest_list('feeds') - - def get_feed(self, feed_id): - """Get the content of a single feed""" - return self._rest_view('feeds', feed_id) - - def add_feed(self, source_format, url, name, input_source, provider, **kwargs): - """Delete a feed""" - new_feed = MISPFeed() - new_feed.from_dict(source_format=source_format, url=url, name=name, - input_source=input_source, provider=provider) - return self._rest_add('feeds', new_feed) - - def get_feed_fields_list(self): - return self._rest_get_parameters('feeds') - - def edit_feed(self, feed_id, **kwargs): - """Delete a feed""" - edit_feed = MISPFeed() - edit_feed.from_dict(**kwargs) - return self._rest_edit('feeds', edit_feed) - - def delete_feed(self, feed_id): - """Delete a feed""" - return self._rest_delete('feeds', feed_id) - - def fetch_feed(self, feed_id): - """Fetch one single feed""" - url = urljoin(self.root_url, 'feeds/fetchFromFeed/{}'.format(feed_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def cache_feeds_all(self): - """ Cache all the feeds""" - url = urljoin(self.root_url, 'feeds/cacheFeeds/all') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def cache_feed(self, feed_id): - """Cache a specific feed""" - url = urljoin(self.root_url, 'feeds/cacheFeeds/{}'.format(feed_id)) - response = self._prepare_request('GET', url) - return self._check_response(response) - - def cache_feeds_freetext(self): - """Cache all the freetext feeds""" - url = urljoin(self.root_url, 'feeds/cacheFeeds/freetext') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def cache_feeds_misp(self): - """Cache all the MISP feeds""" - url = urljoin(self.root_url, 'feeds/cacheFeeds/misp') - response = self._prepare_request('GET', url) - return self._check_response(response) - - def compare_feeds(self): - """Generate the comparison matrix for all the MISP feeds""" - url = urljoin(self.root_url, 'feeds/compareFeeds') - response = self._prepare_request('GET', url) - return self._check_response(response) - - @deprecated - def view_feed(self, feed_ids): - """Alias for get_feed""" - return self.get_feed(feed_ids) - - @deprecated - def view_feeds(self): - """Alias for get_feeds_list""" - return self.get_feeds_list() - - @deprecated - def cache_all_feeds(self): - """Alias for cache_feeds_all""" - return self.cache_feeds_all() - - # ###################### - # ### Sharing Groups ### - # ###################### - - def sharing_group_org_add(self, sharing_group, organisation, extend=False): - '''Add an organisation to a sharing group. - :sharing_group: Sharing group's local instance ID, or Sharing group's global UUID - :organisation: Organisation's local instance ID, or Organisation's global UUID, or Organisation's name as known to the curent instance - :extend: Allow the organisation to extend the group - ''' - to_jsonify = {'sg_id': sharing_group, 'org_id': organisation, 'extend': extend} - url = urljoin(self.root_url, 'sharingGroups/addOrg') - response = self._prepare_request('POST', url, json.dumps(to_jsonify)) - return self._check_response(response) - - def sharing_group_org_remove(self, sharing_group, organisation): - '''Remove an organisation from a sharing group. - :sharing_group: Sharing group's local instance ID, or Sharing group's global UUID - :organisation: Organisation's local instance ID, or Organisation's global UUID, or Organisation's name as known to the curent instance - ''' - to_jsonify = {'sg_id': sharing_group, 'org_id': organisation} - url = urljoin(self.root_url, 'sharingGroups/removeOrg') - response = self._prepare_request('POST', url, json.dumps(to_jsonify)) - return self._check_response(response) - - def sharing_group_server_add(self, sharing_group, server, all_orgs=False): - '''Add a server to a sharing group. - :sharing_group: Sharing group's local instance ID, or Sharing group's global UUID - :server: Server's local instance ID, or URL of the Server, or Server's name as known to the curent instance - :all_orgs: Add all the organisations of the server to the group - ''' - to_jsonify = {'sg_id': sharing_group, 'server_id': server, 'all_orgs': all_orgs} - url = urljoin(self.root_url, 'sharingGroups/addServer') - response = self._prepare_request('POST', url, json.dumps(to_jsonify)) - return self._check_response(response) - - def sharing_group_server_remove(self, sharing_group, server): - '''Remove a server from a sharing group. - :sharing_group: Sharing group's local instance ID, or Sharing group's global UUID - :server: Server's local instance ID, or URL of the Server, or Server's name as known to the curent instance - ''' - to_jsonify = {'sg_id': sharing_group, 'server_id': server} - url = urljoin(self.root_url, 'sharingGroups/removeServer') - response = self._prepare_request('POST', url, json.dumps(to_jsonify)) - return self._check_response(response) - - # ################### - # ### Objects ### - # ################### - - def add_object(self, event_id, *args, **kwargs): - """Add an object - :param event_id: Event ID of the event to attach the object to - :param template_id: Template ID of the template related to that event (not required) - :param misp_object: MISPObject to attach + :param organisation_blocklist: organization block list + :param organisation_blocklist_id: organization block lisd id + :param pythonify: Returns a PyMISP Object instead of the plain json output """ - # NOTE: this slightly fucked up thing is due to the fact template_id was required, and was the 2nd parameter. - template_id = kwargs.get('template_id') - misp_object = kwargs.get('misp_object') - if args: - if isinstance(args[0], MISPObject): - misp_object = args[0] + if organisation_blocklist_id is None: + oblid = get_uuid_or_id_from_abstract_misp(organisation_blocklist) + else: + oblid = get_uuid_or_id_from_abstract_misp(organisation_blocklist_id) + updated_organisation_blocklist = self._update_entries_in_blocklist('organisation', oblid, **organisation_blocklist) + if not (self.global_pythonify or pythonify) or 'errors' in updated_organisation_blocklist: + return updated_organisation_blocklist + o = MISPOrganisationBlocklist() + o.from_dict(**updated_organisation_blocklist) + return o + + def delete_event_blocklist(self, event_blocklist: MISPEventBlocklist | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a blocklisted event by id + + :param event_blocklist: event block list to delete + """ + event_blocklist_id = get_uuid_or_id_from_abstract_misp(event_blocklist) + response = self._prepare_request('POST', f'eventBlocklists/delete/{event_blocklist_id}') + return self._check_json_response(response) + + def delete_organisation_blocklist(self, organisation_blocklist: MISPOrganisationBlocklist | str | UUID) -> dict[str, Any] | list[dict[str, Any]]: + """Delete a blocklisted organisation by id + + :param organisation_blocklist: organization block list to delete + """ + org_blocklist_id = get_uuid_or_id_from_abstract_misp(organisation_blocklist) + response = self._prepare_request('POST', f'orgBlocklists/delete/{org_blocklist_id}') + return self._check_json_response(response) + + # ## END Blocklists ### + + # ## BEGIN Global helpers ### + + def change_sharing_group_on_entity(self, misp_entity: MISPEvent | MISPAttribute | MISPObject, + sharing_group_id: int, pythonify: bool = False) -> dict[str, Any] | MISPEvent | MISPObject | MISPAttribute | MISPShadowAttribute: + """Change the sharing group of an event, an attribute, or an object + + :param misp_entity: entity to change + :param sharing_group_id: group to change + :param pythonify: Returns a PyMISP Object instead of the plain json output + """ + misp_entity.distribution = 4 # Needs to be 'Sharing group' + if 'SharingGroup' in misp_entity: # Delete former SharingGroup information + del misp_entity.SharingGroup + misp_entity.sharing_group_id = sharing_group_id # Set new sharing group id + if isinstance(misp_entity, MISPEvent): + return self.update_event(misp_entity, pythonify=pythonify) + + if isinstance(misp_entity, MISPObject): + return self.update_object(misp_entity, pythonify=pythonify) + + if isinstance(misp_entity, MISPAttribute): + return self.update_attribute(misp_entity, pythonify=pythonify) + + raise PyMISPError('The misp_entity must be MISPEvent, MISPObject or MISPAttribute') + + def tag(self, misp_entity: AbstractMISP | str | dict[str, Any], tag: MISPTag | str | dict[str, Any], + local: bool = False, relationship_type: str | None = None) -> dict[str, Any] | list[dict[str, Any]]: + """Tag an event or an attribute. + + :param misp_entity: a MISPEvent, a MISP Attribute, or a UUID + :param tag: tag to add + :param local: whether to tag locally + :param relationship_type: Type of relationship between the tag and the attribute or event + """ + uuid = get_uuid_or_id_from_abstract_misp(misp_entity) + if isinstance(tag, MISPTag): + tag_name = tag.name if 'name' in tag else "" + elif isinstance(tag, dict): + tag_name = tag.get('name', '') + else: + tag_name = tag + if not tag_name: + raise PyMISPError('tag must be a MISPTag object, a dict with a name key, or a string, and it cannot be empty.') + to_post = {'uuid': uuid, 'tag': tag_name, 'local': local} + if relationship_type: + to_post['relationship_type'] = relationship_type + response = self._prepare_request('POST', 'tags/attachTagToObject', data=to_post) + return self._check_json_response(response) + + def untag(self, misp_entity: AbstractMISP | str | dict[str, Any], tag: MISPTag | str) -> dict[str, Any] | list[dict[str, Any]]: + """Untag an event or an attribute + + :param misp_entity: misp_entity can be a UUID + :param tag: tag to remove + """ + uuid = get_uuid_or_id_from_abstract_misp(misp_entity) + if isinstance(tag, MISPTag): + tag_name = tag.name if 'name' in tag else "" + else: + tag_name = tag + to_post = {'uuid': uuid, 'tag': tag_name} + response = self._prepare_request('POST', 'tags/removeTagFromObject', data=to_post) + return self._check_json_response(response) + + def build_complex_query(self, or_parameters: list[SearchType] | None = None, + and_parameters: list[SearchType] | None = None, + not_parameters: list[SearchType] | None = None) -> dict[str, list[SearchType]]: + '''Build a complex search query. MISP expects a dictionary with AND, OR and NOT keys.''' + to_return = {} + if and_parameters: + if isinstance(and_parameters, str): + to_return['AND'] = [and_parameters] else: - template_id = args[0] - misp_object = args[1] + to_return['AND'] = [p for p in and_parameters if p] + if not_parameters: + if isinstance(not_parameters, str): + to_return['NOT'] = [not_parameters] + else: + to_return['NOT'] = [p for p in not_parameters if p] + if or_parameters: + if isinstance(or_parameters, str): + to_return['OR'] = [or_parameters] + else: + to_return['OR'] = [p for p in or_parameters if p] + return to_return - if template_id is not None: - url = urljoin(self.root_url, 'objects/add/{}/{}'.format(event_id, template_id)) + # ## END Global helpers ### + + # ## MISP internal tasks ### + + def get_all_functions(self, not_implemented: bool = False) -> list[str]: + '''Get all methods available via the API, including ones that are not implemented.''' + response = self._prepare_request('GET', 'servers/queryACL/printAllFunctionNames') + functions = self._check_json_response(response) + # Format as URLs + paths = [] + for controller, methods in functions.items(): + if controller == '*': + continue + for method in methods: + if method.startswith('admin_'): + path = f'admin/{controller}/{method[6:]}' + else: + path = f'{controller}/{method}' + paths.append(path) + + if not not_implemented: + return [str(path)] + + with open(__file__) as f: + content = f.read() + + not_implemented_paths: list[str] = [] + for path in paths: + if path not in content: + not_implemented_paths.append(path) + + return not_implemented_paths + + # ## Internal methods ### + + def _old_misp(self, minimal_version_required: tuple[int], removal_date: str | date | datetime, method: str | None = None, message: str | None = None) -> bool: + if self._misp_version >= minimal_version_required: + return False + if isinstance(removal_date, (datetime, date)): + removal_date = removal_date.isoformat() + to_print = f'The instance of MISP you are using is outdated. Unless you update your MISP instance, {method} will stop working after {removal_date}.' + if message: + to_print += f' {message}' + warnings.warn(to_print, DeprecationWarning) + return True + + def _make_misp_bool(self, parameter: bool | str | None = None) -> int: + '''MISP wants 0 or 1 for bool, so we avoid True/False '0', '1' ''' + if parameter is None: + return 0 + return 1 if int(parameter) else 0 + + def _make_timestamp(self, value: datetime | date | int | str | float | None) -> str | int | float | None: + '''Catch-all method to normalize anything that can be converted to a timestamp''' + if not value: + return None + if isinstance(value, datetime): + return value.timestamp() + + if isinstance(value, date): + return datetime.combine(value, datetime.max.time()).timestamp() + + if isinstance(value, str): + if value.isdigit(): + return value + try: + float(value) + return value + except ValueError: + # The value can also be '1d', '10h', ... + return value + return value + + def _check_json_response(self, response: requests.Response) -> dict[str, Any] | list[dict[str, Any]]: + r = self._check_response(response, expect_json=True) + if isinstance(r, (dict, list)): + return r + # Else: an exception was raised anyway + raise PyMISPUnexpectedResponse(f'A dict was expected, got a string: {r}') + + def _check_head_response(self, response: requests.Response) -> bool: + if response.status_code == 200: + return True + elif response.status_code == 404: + return False else: - url = urljoin(self.root_url, 'objects/add/{}'.format(event_id)) - response = self._prepare_request('POST', url, misp_object.to_json()) - return self._check_response(response) + raise MISPServerError(f'Error code {response.status_code} for HEAD request') - def edit_object(self, misp_object, object_id=None): - """Edit an existing object""" - if object_id: - param = object_id - elif hasattr(misp_object, 'uuid'): - param = misp_object.uuid - elif hasattr(misp_object, 'id'): - param = misp_object.id - else: - raise PyMISPError('In order to update an object, you have to provide an object ID (either in the misp_object, or as a parameter)') - url = urljoin(self.root_url, 'objects/edit/{}'.format(param)) - response = self._prepare_request('POST', url, misp_object.to_json()) - return self._check_response(response) + def _check_response(self, response: requests.Response, lenient_response_type: bool = False, expect_json: bool = False) -> dict[str, Any] | str: + """Check if the response from the server is not an unexpected error""" + if response.status_code >= 500: + headers_without_auth = {h_name: h_value for h_name, h_value in response.request.headers.items() if h_value != self.key} + if logger.level == logging.DEBUG: + logger.debug(everything_broken.format(headers_without_auth, response.request.body, response.text)) + else: + logger.critical(everything_broken.format(headers_without_auth, response.request.body, f'{response.text[:1000]}... (enable debug mode for more details)')) + raise MISPServerError(f'Error code 500:\n{response.text}') - def delete_object(self, id): - """Deletes an object""" - url = urljoin(self.root_url, 'objects/delete/{}'.format(id)) - response = self._prepare_request('POST', url) - return self._check_response(response) + if 400 <= response.status_code < 500: + # The server returns a json message with the error details + try: + error_message = loads(response.content) + except Exception: + raise MISPServerError(f'Error code {response.status_code}:\n{response.text}') - def add_object_reference(self, misp_object_reference): - """Add a reference to an object""" - url = urljoin(self.root_url, 'object_references/add') - response = self._prepare_request('POST', url, misp_object_reference.to_json()) - return self._check_response(response) + logger.error(f'Something went wrong ({response.status_code}): {error_message}') + return {'errors': (response.status_code, error_message)} - def delete_object_reference(self, id): - """Deletes a reference to an object""" - url = urljoin(self.root_url, 'object_references/delete/{}'.format(id)) - response = self._prepare_request('POST', url) - return self._check_response(response) + # At this point, we had no error. - def get_object_templates_list(self): - """Returns the list of Object templates available on the MISP instance""" - url = urljoin(self.root_url, 'objectTemplates') - response = self._prepare_request('GET', url) - return self._check_response(response) + try: + response_json = loads(response.content) + logger.debug(response_json) + if isinstance(response_json, dict) and response_json.get('response') is not None: + # Cleanup. + response_json = response_json['response'] + return response_json + except Exception: + logger.debug(response.text) + if expect_json: + error_msg = f'Unexpected response (size: {len(response.text)}) from server: {response.text}' + raise PyMISPUnexpectedResponse(error_msg) + if lenient_response_type and not response.headers['Content-Type'].startswith('application/json'): + return response.text + if not response.content: + # Empty response + logger.error('Got an empty response.') + return {'errors': 'The response is empty.'} + return response.text - def get_object_template(self, object_uuid): - """Gets the full object template corresponting the UUID passed as parameter""" - url = urljoin(self.root_url, 'objectTemplates/view/{}'.format(object_uuid)) - response = self._prepare_request('GET', url) - return self._check_response(response) - if 'ObjectTemplate' in response: - return response['ObjectTemplate']['id'] - return response + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(url={self.root_url})' - def get_object_template_id(self, object_uuid): - """Gets the template ID corresponting the UUID passed as parameter""" - template = self.get_object_template(object_uuid) - if 'ObjectTemplate' in template: - return template['ObjectTemplate']['id'] - # Contains the error message. - return template + def _prepare_request(self, request_type: str, url: str, data: Iterable[Any] | Mapping[str, Any] | AbstractMISP | bytes | None = None, + params: Mapping[str, Any] = {}, kw_params: Mapping[str, Any] = {}, + output_type: str = 'json', content_type: str = 'json') -> requests.Response: + '''Prepare a request for python-requests''' + if url[0] == '/': + # strip it: it will fail if MISP is in a sub directory + url = url[1:] + # Cake PHP being an idiot, it doesn't accept %20 (space) in the URL path, + # so we need to make it a + instead and hope for the best + url = url.replace(' ', '+') + url = urljoin(self.root_url, url) + d: bytes | str | None = None + if data is not None: + if isinstance(data, bytes): + d = data + else: + if isinstance(data, dict): + # Remove None values. + data = {k: v for k, v in data.items() if v is not None} + d = dumps(data, default=pymisp_json_default) - def update_object_templates(self): - url = urljoin(self.root_url, 'objectTemplates/update') - response = self._prepare_request('POST', url) - return self._check_response(response) + logger.debug('%s - %s', request_type, url) + if d is not None: + logger.debug(d) - # ########################### - # ####### Deprecated ######## - # ########################### + if kw_params: + # CakePHP params in URL + to_append_url = '/'.join([f'{k}:{v}' for k, v in kw_params.items()]) + url = f'{url}/{to_append_url}' - @deprecated - def add_tag(self, event, tag, attribute=False): - if attribute: - to_post = {'request': {'Attribute': {'id': event['id'], 'tag': tag}}} - path = 'attributes/addTag' - else: - # Allow for backwards-compat with old style - if "Event" in event: - event = event["Event"] - to_post = {'request': {'Event': {'id': event['id'], 'tag': tag}}} - path = 'events/addTag' - url = urljoin(self.root_url, path) - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) + req = requests.Request(request_type, url, data=d, params=params) + req.auth = self.auth + prepped = self.__session.prepare_request(req) + prepped.headers.update( + {'Accept': f'application/{output_type}', + 'content-type': f'application/{content_type}'}) + logger.debug(prepped.headers) + settings = self.__session.merge_environment_settings(req.url, proxies=self.proxies or {}, stream=None, + verify=self.ssl, cert=self.cert) + return self.__session.send(prepped, timeout=self.timeout, **settings) - @deprecated - def remove_tag(self, event, tag, attribute=False): - if attribute: - to_post = {'request': {'Attribute': {'id': event['id'], 'tag': tag}}} - path = 'attributes/removeTag' - else: - to_post = {'request': {'Event': {'id': event['Event']['id'], 'tag': tag}}} - path = 'events/removeTag' - url = urljoin(self.root_url, path) - response = self._prepare_request('POST', url, json.dumps(to_post)) - return self._check_response(response) + def _csv_to_dict(self, csv_content: str) -> list[dict[str, Any]]: + '''Makes a list of dict out of a csv file (requires headers)''' + fieldnames, lines = csv_content.split('\n', 1) + fields = fieldnames.split(',') + to_return = [] + for line in csv.reader(lines.split('\n')): + if line: + to_return.append({fname: value for fname, value in zip(fields, line)}) + return to_return diff --git a/pymisp/aping.py b/pymisp/aping.py deleted file mode 100644 index a366511..0000000 --- a/pymisp/aping.py +++ /dev/null @@ -1,501 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from .exceptions import MISPServerError, NewEventError, UpdateEventError, UpdateAttributeError, PyMISPNotImplementedYet -from .api import PyMISP, everything_broken -from .mispevent import MISPEvent, MISPAttribute, MISPSighting, MISPLog, MISPObject -from typing import TypeVar, Optional, Tuple, List, Dict, Union -from datetime import date, datetime -import csv - -import logging -from urllib.parse import urljoin - -SearchType = TypeVar('SearchType', str, int) -# str: string to search / list: values to search (OR) / dict: {'OR': [list], 'NOT': [list], 'AND': [list]} -SearchParameterTypes = TypeVar('SearchParameterTypes', str, List[SearchType], Dict[str, SearchType]) -DateTypes = TypeVar('DateTypes', datetime, date, SearchType, float) -DateInterval = TypeVar('DateInterval', DateTypes, Tuple[DateTypes, DateTypes]) - -ToIDSType = TypeVar('ToIDSType', str, int, bool) - -logger = logging.getLogger('pymisp') - - -class ExpandedPyMISP(PyMISP): - - def build_complex_query(self, or_parameters: Optional[List[SearchType]]=None, - and_parameters: Optional[List[SearchType]]=None, - not_parameters: Optional[List[SearchType]]=None): - to_return = {} - if and_parameters: - to_return['AND'] = and_parameters - if not_parameters: - to_return['NOT'] = not_parameters - if or_parameters: - to_return['OR'] = or_parameters - return to_return - - def toggle_warninglist(self, warninglist_id: List[int]=None, warninglist_name: List[str]=None, force_enable: bool=None): - '''Toggle (enable/disable) the status of a warninglist by ID. - :param warninglist_id: ID of the WarningList - :param force_enable: Force the warning list in the enabled state (does nothing is already enabled) - ''' - return super().toggle_warninglist(warninglist_id, warninglist_name, force_enable) - - def make_timestamp(self, value: DateTypes): - if isinstance(value, datetime): - return datetime.timestamp() - elif isinstance(value, date): - return datetime.combine(value, datetime.max.time()).timestamp() - elif isinstance(value, str): - if value.isdigit(): - return value - else: - try: - float(value) - return value - except ValueError: - # The value can also be '1d', '10h', ... - return value - else: - return value - - def _check_response(self, response): - """Check if the response from the server is not an unexpected error""" - if response.status_code >= 500: - logger.critical(everything_broken.format(response.request.headers, response.request.body, response.text)) - raise MISPServerError('Error code 500:\n{}'.format(response.text)) - elif 400 <= response.status_code < 500: - # The server returns a json message with the error details - error_message = response.json() - logger.error(f'Something went wrong ({response.status_code}): {error_message}') - return {'errors': (response.status_code, error_message)} - - # At this point, we had no error. - - try: - response = response.json() - if logger.isEnabledFor(logging.DEBUG): - logger.debug(response) - if isinstance(response, dict) and response.get('response') is not None: - # Cleanup. - response = response['response'] - return response - except Exception: - if logger.isEnabledFor(logging.DEBUG): - logger.debug(response.text) - if not len(response.content): - # Empty response - logger.error('Got an empty response.') - return {'errors': 'The response is empty.'} - return response.text - - def get_event(self, event_id: int): - event = super().get_event(event_id) - e = MISPEvent() - e.load(event) - return e - - def add_object(self, event_id: int, misp_object: MISPObject): - created_object = super().add_object(event_id, misp_object) - if isinstance(created_object, str): - raise NewEventError(f'Unexpected response from server: {created_object}') - elif 'errors' in created_object: - return created_object - o = MISPObject(misp_object.name) - o.from_dict(**created_object) - return o - - def add_event(self, event: MISPEvent): - created_event = super().add_event(event) - if isinstance(created_event, str): - raise NewEventError(f'Unexpected response from server: {created_event}') - elif 'errors' in created_event: - return created_event - e = MISPEvent() - e.load(created_event) - return e - - def update_event(self, event: MISPEvent): - updated_event = super().update_event(event.uuid, event) - if isinstance(updated_event, str): - raise UpdateEventError(f'Unexpected response from server: {updated_event}') - elif 'errors' in updated_event: - return updated_event - e = MISPEvent() - e.load(updated_event) - return e - - def update_attribute(self, attribute: MISPAttribute): - updated_attribute = super().update_attribute(attribute.uuid, attribute) - if isinstance(updated_attribute, str): - raise UpdateAttributeError(f'Unexpected response from server: {updated_attribute}') - elif 'errors' in updated_attribute: - return updated_attribute - a = MISPAttribute() - a.from_dict(**updated_attribute) - return a - - def search_sightings(self, context: Optional[str]=None, - context_id: Optional[SearchType]=None, - type_sighting: Optional[str]=None, - date_from: Optional[DateTypes]=None, - date_to: Optional[DateTypes]=None, - publish_timestamp: Optional[DateInterval]=None, last: Optional[DateInterval]=None, - org: Optional[SearchType]=None, - source: Optional[str]=None, - include_attribute: Optional[bool]=None, - include_event_meta: Optional[bool]=None, - pythonify: Optional[bool]=False - ): - '''Search sightings - - :param context: The context of the search. Can be either "attribute", "event", or nothing (will then match on events and attributes). - :param context_id: Only relevant if context is either "attribute" or "event". Then it is the relevant ID. - :param type_sighting: Type of sighting - :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. - :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. - :param publish_timestamp: Restrict the results by the last publish timestamp (newer than). - :param org: Search by the creator organisation by supplying the organisation identifier. - :param source: Source of the sighting - :param include_attribute: Include the attribute. - :param include_event_meta: Include the meta information of the event. - - Deprecated: - - :param last: synonym for publish_timestamp - - :Example: - - >>> misp.search_sightings(publish_timestamp='30d') # search sightings for the last 30 days on the instance - [ ... ] - >>> misp.search_sightings(context='attribute', context_id=6, include_attribute=True) # return list of sighting for attribute 6 along with the attribute itself - [ ... ] - >>> misp.search_sightings(context='event', context_id=17, include_event_meta=True, org=2) # return list of sighting for event 17 filtered with org id 2 - ''' - query = {'returnFormat': 'json'} - if context is not None: - if context not in ['attribute', 'event']: - raise ValueError('context has to be in {}'.format(', '.join(['attribute', 'event']))) - url_path = f'sightings/restSearch/{context}' - else: - url_path = 'sightings/restSearch' - query['id'] = context_id - query['type'] = type_sighting - query['from'] = date_from - query['to'] = date_to - query['last'] = publish_timestamp - query['org_id'] = org - query['source'] = source - query['includeAttribute'] = include_attribute - query['includeEvent'] = include_event_meta - - url = urljoin(self.root_url, url_path) - response = self._prepare_request('POST', url, data=query) - normalized_response = self._check_response(response) - if isinstance(normalized_response, str) or (isinstance(normalized_response, dict) - and normalized_response.get('errors')): - return normalized_response - elif pythonify: - to_return = [] - for s in normalized_response: - entries = {} - s_data = s['Sighting'] - if include_event_meta: - e = s_data.pop('Event') - me = MISPEvent() - me.from_dict(**e) - entries['event'] = me - if include_attribute: - a = s_data.pop('Attribute') - ma = MISPAttribute() - ma.from_dict(**a) - entries['attribute'] = ma - ms = MISPSighting() - ms.from_dict(**s_data) - entries['sighting'] = ms - to_return.append(entries) - return to_return - else: - return normalized_response - - def search(self, controller: str='events', return_format: str='json', - limit: Optional[int]=None, page: Optional[int]=None, - value: Optional[SearchParameterTypes]=None, - type_attribute: Optional[SearchParameterTypes]=None, - category: Optional[SearchParameterTypes]=None, - org: Optional[SearchParameterTypes]=None, - tags: Optional[SearchParameterTypes]=None, - quick_filter: Optional[str]=None, quickFilter: Optional[str]=None, - date_from: Optional[DateTypes]=None, - date_to: Optional[DateTypes]=None, - eventid: Optional[SearchType]=None, - with_attachments: Optional[bool]=None, withAttachments: Optional[bool]=None, - metadata: Optional[bool]=None, - uuid: Optional[str]=None, - publish_timestamp: Optional[DateInterval]=None, last: Optional[DateInterval]=None, - timestamp: Optional[DateInterval]=None, - published: Optional[bool]=None, - enforce_warninglist: Optional[bool]=None, enforceWarninglist: Optional[bool]=None, - to_ids: Optional[Union[ToIDSType, List[ToIDSType]]]=None, - deleted: Optional[str]=None, - include_event_uuid: Optional[str]=None, includeEventUuid: Optional[str]=None, - event_timestamp: Optional[DateTypes]=None, - sg_reference_only: Optional[bool]=None, - eventinfo: Optional[str]=None, - searchall: Optional[bool]=None, - requested_attributes: Optional[str]=None, - include_context: Optional[bool]=None, includeContext: Optional[bool]=None, - headerless: Optional[bool]=None, - pythonify: Optional[bool]=False, - **kwargs): - '''Search in the MISP instance - - :param returnFormat: Set the return format of the search (Currently supported: json, xml, openioc, suricata, snort - more formats are being moved to restSearch with the goal being that all searches happen through this API). Can be passed as the first parameter after restSearch or via the JSON payload. - :param limit: Limit the number of results returned, depending on the scope (for example 10 attributes or 10 full events). - :param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300). - :param value: Search for the given value in the attributes' value field. - :param type_attribute: The attribute type, any valid MISP attribute type is accepted. - :param category: The attribute category, any valid MISP attribute category is accepted. - :param org: Search by the creator organisation by supplying the organisation identifier. - :param tags: Tags to search or to exclude. You can pass a list, or the output of `build_complex_query` - :param quick_filter: The string passed to this field will ignore all of the other arguments. MISP will return an xml / json (depending on the header sent) of all events that have a sub-string match on value in the event info, event orgc, or any of the attribute value1 / value2 fields, or in the attribute comment. - :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. - :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. - :param eventid: The events that should be included / excluded from the search - :param with_attachments: If set, encodes the attachments / zipped malware samples as base64 in the data field within each attribute - :param metadata: Only the metadata (event, tags, relations) is returned, attributes and proposals are omitted. - :param uuid: Restrict the results by uuid. - :param publish_timestamp: Restrict the results by the last publish timestamp (newer than). - :param timestamp: Restrict the results by the timestamp (last edit). Any event with a timestamp newer than the given timestamp will be returned. In case you are dealing with /attributes as scope, the attribute's timestamp will be used for the lookup. - :param published: Set whether published or unpublished events should be returned. Do not set the parameter if you want both. - :param enforce_warninglist: Remove any attributes from the result that would cause a hit on a warninglist entry. - :param to_ids: By default all attributes are returned that match the other filter parameters, irregardless of their to_ids setting. To restrict the returned data set to to_ids only attributes set this parameter to 1. 0 for the ones with to_ids set to False. - :param deleted: If this parameter is set to 1, it will return soft-deleted attributes along with active ones. By using "only" as a parameter it will limit the returned data set to soft-deleted data only. - :param include_event_uuid: Instead of just including the event ID, also include the event UUID in each of the attributes. - :param event_timestamp: Only return attributes from events that have received a modification after the given timestamp. - :param sg_reference_only: If this flag is set, sharing group objects will not be included, instead only the sharing group ID is set. - :param eventinfo: Filter on the event's info field. - :param searchall: Search for a full or a substring (delimited by % for substrings) in the event info, event tags, attribute tags, attribute values or attribute comment fields. - :param requested_attributes: [CSV only] Select the fields that you wish to include in the CSV export. By setting event level fields additionally, includeContext is not required to get event metadata. - :param include_context: [CSV Only] Include the event data with each attribute. - :param headerless: [CSV Only] The CSV created when this setting is set to true will not contain the header row. - :param pythonify: Returns a list of PyMISP Objects instead of the plain json output. Warning: it might use a lot of RAM - - Deprecated: - - :param quickFilter: synponym for quick_filter - :param withAttachments: synonym for with_attachments - :param last: synonym for publish_timestamp - :param enforceWarninglist: synonym for enforce_warninglist - :param includeEventUuid: synonym for include_event_uuid - :param includeContext: synonym for include_context - - ''' - - return_formats = ['openioc', 'json', 'xml', 'suricata', 'snort', 'text', 'rpz', 'csv', 'cache', 'stix', 'stix2'] - - if controller not in ['events', 'attributes', 'objects', 'sightings']: - raise ValueError('controller has to be in {}'.format(', '.join(['events', 'attributes', 'objects']))) - - # Deprecated stuff / synonyms - if quickFilter is not None: - quick_filter = quickFilter - if withAttachments is not None: - with_attachments = withAttachments - if last is not None: - publish_timestamp = last - if enforceWarninglist is not None: - enforce_warninglist = enforceWarninglist - if includeEventUuid is not None: - include_event_uuid = includeEventUuid - if includeContext is not None: - include_context = includeContext - - # Add all the parameters in kwargs are aimed at modules, or other 3rd party components, and cannot be sanitized. - # They are passed as-is. - query = kwargs - - if return_format not in return_formats: - raise ValueError('return_format has to be in {}'.format(', '.join(return_formats))) - query['returnFormat'] = return_format - - query['page'] = page - query['limit'] = limit - query['value'] = value - query['type'] = type_attribute - query['category'] = category - query['org'] = org - query['tags'] = tags - query['quickFilter'] = quick_filter - query['from'] = self.make_timestamp(date_from) - query['to'] = self.make_timestamp(date_to) - query['eventid'] = eventid - query['withAttachments'] = with_attachments - query['metadata'] = metadata - query['uuid'] = uuid - if publish_timestamp is not None: - if isinstance(publish_timestamp, (list, tuple)): - query['publish_timestamp'] = (self.make_timestamp(publish_timestamp[0]), self.make_timestamp(publish_timestamp[1])) - else: - query['publish_timestamp'] = self.make_timestamp(publish_timestamp) - if timestamp is not None: - if isinstance(timestamp, (list, tuple)): - query['timestamp'] = (self.make_timestamp(timestamp[0]), self.make_timestamp(timestamp[1])) - else: - query['timestamp'] = self.make_timestamp(timestamp) - query['published'] = published - query['enforceWarninglist'] = enforce_warninglist - if to_ids is not None: - if int(to_ids) not in [0, 1]: - raise ValueError('to_ids has to be in {}'.format(', '.join([0, 1]))) - query['to_ids'] = to_ids - query['deleted'] = deleted - query['includeEventUuid'] = include_event_uuid - if event_timestamp is not None: - if isinstance(event_timestamp, (list, tuple)): - query['event_timestamp'] = (self.make_timestamp(event_timestamp[0]), self.make_timestamp(event_timestamp[1])) - else: - query['event_timestamp'] = self.make_timestamp(event_timestamp) - query['sgReferenceOnly'] = sg_reference_only - query['eventinfo'] = eventinfo - query['searchall'] = searchall - query['requested_attributes'] = requested_attributes - query['includeContext'] = include_context - query['headerless'] = headerless - url = urljoin(self.root_url, f'{controller}/restSearch') - response = self._prepare_request('POST', url, data=query) - normalized_response = self._check_response(response) - if return_format == 'csv' and pythonify and not headerless: - return self._csv_to_dict(normalized_response) - elif isinstance(normalized_response, str) or (isinstance(normalized_response, dict) - and normalized_response.get('errors')): - return normalized_response - elif return_format == 'json' and pythonify: - # The response is in json, we can convert it to a list of pythonic MISP objects - to_return = [] - if controller == 'events': - for e in normalized_response: - me = MISPEvent() - me.load(e) - to_return.append(me) - elif controller == 'attributes': - for a in normalized_response.get('Attribute'): - ma = MISPAttribute() - ma.from_dict(**a) - to_return.append(ma) - elif controller == 'objects': - raise PyMISPNotImplementedYet('Not implemented yet') - return to_return - else: - return normalized_response - - def _csv_to_dict(self, csv_content): - '''Makes a list of dict out of a csv file (requires headers)''' - fieldnames, lines = csv_content.split('\n', 1) - fieldnames = fieldnames.split(',') - to_return = [] - for line in csv.reader(lines.split('\n')): - if line: - to_return.append({fname: value for fname, value in zip(fieldnames, line)}) - return to_return - - def search_logs(self, limit: Optional[int]=None, page: Optional[int]=None, - log_id: Optional[int]=None, title: Optional[str]=None, - created: Optional[DateTypes]=None, model: Optional[str]=None, - action: Optional[str]=None, user_id: Optional[int]=None, - change: Optional[str]=None, email: Optional[str]=None, - org: Optional[str]=None, description: Optional[str]=None, - ip: Optional[str]=None, pythonify: Optional[bool]=False): - '''Search in logs - - Note: to run substring queries simply append/prepend/encapsulate the search term with % - - :param limit: Limit the number of results returned, depending on the scope (for example 10 attributes or 10 full events). - :param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300). - :param log_id: Log ID - :param title: Log Title - :param created: Creation timestamp - :param model: Model name that generated the log entry - :param action: The thing that was done - :param user_id: ID of the user doing the action - :param change: Change that occured - :param email: Email of the user - :param org: Organisation of the User doing the action - :param description: Description of the action - :param ip: Origination IP of the User doing the action - :param pythonify: Returns a list of PyMISP Objects instead or the plain json output. Warning: it might use a lot of RAM - ''' - query = locals() - query.pop('self') - query.pop('pythonify') - if log_id is not None: - query['id'] = query.pop('log_id') - - url = urljoin(self.root_url, 'admin/logs/index') - response = self._prepare_request('POST', url, data=query) - normalized_response = self._check_response(response) - if not pythonify: - return normalized_response - - to_return = [] - for l in normalized_response: - ml = MISPLog() - ml.from_dict(**l['Log']) - to_return.append(ml) - return to_return - - def search_index(self, published: Optional[bool]=None, eventid: Optional[SearchType]=None, - tags: Optional[SearchParameterTypes]=None, - date_from: Optional[DateTypes]=None, - date_to: Optional[DateTypes]=None, - eventinfo: Optional[str]=None, - threatlevel: Optional[List[SearchType]]=None, - distribution: Optional[List[SearchType]]=None, - analysis: Optional[List[SearchType]]=None, - org: Optional[SearchParameterTypes]=None, - timestamp: Optional[DateInterval]=None, - pythonify: Optional[bool]=None): - """Search only at the index level. Using ! in front of a value means NOT (default is OR) - - :param published: Set whether published or unpublished events should be returned. Do not set the parameter if you want both. - :param eventid: The events that should be included / excluded from the search - :param tags: Tags to search or to exclude. You can pass a list, or the output of `build_complex_query` - :param date_from: Events with the date set to a date after the one specified. This filter will use the date of the event. - :param date_to: Events with the date set to a date before the one specified. This filter will use the date of the event. - :param eventinfo: Filter on the event's info field. - :param threatlevel: Threat level(s) (1,2,3,4) | list - :param distribution: Distribution level(s) (0,1,2,3) | list - :param analysis: Analysis level(s) (0,1,2) | list - :param org: Search by the creator organisation by supplying the organisation identifier. - :param timestamp: Restrict the results by the timestamp (last edit). Any event with a timestamp newer than the given timestamp will be returned. In case you are dealing with /attributes as scope, the attribute's timestamp will be used for the lookup. - :param pythonify: Returns a list of PyMISP Objects instead or the plain json output. Warning: it might use a lot of RAM - """ - query = locals() - query.pop('self') - query.pop('pythonify') - if query.get('date_from'): - query['datefrom'] = self.make_timestamp(query.pop('date_from')) - if query.get('date_to'): - query['dateuntil'] = self.make_timestamp(query.pop('date_to')) - - if query.get('timestamp') is not None: - timestamp = query.pop('timestamp') - if isinstance(timestamp, (list, tuple)): - query['timestamp'] = (self.make_timestamp(timestamp[0]), self.make_timestamp(timestamp[1])) - else: - query['timestamp'] = self.make_timestamp(timestamp) - - url = urljoin(self.root_url, 'events/index') - response = self._prepare_request('POST', url, data=query) - normalized_response = self._check_response(response) - - if not pythonify: - return normalized_response - to_return = [] - for e_meta in normalized_response: - me = MISPEvent() - me.from_dict(**e_meta) - to_return.append(me) - return to_return diff --git a/pymisp/data/describeTypes.json b/pymisp/data/describeTypes.json index 71f4fea..252fde3 100644 --- a/pymisp/data/describeTypes.json +++ b/pymisp/data/describeTypes.json @@ -1,1238 +1,1496 @@ { - "result": { - "sane_defaults": { - "md5": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha1": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha256": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "pdb": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "filename|md5": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha1": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha256": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "ip-src": { - "default_category": "Network activity", - "to_ids": 1 - }, - "ip-dst": { - "default_category": "Network activity", - "to_ids": 1 - }, - "hostname": { - "default_category": "Network activity", - "to_ids": 1 - }, - "domain": { - "default_category": "Network activity", - "to_ids": 1 - }, - "domain|ip": { - "default_category": "Network activity", - "to_ids": 1 - }, - "email-src": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "email-dst": { - "default_category": "Network activity", - "to_ids": 1 - }, - "email-subject": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-attachment": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "email-body": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "float": { - "default_category": "Other", - "to_ids": 0 - }, - "url": { - "default_category": "Network activity", - "to_ids": 1 - }, - "http-method": { - "default_category": "Network activity", - "to_ids": 0 - }, - "user-agent": { - "default_category": "Network activity", - "to_ids": 0 - }, - "ja3-fingerprint-md5": { - "default_category": "Network activity", - "to_ids": 1 - }, - "hassh-md5": { - "default_category": "Network activity", - "to_ids": 1 - }, - "hasshserver-md5": { - "default_category": "Network activity", - "to_ids": 1 - }, - "regkey": { - "default_category": "Persistence mechanism", - "to_ids": 1 - }, - "regkey|value": { - "default_category": "Persistence mechanism", - "to_ids": 1 - }, - "AS": { - "default_category": "Network activity", - "to_ids": 0 - }, - "snort": { - "default_category": "Network activity", - "to_ids": 1 - }, - "bro": { - "default_category": "Network activity", - "to_ids": 1 - }, - "zeek": { - "default_category": "Network activity", - "to_ids": 1 - }, - "pattern-in-file": { - "default_category": "Payload installation", - "to_ids": 1 - }, - "pattern-in-traffic": { - "default_category": "Network activity", - "to_ids": 1 - }, - "pattern-in-memory": { - "default_category": "Payload installation", - "to_ids": 1 - }, - "yara": { - "default_category": "Payload installation", - "to_ids": 1 - }, - "stix2-pattern": { - "default_category": "Payload installation", - "to_ids": 1 - }, - "sigma": { - "default_category": "Payload installation", - "to_ids": 1 - }, - "gene": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "mime-type": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "identity-card-number": { - "default_category": "Person", - "to_ids": 0 - }, - "cookie": { - "default_category": "Network activity", - "to_ids": 0 - }, - "vulnerability": { - "default_category": "External analysis", - "to_ids": 0 - }, - "attachment": { - "default_category": "External analysis", - "to_ids": 0 - }, - "malware-sample": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "link": { - "default_category": "External analysis", - "to_ids": 0 - }, - "comment": { - "default_category": "Other", - "to_ids": 0 - }, - "text": { - "default_category": "Other", - "to_ids": 0 - }, - "hex": { - "default_category": "Other", - "to_ids": 0 - }, - "other": { - "default_category": "Other", - "to_ids": 0 - }, - "named pipe": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "mutex": { - "default_category": "Artifacts dropped", - "to_ids": 1 - }, - "target-user": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "target-email": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "target-machine": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "target-org": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "target-location": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "target-external": { - "default_category": "Targeting data", - "to_ids": 0 - }, - "btc": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "xmr": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "iban": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "bic": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "bank-account-nr": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "aba-rtn": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "bin": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "cc-number": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "prtn": { - "default_category": "Financial fraud", - "to_ids": 1 - }, - "phone-number": { - "default_category": "Person", - "to_ids": 0 - }, - "threat-actor": { - "default_category": "Attribution", - "to_ids": 0 - }, - "campaign-name": { - "default_category": "Attribution", - "to_ids": 0 - }, - "campaign-id": { - "default_category": "Attribution", - "to_ids": 0 - }, - "malware-type": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "uri": { - "default_category": "Network activity", - "to_ids": 1 - }, - "authentihash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "ssdeep": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "imphash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "pehash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "impfuzzy": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha224": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha384": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha512": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha512/224": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "sha512/256": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "tlsh": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "cdhash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|authentihash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|ssdeep": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|imphash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|impfuzzy": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|pehash": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha224": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha384": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha512": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha512/224": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|sha512/256": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "filename|tlsh": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "windows-scheduled-task": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "windows-service-name": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "windows-service-displayname": { - "default_category": "Artifacts dropped", - "to_ids": 0 - }, - "whois-registrant-email": { - "default_category": "Attribution", - "to_ids": 0 - }, - "whois-registrant-phone": { - "default_category": "Attribution", - "to_ids": 0 - }, - "whois-registrant-name": { - "default_category": "Attribution", - "to_ids": 0 - }, - "whois-registrant-org": { - "default_category": "Attribution", - "to_ids": 0 - }, - "whois-registrar": { - "default_category": "Attribution", - "to_ids": 0 - }, - "whois-creation-date": { - "default_category": "Attribution", - "to_ids": 0 - }, - "x509-fingerprint-sha1": { - "default_category": "Network activity", - "to_ids": 1 - }, - "x509-fingerprint-md5": { - "default_category": "Network activity", - "to_ids": 1 - }, - "x509-fingerprint-sha256": { - "default_category": "Network activity", - "to_ids": 1 - }, - "dns-soa-email": { - "default_category": "Attribution", - "to_ids": 0 - }, - "size-in-bytes": { - "default_category": "Other", - "to_ids": 0 - }, - "counter": { - "default_category": "Other", - "to_ids": 0 - }, - "datetime": { - "default_category": "Other", - "to_ids": 0 - }, - "cpe": { - "default_category": "Other", - "to_ids": 0 - }, - "port": { - "default_category": "Network activity", - "to_ids": 0 - }, - "ip-dst|port": { - "default_category": "Network activity", - "to_ids": 1 - }, - "ip-src|port": { - "default_category": "Network activity", - "to_ids": 1 - }, - "hostname|port": { - "default_category": "Network activity", - "to_ids": 1 - }, - "mac-address": { - "default_category": "Network activity", - "to_ids": 0 - }, - "mac-eui-64": { - "default_category": "Network activity", - "to_ids": 0 - }, - "email-dst-display-name": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-src-display-name": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-header": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-reply-to": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-x-mailer": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-mime-boundary": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-thread-index": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "email-message-id": { - "default_category": "Payload delivery", - "to_ids": 0 - }, - "github-username": { - "default_category": "Social network", - "to_ids": 0 - }, - "github-repository": { - "default_category": "Social network", - "to_ids": 0 - }, - "github-organisation": { - "default_category": "Social network", - "to_ids": 0 - }, - "jabber-id": { - "default_category": "Social network", - "to_ids": 0 - }, - "twitter-id": { - "default_category": "Social network", - "to_ids": 0 - }, - "first-name": { - "default_category": "Person", - "to_ids": 0 - }, - "middle-name": { - "default_category": "Person", - "to_ids": 0 - }, - "last-name": { - "default_category": "Person", - "to_ids": 0 - }, - "date-of-birth": { - "default_category": "Person", - "to_ids": 0 - }, - "place-of-birth": { - "default_category": "Person", - "to_ids": 0 - }, - "gender": { - "default_category": "Person", - "to_ids": 0 - }, - "passport-number": { - "default_category": "Person", - "to_ids": 0 - }, - "passport-country": { - "default_category": "Person", - "to_ids": 0 - }, - "passport-expiration": { - "default_category": "Person", - "to_ids": 0 - }, - "redress-number": { - "default_category": "Person", - "to_ids": 0 - }, - "nationality": { - "default_category": "Person", - "to_ids": 0 - }, - "visa-number": { - "default_category": "Person", - "to_ids": 0 - }, - "issue-date-of-the-visa": { - "default_category": "Person", - "to_ids": 0 - }, - "primary-residence": { - "default_category": "Person", - "to_ids": 0 - }, - "country-of-residence": { - "default_category": "Person", - "to_ids": 0 - }, - "special-service-request": { - "default_category": "Person", - "to_ids": 0 - }, - "frequent-flyer-number": { - "default_category": "Person", - "to_ids": 0 - }, - "travel-details": { - "default_category": "Person", - "to_ids": 0 - }, - "payment-details": { - "default_category": "Person", - "to_ids": 0 - }, - "place-port-of-original-embarkation": { - "default_category": "Person", - "to_ids": 0 - }, - "place-port-of-clearance": { - "default_category": "Person", - "to_ids": 0 - }, - "place-port-of-onward-foreign-destination": { - "default_category": "Person", - "to_ids": 0 - }, - "passenger-name-record-locator-number": { - "default_category": "Person", - "to_ids": 0 - }, - "mobile-application-id": { - "default_category": "Payload delivery", - "to_ids": 1 - }, - "cortex": { - "default_category": "External analysis", - "to_ids": 0 - }, - "boolean": { - "default_category": "Other", - "to_ids": 0 - }, - "anonymised": { - "default_category": "Other", - "to_ids": 0 - } - }, - "types": [ - "md5", - "sha1", - "sha256", - "filename", - "pdb", - "filename|md5", - "filename|sha1", - "filename|sha256", - "ip-src", - "ip-dst", - "hostname", - "domain", - "domain|ip", - "email-src", - "email-dst", - "email-subject", - "email-attachment", - "email-body", - "float", - "url", - "http-method", - "user-agent", - "ja3-fingerprint-md5", - "hassh-md5", - "hasshserver-md5", - "regkey", - "regkey|value", - "AS", - "snort", - "bro", - "zeek", - "pattern-in-file", - "pattern-in-traffic", - "pattern-in-memory", - "yara", - "stix2-pattern", - "sigma", - "gene", - "mime-type", - "identity-card-number", - "cookie", - "vulnerability", - "attachment", - "malware-sample", - "link", - "comment", - "text", - "hex", - "other", - "named pipe", - "mutex", - "target-user", - "target-email", - "target-machine", - "target-org", - "target-location", - "target-external", - "btc", - "xmr", - "iban", - "bic", - "bank-account-nr", - "aba-rtn", - "bin", - "cc-number", - "prtn", - "phone-number", - "threat-actor", - "campaign-name", - "campaign-id", - "malware-type", - "uri", - "authentihash", - "ssdeep", - "imphash", - "pehash", - "impfuzzy", - "sha224", - "sha384", - "sha512", - "sha512/224", - "sha512/256", - "tlsh", - "cdhash", - "filename|authentihash", - "filename|ssdeep", - "filename|imphash", - "filename|impfuzzy", - "filename|pehash", - "filename|sha224", - "filename|sha384", - "filename|sha512", - "filename|sha512/224", - "filename|sha512/256", - "filename|tlsh", - "windows-scheduled-task", - "windows-service-name", - "windows-service-displayname", - "whois-registrant-email", - "whois-registrant-phone", - "whois-registrant-name", - "whois-registrant-org", - "whois-registrar", - "whois-creation-date", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "dns-soa-email", - "size-in-bytes", - "counter", - "datetime", - "cpe", - "port", - "ip-dst|port", - "ip-src|port", - "hostname|port", - "mac-address", - "mac-eui-64", - "email-dst-display-name", - "email-src-display-name", - "email-header", - "email-reply-to", - "email-x-mailer", - "email-mime-boundary", - "email-thread-index", - "email-message-id", - "github-username", - "github-repository", - "github-organisation", - "jabber-id", - "twitter-id", - "first-name", - "middle-name", - "last-name", - "date-of-birth", - "place-of-birth", - "gender", - "passport-number", - "passport-country", - "passport-expiration", - "redress-number", - "nationality", - "visa-number", - "issue-date-of-the-visa", - "primary-residence", - "country-of-residence", - "special-service-request", - "frequent-flyer-number", - "travel-details", - "payment-details", - "place-port-of-original-embarkation", - "place-port-of-clearance", - "place-port-of-onward-foreign-destination", - "passenger-name-record-locator-number", - "mobile-application-id", - "cortex", - "boolean", - "anonymised" - ], - "categories": [ - "Internal reference", - "Targeting data", - "Antivirus detection", - "Payload delivery", - "Artifacts dropped", - "Payload installation", - "Persistence mechanism", - "Network activity", - "Payload type", - "Attribution", - "External analysis", - "Financial fraud", - "Support Tool", - "Social network", - "Person", - "Other" - ], - "category_type_mappings": { - "Internal reference": [ - "text", - "link", - "comment", - "other", - "hex", - "anonymised" - ], - "Targeting data": [ - "target-user", - "target-email", - "target-machine", - "target-org", - "target-location", - "target-external", - "comment", - "anonymised" - ], - "Antivirus detection": [ - "link", - "comment", - "text", - "hex", - "attachment", - "other", - "anonymised" - ], - "Payload delivery": [ - "md5", - "sha1", - "sha224", - "sha256", - "sha384", - "sha512", - "sha512/224", - "sha512/256", - "ssdeep", - "imphash", - "impfuzzy", - "authentihash", - "pehash", - "tlsh", - "cdhash", - "filename", - "filename|md5", - "filename|sha1", - "filename|sha224", - "filename|sha256", - "filename|sha384", - "filename|sha512", - "filename|sha512/224", - "filename|sha512/256", - "filename|authentihash", - "filename|ssdeep", - "filename|tlsh", - "filename|imphash", - "filename|impfuzzy", - "filename|pehash", - "mac-address", - "mac-eui-64", - "ip-src", - "ip-dst", - "ip-dst|port", - "ip-src|port", - "hostname", - "domain", - "email-src", - "email-dst", - "email-subject", - "email-attachment", - "email-body", - "url", - "user-agent", - "AS", - "pattern-in-file", - "pattern-in-traffic", - "stix2-pattern", - "yara", - "sigma", - "mime-type", - "attachment", - "malware-sample", - "link", - "malware-type", - "comment", - "text", - "hex", - "vulnerability", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "ja3-fingerprint-md5", - "hassh-md5", - "hasshserver-md5", - "other", - "hostname|port", - "email-dst-display-name", - "email-src-display-name", - "email-header", - "email-reply-to", - "email-x-mailer", - "email-mime-boundary", - "email-thread-index", - "email-message-id", - "mobile-application-id", - "whois-registrant-email", - "anonymised" - ], - "Artifacts dropped": [ - "md5", - "sha1", - "sha224", - "sha256", - "sha384", - "sha512", - "sha512/224", - "sha512/256", - "ssdeep", - "imphash", - "impfuzzy", - "authentihash", - "cdhash", - "filename", - "filename|md5", - "filename|sha1", - "filename|sha224", - "filename|sha256", - "filename|sha384", - "filename|sha512", - "filename|sha512/224", - "filename|sha512/256", - "filename|authentihash", - "filename|ssdeep", - "filename|tlsh", - "filename|imphash", - "filename|impfuzzy", - "filename|pehash", - "regkey", - "regkey|value", - "pattern-in-file", - "pattern-in-memory", - "pdb", - "stix2-pattern", - "yara", - "sigma", - "attachment", - "malware-sample", - "named pipe", - "mutex", - "windows-scheduled-task", - "windows-service-name", - "windows-service-displayname", - "comment", - "text", - "hex", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "other", - "cookie", - "gene", - "mime-type", - "anonymised" - ], - "Payload installation": [ - "md5", - "sha1", - "sha224", - "sha256", - "sha384", - "sha512", - "sha512/224", - "sha512/256", - "ssdeep", - "imphash", - "impfuzzy", - "authentihash", - "pehash", - "tlsh", - "cdhash", - "filename", - "filename|md5", - "filename|sha1", - "filename|sha224", - "filename|sha256", - "filename|sha384", - "filename|sha512", - "filename|sha512/224", - "filename|sha512/256", - "filename|authentihash", - "filename|ssdeep", - "filename|tlsh", - "filename|imphash", - "filename|impfuzzy", - "filename|pehash", - "pattern-in-file", - "pattern-in-traffic", - "pattern-in-memory", - "stix2-pattern", - "yara", - "sigma", - "vulnerability", - "attachment", - "malware-sample", - "malware-type", - "comment", - "text", - "hex", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "mobile-application-id", - "other", - "mime-type", - "anonymised" - ], - "Persistence mechanism": [ - "filename", - "regkey", - "regkey|value", - "comment", - "text", - "other", - "hex", - "anonymised" - ], - "Network activity": [ - "ip-src", - "ip-dst", - "ip-dst|port", - "ip-src|port", - "port", - "hostname", - "domain", - "domain|ip", - "mac-address", - "mac-eui-64", - "email-dst", - "url", - "uri", - "user-agent", - "http-method", - "AS", - "snort", - "pattern-in-file", - "stix2-pattern", - "pattern-in-traffic", - "attachment", - "comment", - "text", - "x509-fingerprint-md5", - "x509-fingerprint-sha1", - "x509-fingerprint-sha256", - "ja3-fingerprint-md5", - "hassh-md5", - "hasshserver-md5", - "other", - "hex", - "cookie", - "hostname|port", - "bro", - "zeek", - "anonymised" - ], - "Payload type": [ - "comment", - "text", - "other", - "anonymised" - ], - "Attribution": [ - "threat-actor", - "campaign-name", - "campaign-id", - "whois-registrant-phone", - "whois-registrant-email", - "whois-registrant-name", - "whois-registrant-org", - "whois-registrar", - "whois-creation-date", - "comment", - "text", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "other", - "dns-soa-email", - "anonymised" - ], - "External analysis": [ - "md5", - "sha1", - "sha256", - "filename", - "filename|md5", - "filename|sha1", - "filename|sha256", - "ip-src", - "ip-dst", - "ip-dst|port", - "ip-src|port", - "mac-address", - "mac-eui-64", - "hostname", - "domain", - "domain|ip", - "url", - "user-agent", - "regkey", - "regkey|value", - "AS", - "snort", - "bro", - "zeek", - "pattern-in-file", - "pattern-in-traffic", - "pattern-in-memory", - "vulnerability", - "attachment", - "malware-sample", - "link", - "comment", - "text", - "x509-fingerprint-sha1", - "x509-fingerprint-md5", - "x509-fingerprint-sha256", - "ja3-fingerprint-md5", - "hassh-md5", - "hasshserver-md5", - "github-repository", - "other", - "cortex", - "anonymised" - ], - "Financial fraud": [ - "btc", - "xmr", - "iban", - "bic", - "bank-account-nr", - "aba-rtn", - "bin", - "cc-number", - "prtn", - "phone-number", - "comment", - "text", - "other", - "hex", - "anonymised" - ], - "Support Tool": [ - "link", - "text", - "attachment", - "comment", - "other", - "hex", - "anonymised" - ], - "Social network": [ - "github-username", - "github-repository", - "github-organisation", - "jabber-id", - "twitter-id", - "email-src", - "email-dst", - "comment", - "text", - "other", - "whois-registrant-email", - "anonymised" - ], - "Person": [ - "first-name", - "middle-name", - "last-name", - "date-of-birth", - "place-of-birth", - "gender", - "passport-number", - "passport-country", - "passport-expiration", - "redress-number", - "nationality", - "visa-number", - "issue-date-of-the-visa", - "primary-residence", - "country-of-residence", - "special-service-request", - "frequent-flyer-number", - "travel-details", - "payment-details", - "place-port-of-original-embarkation", - "place-port-of-clearance", - "place-port-of-onward-foreign-destination", - "passenger-name-record-locator-number", - "comment", - "text", - "other", - "phone-number", - "identity-card-number", - "anonymised" - ], - "Other": [ - "comment", - "text", - "other", - "size-in-bytes", - "counter", - "datetime", - "cpe", - "port", - "float", - "hex", - "phone-number", - "boolean", - "anonymised" - ] + "result": { + "sane_defaults": { + "md5": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha1": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "pdb": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "filename|md5": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha1": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "ip-src": { + "default_category": "Network activity", + "to_ids": 1 + }, + "ip-dst": { + "default_category": "Network activity", + "to_ids": 1 + }, + "hostname": { + "default_category": "Network activity", + "to_ids": 1 + }, + "domain": { + "default_category": "Network activity", + "to_ids": 1 + }, + "domain|ip": { + "default_category": "Network activity", + "to_ids": 1 + }, + "email": { + "default_category": "Social network", + "to_ids": 1 + }, + "email-src": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "eppn": { + "default_category": "Network activity", + "to_ids": 1 + }, + "email-dst": { + "default_category": "Network activity", + "to_ids": 1 + }, + "email-subject": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-attachment": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "email-body": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "float": { + "default_category": "Other", + "to_ids": 0 + }, + "git-commit-id": { + "default_category": "Internal reference", + "to_ids": 0 + }, + "url": { + "default_category": "Network activity", + "to_ids": 1 + }, + "http-method": { + "default_category": "Network activity", + "to_ids": 0 + }, + "user-agent": { + "default_category": "Network activity", + "to_ids": 0 + }, + "ja3-fingerprint-md5": { + "default_category": "Network activity", + "to_ids": 1 + }, + "jarm-fingerprint": { + "default_category": "Network activity", + "to_ids": 1 + }, + "favicon-mmh3": { + "default_category": "Network activity", + "to_ids": 1 + }, + "hassh-md5": { + "default_category": "Network activity", + "to_ids": 1 + }, + "hasshserver-md5": { + "default_category": "Network activity", + "to_ids": 1 + }, + "regkey": { + "default_category": "Persistence mechanism", + "to_ids": 1 + }, + "regkey|value": { + "default_category": "Persistence mechanism", + "to_ids": 1 + }, + "AS": { + "default_category": "Network activity", + "to_ids": 0 + }, + "snort": { + "default_category": "Network activity", + "to_ids": 1 + }, + "bro": { + "default_category": "Network activity", + "to_ids": 1 + }, + "zeek": { + "default_category": "Network activity", + "to_ids": 1 + }, + "community-id": { + "default_category": "Network activity", + "to_ids": 1 + }, + "dom-hash": { + "default_category": "Network activity", + "to_ids": 1 + }, + "pattern-in-file": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "pattern-in-traffic": { + "default_category": "Network activity", + "to_ids": 1 + }, + "pattern-in-memory": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "filename-pattern": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "pgp-public-key": { + "default_category": "Person", + "to_ids": 0 + }, + "pgp-private-key": { + "default_category": "Person", + "to_ids": 0 + }, + "ssh-fingerprint": { + "default_category": "Network activity", + "to_ids": 0 + }, + "yara": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "stix2-pattern": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "sigma": { + "default_category": "Payload installation", + "to_ids": 1 + }, + "gene": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "kusto-query": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "mime-type": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "identity-card-number": { + "default_category": "Person", + "to_ids": 0 + }, + "cookie": { + "default_category": "Network activity", + "to_ids": 0 + }, + "vulnerability": { + "default_category": "External analysis", + "to_ids": 0 + }, + "cpe": { + "default_category": "External analysis", + "to_ids": 0 + }, + "weakness": { + "default_category": "External analysis", + "to_ids": 0 + }, + "attachment": { + "default_category": "External analysis", + "to_ids": 0 + }, + "malware-sample": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "link": { + "default_category": "External analysis", + "to_ids": 0 + }, + "comment": { + "default_category": "Other", + "to_ids": 0 + }, + "text": { + "default_category": "Other", + "to_ids": 0 + }, + "hex": { + "default_category": "Other", + "to_ids": 0 + }, + "other": { + "default_category": "Other", + "to_ids": 0 + }, + "named pipe": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "mutex": { + "default_category": "Artifacts dropped", + "to_ids": 1 + }, + "process-state": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "target-user": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "target-email": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "target-machine": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "target-org": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "target-location": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "target-external": { + "default_category": "Targeting data", + "to_ids": 0 + }, + "btc": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "dash": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "xmr": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "iban": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "bic": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "bank-account-nr": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "aba-rtn": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "bin": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "cc-number": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "prtn": { + "default_category": "Financial fraud", + "to_ids": 1 + }, + "phone-number": { + "default_category": "Person", + "to_ids": 0 + }, + "threat-actor": { + "default_category": "Attribution", + "to_ids": 0 + }, + "campaign-name": { + "default_category": "Attribution", + "to_ids": 0 + }, + "campaign-id": { + "default_category": "Attribution", + "to_ids": 0 + }, + "malware-type": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "uri": { + "default_category": "Network activity", + "to_ids": 1 + }, + "authentihash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "vhash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "ssdeep": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "imphash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "telfhash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "pehash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "impfuzzy": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha384": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha512": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha512/224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha512/256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha3-224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha3-256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha3-384": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "sha3-512": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "tlsh": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "cdhash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|authentihash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|vhash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|ssdeep": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|imphash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|impfuzzy": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|pehash": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha384": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha512": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha512/224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha512/256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha3-224": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha3-256": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha3-384": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|sha3-512": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "filename|tlsh": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "windows-scheduled-task": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "windows-service-name": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "windows-service-displayname": { + "default_category": "Artifacts dropped", + "to_ids": 0 + }, + "whois-registrant-email": { + "default_category": "Attribution", + "to_ids": 0 + }, + "whois-registrant-phone": { + "default_category": "Attribution", + "to_ids": 0 + }, + "whois-registrant-name": { + "default_category": "Attribution", + "to_ids": 0 + }, + "whois-registrant-org": { + "default_category": "Attribution", + "to_ids": 0 + }, + "whois-registrar": { + "default_category": "Attribution", + "to_ids": 0 + }, + "whois-creation-date": { + "default_category": "Attribution", + "to_ids": 0 + }, + "x509-fingerprint-sha1": { + "default_category": "Network activity", + "to_ids": 1 + }, + "x509-fingerprint-md5": { + "default_category": "Network activity", + "to_ids": 1 + }, + "x509-fingerprint-sha256": { + "default_category": "Network activity", + "to_ids": 1 + }, + "dns-soa-email": { + "default_category": "Attribution", + "to_ids": 0 + }, + "size-in-bytes": { + "default_category": "Other", + "to_ids": 0 + }, + "counter": { + "default_category": "Other", + "to_ids": 0 + }, + "integer": { + "default_category": "Other", + "to_ids": 0 + }, + "datetime": { + "default_category": "Other", + "to_ids": 0 + }, + "port": { + "default_category": "Network activity", + "to_ids": 0 + }, + "ip-dst|port": { + "default_category": "Network activity", + "to_ids": 1 + }, + "ip-src|port": { + "default_category": "Network activity", + "to_ids": 1 + }, + "hostname|port": { + "default_category": "Network activity", + "to_ids": 1 + }, + "mac-address": { + "default_category": "Network activity", + "to_ids": 0 + }, + "mac-eui-64": { + "default_category": "Network activity", + "to_ids": 0 + }, + "email-dst-display-name": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-src-display-name": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-header": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-reply-to": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-x-mailer": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-mime-boundary": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-thread-index": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "email-message-id": { + "default_category": "Payload delivery", + "to_ids": 0 + }, + "github-username": { + "default_category": "Social network", + "to_ids": 0 + }, + "github-repository": { + "default_category": "Social network", + "to_ids": 0 + }, + "github-organisation": { + "default_category": "Social network", + "to_ids": 0 + }, + "jabber-id": { + "default_category": "Social network", + "to_ids": 0 + }, + "twitter-id": { + "default_category": "Social network", + "to_ids": 0 + }, + "dkim": { + "default_category": "Network activity", + "to_ids": 0 + }, + "dkim-signature": { + "default_category": "Network activity", + "to_ids": 0 + }, + "first-name": { + "default_category": "Person", + "to_ids": 0 + }, + "middle-name": { + "default_category": "Person", + "to_ids": 0 + }, + "last-name": { + "default_category": "Person", + "to_ids": 0 + }, + "full-name": { + "default_category": "Person", + "to_ids": 0 + }, + "date-of-birth": { + "default_category": "Person", + "to_ids": 0 + }, + "place-of-birth": { + "default_category": "Person", + "to_ids": 0 + }, + "gender": { + "default_category": "Person", + "to_ids": 0 + }, + "passport-number": { + "default_category": "Person", + "to_ids": 0 + }, + "passport-country": { + "default_category": "Person", + "to_ids": 0 + }, + "passport-expiration": { + "default_category": "Person", + "to_ids": 0 + }, + "redress-number": { + "default_category": "Person", + "to_ids": 0 + }, + "nationality": { + "default_category": "Person", + "to_ids": 0 + }, + "visa-number": { + "default_category": "Person", + "to_ids": 0 + }, + "issue-date-of-the-visa": { + "default_category": "Person", + "to_ids": 0 + }, + "primary-residence": { + "default_category": "Person", + "to_ids": 0 + }, + "country-of-residence": { + "default_category": "Person", + "to_ids": 0 + }, + "special-service-request": { + "default_category": "Person", + "to_ids": 0 + }, + "frequent-flyer-number": { + "default_category": "Person", + "to_ids": 0 + }, + "travel-details": { + "default_category": "Person", + "to_ids": 0 + }, + "payment-details": { + "default_category": "Person", + "to_ids": 0 + }, + "place-port-of-original-embarkation": { + "default_category": "Person", + "to_ids": 0 + }, + "place-port-of-clearance": { + "default_category": "Person", + "to_ids": 0 + }, + "place-port-of-onward-foreign-destination": { + "default_category": "Person", + "to_ids": 0 + }, + "passenger-name-record-locator-number": { + "default_category": "Person", + "to_ids": 0 + }, + "mobile-application-id": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "azure-application-id": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "chrome-extension-id": { + "default_category": "Payload delivery", + "to_ids": 1 + }, + "cortex": { + "default_category": "External analysis", + "to_ids": 0 + }, + "boolean": { + "default_category": "Other", + "to_ids": 0 + }, + "anonymised": { + "default_category": "Other", + "to_ids": 0 + }, + "onion-address": { + "default_category": "Network activity", + "to_ids": 1 + } + }, + "types": [ + "md5", + "sha1", + "sha256", + "filename", + "pdb", + "filename|md5", + "filename|sha1", + "filename|sha256", + "ip-src", + "ip-dst", + "hostname", + "domain", + "domain|ip", + "email", + "email-src", + "eppn", + "email-dst", + "email-subject", + "email-attachment", + "email-body", + "float", + "git-commit-id", + "url", + "http-method", + "user-agent", + "ja3-fingerprint-md5", + "jarm-fingerprint", + "favicon-mmh3", + "hassh-md5", + "hasshserver-md5", + "regkey", + "regkey|value", + "AS", + "snort", + "bro", + "zeek", + "community-id", + "dom-hash", + "pattern-in-file", + "pattern-in-traffic", + "pattern-in-memory", + "filename-pattern", + "pgp-public-key", + "pgp-private-key", + "ssh-fingerprint", + "yara", + "stix2-pattern", + "sigma", + "gene", + "kusto-query", + "mime-type", + "identity-card-number", + "cookie", + "vulnerability", + "cpe", + "weakness", + "attachment", + "malware-sample", + "link", + "comment", + "text", + "hex", + "other", + "named pipe", + "mutex", + "process-state", + "target-user", + "target-email", + "target-machine", + "target-org", + "target-location", + "target-external", + "btc", + "dash", + "xmr", + "iban", + "bic", + "bank-account-nr", + "aba-rtn", + "bin", + "cc-number", + "prtn", + "phone-number", + "threat-actor", + "campaign-name", + "campaign-id", + "malware-type", + "uri", + "authentihash", + "vhash", + "ssdeep", + "imphash", + "telfhash", + "pehash", + "impfuzzy", + "sha224", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "tlsh", + "cdhash", + "filename|authentihash", + "filename|vhash", + "filename|ssdeep", + "filename|imphash", + "filename|impfuzzy", + "filename|pehash", + "filename|sha224", + "filename|sha384", + "filename|sha512", + "filename|sha512/224", + "filename|sha512/256", + "filename|sha3-224", + "filename|sha3-256", + "filename|sha3-384", + "filename|sha3-512", + "filename|tlsh", + "windows-scheduled-task", + "windows-service-name", + "windows-service-displayname", + "whois-registrant-email", + "whois-registrant-phone", + "whois-registrant-name", + "whois-registrant-org", + "whois-registrar", + "whois-creation-date", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "dns-soa-email", + "size-in-bytes", + "counter", + "integer", + "datetime", + "port", + "ip-dst|port", + "ip-src|port", + "hostname|port", + "mac-address", + "mac-eui-64", + "email-dst-display-name", + "email-src-display-name", + "email-header", + "email-reply-to", + "email-x-mailer", + "email-mime-boundary", + "email-thread-index", + "email-message-id", + "github-username", + "github-repository", + "github-organisation", + "jabber-id", + "twitter-id", + "dkim", + "dkim-signature", + "first-name", + "middle-name", + "last-name", + "full-name", + "date-of-birth", + "place-of-birth", + "gender", + "passport-number", + "passport-country", + "passport-expiration", + "redress-number", + "nationality", + "visa-number", + "issue-date-of-the-visa", + "primary-residence", + "country-of-residence", + "special-service-request", + "frequent-flyer-number", + "travel-details", + "payment-details", + "place-port-of-original-embarkation", + "place-port-of-clearance", + "place-port-of-onward-foreign-destination", + "passenger-name-record-locator-number", + "mobile-application-id", + "azure-application-id", + "chrome-extension-id", + "cortex", + "boolean", + "anonymised", + "onion-address" + ], + "categories": [ + "Internal reference", + "Targeting data", + "Antivirus detection", + "Payload delivery", + "Artifacts dropped", + "Payload installation", + "Persistence mechanism", + "Network activity", + "Payload type", + "Attribution", + "External analysis", + "Financial fraud", + "Support Tool", + "Social network", + "Person", + "Other" + ], + "category_type_mappings": { + "Internal reference": [ + "text", + "link", + "comment", + "other", + "hex", + "anonymised", + "git-commit-id" + ], + "Targeting data": [ + "target-user", + "target-email", + "target-machine", + "target-org", + "target-location", + "target-external", + "comment", + "anonymised" + ], + "Antivirus detection": [ + "link", + "comment", + "text", + "hex", + "attachment", + "other", + "anonymised" + ], + "Payload delivery": [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ssdeep", + "imphash", + "telfhash", + "impfuzzy", + "authentihash", + "vhash", + "pehash", + "tlsh", + "cdhash", + "filename", + "filename|md5", + "filename|sha1", + "filename|sha224", + "filename|sha256", + "filename|sha384", + "filename|sha512", + "filename|sha512/224", + "filename|sha512/256", + "filename|sha3-224", + "filename|sha3-256", + "filename|sha3-384", + "filename|sha3-512", + "filename|authentihash", + "filename|vhash", + "filename|ssdeep", + "filename|tlsh", + "filename|imphash", + "filename|impfuzzy", + "filename|pehash", + "mac-address", + "mac-eui-64", + "ip-src", + "ip-dst", + "ip-dst|port", + "ip-src|port", + "hostname", + "domain", + "email", + "email-src", + "email-dst", + "email-subject", + "email-attachment", + "email-body", + "url", + "user-agent", + "AS", + "pattern-in-file", + "pattern-in-traffic", + "filename-pattern", + "stix2-pattern", + "yara", + "sigma", + "mime-type", + "attachment", + "malware-sample", + "link", + "malware-type", + "comment", + "text", + "hex", + "vulnerability", + "cpe", + "weakness", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "ja3-fingerprint-md5", + "jarm-fingerprint", + "hassh-md5", + "hasshserver-md5", + "other", + "hostname|port", + "email-dst-display-name", + "email-src-display-name", + "email-header", + "email-reply-to", + "email-x-mailer", + "email-mime-boundary", + "email-thread-index", + "email-message-id", + "azure-application-id", + "mobile-application-id", + "chrome-extension-id", + "whois-registrant-email", + "anonymised", + "onion-address" + ], + "Artifacts dropped": [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ssdeep", + "imphash", + "telfhash", + "impfuzzy", + "authentihash", + "vhash", + "cdhash", + "filename", + "filename|md5", + "filename|sha1", + "filename|sha224", + "filename|sha256", + "filename|sha384", + "filename|sha512", + "filename|sha512/224", + "filename|sha512/256", + "filename|sha3-224", + "filename|sha3-256", + "filename|sha3-384", + "filename|sha3-512", + "filename|authentihash", + "filename|vhash", + "filename|ssdeep", + "filename|tlsh", + "filename|imphash", + "filename|impfuzzy", + "filename|pehash", + "regkey", + "regkey|value", + "pattern-in-file", + "pattern-in-memory", + "filename-pattern", + "pdb", + "stix2-pattern", + "yara", + "sigma", + "attachment", + "malware-sample", + "named pipe", + "mutex", + "process-state", + "windows-scheduled-task", + "windows-service-name", + "windows-service-displayname", + "comment", + "text", + "hex", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "other", + "cookie", + "gene", + "kusto-query", + "mime-type", + "anonymised", + "pgp-public-key", + "pgp-private-key" + ], + "Payload installation": [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ssdeep", + "imphash", + "telfhash", + "impfuzzy", + "authentihash", + "vhash", + "pehash", + "tlsh", + "cdhash", + "filename", + "filename|md5", + "filename|sha1", + "filename|sha224", + "filename|sha256", + "filename|sha384", + "filename|sha512", + "filename|sha512/224", + "filename|sha512/256", + "filename|sha3-224", + "filename|sha3-256", + "filename|sha3-384", + "filename|sha3-512", + "filename|authentihash", + "filename|vhash", + "filename|ssdeep", + "filename|tlsh", + "filename|imphash", + "filename|impfuzzy", + "filename|pehash", + "pattern-in-file", + "pattern-in-traffic", + "pattern-in-memory", + "filename-pattern", + "stix2-pattern", + "yara", + "sigma", + "vulnerability", + "cpe", + "weakness", + "attachment", + "malware-sample", + "malware-type", + "comment", + "text", + "hex", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "azure-application-id", + "mobile-application-id", + "chrome-extension-id", + "other", + "mime-type", + "anonymised" + ], + "Persistence mechanism": [ + "filename", + "regkey", + "regkey|value", + "comment", + "text", + "other", + "hex", + "anonymised" + ], + "Network activity": [ + "ip-src", + "ip-dst", + "ip-dst|port", + "ip-src|port", + "port", + "hostname", + "domain", + "domain|ip", + "mac-address", + "mac-eui-64", + "email", + "email-dst", + "email-src", + "eppn", + "url", + "uri", + "user-agent", + "http-method", + "AS", + "snort", + "pattern-in-file", + "filename-pattern", + "stix2-pattern", + "pattern-in-traffic", + "attachment", + "comment", + "text", + "x509-fingerprint-md5", + "x509-fingerprint-sha1", + "x509-fingerprint-sha256", + "ja3-fingerprint-md5", + "jarm-fingerprint", + "hassh-md5", + "hasshserver-md5", + "other", + "hex", + "cookie", + "hostname|port", + "bro", + "zeek", + "anonymised", + "community-id", + "email-subject", + "favicon-mmh3", + "dkim", + "dkim-signature", + "ssh-fingerprint", + "dom-hash", + "onion-address" + ], + "Payload type": [ + "comment", + "text", + "other", + "anonymised" + ], + "Attribution": [ + "threat-actor", + "campaign-name", + "campaign-id", + "whois-registrant-phone", + "whois-registrant-email", + "whois-registrant-name", + "whois-registrant-org", + "whois-registrar", + "whois-creation-date", + "comment", + "text", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "other", + "dns-soa-email", + "anonymised", + "email" + ], + "External analysis": [ + "md5", + "sha1", + "sha256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "filename", + "filename|md5", + "filename|sha1", + "filename|sha256", + "filename|sha3-224", + "filename|sha3-256", + "filename|sha3-384", + "filename|sha3-512", + "ip-src", + "ip-dst", + "ip-dst|port", + "ip-src|port", + "mac-address", + "mac-eui-64", + "hostname", + "domain", + "domain|ip", + "url", + "user-agent", + "regkey", + "regkey|value", + "AS", + "snort", + "bro", + "zeek", + "pattern-in-file", + "pattern-in-traffic", + "pattern-in-memory", + "filename-pattern", + "vulnerability", + "cpe", + "weakness", + "attachment", + "malware-sample", + "link", + "comment", + "text", + "x509-fingerprint-sha1", + "x509-fingerprint-md5", + "x509-fingerprint-sha256", + "ja3-fingerprint-md5", + "jarm-fingerprint", + "hassh-md5", + "hasshserver-md5", + "github-repository", + "other", + "cortex", + "anonymised", + "community-id", + "dom-hash", + "onion-address" + ], + "Financial fraud": [ + "btc", + "dash", + "xmr", + "iban", + "bic", + "bank-account-nr", + "aba-rtn", + "bin", + "cc-number", + "prtn", + "phone-number", + "comment", + "text", + "other", + "hex", + "anonymised" + ], + "Support Tool": [ + "link", + "text", + "attachment", + "comment", + "other", + "hex", + "anonymised" + ], + "Social network": [ + "github-username", + "github-repository", + "github-organisation", + "jabber-id", + "twitter-id", + "email", + "email-src", + "email-dst", + "eppn", + "comment", + "text", + "other", + "whois-registrant-email", + "anonymised", + "pgp-public-key", + "pgp-private-key" + ], + "Person": [ + "first-name", + "middle-name", + "last-name", + "full-name", + "date-of-birth", + "place-of-birth", + "gender", + "passport-number", + "passport-country", + "passport-expiration", + "redress-number", + "nationality", + "visa-number", + "issue-date-of-the-visa", + "primary-residence", + "country-of-residence", + "special-service-request", + "frequent-flyer-number", + "travel-details", + "payment-details", + "place-port-of-original-embarkation", + "place-port-of-clearance", + "place-port-of-onward-foreign-destination", + "passenger-name-record-locator-number", + "comment", + "text", + "other", + "phone-number", + "identity-card-number", + "anonymised", + "email", + "pgp-public-key", + "pgp-private-key" + ], + "Other": [ + "comment", + "text", + "other", + "size-in-bytes", + "counter", + "integer", + "datetime", + "cpe", + "port", + "float", + "hex", + "phone-number", + "boolean", + "anonymised", + "pgp-public-key", + "pgp-private-key" + ] + } } - } } diff --git a/pymisp/data/misp-objects b/pymisp/data/misp-objects index 0c6b7b4..dcf0c3f 160000 --- a/pymisp/data/misp-objects +++ b/pymisp/data/misp-objects @@ -1 +1 @@ -Subproject commit 0c6b7b4302b58b0b1ab8371acdd3e4b988609a88 +Subproject commit dcf0c3febcacc3b6dd8d0d390b7ec59254cd46ba diff --git a/pymisp/exceptions.py b/pymisp/exceptions.py index 1d7663f..948ab12 100644 --- a/pymisp/exceptions.py +++ b/pymisp/exceptions.py @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations class PyMISPError(Exception): - def __init__(self, message): - super(PyMISPError, self).__init__(message) + def __init__(self, message: str) -> None: + super().__init__(message) self.message = message @@ -19,10 +19,38 @@ class NewAttributeError(PyMISPError): pass +class NewEventReportError(PyMISPError): + pass + + +class NewAnalystDataError(PyMISPError): + pass + + +class NewNoteError(PyMISPError): + pass + + +class NewOpinionError(PyMISPError): + pass + + +class NewRelationshipError(PyMISPError): + pass + + class UpdateAttributeError(PyMISPError): pass +class NewGalaxyClusterError(PyMISPError): + pass + + +class NewGalaxyClusterRelationError(PyMISPError): + pass + + class SearchError(PyMISPError): pass @@ -39,17 +67,29 @@ class NoKey(PyMISPError): pass -class MISPObjectException(PyMISPError): - pass +class MISPAttributeException(PyMISPError): + """A base class for attribute specific exceptions""" +class MISPObjectException(PyMISPError): + """A base class for object specific exceptions""" + + +class InvalidMISPAttribute(MISPAttributeException): + """Exception raised when an attribute doesn't respect the constraints in the definition""" + +class InvalidMISPObjectAttribute(MISPAttributeException): + """Exception raised when an object attribute doesn't respect the constraints in the definition""" class InvalidMISPObject(MISPObjectException): - """Exception raised when an object doesn't respect the contrains in the definition""" - pass + """Exception raised when an object doesn't respect the constraints in the definition""" class UnknownMISPObjectTemplate(MISPObjectException): """Exception raised when the template is unknown""" + + + +class InvalidMISPGalaxy(PyMISPError): pass @@ -66,7 +106,7 @@ class PyMISPNotImplementedYet(PyMISPError): class PyMISPUnexpectedResponse(PyMISPError): - pass + pass class PyMISPEmptyResponse(PyMISPError): diff --git a/pymisp/mispevent.py b/pymisp/mispevent.py index 8116acd..362fd1e 100644 --- a/pymisp/mispevent.py +++ b/pymisp/mispevent.py @@ -1,93 +1,146 @@ +from __future__ import annotations -# -*- coding: utf-8 -*- - -import datetime -import json +from datetime import timezone, datetime, date +import copy import os import base64 -from io import BytesIO -from zipfile import ZipFile import sys +from io import BytesIO, BufferedIOBase, TextIOBase +from zipfile import ZipFile import uuid +from uuid import UUID from collections import defaultdict - -from . import deprecated -from .abstract import AbstractMISP -from .exceptions import UnknownMISPObjectTemplate, InvalidMISPObject, PyMISPError, NewEventError, NewAttributeError - import logging +import hashlib +from pathlib import Path +from typing import IO, Any, Sequence +import warnings + +try: + # orjson is optional dependency that speedups parsing and encoding JSON + import orjson as json # type: ignore +except ImportError: + import json + +from .abstract import AbstractMISP, MISPTag +from .exceptions import (NewNoteError, NewOpinionError, NewRelationshipError, UnknownMISPObjectTemplate, InvalidMISPGalaxy, InvalidMISPAttribute, + InvalidMISPObject, InvalidMISPObjectAttribute, PyMISPError, NewEventError, NewAttributeError, NewEventReportError, + NewGalaxyClusterError, NewGalaxyClusterRelationError, NewAnalystDataError) + logger = logging.getLogger('pymisp') -if sys.version_info < (3, 0): - logger.warning("You're using python 2, it is strongly recommended to use python >=3.6") +class AnalystDataBehaviorMixin(AbstractMISP): - # This is required because Python 2 is a pain. - from datetime import tzinfo, timedelta + # NOTE: edited here must be the property of Abstract MISP - class UTC(tzinfo): - """UTC""" + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(**kwargs) + self.uuid: str # Created in the child class + self._analyst_data_object_type: str # Must be defined in the child class + self.Note: list[MISPNote] = [] + self.Opinion: list[MISPOpinion] = [] + self.Relationship: list[MISPRelationship] = [] - def utcoffset(self, dt): - return timedelta(0) + @property + def analyst_data_object_type(self) -> str: + return self._analyst_data_object_type - def tzname(self, dt): - return "UTC" + @property + def notes(self) -> list[MISPNote]: + return self.Note - def dst(self, dt): - return timedelta(0) + @property + def opinions(self) -> list[MISPOpinion]: + return self.Opinion + @property + def relationships(self) -> list[MISPRelationship]: + return self.Relationship -if (3, 0) <= sys.version_info < (3, 6): - OLD_PY3 = True -else: - OLD_PY3 = False + def add_note(self, note: str, language: str | None = None, **kwargs) -> MISPNote: # type: ignore[no-untyped-def] + the_note = MISPNote() + the_note.from_dict(note=note, language=language, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.notes.append(the_note) + self.edited = True + return the_note + + def add_opinion(self, opinion: int, comment: str | None = None, **kwargs) -> MISPOpinion: # type: ignore[no-untyped-def] + the_opinion = MISPOpinion() + the_opinion.from_dict(opinion=opinion, comment=comment, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.opinions.append(the_opinion) + self.edited = True + return the_opinion + + def add_relationship(self, related_object_type: AbstractMISP | str, related_object_uuid: str | None, relationship_type: str, **kwargs) -> MISPRelationship: # type: ignore[no-untyped-def] + the_relationship = MISPRelationship() + the_relationship.from_dict(related_object_type=related_object_type, related_object_uuid=related_object_uuid, + relationship_type=relationship_type, + object_uuid=self.uuid, object_type=self.analyst_data_object_type, + **kwargs) + self.relationships.append(the_relationship) + self.edited = True + return the_relationship + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + # These members need a fully initialized class to be loaded properly + notes = kwargs.pop('Note', []) + opinions = kwargs.pop('Opinion', []) + relationships = kwargs.pop('Relationship', []) + super().from_dict(**kwargs) + for note in notes: + note.pop('object_uuid', None) + note.pop('object_type', None) + self.add_note(**note) + for opinion in opinions: + opinion.pop('object_uuid', None) + opinion.pop('object_type', None) + self.add_opinion(**opinion) + for relationship in relationships: + relationship.pop('object_uuid', None) + relationship.pop('object_type', None) + self.add_relationship(**relationship) try: from dateutil.parser import parse except ImportError: logger.exception("Cannot import dateutil") - pass - -try: - import jsonschema -except ImportError: - logger.exception("Cannot import jsonschema") - pass - -try: - # pyme renamed to gpg the 2016-10-28 - import gpg - from gpg.constants.sig import mode - has_pyme = True -except ImportError: - try: - # pyme renamed to gpg the 2016-10-28 - import pyme as gpg - from pyme.constants.sig import mode - has_pyme = True - except ImportError: - has_pyme = False - -# Least dirty way to support python 2 and 3 -try: - basestring - unicode -except NameError: - basestring = str - unicode = str -def _int_to_str(d): - # transform all integer back to string - for k, v in d.items(): - if isinstance(v, (int, float)) and not isinstance(v, bool): - d[k] = str(v) - return d +def _make_datetime(value: int | float | str | datetime | date) -> datetime: + if isinstance(value, (int, float)): + # Timestamp + value = datetime.fromtimestamp(value) + elif isinstance(value, str): + try: + # faster + value = datetime.fromisoformat(value) + except Exception: + value = parse(value) # type: ignore[arg-type] + elif isinstance(value, datetime): + pass + elif isinstance(value, date): # NOTE: date has to be *after* datetime, or it will be overwritten + value = datetime.combine(value, datetime.min.time()) + else: + raise PyMISPError(f'Invalid format for {value}: {type(value)}.') + + if not value.tzinfo: + # set localtimezone if not present + value = value.astimezone() + return value -def make_bool(value): +def make_bool(value: bool | int | str | dict[str, Any] | list[Any] | None) -> bool: + """Converts the supplied value to a boolean. + + :param value: Value to interpret as a boolean. An empty string, dict + or list is False; value None is also False. + """ if isinstance(value, bool): return value if isinstance(value, int): @@ -100,67 +153,378 @@ def make_bool(value): return False return True else: - raise Exception('Unable to convert {} to a boolean.'.format(value)) + raise PyMISPError(f'Unable to convert {value} to a boolean.') -class MISPAttribute(AbstractMISP): +class MISPOrganisation(AbstractMISP): - def __init__(self, describe_types=None, strict=False): - """Represents an Attribute - :describe_type: Use it is you want to overwrite the defualt describeTypes.json file (you don't) - :strict: If false, fallback to sane defaults for the attribute type if the ones passed by the user are incorrect + _fields_for_feed: set[str] = {'name', 'uuid'} + + def __init__(self) -> None: + super().__init__() + self.id: int + self.name: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Organisation' in kwargs: + kwargs = kwargs['Organisation'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'name'): + return f'<{self.__class__.__name__}(type={self.name})' + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPSharingGroupOrg(AbstractMISP): + _fields_for_feed: set[str] = {'extend', 'Organisation'} + + def __init__(self) -> None: + super().__init__() + self.extend: bool + self.Organisation: MISPOrganisation + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'SharingGroupOrg' in kwargs: + kwargs = kwargs['SharingGroupOrg'] + if 'Organisation' in kwargs: + self.Organisation = MISPOrganisation() + self.Organisation.from_dict(**kwargs.pop('Organisation')) + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'Organisation') and hasattr(self, 'extend'): + return f'<{self.__class__.__name__}(Org={self.Organisation.name}, extend={self.extend})' + return f'<{self.__class__.__name__}(NotInitialized)' + + def _to_feed(self) -> dict[str, Any]: + to_return = super()._to_feed() + to_return['Organisation'] = self.Organisation._to_feed() + return to_return + + +class MISPSharingGroup(AbstractMISP): + _fields_for_feed: set[str] = {'uuid', 'name', 'roaming', 'created', 'organisation_uuid', 'Organisation', 'SharingGroupOrg', 'SharingGroupServer'} + + def __init__(self) -> None: + super().__init__() + self.name: str + self.SharingGroupOrg: list[MISPSharingGroupOrg] = [] + + @property + def sgorgs(self) -> list[MISPSharingGroupOrg]: + return self.SharingGroupOrg + + @sgorgs.setter + def sgorgs(self, sgorgs: list[MISPSharingGroupOrg]) -> None: + if all(isinstance(x, MISPSharingGroupOrg) for x in sgorgs): + self.SharingGroupOrg = sgorgs + else: + raise PyMISPError('All the attributes have to be of type MISPSharingGroupOrg.') + + def add_sgorg(self, sgorg: dict[str, Any]) -> MISPSharingGroupOrg: + misp_sgorg = MISPSharingGroupOrg() + misp_sgorg.from_dict(**sgorg) + self.SharingGroupOrg.append(misp_sgorg) + return misp_sgorg + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'SharingGroupOrg' in kwargs: + [self.add_sgorg(sgorg) for sgorg in kwargs.pop('SharingGroupOrg')] + if 'SharingGroup' in kwargs: + kwargs = kwargs['SharingGroup'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'name'): + return f'<{self.__class__.__name__}(name={self.name})>' + return f'<{self.__class__.__name__}(NotInitialized)>' + + def _to_feed(self) -> dict[str, Any]: + to_return = super()._to_feed() + to_return['SharingGroupOrg'] = [sgorg._to_feed() for sgorg in self.SharingGroupOrg] + to_return['Organisation'].pop('id', None) + for server in to_return['SharingGroupServer']: + server.pop('id', None) + server.pop('sharing_group_id', None) + server.pop('server_id', None) + server['Server'].pop('id', None) + return to_return + + +class MISPShadowAttribute(AbstractMISP): + + def __init__(self) -> None: + super().__init__() + self.type: str + self.value: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'ShadowAttribute' in kwargs: + kwargs = kwargs['ShadowAttribute'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'value'): + return f'<{self.__class__.__name__}(type={self.type}, value={self.value})' + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPSighting(AbstractMISP): + + def __init__(self) -> None: + super().__init__() + self.id: int + self.value: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + """Initialize the MISPSighting from a dictionary + + :param value: Value of the attribute the sighting is related too. Pushing this object + will update the sighting count of each attribute with this value on the instance. + :param uuid: UUID of the attribute to update + :param id: ID of the attriute to update + :param source: Source of the sighting + :param type: Type of the sighting + :param timestamp: Timestamp associated to the sighting """ - super(MISPAttribute, self).__init__() - if not describe_types: - ressources_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') - with open(os.path.join(ressources_path, 'describeTypes.json'), 'rb') as f: - if OLD_PY3: - t = json.loads(f.read().decode()) - else: - t = json.load(f) - describe_types = t['result'] - self.__categories = describe_types['categories'] - self._types = describe_types['types'] - self.__category_type_mapping = describe_types['category_type_mappings'] - self.__sane_default = describe_types['sane_defaults'] - self.__strict = strict - self.uuid = str(uuid.uuid4()) - self.ShadowAttribute = [] + if 'Sighting' in kwargs: + kwargs = kwargs['Sighting'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'value'): + return '<{self.__class__.__name__}(value={self.value})'.format(self=self) + if hasattr(self, 'id'): + return '<{self.__class__.__name__}(id={self.id})'.format(self=self) + if hasattr(self, 'uuid'): + return '<{self.__class__.__name__}(uuid={self.uuid})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPAttribute(AnalystDataBehaviorMixin): + _fields_for_feed: set[str] = {'uuid', 'value', 'category', 'type', 'comment', 'data', + 'deleted', 'timestamp', 'to_ids', 'disable_correlation', + 'first_seen', 'last_seen'} + + _analyst_data_object_type = 'Attribute' + + def __init__(self, describe_types: dict[str, Any] | None = None, strict: bool = False): + """Represents an Attribute + + :param describe_types: Use it if you want to overwrite the default describeTypes.json file (you don't) + :param strict: If false, fallback to sane defaults for the attribute type if the ones passed by the user are incorrect + """ + super().__init__() + if describe_types: + self.describe_types: dict[str, Any] = describe_types + self.__categories: list[str] = self.describe_types['categories'] + self.__category_type_mapping: dict[str, list[str]] = self.describe_types['category_type_mappings'] + self.__sane_default: dict[str, dict[str, str | int]] = self.describe_types['sane_defaults'] + self.__strict: bool = strict + self.data: BytesIO | None = None + self.first_seen: datetime + self.last_seen: datetime + self.uuid: str = str(uuid.uuid4()) + self.ShadowAttribute: list[MISPShadowAttribute] = [] + self.SharingGroup: MISPSharingGroup + self.Sighting: list[MISPSighting] = [] + self.Tag: list[MISPTag] = [] + self.Galaxy: list[MISPGalaxy] = [] + + self.expand: str + self.timestamp: float | int | datetime + + # For search + self.Event: MISPEvent + self.RelatedAttribute: list[MISPAttribute] + + # For malware sample + self._malware_binary: BytesIO | None + + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] + return super()._add_tag(tag, **kwargs) @property - def known_types(self): + def tags(self) -> list[MISPTag]: + """Returns a list of tags associated to this Attribute""" + return self.Tag + + @tags.setter + def tags(self, tags: list[MISPTag]) -> None: + """Set a list of prepared MISPTag.""" + super()._set_tags(tags) + + def add_galaxy(self, galaxy: MISPGalaxy | dict[str, Any] | None = None, **kwargs) -> MISPGalaxy: # type: ignore[no-untyped-def] + """Add a galaxy to the Attribute, either by passing a MISPGalaxy or a dictionary""" + if isinstance(galaxy, MISPGalaxy): + self.galaxies.append(galaxy) + return galaxy + if isinstance(galaxy, dict): + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + elif kwargs: + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**kwargs) + else: + raise InvalidMISPGalaxy("A Galaxy to add to an existing Attribute needs to be either a MISPGalaxy or a plain python dictionary") + self.galaxies.append(misp_galaxy) + return misp_galaxy + + @property + def galaxies(self) -> list[MISPGalaxy]: + """Returns a list of galaxies associated to this Attribute""" + return self.Galaxy + + def _prepare_data(self, data: Path | str | bytes | BytesIO | None) -> None: + if not data: + super().__setattr__('data', None) + return + + if isinstance(data, BytesIO): + super().__setattr__('data', data) + elif isinstance(data, Path): + with data.open('rb') as f_temp: + super().__setattr__('data', BytesIO(f_temp.read())) + elif isinstance(data, (str, bytes)): + super().__setattr__('data', BytesIO(base64.b64decode(data))) + else: + raise PyMISPError(f'Invalid type ({type(data)}) for the data key: {data}') + + if self.type == 'malware-sample': + try: + # Ignore type, if data is None -> exception + with ZipFile(self.data) as f: # type: ignore + if not self.__is_misp_encrypted_file(f): + raise PyMISPError('Not an existing malware sample') + for name in f.namelist(): + if name.endswith('.filename.txt'): + with f.open(name, pwd=b'infected') as unpacked: + self.malware_filename = unpacked.read().decode().strip() + else: + # decrypting a zipped file is extremely slow. We do it on-demand in self.malware_binary + continue + except Exception: + # not a encrypted zip file, assuming it is a new malware sample + self._prepare_new_malware_sample() + + def __setattr__(self, name: str, value: Any) -> None: + if name in ['first_seen', 'last_seen']: + _datetime = _make_datetime(value) + + # NOTE: the two following should be exceptions, but there are existing events in this state, + # And we cannot dump them if it is there. + if name == 'last_seen' and hasattr(self, 'first_seen') and self.first_seen > _datetime: + logger.warning(f'last_seen ({value}) has to be after first_seen ({self.first_seen})') + if name == 'first_seen' and hasattr(self, 'last_seen') and self.last_seen < _datetime: + logger.warning(f'first_seen ({value}) has to be before last_seen ({self.last_seen})') + super().__setattr__(name, _datetime) + elif name == 'data': + self._prepare_data(value) + else: + super().__setattr__(name, value) + + def hash_values(self, algorithm: str = 'sha512') -> list[str]: + """Compute the hash of every value for fast lookups""" + if algorithm not in hashlib.algorithms_available: + raise PyMISPError(f'The algorithm {algorithm} is not available for hashing.') + if '|' in self.type or self.type == 'malware-sample': + hashes = [] + for v in self.value.split('|'): + h = hashlib.new(algorithm) + h.update(v.encode("utf-8")) + hashes.append(h.hexdigest()) + return hashes + else: + h = hashlib.new(algorithm) + to_encode = self.value + if not isinstance(to_encode, str): + to_encode = str(to_encode) + h.update(to_encode.encode("utf-8")) + return [h.hexdigest()] + + def _set_default(self) -> None: + if not hasattr(self, 'comment'): + self.comment = '' + if not hasattr(self, 'timestamp'): + self.timestamp = datetime.timestamp(datetime.now()) + + def _to_feed(self, with_distribution: bool=False) -> dict[str, Any]: + if with_distribution: + self._fields_for_feed.add('distribution') + to_return = super()._to_feed() + if self.data: + to_return['data'] = base64.b64encode(self.data.getvalue()).decode() + if self.tags: + to_return['Tag'] = list(filter(None, [tag._to_feed() for tag in self.tags])) + if with_distribution: + try: + to_return['SharingGroup'] = self.SharingGroup._to_feed() + except AttributeError: + pass + return to_return + + @property + def known_types(self) -> list[str]: """Returns a list of all the known MISP attributes types""" - return self._types + return self.describe_types['types'] @property - def malware_binary(self): - """Returns a BytesIO of the malware (if the attribute has one, obvs).""" + def malware_binary(self) -> BytesIO | None: + """Returns a BytesIO of the malware, if the attribute has one. + Decrypts, unpacks and caches the binary on the first invocation, + which may require some time for large attachments (~1s/MB). + """ + if self.type != 'malware-sample': + # Not a malware sample + return None if hasattr(self, '_malware_binary'): + # Already unpacked + return self._malware_binary + elif hasattr(self, 'malware_filename'): + # Have a binary, but didn't decrypt it yet + with ZipFile(self.data) as f: # type: ignore + for name in f.namelist(): + if not name.endswith('.filename.txt'): + with f.open(name, pwd=b'infected') as unpacked: + self._malware_binary = BytesIO(unpacked.read()) return self._malware_binary return None @property - def shadow_attributes(self): + def shadow_attributes(self) -> list[MISPShadowAttribute]: return self.ShadowAttribute @shadow_attributes.setter - def shadow_attributes(self, shadow_attributes): + def shadow_attributes(self, shadow_attributes: list[MISPShadowAttribute]) -> None: """Set a list of prepared MISPShadowAttribute.""" if all(isinstance(x, MISPShadowAttribute) for x in shadow_attributes): self.ShadowAttribute = shadow_attributes else: raise PyMISPError('All the attributes have to be of type MISPShadowAttribute.') - def delete(self): + @property + def sightings(self) -> list[MISPSighting]: + return self.Sighting + + @sightings.setter + def sightings(self, sightings: list[MISPSighting]) -> None: + """Set a list of prepared MISPSighting.""" + if all(isinstance(x, MISPSighting) for x in sightings): + self.Sighting = sightings + else: + raise PyMISPError('All the attributes have to be of type MISPSighting.') + + def delete(self) -> None: """Mark the attribute as deleted (soft delete)""" self.deleted = True - def add_proposal(self, shadow_attribute=None, **kwargs): + def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Alias for add_shadow_attribute""" return self.add_shadow_attribute(shadow_attribute, **kwargs) - def add_shadow_attribute(self, shadow_attribute=None, **kwargs): - """Add a tag to the attribute (by name or a MISPTag object)""" + def add_shadow_attribute(self, shadow_attribute: MISPShadowAttribute | dict[str, Any] | None = None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] + """Add a shadow attribute to the attribute (by name or a MISPShadowAttribute object)""" if isinstance(shadow_attribute, MISPShadowAttribute): misp_shadow_attribute = shadow_attribute elif isinstance(shadow_attribute, dict): @@ -170,14 +534,30 @@ class MISPAttribute(AbstractMISP): misp_shadow_attribute = MISPShadowAttribute() misp_shadow_attribute.from_dict(**kwargs) else: - raise PyMISPError("The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict): {}".format(shadow_attribute)) + raise PyMISPError(f"The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {shadow_attribute}") self.shadow_attributes.append(misp_shadow_attribute) self.edited = True return misp_shadow_attribute - def from_dict(self, **kwargs): - if kwargs.get('Attribute'): - kwargs = kwargs.get('Attribute') + def add_sighting(self, sighting: MISPSighting | dict[str, Any] | None = None, **kwargs) -> MISPSighting: # type: ignore[no-untyped-def] + """Add a sighting to the attribute (by name or a MISPSighting object)""" + if isinstance(sighting, MISPSighting): + misp_sighting = sighting + elif isinstance(sighting, dict): + misp_sighting = MISPSighting() + misp_sighting.from_dict(**sighting) + elif kwargs: + misp_sighting = MISPSighting() + misp_sighting.from_dict(**kwargs) + else: + raise PyMISPError(f"The sighting is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {sighting}") + self.sightings.append(misp_sighting) + self.edited = True + return misp_sighting + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Attribute' in kwargs: + kwargs = kwargs['Attribute'] if kwargs.get('type') and kwargs.get('category'): if kwargs['type'] not in self.__category_type_mapping[kwargs['category']]: if self.__strict: @@ -190,13 +570,28 @@ class MISPAttribute(AbstractMISP): if self.type is None: raise NewAttributeError('The type of the attribute is required.') if self.type not in self.known_types: - raise NewAttributeError('{} is invalid, type has to be in {}'.format(self.type, (', '.join(self._types)))) + raise NewAttributeError('{} is invalid, type has to be in {}'.format(self.type, (', '.join(self.known_types)))) type_defaults = self.__sane_default[self.type] self.value = kwargs.pop('value', None) if self.value is None: raise NewAttributeError('The value of the attribute is required.') + if self.type == 'datetime' and isinstance(self.value, str): + try: + # Faster + if sys.version_info >= (3, 7): + self.value = datetime.fromisoformat(self.value) + else: + if '+' in self.value or '-' in self.value: + self.value = datetime.strptime(self.value, "%Y-%m-%dT%H:%M:%S.%f%z") + elif '.' in self.value: + self.value = datetime.strptime(self.value, "%Y-%m-%dT%H:%M:%S.%f") + else: + self.value = datetime.strptime(self.value, "%Y-%m-%dT%H:%M:%S") + except ValueError: + # Slower, but if the other ones fail, that's a good fallback + self.value = parse(self.value) # Default values self.category = kwargs.pop('category', type_defaults['default_category']) @@ -213,27 +608,45 @@ class MISPAttribute(AbstractMISP): self.to_ids = make_bool(self.to_ids) if not isinstance(self.to_ids, bool): - raise NewAttributeError('{} is invalid, to_ids has to be True or False'.format(self.to_ids)) + raise NewAttributeError(f'{self.to_ids} is invalid, to_ids has to be True or False') self.distribution = kwargs.pop('distribution', None) if self.distribution is not None: self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4, 5]: - raise NewAttributeError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5'.format(self.distribution)) + raise NewAttributeError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') # other possible values if kwargs.get('data'): self.data = kwargs.pop('data') - self._load_data() if kwargs.get('id'): self.id = int(kwargs.pop('id')) if kwargs.get('event_id'): self.event_id = int(kwargs.pop('event_id')) if kwargs.get('timestamp'): - if sys.version_info >= (3, 3): - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), datetime.timezone.utc) + ts = kwargs.pop('timestamp') + if isinstance(ts, datetime): + self.timestamp = ts else: - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), UTC()) + self.timestamp = datetime.fromtimestamp(int(ts), timezone.utc) + if kwargs.get('first_seen'): + fs = kwargs.pop('first_seen') + try: + # Faster + self.first_seen = datetime.fromisoformat(fs) + except Exception: + # Use __setattr__ + self.first_seen = fs + + if kwargs.get('last_seen'): + ls = kwargs.pop('last_seen') + try: + # Faster + self.last_seen = datetime.fromisoformat(ls) + except Exception: + # Use __setattr__ + self.last_seen = ls + if kwargs.get('sharing_group_id'): self.sharing_group_id = int(kwargs.pop('sharing_group_id')) @@ -243,44 +656,45 @@ class MISPAttribute(AbstractMISP): raise NewAttributeError('If the distribution is set to sharing group, a sharing group ID is required.') elif not self.sharing_group_id: # Cannot be None or 0 either. - raise NewAttributeError('If the distribution is set to sharing group, a sharing group ID is required (cannot be {}).'.format(self.sharing_group_id)) + raise NewAttributeError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') if kwargs.get('Tag'): - for tag in kwargs.pop('Tag'): - self.add_tag(tag) + [self.add_tag(tag) for tag in kwargs.pop('Tag')] + if kwargs.get('Galaxy'): + [self.add_galaxy(galaxy) for galaxy in kwargs.pop('Galaxy')] + if kwargs.get('Sighting'): + [self.add_sighting(sighting) for sighting in kwargs.pop('Sighting')] if kwargs.get('ShadowAttribute'): - for s_attr in kwargs.pop('ShadowAttribute'): - self.add_shadow_attribute(s_attr) + [self.add_shadow_attribute(s_attr) for s_attr in kwargs.pop('ShadowAttribute')] + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) # If the user wants to disable correlation, let them. Defaults to False. self.disable_correlation = kwargs.pop("disable_correlation", False) if self.disable_correlation is None: self.disable_correlation = False - super(MISPAttribute, self).from_dict(**kwargs) + super().from_dict(**kwargs) - def to_dict(self): - to_return = super(MISPAttribute, self).to_dict() - if to_return.get('data'): + def to_dict(self, json_format: bool = False) -> dict[str, Any]: + to_return = super().to_dict(json_format) + if self.data: to_return['data'] = base64.b64encode(self.data.getvalue()).decode() return to_return - def _prepare_new_malware_sample(self): + def _prepare_new_malware_sample(self) -> None: if '|' in self.value: # Get the filename, ignore the md5, because humans. - self.malware_filename, md5 = self.value.split('|') + self.malware_filename, md5 = self.value.rsplit('|', 1) else: # Assuming the user only passed the filename self.malware_filename = self.value - # m = hashlib.md5() - # m.update(self.data.getvalue()) self.value = self.malware_filename - # md5 = m.hexdigest() - # self.value = '{}|{}'.format(self.malware_filename, md5) self._malware_binary = self.data self.encrypt = True - def __is_misp_encrypted_file(self, f): + def __is_misp_encrypted_file(self, f: ZipFile) -> bool: files_list = f.namelist() if len(files_list) != 2: return False @@ -295,218 +709,1152 @@ class MISPAttribute(AbstractMISP): return False return True - def _load_data(self): - if not isinstance(self.data, BytesIO): - self.data = BytesIO(base64.b64decode(self.data)) - if self.type == 'malware-sample': - try: - with ZipFile(self.data) as f: - if not self.__is_misp_encrypted_file(f): - raise Exception('Not an existing malware sample') - for name in f.namelist(): - if name.endswith('.filename.txt'): - with f.open(name, pwd=b'infected') as unpacked: - self.malware_filename = unpacked.read().decode().strip() - else: - with f.open(name, pwd=b'infected') as unpacked: - self._malware_binary = BytesIO(unpacked.read()) - except Exception: - # not a encrypted zip file, assuming it is a new malware sample - self._prepare_new_malware_sample() - - def __repr__(self): + def __repr__(self) -> str: if hasattr(self, 'value'): return '<{self.__class__.__name__}(type={self.type}, value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) - - def verify(self, gpg_uid): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - signed_data = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(signed_data, signature=base64.b64decode(self.sig), verify=keys[:1]) - return {self.uuid: True} - except Exception: - return {self.uuid: False} - - def _serialize(self): # pragma: no cover - # Not used - return '{type}{category}{to_ids}{uuid}{timestamp}{comment}{deleted}{value}'.format( - type=self.type, category=self.category, to_ids=self.to_ids, uuid=self.uuid, timestamp=self.timestamp, - comment=self.comment, deleted=self.deleted, value=self.value).encode() - - def sign(self, gpg_uid, passphrase=None): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_sign = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign, mode=mode.DETACH) - self.sig = base64.b64encode(signed).decode() - - @deprecated - def get_known_types(self): # pragma: no cover - return self.known_types - - @deprecated - def get_malware_binary(self): # pragma: no cover - return self.malware_binary - - @deprecated - def _json(self): # pragma: no cover - return self.to_dict() - - @deprecated - def _json_full(self): # pragma: no cover - return self.to_dict() - - @deprecated - def set_all_values(self, **kwargs): # pragma: no cover - self.from_dict(**kwargs) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPEvent(AbstractMISP): +class MISPObjectReference(AbstractMISP): - def __init__(self, describe_types=None, strict_validation=False, **kwargs): - super(MISPEvent, self).__init__(**kwargs) - ressources_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') - if strict_validation: - with open(os.path.join(ressources_path, 'schema.json'), 'rb') as f: - if OLD_PY3: - self.__json_schema = json.loads(f.read().decode()) - else: - self.__json_schema = json.load(f) + _fields_for_feed: set[str] = {'uuid', 'timestamp', 'relationship_type', 'comment', + 'object_uuid', 'referenced_uuid'} + + def __init__(self) -> None: + super().__init__() + self.uuid = str(uuid.uuid4()) + self.object_uuid: str + self.referenced_uuid: str + self.relationship_type: str + + def _set_default(self) -> None: + if not hasattr(self, 'comment'): + self.comment = '' + if not hasattr(self, 'timestamp'): + self.timestamp = datetime.timestamp(datetime.now()) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'ObjectReference' in kwargs: + kwargs = kwargs['ObjectReference'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'referenced_uuid') and hasattr(self, 'object_uuid'): + return '<{self.__class__.__name__}(object_uuid={self.object_uuid}, referenced_uuid={self.referenced_uuid}, relationship_type={self.relationship_type})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPObject(AnalystDataBehaviorMixin): + + _fields_for_feed: set[str] = {'name', 'meta-category', 'description', 'template_uuid', + 'template_version', 'uuid', 'timestamp', 'comment', + 'first_seen', 'last_seen', 'deleted'} + + _analyst_data_object_type = 'Object' + + def __init__(self, name: str, strict: bool = False, standalone: bool = True, # type: ignore[no-untyped-def] + default_attributes_parameters: dict[str, Any] = {}, **kwargs) -> None: + ''' Master class representing a generic MISP object + + :param name: Name of the object + :param strict: Enforce validation with the object templates + :param standalone: The object will be pushed as directly on MISP, not as a part of an event. + In this case the ObjectReference needs to be pushed manually and cannot be in the JSON dump. + :param default_attributes_parameters: Used as template for the attributes if they are not overwritten in add_attribute + :param misp_objects_path_custom: Path to custom object templates + :param misp_objects_template_custom: Template of the object. Expects the content (dict, loaded with json.load or json.loads) of a template definition file, see repository MISP/misp-objects. + ''' + super().__init__(**kwargs) + self._strict: bool = strict + self.name: str = name + self._known_template: bool = False + self.id: int + self._definition: dict[str, Any] | None + self.timestamp: float | int | datetime + + misp_objects_template_custom = kwargs.pop('misp_objects_template_custom', None) + misp_objects_path_custom = kwargs.pop('misp_objects_path_custom', None) + if misp_objects_template_custom: + self._set_template(misp_objects_template_custom=misp_objects_template_custom) else: - with open(os.path.join(ressources_path, 'schema-lax.json'), 'rb') as f: - if OLD_PY3: - self.__json_schema = json.loads(f.read().decode()) + # Fall back to default path if None + self._set_template(misp_objects_path_custom=misp_objects_path_custom) + + self.uuid: str = str(uuid.uuid4()) + self.first_seen: datetime + self.last_seen: datetime + self.__fast_attribute_access: dict[str, Any] = defaultdict(list) # Hashtable object_relation: [attributes] + self.ObjectReference: list[MISPObjectReference] = [] + self._standalone: bool = False + self.Attribute: list[MISPObjectAttribute] = [] + self.SharingGroup: MISPSharingGroup + self._default_attributes_parameters: dict[str, Any] + if isinstance(default_attributes_parameters, MISPAttribute): + # Just make sure we're not modifying an existing MISPAttribute + self._default_attributes_parameters = default_attributes_parameters.to_dict() + else: + self._default_attributes_parameters = copy.copy(default_attributes_parameters) + if self._default_attributes_parameters: + # Let's clean that up + self._default_attributes_parameters.pop('value', None) # duh + self._default_attributes_parameters.pop('uuid', None) # duh + self._default_attributes_parameters.pop('id', None) # duh + self._default_attributes_parameters.pop('object_id', None) # duh + self._default_attributes_parameters.pop('type', None) # depends on the value + self._default_attributes_parameters.pop('object_relation', None) # depends on the value + self._default_attributes_parameters.pop('disable_correlation', None) # depends on the value + self._default_attributes_parameters.pop('to_ids', None) # depends on the value + self._default_attributes_parameters.pop('deleted', None) # doesn't make sense to pre-set it + self._default_attributes_parameters.pop('data', None) # in case the original in a sample or an attachment + + # Those values are set for the current object, if they exist, but not pop'd because they are still useful for the attributes + self.distribution: int = self._default_attributes_parameters.get('distribution', 5) + self.sharing_group_id: int = self._default_attributes_parameters.get('sharing_group_id', 0) + else: + self.distribution = 5 # Default to inherit + self.sharing_group_id = 0 + self.standalone = standalone + + def _load_template_path(self, template_path: Path | str) -> bool: + template = self._load_json(template_path) + if not template: + self._definition = None + return False + self._load_template(template) + return True + + def _load_template(self, template: dict[str, Any]) -> None: + self._definition = template + setattr(self, 'meta-category', self._definition['meta-category']) + self.template_uuid = self._definition['uuid'] + self.description = self._definition['description'] + self.template_version = self._definition['version'] + + def _set_default(self) -> None: + if not hasattr(self, 'comment'): + self.comment = '' + if not hasattr(self, 'timestamp'): + self.timestamp = datetime.timestamp(datetime.now()) + + def _to_feed(self, with_distribution: bool=False) -> dict[str, Any]: + if with_distribution: + self._fields_for_feed.add('distribution') + if not hasattr(self, 'template_uuid'): # workaround for old events where the template_uuid was not yet mandatory + self.template_uuid = str(uuid.uuid5(uuid.UUID("9319371e-2504-4128-8410-3741cebbcfd3"), self.name)) + if not hasattr(self, 'description'): # workaround for old events where description is not always set + self.description = '' + if not hasattr(self, 'meta-category'): # workaround for old events where meta-category is not always set + setattr(self, 'meta-category', 'misc') + to_return = super()._to_feed() + if self.references: + to_return['ObjectReference'] = [reference._to_feed() for reference in self.references] + if with_distribution: + try: + to_return['SharingGroup'] = self.SharingGroup._to_feed() + except AttributeError: + pass + return to_return + + def __setattr__(self, name: str, value: Any) -> None: + if name in ['first_seen', 'last_seen']: + value = _make_datetime(value) + + if name == 'last_seen' and hasattr(self, 'first_seen') and self.first_seen > value: + logger.warning(f'last_seen ({value}) has to be after first_seen ({self.first_seen})') + if name == 'first_seen' and hasattr(self, 'last_seen') and self.last_seen < value: + logger.warning(f'first_seen ({value}) has to be before last_seen ({self.last_seen})') + super().__setattr__(name, value) + + def force_misp_objects_path_custom(self, misp_objects_path_custom: Path | str, object_name: str | None = None) -> None: + if object_name: + self.name = object_name + self._set_template(misp_objects_path_custom) + + def _set_template(self, misp_objects_path_custom: Path | str | None = None, misp_objects_template_custom: dict[str, Any] | None = None) -> None: + if misp_objects_template_custom: + # A complete template was given to the constructor + self._load_template(misp_objects_template_custom) + self._known_template = True + else: + if misp_objects_path_custom: + # If misp_objects_path_custom is given, and an object with the given name exists, use that. + if isinstance(misp_objects_path_custom, str): + self.misp_objects_path = Path(misp_objects_path_custom) else: - self.__json_schema = json.load(f) + self.misp_objects_path = misp_objects_path_custom + + # Try to get the template + self._known_template = self._load_template_path(self.misp_objects_path / self.name / 'definition.json') + + if not self._known_template and self._strict: + raise UnknownMISPObjectTemplate(f'{self.name} is unknown in the MISP object directory.') + else: + # Then we have no meta-category, template_uuid, description and template_version + pass + + def delete(self) -> None: + """Mark the object as deleted (soft delete)""" + self.deleted = True + for a in self.attributes: + a.delete() + + @property + def disable_validation(self) -> None: + self._strict = False + + @property + def attributes(self) -> list[MISPObjectAttribute]: + return self.Attribute + + @attributes.setter + def attributes(self, attributes: list[MISPObjectAttribute]) -> None: + if all(isinstance(x, MISPObjectAttribute) for x in attributes): + self.Attribute = attributes + self.__fast_attribute_access = defaultdict(list) + else: + raise PyMISPError('All the attributes have to be of type MISPObjectAttribute.') + + @property + def references(self) -> list[MISPObjectReference]: + return self.ObjectReference + + @references.setter + def references(self, references: list[MISPObjectReference]) -> None: + if all(isinstance(x, MISPObjectReference) for x in references): + self.ObjectReference = references + else: + raise PyMISPError('All the attributes have to be of type MISPObjectReference.') + + @property + def standalone(self) -> bool: + return self._standalone + + @standalone.setter + def standalone(self, new_standalone: bool) -> None: + if self._standalone != new_standalone: + if new_standalone: + self.update_not_jsonable("ObjectReference") + else: + self._remove_from_not_jsonable("ObjectReference") + self._standalone = new_standalone + else: + pass + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Object' in kwargs: + kwargs = kwargs['Object'] + if self._known_template: + if kwargs.get('template_uuid') and kwargs['template_uuid'] != self.template_uuid: + if self._strict: + raise UnknownMISPObjectTemplate('UUID of the object is different from the one of the template.') + else: + self._known_template = False + if kwargs.get('template_version') and int(kwargs['template_version']) != self.template_version: + if self._strict: + raise UnknownMISPObjectTemplate('Version of the object ({}) is different from the one of the template ({}).'.format(kwargs['template_version'], self.template_version)) + else: + self._known_template = False + + # depending on how the object is initialized, we may have a few keys to pop + kwargs.pop('misp_objects_template_custom', None) + kwargs.pop('misp_objects_path_custom', None) + + if 'distribution' in kwargs and kwargs['distribution'] is not None: + self.distribution = kwargs.pop('distribution') + self.distribution = int(self.distribution) + if self.distribution not in [0, 1, 2, 3, 4, 5]: + raise NewAttributeError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') + + if kwargs.get('timestamp'): + ts = kwargs.pop('timestamp') + if isinstance(ts, datetime): + self.timestamp = ts + else: + self.timestamp = datetime.fromtimestamp(int(ts), timezone.utc) + + if kwargs.get('first_seen'): + fs = kwargs.pop('first_seen') + try: + # Faster + self.first_seen = datetime.fromisoformat(fs) + except Exception: + # Use __setattr__ + self.first_seen = fs + + if kwargs.get('last_seen'): + ls = kwargs.pop('last_seen') + try: + # Faster + self.last_seen = datetime.fromisoformat(ls) + except Exception: + # Use __setattr__ + self.last_seen = ls + + if kwargs.get('Attribute'): + [self.add_attribute(**a) for a in kwargs.pop('Attribute')] + if kwargs.get('ObjectReference'): + [self.add_reference(**r) for r in kwargs.pop('ObjectReference')] + + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) + # Not supported yet - https://github.com/MISP/PyMISP/issues/168 + # if kwargs.get('Tag'): + # for tag in kwargs.pop('Tag'): + # self.add_tag(tag) + + super().from_dict(**kwargs) + + def add_reference(self, referenced_uuid: AbstractMISP | str, relationship_type: str, comment: str | None = None, **kwargs) -> MISPObjectReference: # type: ignore[no-untyped-def] + """Add a link (uuid) to another object""" + if isinstance(referenced_uuid, AbstractMISP): + # Allow to pass an object or an attribute instead of its UUID + referenced_uuid = referenced_uuid.uuid + if 'object_uuid' in kwargs and not kwargs.get('object_uuid'): + # Unexplained None in object_uuid key -> https://github.com/MISP/PyMISP/issues/640 + kwargs.pop('object_uuid') + object_uuid = self.uuid + elif kwargs.get('object_uuid'): + # Load existing object + object_uuid = kwargs.pop('object_uuid') + else: + # New reference + object_uuid = self.uuid + reference = MISPObjectReference() + reference.from_dict(object_uuid=object_uuid, referenced_uuid=referenced_uuid, + relationship_type=relationship_type, comment=comment, **kwargs) + self.ObjectReference.append(reference) + self.edited = True + return reference + + def get_attribute_by_id(self, attribute_id: str | int) -> MISPObjectAttribute: + """Get an object attribute by ID + + :param attribute_id: The ID of the seeking object attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'id') and attribute.id == attribute_id: + return attribute + + raise InvalidMISPObjectAttribute(f'Object attribute with {attribute_id} does not exist in this event') + + def get_attribute_by_uuid(self, attribute_uuid: str) -> MISPObjectAttribute: + """Get an object attribute by UUID + + :param attribute_uuid: The UUID of the seeking object attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'uuid') and attribute.uuid == attribute_uuid: + return attribute + + raise InvalidMISPObjectAttribute(f'Object attribute with {attribute_uuid} does not exist in this event') + + def get_attributes_by_relation(self, object_relation: str) -> list[MISPAttribute]: + '''Returns the list of attributes with the given object relation in the object''' + return self._fast_attribute_access.get(object_relation, []) + + @property + def _fast_attribute_access(self) -> dict[str, Any]: + if not self.__fast_attribute_access: + for a in self.attributes: + self.__fast_attribute_access[a.object_relation].append(a) + return self.__fast_attribute_access + + def has_attributes_by_relation(self, list_of_relations: list[str]) -> bool: + '''True if all the relations in the list are defined in the object''' + return all(relation in self._fast_attribute_access for relation in list_of_relations) + + def add_attribute(self, object_relation: str, simple_value: str | int | float | None = None, **value) -> MISPAttribute | None: # type: ignore[no-untyped-def] + """Add an attribute. + :param object_relation: The object relation of the attribute you're adding to the object + :param simple_value: The value + :param value: dictionary with all the keys supported by MISPAttribute + + Note: as long as PyMISP knows about the object template, only the object_relation and the simple_value are required. + If PyMISP doesn't know the template, you also **must** pass a type. + All the other options that can be passed along when creating an attribute (comment, IDS flag, ...) + will be either taked out of the template, or out of the default setting for the type as defined on the MISP instance. + """ + if simple_value is not None: # /!\ The value *can* be 0 + value['value'] = simple_value + if value.get('value') is None: + logger.warning(f"The value of the attribute you're trying to add is None, skipping it. Object relation: {object_relation}") + return None + else: + if isinstance(value['value'], bytes): + # That shouldn't happen, but we live in the real world, and it does. + # So we try to decode (otherwise, MISP barf), and raise a warning if needed. + try: + value['value'] = value['value'].decode() + except Exception: + logger.warning("The value of the attribute you're trying to add is a bytestream ({!r}), and we're unable to make it a string.".format(value['value'])) + return None + + # Make sure we're not adding an empty value. + if isinstance(value['value'], str): + value['value'] = value['value'].strip().strip('\x00') + if value['value'] == '': + logger.warning(f"The value of the attribute you're trying to add is an empty string, skipping it. Object relation: {object_relation}") + return None + if self._known_template and self._definition: + if object_relation in self._definition['attributes']: + attribute = MISPObjectAttribute(self._definition['attributes'][object_relation]) + else: + # Woopsie, this object_relation is unknown, no sane defaults for you. + logger.warning(f"The template ({self.name}) doesn't have the object_relation ({object_relation}) you're trying to add. If you are creating a new event to push to MISP, please review your code so it matches the template.") + attribute = MISPObjectAttribute({}) + else: + attribute = MISPObjectAttribute({}) + # Overwrite the parameters of self._default_attributes_parameters with the ones of value + attribute.from_dict(object_relation=object_relation, **{**self._default_attributes_parameters, **value}) + self.__fast_attribute_access[object_relation].append(attribute) + self.Attribute.append(attribute) + self.edited = True + return attribute + + def add_attributes(self, object_relation: str, *attributes: Sequence[str | dict[str, Any] | MISPAttribute]) -> list[MISPAttribute | None]: + '''Add multiple attributes with the same object_relation. + Helper for object_relation when multiple is True in the template. + It is the same as calling multiple times add_attribute with the same object_relation. + ''' + to_return = [] + for attribute in attributes: + if isinstance(attribute, MISPAttribute): + a = self.add_attribute(object_relation, **attribute.to_dict()) + elif isinstance(attribute, dict): + a = self.add_attribute(object_relation, **attribute) # type: ignore[misc] + else: + a = self.add_attribute(object_relation, value=attribute) + to_return.append(a) + return to_return + + def to_dict(self, json_format: bool = False, strict: bool = False) -> dict[str, Any]: + if strict or self._strict and self._known_template: + self._validate() + return super().to_dict(json_format) + + def to_json(self, sort_keys: bool = False, indent: int | None = None, strict: bool = False) -> str: + if strict or self._strict and self._known_template: + self._validate() + return super().to_json(sort_keys=sort_keys, indent=indent) + + def _validate(self) -> bool: + if not self._definition: + raise PyMISPError('No object definition available, unable to validate.') + """Make sure the object we're creating has the required fields""" + if self._definition.get('required'): + required_missing = set(self._definition['required']) - set(self._fast_attribute_access.keys()) + if required_missing: + raise InvalidMISPObject(f'{required_missing} are required.') + if self._definition.get('requiredOneOf'): + if not set(self._definition['requiredOneOf']) & set(self._fast_attribute_access.keys()): + # We ecpect at least one of the object_relation in requiredOneOf, and it isn't the case + raise InvalidMISPObject('At least one of the following attributes is required: {}'.format(', '.join(self._definition['requiredOneOf']))) + for rel, attrs in self._fast_attribute_access.items(): + if len(attrs) == 1: + # object_relation's here only once, everything's cool, moving on + continue + if not self._definition['attributes'][rel].get('multiple'): + # object_relation's here more than once, but it isn't allowed in the template. + raise InvalidMISPObject(f'Multiple occurrences of {rel} is not allowed') + return True + + def __repr__(self) -> str: + if hasattr(self, 'name'): + return '<{self.__class__.__name__}(name={self.name})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPEventReport(AnalystDataBehaviorMixin): + + _fields_for_feed: set[str] = {'uuid', 'name', 'content', 'timestamp', 'deleted'} + _analyst_data_object_type = 'EventReport' + + timestamp: float | int | datetime + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.uuid: str = str(uuid.uuid4()) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'EventReport' in kwargs: + kwargs = kwargs['EventReport'] + + self.distribution = kwargs.pop('distribution', None) + if self.distribution is not None: + self.distribution = int(self.distribution) + if self.distribution not in [0, 1, 2, 3, 4, 5]: + raise NewEventReportError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') + + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + + if self.distribution == 4: + # The distribution is set to sharing group, a sharing_group_id is required. + if not hasattr(self, 'sharing_group_id'): + raise NewEventReportError('If the distribution is set to sharing group, a sharing group ID is required.') + elif not self.sharing_group_id: + # Cannot be None or 0 either. + raise NewEventReportError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') + + self.name = kwargs.pop('name', None) + if self.name is None: + raise NewEventReportError('The name of the event report is required.') + + self.content = kwargs.pop('content', None) + if self.content is None: + raise NewAttributeError('The content of the event report is required.') + + if kwargs.get('id'): + self.id = int(kwargs.pop('id')) + if kwargs.get('event_id'): + self.event_id = int(kwargs.pop('event_id')) + if kwargs.get('timestamp'): + ts = kwargs.pop('timestamp') + if isinstance(ts, datetime): + self.timestamp = ts + else: + self.timestamp = datetime.fromtimestamp(int(ts), timezone.utc) + if kwargs.get('deleted'): + self.deleted = kwargs.pop('deleted') + + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'name'): + return '<{self.__class__.__name__}(name={self.name})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + def _set_default(self) -> None: + if not hasattr(self, 'timestamp'): + self.timestamp = datetime.timestamp(datetime.now()) + if not hasattr(self, 'name'): + self.name = '' + if not hasattr(self, 'content'): + self.content = '' + + +class MISPGalaxyClusterElement(AbstractMISP): + """A MISP Galaxy cluster element, providing further info on a cluster + + Creating a new galaxy cluster element can take the following parameters + + :param key: The key/identifier of the element + :type key: str + :param value: The value of the element + :type value: str + """ + + key: str + value: str + + def __repr__(self) -> str: + if hasattr(self, 'key') and hasattr(self, 'value'): + return '<{self.__class__.__name__}(key={self.key}, value={self.value})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + def __setattr__(self, key: str, value: Any) -> None: + if key == "value" and isinstance(value, list): + raise PyMISPError("You tried to set a list to a cluster element's value. " + "Instead, create seperate elements for each value") + super().__setattr__(key, value) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if kwargs.get('id'): + self.id = int(kwargs.pop('id')) + if kwargs.get('galaxy_cluster_id'): + self.galaxy_cluster_id = int(kwargs.pop('galaxy_cluster_id')) + + super().from_dict(**kwargs) + + +class MISPGalaxyClusterRelation(AbstractMISP): + """A MISP Galaxy cluster relation, linking one cluster to another + + Creating a new galaxy cluster can take the following parameters + + :param galaxy_cluster_uuid: The UUID of the galaxy the relation links to + :param referenced_galaxy_cluster_type: The relation type, e.g. dropped-by + :param referenced_galaxy_cluster_uuid: The UUID of the related galaxy + :param distribution: The distribution of the relation, one of 0, 1, 2, 3, 4, default 0 + :param sharing_group_id: The sharing group of the relation, only when distribution is 4 + """ + + def __repr__(self) -> str: + if hasattr(self, "referenced_galaxy_cluster_type"): + return '<{self.__class__.__name__}(referenced_galaxy_cluster_type={self.referenced_galaxy_cluster_type})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + def __init__(self) -> None: + super().__init__() + self.galaxy_cluster_uuid: str + self.referenced_galaxy_cluster_uuid: str + self.distribution: int = 0 + self.referenced_galaxy_cluster_type: str + self.Tag: list[MISPTag] = [] + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + # Default values for a valid event to send to a MISP instance + self.distribution = int(kwargs.pop('distribution', 0)) + if self.distribution not in [0, 1, 2, 3, 4, 5]: + raise NewGalaxyClusterRelationError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4') + + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + + if self.distribution == 4: + # The distribution is set to sharing group, a sharing_group_id is required. + if not hasattr(self, 'sharing_group_id'): + raise NewGalaxyClusterRelationError('If the distribution is set to sharing group, a sharing group ID is required.') + elif not self.sharing_group_id: + # Cannot be None or 0 either. + raise NewGalaxyClusterRelationError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') + + if kwargs.get('id'): + self.id = int(kwargs.pop('id')) + if kwargs.get('orgc_id'): + self.orgc_id = int(kwargs.pop('orgc_id')) + if kwargs.get('org_id'): + self.org_id = int(kwargs.pop('org_id')) + if kwargs.get('galaxy_id'): + self.galaxy_id = int(kwargs.pop('galaxy_id')) + if kwargs.get('tag_id'): + self.tag_id = int(kwargs.pop('tag_id')) + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + if kwargs.get('Tag'): + [self.add_tag(**t) for t in kwargs.pop('Tag')] + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) + super().from_dict(**kwargs) + + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] + return super()._add_tag(tag, **kwargs) + + @property + def tags(self) -> list[MISPTag]: + """Returns a list of tags associated to this Attribute""" + return self.Tag + + @tags.setter + def tags(self, tags: list[MISPTag]) -> None: + """Set a list of prepared MISPTag.""" + super()._set_tags(tags) + + +class MISPGalaxyCluster(AbstractMISP): + """A MISP galaxy cluster, storing respective galaxy elements and relations. + Used to view default galaxy clusters and add/edit/update/delete Galaxy 2.0 clusters + + Creating a new galaxy cluster can take the following parameters + + :param value: The value of the galaxy cluster + :type value: str + :param description: The description of the galaxy cluster + :type description: str + :param distribution: The distribution type, one of 0, 1, 2, 3, 4 + :type distribution: int + :param sharing_group_id: The sharing group ID, if distribution is set to 4 + :type sharing_group_id: int, optional + :param authors: A list of authors of the galaxy cluster + :type authors: list[str], optional + :param cluster_elements: List of MISPGalaxyClusterElement + :type cluster_elements: list[MISPGalaxyClusterElement], optional + :param cluster_relations: List of MISPGalaxyClusterRelation + :type cluster_relations: list[MISPGalaxyClusterRelation], optional + """ + + id: int | str | None + tag_name: str + galaxy_id: str | None + + def __init__(self) -> None: + super().__init__() + self.Galaxy: MISPGalaxy + self.GalaxyElement: list[MISPGalaxyClusterElement] = [] + self.meta: dict[str, Any] = {} + self.GalaxyClusterRelation: list[MISPGalaxyClusterRelation] = [] + self.Org: MISPOrganisation + self.Orgc: MISPOrganisation + self.SharingGroup: MISPSharingGroup + self.value: str + # Set any inititialized cluster to be False + self.default = False + + @property + def cluster_elements(self) -> list[MISPGalaxyClusterElement]: + return self.GalaxyElement + + @cluster_elements.setter + def cluster_elements(self, cluster_elements: list[MISPGalaxyClusterElement]) -> None: + self.GalaxyElement = cluster_elements + + @property + def cluster_relations(self) -> list[MISPGalaxyClusterRelation]: + return self.GalaxyClusterRelation + + @cluster_relations.setter + def cluster_relations(self, cluster_relations: list[MISPGalaxyClusterRelation]) -> None: + self.GalaxyClusterRelation = cluster_relations + + def parse_meta_as_elements(self) -> None: + """Function to parse the meta field into GalaxyClusterElements""" + # Parse the cluster elements from the kwargs meta fields + for key, value in self.meta.items(): + # The meta will merge fields together, i.e. Two 'countries' will be a list, so split these up + if not isinstance(value, list): + value = [value] + for v in value: + self.add_cluster_element(key=key, value=v) + + @property + def elements_meta(self) -> dict[str, Any]: + """Function to return the galaxy cluster elements as a dictionary structure of lists + that comes from a MISPGalaxy within a MISPEvent. Lossy, you lose the element ID + """ + response = defaultdict(list) + for element in self.cluster_elements: + response[element.key].append(element.value) + return dict(response) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'GalaxyCluster' in kwargs: + kwargs = kwargs['GalaxyCluster'] + self.default = kwargs.pop('default', False) + # If the default field is set, we shouldn't have distribution or sharing group ID set + if self.default: + blocked_fields = ["distribution" "sharing_group_id"] + for field in blocked_fields: + if kwargs.get(field, None): + raise NewGalaxyClusterError( + f"The field '{field}' cannot be set on a default galaxy cluster" + ) + + self.distribution = int(kwargs.pop('distribution', 0)) + if self.distribution not in [0, 1, 2, 3, 4]: + raise NewGalaxyClusterError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4') + + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + + if self.distribution == 4: + # The distribution is set to sharing group, a sharing_group_id is required. + if not hasattr(self, 'sharing_group_id'): + raise NewGalaxyClusterError('If the distribution is set to sharing group, a sharing group ID is required.') + elif not self.sharing_group_id: + # Cannot be None or 0 either. + raise NewGalaxyClusterError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') + + if 'uuid' in kwargs: + self.uuid = kwargs.pop('uuid') + if 'meta' in kwargs: + self.meta = kwargs.pop('meta') + if 'Galaxy' in kwargs: + self.Galaxy = MISPGalaxy() + self.Galaxy.from_dict(**kwargs.pop('Galaxy')) + if 'GalaxyElement' in kwargs: + [self.add_cluster_element(**e) for e in kwargs.pop('GalaxyElement')] + if 'Org' in kwargs: + self.Org = MISPOrganisation() + self.Org.from_dict(**kwargs.pop('Org')) + if 'Orgc' in kwargs: + self.Orgc = MISPOrganisation() + self.Orgc.from_dict(**kwargs.pop('Orgc')) + if 'GalaxyClusterRelation' in kwargs: + [self.add_cluster_relation(**r) for r in kwargs.pop('GalaxyClusterRelation')] + if 'SharingGroup' in kwargs: + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) + super().from_dict(**kwargs) + + def add_cluster_element(self, key: str, value: str, **kwargs) -> MISPGalaxyClusterElement: # type: ignore[no-untyped-def] + """Add a cluster relation to a MISPGalaxyCluster, key and value are required + + :param key: The key name of the element + :type key: str + :param value: The value of the element + :type value: str + """ + + cluster_element = MISPGalaxyClusterElement() + cluster_element.from_dict(key=key, value=value, **kwargs) + self.cluster_elements.append(cluster_element) + return cluster_element + + def add_cluster_relation(self, referenced_galaxy_cluster_uuid: MISPGalaxyCluster | str | UUID, referenced_galaxy_cluster_type: str, galaxy_cluster_uuid: str | None = None, **kwargs: dict[str, Any]) -> MISPGalaxyClusterRelation: + """Add a cluster relation to a MISPGalaxyCluster. + + :param referenced_galaxy_cluster_uuid: UUID of the related cluster + :type referenced_galaxy_cluster_uuid: uuid + :param referenced_galaxy_cluster_type: Relation type + :type referenced_galaxy_cluster_type: uuid + :param galaxy_cluster_uuid: UUID of this cluster, leave blank to use the stored UUID + :param galaxy_cluster_uuid: uuid, Optional + """ + + if not getattr(self, "uuid", None): + raise PyMISPError("The cluster does not have a UUID, make sure it is a valid galaxy cluster") + cluster_relation = MISPGalaxyClusterRelation() + + if isinstance(referenced_galaxy_cluster_uuid, MISPGalaxyCluster): + referenced_galaxy_cluster_uuid = referenced_galaxy_cluster_uuid.uuid + + cluster_relation.from_dict( + referenced_galaxy_cluster_uuid=referenced_galaxy_cluster_uuid, + referenced_galaxy_cluster_type=referenced_galaxy_cluster_type, + galaxy_cluster_uuid=galaxy_cluster_uuid or self.uuid, + **kwargs + ) + self.cluster_relations.append(cluster_relation) + return cluster_relation + + def __repr__(self) -> str: + if hasattr(self, 'value'): + return '<{self.__class__.__name__}(value={self.value})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPGalaxy(AbstractMISP): + """Galaxy class, used to view a galaxy and respective clusters""" + + id: str | None + + def __init__(self) -> None: + super().__init__() + self.GalaxyCluster: list[MISPGalaxyCluster] = [] + self.name: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + """Galaxy could be in one of the following formats: + {'Galaxy': {}, 'GalaxyCluster': []} + {'Galaxy': {'GalaxyCluster': []}} + """ + + if 'GalaxyCluster' in kwargs and kwargs.get("withCluster", True): + # Parse the cluster from the kwargs + [self.add_galaxy_cluster(**e) for e in kwargs.pop('GalaxyCluster')] + + if 'Galaxy' in kwargs: + kwargs = kwargs['Galaxy'] + super().from_dict(**kwargs) + + @property + def clusters(self) -> list[MISPGalaxyCluster]: + return self.GalaxyCluster + + def add_galaxy_cluster(self, **kwargs) -> MISPGalaxyCluster: # type: ignore[no-untyped-def] + """Add a MISP galaxy cluster into a MISPGalaxy. + Supports all other parameters supported by MISPGalaxyCluster""" + + galaxy_cluster = MISPGalaxyCluster() + galaxy_cluster.from_dict(**kwargs) + self.clusters.append(galaxy_cluster) + return galaxy_cluster + + def __repr__(self) -> str: + if hasattr(self, 'name'): + return '<{self.__class__.__name__}(name={self.name})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPEvent(AnalystDataBehaviorMixin): + + _fields_for_feed: set[str] = {'uuid', 'info', 'threat_level_id', 'analysis', 'timestamp', + 'publish_timestamp', 'published', 'date', 'extends_uuid'} + + _analyst_data_object_type = 'Event' + + def __init__(self, describe_types: dict[str, Any] | None = None, strict_validation: bool = False, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(**kwargs) + self.__schema_file = 'schema.json' if strict_validation else 'schema-lax.json' + if describe_types: # This variable is used in add_attribute in order to avoid duplicating the structure - self._describe_types = describe_types - else: - with open(os.path.join(ressources_path, 'describeTypes.json'), 'rb') as f: - if OLD_PY3: - t = json.loads(f.read().decode()) - else: - t = json.load(f) - self._describe_types = t['result'] + self.describe_types = describe_types - self._types = self._describe_types['types'] - self.Attribute = [] - self.Object = [] - self.RelatedEvent = [] - self.ShadowAttribute = [] + self.uuid: str = str(uuid.uuid4()) + self.date: date + self.Attribute: list[MISPAttribute] = [] + self.Object: list[MISPObject] = [] + self.RelatedEvent: list[MISPEvent] = [] + self.ShadowAttribute: list[MISPShadowAttribute] = [] + self.SharingGroup: MISPSharingGroup + self.EventReport: list[MISPEventReport] = [] + self.Tag: list[MISPTag] = [] + self.Galaxy: list[MISPGalaxy] = [] + + self.publish_timestamp: float | int | datetime + self.timestamp: float | int | datetime + + def add_tag(self, tag: str | MISPTag | dict[str, Any] | None = None, **kwargs) -> MISPTag: # type: ignore[no-untyped-def] + return super()._add_tag(tag, **kwargs) @property - def known_types(self): - return self._types + def tags(self) -> list[MISPTag]: + """Returns a list of tags associated to this Event""" + return self.Tag + + @tags.setter + def tags(self, tags: list[MISPTag]) -> None: + """Set a list of prepared MISPTag.""" + super()._set_tags(tags) + + def _set_default(self) -> None: + """There are a few keys that could, or need to be set by default for the feed generator""" + if not hasattr(self, 'published'): + self.published = True + if not hasattr(self, 'uuid'): + self.uuid = str(uuid.uuid4()) + if not hasattr(self, 'extends_uuid'): + self.extends_uuid = '' + if not hasattr(self, 'date'): + self.set_date(date.today()) + if not hasattr(self, 'timestamp'): + self.timestamp = datetime.timestamp(datetime.now()) + if not hasattr(self, 'publish_timestamp'): + self.publish_timestamp = datetime.timestamp(datetime.now()) + if not hasattr(self, 'analysis'): + # analysis: 0 means initial, 1 ongoing, 2 completed + self.analysis = 2 + if not hasattr(self, 'threat_level_id'): + # threat_level_id 4 means undefined. Tags are recommended. + self.threat_level_id = 4 @property - def org(self): + def manifest(self) -> dict[str, Any]: + required = ['info', 'Orgc'] + for r in required: + if not hasattr(self, r): + raise PyMISPError('The field {} is required to generate the event manifest.') + + self._set_default() + + return { + self.uuid: { + 'Orgc': self.Orgc._to_feed(), + 'Tag': list(filter(None, [tag._to_feed() for tag in self.tags])), + 'info': self.info, + 'date': self.date.isoformat(), + 'analysis': self.analysis, + 'threat_level_id': self.threat_level_id, + 'timestamp': self._datetime_to_timestamp(self.timestamp) + } + } + + def attributes_hashes(self, algorithm: str = 'sha512') -> list[str]: + to_return: list[str] = [] + for attribute in self.attributes: + to_return += attribute.hash_values(algorithm) + for obj in self.objects: + for attribute in obj.attributes: + to_return += attribute.hash_values(algorithm) + return to_return + + def to_feed(self, valid_distributions: list[int] = [0, 1, 2, 3, 4, 5], with_meta: bool = False, with_distribution: bool=False, with_local_tags: bool = True, with_event_reports: bool = True) -> dict[str, Any]: + """ Generate a json output for MISP Feed. + + :param valid_distributions: only makes sense if the distribution key is set; i.e., the event is exported from a MISP instance. + :param with_distribution: exports distribution and Sharing Group info; otherwise all SharingGroup information is discarded (protecting privacy) + :param with_local_tags: tag export includes local exportable tags along with global exportable tags + :param with_event_reports: include event reports in the returned MISP event + """ + required = ['info', 'Orgc'] + for r in required: + if not hasattr(self, r): + raise PyMISPError(f'The field {r} is required to generate the event feed output.') + + if (hasattr(self, 'distribution') + and self.distribution is not None + and int(self.distribution) not in valid_distributions): + return {} + + if with_distribution: + self._fields_for_feed.add('distribution') + + to_return = super()._to_feed() + if with_meta: + to_return['_hashes'] = [] + to_return['_manifest'] = self.manifest + + to_return['Orgc'] = self.Orgc._to_feed() + to_return['Tag'] = list(filter(None, [tag._to_feed(with_local_tags) for tag in self.tags])) + if self.attributes: + to_return['Attribute'] = [] + for attribute in self.attributes: + if (valid_distributions and attribute.get('distribution') is not None and attribute.distribution not in valid_distributions): + continue + to_return['Attribute'].append(attribute._to_feed(with_distribution=with_distribution)) + if with_meta: + to_return['_hashes'] += attribute.hash_values('md5') + + if self.objects: + to_return['Object'] = [] + for obj in self.objects: + if (valid_distributions and obj.get('distribution') is not None and obj.distribution not in valid_distributions): + continue + if with_distribution: + obj._fields_for_feed.add('distribution') + obj_to_attach = obj._to_feed(with_distribution=with_distribution) + obj_to_attach['Attribute'] = [] + for attribute in obj.attributes: + if (valid_distributions and attribute.get('distribution') is not None and attribute.distribution not in valid_distributions): + continue + obj_to_attach['Attribute'].append(attribute._to_feed(with_distribution=with_distribution)) + if with_meta: + to_return['_hashes'] += attribute.hash_values('md5') + to_return['Object'].append(obj_to_attach) + + if with_distribution: + try: + to_return['SharingGroup'] = self.SharingGroup._to_feed() + except AttributeError: + pass + + if with_event_reports and self.event_reports: + to_return['EventReport'] = [] + for event_report in self.event_reports: + if (valid_distributions and event_report.get('distribution') is not None and event_report.distribution not in valid_distributions): + continue + if not with_distribution: + event_report.pop('distribution', None) + event_report.pop('SharingGroup', None) + event_report.pop('sharing_group_id', None) + to_return['EventReport'].append(event_report.to_dict()) + + return {'Event': to_return} + + @property + def known_types(self) -> list[str]: + return self.describe_types['types'] + + @property + def org(self) -> MISPOrganisation: return self.Org @property - def orgc(self): + def orgc(self) -> MISPOrganisation: return self.Orgc @orgc.setter - def orgc(self, orgc): + def orgc(self, orgc: MISPOrganisation) -> None: if isinstance(orgc, MISPOrganisation): self.Orgc = orgc else: raise PyMISPError('Orgc must be of type MISPOrganisation.') @property - def attributes(self): + def attributes(self) -> list[MISPAttribute]: return self.Attribute @attributes.setter - def attributes(self, attributes): + def attributes(self, attributes: list[MISPAttribute]) -> None: if all(isinstance(x, MISPAttribute) for x in attributes): self.Attribute = attributes else: raise PyMISPError('All the attributes have to be of type MISPAttribute.') @property - def shadow_attributes(self): + def event_reports(self) -> list[MISPEventReport]: + return self.EventReport + + @property + def shadow_attributes(self) -> list[MISPShadowAttribute]: return self.ShadowAttribute @shadow_attributes.setter - def shadow_attributes(self, shadow_attributes): + def shadow_attributes(self, shadow_attributes: list[MISPShadowAttribute]) -> None: if all(isinstance(x, MISPShadowAttribute) for x in shadow_attributes): self.ShadowAttribute = shadow_attributes else: raise PyMISPError('All the attributes have to be of type MISPShadowAttribute.') @property - def related_events(self): + def related_events(self) -> list[MISPEvent]: return self.RelatedEvent @property - def objects(self): + def galaxies(self) -> list[MISPGalaxy]: + return self.Galaxy + + @galaxies.setter + def galaxies(self, galaxies: list[MISPGalaxy]) -> None: + if all(isinstance(x, MISPGalaxy) for x in galaxies): + self.Galaxy = galaxies + else: + raise PyMISPError('All the attributes have to be of type MISPGalaxy.') + + @property + def objects(self) -> list[MISPObject]: return self.Object @objects.setter - def objects(self, objects): + def objects(self, objects: list[MISPObject]) -> None: if all(isinstance(x, MISPObject) for x in objects): self.Object = objects else: raise PyMISPError('All the attributes have to be of type MISPObject.') - def load_file(self, event_path): + def load_file(self, event_path: Path | str, validate: bool = False, metadata_only: bool = False) -> None: """Load a JSON dump from a file on the disk""" if not os.path.exists(event_path): raise PyMISPError('Invalid path, unable to load the event.') with open(event_path, 'rb') as f: - self.load(f) + self.load(f, validate, metadata_only) - def load(self, json_event, validate=False): + def load(self, json_event: IO[bytes] | IO[str] | str | bytes | dict[str, Any], validate: bool = False, metadata_only: bool = False) -> None: """Load a JSON dump from a pseudo file or a JSON string""" - if hasattr(json_event, 'read'): - # python2 and python3 compatible to find if we have a file + if isinstance(json_event, (BufferedIOBase, TextIOBase)): json_event = json_event.read() - if isinstance(json_event, (basestring, bytes)): - if OLD_PY3 and isinstance(json_event, bytes): - json_event = json_event.decode() + + if isinstance(json_event, (str, bytes)): json_event = json.loads(json_event) - if json_event.get('response'): - event = json_event.get('response')[0] + + if isinstance(json_event, dict) and 'response' in json_event and isinstance(json_event['response'], list): + event = json_event['response'][0] else: event = json_event if not event: raise PyMISPError('Invalid event') + if metadata_only: + event.pop('Attribute', None) + event.pop('Object', None) self.from_dict(**event) if validate: - jsonschema.validate(json.loads(self.to_json()), self.__json_schema) + warnings.warn('The validate parameter is deprecated because PyMISP is more flexible at loading event than the schema') - def set_date(self, date, ignore_invalid=False): - """Set a date for the event (string, datetime, or date object)""" - if isinstance(date, basestring) or isinstance(date, unicode): - self.date = parse(date).date() - elif isinstance(date, int): - self.date = datetime.datetime.utcfromtimestamp(date).date() - elif isinstance(date, datetime.datetime): - self.date = date.date() - elif isinstance(date, datetime.date): - self.date = date - else: - if ignore_invalid: - self.date = datetime.date.today() + def __setattr__(self, name: str, value: Any) -> None: + if name in ['date']: + if isinstance(value, date): + pass + elif isinstance(value, str): + try: + # faster + value = date.fromisoformat(value) + except Exception: + value = parse(value).date() + elif isinstance(value, (int, float)): + value = date.fromtimestamp(value) + elif isinstance(value, datetime): + value = value.date() else: - raise NewEventError('Invalid format for the date: {} - {}'.format(date, type(date))) + raise NewEventError(f'Invalid format for the date: {type(value)} - {value}') + super().__setattr__(name, value) - def from_dict(self, **kwargs): - if kwargs.get('Event'): - kwargs = kwargs.get('Event') + def set_date(self, d: str | int | float | datetime | date | None = None, ignore_invalid: bool = False) -> None: + """Set a date for the event + + :param d: String, datetime, or date object + :param ignore_invalid: if True, assigns current date if d is not an expected type + """ + if isinstance(d, (str, int, float, datetime, date)): + self.date = d # type: ignore + elif ignore_invalid: + self.date = date.today() + else: + raise NewEventError(f'Invalid format for the date: {type(d)} - {d}') + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Event' in kwargs: + kwargs = kwargs['Event'] # Required value self.info = kwargs.pop('info', None) if self.info is None: @@ -517,17 +1865,17 @@ class MISPEvent(AbstractMISP): if self.distribution is not None: self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4]: - raise NewEventError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4'.format(self.distribution)) + raise NewEventError(f'{self.info}: {self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4') if kwargs.get('threat_level_id') is not None: self.threat_level_id = int(kwargs.pop('threat_level_id')) if self.threat_level_id not in [1, 2, 3, 4]: - raise NewEventError('{} is invalid, the threat_level has to be in 1, 2, 3, 4'.format(self.threat_level_id)) + raise NewEventError(f'{self.info}: {self.threat_level_id} is invalid, the threat_level_id has to be in 1, 2, 3, 4') if kwargs.get('analysis') is not None: self.analysis = int(kwargs.pop('analysis')) if self.analysis not in [0, 1, 2]: - raise NewEventError('{} is invalid, the analysis has to be in 0, 1, 2'.format(self.analysis)) + raise NewEventError(f'{self.info}: {self.analysis} is invalid, the analysis has to be in 0, 1, 2') self.published = kwargs.pop('published', None) if self.published is True: @@ -538,8 +1886,11 @@ class MISPEvent(AbstractMISP): if kwargs.get('date'): self.set_date(kwargs.pop('date')) if kwargs.get('Attribute'): - for a in kwargs.pop('Attribute'): - self.add_attribute(**a) + [self.add_attribute(**a) for a in kwargs.pop('Attribute')] + if kwargs.get('Galaxy'): + [self.add_galaxy(**e) for e in kwargs.pop('Galaxy')] + if kwargs.get('EventReport'): + [self.add_event_report(**e) for e in kwargs.pop('EventReport')] # All other keys if kwargs.get('id'): @@ -549,54 +1900,53 @@ class MISPEvent(AbstractMISP): if kwargs.get('org_id'): self.org_id = int(kwargs.pop('org_id')) if kwargs.get('timestamp'): - if sys.version_info >= (3, 3): - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), datetime.timezone.utc) - else: - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), UTC()) + self.timestamp = datetime.fromtimestamp(int(kwargs.pop('timestamp')), timezone.utc) if kwargs.get('publish_timestamp'): - if sys.version_info >= (3, 3): - self.publish_timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('publish_timestamp')), datetime.timezone.utc) - else: - self.publish_timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('publish_timestamp')), UTC()) + self.publish_timestamp = datetime.fromtimestamp(int(kwargs.pop('publish_timestamp')), timezone.utc) + if kwargs.get('sighting_timestamp'): + self.sighting_timestamp = datetime.fromtimestamp(int(kwargs.pop('sighting_timestamp')), timezone.utc) if kwargs.get('sharing_group_id'): self.sharing_group_id = int(kwargs.pop('sharing_group_id')) if kwargs.get('RelatedEvent'): for rel_event in kwargs.pop('RelatedEvent'): sub_event = MISPEvent() sub_event.load(rel_event) - self.RelatedEvent.append(sub_event) + self.RelatedEvent.append({'Event': sub_event}) # type: ignore[arg-type] if kwargs.get('Tag'): - for tag in kwargs.pop('Tag'): - self.add_tag(tag) + [self.add_tag(tag) for tag in kwargs.pop('Tag')] if kwargs.get('Object'): - for obj in kwargs.pop('Object'): - self.add_object(obj) + [self.add_object(obj) for obj in kwargs.pop('Object')] if kwargs.get('Org'): self.Org = MISPOrganisation() self.Org.from_dict(**kwargs.pop('Org')) if kwargs.get('Orgc'): self.Orgc = MISPOrganisation() self.Orgc.from_dict(**kwargs.pop('Orgc')) + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) - super(MISPEvent, self).from_dict(**kwargs) + super().from_dict(**kwargs) - def to_dict(self): - to_return = super(MISPEvent, self).to_dict() + def to_dict(self, json_format: bool = False) -> dict[str, Any]: + to_return = super().to_dict(json_format) if to_return.get('date'): - if isinstance(self.date, datetime.datetime): + if isinstance(self.date, datetime): self.date = self.date.date() to_return['date'] = self.date.isoformat() if to_return.get('publish_timestamp'): - to_return['publish_timestamp'] = self._datetime_to_timestamp(self.publish_timestamp) + to_return['publish_timestamp'] = str(self._datetime_to_timestamp(self.publish_timestamp)) + if to_return.get('sighting_timestamp'): + to_return['sighting_timestamp'] = str(self._datetime_to_timestamp(self.sighting_timestamp)) - return {'Event': _int_to_str(to_return)} + return to_return - def add_proposal(self, shadow_attribute=None, **kwargs): + def add_proposal(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Alias for add_shadow_attribute""" return self.add_shadow_attribute(shadow_attribute, **kwargs) - def add_shadow_attribute(self, shadow_attribute=None, **kwargs): + def add_shadow_attribute(self, shadow_attribute=None, **kwargs) -> MISPShadowAttribute: # type: ignore[no-untyped-def] """Add a tag to the attribute (by name or a MISPTag object)""" if isinstance(shadow_attribute, MISPShadowAttribute): misp_shadow_attribute = shadow_attribute @@ -607,94 +1957,156 @@ class MISPEvent(AbstractMISP): misp_shadow_attribute = MISPShadowAttribute() misp_shadow_attribute.from_dict(**kwargs) else: - raise PyMISPError("The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict): {}".format(shadow_attribute)) + raise PyMISPError(f"The shadow_attribute is in an invalid format (can be either string, MISPShadowAttribute, or an expanded dict[str, Any]): {shadow_attribute}") self.shadow_attributes.append(misp_shadow_attribute) self.edited = True return misp_shadow_attribute - def get_attribute_tag(self, attribute_identifier): - '''Return the tags associated to an attribute or an object attribute. - :attribute_identifier: can be an ID, UUID, or the value. - ''' - tags = [] + def get_attribute_tag(self, attribute_identifier: str) -> list[MISPTag]: + """Return the tags associated to an attribute or an object attribute. + + :param attribute_identifier: can be an ID, UUID, or the value. + """ + tags: list[MISPTag] = [] for a in self.attributes + [attribute for o in self.objects for attribute in o.attributes]: - if ((hasattr(a, 'id') and a.id == attribute_identifier) + if ((hasattr(a, 'id') and str(a.id) == attribute_identifier) or (hasattr(a, 'uuid') and a.uuid == attribute_identifier) or (hasattr(a, 'value') and attribute_identifier == a.value - or attribute_identifier in a.value.split('|'))): + or (isinstance(a.value, str) and attribute_identifier in a.value.split('|')))): tags += a.tags return tags - def add_attribute_tag(self, tag, attribute_identifier): - '''Add a tag to an existing attribute, raise an Exception if the attribute doesn't exists. - :tag: Tag name as a string, MISPTag instance, or dictionary - :attribute_identifier: can be an ID, UUID, or the value. - ''' + def add_attribute_tag(self, tag: MISPTag | str, attribute_identifier: str) -> list[MISPAttribute]: + """Add a tag to an existing attribute. Raise an Exception if the attribute doesn't exist. + + :param tag: Tag name as a string, MISPTag instance, or dictionary + :param attribute_identifier: can be an ID, UUID, or the value. + """ attributes = [] for a in self.attributes + [attribute for o in self.objects for attribute in o.attributes]: - if ((hasattr(a, 'id') and a.id == attribute_identifier) + if ((hasattr(a, 'id') and str(a.id) == attribute_identifier) or (hasattr(a, 'uuid') and a.uuid == attribute_identifier) or (hasattr(a, 'value') and attribute_identifier == a.value - or attribute_identifier in a.value.split('|'))): + or (isinstance(a.value, str) and attribute_identifier in a.value.split('|')))): a.add_tag(tag) attributes.append(a) if not attributes: - raise Exception('No attribute with identifier {} found.'.format(attribute_identifier)) + raise PyMISPError(f'No attribute with identifier {attribute_identifier} found.') self.edited = True return attributes - def publish(self): + def publish(self) -> None: """Mark the attribute as published""" self.published = True - def unpublish(self): + def unpublish(self) -> None: """Mark the attribute as un-published (set publish flag to false)""" self.published = False - def delete_attribute(self, attribute_id): - """Delete an attribute, you can search by ID or UUID""" - found = False + def delete_attribute(self, attribute_id: str) -> None: + """Delete an attribute + + :param attribute_id: ID or UUID + """ for a in self.attributes: - if ((hasattr(a, 'id') and a.id == attribute_id) + if ((hasattr(a, 'id') and str(a.id) == attribute_id) or (hasattr(a, 'uuid') and a.uuid == attribute_id)): a.delete() - found = True break - if not found: - raise Exception('No attribute with UUID/ID {} found.'.format(attribute_id)) + else: + raise PyMISPError(f'No attribute with UUID/ID {attribute_id} found.') - def add_attribute(self, type, value, **kwargs): + def add_attribute(self, type: str, value: str | int | float, **kwargs) -> MISPAttribute | list[MISPAttribute]: # type: ignore[no-untyped-def] """Add an attribute. type and value are required but you can pass all other parameters supported by MISPAttribute""" - attr_list = [] + attr_list: list[MISPAttribute] = [] if isinstance(value, list): attr_list = [self.add_attribute(type=type, value=a, **kwargs) for a in value] else: - attribute = MISPAttribute(describe_types=self._describe_types) + attribute = MISPAttribute(describe_types=self.describe_types) attribute.from_dict(type=type, value=value, **kwargs) self.attributes.append(attribute) self.edited = True if attr_list: return attr_list - else: - return attribute + return attribute - def get_object_by_id(self, object_id): - """Get an object by ID (the ID is the one set by the server when creating the new object)""" + def add_event_report(self, name: str, content: str, **kwargs) -> MISPEventReport: # type: ignore[no-untyped-def] + """Add an event report. name and value are requred but you can pass all + other parameters supported by MISPEventReport""" + event_report = MISPEventReport() + event_report.from_dict(name=name, content=content, **kwargs) + self.event_reports.append(event_report) + self.edited = True + return event_report + + def add_galaxy(self, galaxy: MISPGalaxy | dict[str, Any] | None = None, **kwargs) -> MISPGalaxy: # type: ignore[no-untyped-def] + """Add a galaxy and sub-clusters into an event, either by passing + a MISPGalaxy or a dictionary. + Supports all other parameters supported by MISPGalaxy""" + if isinstance(galaxy, MISPGalaxy): + self.galaxies.append(galaxy) + return galaxy + if isinstance(galaxy, dict): + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + elif kwargs: + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**kwargs) + else: + raise InvalidMISPGalaxy("A Galaxy to add to an existing Event needs to be either a MISPGalaxy or a plain python dictionary") + self.galaxies.append(misp_galaxy) + return misp_galaxy + + def get_attribute_by_id(self, attribute_id: str | int) -> MISPAttribute: + """Get an attribute by ID + + :param attribute_id: The ID of the seeking attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'id') and int(attribute.id) == int(attribute_id): + return attribute + raise InvalidMISPAttribute(f'Attribute with {attribute_id} does not exist in this event') + + def get_attribute_by_uuid(self, attribute_uuid: str) -> MISPAttribute: + """Get an attribute by UUID + + :param attribute_uuid: The UUID of the seeking attribute""" + for attribute in self.attributes: + if hasattr(attribute, 'uuid') and attribute.uuid == attribute_uuid: + return attribute + + raise InvalidMISPAttribute(f'Attribute with {attribute_uuid} does not exist in this event') + + def get_object_by_id(self, object_id: str | int) -> MISPObject: + """Get an object by ID + + :param object_id: the ID is the one set by the server when creating the new object""" for obj in self.objects: if hasattr(obj, 'id') and int(obj.id) == int(object_id): return obj - raise InvalidMISPObject('Object with {} does not exist in this event'.format(object_id)) + raise InvalidMISPObject(f'Object with {object_id} does not exist in this event') - def get_object_by_uuid(self, object_uuid): - """Get an object by UUID (UUID is set by the server when creating the new object)""" + def get_object_by_uuid(self, object_uuid: str) -> MISPObject: + """Get an object by UUID + + :param object_uuid: the UUID is set by the server when creating the new object""" for obj in self.objects: if hasattr(obj, 'uuid') and obj.uuid == object_uuid: return obj - raise InvalidMISPObject('Object with {} does not exist in this event'.format(object_uuid)) + raise InvalidMISPObject(f'Object with {object_uuid} does not exist in this event') - def add_object(self, obj=None, **kwargs): + def get_objects_by_name(self, object_name: str) -> list[MISPObject]: + """Get objects by name + + :param object_name: name is set by the server when creating the new object""" + objects = [] + for obj in self.objects: + if hasattr(obj, 'uuid') and obj.name == object_name: + objects.append(obj) + return objects + + def add_object(self, obj: MISPObject | dict[str, Any] | None = None, **kwargs) -> MISPObject: # type: ignore[no-untyped-def] """Add an object to the Event, either by passing a MISPObject, or a dictionary""" if isinstance(obj, MISPObject): misp_obj = obj @@ -710,190 +2122,253 @@ class MISPEvent(AbstractMISP): misp_obj.from_dict(**kwargs) else: raise InvalidMISPObject("An object to add to an existing Event needs to be either a MISPObject, or a plain python dictionary") + misp_obj.standalone = False self.Object.append(misp_obj) self.edited = True return misp_obj - def __repr__(self): + def delete_object(self, object_id: str) -> None: + """Delete an object + + :param object_id: ID or UUID + """ + for o in self.objects: + if ((hasattr(o, 'id') and object_id.isdigit() and int(o.id) == int(object_id)) + or (hasattr(o, 'uuid') and o.uuid == object_id)): + o.delete() + break + else: + raise PyMISPError(f'No object with UUID/ID {object_id} found.') + + def run_expansions(self) -> None: + for index, attribute in enumerate(self.attributes): + if 'expand' not in attribute: + continue + # NOTE: Always make sure the attribute with the expand key is either completely removed, + # of the key is deleted to avoid seeing it processed again on MISP side + elif attribute.expand == 'binary': + try: + from .tools import make_binary_objects + except ImportError as e: + logger.info(f'Unable to load make_binary_objects: {e}') + continue + file_object, bin_type_object, bin_section_objects = make_binary_objects(pseudofile=attribute.malware_binary, filename=attribute.malware_filename) + self.add_object(file_object) + if bin_type_object: + self.add_object(bin_type_object) + if bin_section_objects: + for bin_section_object in bin_section_objects: + self.add_object(bin_section_object) + self.attributes.pop(index) + else: + logger.warning(f'No expansions for this data type ({attribute.type}). Open an issue if needed.') + + def __repr__(self) -> str: if hasattr(self, 'info'): return '<{self.__class__.__name__}(info={self.info})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) - - def _serialize(self): - return '{date}{threat_level_id}{info}{uuid}{analysis}{timestamp}'.format( - date=self.date, threat_level_id=self.threat_level_id, info=self.info, - uuid=self.uuid, analysis=self.analysis, timestamp=self.timestamp).encode() - - def _serialize_sigs(self): # pragma: no cover - # Not used - all_sigs = self.sig - for a in self.attributes: - all_sigs += a.sig - return all_sigs.encode() - - def sign(self, gpg_uid, passphrase=None): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_sign = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign, mode=mode.DETACH) - self.sig = base64.b64encode(signed).decode() - for a in self.attributes: - a.sign(gpg_uid, passphrase) - to_sign_global = self._serialize_sigs() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - c.signers = keys[:1] - if passphrase: - c.set_passphrase_cb(lambda *args: passphrase) - signed, _ = c.sign(to_sign_global, mode=mode.DETACH) - self.global_sig = base64.b64encode(signed).decode() - - def verify(self, gpg_uid): # pragma: no cover - # Not used - if not has_pyme: - raise PyMISPError('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.') - to_return = {} - signed_data = self._serialize() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(signed_data, signature=base64.b64decode(self.sig), verify=keys[:1]) - to_return[self.uuid] = True - except Exception: - to_return[self.uuid] = False - for a in self.attributes: - to_return.update(a.verify(gpg_uid)) - to_verify_global = self._serialize_sigs() - with gpg.Context() as c: - keys = list(c.keylist(gpg_uid)) - try: - c.verify(to_verify_global, signature=base64.b64decode(self.global_sig), verify=keys[:1]) - to_return['global'] = True - except Exception: - to_return['global'] = False - return to_return - - @deprecated - def get_known_types(self): # pragma: no cover - return self.known_types - - @deprecated - def set_all_values(self, **kwargs): # pragma: no cover - self.from_dict(**kwargs) - - @deprecated - def _json(self): # pragma: no cover - return self.to_dict() + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPObjectReference(AbstractMISP): +class MISPObjectTemplate(AbstractMISP): - def __init__(self): - super(MISPObjectReference, self).__init__() + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'ObjectTemplate' in kwargs: + kwargs = kwargs['ObjectTemplate'] + super().from_dict(**kwargs) - def from_dict(self, object_uuid, referenced_uuid, relationship_type, comment=None, **kwargs): - self.object_uuid = object_uuid - self.referenced_uuid = referenced_uuid - self.relationship_type = relationship_type - self.comment = comment - super(MISPObjectReference, self).from_dict(**kwargs) - - def __repr__(self): - if hasattr(self, 'referenced_uuid'): - return '<{self.__class__.__name__}(object_uuid={self.object_uuid}, referenced_uuid={self.referenced_uuid}, relationship_type={self.relationship_type})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(self.name)' class MISPUser(AbstractMISP): - def __init__(self): - super(MISPUser, self).__init__() + authkey: str - def from_dict(self, **kwargs): - if kwargs.get('User'): - kwargs = kwargs.get('User') - super(MISPUser, self).from_dict(**kwargs) + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.email: str + self.password: str | None + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'User' in kwargs: + kwargs = kwargs['User'] + super().from_dict(**kwargs) + if hasattr(self, 'password') and self.password and set(self.password) == {'*', }: + self.password = None -class MISPOrganisation(AbstractMISP): - - def __init__(self): - super(MISPOrganisation, self).__init__() - - def from_dict(self, **kwargs): - if kwargs.get('Organisation'): - kwargs = kwargs.get('Organisation') - super(MISPOrganisation, self).from_dict(**kwargs) + def __repr__(self) -> str: + if hasattr(self, 'email'): + return '<{self.__class__.__name__}(email={self.email})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' class MISPFeed(AbstractMISP): - def __init__(self): - super(MISPFeed, self).__init__() + settings: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Feed' in kwargs: + kwargs = kwargs['Feed'] + super().from_dict(**kwargs) + if hasattr(self, 'settings'): + try: + self.settings = json.loads(self.settings) + except json.decoder.JSONDecodeError as e: + logger.error(f"Failed to parse feed settings: {self.settings}") + raise e + + +class MISPWarninglist(AbstractMISP): + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Warninglist' in kwargs: + kwargs = kwargs['Warninglist'] + super().from_dict(**kwargs) + + +class MISPTaxonomy(AbstractMISP): + + enabled: bool + namespace: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Taxonomy' in kwargs: + kwargs = kwargs['Taxonomy'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(namespace={self.namespace})>' + + +class MISPNoticelist(AbstractMISP): + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Noticelist' in kwargs: + kwargs = kwargs['Noticelist'] + super().from_dict(**kwargs) + + +class MISPCorrelationExclusion(AbstractMISP): + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'CorrelationExclusion' in kwargs: + kwargs = kwargs['CorrelationExclusion'] + super().from_dict(**kwargs) + + +class MISPRole(AbstractMISP): + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.name: str + self.perm_add: bool + self.perm_modify: bool + self.perm_modify_org: bool + self.perm_publish: bool + self.perm_delegate: bool + self.perm_sync: bool + self.perm_admin: bool + self.perm_audit: bool + self.perm_auth: bool + self.perm_site_admin: bool + self.perm_regexp_access: bool + self.perm_tagger: bool + self.perm_template: bool + self.perm_sharing_group: bool + self.perm_tag_editor: bool + self.perm_sighting: bool + self.perm_object_template: bool + self.default_role: bool + self.memory_limit: str | int + self.max_execution_time: str | int + self.restricted_to_site_admin: bool + self.perm_publish_zmq: bool + self.perm_publish_kafka: bool + self.perm_decaying: bool + self.enforce_rate_limit: bool + self.rate_limit_count: str | int + self.perm_galaxy_editor: bool + self.perm_warninglist: bool + self.perm_view_feed_correlations: bool + self.perm_analyst_data: bool + self.permission: str + self.permission_description: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Role' in kwargs: + kwargs = kwargs['Role'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return '<{self.__class__.__name__}({self.name})'.format(self=self) + + +class MISPServer(AbstractMISP): + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Server' in kwargs: + kwargs = kwargs['Server'] + super().from_dict(**kwargs) class MISPLog(AbstractMISP): - def __init__(self): - super(MISPLog, self).__init__() + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.model: str + self.action: str + self.title: str - def __repr__(self): + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Log' in kwargs: + kwargs = kwargs['Log'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: return '<{self.__class__.__name__}({self.model}, {self.action}, {self.title})'.format(self=self) -class MISPSighting(AbstractMISP): +class MISPEventDelegation(AbstractMISP): - def __init__(self): - super(MISPSighting, self).__init__() + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.org_id: int + self.requester_org_id: int + self.event_id: int - def from_dict(self, value=None, uuid=None, id=None, source=None, type=None, timestamp=None, **kwargs): - """Initialize the MISPSighting from a dictionary - :value: Value of the attribute the sighting is related too. Pushing this object - will update the sighting count of each attriutes with thifs value on the instance - :uuid: UUID of the attribute to update - :id: ID of the attriute to update - :source: Source of the sighting - :type: Type of the sighting - :timestamp: Timestamp associated to the sighting - """ - self.value = value - self.uuid = uuid - self.id = id - self.source = source - self.type = type - self.timestamp = timestamp - super(MISPSighting, self).from_dict(**kwargs) + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'EventDelegation' in kwargs: + kwargs = kwargs['EventDelegation'] + super().from_dict(**kwargs) - def __repr__(self): - if hasattr(self, 'value'): - return '<{self.__class__.__name__}(value={self.value})'.format(self=self) - if hasattr(self, 'id'): - return '<{self.__class__.__name__}(value={self.id})'.format(self=self) - if hasattr(self, 'uuid'): - return '<{self.__class__.__name__}(value={self.uuid})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + def __repr__(self) -> str: + return '<{self.__class__.__name__}(org_id={self.org_id}, requester_org_id={self.requester_org_id}, {self.event_id})'.format(self=self) class MISPObjectAttribute(MISPAttribute): - def __init__(self, definition): - super(MISPObjectAttribute, self).__init__() + _fields_for_feed: set[str] = {'uuid', 'value', 'category', 'type', 'comment', 'data', + 'deleted', 'timestamp', 'to_ids', 'disable_correlation', + 'first_seen', 'last_seen', 'object_relation'} + + def __init__(self, definition: dict[str, Any]) -> None: + super().__init__() self._definition = definition - def from_dict(self, object_relation, value, **kwargs): + def from_dict(self, object_relation: str, value: str | int | float, **kwargs): # type: ignore + # NOTE: Signature of "from_dict" incompatible with supertype "MISPAttribute" self.object_relation = object_relation self.value = value + if 'Attribute' in kwargs: + kwargs = kwargs['Attribute'] # Initialize the new MISPAttribute # Get the misp attribute type from the definition self.type = kwargs.pop('type', None) if self.type is None: self.type = self._definition.get('misp-attribute') + if 'category' not in kwargs and 'categories' in self._definition: + # Get first category in the list from the object template as default + self.category = self._definition['categories'][0] self.disable_correlation = kwargs.pop('disable_correlation', None) if self.disable_correlation is None: # The correlation can be disabled by default in the object definition. @@ -905,243 +2380,298 @@ class MISPObjectAttribute(MISPAttribute): self.to_ids = self._definition.get('to_ids') if not self.type: raise NewAttributeError("The type of the attribute is required. Is the object template missing?") - super(MISPObjectAttribute, self).from_dict(**dict(self, **kwargs)) + super().from_dict(**{**self, **kwargs}) - def __repr__(self): + def __repr__(self) -> str: if hasattr(self, 'value'): return '<{self.__class__.__name__}(object_relation={self.object_relation}, value={self.value})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' -class MISPShadowAttribute(MISPAttribute): +class MISPCommunity(AbstractMISP): - def __init__(self): - super(MISPShadowAttribute, self).__init__() + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.name: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Community' in kwargs: + kwargs = kwargs['Community'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(name={self.name}, uuid={self.uuid})' -class MISPObject(AbstractMISP): +class MISPUserSetting(AbstractMISP): - def __init__(self, name, strict=False, standalone=False, default_attributes_parameters={}, **kwargs): - ''' Master class representing a generic MISP object - :name: Name of the object + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.setting: str - :strict: Enforce validation with the object templates + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'UserSetting' in kwargs: + kwargs = kwargs['UserSetting'] + super().from_dict(**kwargs) - :standalone: The object will be pushed as directly on MISP, not as a part of an event. - In this case the ObjectReference needs to be pushed manually and cannot be in the JSON dump. + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(name={self.setting}' - :default_attributes_parameters: Used as template for the attributes if they are not overwritten in add_attribute - :misp_objects_path_custom: Path to custom object templates - ''' - super(MISPObject, self).__init__(**kwargs) - self._strict = strict - self.name = name - misp_objects_path = os.path.join( - os.path.abspath(os.path.dirname(sys.modules['pymisp'].__file__)), - 'data', 'misp-objects', 'objects') - misp_objects_path_custom = kwargs.get('misp_objects_path_custom') - if misp_objects_path_custom and os.path.exists(os.path.join(misp_objects_path_custom, self.name, 'definition.json')): - # Use the local object path by default if provided (allows to overwrite a default template) - template_path = os.path.join(misp_objects_path_custom, self.name, 'definition.json') - self._known_template = True - elif os.path.exists(os.path.join(misp_objects_path, self.name, 'definition.json')): - template_path = os.path.join(misp_objects_path, self.name, 'definition.json') - self._known_template = True +class MISPInbox(AbstractMISP): + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.data: dict[str, Any] + self.type: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Inbox' in kwargs: + kwargs = kwargs['Inbox'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(name={self.type})>' + + +class MISPEventBlocklist(AbstractMISP): + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.event_uuid: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'EventBlocklist' in kwargs: + kwargs = kwargs['EventBlocklist'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(event_uuid={self.event_uuid}' + + +class MISPOrganisationBlocklist(AbstractMISP): + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.org_uuid: str + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'OrgBlocklist' in kwargs: + kwargs = kwargs['OrgBlocklist'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(org_uuid={self.org_uuid}' + + +class MISPDecayingModel(AbstractMISP): + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) + self.uuid: str + self.id: int + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'DecayingModel' in kwargs: + kwargs = kwargs['DecayingModel'] + super().from_dict(**kwargs) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__}(uuid={self.uuid})>' + + +class MISPAnalystData(AbstractMISP): + + _fields_for_feed: set[str] = {'uuid', 'object_uuid', 'object_type', 'authors', + 'created', 'distribution', 'sharing_group_id', 'note_type_name'} + + valid_object_type = {'Attribute', 'Event', 'EventReport', 'GalaxyCluster', 'Galaxy', + 'Object', 'Note', 'Opinion', 'Relationship', 'Organisation', + 'SharingGroup'} + + @property + def org(self) -> MISPOrganisation: + return self.Org + + @property + def orgc(self) -> MISPOrganisation: + return self.Orgc + + @orgc.setter + def orgc(self, orgc: MISPOrganisation) -> None: + if isinstance(orgc, MISPOrganisation): + self.Orgc = orgc else: - if self._strict: - raise UnknownMISPObjectTemplate('{} is unknown in the MISP object directory.'.format(self.name)) - else: - self._known_template = False - if self._known_template: - with open(template_path, 'rb') as f: - if OLD_PY3: - self._definition = json.loads(f.read().decode()) - else: - self._definition = json.load(f) - setattr(self, 'meta-category', self._definition['meta-category']) - self.template_uuid = self._definition['uuid'] - self.description = self._definition['description'] - self.template_version = self._definition['version'] - else: - # Then we have no meta-category, template_uuid, description and template_version - pass + raise PyMISPError('Orgc must be of type MISPOrganisation.') + + def __new__(cls, *args, **kwargs): + if cls is MISPAnalystData: + raise TypeError(f"only children of '{cls.__name__}' may be instantiated") + return object.__new__(cls) + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(**kwargs) self.uuid = str(uuid.uuid4()) - self.__fast_attribute_access = defaultdict(list) # Hashtable object_relation: [attributes] - self.ObjectReference = [] - self.Attribute = [] - if isinstance(default_attributes_parameters, MISPAttribute): - # Just make sure we're not modifying an existing MISPAttribute - self._default_attributes_parameters = default_attributes_parameters.to_dict() - else: - self._default_attributes_parameters = default_attributes_parameters - if self._default_attributes_parameters: - # Let's clean that up - self._default_attributes_parameters.pop('value', None) # duh - self._default_attributes_parameters.pop('uuid', None) # duh - self._default_attributes_parameters.pop('id', None) # duh - self._default_attributes_parameters.pop('object_id', None) # duh - self._default_attributes_parameters.pop('type', None) # depends on the value - self._default_attributes_parameters.pop('object_relation', None) # depends on the value - self._default_attributes_parameters.pop('disable_correlation', None) # depends on the value - self._default_attributes_parameters.pop('to_ids', None) # depends on the value - self._default_attributes_parameters.pop('deleted', None) # doesn't make sense to pre-set it - self._default_attributes_parameters.pop('data', None) # in case the original in a sample or an attachment + self.object_uuid: str + self.object_type: str + self.authors: str + self.created: float | int | datetime + self.modified: float | int | datetime + self.SharingGroup: MISPSharingGroup - # Those values are set for the current object, if they exist, but not pop'd because they are still useful for the attributes - self.distribution = self._default_attributes_parameters.get('distribution', 5) - self.sharing_group_id = self._default_attributes_parameters.get('sharing_group_id', 0) - else: - self.distribution = 5 # Default to inherit - self.sharing_group_id = 0 - self._standalone = standalone - if self._standalone: - # Mark as non_jsonable because we need to add the references manually after the object(s) have been created - self.update_not_jsonable('ObjectReference') - - @property - def attributes(self): - return self.Attribute - - @attributes.setter - def attributes(self, attributes): - if all(isinstance(x, MISPObjectAttribute) for x in attributes): - self.Attribute = attributes - self.__fast_attribute_access = defaultdict(list) - else: - raise PyMISPError('All the attributes have to be of type MISPObjectAttribute.') - - @property - def references(self): - return self.ObjectReference - - @references.setter - def references(self, references): - if all(isinstance(x, MISPObjectReference) for x in references): - self.ObjectReference = references - else: - raise PyMISPError('All the attributes have to be of type MISPObjectReference.') - - def from_dict(self, **kwargs): - if kwargs.get('Object'): - kwargs = kwargs.get('Object') - if self._known_template: - if kwargs.get('template_uuid') and kwargs['template_uuid'] != self.template_uuid: - if self._strict: - raise UnknownMISPObjectTemplate('UUID of the object is different from the one of the template.') - else: - self._known_template = False - if kwargs.get('template_version') and int(kwargs['template_version']) != self.template_version: - if self._strict: - raise UnknownMISPObjectTemplate('Version of the object ({}) is different from the one of the template ({}).'.format(kwargs['template_version'], self.template_version)) - else: - self._known_template = False - - if 'distribution' in kwargs and kwargs['distribution'] is not None: - self.distribution = kwargs.pop('distribution') + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + self.distribution = kwargs.pop('distribution', None) + if self.distribution is not None: self.distribution = int(self.distribution) if self.distribution not in [0, 1, 2, 3, 4, 5]: - raise NewAttributeError('{} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5'.format(self.distribution)) + raise NewAnalystDataError(f'{self.distribution} is invalid, the distribution has to be in 0, 1, 2, 3, 4, 5') - if kwargs.get('timestamp'): - if sys.version_info >= (3, 3): - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), datetime.timezone.utc) + if kwargs.get('sharing_group_id'): + self.sharing_group_id = int(kwargs.pop('sharing_group_id')) + + if self.distribution == 4: + # The distribution is set to sharing group, a sharing_group_id is required. + if not hasattr(self, 'sharing_group_id'): + raise NewAnalystDataError('If the distribution is set to sharing group, a sharing group ID is required.') + elif not self.sharing_group_id: + # Cannot be None or 0 either. + raise NewAnalystDataError(f'If the distribution is set to sharing group, a sharing group ID is required (cannot be {self.sharing_group_id}).') + + self.object_uuid = kwargs.pop('object_uuid', None) + if self.object_uuid is None: + raise NewAnalystDataError('The UUID for which this element is attached is required.') + self.object_type = kwargs.pop('object_type', None) + if self.object_type is None: + raise NewAnalystDataError('The element type for which this element is attached is required.') + if self.object_type not in self.valid_object_type: + raise NewAnalystDataError('The element type is not a valid type. Actual: {self.object_type}.') + + if kwargs.get('id'): + self.id = int(kwargs.pop('id')) + if kwargs.get('created'): + ts = kwargs.pop('created') + if isinstance(ts, datetime): + self.created = ts else: - self.timestamp = datetime.datetime.fromtimestamp(int(kwargs.pop('timestamp')), UTC()) - if kwargs.get('Attribute'): - for a in kwargs.pop('Attribute'): - self.add_attribute(**a) - if kwargs.get('ObjectReference'): - for r in kwargs.pop('ObjectReference'): - self.add_reference(**r) - - # Not supported yet - https://github.com/MISP/PyMISP/issues/168 - # if kwargs.get('Tag'): - # for tag in kwargs.pop('Tag'): - # self.add_tag(tag) - - super(MISPObject, self).from_dict(**kwargs) - - def add_reference(self, referenced_uuid, relationship_type, comment=None, **kwargs): - """Add a link (uuid) to an other object""" - if kwargs.get('object_uuid'): - # Load existing object - object_uuid = kwargs.pop('object_uuid') - else: - # New reference - object_uuid = self.uuid - reference = MISPObjectReference() - reference.from_dict(object_uuid=object_uuid, referenced_uuid=referenced_uuid, - relationship_type=relationship_type, comment=comment, **kwargs) - self.ObjectReference.append(reference) - self.edited = True - - def get_attributes_by_relation(self, object_relation): - '''Returns the list of attributes with the given object relation in the object''' - return self._fast_attribute_access.get(object_relation, []) - - @property - def _fast_attribute_access(self): - if not self.__fast_attribute_access: - for a in self.attributes: - self.__fast_attribute_access[a.object_relation].append(a) - return self.__fast_attribute_access - - def has_attributes_by_relation(self, list_of_relations): - '''True if all the relations in the list are defined in the object''' - return all(relation in self._fast_attribute_access for relation in list_of_relations) - - def add_attribute(self, object_relation, **value): - """Add an attribute. object_relation is required and the value key is a - dictionary with all the keys supported by MISPAttribute""" - if value.get('value') is None: - return None - if self._known_template: - if self._definition['attributes'].get(object_relation): - attribute = MISPObjectAttribute(self._definition['attributes'][object_relation]) + self.created = datetime.fromisoformat(ts + '+00:00') # Force UTC TZ + if kwargs.get('modified'): + ts = kwargs.pop('modified') + if isinstance(ts, datetime): + self.modified = ts else: - # Woopsie, this object_relation is unknown, no sane defaults for you. - logger.warning("The template ({}) doesn't have the object_relation ({}) you're trying to add.".format(self.name, object_relation)) - attribute = MISPObjectAttribute({}) + self.modified = datetime.fromisoformat(ts + '+00:00') # Force UTC TZ + + if kwargs.get('Org'): + self.Org = MISPOrganisation() + self.Org.from_dict(**kwargs.pop('Org')) + if kwargs.get('Orgc'): + self.Orgc = MISPOrganisation() + self.Orgc.from_dict(**kwargs.pop('Orgc')) + if kwargs.get('SharingGroup'): + self.SharingGroup = MISPSharingGroup() + self.SharingGroup.from_dict(**kwargs.pop('SharingGroup')) + + super().from_dict(**kwargs) + + def _set_default(self) -> None: + if not hasattr(self, 'created'): + self.created = datetime.timestamp(datetime.now()) + if not hasattr(self, 'modified'): + self.modified = self.created + + +class MISPNote(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'note', 'language'}) + + _analyst_data_object_type = 'Note' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.note: str + self.language: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Note' in kwargs: + kwargs = kwargs['Note'] + self.note = kwargs.pop('note', None) + if self.note is None: + raise NewNoteError('The text note of the note is required.') + super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'note'): + return '<{self.__class__.__name__}(note={self.note})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' + + +class MISPOpinion(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'opinion', 'comment'}) + + _analyst_data_object_type = 'Opinion' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.opinion: int + self.comment: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Opinion' in kwargs: + kwargs = kwargs['Opinion'] + self.opinion = kwargs.pop('opinion', None) + if self.opinion is not None: + self.opinion = int(self.opinion) + if not (0 <= self.opinion <= 100): + raise NewOpinionError('The opinion value must be between 0 and 100 included.') else: - attribute = MISPObjectAttribute({}) - # Overwrite the parameters of self._default_attributes_parameters with the ones of value - attribute.from_dict(object_relation=object_relation, **dict(self._default_attributes_parameters, **value)) - self.__fast_attribute_access[object_relation].append(attribute) - self.Attribute.append(attribute) - self.edited = True - return attribute + raise NewOpinionError('The opinion value is required.') - def to_dict(self, strict=False): - if strict or self._strict and self._known_template: - self._validate() - return super(MISPObject, self).to_dict() + self.comment = kwargs.pop('comment', None) + if self.comment is None: + raise NewOpinionError('The text comment is required.') - def to_json(self, strict=False): - if strict or self._strict and self._known_template: - self._validate() - return super(MISPObject, self).to_json() + return super().from_dict(**kwargs) - def _validate(self): - """Make sure the object we're creating has the required fields""" - if self._definition.get('required'): - required_missing = set(self._definition.get('required')) - set(self._fast_attribute_access.keys()) - if required_missing: - raise InvalidMISPObject('{} are required.'.format(required_missing)) - if self._definition.get('requiredOneOf'): - if not set(self._definition['requiredOneOf']) & set(self._fast_attribute_access.keys()): - # We ecpect at least one of the object_relation in requiredOneOf, and it isn't the case - raise InvalidMISPObject('At least one of the following attributes is required: {}'.format(', '.join(self._definition['requiredOneOf']))) - for rel, attrs in self._fast_attribute_access.items(): - if len(attrs) == 1: - # object_relation's here only once, everything's cool, moving on - continue - if not self._definition['attributes'][rel].get('multiple'): - # object_relation's here more than once, but it isn't allowed in the template. - raise InvalidMISPObject('Multiple occurrences of {} is not allowed'.format(rel)) - return True + def __repr__(self) -> str: + if hasattr(self, 'opinion'): + return '<{self.__class__.__name__}([opinion={self.opinion}] comment={self.comment})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' - def __repr__(self): - if hasattr(self, 'name'): - return '<{self.__class__.__name__}(name={self.name})'.format(self=self) - return '<{self.__class__.__name__}(NotInitialized)'.format(self=self) + +class MISPRelationship(AnalystDataBehaviorMixin, MISPAnalystData): + + _fields_for_feed: set[str] = MISPAnalystData._fields_for_feed.union({'related_object_uuid', 'related_object_type', 'relationship_type'}) + + _analyst_data_object_type = 'Relationship' + + def __init__(self, **kwargs: dict[str, Any]) -> None: + self.related_object_uuid: str + self.related_object_type: str + self.relationship_type: str + super().__init__(**kwargs) + + def from_dict(self, **kwargs) -> None: # type: ignore[no-untyped-def] + if 'Relationship' in kwargs: + kwargs = kwargs['Relationship'] + self.related_object_type = kwargs.pop('related_object_type', None) + if self.related_object_type is None: + raise NewRelationshipError('The target object type for this relationship is required.') + + self.related_object_uuid = kwargs.pop('related_object_uuid', None) + if self.related_object_uuid is None: + if not isinstance(self.related_object_type, AbstractMISP): + raise NewRelationshipError('The target UUID for this relationship is required.') + else: + self.related_object_uuid = self.related_object_type.uuid + self.related_object_type = self.related_object_type._analyst_data_object_type + + if self.related_object_type not in self.valid_object_type: + raise NewAnalystDataError(f'The target object type is not a valid type. Actual: {self.related_object_type}.') + + return super().from_dict(**kwargs) + + def __repr__(self) -> str: + if hasattr(self, 'related_object_uuid') and hasattr(self, 'object_uuid'): + return '<{self.__class__.__name__}(object_uuid={self.object_uuid}, related_object_type={self.related_object_type}, related_object_uuid={self.related_object_uuid}, relationship_type={self.relationship_type})'.format(self=self) + return f'<{self.__class__.__name__}(NotInitialized)' diff --git a/pymisp/py.typed b/pymisp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pymisp/tools/__init__.py b/pymisp/tools/__init__.py index 07a6238..45907b3 100644 --- a/pymisp/tools/__init__.py +++ b/pymisp/tools/__init__.py @@ -1,11 +1,8 @@ -import sys +from __future__ import annotations from .vtreportobject import VTReportObject # noqa from .neo4j import Neo4j # noqa from .fileobject import FileObject # noqa -from .peobject import PEObject, PESectionObject # noqa -from .elfobject import ELFObject, ELFSectionObject # noqa -from .machoobject import MachOObject, MachOSectionObject # noqa from .create_misp_object import make_binary_objects # noqa from .abstractgenerator import AbstractMISPObjectGenerator # noqa from .genericgenerator import GenericObjectGenerator # noqa @@ -15,8 +12,44 @@ from .fail2banobject import Fail2BanObject # noqa from .domainipobject import DomainIPObject # noqa from .asnobject import ASNObject # noqa from .geolocationobject import GeolocationObject # noqa +from .git_vuln_finder_object import GitVulnFinderObject # noqa -if sys.version_info >= (3, 6): +from .vehicleobject import VehicleObject # noqa +from .csvloader import CSVLoader # noqa +from .sshauthkeyobject import SSHAuthorizedKeysObject # noqa +from .feed import feed_meta_generator # noqa +from .update_objects import update_objects # noqa + +try: from .emailobject import EMailObject # noqa - from .vehicleobject import VehicleObject # noqa - from .csvloader import CSVLoader # noqa +except ImportError: + # Requires 'extract_msg', "RTFDE", "oletools" + # pymisp needs to be installed with the email parameter + pass + +try: + from .urlobject import URLObject # noqa +except ImportError: + # Requires pyfaup, optional dependency [url] + pass +except OSError: + # faup required liblua-5.3 + pass + +try: + from .peobject import PEObject, PESectionObject # noqa + from .elfobject import ELFObject, ELFSectionObject # noqa + from .machoobject import MachOObject, MachOSectionObject # noqa +except ImportError: + # Requires lief, optional [fileobjects] + pass + +__all__ = ['VTReportObject', 'Neo4j', 'FileObject', 'make_binary_objects', + 'AbstractMISPObjectGenerator', 'GenericObjectGenerator', + 'load_openioc', 'load_openioc_file', 'SBSignatureObject', + 'Fail2BanObject', 'DomainIPObject', 'ASNObject', 'GeolocationObject', + 'GitVulnFinderObject', 'VehicleObject', 'CSVLoader', + 'SSHAuthorizedKeysObject', 'feed_meta_generator', 'update_objects', + 'EMailObject', 'URLObject', 'PEObject', 'PESectionObject', 'ELFObject', + 'ELFSectionObject', 'MachOObject', 'MachOSectionObject' + ] diff --git a/pymisp/tools/_psl_faup.py b/pymisp/tools/_psl_faup.py new file mode 100644 index 0000000..388fed4 --- /dev/null +++ b/pymisp/tools/_psl_faup.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import ipaddress +import socket +import idna +from publicsuffixlist import PublicSuffixList # type: ignore +from urllib.parse import urlparse, urlunparse, ParseResult + + +class UrlNotDecoded(Exception): + pass + + +class PSLFaup: + """ + Fake Faup Python Library using PSL for Windows support + """ + + def __init__(self) -> None: + self.decoded = False + self.psl = PublicSuffixList() + self._url: ParseResult | None = None + self._retval: dict[str, str | int | None | bytes] = {} + self.ip_as_host = '' + + def _clear(self) -> None: + self.decoded = False + self._url = None + self._retval = {} + self.ip_as_host = '' + + def decode(self, url: str) -> None: + """ + This function creates a dict of all the url fields. + :param url: The URL to normalize + """ + self._clear() + if isinstance(url, bytes) and b'//' not in url[:10]: + url = b'//' + url + elif '//' not in url[:10]: + url = '//' + url + self._url = urlparse(url) + + if self._url is None: + raise UrlNotDecoded("Unable to parse URL") + + self.ip_as_host = '' + if self._url.hostname is None: + raise UrlNotDecoded("Unable to parse URL") + hostname = _ensure_str(self._url.hostname) + try: + ipv4_bytes = socket.inet_aton(hostname) + ipv4 = ipaddress.IPv4Address(ipv4_bytes) + self.ip_as_host = ipv4.compressed + except (OSError, ValueError): + try: + addr, _, _ = hostname.partition('%') + ipv6 = ipaddress.IPv6Address(addr) + self.ip_as_host = ipv6.compressed + except ValueError: + pass + + self.decoded = True + self._retval = {} + + @property + def url(self) -> bytes | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if host := self.get_host(): + netloc = host + ('' if self.get_port() is None else f':{self.get_port()}') + return _ensure_bytes( + urlunparse( + (self.get_scheme(), netloc, self.get_resource_path(), + '', self.get_query_string(), self.get_fragment(),) + ) + ) + return None + + def get_scheme(self) -> str: + """ + Get the scheme of the url given in the decode function + :returns: The URL scheme + """ + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + return _ensure_str(self._url.scheme if self._url.scheme else '') + + def get_credential(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self._url.username and self._url.password: + return _ensure_str(self._url.username) + ':' + _ensure_str(self._url.password) + if self._url.username: + return _ensure_str(self._url.username) + return None + + def get_subdomain(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self.get_host() is not None and not self.ip_as_host: + domain = self.get_domain() + host = self.get_host() + if domain and host and domain in host: + return host.rsplit(domain, 1)[0].rstrip('.') or None + return None + + def get_domain(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self.get_host() is not None and not self.ip_as_host: + return self.psl.privatesuffix(self.get_host()) + return None + + def get_domain_without_tld(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self.get_tld() is not None and not self.ip_as_host: + if domain := self.get_domain(): + return domain.rsplit(self.get_tld(), 1)[0].rstrip('.') + return None + + def get_host(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self._url.hostname is None: + return None + elif self._url.hostname.isascii(): + return _ensure_str(self._url.hostname) + else: + return _ensure_str(idna.encode(self._url.hostname, uts46=True)) + + def get_unicode_host(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if not self.ip_as_host: + if host := self.get_host(): + return idna.decode(host, uts46=True) + return None + + def get_tld(self) -> str | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + if self.get_host() is not None and not self.ip_as_host: + return self.psl.publicsuffix(self.get_host()) + return None + + def get_port(self) -> int | None: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + return self._url.port + + def get_resource_path(self) -> str: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + return _ensure_str(self._url.path) + + def get_query_string(self) -> str: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + return _ensure_str(self._url.query) + + def get_fragment(self) -> str: + if not self.decoded or not self._url: + raise UrlNotDecoded("You must call faup.decode() first") + + return _ensure_str(self._url.fragment) + + def get(self) -> dict[str, str | int | None | bytes]: + self._retval["scheme"] = self.get_scheme() + self._retval["tld"] = self.get_tld() + self._retval["domain"] = self.get_domain() + self._retval["domain_without_tld"] = self.get_domain_without_tld() + self._retval["subdomain"] = self.get_subdomain() + self._retval["host"] = self.get_host() + self._retval["port"] = self.get_port() + self._retval["resource_path"] = self.get_resource_path() + self._retval["query_string"] = self.get_query_string() + self._retval["fragment"] = self.get_fragment() + self._retval["url"] = self.url + return self._retval + + +def _ensure_bytes(binary: str | bytes) -> bytes: + if isinstance(binary, bytes): + return binary + else: + return binary.encode('utf-8') + + +def _ensure_str(string: str | bytes) -> str: + if isinstance(string, str): + return string + else: + return string.decode('utf-8') diff --git a/pymisp/tools/abstractgenerator.py b/pymisp/tools/abstractgenerator.py index 71520db..a3ca26f 100644 --- a/pymisp/tools/abstractgenerator.py +++ b/pymisp/tools/abstractgenerator.py @@ -1,15 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from datetime import datetime, date +from dateutil.parser import parse + +from typing import Any from .. import MISPObject from ..exceptions import InvalidMISPObject -from datetime import datetime, date -from dateutil.parser import parse class AbstractMISPObjectGenerator(MISPObject): - def _detect_epoch(self, timestamp): + def _detect_epoch(self, timestamp: str | int | float) -> bool: try: tmp = float(timestamp) if tmp < 30000000: @@ -20,7 +24,7 @@ class AbstractMISPObjectGenerator(MISPObject): except ValueError: return False - def _sanitize_timestamp(self, timestamp): + def _sanitize_timestamp(self, timestamp: datetime | date | dict[str, Any] | str | int | float | None = None) -> datetime: if not timestamp: return datetime.now() @@ -31,22 +35,27 @@ class AbstractMISPObjectGenerator(MISPObject): elif isinstance(timestamp, dict): if not isinstance(timestamp['value'], datetime): timestamp['value'] = parse(timestamp['value']) - return timestamp - elif not isinstance(timestamp, datetime): # Supported: float/int/string - if self._detect_epoch(timestamp): + return timestamp['value'] + else: # Supported: float/int/string + if isinstance(timestamp, (str, int, float)) and self._detect_epoch(timestamp): + # It converts to the *local* datetime, which is consistent with the rest of the code. return datetime.fromtimestamp(float(timestamp)) - return parse(timestamp) - return timestamp + elif isinstance(timestamp, str): + return parse(timestamp) + else: + raise Exception(f'Unable to convert {timestamp} to a datetime.') - def generate_attributes(self): + def generate_attributes(self) -> None: """Contains the logic where all the values of the object are gathered""" - if hasattr(self, '_parameters'): + if hasattr(self, '_parameters') and self._definition is not None: for object_relation in self._definition['attributes']: value = self._parameters.pop(object_relation, None) if not value: continue if isinstance(value, dict): self.add_attribute(object_relation, **value) + elif isinstance(value, list): + self.add_attributes(object_relation, *value) else: # Assume it is the value only self.add_attribute(object_relation, value=value) diff --git a/pymisp/tools/asnobject.py b/pymisp/tools/asnobject.py index ce80029..685da7a 100644 --- a/pymisp/tools/asnobject.py +++ b/pymisp/tools/asnobject.py @@ -1,22 +1,26 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class ASNObject(AbstractMISPObjectGenerator): - def __init__(self, parameters, strict=True, standalone=True, **kwargs): - super(ASNObject, self).__init__('asn', strict=strict, standalone=standalone, **kwargs) + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('asn', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) self._parameters['last-seen'] = last - return super(ASNObject, self).generate_attributes() + super().generate_attributes() diff --git a/pymisp/tools/create_misp_object.py b/pymisp/tools/create_misp_object.py index ffbfb46..d2f402c 100644 --- a/pymisp/tools/create_misp_object.py +++ b/pymisp/tools/create_misp_object.py @@ -1,93 +1,69 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys +from __future__ import annotations -from . import FileObject, PEObject, ELFObject, MachOObject -from ..exceptions import MISPObjectException import logging +from io import BytesIO +from typing import Any, TYPE_CHECKING + +from ..exceptions import MISPObjectException +from . import FileObject logger = logging.getLogger('pymisp') try: import lief - from lief import Logger - Logger.disable() + import lief.logging + lief.logging.disable() HAS_LIEF = True + + from .peobject import make_pe_objects + from .elfobject import make_elf_objects + from .machoobject import make_macho_objects +except AttributeError: + HAS_LIEF = False + logger.critical('You need lief >= 0.11.0. The quick and dirty fix is: pip3 install --force pymisp[fileobjects]') + except ImportError: HAS_LIEF = False +if TYPE_CHECKING: + from . import PEObject, ELFObject, MachOObject, PESectionObject, ELFSectionObject, MachOSectionObject + class FileTypeNotImplemented(MISPObjectException): pass -def make_pe_objects(lief_parsed, misp_file, standalone=True, default_attributes_parameters={}): - pe_object = PEObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) - misp_file.add_reference(pe_object.uuid, 'included-in', 'PE indicators') - pe_sections = [] - for s in pe_object.sections: - pe_sections.append(s) - return misp_file, pe_object, pe_sections - - -def make_elf_objects(lief_parsed, misp_file, standalone=True, default_attributes_parameters={}): - elf_object = ELFObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) - misp_file.add_reference(elf_object.uuid, 'included-in', 'ELF indicators') - elf_sections = [] - for s in elf_object.sections: - elf_sections.append(s) - return misp_file, elf_object, elf_sections - - -def make_macho_objects(lief_parsed, misp_file, standalone=True, default_attributes_parameters={}): - macho_object = MachOObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) - misp_file.add_reference(macho_object.uuid, 'included-in', 'MachO indicators') - macho_sections = [] - for s in macho_object.sections: - macho_sections.append(s) - return misp_file, macho_object, macho_sections - - -def make_binary_objects(filepath=None, pseudofile=None, filename=None, standalone=True, default_attributes_parameters={}): +def make_binary_objects(filepath: str | None = None, + pseudofile: BytesIO | bytes | None = None, + filename: str | None = None, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, PEObject | ELFObject | MachOObject | None, list[PESectionObject] | list[ELFSectionObject] | list[MachOSectionObject]]: misp_file = FileObject(filepath=filepath, pseudofile=pseudofile, filename=filename, standalone=standalone, default_attributes_parameters=default_attributes_parameters) - if HAS_LIEF and filepath or (pseudofile and filename): - try: - if filepath: - lief_parsed = lief.parse(filepath=filepath) - else: - if sys.version_info < (3, 0): - logger.critical('Pseudofile is not supported in python2. Just update.') - lief_parsed = None - else: - lief_parsed = lief.parse(raw=pseudofile.getvalue(), name=filename) - if isinstance(lief_parsed, lief.PE.Binary): - return make_pe_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - elif isinstance(lief_parsed, lief.ELF.Binary): - return make_elf_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - elif isinstance(lief_parsed, lief.MachO.Binary): - return make_macho_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) - except lief.bad_format as e: - logger.warning('Bad format: {}'.format(e)) - except lief.bad_file as e: - logger.warning('Bad file: {}'.format(e)) - except lief.conversion_error as e: - logger.warning('Conversion file: {}'.format(e)) - except lief.builder_error as e: - logger.warning('Builder file: {}'.format(e)) - except lief.parser_error as e: - logger.warning('Parser error: {}'.format(e)) - except lief.integrity_error as e: - logger.warning('Integrity error: {}'.format(e)) - except lief.pe_error as e: - logger.warning('PE error: {}'.format(e)) - except lief.type_error as e: - logger.warning('Type error: {}'.format(e)) - except lief.exception as e: - logger.warning('Lief exception: {}'.format(e)) - except FileTypeNotImplemented as e: - logger.warning(e) + if HAS_LIEF and (filepath or pseudofile): + if filepath: + lief_parsed = lief.parse(filepath=filepath) + elif pseudofile: + if isinstance(pseudofile, bytes): + lief_parsed = lief.parse(raw=pseudofile) + else: # BytesIO + lief_parsed = lief.parse(obj=pseudofile) + else: + logger.critical('You need either a filepath, or a pseudofile and a filename.') + lief_parsed = None + + if isinstance(lief_parsed, lief.lief_errors): + logger.warning('Got an error parsing the file: {lief_parsed}') + elif isinstance(lief_parsed, lief.PE.Binary): + return make_pe_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + elif isinstance(lief_parsed, lief.ELF.Binary): + return make_elf_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + elif isinstance(lief_parsed, lief.MachO.Binary): + return make_macho_objects(lief_parsed, misp_file, standalone, default_attributes_parameters) + else: + logger.critical(f'Unexpected type from lief: {type(lief_parsed)}') if not HAS_LIEF: logger.warning('Please install lief, documentation here: https://github.com/lief-project/LIEF') - return misp_file, None, None + return misp_file, None, [] diff --git a/pymisp/tools/csvloader.py b/pymisp/tools/csvloader.py index 86efe39..e0452ec 100644 --- a/pymisp/tools/csvloader.py +++ b/pymisp/tools/csvloader.py @@ -1,48 +1,63 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from pathlib import Path + import csv from pymisp import MISPObject class CSVLoader(): - def __init__(self, template_name: str, csv_path: Path, fieldnames: list=[], has_fieldnames=False): + def __init__(self, template_name: str, csv_path: Path, + fieldnames: list[str] | None = None, has_fieldnames: bool=False, + delimiter: str = ',', quotechar: str = '"') -> None: self.template_name = template_name + self.delimiter = delimiter + self.quotechar = quotechar self.csv_path = csv_path - self.fieldnames = [f.strip().lower() for f in fieldnames] + self.fieldnames = [] + if fieldnames: + self.fieldnames = [f.strip() for f in fieldnames] if not self.fieldnames: - # If the user doesn't pass fieldnames, we assume the CSV has them. + # If the user doesn't pass fieldnames, they must be in the CSV. self.has_fieldnames = True else: self.has_fieldnames = has_fieldnames - def load(self): + def load(self) -> list[MISPObject]: objects = [] with open(self.csv_path, newline='') as csvfile: - reader = csv.reader(csvfile) + reader = csv.reader(csvfile, delimiter=self.delimiter, quotechar=self.quotechar) if self.has_fieldnames: - # The file has fieldnames, we either ignore it, or validate its validity - fieldnames = [f.strip().lower() for f in reader.__next__()] + # The file has fieldnames, we either ignore it, or use them as object-relation + fieldnames = [f.strip() for f in reader.__next__()] if not self.fieldnames: self.fieldnames = fieldnames if not self.fieldnames: - raise Exception(f'No fieldnames, impossible to create objects.') - else: - # Check if the CSV file has a header, and if it matches with the object template - tmp_object = MISPObject(self.template_name) - allowed_fieldnames = list(tmp_object._definition['attributes'].keys()) - for fieldname in self.fieldnames: - if fieldname not in allowed_fieldnames: - raise Exception(f'{fieldname} is not a valid object relation for {self.template_name}: {allowed_fieldnames}') + raise Exception('No fieldnames, impossible to create objects.') + + # Check if the CSV file has a header, and if it matches with the object template + tmp_object = MISPObject(self.template_name) + + if not tmp_object._definition or not tmp_object._definition['attributes']: + raise Exception(f'Unable to find the object template ({self.template_name}), impossible to create objects.') + allowed_fieldnames = list(tmp_object._definition['attributes'].keys()) + for fieldname in self.fieldnames: + if fieldname not in allowed_fieldnames: + raise Exception(f'{fieldname} is not a valid object relation for {self.template_name}: {allowed_fieldnames}') for row in reader: tmp_object = MISPObject(self.template_name) + has_attribute = False for object_relation, value in zip(self.fieldnames, row): - tmp_object.add_attribute(object_relation, value=value) - objects.append(tmp_object) + if value: + has_attribute = True + tmp_object.add_attribute(object_relation, value=value) + if has_attribute: + objects.append(tmp_object) return objects diff --git a/pymisp/tools/domainipobject.py b/pymisp/tools/domainipobject.py index 62dc554..2269342 100644 --- a/pymisp/tools/domainipobject.py +++ b/pymisp/tools/domainipobject.py @@ -1,22 +1,26 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class DomainIPObject(AbstractMISPObjectGenerator): - def __init__(self, parameters, strict=True, standalone=True, **kwargs): - super(DomainIPObject, self).__init__('domain-ip', strict=strict, standalone=standalone, **kwargs) + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('domain-ip', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) self._parameters['last-seen'] = last - return super(DomainIPObject, self).generate_attributes() + super().generate_attributes() diff --git a/pymisp/tools/elfobject.py b/pymisp/tools/elfobject.py index d58390c..c0b6ceb 100644 --- a/pymisp/tools/elfobject.py +++ b/pymisp/tools/elfobject.py @@ -1,53 +1,76 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from .abstractgenerator import AbstractMISPObjectGenerator -from ..exceptions import InvalidMISPObject -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 +from __future__ import annotations + import logging -logger = logging.getLogger('pymisp') +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any + +from . import FileObject +from .abstractgenerator import AbstractMISPObjectGenerator +from ..exceptions import InvalidMISPObject + +import lief try: - import lief - HAS_LIEF = True -except ImportError: - HAS_LIEF = False - -try: - import pydeep + import pydeep # type: ignore HAS_PYDEEP = True except ImportError: HAS_PYDEEP = False +logger = logging.getLogger('pymisp') + + +def make_elf_objects(lief_parsed: lief.ELF.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, ELFObject, list[ELFSectionObject]]: + elf_object = ELFObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) + misp_file.add_reference(elf_object.uuid, 'includes', 'ELF indicators') + elf_sections = [] + for s in elf_object.sections: + elf_sections.append(s) + return misp_file, elf_object, elf_sections + class ELFObject(AbstractMISPObjectGenerator): - def __init__(self, parsed=None, filepath=None, pseudofile=None, standalone=True, **kwargs): + __elf: lief.ELF.Binary + + def __init__(self, parsed: lief.ELF.Binary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | bytes | list[int] | None = None, **kwargs) -> None: + """Creates an ELF object, with lief""" + super().__init__('elf', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") - if not HAS_LIEF: - raise ImportError('Please install lief, documentation here: https://github.com/lief-project/LIEF') + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__elf = lief.ELF.parse(raw=pseudofile.getvalue()) + e = lief.ELF.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__elf = lief.ELF.parse(raw=pseudofile) + e = lief.ELF.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + e = lief.ELF.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not e: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__elf = e elif filepath: - self.__elf = lief.ELF.parse(filepath) + if e := lief.ELF.parse(filepath): + self.__elf = e elif parsed: # Got an already parsed blob if isinstance(parsed, lief.ELF.Binary): self.__elf = parsed else: - raise InvalidMISPObject('Not a lief.ELF.Binary: {}'.format(type(parsed))) - super(ELFObject, self).__init__('elf', standalone=standalone, **kwargs) + raise InvalidMISPObject(f'Not a lief.ELF.Binary: {type(parsed)}') self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: # General information self.add_attribute('type', value=str(self.__elf.header.file_type).split('.')[1]) self.add_attribute('entrypoint-address', value=self.__elf.entrypoint) @@ -58,8 +81,10 @@ class ELFObject(AbstractMISPObjectGenerator): if self.__elf.sections: pos = 0 for section in self.__elf.sections: - s = ELFSectionObject(section, self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'included-in', 'Section {} of ELF'.format(pos)) + if not section.name: + continue + s = ELFSectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) + self.add_reference(s.uuid, 'includes', f'Section {pos} of ELF') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) @@ -67,21 +92,22 @@ class ELFObject(AbstractMISPObjectGenerator): class ELFSectionObject(AbstractMISPObjectGenerator): - def __init__(self, section, standalone=True, **kwargs): + def __init__(self, section: lief.ELF.Section, **kwargs) -> None: # type: ignore[no-untyped-def] + """Creates an ELF Section object. Object generated by ELFObject.""" # Python3 way # super().__init__('pe-section') - super(ELFSectionObject, self).__init__('elf-section', standalone=standalone, **kwargs) + super().__init__('elf-section', **kwargs) self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) self.add_attribute('type', value=str(self.__section.type).split('.')[1]) for flag in self.__section.flags_list: self.add_attribute('flag', value=str(flag).split('.')[1]) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: self.add_attribute('entropy', value=self.__section.entropy) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) diff --git a/pymisp/tools/emailobject.py b/pymisp/tools/emailobject.py index 7f83f36..fff56c5 100644 --- a/pymisp/tools/emailobject.py +++ b/pymisp/tools/emailobject.py @@ -1,63 +1,409 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 -from ..exceptions import InvalidMISPObject -from .abstractgenerator import AbstractMISPObjectGenerator -from io import BytesIO +from __future__ import annotations + +import re import logging -from email import message_from_bytes, policy +import ipaddress +import email.utils +from email import policy, message_from_bytes +from email.message import EmailMessage +from io import BytesIO +from pathlib import Path +from typing import cast, Any + +from extract_msg import openMsg +from extract_msg.msg_classes import MessageBase +from extract_msg.attachments import AttachmentBase, SignedAttachment +from extract_msg.properties import FixedLengthProp +from RTFDE.exceptions import MalformedEncapsulatedRtf, NotEncapsulatedRtf # type: ignore +from RTFDE.deencapsulate import DeEncapsulator # type: ignore +from oletools.common.codepages import codepage2codec # type: ignore + +from ..exceptions import InvalidMISPObject, MISPObjectException, NewAttributeError +from .abstractgenerator import AbstractMISPObjectGenerator logger = logging.getLogger('pymisp') -class EMailObject(AbstractMISPObjectGenerator): +class MISPMsgConverstionError(MISPObjectException): + pass - def __init__(self, filepath=None, pseudofile=None, attach_original_email=True, standalone=True, **kwargs): - if filepath: - with open(filepath, 'rb') as f: - self.__pseudofile = BytesIO(f.read()) - elif pseudofile and isinstance(pseudofile, BytesIO): - self.__pseudofile = pseudofile - else: - raise InvalidMISPObject('File buffer (BytesIO) or a path is required.') - # PY3 way: - # super().__init__('file') - super(EMailObject, self).__init__('email', standalone=standalone, **kwargs) - self.__email = message_from_bytes(self.__pseudofile.getvalue(), policy=policy.default) - if attach_original_email: - self.add_attribute('eml', value='Full email.eml', data=self.__pseudofile) + +class EMailObject(AbstractMISPObjectGenerator): + def __init__(self, filepath: Path | str | None=None, pseudofile: BytesIO | bytes | None=None, # type: ignore[no-untyped-def] + attach_original_email: bool = True, **kwargs) -> None: + super().__init__('email', **kwargs) + + self.attach_original_email = attach_original_email + self.encapsulated_body: str | None = None + self.eml_from_msg: bool | None = None + self.raw_emails: dict[str, BytesIO | None] = {'msg': None, 'eml': None} + + self.__pseudofile = self.create_pseudofile(filepath, pseudofile) + self.email = self.parse_email() self.generate_attributes() - @property - def email(self): - return self.__email + def parse_email(self) -> EmailMessage: + """Convert email into EmailMessage.""" + content_in_bytes = self.__pseudofile.getvalue().strip() + eml = message_from_bytes(content_in_bytes, + _class=EmailMessage, + policy=policy.default) + eml = cast(EmailMessage, eml) # Only needed to quiet mypy + if len(eml) != 0: + self.raw_emails['eml'] = self.__pseudofile + return eml + else: + logger.debug("Email not in standard .eml format. Attempting to decode email from other formats.") + try: # Check for .msg formatted emails. + # Msg files have the same header signature as the CFB format + if content_in_bytes[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1": + message = self._msg_to_eml(content_in_bytes) + if len(message) != 0: + self.eml_from_msg = True + self.raw_emails['msg'] = self.__pseudofile + self.raw_emails['msg'] = BytesIO(message.as_bytes()) + return message + except ValueError as _e: # Exception + logger.debug("Email not in .msg format or is a corrupted .msg. Attempting to decode email from other formats.") + logger.debug(f"Error: {_e} ") + try: + if content_in_bytes[:3] == b'\xef\xbb\xbf': # utf-8-sig byte-order mark (BOM) + eml_bytes = content_in_bytes.decode("utf_8_sig").encode("utf-8") + eml = email.message_from_bytes(eml_bytes, + policy=policy.default) + eml = cast(EmailMessage, eml) # Only needed to quiet mypy + if len(eml) != 0: + self.raw_emails['eml'] = BytesIO(eml_bytes) + return eml + except UnicodeDecodeError: + pass + raise InvalidMISPObject("EmailObject does not know how to decode data passed to it. Object may not be an email. If this is an email please submit it as an issue to PyMISP so we can add support.") + + @staticmethod + def create_pseudofile(filepath: Path | str | None = None, + pseudofile: BytesIO | bytes | None = None) -> BytesIO: + """Creates a pseudofile using directly passed data or data loaded from file path. + """ + if filepath: + with open(filepath, 'rb') as f: + return BytesIO(f.read()) + elif pseudofile and isinstance(pseudofile, BytesIO): + return pseudofile + elif pseudofile and isinstance(pseudofile, bytes): + return BytesIO(pseudofile) + else: + raise InvalidMISPObject('File buffer (BytesIO) or a path is required.') + + def _msg_to_eml(self, msg_bytes: bytes) -> EmailMessage: + """Converts a msg into an eml.""" + # NOTE: openMsg returns a MessageBase, not a MSGFile + msg_obj: MessageBase = openMsg(msg_bytes) # type: ignore + # msg obj stores the original raw header here + message, body, attachments = self._extract_msg_objects(msg_obj) + eml = self._build_eml(message, body, attachments) + return eml + + def _extract_msg_objects(self, msg_obj: MessageBase) -> tuple[EmailMessage, dict[str, Any], list[AttachmentBase] | list[SignedAttachment]]: + """Extracts email objects needed to construct an eml from a msg.""" + message: EmailMessage = email.message_from_string(msg_obj.header.as_string(), policy=policy.default) # type: ignore + body = {} + if msg_obj.body is not None: + body['text'] = {"obj": msg_obj.body, + "subtype": 'plain', + "charset": "utf-8", + "cte": "base64"} + if msg_obj.htmlBody is not None: + try: + if isinstance(msg_obj.props['3FDE0003'], FixedLengthProp): + _html_encoding_raw = msg_obj.props['3FDE0003'].value + _html_encoding = codepage2codec(_html_encoding_raw) + else: + _html_encoding = msg_obj.stringEncoding + except KeyError: + _html_encoding = msg_obj.stringEncoding + body['html'] = {'obj': msg_obj.htmlBody.decode(), + "subtype": 'html', + "charset": _html_encoding, + "cte": "base64"} + if msg_obj.rtfBody is not None: + body['rtf'] = {"obj": msg_obj.rtfBody.decode(), + "subtype": 'rtf', + "charset": 'ascii', + "cte": "base64"} + try: + rtf_obj = DeEncapsulator(msg_obj.rtfBody) + rtf_obj.deencapsulate() + if (rtf_obj.content_type == "html") and (msg_obj.htmlBody is None): + self.encapsulated_body = 'text/html' + body['html'] = {"obj": rtf_obj.html, + "subtype": 'html', + "charset": rtf_obj.text_codec, + "cte": "base64"} + elif (rtf_obj.content_type == "text") and (msg_obj.body is None): + self.encapsulated_body = 'text/plain' + body['text'] = {"obj": rtf_obj.plain_text, + "subtype": 'plain', + "charset": rtf_obj.text_codec} + except NotEncapsulatedRtf: + logger.debug("RTF body in Msg object is not encapsualted.") + except MalformedEncapsulatedRtf: + logger.info("RTF body in Msg object contains encapsulated content, but it is malformed and can't be converted.") + attachments = msg_obj.attachments + return message, body, attachments + + def _build_eml(self, message: EmailMessage, body: dict[str, Any], attachments: list[Any]) -> EmailMessage: + """Constructs an eml file from objects extracted from a msg.""" + # Order the body objects by increasing complexity and toss any missing objects + body_objects: list[dict[str, Any]] = [i for i in [body.get('text'), + body.get('html'), + body.get('rtf')] if i is not None] + # If this a non-multipart email then we only need to attach the payload + if message.get_content_maintype() != 'multipart': + for _body in body_objects: + if "text/{}".format(_body['subtype']) == message.get_content_type(): + message.set_content(**_body) + return message + raise MISPMsgConverstionError("Unable to find appropriate eml payload in message body.") + # If multipart we are going to have to set the content type to null and build it back up. + _orig_boundry = message.get_boundary() + message.clear_content() + # See if we are dealing with `related` inline content + related_content = {} + if isinstance(body.get('html', None), dict): + _html = body.get('html', {}).get('obj') + for attch in attachments: + if _html.find(f"cid:{attch.cid}") != -1: + _content_type = attch.getStringStream('__substg1.0_370E') + maintype, subtype = _content_type.split("/", 1) + related_content[attch.cid] = (attch, + {'obj': attch.data, + "maintype": maintype, + "subtype": subtype, + "cid": attch.cid, + "filename": attch.longFilename}) + if len(related_content) > 0: + if body.get('text', None) is not None: + # Text always goes first in an alternative, but we need the related object first + body_text = body.get('text') + if isinstance(body_text, dict): + message.add_related(**body_text) + else: + body_html = body.get('html') + if isinstance(body_html, dict): + message.add_related(**body_html) + for mime_items in related_content.values(): + if isinstance(mime_items[1], dict): + message.add_related(**mime_items[1]) + if p := message.get_payload(): + if isinstance(p, list): + cur_attach = p[-1] + else: + cur_attach = p + self._update_content_disp_properties(mime_items[0], cur_attach) + if body.get('text', None): + # Now add the HTML as an alternative within the related obj + if p := message.get_payload(): + if isinstance(p, list): + related = p[0] + else: + related = p + related.add_alternative(**body.get('html')) + else: + for mime_dict in body_objects: + # If encapsulated then don't attach RTF + if self.encapsulated_body is not None: + if mime_dict.get('subtype', "") == "rtf": + continue + if isinstance(mime_dict, dict): + message.add_alternative(**mime_dict) + for attch in attachments: # Add attachments at the end. + if attch.cid not in related_content.keys(): + _content_type = attch.getStringStream('__substg1.0_370E') + maintype, subtype = _content_type.split("/", 1) + message.add_attachment(attch.data, + maintype=maintype, + subtype=subtype, + cid=attch.cid, + filename=attch.longFilename) + if p := message.get_payload(): + if isinstance(p, list): + cur_attach = p[-1] + else: + cur_attach = p + self._update_content_disp_properties(attch, cur_attach) + if _orig_boundry is not None: + message.set_boundary(_orig_boundry) # Set back original boundary + return message + + @staticmethod + def _update_content_disp_properties(msg_attch: AttachmentBase, eml_attch: EmailMessage) -> None: + """Set Content-Disposition params on binary eml objects + + You currently have to set non-filename content-disp params by hand in python. + """ + attch_cont_disp_props = {'30070040': "creation-date", + '30080040': "modification-date"} + for num, name in attch_cont_disp_props.items(): + try: + eml_attch.set_param(name, + email.utils.format_datetime(msg_attch.props.getValue(num)), + header='Content-Disposition') + except KeyError: + # It's fine if they don't have those values + pass @property - def attachments(self): + def attachments(self) -> list[tuple[str | None, BytesIO]]: to_return = [] - for attachment in self.__email.iter_attachments(): - to_return.append((attachment.get_filename(), BytesIO(attachment.get_content()))) + try: + for attachment in self.email.iter_attachments(): + content = attachment.get_content() + if isinstance(content, str): + content = content.encode() + to_return.append((attachment.get_filename(), BytesIO(content))) + except AttributeError: + # ignore bug in Python3.6, that cause exception for empty email body, + # see https://stackoverflow.com/questions/56391306/attributeerror-str-object-has-no-attribute-copy-when-parsing-multipart-emai + pass return to_return - def generate_attributes(self): - if self.__email.get_body(preferencelist=('html', 'plain')): - self.add_attribute('email-body', value=self.__email.get_body(preferencelist=('html', 'plain')).get_payload(decode=True).decode('utf8', 'surrogateescape')) - if 'Reply-To' in self.__email: - self.add_attribute('reply-to', value=self.__email['Reply-To']) - if 'Message-ID' in self.__email: - self.add_attribute('message-id', value=self.__email['Message-ID']) - if 'To' in self.__email: - for to in self.__email['To'].split(','): - self.add_attribute('to', value=to.strip()) - if 'Cc' in self.__email: - for cc in self.__email['Cc'].split(','): - self.add_attribute('cc', value=cc.strip()) - if 'Subject' in self.__email: - self.add_attribute('subject', value=self.__email['Subject']) - if 'From' in self.__email: - for e_from in self.__email['From'].split(','): - self.add_attribute('from', value=e_from.strip()) - if 'Return-Path' in self.__email: - self.add_attribute('return-path', value=self.__email['Return-Path']) - if 'User-Agent' in self.__email: - self.add_attribute('user-agent', value=self.__email['User-Agent']) + def generate_attributes(self) -> None: + + # Attach original & Converted + if self.attach_original_email is not None: + self.add_attribute("eml", value="Full email.eml", + data=self.raw_emails.get('eml'), + comment="Converted from MSG format" if self.eml_from_msg else None) + if self.raw_emails.get('msg', None) is not None: + self.add_attribute("msg", value="Full email.msg", + data=self.raw_emails.get('msg')) + + message = self.email + body: EmailMessage + + if body := message.get_body(preferencelist=['plain']): + comment = f"{body.get_content_type()} body" + if self.encapsulated_body == body.get_content_type(): + comment += " De-Encapsulated from RTF in original msg." + self.add_attribute("email-body", + body.get_content(), + comment=comment) + + if body := message.get_body(preferencelist=['html']): + comment = f"{body.get_content_type()} body" + if self.encapsulated_body == body.get_content_type(): + comment += " De-Encapsulated from RTF in original msg." + self.add_attribute("email-body", + body.get_content(), + comment=comment) + + headers = [f"{k}: {v}" for k, v in message.items()] + if headers: + self.add_attribute("header", "\n".join(headers)) + + if "Date" in message and message['date'].datetime is not None: + self.add_attribute("send-date", message['date'].datetime) + + if "To" in message: + self.__add_emails("to", message["To"]) + if "Delivered-To" in message: + self.__add_emails("to", message["Delivered-To"]) + + if "From" in message: + self.__add_emails("from", message["From"]) + + if "Return-Path" in message: + realname, address = email.utils.parseaddr(message["Return-Path"]) + self.add_attribute("return-path", address) + + if "Reply-To" in message: + self.__add_emails("reply-to", message["reply-to"]) + + if "Bcc" in message: + self.__add_emails("bcc", message["Bcc"]) + + if "Cc" in message: + self.__add_emails("cc", message["Cc"]) + + if "Subject" in message: + self.add_attribute("subject", message["Subject"]) + + if "Message-ID" in message: + self.add_attribute("message-id", message["Message-ID"]) + + if "User-Agent" in message: + self.add_attribute("user-agent", message["User-Agent"]) + + boundary = message.get_boundary() + if boundary: + self.add_attribute("mime-boundary", boundary) + + if "X-Mailer" in message: + self.add_attribute("x-mailer", message["X-Mailer"]) + + if "Thread-Index" in message: + self.add_attribute("thread-index", message["Thread-Index"]) + + self.__generate_received() + + def __add_emails(self, typ: str, data: str, insert_display_names: bool = True) -> None: + addresses: list[dict[str, str]] = [] + display_names: list[dict[str, str]] = [] + + for realname, address in email.utils.getaddresses([data]): + if address and realname: + addresses.append({"value": address, "comment": f"{realname} <{address}>"}) + elif address: + addresses.append({"value": address}) + else: # parsing failed, skip + continue + + if realname: + display_names.append({"value": realname, "comment": f"{realname} <{address}>"}) + + for a in addresses: + self.add_attribute(typ, **a) + if insert_display_names and display_names: + try: + for d in display_names: + self.add_attribute(f"{typ}-display-name", **d) + except NewAttributeError: + # email object doesn't support display name for all email addrs + pass + + def __generate_received(self) -> None: + """ + Extract IP addresses from received headers that are not private. Also extract hostnames or domains. + """ + received_items = self.email.get_all("received") + if received_items is None: + return + for received in received_items: + fromstr = re.split(r"\sby\s", received)[0].strip() + if fromstr.startswith('from') is not True: + continue + for i in ['(', ')', '[', ']']: + fromstr = fromstr.replace(i, " ") + tokens = fromstr.split(" ") + ip = None + for token in tokens: + try: + ip = ipaddress.ip_address(token) + break + except ValueError: + pass # token is not IP address + + if not ip or ip.is_private: + continue # skip header if IP not found or is private + + self.add_attribute("received-header-ip", value=str(ip), comment=fromstr) + + # The hostnames and/or domains always come after the "Received: from" + # part so we can use regex to pick up those attributes. + received_from = re.findall(r'(?<=from\s)[\w\d\.\-]+\.\w{2,24}', str(received_items)) + try: + [self.add_attribute("received-header-hostname", i) for i in received_from] + except Exception: + pass diff --git a/pymisp/tools/ext_lookups.py b/pymisp/tools/ext_lookups.py index 7a439c4..c4e81ac 100644 --- a/pymisp/tools/ext_lookups.py +++ b/pymisp/tools/ext_lookups.py @@ -1,20 +1,21 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations try: - from pymispgalaxies import Clusters + from pymispgalaxies import Clusters # type: ignore has_pymispgalaxies = True except ImportError: has_pymispgalaxies = False try: - from pytaxonomies import Taxonomies + from pytaxonomies import Taxonomies # type: ignore has_pymispgalaxies = True except ImportError: has_pymispgalaxies = False -def revert_tag_from_galaxies(tag): +def revert_tag_from_galaxies(tag: str) -> list[str]: clusters = Clusters() try: return clusters.revert_machinetag(tag) @@ -22,7 +23,7 @@ def revert_tag_from_galaxies(tag): return [] -def revert_tag_from_taxonomies(tag): +def revert_tag_from_taxonomies(tag: str) -> list[str]: taxonomies = Taxonomies() try: return taxonomies.revert_machinetag(tag) @@ -30,7 +31,7 @@ def revert_tag_from_taxonomies(tag): return [] -def search_taxonomies(query): +def search_taxonomies(query: str) -> list[str]: taxonomies = Taxonomies() found = taxonomies.search(query) if not found: @@ -38,6 +39,6 @@ def search_taxonomies(query): return found -def search_galaxies(query): +def search_galaxies(query: str) -> list[str]: clusters = Clusters() return clusters.search(query) diff --git a/pymisp/tools/fail2banobject.py b/pymisp/tools/fail2banobject.py index e49cd50..a8e3fda 100644 --- a/pymisp/tools/fail2banobject.py +++ b/pymisp/tools/fail2banobject.py @@ -1,20 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class Fail2BanObject(AbstractMISPObjectGenerator): - def __init__(self, parameters, strict=True, standalone=True, **kwargs): - super(Fail2BanObject, self).__init__('fail2ban', strict=strict, standalone=standalone, **kwargs) + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs): # type: ignore[no-untyped-def] + super().__init__('fail2ban', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: timestamp = self._sanitize_timestamp(self._parameters.pop('processing-timestamp', None)) self._parameters['processing-timestamp'] = timestamp - return super(Fail2BanObject, self).generate_attributes() + super().generate_attributes() diff --git a/pymisp/tools/feed.py b/pymisp/tools/feed.py new file mode 100644 index 0000000..0452a25 --- /dev/null +++ b/pymisp/tools/feed.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +from pathlib import Path +from pymisp import MISPEvent +import json + + +def feed_meta_generator(path: Path) -> None: + manifests = {} + hashes: list[str] = [] + + for f_name in path.glob('*.json'): + if str(f_name.name) == 'manifest.json': + continue + event = MISPEvent() + event.load_file(str(f_name)) + manifests.update(event.manifest) + hashes += [f'{h},{event.uuid}' for h in event.attributes_hashes('md5')] + + with (path / 'manifest.json').open('w') as f: + json.dump(manifests, f) + + with (path / 'hashes.csv').open('w') as f: + for h in hashes: + f.write(f'{h}\n') diff --git a/pymisp/tools/fileobject.py b/pymisp/tools/fileobject.py index e9b05cd..f8b277e 100644 --- a/pymisp/tools/fileobject.py +++ b/pymisp/tools/fileobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from ..exceptions import InvalidMISPObject from .abstractgenerator import AbstractMISPObjectGenerator @@ -9,12 +10,13 @@ from hashlib import md5, sha1, sha256, sha512 import math from collections import Counter import logging +from pathlib import Path logger = logging.getLogger('pymisp') try: - import pydeep + import pydeep # type: ignore HAS_PYDEEP = True except ImportError: HAS_PYDEEP = False @@ -28,11 +30,14 @@ except ImportError: class FileObject(AbstractMISPObjectGenerator): - def __init__(self, filepath=None, pseudofile=None, filename=None, standalone=True, **kwargs): + def __init__(self, filepath: Path | str | None = None, # type: ignore[no-untyped-def] + pseudofile: BytesIO | bytes | None = None, + filename: str | None = None, **kwargs) -> None: + super().__init__('file', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if not HAS_MAGIC: - logger.warning("Please install python-magic: pip install python-magic.") + logger.warning("python-magic is missing, please install pymisp this way: pip install pymisp[fileobjects]") if filename: # Useful in case the file is copied with a pre-defined name by a script but we want to keep the original name self.__filename = filename @@ -49,38 +54,35 @@ class FileObject(AbstractMISPObjectGenerator): self.__pseudofile = pseudofile else: raise InvalidMISPObject('File buffer (BytesIO) or a path is required.') - # PY3 way: - # super().__init__('file') - super(FileObject, self).__init__('file', standalone=standalone, **kwargs) self.__data = self.__pseudofile.getvalue() self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('filename', value=self.__filename) - size = self.add_attribute('size-in-bytes', value=len(self.__data)) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=len(self.__data)) + if len(self.__data) > 0: self.add_attribute('entropy', value=self.__entropy_H(self.__data)) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) self.add_attribute('sha256', value=sha256(self.__data).hexdigest()) self.add_attribute('sha512', value=sha512(self.__data).hexdigest()) - self.add_attribute('malware-sample', value=self.__filename, data=self.__pseudofile) + self.add_attribute('malware-sample', value=self.__filename, data=self.__pseudofile, disable_correlation=True) if HAS_MAGIC: - self.add_attribute('mimetype', value=magic.from_buffer(self.__data)) + self.add_attribute('mimetype', value=magic.from_buffer(self.__data, mime=True)) if HAS_PYDEEP: self.add_attribute('ssdeep', value=pydeep.hash_buf(self.__data).decode()) - def __entropy_H(self, data): + def __entropy_H(self, data: bytes) -> float: """Calculate the entropy of a chunk of data.""" # NOTE: copy of the entropy function from pefile if len(data) == 0: return 0.0 - occurences = Counter(bytearray(data)) + occurrences = Counter(bytearray(data)) - entropy = 0 - for x in occurences.values(): + entropy = 0.0 + for x in occurrences.values(): p_x = float(x) / len(data) entropy -= p_x * math.log(p_x, 2) diff --git a/pymisp/tools/genericgenerator.py b/pymisp/tools/genericgenerator.py index cb339a2..811f604 100644 --- a/pymisp/tools/genericgenerator.py +++ b/pymisp/tools/genericgenerator.py @@ -1,12 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator class GenericObjectGenerator(AbstractMISPObjectGenerator): - def generate_attributes(self, attributes): + # FIXME: this method is different from the master one, and that's probably not a good idea. + def generate_attributes(self, attributes: list[dict[str, Any]]) -> None: # type: ignore[override] """Generates MISPObjectAttributes from a list of dictionaries. Each entry if the list must be in one of the two following formats: * {: } diff --git a/pymisp/tools/geolocationobject.py b/pymisp/tools/geolocationobject.py index b8b1c24..fc995c9 100644 --- a/pymisp/tools/geolocationobject.py +++ b/pymisp/tools/geolocationobject.py @@ -1,22 +1,26 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator -import logging logger = logging.getLogger('pymisp') class GeolocationObject(AbstractMISPObjectGenerator): - def __init__(self, parameters, strict=True, standalone=True, **kwargs): - super(GeolocationObject, self).__init__('asn', strict=strict, standalone=standalone, **kwargs) + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('geolocation', strict=strict, **kwargs) self._parameters = parameters self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: first = self._sanitize_timestamp(self._parameters.pop('first-seen', None)) self._parameters['first-seen'] = first last = self._sanitize_timestamp(self._parameters.pop('last-seen', None)) self._parameters['last-seen'] = last - return super(GeolocationObject, self).generate_attributes() + super().generate_attributes() diff --git a/pymisp/tools/git_vuln_finder_object.py b/pymisp/tools/git_vuln_finder_object.py new file mode 100644 index 0000000..21ec512 --- /dev/null +++ b/pymisp/tools/git_vuln_finder_object.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import logging + +from typing import Any + +from .abstractgenerator import AbstractMISPObjectGenerator + +logger = logging.getLogger('pymisp') + + +class GitVulnFinderObject(AbstractMISPObjectGenerator): + + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('git-vuln-finder', strict=strict, **kwargs) + self._parameters = parameters + self.generate_attributes() + + def generate_attributes(self) -> None: + authored_date = self._sanitize_timestamp(self._parameters.pop('authored_date', None)) + self._parameters['authored_date'] = authored_date + committed_date = self._sanitize_timestamp(self._parameters.pop('committed_date', None)) + self._parameters['committed_date'] = committed_date + if 'stats' in self._parameters: + stats = self._parameters.pop('stats') + self._parameters['stats.insertions'] = stats.pop('insertions') + self._parameters['stats.deletions'] = stats.pop('deletions') + self._parameters['stats.lines'] = stats.pop('lines') + self._parameters['stats.files'] = stats.pop('files') + super().generate_attributes() diff --git a/pymisp/tools/load_warninglists.py b/pymisp/tools/load_warninglists.py index b94b936..feb5ea3 100644 --- a/pymisp/tools/load_warninglists.py +++ b/pymisp/tools/load_warninglists.py @@ -1,26 +1,30 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations + +from ..api import PyMISP try: - from pymispwarninglists import WarningLists + from pymispwarninglists import WarningLists, WarningList # type: ignore has_pymispwarninglists = True except ImportError: has_pymispwarninglists = False -def from_instance(pymisp_instance, slow_search=False): +def from_instance(pymisp_instance: PyMISP, slow_search: bool=False) -> WarningLists: """Load the warnindlist from an existing MISP instance :pymisp_instance: Already instantialized PyMISP instance.""" - warninglists_index = pymisp_instance.get_warninglists()['Warninglists'] + warninglists_index = pymisp_instance.warninglists(pythonify=True) all_warningslists = [] for warninglist in warninglists_index: - wl = pymisp_instance.get_warninglist(warninglist['Warninglist']['id'])['Warninglist'] - wl['list'] = wl.pop('WarninglistEntry') - all_warningslists.append(wl) + if isinstance(warninglist, WarningList): + wl = pymisp_instance.get_warninglist(warninglist['Warninglist']['id'])['Warninglist'] + wl['list'] = wl.pop('WarninglistEntry') + all_warningslists.append(wl) return WarningLists(slow_search, all_warningslists) -def from_package(slow_search=False): +def from_package(slow_search: bool=False) -> WarningLists: return WarningLists(slow_search) diff --git a/pymisp/tools/machoobject.py b/pymisp/tools/machoobject.py index ed6e2ae..ad68e46 100644 --- a/pymisp/tools/machoobject.py +++ b/pymisp/tools/machoobject.py @@ -1,58 +1,81 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from ..exceptions import InvalidMISPObject -from .abstractgenerator import AbstractMISPObjectGenerator -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 +from __future__ import annotations + import logging -logger = logging.getLogger('pymisp') +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any +from ..exceptions import InvalidMISPObject + +from . import FileObject +from .abstractgenerator import AbstractMISPObjectGenerator + +import lief try: - import lief - HAS_LIEF = True -except ImportError: - HAS_LIEF = False - -try: - import pydeep + import pydeep # type: ignore HAS_PYDEEP = True except ImportError: HAS_PYDEEP = False +logger = logging.getLogger('pymisp') + + +def make_macho_objects(lief_parsed: lief.MachO.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, MachOObject, list[MachOSectionObject]]: + macho_object = MachOObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) + misp_file.add_reference(macho_object.uuid, 'includes', 'MachO indicators') + macho_sections = [] + for s in macho_object.sections: + macho_sections.append(s) + return misp_file, macho_object, macho_sections + class MachOObject(AbstractMISPObjectGenerator): - def __init__(self, parsed=None, filepath=None, pseudofile=None, standalone=True, **kwargs): + __macho: lief.MachO.Binary + + def __init__(self, parsed: lief.MachO.Binary | lief.MachO.FatBinary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | list[int] | None = None, + **kwargs) -> None: + """Creates an MachO object, with lief""" + super().__init__('macho', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") - if not HAS_LIEF: - raise ImportError('Please install lief, documentation here: https://github.com/lief-project/LIEF') + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__macho = lief.MachO.parse(raw=pseudofile.getvalue()) + m = lief.MachO.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__macho = lief.MachO.parse(raw=pseudofile) + m = lief.MachO.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + m = lief.MachO.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not m: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__macho = m.at(0) elif filepath: - self.__macho = lief.MachO.parse(filepath) + if m := lief.MachO.parse(filepath): + self.__macho = m.at(0) elif parsed: # Got an already parsed blob - if isinstance(parsed, lief.MachO.Binary): + if isinstance(parsed, lief.MachO.FatBinary): + self.__macho = parsed.at(0) + elif isinstance(parsed, lief.MachO.Binary): self.__macho = parsed else: - raise InvalidMISPObject('Not a lief.MachO.Binary: {}'.format(type(parsed))) - # Python3 way - # super().__init__('elf') - super(MachOObject, self).__init__('macho', standalone=standalone, **kwargs) + raise InvalidMISPObject(f'Not a lief.MachO.Binary: {type(parsed)}') self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('type', value=str(self.__macho.header.file_type).split('.')[1]) - self.add_attribute('name', value=self.__macho.name) # General information if self.__macho.has_entrypoint: self.add_attribute('entrypoint-address', value=self.__macho.entrypoint) @@ -61,8 +84,8 @@ class MachOObject(AbstractMISPObjectGenerator): if self.__macho.sections: pos = 0 for section in self.__macho.sections: - s = MachOSectionObject(section, self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'included-in', 'Section {} of MachO'.format(pos)) + s = MachOSectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) + self.add_reference(s.uuid, 'includes', f'Section {pos} of MachO') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) @@ -70,18 +93,19 @@ class MachOObject(AbstractMISPObjectGenerator): class MachOSectionObject(AbstractMISPObjectGenerator): - def __init__(self, section, standalone=True, **kwargs): + def __init__(self, section: lief.MachO.Section, **kwargs) -> None: # type: ignore[no-untyped-def] + """Creates an MachO Section object. Object generated by MachOObject.""" # Python3 way # super().__init__('pe-section') - super(MachOSectionObject, self).__init__('macho-section', standalone=standalone, **kwargs) + super().__init__('macho-section', **kwargs) self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: self.add_attribute('entropy', value=self.__section.entropy) self.add_attribute('md5', value=md5(self.__data).hexdigest()) self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) diff --git a/pymisp/tools/microblogobject.py b/pymisp/tools/microblogobject.py new file mode 100644 index 0000000..63d20a1 --- /dev/null +++ b/pymisp/tools/microblogobject.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import logging +from typing import Any +# NOTE: Reference on how this module is used: https://vvx7.io/posts/2020/05/misp-slack-bot/ + +from .abstractgenerator import AbstractMISPObjectGenerator + +logger = logging.getLogger('pymisp') + + +class MicroblogObject(AbstractMISPObjectGenerator): + + def __init__(self, parameters: dict[str, Any], strict: bool = True, **kwargs): # type: ignore[no-untyped-def] + super().__init__('microblog', strict=strict, **kwargs) + self._parameters = parameters + self.generate_attributes() + + def generate_attributes(self) -> None: + # Raw post. + if 'post' in self._parameters: + self.add_attribute('post', value=self._parameters['post']) + + # Title of the post. + if 'title' in self._parameters: + self.add_attribute('title', value=self._parameters['title']) + + # Original link into the microblog post (Supposed harmless). + if 'link' in self._parameters: + self.add_attribute('link', value=self._parameters['link']) + + # Original URL location of the microblog post (potentially malicious. + if 'url' in self._parameters: + if isinstance(self._parameters.get('url'), list): + for i in self._parameters['url']: + self.add_attribute('url', value=i) + else: + self.add_attribute('url', value=self._parameters['url']) + + # Archive of the original document (Internet Archive, Archive.is, etc). + if 'archive' in self._parameters: + if isinstance(self._parameters.get('archive'), list): + for i in self._parameters['archive']: + self.add_attribute('archive', value=i) + else: + self.add_attribute('archive', value=self._parameters['archive']) + + # Display name of the account who posted the microblog. + if 'display-name' in self._parameters: + self.add_attribute('display-name', value=self._parameters['display-name']) + + # The user ID of the microblog this post replies to. + if 'in-reply-to-user-id' in self._parameters: + self.add_attribute('in-reply-to-user-id', value=self._parameters['in-reply-to-user-id']) + + # The microblog ID of the microblog this post replies to. + if 'in-reply-to-status-id' in self._parameters: + self.add_attribute('in-reply-to-status-id', value=self._parameters['in-reply-to-status-id']) + + # The user display name of the microblog this post replies to. + if 'in-reply-to-display-name' in self._parameters: + self.add_attribute('in-reply-to-display-name', value=self._parameters['in-reply-to-display-name']) + + # The language of the post. + if 'language' in self._parameters: + self.add_attribute('language', value=self._parameters['language'], disable_correlation=True) + + # The microblog post file or screen capture. + # if 'attachment' in self._parameters: + # self.add_attribute('attachment', value=self._parameters['attachment']) + + # Type of the microblog post. + type_allowed_values = ["Twitter", "Facebook", "LinkedIn", "Reddit", "Google+", + "Instagram", "Forum", "Other"] + if 'type' in self._parameters: + if isinstance(self._parameters.get('type'), list): + for i in self._parameters['type']: + if i in type_allowed_values: + self.add_attribute('type', value=i) + else: + if self._parameters['type'] in type_allowed_values: + self.add_attribute('type', value=self._parameters['type']) + + # State of the microblog post. + type_allowed_values = ["Informative", "Malicious", "Misinformation", "Disinformation", "Unknown"] + if 'state' in self._parameters: + if isinstance(self._parameters.get('state'), list): + for i in self._parameters['state']: + if i in type_allowed_values: + self.add_attribute('state', value=i) + else: + if self._parameters['state'] in type_allowed_values: + self.add_attribute('state', value=self._parameters['state']) + + # Username who posted the microblog post (without the @ prefix). + if 'username' in self._parameters: + self.add_attribute('username', value=self._parameters['username']) + + # == the username account verified by the operator of the microblog platform. + type_allowed_values = ["Verified", "Unverified", "Unknown"] + if 'verified-username' in self._parameters: + if isinstance(self._parameters.get('verified-username'), list): + for i in self._parameters['verified-username']: + if i in type_allowed_values: + self.add_attribute('verified-username', value=i) + else: + if self._parameters['verified-username'] in type_allowed_values: + self.add_attribute('verified-username', value=self._parameters['verified-username']) + + # embedded-link. + if 'embedded-link' in self._parameters: + if isinstance(self._parameters.get('embedded-link'), list): + for i in self._parameters['embedded-link']: + self.add_attribute('embedded-link', value=i) + else: + self.add_attribute('embedded-link', value=self._parameters['embedded-link']) + + # embedded-safe-link + if 'embedded-safe-link' in self._parameters: + if isinstance(self._parameters.get('embedded-safe-link'), list): + for i in self._parameters['embedded-safe-link']: + self.add_attribute('embedded-safe-link', value=i) + else: + self.add_attribute('embedded-safe-link', value=self._parameters['embedded-safe-link']) + + # Hashtag into the microblog post. + if 'hashtag' in self._parameters: + if isinstance(self._parameters.get('hashtag'), list): + for i in self._parameters['hashtag']: + self.add_attribute('hashtag', value=i) + else: + self.add_attribute('hashtag', value=self._parameters['hashtag']) + + # username quoted + if 'username-quoted' in self._parameters: + if isinstance(self._parameters.get('username-quoted'), list): + for i in self._parameters['username-quoted']: + self.add_attribute('username-quoted', value=i) + else: + self.add_attribute('username-quoted', value=self._parameters['username-quoted']) + + # twitter post id + if 'twitter-id' in self._parameters: + self.add_attribute('twitter-id', value=self._parameters['twitter-id']) diff --git a/pymisp/tools/neo4j.py b/pymisp/tools/neo4j.py index e77d0c0..7a71978 100644 --- a/pymisp/tools/neo4j.py +++ b/pymisp/tools/neo4j.py @@ -1,11 +1,12 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations import glob import os + from .. import MISPEvent try: - from py2neo import authenticate, Graph, Node, Relationship + from py2neo import authenticate, Graph, Node, Relationship # type: ignore has_py2neo = True except ImportError: has_py2neo = False @@ -13,23 +14,23 @@ except ImportError: class Neo4j(): - def __init__(self, host='localhost:7474', username='neo4j', password='neo4j'): + def __init__(self, host: str='localhost:7474', username: str='neo4j', password: str='neo4j') -> None: if not has_py2neo: raise Exception('py2neo is required, please install: pip install py2neo') authenticate(host, username, password) - self.graph = Graph("http://{}/db/data/".format(host)) + self.graph = Graph(f"http://{host}/db/data/") - def load_events_directory(self, directory): - self.events = [] + def load_events_directory(self, directory: str) -> None: + self.events: list[MISPEvent] = [] for path in glob.glob(os.path.join(directory, '*.json')): e = MISPEvent() e.load(path) self.import_event(e) - def del_all(self): + def del_all(self) -> None: self.graph.delete_all() - def import_event(self, event): + def import_event(self, event: MISPEvent) -> None: tx = self.graph.begin() event_node = Node('Event', uuid=event.uuid, name=event.info) # event_node['distribution'] = event.distribution diff --git a/pymisp/tools/openioc.py b/pymisp/tools/openioc.py old mode 100755 new mode 100644 index 6251b48..488c5b0 --- a/pymisp/tools/openioc.py +++ b/pymisp/tools/openioc.py @@ -1,11 +1,10 @@ - -# -*- coding: utf-8 -*- +from __future__ import annotations import os from .. import MISPEvent try: - from bs4 import BeautifulSoup + from bs4 import BeautifulSoup # type: ignore has_bs4 = True except ImportError: has_bs4 = False @@ -100,7 +99,7 @@ iocMispMapping = { 'RouteEntryItem/Destination': {'type': 'ip-dst'}, 'RouteEntryItem/Destination/IP': {'type': 'ip-dst', 'comment': 'RouteDestination. '}, - 'RouteEntryItem/Destination/string': {'type': 'url', 'comment': 'RouteDestination. '}, + 'RouteEntryItem/Destination/string': {'type': 'hostname', 'comment': 'RouteDestination. '}, 'ServiceItem/name': {'type': 'windows-service-name'}, @@ -156,7 +155,7 @@ def extract_field(report, field_name): def load_openioc_file(openioc_path): if not os.path.exists(openioc_path): raise Exception("Path doesn't exists.") - with open(openioc_path, 'r') as f: + with open(openioc_path) as f: return load_openioc(f) @@ -218,7 +217,12 @@ def set_values(value1, value2=None): compositeMapping = '{}|{}'.format(value1.find('context')['search'], value2.find('context')['search']) mapping = get_mapping(compositeMapping, mappingDict=iocMispCompositeMapping) else: - mapping = get_mapping(value1.find('context')['search']) + context_search = value1.find('context')['search'] + content_type = value1.find('content').get('type', None) + if "RouteEntryItem/Destination" in context_search and content_type: + mapping = get_mapping(context_search + '/' + content_type) + else: + mapping = get_mapping(context_search) if mapping: attribute_values.update(mapping) diff --git a/pymisp/tools/pdf_fonts b/pymisp/tools/pdf_fonts deleted file mode 160000 index 7ff2220..0000000 --- a/pymisp/tools/pdf_fonts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7ff222022e6ad99e11a201f62d57da4bff1337ee diff --git a/pymisp/tools/peobject.py b/pymisp/tools/peobject.py index d55a97b..08f35cf 100644 --- a/pymisp/tools/peobject.py +++ b/pymisp/tools/peobject.py @@ -1,71 +1,97 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from ..exceptions import InvalidMISPObject -from .abstractgenerator import AbstractMISPObjectGenerator -from io import BytesIO -from hashlib import md5, sha1, sha256, sha512 -from datetime import datetime +from __future__ import annotations + import logging -logger = logging.getLogger('pymisp') +from base64 import b64encode +from datetime import datetime +from hashlib import md5, sha1, sha256, sha512 +from io import BytesIO +from pathlib import Path +from typing import Any + +from . import FileObject +from .abstractgenerator import AbstractMISPObjectGenerator +from ..exceptions import InvalidMISPObject + +import lief +import lief.PE try: - import lief - HAS_LIEF = True -except ImportError: - HAS_LIEF = False - -try: - import pydeep + import pydeep # type: ignore HAS_PYDEEP = True except ImportError: HAS_PYDEEP = False +logger = logging.getLogger('pymisp') + + +def make_pe_objects(lief_parsed: lief.PE.Binary, + misp_file: FileObject, + standalone: bool = True, + default_attributes_parameters: dict[str, Any] = {}) -> tuple[FileObject, PEObject, list[PESectionObject]]: + pe_object = PEObject(parsed=lief_parsed, standalone=standalone, default_attributes_parameters=default_attributes_parameters) + misp_file.add_reference(pe_object.uuid, 'includes', 'PE indicators') + pe_sections = [] + for s in pe_object.sections: + pe_sections.append(s) + return misp_file, pe_object, pe_sections + class PEObject(AbstractMISPObjectGenerator): - def __init__(self, parsed=None, filepath=None, pseudofile=None, standalone=True, **kwargs): + __pe: lief.PE.Binary + + def __init__(self, parsed: lief.PE.Binary | None = None, # type: ignore[no-untyped-def] + filepath: Path | str | None = None, + pseudofile: BytesIO | list[int] | None = None, + **kwargs) -> None: + """Creates an PE object, with lief""" + super().__init__('pe', **kwargs) if not HAS_PYDEEP: - logger.warning("Please install pydeep: pip install git+https://github.com/kbandla/pydeep.git") - if not HAS_LIEF: - raise ImportError('Please install lief, documentation here: https://github.com/lief-project/LIEF') + logger.warning("pydeep is missing, please install pymisp this way: pip install pymisp[fileobjects]") if pseudofile: if isinstance(pseudofile, BytesIO): - self.__pe = lief.PE.parse(raw=pseudofile.getvalue()) + p = lief.PE.parse(obj=pseudofile) elif isinstance(pseudofile, bytes): - self.__pe = lief.PE.parse(raw=pseudofile) + p = lief.PE.parse(raw=list(pseudofile)) + elif isinstance(pseudofile, list): + p = lief.PE.parse(raw=pseudofile) else: - raise InvalidMISPObject('Pseudo file can be BytesIO or bytes got {}'.format(type(pseudofile))) + raise InvalidMISPObject(f'Pseudo file can be BytesIO or bytes got {type(pseudofile)}') + if not p: + raise InvalidMISPObject('Unable to parse pseudofile') + self.__pe = p elif filepath: - self.__pe = lief.PE.parse(filepath) + if p := lief.PE.parse(filepath): + self.__pe = p + else: + raise InvalidMISPObject(f'Unable to parse {filepath}') elif parsed: # Got an already parsed blob if isinstance(parsed, lief.PE.Binary): self.__pe = parsed else: - raise InvalidMISPObject('Not a lief.PE.Binary: {}'.format(type(parsed))) - # Python3 way - # super().__init__('pe') - super(PEObject, self).__init__('pe', standalone=standalone, **kwargs) + raise InvalidMISPObject(f'Not a lief.PE.Binary: {type(parsed)}') self.generate_attributes() - def _is_exe(self): + def _is_exe(self) -> bool: if not self._is_dll() and not self._is_driver(): - return self.__pe.header.has_characteristic(lief.PE.HEADER_CHARACTERISTICS.EXECUTABLE_IMAGE) + return self.__pe.header.has_characteristic(lief.PE.Header.CHARACTERISTICS.EXECUTABLE_IMAGE) return False - def _is_dll(self): - return self.__pe.header.has_characteristic(lief.PE.HEADER_CHARACTERISTICS.DLL) + def _is_dll(self) -> bool: + return self.__pe.header.has_characteristic(lief.PE.Header.CHARACTERISTICS.DLL) - def _is_driver(self): + def _is_driver(self) -> bool: # List from pefile - system_DLLs = set(('ntoskrnl.exe', 'hal.dll', 'ndis.sys', 'bootvid.dll', 'kdcom.dll')) + system_DLLs = {'ntoskrnl.exe', 'hal.dll', 'ndis.sys', 'bootvid.dll', 'kdcom.dll'} if system_DLLs.intersection([imp.lower() for imp in self.__pe.libraries]): return True return False - def _get_pe_type(self): + def _get_pe_type(self) -> str: if self._is_dll(): return 'dll' elif self._is_driver(): @@ -75,64 +101,137 @@ class PEObject(AbstractMISPObjectGenerator): else: return 'unknown' - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('type', value=self._get_pe_type()) # General information self.add_attribute('entrypoint-address', value=self.__pe.entrypoint) self.add_attribute('compilation-timestamp', value=datetime.utcfromtimestamp(self.__pe.header.time_date_stamps).isoformat()) - # self.imphash = self.__pe.get_imphash() - try: - if (self.__pe.has_resources and - self.__pe.resources_manager.has_version and - self.__pe.resources_manager.version.has_string_file_info and - self.__pe.resources_manager.version.string_file_info.langcode_items): - fileinfo = dict(self.__pe.resources_manager.version.string_file_info.langcode_items[0].items.items()) + self.add_attribute('imphash', value=lief.PE.get_imphash(self.__pe, lief.PE.IMPHASH_MODE.PEFILE)) + self.add_attribute('authentihash', value=self.__pe.authentihash_sha256.hex()) + r_manager = self.__pe.resources_manager + if isinstance(r_manager, lief.PE.ResourcesManager): + version = r_manager.version + if isinstance(version, lief.PE.ResourceVersion) and version.string_file_info is not None: + fileinfo = dict(version.string_file_info.langcode_items[0].items.items()) self.add_attribute('original-filename', value=fileinfo.get('OriginalFilename')) self.add_attribute('internal-filename', value=fileinfo.get('InternalName')) self.add_attribute('file-description', value=fileinfo.get('FileDescription')) self.add_attribute('file-version', value=fileinfo.get('FileVersion')) - self.add_attribute('lang-id', value=self.__pe.resources_manager.version.string_file_info.langcode_items[0].key) self.add_attribute('product-name', value=fileinfo.get('ProductName')) self.add_attribute('product-version', value=fileinfo.get('ProductVersion')) self.add_attribute('company-name', value=fileinfo.get('CompanyName')) self.add_attribute('legal-copyright', value=fileinfo.get('LegalCopyright')) - except lief.read_out_of_bound: - # The file is corrupted - pass + self.add_attribute('lang-id', value=version.string_file_info.langcode_items[0].key) # Sections self.sections = [] if self.__pe.sections: pos = 0 for section in self.__pe.sections: - s = PESectionObject(section, self._standalone, default_attributes_parameters=self._default_attributes_parameters) - self.add_reference(s.uuid, 'included-in', 'Section {} of PE'.format(pos)) - if ((self.__pe.entrypoint >= section.virtual_address) and - (self.__pe.entrypoint < (section.virtual_address + section.virtual_size))): - self.add_attribute('entrypoint-section-at-position', value='{}|{}'.format(section.name, pos)) + if not section.name and not section.size: + # Skip section if name is none AND size is 0. + continue + s = PESectionObject(section, standalone=self._standalone, default_attributes_parameters=self._default_attributes_parameters) + self.add_reference(s.uuid, 'includes', f'Section {pos} of PE') + if ((self.__pe.entrypoint >= section.virtual_address) + and (self.__pe.entrypoint < (section.virtual_address + section.virtual_size))): + if isinstance(section.name, bytes): + section_name = section.name.decode() + else: + section_name = section.name + self.add_attribute('entrypoint-section-at-position', value=f'{section_name}|{pos}') pos += 1 self.sections.append(s) self.add_attribute('number-sections', value=len(self.sections)) - # TODO: TLSSection / DIRECTORY_ENTRY_TLS + # Signatures + self.certificates = [] + self.signers = [] + for sign in self.__pe.signatures: + for c in sign.certificates: + cert_obj = PECertificate(c) + self.add_reference(cert_obj.uuid, 'signed-by') + self.certificates.append(cert_obj) + for s_info in sign.signers: + signer_obj = PESigners(s_info) + self.add_reference(signer_obj.uuid, 'signed-by') + self.signers.append(signer_obj) + + +class PECertificate(AbstractMISPObjectGenerator): + + def __init__(self, certificate: lief.PE.x509, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('x509') + self.__certificate = certificate + self.generate_attributes() + + def generate_attributes(self) -> None: + self.add_attribute('issuer', value=self.__certificate.issuer) + self.add_attribute('serial-number', value=self.__certificate.serial_number) + if len(self.__certificate.valid_from) == 6: + self.add_attribute('validity-not-before', + value=datetime(year=self.__certificate.valid_from[0], + month=self.__certificate.valid_from[1], + day=self.__certificate.valid_from[2], + hour=self.__certificate.valid_from[3], + minute=self.__certificate.valid_from[4], + second=self.__certificate.valid_from[5])) + if len(self.__certificate.valid_to) == 6: + self.add_attribute('validity-not-after', + value=datetime(year=self.__certificate.valid_to[0], + month=self.__certificate.valid_to[1], + day=self.__certificate.valid_to[2], + hour=self.__certificate.valid_to[3], + minute=self.__certificate.valid_to[4], + second=self.__certificate.valid_to[5])) + self.add_attribute('version', value=self.__certificate.version) + self.add_attribute('subject', value=self.__certificate.subject) + self.add_attribute('signature_algorithm', value=self.__certificate.signature_algorithm) + self.add_attribute('raw-base64', value=b64encode(self.__certificate.raw)) + + +class PESigners(AbstractMISPObjectGenerator): + + def __init__(self, signer: lief.PE.SignerInfo, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('authenticode-signerinfo') + self.__signer = signer + self.generate_attributes() + + def generate_attributes(self) -> None: + self.add_attribute('issuer', value=self.__signer.issuer) + self.add_attribute('serial-number', value=self.__signer.serial_number) + self.add_attribute('version', value=self.__signer.version) + self.add_attribute('digest_algorithm', value=str(self.__signer.digest_algorithm)) + self.add_attribute('encryption_algorithm', value=str(self.__signer.encryption_algorithm)) + self.add_attribute('digest-base64', value=b64encode(self.__signer.encrypted_digest)) + info: lief.PE.SpcSpOpusInfo = self.__signer.get_attribute(lief.PE.Attribute.TYPE.SPC_SP_OPUS_INFO) # type: ignore[assignment] + if info: + self.add_attribute('program-name', value=info.program_name) + self.add_attribute('url', value=info.more_info) class PESectionObject(AbstractMISPObjectGenerator): - def __init__(self, section, standalone=True, **kwargs): - # Python3 way - # super().__init__('pe-section') - super(PESectionObject, self).__init__('pe-section', standalone=standalone, **kwargs) + def __init__(self, section: lief.PE.Section, **kwargs) -> None: # type: ignore[no-untyped-def] + """Creates an PE Section object. Object generated by PEObject.""" + super().__init__('pe-section') self.__section = section self.__data = bytes(self.__section.content) self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: self.add_attribute('name', value=self.__section.name) - size = self.add_attribute('size-in-bytes', value=self.__section.size) - if int(size.value) > 0: + self.add_attribute('size-in-bytes', value=self.__section.size) + if int(self.__section.size) > 0: + # zero-filled sections can create too many correlations + to_ids = float(self.__section.entropy) > 0 + disable_correlation = not to_ids self.add_attribute('entropy', value=self.__section.entropy) - self.add_attribute('md5', value=md5(self.__data).hexdigest()) - self.add_attribute('sha1', value=sha1(self.__data).hexdigest()) - self.add_attribute('sha256', value=sha256(self.__data).hexdigest()) - self.add_attribute('sha512', value=sha512(self.__data).hexdigest()) - if HAS_PYDEEP: - self.add_attribute('ssdeep', value=pydeep.hash_buf(self.__data).decode()) + self.add_attribute('md5', value=md5(self.__data).hexdigest(), disable_correlation=disable_correlation, to_ids=to_ids) + self.add_attribute('sha1', value=sha1(self.__data).hexdigest(), disable_correlation=disable_correlation, to_ids=to_ids) + self.add_attribute('sha256', value=sha256(self.__data).hexdigest(), disable_correlation=disable_correlation, to_ids=to_ids) + self.add_attribute('sha512', value=sha512(self.__data).hexdigest(), disable_correlation=disable_correlation, to_ids=to_ids) + if HAS_PYDEEP and float(self.__section.entropy) > 0: + if self.__section.name == '.rsrc': + # ssdeep of .rsrc creates too many correlations + disable_correlation = True + to_ids = False + self.add_attribute('ssdeep', value=pydeep.hash_buf(self.__data).decode(), disable_correlation=disable_correlation, to_ids=to_ids) diff --git a/pymisp/tools/reportlab_generator.py b/pymisp/tools/reportlab_generator.py index 98127e5..96efc7b 100644 --- a/pymisp/tools/reportlab_generator.py +++ b/pymisp/tools/reportlab_generator.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations # Standard imports import base64 @@ -11,6 +12,8 @@ from pathlib import Path import sys import os +import requests + if sys.version_info.major >= 3: from html import escape else: @@ -20,17 +23,17 @@ logger = logging.getLogger('pymisp') # Potentially not installed imports try: - from reportlab.pdfgen import canvas - from reportlab.pdfbase.pdfmetrics import stringWidth, registerFont - from reportlab.pdfbase.ttfonts import TTFont - from reportlab.lib import colors - from reportlab.lib.pagesizes import A4 + from reportlab.pdfgen import canvas # type: ignore + from reportlab.pdfbase.pdfmetrics import stringWidth, registerFont # type: ignore + from reportlab.pdfbase.ttfonts import TTFont # type: ignore + from reportlab.lib import colors # type: ignore + from reportlab.lib.pagesizes import A4 # type: ignore - from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Table, TableStyle, Flowable, Image, Indenter + from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Table, TableStyle, Flowable, Image, Indenter # type: ignore - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.lib.units import mm - from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle # type: ignore + from reportlab.lib.units import mm # type: ignore + from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT # type: ignore HAS_REPORTLAB = True except ImportError: @@ -49,7 +52,7 @@ def create_flowable_tag(misp_tag): return [Flowable_Tag(text=misp_tag.name, color=misp_tag.colour, custom_style=col1_style)] -class Flowable_Tag(Flowable): +class Flowable_Tag(Flowable): # type: ignore[misc] """ Custom flowable to handle tags. Draw one Tag with the webview formatting Modified from : http://two.pairlist.net/pipermail/reportlab-users/2005-February/003695.html @@ -108,7 +111,7 @@ class Flowable_Tag(Flowable): LEFT_INTERNAL_PADDING = 2 ELONGATION = LEFT_INTERNAL_PADDING * 2 - p = Paragraph("{}".format(self.choose_good_text_color(), self.text), style=self.custom_style) + p = Paragraph(f"{self.text}", style=self.custom_style) string_width = stringWidth(self.text, self.custom_style.fontName, self.custom_style.fontSize) self.width = string_width + ELONGATION @@ -409,10 +412,20 @@ def internationalize_font(config=None): NotoSansCJKtc - Medium.ttf ''' font_path = Path(sys.modules['pymisp'].__file__).parent / 'tools' / 'pdf_fonts' / 'Noto_TTF' - noto_bold = font_path / "NotoSansCJKtc-Bold.ttf" noto = font_path / "NotoSansCJKtc-DemiLight.ttf" + if not font_path.is_dir() or not noto_bold.is_file() or not noto.is_file(): + font_path.mkdir(parents=True, exist_ok=True) + if not noto_bold.is_file(): + bf = requests.get('https://github.com/MISP/pdf_fonts/raw/refs/heads/master/Noto_TTF/NotoSansCJKtc-Bold.ttf') + with open(noto_bold, 'wb') as f: + f.write(bf.content) + if not noto.is_file(): + rf = requests.get('https://github.com/MISP/pdf_fonts/raw/refs/heads/master/Noto_TTF/NotoSansCJKtc-DemiLight.ttf') + with open(noto, 'wb') as f: + f.write(rf.content) + if noto_bold.is_file() and noto.is_file(): registerFont(TTFont("Noto", noto)) registerFont(TTFont("Noto-bold", noto_bold)) @@ -420,7 +433,7 @@ def internationalize_font(config=None): FIRST_COL_FONT = 'Noto-bold' SECOND_COL_FONT = 'Noto' else: - logger.error(f"Trying to load a custom (internationalization) font, unable to access the file: {noto_bold}") + logger.error(f"Trying to load a custom (internationalization) font, unable to access the file: {noto_bold} / {noto}") def get_table_styles(): @@ -481,14 +494,17 @@ def get_clusters_table_styles(): def safe_string(bad_str): return escape(str(bad_str)) + def is_safe_value(value): return (value is not None and value != "") + def is_safe_table(value): return (value is not None and value != []) + def is_safe_attribute(curr_object, attribute_name): return (hasattr(curr_object, attribute_name) and getattr(curr_object, attribute_name) is not None @@ -612,7 +628,7 @@ class Value_Formatter(): curr_uuid = str(is_safe_value(uuid)) curr_baseurl = self.config[moduleconfig[0]] curr_url = uuid_to_url(curr_baseurl, curr_uuid) - html_url = "{}".format(curr_url, safe_string(text)) + html_url = f"{safe_string(text)}" if color: # They want fancy colors @@ -660,7 +676,7 @@ class Value_Formatter(): return self.get_unoverflowable_paragraph(answer) - def get_threat_value(self, threat_level = None): + def get_threat_value(self, threat_level=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event threat :param threat_level: MISP_EVENT threat level (int) to be formatted @@ -671,9 +687,9 @@ class Value_Formatter(): if is_safe_value(threat_level) and str(threat_level) in threat_map: answer = threat_map[safe_string(threat_level)] - return self.get_unoverflowable_paragraph(answer,do_escape_string=False) + return self.get_unoverflowable_paragraph(answer, do_escape_string=False) - def get_analysis_value(self, analysis_level = None): + def get_analysis_value(self, analysis_level=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event analysis :param analysis_level: MISP_EVENT analysis level (int) to be formatted @@ -684,7 +700,7 @@ class Value_Formatter(): if is_safe_value(analysis_level) and str(analysis_level) in analysis_map: answer = analysis_map[safe_string(analysis_level)] - return self.get_unoverflowable_paragraph(answer,do_escape_string=False) + return self.get_unoverflowable_paragraph(answer, do_escape_string=False) def get_timestamp_value(self, timestamp=None): ''' @@ -741,7 +757,7 @@ class Value_Formatter(): answer = YES_ANSWER if is_safe_value(published_timestamp): # Published and have published date - answer += '({})'.format(published_timestamp.strftime(EXPORT_DATE_FORMAT)) + answer += f'({published_timestamp.strftime(EXPORT_DATE_FORMAT)})' else: # Published without published date answer += "(no date)" @@ -764,7 +780,7 @@ class Value_Formatter(): try: # Get the image - buf = image_buffer # TODO : Do verification on the buffer ? + buf = image_buffer # TODO : Do verification on the buffer ? # Create image within a bounded box (to allow pdf creation) img = Image(buf, width=FRAME_PICTURE_MAX_WIDTH, height=FRAME_PICTURE_MAX_HEIGHT, kind='bound') @@ -821,8 +837,8 @@ class Value_Formatter(): if is_safe_dict_attribute(misp_galaxy, 'name'): answer = '{} from {}:{}'.format(safe_string(misp_galaxy['name']), - safe_string(misp_galaxy["namespace"]), - safe_string(misp_galaxy["type"])) + safe_string(misp_galaxy["namespace"]), + safe_string(misp_galaxy["type"])) return self.get_unoverflowable_paragraph(answer, do_small=True) @@ -866,7 +882,7 @@ class Event_Metadata(): ######################################################################## # General Event's Attributes formater - def create_flowable_table_from_event(self, misp_event ): + def create_flowable_table_from_event(self, misp_event): ''' Returns Table presenting a MISP event :param misp_event: A misp event (complete or not) @@ -879,8 +895,8 @@ class Event_Metadata(): # Manual addition # UUID data.append([self.value_formatter.get_col1_paragraph("UUID"), - self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid',None), - text=misp_event.get('uuid',None))]) + self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), + text=misp_event.get('uuid', None))]) # Date data.append({self.value_formatter.get_col1_paragraph("Date"), @@ -888,48 +904,48 @@ class Event_Metadata(): # Owner data.append([self.value_formatter.get_col1_paragraph("Owner org"), - self.value_formatter.get_owner_value(owner=misp_event.get('owner',None))]) + self.value_formatter.get_owner_value(owner=misp_event.get('owner', None))]) # Threat data.append([self.value_formatter.get_col1_paragraph("Threat level"), - self.value_formatter.get_threat_value(threat_level=misp_event.get('threat_level_id',None))]) + self.value_formatter.get_threat_value(threat_level=misp_event.get('threat_level_id', None))]) # Analysis data.append([self.value_formatter.get_col1_paragraph("Analysis"), - self.value_formatter.get_analysis_value(analysis_level=misp_event.get('analysis',None))]) + self.value_formatter.get_analysis_value(analysis_level=misp_event.get('analysis', None))]) # Info data.append([self.value_formatter.get_col1_paragraph("Info"), - self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid',None), - text=misp_event.get('info',None))]) + self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), + text=misp_event.get('info', None))]) # Timestamp data.append([self.value_formatter.get_col1_paragraph("Event date"), - self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp',None))]) + self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp', None))]) # Published data.append([self.value_formatter.get_col1_paragraph("Published"), - self.value_formatter.get_published_value(published_bool=misp_event.get('published',None), - published_timestamp=misp_event.get('publish_timestamp',None))]) + self.value_formatter.get_published_value(published_bool=misp_event.get('published', None), + published_timestamp=misp_event.get('publish_timestamp', None))]) # Creator organisation data.append([self.value_formatter.get_col1_paragraph("Creator Org"), - self.value_formatter.get_creator_organisation_value(creator=misp_event.get('Orgc',None))]) + self.value_formatter.get_creator_organisation_value(creator=misp_event.get('Orgc', None))]) # Number of Attributes data.append([self.value_formatter.get_col1_paragraph("# Attributes"), - self.value_formatter.get_attributes_number_value(attributes=misp_event.get('Attribute',None))]) + self.value_formatter.get_attributes_number_value(attributes=misp_event.get('Attribute', None))]) # Tags curr_Tags = Tags(self.config, self.value_formatter) data.append([self.value_formatter.get_col1_paragraph("Tags"), - curr_Tags.get_tag_value(tags=misp_event.get('Tag',None))]) + curr_Tags.get_tag_value(tags=misp_event.get('Tag', None))]) flowable_table.append(create_flowable_table_from_data(data)) # Correlation - if is_safe_table(misp_event.get('RelatedEvent',None)) and is_in_config(self.config, 4): - flowable_table += self.get_correlation_values(related_events=misp_event.get('RelatedEvent',None)) + if is_safe_table(misp_event.get('RelatedEvent', None)) and is_in_config(self.config, 4): + flowable_table += self.get_correlation_values(related_events=misp_event.get('RelatedEvent', None)) # Galaxies if is_safe_attribute_table(misp_event, "Related Galaxies") and is_in_config(self.config, 3): @@ -952,17 +968,17 @@ class Event_Metadata(): # Manual addition # UUID data.append([self.value_formatter.get_col1_paragraph("UUID"), - self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid',None), - text=misp_event.get('uuid',None))]) + self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), + text=misp_event.get('uuid', None))]) # Info data.append([self.value_formatter.get_col1_paragraph("Info"), - self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid',None), - text=misp_event.get('info',None))]) + self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), + text=misp_event.get('info', None))]) # Timestamp data.append([self.value_formatter.get_col1_paragraph("Event date"), - self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp',None))]) + self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp', None))]) flowable_table.append(create_flowable_table_from_data(data)) @@ -1088,7 +1104,7 @@ class Event_Metadata(): Paragraph("Related Event #" + str(i + OFFSET), self.sample_style_sheet['Heading4'])) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) - flowable_table += self.create_reduced_flowable_table_from_event(evt) + flowable_table += self.create_reduced_flowable_table_from_event(evt['Event']) i += 1 else: return flowable_table.append(self.value_formatter.get_unoverflowable_paragraph(DEFAULT_VALUE)) @@ -1167,10 +1183,10 @@ class Attributes(): # data.append([Paragraph(item[0], col1_style), Paragraph(item[2], col2_style)]) # Handle Special case for links (Value) - There were not written in the previous loop - if not STANDARD_TYPE and is_safe_value(misp_attribute.get('value',None)): + if not STANDARD_TYPE and is_safe_value(misp_attribute.get('value', None)): data.append([self.value_formatter.get_col1_paragraph("Value"), - self.value_formatter.get_good_or_bad_link(value=misp_attribute.get('value',None), - type=misp_attribute.get('type',None))]) + self.value_formatter.get_good_or_bad_link(value=misp_attribute.get('value', None), + type=misp_attribute.get('type', None))]) # Handle pictures if is_safe_value(misp_attribute.get('data', None)) and misp_attribute.type == IMAGE_TYPE: @@ -1190,7 +1206,7 @@ class Attributes(): if is_safe_table(misp_attribute.get('Sighting', None)): data.append([self.value_formatter.get_col1_paragraph("Sighting"), - curr_Sighting.create_flowable_paragraph_from_sightings(sightings=misp_attribute.get('Sighting',None))]) + curr_Sighting.create_flowable_paragraph_from_sightings(sightings=misp_attribute.get('Sighting', None))]) flowable_table.append(create_flowable_table_from_data(data)) @@ -1399,7 +1415,7 @@ class Object(): data = [create_flowable_table_from_data(data)] # Handle all the attributes - if is_safe_value(misp_object.get("Attribute",None)): + if is_safe_value(misp_object.get("Attribute", None)): curr_attributes = Attributes(self.config, self.value_formatter) data.append(Indenter(left=INDENT_SIZE)) data += curr_attributes.create_flowable_table_from_attributes(misp_object) @@ -1674,8 +1690,8 @@ def collect_parts(misp_event, config=None): # Create stuff title_style = ParagraphStyle(name='Column_1', parent=sample_style_sheet['Heading1'], fontName=FIRST_COL_FONT, alignment=TA_CENTER) - title = curr_val_f.get_value_link_to_event(uuid=misp_event.get('uuid',None), - text=misp_event.get('info',None), + title = curr_val_f.get_value_link_to_event(uuid=misp_event.get('uuid', None), + text=misp_event.get('info', None), curr_style=title_style, color=False) # Add all parts to final PDF flowables.append(title) @@ -1708,7 +1724,7 @@ def collect_parts(misp_event, config=None): flowables.append(PageBreak()) event_objects_title = Paragraph("Objects", sample_style_sheet['Heading2']) - table_objects = curr_object.create_flowable_table_from_objects(objects=misp_event.get("Object",None)) + table_objects = curr_object.create_flowable_table_from_objects(objects=misp_event.get("Object", None)) flowables.append(event_objects_title) flowables += table_objects diff --git a/pymisp/tools/sbsignatureobject.py b/pymisp/tools/sbsignatureobject.py index 8b7f3c1..a7356a3 100644 --- a/pymisp/tools/sbsignatureobject.py +++ b/pymisp/tools/sbsignatureobject.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations from .abstractgenerator import AbstractMISPObjectGenerator @@ -8,13 +9,13 @@ class SBSignatureObject(AbstractMISPObjectGenerator): ''' Sandbox Analyzer ''' - def __init__(self, software, report, standalone=True, **kwargs): - super(SBSignatureObject, self).__init__("sb-signature", **kwargs) + def __init__(self, software: str, report: list[tuple[str, str]], **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('sb-signature', **kwargs) self._software = software self._report = report self.generate_attributes() - def generate_attributes(self): + def generate_attributes(self) -> None: ''' Parse the report for relevant attributes ''' self.add_attribute("software", value=self._software) for (signature_name, description) in self._report: diff --git a/pymisp/tools/sshauthkeyobject.py b/pymisp/tools/sshauthkeyobject.py new file mode 100644 index 0000000..ce6c2fb --- /dev/null +++ b/pymisp/tools/sshauthkeyobject.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import logging + +from io import StringIO +from pathlib import Path + +from ..exceptions import InvalidMISPObject +from .abstractgenerator import AbstractMISPObjectGenerator + +logger = logging.getLogger('pymisp') + + +class SSHAuthorizedKeysObject(AbstractMISPObjectGenerator): + + def __init__(self, authorized_keys_path: Path | str | None = None, # type: ignore[no-untyped-def] + authorized_keys_pseudofile: StringIO | None = None, **kwargs): + super().__init__('ssh-authorized-keys', **kwargs) + if authorized_keys_path: + with open(authorized_keys_path) as f: + self.__pseudofile = StringIO(f.read()) + elif authorized_keys_pseudofile and isinstance(authorized_keys_pseudofile, StringIO): + self.__pseudofile = authorized_keys_pseudofile + else: + raise InvalidMISPObject('File buffer (StringIO) or a path is required.') + self.__data = self.__pseudofile.getvalue() + self.generate_attributes() + + def generate_attributes(self) -> None: + for line in self.__pseudofile: + if line.startswith('ssh') or line.startswith('ecdsa'): + key = line.split(' ')[1] + self.add_attribute('key', key) diff --git a/pymisp/tools/stix.py b/pymisp/tools/stix.py deleted file mode 100644 index fe8430a..0000000 --- a/pymisp/tools/stix.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- - -try: - from misp_stix_converter.converters.buildMISPAttribute import buildEvent - from misp_stix_converter.converters import convert - from misp_stix_converter.converters.convert import MISPtoSTIX - has_misp_stix_converter = True -except ImportError: - has_misp_stix_converter = False - - -def load_stix(stix, distribution=3, threat_level_id=2, analysis=0): - '''Returns a MISPEvent object from a STIX package''' - if not has_misp_stix_converter: - raise Exception('You need to install misp_stix_converter: pip install git+https://github.com/MISP/MISP-STIX-Converter.git') - stix = convert.load_stix(stix) - return buildEvent(stix, distribution=distribution, - threat_level_id=threat_level_id, analysis=analysis) - - -def make_stix_package(misp_event, to_json=False, to_xml=False): - '''Returns a STIXPackage from a MISPEvent. - - Optionally can return the package in json or xml. - - ''' - if not has_misp_stix_converter: - raise Exception('You need to install misp_stix_converter: pip install git+https://github.com/MISP/MISP-STIX-Converter.git') - package = MISPtoSTIX(misp_event) - if to_json: - return package.to_json() - elif to_xml: - return package.to_xml() - else: - return package diff --git a/pymisp/tools/update_objects.py b/pymisp/tools/update_objects.py new file mode 100644 index 0000000..7f1dd25 --- /dev/null +++ b/pymisp/tools/update_objects.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import zipfile +from io import BytesIO +from pathlib import Path + +import requests + +from ..abstract import resources_path + +static_repo = "https://github.com/MISP/misp-objects/archive/main.zip" + + +def update_objects() -> None: + r = requests.get(static_repo) + + zipped_repo = BytesIO(r.content) + + with zipfile.ZipFile(zipped_repo, 'r') as myzip: + for name in myzip.namelist(): + if not name.endswith('.json'): + continue + name_on_disk = name.replace('misp-objects-main', 'misp-objects') + path = resources_path / Path(name_on_disk) + if not path.parent.exists(): + path.parent.mkdir(parents=True) + with path.open('wb') as f: + f.write(myzip.read(name)) diff --git a/pymisp/tools/urlobject.py b/pymisp/tools/urlobject.py new file mode 100644 index 0000000..3465541 --- /dev/null +++ b/pymisp/tools/urlobject.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import logging + +from urllib.parse import unquote_plus + +from .abstractgenerator import AbstractMISPObjectGenerator + +try: + from pyfaup.faup import Faup # type: ignore +except (OSError, ImportError): + from ._psl_faup import PSLFaup as Faup + +logger = logging.getLogger('pymisp') + +faup = Faup() + + +class URLObject(AbstractMISPObjectGenerator): + + def __init__(self, url: str, generate_all=False, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('url', **kwargs) + self._generate_all = True if generate_all is True else False + faup.decode(unquote_plus(url)) + self.generate_attributes() + + def generate_attributes(self) -> None: + self.add_attribute('url', value=faup.url.decode()) + if faup.get_host(): + self.add_attribute('host', value=faup.get_host()) + if faup.get_domain(): + self.add_attribute('domain', value=faup.get_domain()) + if self._generate_all: + if hasattr(faup, 'ip_as_host') and faup.ip_as_host: + self.attributes = [attr for attr in self.attributes + if attr.object_relation not in ('host', 'domain')] + self.add_attribute('ip', value=faup.ip_as_host) + if faup.get_credential(): + self.add_attribute('credential', value=faup.get_credential()) + if faup.get_fragment(): + self.add_attribute('fragment', value=faup.get_fragment()) + if faup.get_port(): + self.add_attribute('port', value=faup.get_port()) + if faup.get_query_string(): + self.add_attribute('query_string', value=faup.get_query_string()) + if faup.get_resource_path(): + self.add_attribute('resource_path', value=faup.get_resource_path()) + if faup.get_scheme(): + self.add_attribute('scheme', value=faup.get_scheme()) + if faup.get_tld(): + self.add_attribute('tld', value=faup.get_tld()) + if faup.get_domain_without_tld(): + self.add_attribute('domain_without_tld', value=faup.get_domain_without_tld()) + if faup.get_subdomain(): + self.add_attribute('subdomain', value=faup.get_subdomain()) + if hasattr(faup, 'get_unicode_host') and faup.get_unicode_host() != faup.get_host(): + self.add_attribute('text', value=faup.get_unicode_host()) diff --git a/pymisp/tools/vehicleobject.py b/pymisp/tools/vehicleobject.py index ea347bc..75c9a4a 100644 --- a/pymisp/tools/vehicleobject.py +++ b/pymisp/tools/vehicleobject.py @@ -1,10 +1,11 @@ #!/usr/bin/python3 -import sys -import getopt +from __future__ import annotations + import requests import json -from pymisp import MISPObject + +from typing import Any from .abstractgenerator import AbstractMISPObjectGenerator @@ -14,14 +15,16 @@ from .abstractgenerator import AbstractMISPObjectGenerator class VehicleObject(AbstractMISPObjectGenerator): '''Vehicle object generator out of regcheck.org.uk''' - country_urls = { + country_urls: dict[str, str] = { 'fr': "http://www.regcheck.org.uk/api/reg.asmx/CheckFrance", 'es': "http://www.regcheck.org.uk/api/reg.asmx/CheckSpain", 'uk': "http://www.regcheck.org.uk/api/reg.asmx/Check" } - def __init__(self, country: str, registration: str, username: str, standalone=True, **kwargs): - super(VehicleObject, self).__init__("vehicle", standalone=standalone, **kwargs) + def __init__(self, country: str, registration: str, username: str, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('vehicle', **kwargs) + if country not in self.country_urls: + raise ValueError(f"Country {country} not supportet, must be one of {self.country_urls.keys()}") self._country = country self._registration = registration self._username = username @@ -29,10 +32,10 @@ class VehicleObject(AbstractMISPObjectGenerator): self.generate_attributes() @property - def report(self): + def report(self) -> dict[str, Any]: return self._report - def generate_attributes(self): + def generate_attributes(self) -> None: carDescription = self._report["Description"] carMake = self._report["CarMake"]["CurrentTextValue"] carModel = self._report["CarModel"]["CurrentTextValue"] @@ -68,14 +71,14 @@ class VehicleObject(AbstractMISPObjectGenerator): self.add_attribute('date-first-registration', type='text', value=firstRegistration) self.add_attribute('image-url', type='text', value=ImageUrl) - def _query(self): - payload = "RegistrationNumber={}&username={}".format(self._registration, self._username) + def _query(self) -> dict[str, Any]: + payload = f"RegistrationNumber={self._registration}&username={self._username}" headers = { 'Content-Type': "application/x-www-form-urlencoded", 'cache-control': "no-cache", } - response = requests.request("POST", self.country_urls.get(self._country), data=payload, headers=headers) + response = requests.request("POST", self.country_urls[self._country], data=payload, headers=headers) # FIXME: Clean that up. for item in response.text.split(""): if "" in item: diff --git a/pymisp/tools/vtreportobject.py b/pymisp/tools/vtreportobject.py index 69e856e..5374f5d 100644 --- a/pymisp/tools/vtreportobject.py +++ b/pymisp/tools/vtreportobject.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- + +from __future__ import annotations import re +from typing import Any import requests try: @@ -23,10 +25,8 @@ class VTReportObject(AbstractMISPObjectGenerator): :indicator: IOC to search VirusTotal for ''' - def __init__(self, apikey, indicator, vt_proxies=None, standalone=True, **kwargs): - # PY3 way: - # super().__init__("virustotal-report") - super(VTReportObject, self).__init__("virustotal-report", standalone=standalone, **kwargs) + def __init__(self, apikey: str, indicator: str, vt_proxies: dict[str, str] | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__('virustotal-report', **kwargs) indicator = indicator.strip() self._resource_type = self.__validate_resource(indicator) if self._resource_type: @@ -34,20 +34,20 @@ class VTReportObject(AbstractMISPObjectGenerator): self._report = self.__query_virustotal(apikey, indicator) self.generate_attributes() else: - error_msg = "A valid indicator is required. (One of type url, md5, sha1, sha256). Received '{}' instead".format(indicator) + error_msg = f"A valid indicator is required. (One of type url, md5, sha1, sha256). Received '{indicator}' instead" raise InvalidMISPObject(error_msg) - def get_report(self): + def get_report(self) -> dict[str, Any]: return self._report - def generate_attributes(self): + def generate_attributes(self) -> None: ''' Parse the VirusTotal report for relevant attributes ''' self.add_attribute("last-submission", value=self._report["scan_date"]) self.add_attribute("permalink", value=self._report["permalink"]) ratio = "{}/{}".format(self._report["positives"], self._report["total"]) self.add_attribute("detection-ratio", value=ratio) - def __validate_resource(self, ioc): + def __validate_resource(self, ioc: str) -> str | bool: ''' Validate the data type of an indicator. Domains and IP addresses aren't supported because @@ -63,7 +63,7 @@ class VTReportObject(AbstractMISPObjectGenerator): return "file" return False - def __query_virustotal(self, apikey, resource): + def __query_virustotal(self, apikey: str, resource: str) -> dict[str, Any]: ''' Query VirusTotal for information about an indicator @@ -71,16 +71,16 @@ class VTReportObject(AbstractMISPObjectGenerator): :resource: Indicator to search in VirusTotal ''' - url = "https://www.virustotal.com/vtapi/v2/{}/report".format(self._resource_type) + url = f"https://www.virustotal.com/vtapi/v2/{self._resource_type}/report" params = {"apikey": apikey, "resource": resource} # for now assume we're using a public API key - we'll figure out private keys later if self._proxies: report = requests.get(url, params=params, proxies=self._proxies) else: report = requests.get(url, params=params) - report = report.json() - if report["response_code"] == 1: - return report + report_json = report.json() + if report_json["response_code"] == 1: + return report_json else: - error_msg = "{}: {}".format(resource, report["verbose_msg"]) + error_msg = "{}: {}".format(resource, report_json["verbose_msg"]) raise InvalidMISPObject(error_msg) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c78be8c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,96 @@ +[tool.poetry] +name = "pymisp" +version = "2.5.2" +description = "Python API for MISP." +authors = ["Raphaël Vinot "] +license = "BSD-2-Clause" +repository = "https://github.com/MISP/PyMISP" +documentation = "https://pymisp.readthedocs.io" + +readme = "README.md" + +classifiers=[ + 'License :: OSI Approved :: BSD License', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Operating System :: POSIX :: Linux', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Telecommunications Industry', + 'Intended Audience :: Information Technology', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Topic :: Security', + 'Topic :: Internet' +] + +include = [ + {path = "CHANGELOG.txt", format = "sdist"}, + "pymisp/data/*.json", + "pymisp/data/misp-objects/schema_objects.json", + "pymisp/data/misp-objects/schema_relationships.json", + "pymisp/data/misp-objects/objects/*/definition.json", + "pymisp/data/misp-objects/relationships/definition.json", + "pymisp/tools/pdf_fonts/Noto_TTF/*", + "docs", + "examples", + "tests", +] + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/MISP/PyMISP/issues" +"Source" = "https://github.com/MISP/PyMISP" + +[tool.poetry.dependencies] +python = "^3.9" +requests = "^2.32.3" +python-dateutil = "^2.9.0.post0" +deprecated = "^1.2.15" +extract_msg = {version = "^0.52", optional = true} +RTFDE = {version = "^0.1.1", optional = true} +oletools = {version = "^0.60.1", optional = true} +python-magic = {version = "^0.4.27", optional = true} +pydeep2 = {version = "^0.5.1", optional = true} +lief = {version = "^0.15.0", optional = true} +beautifulsoup4 = {version = "^4.12.3", optional = true} +validators = {version = "^0.34.0", optional = true} +sphinx-autodoc-typehints = {version = "^2.5.0", optional = true, python = ">=3.10"} +docutils = {version = "^0.21.1", optional = true, python = ">=3.10"} +recommonmark = {version = "^0.7.1", optional = true, python = ">=3.10"} +reportlab = {version = "^4.2.5", optional = true} +pyfaup = {version = "^1.2", optional = true} +publicsuffixlist = {version = "^1.0.2.20241113", optional = true} +urllib3 = {extras = ["brotli"], version = "*", optional = true} +Sphinx = [ + {version = "^8", python = ">=3.10", optional = true} +] + +[tool.poetry.extras] +fileobjects = ['python-magic', 'pydeep2', 'lief'] +openioc = ['beautifulsoup4'] +virustotal = ['validators'] +docs = ['sphinx-autodoc-typehints', 'recommonmark', 'sphinx', 'docutils'] +pdfexport = ['reportlab'] +url = ['pyfaup'] +email = ['extract_msg', "RTFDE", "oletools"] +brotli = ['urllib3'] + +[tool.poetry.group.dev.dependencies] +requests-mock = "^1.12.1" +mypy = "^1.13.0" +ipython = [ + {version = "^8.18.0", python = "<3.10"}, + {version = "^8.19.0", python = ">=3.10"} +] +jupyterlab = "^4.3.1" +types-requests = "^2.32.0.20241016" +types-python-dateutil = "^2.9.0.20241003" +types-redis = "^4.6.0.20241004" +types-Flask = "^1.1.6" +pytest-cov = "^6.0.0" + +[build-system] +requires = ["poetry_core>=1.1", "setuptools"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index ab6e4aa..0000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from os import path - -from setuptools import setup - -import pymisp - -this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), 'r') as f: - long_description = f.read() - -setup( - name='pymisp', - version=pymisp.__version__, - author='Raphaël Vinot', - author_email='raphael.vinot@circl.lu', - maintainer='Raphaël Vinot', - url='https://github.com/MISP/PyMISP', - project_urls={ - 'Documentation': 'http://pymisp.readthedocs.io', - 'Source': 'https://github.com/MISP/PyMISP', - 'Tracker': 'https://github.com/MISP/PyMISP/issues', - }, - description='Python API for MISP.', - long_description=long_description, - long_description_content_type='text/markdown', - packages=['pymisp', 'pymisp.tools'], - classifiers=[ - 'License :: OSI Approved :: BSD License', - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Operating System :: POSIX :: Linux', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Telecommunications Industry', - 'Intended Audience :: Information Technology', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Security', - 'Topic :: Internet', - ], - install_requires=['six', 'requests', 'python-dateutil', 'jsonschema', - 'python-dateutil', 'enum34;python_version<"3.4"', - 'functools32;python_version<"3.0"'], - extras_require={'fileobjects': ['lief>=0.8', 'python-magic'], - 'neo': ['py2neo'], - 'openioc': ['beautifulsoup4'], - 'virustotal': ['validators'], - 'docs': ['sphinx-autodoc-typehints'], - 'pdfexport': ['reportlab']}, - tests_require=[ - 'jsonschema', - 'python-magic', - 'requests-mock' - ], - test_suite="tests.test_offline", - include_package_data=True, - package_data={'pymisp': ['data/*.json', - 'data/misp-objects/schema_objects.json', - 'data/misp-objects/schema_relationships.json', - 'data/misp-objects/objects/*/definition.json', - 'data/misp-objects/relationships/definition.json', - 'tools/pdf_fonts/Noto_TTF/*']}, -) diff --git a/tests/csv_testfiles/invalid_fieldnames.csv b/tests/csv_testfiles/invalid_fieldnames.csv new file mode 100644 index 0000000..c9cfdc0 --- /dev/null +++ b/tests/csv_testfiles/invalid_fieldnames.csv @@ -0,0 +1,11 @@ +SHA1,fileName,size +2a030cc6d84d5785f5e84d0f5888a411d4b06d01,soft.exe,45568 +2abae839362edfe52d9ebe282fb61113d22b331f,sttager.exe,20480 +6995a32e0a4d4f6d0c9b2a00a96d69bff4b83ea7,test443.exe,373911 +87b1f17fbb4a1e8eef4cb31c1c0194b1426c868c,veil.exe,345761 +afc36916a4df934446681ea28bef6add4decb98a,80_http.exe.exe,411850 +f832d94391a8d2d5cf92773e6c912905ec7c40c7,test1.exe,406636 +056823c7891a04b2fec8903eb401ae3291743a54,beca.exe.exe,23808 +b7afa7acf1b7ded2c4e3d0884b5cdaa230d9f82e,shell1.exe,24576 +4b50b6b9157026ab408d966ece02d1cef8045f82,starggge.exe,27136 +6042dfd50d33da40e383baec4a7ef7c75bf17481,8_32.exe,24064 diff --git a/tests/csv_testfiles/valid_fieldnames.csv b/tests/csv_testfiles/valid_fieldnames.csv new file mode 100644 index 0000000..45ac6bf --- /dev/null +++ b/tests/csv_testfiles/valid_fieldnames.csv @@ -0,0 +1,4 @@ +md5, sha1, sha256 +644087ccca16d2a728ef7685a4106f09, eabd6974ac71efd72d9e0688d5a6131f336d169c, 385e31c97e3a07bbb81513f0cd0979e64e6b014943902efd002f57b21eadd41e +34187a34d0a3c5d63016c26346371b54, ce8209ff9828aa8cb095bd7d1589fc4d394c298c, 5f815b8a8e77731c9ca2b3a07a27f880ef24d54e458d77bdabbbaf2269fe96c3 +871aa15f4d61c85e1284e1be3f99f705, 236eac0b19f91117b27f1b198a4d8490d99ec2e5, b434bccf0a5ff75b27184e661df751466aef69f35fbd7b8b8692302b8b886262 diff --git a/tests/email_testfiles/mail_1.eml.zip b/tests/email_testfiles/mail_1.eml.zip new file mode 100644 index 0000000..b01dab8 Binary files /dev/null and b/tests/email_testfiles/mail_1.eml.zip differ diff --git a/tests/email_testfiles/mail_1.msg b/tests/email_testfiles/mail_1.msg new file mode 100644 index 0000000..23f03a4 Binary files /dev/null and b/tests/email_testfiles/mail_1.msg differ diff --git a/tests/email_testfiles/mail_1_bom.eml b/tests/email_testfiles/mail_1_bom.eml new file mode 100644 index 0000000..563a06c --- /dev/null +++ b/tests/email_testfiles/mail_1_bom.eml @@ -0,0 +1,858 @@ +Return-Path: +Delivered-To: kinney@noth.com +Received: (qmail 11769 invoked from network); 22 Aug 2016 14:23:01 -0000 +Received: from smtprelay0207.b.hostedemail.com (HELO smtprelay.b.hostedemail.com) (64.98.42.207) + by smtp.server.net with SMTP; 22 Aug 2016 14:23:01 -0000 +Received: from filter.hostedemail.com (10.5.19.248.rfc1918.com [10.5.19.248]) + by smtprelay06.b.hostedemail.com (Postfix) with ESMTP id 2CC378D014 + for ; Mon, 22 Aug 2016 14:22:58 +0000 (UTC) +Received: from DM6PR06MB4475.namprd06.prod.outlook.com (2603:10b6:207:3d::31) + by BL0PR06MB4465.namprd06.prod.outlook.com with HTTPS id 12345 via + BL0PR02CA0054.NAMPRD02.PROD.OUTLOOK.COM; Mon, 1 Oct 2018 09:49:22 +0000 +Received: from DM3NAM03FT035.eop-NAM03.prod.protection.outlook.com + (2a01:111:f400:7e49::205) by CY4PR0601CA0051.outlook.office365.com + (2603:10b6:910:89::28) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.20.1185.23 via Frontend + Transport; Mon, 1 Oct 2018 09:49:21 +0000 +X-Session-Marker: 6A64617A657940616C6578616E646572736D6974682E636F6D +X-Spam-Summary: 69,4.5,0,,d41d8cd98f00b204,suvorov.s@nalg.ru,:,RULES_HIT:46:150:152:379:553:871:967:989:1000:1254:1260:1263:1313:1381:1516:1517:1520:1575:1594:1605:1676:1699:1730:1747:1764:1777:1792:1823:2044:2197:2199:2393:2525:2560:2563:2682:2685:2827:2859:2911:2933:2937:2939:2942:2945:2947:2951:2954:3022:3867:3872:3890:3934:3936:3938:3941:3944:3947:3950:3953:3956:3959:4425:5007:6001:6261:6506:6678:6747:6748:7281:7398:7688:8599:8824:8957:9009:9025:9388:10004:10848:11604:11638:11639:11783:11914:12043:12185:12445:12517:12519:12740:13026:14149:14381:14658:14659:14687:21080:21221:30054:30055:30065:30066,0,RBL:none,CacheIP:none,Bayesian:0.5,0.5,0.5,Netcheck:none,DomainCache:0,MSF:not bulk,SPF:fn,MSBL:0,DNSBL:none,Custom_rules:0:0:0,LFtime:5,LUA_SUMMARY:none +X-HE-Tag: print38_7083d7fd63e24 +X-Filterd-Recvd-Size: 64695 +Received: from computer_3436 (unknown [43.230.105.145]) + (Authenticated sender: jdazey@alexandersmith.com) + by omf06.b.hostedemail.com (Postfix) with ESMTPA + for ; Mon, 22 Aug 2016 14:22:52 +0000 (UTC) +From: =?UTF-8?B?0YHQu9GD0LbQsdCwINCk0J3QoSDQlNCw0L3QuNC40Lsg0KHRg9Cy0L7RgNC+0LI=?= +To: kinney@noth.com +Subject: =?UTF-8?B?0L/QuNGB0YzQvNC+INGD0LLQtdC00L7QvC3QtQ==?= +Content-Type: multipart/mixed; boundary="2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7" + +--2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: base64 + +0J3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LohPGJyPg0K0JjQvdGE0L7RgNC80LjRgNGD +0LXQvCDQktCw0YEg0L7QsSDQuNC80LXRjtGJ0LXQudGB0Y8g0LfQsNC00L7Qu9C20LXQvdC90L7R +gdGC0LguPHA+DQrQn9GA0L7RgdGM0LHQsCDQvtC30L3QsNC60L7QvNC40YLRjNGB0Y8g0LIg0L/R +gNC40LvQvtC20LXQvdC40LguPHA+DQo8YnI+DQo8YnI+DQrQoSDRg9Cy0LDQttC10L3QuNC10Lws +PHA+DQrQuNC90YHQv9C10LrRgtC+0YAg0KTQndChINCg0KQg0JXQs9C+0YAg0KHRg9Cy0L7RgNC+ +0LIuPGJyPg0KPHA+DQo8YnI+DQo8cD4NCjxwPg0K0JjQvdGE0L7RgNC80LDRhtC40L7QvdC90YvQ +uSDRgNCw0LfQtNC10Ls8YnI+DQo8YnI+DQrQn9GA0LXQt9C40LTQtdC90YIg0L/QvtGA0YPRh9C4 +0Lsg0L/RgNCw0LLQuNGC0LXQu9GM0YHRgtCy0YMg0LfQsNC60YDQtdC/0LjRgtGMINCyINC30LDQ +utC+0L3QtSDRgNC40YHQui3QvtGA0LjQtdC90YLQuNGA0L7QstCw0L3QvdGL0Lkg0L/QvtC00YXQ +vtC0INC6INC/0YDQvtCy0LXRgNC60LDQvDxwPg0KPHA+DQoxNSDQsNCy0LPRg9GB0YLQsCAyMDE2 +PGJyPg0K0JLQsNC70LXRgNC40Y8g0JfQtdC90L7QstC40L3QsDxicj4NCjxwPg0K0J/RgNC10LfQ +uNC00LXQvdGCINC/0L7RgNGD0YfQuNC7INC/0YDQsNCy0LjRgtC10LvRjNGB0YLQstGDINC30LDQ +utGA0LXQv9C40YLRjCDQsiDQt9Cw0LrQvtC90LUg0YDQuNGB0Lot0L7RgNC40LXQvdGC0LjRgNC+ +0LLQsNC90L3Ri9C5INC/0L7QtNGF0L7QtCDQuiDQv9GA0L7QstC10YDQutCw0LzQktCy0LXRgdGC +0Lgg0YLQsNC60L7QuSDQv9C+0LTRhdC+0LQg0L/RgNC4INC+0YDQs9Cw0L3QuNC30LDRhtC40Lgg +0LrQvtC90YLRgNC+0LvRjNC90L4t0L3QsNC00LfQvtGA0L3Ri9GFINC80LXRgNC+0L/RgNC40Y/R +gtC40Lkg0Lgg0LIg0YbQtdC70L7QvCDQvtC/0YLQuNC80LjQt9C40YDQvtCy0LDRgtGMINC/0L7Q +tNC+0LHQvdGL0LUg0LzQtdGA0L7Qv9GA0LjRj9GC0LjRjyDQvdCwINGA0LXQs9C40L7QvdCw0LvR +jNC90L7QvCDQuCDQvNGD0L3QuNGG0LjQv9Cw0LvRjNC90L7QvCDRg9GA0L7QstC90Y/RhSDQn9GA +0LXQt9C40LTQtdC90YIg0KDQpCDQktC70LDQtNC40LzQuNGAINCf0YPRgtC40L0g0L/QvtGA0YPR +h9C40Lsg0LrQsNCx0LzQuNC90YMg0LTQviAzMSDQtNC10LrQsNCx0YDRjyDRgtC10LrRg9GJ0LXQ +s9C+INCz0L7QtNCwLiDQmNC30LzQtdC90LXQvdC40Y8g0LzQvtCz0YPRgiDQutC+0YHQvdGD0YLR +jNGB0Y8g0Lgg0LLQvdC10L/Qu9Cw0L3QvtCy0YvRhSDQv9GA0L7QstC10YDQvtC6LiDQkiDRgdC+ +0L7RgtCy0LXRgtGB0YLQstC40Lgg0YEg0YPQutCw0LfQsNC90LjRj9C80Lgg0L/RgNC10LfQuNC0 +0LXQvdGC0LAsINCT0LXQvdC10YDQsNC70YzQvdCw0Y8g0L/RgNC+0LrRg9GA0LDRgtGD0YDQsCDQ +oNCkINC00L7Qu9C20L3QsCDQstC90LXRgdGC0Lgg0LIg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM +0YHRgtCy0L4g0L/QvtC/0YDQsNCy0LrQuCwg0L/RgNC10LTRg9GB0LzQsNGC0YDQuNCy0LDRjtGJ +0LjQtSDRgdC+0LPQu9Cw0YHQvtCy0LDQvdC40LUg0YLQsNC60LjRhSDQv9GA0L7QstC10YDQvtC6 +INGBINC+0YDQs9Cw0L3QsNC80Lgg0L/RgNC+0LrRg9GA0LDRgtGD0YDRiy4g0K3RgtC+INC30LDR +gtGA0L7QvdC10YIg0L/RgNC+0LLQtdGA0LrQuCwg0LjQvdC40YbQuNC40YDRg9C10LzRi9C1INCy +INGB0LLRj9C30Lgg0YEg0L3QsNGA0YPRiNC10L3QuNC10Lwg0L/RgNCw0LIg0L/QvtGC0YDQtdCx +0LjRgtC10LvQtdC5LCDQuCDQvdC10LrQvtGC0L7RgNGL0LUg0LTRgNGD0LPQuNC1LjxwPg0KPGJy +Pg0K0J3QsNGA0Y/QtNGDINGBINGN0YLQuNC8LCDRgNGP0LQg0LfQsNC60L7QvdC+0LTQsNGC0LXQ +u9GM0L3Ri9GFINC40LfQvNC10L3QtdC90LjQuSDQvNC+0LbQtdGCINC60L7RgdC90YPRgtGM0YHR +jyDQtdC00LjQvdC+0LPQviDRgNC10LXRgdGC0YDQsCDQv9GA0L7QstC10YDQvtC6IChwcm92ZXJr +aS5nb3YucnUpLjxwPg0KPGJyPg0K0KLQsNC6LCDQvtGA0LPQsNC90YssINGA0LXQsNC70LjQt9GD +0Y7RidC40LUg0LrQvtC90YLRgNC+0LvRjNC90L4t0L3QsNC00LfQvtGA0L3Ri9C1INC/0L7Qu9C9 +0L7QvNC+0YfQuNGPLCDQtNC+0LvQttC90Ysg0LHRg9C00YPRgiDQv9C+INGB0L7Qs9C70LDRgdC+ +0LLQsNC90LjRjiDRgSDQk9C10L3QtdGA0LDQu9GM0L3QvtC5INC/0YDQvtC60YPRgNCw0YLRg9GA +0L7QuSDQoNCkINGA0LDQt9GA0LDQsdC+0YLQsNGC0Ywg0Lgg0LjQt9C00LDRgtGMINCw0LrRgtGL +LCDRgNC10LPQu9Cw0LzQtdC90YLQuNGA0YPRjtGJ0LjQtSDQv9C+0YDRj9C00L7QuiDQstC90LXR +gdC10L3QuNGPINC40L3RhNC+0YDQvNCw0YbQuNC4INC+INC/0YDQvtCy0LXRgNC60LDRhSDQsiDQ +tdC00LjQvdGL0Lkg0YDQtdC10YHRgtGAINC/0YDQvtCy0LXRgNC+0LouINCQINC30LAg0L3QtdCy +0L3QtdGB0LXQvdC40LUg0YLQsNC60LjRhSDRgdCy0LXQtNC10L3QuNC5INC4INC30LAg0L3QsNGA +0YPRiNC10L3QuNC1INC/0L7RgNGP0LTQutCwINC4INGB0YDQvtC60L7QsiDQuNGFINCy0L3QtdGB +0LXQvdC40Y8g0L/RgNC10LTQu9Cw0LPQsNC10YLRgdGPINGD0YHRgtCw0L3QvtCy0LjRgtGMINCw +0LTQvNC40L3QuNGB0YLRgNCw0YLQuNCy0L3Rg9GOINC+0YLQstC10YLRgdGC0LLQtdC90L3QvtGB +0YLRjC4g0JrQsNC60LjQtSDQuNC80LXQvdC90L4g0L3QsNC60LDQt9Cw0L3QuNGPINC80L7Qs9GD +0YIg0LHRi9GC0Ywg0LLQstC10LTQtdC90Ysg0LIg0JrQvtCQ0J8g0KDQpCDigJMg0L3QtSDRg9GC +0L7Rh9C90Y/QtdGC0YHRjy4g0J7QttC40LTQsNC10YLRgdGPLCDRh9GC0L4g0YPQutCw0LfQsNC9 +0L3Ri9C1INC/0L7RgNGD0YfQtdC90LjRjyDQsdGD0LTRg9GCINC40YHQv9C+0LvQvdC10L3RiyDQ +uiAxINC00LXQutCw0LHRgNGPLjxicj4NCjxwPg0K0JrRgNC+0LzQtSDRgtC+0LPQviwg0LTQviAx +NSDQtNC10LrQsNCx0YDRjyDQk9C10L3QtdGA0LDQu9GM0L3QvtC5INC/0YDQvtC60YPRgNCw0YLR +g9GA0LUg0KDQpCDQvdC10L7QsdGF0L7QtNC40LzQviDQsdGD0LTQtdGCINC00L7RgNCw0LHQvtGC +0LDRgtGMINC10LTQuNC90YvQuSDRgNC10LXRgdGC0YAg0L/RgNC+0LLQtdGA0L7QuiDRgtCw0Los +INGH0YLQvtCx0Ysg0L/RgNC4INCy0L3QtdGB0LXQvdC40Lgg0LIg0L3QtdCz0L4g0LjQvdGE0L7R +gNC80LDRhtC40Lgg0YHRgtCw0LvQviDQstC+0LfQvNC+0LbQvdGL0Lwg0LjRgdC/0L7Qu9GM0LfQ +vtCy0LDQvdC40LUg0YHQstC10LTQtdC90LjQuSDQuNC3INC00YDRg9Cz0LjRhSDQs9C+0YHRg9C0 +0LDRgNGB0YLQstC10L3QvdGL0YUg0LjQvdGE0L7RgNC80LDRhtC40L7QvdC90YvRhSDRgdC40YHR +gtC10LwsINGB0L/RgNCw0LLQvtGH0L3QuNC60L7QsiDQuCDQutC70LDRgdGB0LjRhNC40LrQsNGC +0L7RgNC+0LIuINCi0LDQutC20LUg0LPQu9Cw0LLQsCDQs9C+0YHRg9C00LDRgNGB0YLQstCwINGD +0LrQsNC30LDQuywg0YfRgtC+INC90LXQvtCx0YXQvtC00LjQvNCwINGE0L7RgNC80LDQu9C40LfQ +sNGG0LjRjyDQuCDQutC+0L3QutGA0LXRgtC40LfQsNGG0LjRjyDQstC90L7RgdC40LzQvtC5INCy +INGA0LXQtdGB0YLRgCDQuNC90YTQvtGA0LzQsNGG0LjQuCwg0LXRgdC70Lgg0L7QvdCwINC60LDR +gdCw0LXRgtGB0Y8g0L/RgNCw0LLQvtCy0YvRhSDQvtGB0L3QvtCy0LDQvdC40Lkg0LTQu9GPINC+ +0YDQs9Cw0L3QuNC30LDRhtC40Lgg0L/RgNC+0LLQtdGA0L7Qui4g0KDQtdGH0Ywg0LjQtNC10YIg +0Lgg0L4g0YTQvtGA0LzQsNC70LjQt9Cw0YbQuNC4INGC0YDQtdCx0L7QstCw0L3QuNC5LCDRgdC+ +0LHQu9GO0LTQtdC90LjQtSDQutC+0YLQvtGA0YvRhSDQvtGG0LXQvdC40LLQsNC10YLRgdGPINC/ +0YDQuCDQv9GA0L7QstC10LTQtdC90LjQuCDRgdC+0L7RgtCy0LXRgtGB0YLQstGD0Y7RidC40YUg +0LzQtdGA0L7Qv9GA0LjRj9GC0LjQuSwg0LAg0YLQsNC60LbQtSDRgNC10LfRg9C70YzRgtCw0YLQ +vtCyINC/0YDQvtCy0LXRgNC+0Log0Lgg0L/RgNC40L3Rj9GC0YvRhSDQvNC10YAuPHA+DQo8YnI+ +DQrQn9C+0LzQuNC80L4g0Y3RgtC+0LPQviDQv9GA0LXQt9C40LTQtdC90YIg0L/QvtGA0YPRh9C4 +0Lsg0L/RgNCw0LLQuNGC0LXQu9GM0YHRgtCy0YMg0YDQsNGB0YHQvNC+0YLRgNC10YLRjCDQstC+ +0L/RgNC+0YEg0L4g0YHRgtC40LzRg9C70LjRgNGD0Y7RidC40YUg0LLRi9C/0LvQsNGC0LDRhSDQ +tNC70Y8g0YHQvtGC0YDRg9C00L3QuNC60L7QsiDRhNC10LTQtdGA0LDQu9GM0L3Ri9GFINC40YHQ +v9C+0LvQvdC40YLQtdC70YzQvdGL0YUg0L7RgNCz0LDQvdC+0LIsINC60L7RgtC+0YDRi9C1INC+ +0YHRg9GJ0LXRgdGC0LLQu9GP0Y7RgiDQv9GA0L7QstC10YDQutC4LjxwPg0KPGJyPg0K0J/QviDR +gNC10LfRg9C70YzRgtCw0YLQsNC8INGN0LvQtdC60YLRgNC+0L3QvdC+0LPQviDQsNGD0LrRhtC4 +0L7QvdCwINC30LDQutC70Y7Rh9Cw0YLRjCDQutC+0L3RgtGA0LDQutGCINC90LAg0LHRg9C80LDQ +s9C1INC90LUg0L3Rg9C20L3Qvjxicj4NCjxwPg0KMTIg0LDQstCz0YPRgdGC0LAgMjAxNjxicj4N +CtCS0LDQu9C10YDQuNGPINCX0LXQvdC+0LLQuNC90LA8cD4NCjxicj4NCtCf0L4g0YDQtdC30YPQ +u9GM0YLQsNGC0LDQvCDRjdC70LXQutGC0YDQvtC90L3QvtCz0L4g0LDRg9C60YbQuNC+0L3QsCDQ +t9Cw0LrQu9GO0YfQsNGC0Ywg0LrQvtC90YLRgNCw0LrRgiDQvdCwINCx0YPQvNCw0LPQtSDQvdC1 +INC90YPQttC90L7QnNC40L3RjdC60L7QvdC+0LzRgNCw0LfQstC40YLQuNGPINGA0LDQt9GK0Y/R +gdC90LjQu9C+LCDRh9GC0L4g0L/QviDRgNC10LfRg9C70YzRgtCw0YLQsNC8INGN0LvQtdC60YLR +gNC+0L3QvdC+0LPQviDQsNGD0LrRhtC40L7QvdCwINC/0YDQuCDQvdCw0LvQuNGH0LjQuCDQt9Cw +0LrQu9GO0YfQtdC90L3QvtCz0L4g0LrQvtC90YLRgNCw0LrRgtCwINCyINGN0LvQtdC60YLRgNC+ +0L3QvdC+0Lkg0YTQvtGA0LzQtSDQt9Cw0LrQu9GO0YfQsNGC0Ywg0LrQvtC90YLRgNCw0LrRgiDQ +tdGJ0LUg0Lgg0LIg0L/QuNGB0YzQvNC10L3QvdC+0Lkg0YTQvtGA0LzQtSDQvdCwINCx0YPQvNCw +0LbQvdC+0Lwg0L3QvtGB0LjRgtC10LvQtSDQvdC1INC90YPQttC90L4uINCS0LXQtNC+0LzRgdGC +0LLQviDQv9C+0LTRh9C10YDQutC90YPQu9C+LCDRh9GC0L4g0YLQsNC60LDRjyDQvdC10L7QsdGF +0L7QtNC40LzQvtGB0YLRjCDQvdC1INC/0YDQtdC00YPRgdC80L7RgtGA0LXQvdCwINC00LXQudGB +0YLQstGD0Y7RidC40Lwg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM0YHRgtCy0L7QvCAo0L/QuNGB +0YzQvNC+INCc0LjQvdGN0LrQvtC90L7QvNGA0LDQt9Cy0LjRgtC40Y8g0KDQvtGB0YHQuNC4INC+ +0YIgNSDQuNGO0LvRjyAyMDE2INCzLiDihJYg0JQyONC4LTE2ODcpLjxwPg0KPHA+DQrQodC10LPQ +vtC00L3RjyDQtNC70Y8g0YLQvtCz0L4sINGH0YLQvtCx0Ysg0LfQsNC60LvRjtGH0LjRgtGMINGN +0LvQtdC60YLRgNC+0L3QvdGL0Lkg0LrQvtC90YLRgNCw0LrRgiDQvdCwINGN0LvQtdC60YLRgNC+ +0L3QvdC+0Lwg0LDRg9C60YbQuNC+0L3QtSwg0LXQs9C+INC90LXQvtCx0YXQvtC00LjQvNC+INGA +0LDQt9C80LXRgdGC0LjRgtGMINCyINC10LTQuNC90L7QuSDQuNC90YTQvtGA0LzQsNGG0LjQvtC9 +0L3QvtC5INGB0LjRgdGC0LXQvNC1ICjQldCY0KEpLCDQv9GA0LjRh9C10Lwg0L7QvSDQtNC+0LvQ +ttC10L0g0LHRi9GC0Ywg0L/QvtC00L/QuNGB0LDQvSDRg9GB0LjQu9C10L3QvdC+0Lkg0Y3Qu9C1 +0LrRgtGA0L7QvdC90L7QuSDQv9C+0LTQv9C40YHRjNGOINC70LjRhtCwLCDQuNC80LXRjtGJ0LXQ +s9C+INC/0YDQsNCy0L4g0LTQtdC50YHRgtCy0L7QstCw0YLRjCDQvtGCINC40LzQtdC90Lgg0LfQ +sNC60LDQt9GH0LjQutCwLiDQodC00LXQu9Cw0YLRjCDRjdGC0L4g0L3QtdC+0LHRhdC+0LTQuNC8 +0L4g0LIg0YLQtdGH0LXQvdC40LUg0YLRgNC10YUg0YDQsNCx0L7Rh9C40YUg0LTQvdC10Lkg0YEg +0LTQsNGC0Ysg0YDQsNC30LzQtdGJ0LXQvdC40Y8g0L/RgNC+0LXQutGC0LAg0YLQvtCz0L4g0LbQ +tSDQutC+0L3RgtGA0LDQutGC0LAuPGJyPg0KPGJyPg0K0JIg0LrQsNC60LjRhSDRgdC70YPRh9Cw +0Y/RhSDQt9Cw0LrQsNC30YfQuNC6INC+0LHRj9C30LDQvSDQv9GA0L7QstC+0LTQuNGC0Ywg0Y3Q +u9C10LrRgtGA0L7QvdC90YvQuSDQsNGD0LrRhtC40L7QvT8g0KPQt9C90LDQudGC0LUg0LjQtyDQ +vNCw0YLQtdGA0LjQsNC70LAgItCj0YHQu9C+0LLQuNGPINC/0YDQuNC80LXQvdC10L3QuNGPINGN +0LvQtdC60YLRgNC+0L3QvdC+0LPQviDQsNGD0LrRhtC40L7QvdCwIiDQsiAi0K3QvdGG0LjQutC7 +0L7Qv9C10LTQuNC4INGA0LXRiNC10L3QuNC5LiDQk9C+0YHRg9C00LDRgNGB0YLQstC10L3QvdGL +0LUg0Lgg0LrQvtGA0L/QvtGA0LDRgtC40LLQvdGL0LUg0LfQsNC60YPQv9C60LgiINC40L3RgtC1 +0YDQvdC10YIt0LLQtdGA0YHQuNC4INGB0LjRgdGC0LXQvNGLINCT0JDQoNCQ0J3Qoi4g0J/QvtC7 +0YPRh9C40YLQtSDQv9C+0LvQvdGL0Lkg0LTQvtGB0YLRg9C/INC90LAgMyDQtNC90Y8g0LHQtdGB +0L/Qu9Cw0YLQvdC+ITxwPg0K0J/QvtC70YPRh9C40YLRjCDQtNC+0YHRgtGD0L88cD4NCtCj0LrQ +sNC30LDQvdC90YvQuSDQv9GA0L7QtdC60YIg0L/RgNC4INGN0YLQvtC8INGC0L7QttC1INC00L7Q +u9C20LXQvSDQsdGL0YLRjCDQv9C+0LTQv9C40YHQsNC9INGD0YHQuNC70LXQvdC90L7QuSDRjdC7 +0LXQutGC0YDQvtC90L3QvtC5INC/0L7QtNC/0LjRgdGM0Y4sINC90L4g0YPQttC1INC70LjRhtCw +LCDQutC+0YLQvtGA0L7QtSDQuNC80LXQtdGCINC/0YDQsNCy0L4g0LTQtdC50YHRgtCy0L7QstCw +0YLRjCDQvtGCINC40LzQtdC90Lgg0L/QvtCx0LXQtNC40YLQtdC70Y8g0Y3Qu9C10LrRgtGA0L7Q +vdC90L7Qs9C+INCw0YPQutGG0LjQvtC90LAuINCf0L7QvNC40LzQviDQv9GA0L7QtdC60YLQsCDQ +utC+0L3RgtGA0LDQutGC0LAg0YLQsNC60L7QuSDQv9C+0LHQtdC00LjRgtC10LvRjCDQtNC+0LvQ +ttC10L0g0L/RgNC10LTQvtGB0YLQsNCy0LjRgtGMINC+0LHQtdGB0L/QtdGH0LXQvdC40LUg0LjR +gdC/0L7Qu9C90LXQvdC40Y8g0LrQvtC90YLRgNCw0LrRgtCwICjRhy4gNyDRgdGCLiA3MCDQpNC1 +0LTQtdGA0LDQu9GM0L3QvtCz0L4g0LfQsNC60L7QvdCwINC+0YIgNSDQsNC/0YDQtdC70Y8gMjAx +MyDQsy4g4oSWIDQ0LdCk0JcgItCeINC60L7QvdGC0YDQsNC60YLQvdC+0Lkg0YHQuNGB0YLQtdC8 +0LUg0LIg0YHRhNC10YDQtSDQt9Cw0LrRg9C/0L7QuiDRgtC+0LLQsNGA0L7Qsiwg0YDQsNCx0L7R +giwg0YPRgdC70YPQsyDQtNC70Y8g0L7QsdC10YHQv9C10YfQtdC90LjRjyDQs9C+0YHRg9C00LDR +gNGB0YLQstC10L3QvdGL0YUg0Lgg0LzRg9C90LjRhtC40L/QsNC70YzQvdGL0YUg0L3Rg9C20LQi +OyDQtNCw0LvQtdC1IOKAkyDQl9Cw0LrQvtC9IOKEliA0NC3QpNCXKS48cD4NCjxicj4NCtChINC8 +0L7QvNC10L3RgtCwINGA0LDQt9C80LXRidC10L3QuNGPINCyINCV0JjQoSDQv9C+0LTQv9C40YHQ +sNC90L3QvtCz0L4g0LfQsNC60LDQt9GH0LjQutC+0Lwg0LrQvtC90YLRgNCw0LrRgtCwINC+0L0g +0YHRh9C40YLQsNC10YLRgdGPINC30LDQutC70Y7Rh9C10L3QvdGL0LwgKNGHLiA4INGB0YIuIDcw +INCX0LDQutC+0L3QsCDihJYgNDQt0KTQlykuPGJyPg0KPGJyPg0K0J3QsNC/0L7QvNC90LjQvCwg +0L/QviDQtNC10LnRgdGC0LLRg9GO0YnQtdC80YMg0LfQsNC60L7QvdC+0LTQsNGC0LXQu9GM0YHR +gtCy0YMsINGN0LvQtdC60YLRgNC+0L3QvdGL0Lkg0LDRg9C60YbQuNC+0L0g0L/RgNC+0LLQvtC0 +0LjRgtGB0Y8g0L/Rg9GC0LXQvCDRgdC90LjQttC10L3QuNGPINC90LDRh9Cw0LvRjNC90L7QuSAo +0LzQsNC60YHQuNC80LDQu9GM0L3QvtC5KSDRhtC10L3RiyDQutC+0L3RgtGA0LDQutGC0LAsINGD +0LrQsNC30LDQvdC90L7QuSDQsiDQuNC30LLQtdGJ0LXQvdC40Lgg0L4g0L/RgNC+0LLQtdC00LXQ +vdC40Lgg0YLQsNC60L7Qs9C+INCw0YPQutGG0LjQvtC90LAuINCSINC90LXQvCDQvNC+0LPRg9GC +INGD0YfQsNGB0YLQstC+0LLQsNGC0Ywg0YLQvtC70YzQutC+INCw0LrQutGA0LXQtNC40YLQvtCy +0LDQvdC90YvQtSDRg9GH0LDRgdGC0L3QuNC60LgsINCwINC80LXRgdGC0L7QvCDQtdCz0L4g0L/R +gNC+0LLQtdC00LXQvdC40Y8g0Y/QstC70Y/QtdGC0YHRjyDRjdC70LXQutGC0YDQvtC90L3QsNGP +INC/0LvQvtGJ0LDQtNC60LAgKNGB0YIuIDY4INCX0LDQutC+0L3QsCDihJYgNDQt0KTQlykuPGJy +Pg0KPHA+DQo1INC00LXQutCw0LHRgNGPIDIwMTQg0LPQvtC00LAg0YHQvtGB0YLQvtGP0LvQvtGB +0Ywg0LjQvdGC0LXRgNC90LXRgi3QuNC90YLQtdGA0LLRjNGOINGBINC90LDRh9Cw0LvRjNC90LjQ +utC+0Lwg0KPQv9GA0LDQstC70LXQvdC40Y8g0LrQsNC80LXRgNCw0LvRjNC90L7Qs9C+INC60L7Q +vdGC0YDQvtC70Y8g0KTQtdC00LXRgNCw0LvRjNC90L7QuSDQvdCw0LvQvtCz0L7QstC+0Lkg0YHQ +u9GD0LbQsdGLINCh0LDRgtC40L3Ri9C8INCU0LzQuNGC0YDQuNC10Lwg0KHRgtCw0L3QuNGB0LvQ +sNCy0L7QstC40YfQtdC8Ljxicj4NCjxicj4NCtCi0LXQvNCwINC40L3RgtC10YDQvdC10YIt0LjQ +vdGC0LXRgNCy0YzRjjogItCd0LDQu9C+0LMg0L3QsCDQtNC+0LHQsNCy0LvQtdC90L3Rg9GOINGB +0YLQvtC40LzQvtGB0YLRjDog0L3QvtCy0LDRhtC40LggMjAxNSDQs9C+0LTQsCIuPGJyPg0KPHA+ +DQrQktC10LTRg9GJ0LDRjzog0JTQvtCx0YDRi9C5INC00LXQvdGMLCDQlNC80LjRgtGA0LjQuSDQ +odGC0LDQvdC40YHQu9Cw0LLQvtCy0LjRhyEg0KDQsNGB0YHQutCw0LbQuNGC0LUsINC/0L7QttCw +0LvRg9C50YHRgtCwLCDQutCw0LrQuNC1INC90L7QstCw0YbQuNC4LCDRgdCy0Y/Qt9Cw0L3QvdGL +0LUg0YEg0L/RgNC10LTRgdGC0LDQstC70LXQvdC40LXQvCDQvtGC0YfQtdGC0L3QvtGB0YLQuCDQ +v9C+INCd0JTQoSwg0L7QttC40LTQsNGO0YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC4 +0LrQvtCyINCyIDIwMTUg0LPQvtC00YM/PGJyPg0KPGJyPg0K0KHQsNGC0LjQvSDQlC7QoS46INCU +0L7QsdGA0YvQuSDQtNC10L3RjCEg0J3QsNGH0LjQvdCw0Y8g0YEg0L3QsNC70L7Qs9C+0LLQvtCz +0L4g0L/QtdGA0LjQvtC00LAg0LfQsCAxINC60LLQsNGA0YLQsNC7IDIwMTUg0LPQvtC00LAg0L3Q +sCDQvtGB0L3QvtCy0LDQvdC40Lgg0L/Rg9C90LrRgtCwIDUuMSDRgdGC0LDRgtGM0LggMTc0INCa +0L7QtNC10LrRgdCwICjQsiDRgNC10LTQsNC60YbQuNC4INCk0LXQtNC10YDQsNC70YzQvdC+0LPQ +viDQt9Cw0LrQvtC90LAg0L7RgiAyOC4wNi4yMDEzIOKEliAxMzQt0KTQlykg0LIg0L3QsNC70L7Q +s9C+0LLRg9GOINC00LXQutC70LDRgNCw0YbQuNGOINC/0L4g0J3QlNChINCy0LrQu9GO0YfQsNGO +0YLRgdGPINGB0LLQtdC00LXQvdC40Y8sINGD0LrQsNC30LDQvdC90YvQtSDQsiDQutC90LjQs9C1 +INC/0L7QutGD0L/QvtC6INC4INC60L3QuNCz0LUg0L/RgNC+0LTQsNC2LiDQn9GA0Lgg0L7RgdGD +0YnQtdGB0YLQstC70LXQvdC40Lgg0L/QvtGB0YDQtdC00L3QuNGH0LXRgdC60L7QuSDQtNC10Y/R +gtC10LvRjNC90L7RgdGC0Lgg0LIg0L3QsNC70L7Qs9C+0LLRg9GOINC00LXQutC70LDRgNCw0YbQ +uNGOINC/0L4g0J3QlNChINCy0LrQu9GO0YfQsNGO0YLRgdGPINGB0LLQtdC00LXQvdC40Y8sINGD +0LrQsNC30LDQvdC90YvQtSDQsiDQttGD0YDQvdCw0LvQtSDRg9GH0LXRgtCwINC/0L7Qu9GD0YfQ +tdC90L3Ri9GFINC4INCy0YvRgdGC0LDQstC70LXQvdC90YvRhSDRgdGH0LXRgtC+0LIt0YTQsNC6 +0YLRg9GALCDQsiDQvtGC0L3QvtGI0LXQvdC40Lgg0YPQutCw0LfQsNC90L3QvtC5INC00LXRj9GC +0LXQu9GM0L3QvtGB0YLQuC48YnI+DQo8YnI+DQrQn9GA0LjQutCw0LfQvtC8INCk0J3QoSDQoNC+ +0YHRgdC40Lgg0L7RgiAyOS4xMC4yMDE0IOKEliDQnNCc0JItNy0zLzU1OEAg0YPRgtCy0LXRgNC2 +0LTQtdC90LAg0YTQvtGA0LzQsCDQvdCw0LvQvtCz0L7QstC+0Lkg0LTQtdC60LvQsNGA0LDRhtC4 +0Lgg0L/QviDQndCU0KEsINC/0L7RgNGP0LTQvtC6INC10LUg0LfQsNC/0L7Qu9C90LXQvdC40Y8g +0Lgg0YTQvtGA0LzQsNGCINC/0YDQtdC00YHRgtCw0LLQu9C10L3QuNGPINCyINGN0LvQtdC60YLR +gNC+0L3QvdC+0Lkg0YTQvtGA0LzQtS4g0JIg0L3QsNGB0YLQvtGP0YnQtdC1INCy0YDQtdC80Y8g +0L/RgNC40LrQsNC3INC/0YDQvtGF0L7QtNC40YIg0LPQvtGB0YPQtNCw0YDRgdGC0LLQtdC90L3R +g9GOINGA0LXQs9C40YHRgtGA0LDRhtC40Y4g0LIg0JzQuNC90Y7RgdGC0LUg0KDQvtGB0YHQuNC4 +LjxwPg0KPHA+DQrQkiDQvdC+0LLQvtC5INGE0L7RgNC80LUg0L3QsNC70L7Qs9C+0LLQvtC5INC0 +0LXQutC70LDRgNCw0YbQuNC4INC/0L4g0J3QlNChINC/0YDQtdC00YPRgdC80L7RgtGA0LXQvdGL +INGA0LDQt9C00LXQu9GLLCDRgdC+0LTQtdGA0LbQsNGJ0LjQtSDRgdCy0LXQtNC10L3QuNGPINC4 +0Lcg0LrQvdC40LMg0L/QvtC60YPQv9C+0LosINC60L3QuNCzINC/0YDQvtC00LDQtiwg0LbRg9GA +0L3QsNC70L7QsiDRg9GH0LXRgtCwINC/0L7Qu9GD0YfQtdC90L3Ri9GFINC4INCy0YvRgdGC0LDQ +stC70LXQvdC90YvRhSDRgdGH0LXRgtC+0LIt0YTQsNC60YLRg9GALjxicj4NCjxicj4NCtCSINGB +0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDQv9GD0L3QutGC0L7QvCAzINGB0YLQsNGC0YzQuCA4 +MCDQuCDQv9GD0L3QutGC0L7QvCA1INGB0YLQsNGC0YzQuCAxNzQg0JrQvtC00LXQutGB0LAg0L3Q +sNC70L7Qs9C+0LLQsNGPINC00LXQutC70LDRgNCw0YbQuNGPINC/0L4g0J3QlNChINC00L7Qu9C2 +0L3QsCDQv9GA0LXQtNGB0YLQsNCy0LvRj9GC0YzRgdGPINCyINGN0LvQtdC60YLRgNC+0L3QvdC+ +0Lkg0YTQvtGA0LzQtSDQv9C+INGC0LXQu9C10LrQvtC80LzRg9C90LjQutCw0YbQuNC+0L3QvdGL +0Lwg0LrQsNC90LDQu9Cw0Lwg0YHQstGP0LfQuCDRh9C10YDQtdC3INC+0L/QtdGA0LDRgtC+0YDQ +sCDRjdC70LXQutGC0YDQvtC90L3QvtCz0L4g0LTQvtC60YPQvNC10L3RgtC+0L7QsdC+0YDQvtGC +0LAuINCi0LDQutC40Lwg0L7QsdGA0LDQt9C+0LwsINGE0L7RgNC80LjRgNC+0LLQsNC90LjQtSDQ +vdCw0LvQvtCz0L7QstC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/QviDQndCU0KEg0L3QsCDQ +sdGD0LzQsNC20L3Ri9GFINC90L7RgdC40YLQtdC70Y/RhSDQt9Cw0LrQvtC90L7QtNCw0YLQtdC7 +0YzRgdGC0LLQvtC8INC+INC90LDQu9C+0LPQsNGFINC4INGB0LHQvtGA0LDRhSDQvdC1INC/0YDQ +tdC00YPRgdC80L7RgtGA0LXQvdC+Ljxicj4NCjxicj4NCtCb0LjRhtCwLCDQvdC1INGP0LLQu9GP +0Y7RidC40LXRgdGPINC90LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INCd0JTQ +oSDQuNC70Lgg0L3QsNC70L7Qs9C+0LLRi9C80Lgg0LDQs9C10L3RgtCw0LzQuCDQv9C+INCd0JTQ +oSwg0L3QviDQvtGB0YPRidC10YHRgtCy0LvRj9GO0YnQuNC1INC/0L7RgdGA0LXQtNC90LjRh9C1 +0YHQutGD0Y4g0LTQtdGP0YLQtdC70YzQvdC+0YHRgtGMLCDQtNC+0LvQttC90Ysg0L3QsCDQvtGB +0L3QvtCy0LDQvdC40Lgg0L/Rg9C90LrRgtCwIDUuMiDRgdGC0LDRgtGM0LggMTc0INCa0L7QtNC1 +0LrRgdCwINC/0YDQtdC00YHRgtCw0LLQu9GP0YLRjCDQsiDQvdCw0LvQvtCz0L7QstGL0Lkg0L7R +gNCz0LDQvSDQsiDQvtGC0L3QvtGI0LXQvdC40Lgg0YPQutCw0LfQsNC90L3QvtC5INC00LXRj9GC +0LXQu9GM0L3QvtGB0YLQuCDQttGD0YDQvdCw0Lsg0YPRh9C10YLQsCDQv9C+0LvRg9GH0LXQvdC9 +0YvRhSDQuCDQstGL0YHRgtCw0LLQu9C10L3QvdGL0YUg0YHRh9C10YLQvtCyLdGE0LDQutGC0YPR +gCDQv9C+INCi0JrQoSDRh9C10YDQtdC3INC+0L/QtdGA0LDRgtC+0YDQsCDQrdCU0J4uPHA+DQo8 +cD4NCtCk0LXQtNC10YDQsNC70YzQvdC+0Lkg0L3QsNC70L7Qs9C+0LLQvtC5INGB0LvRg9C20LHQ +vtC5INGA0LDQt9GA0LDQsdC+0YLQsNC90LAg0LHQtdGB0L/Qu9Cw0YLQvdCw0Y8g0L/RgNC+0LPR +gNCw0LzQvNCwICLQndCw0LvQvtCz0L7Qv9C70LDRgtC10LvRjNGJ0LjQuiDQrtCbIiDQtNC70Y8g +0LLQtdC00LXQvdC40Y8g0LrQvdC40LMg0L/QvtC60YPQv9C+0Log0Lgg0L/RgNC+0LTQsNC2LCDQ +sCDRgtCw0LrQttC1INGE0L7RgNC80LjRgNC+0LLQsNC90LjRjyDQvdCw0LvQvtCz0L7QstC+0Lkg +0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/QviDQndCU0KEuINCU0LDQvdC90YvQvCDQv9GA0L7Qs9GA +0LDQvNC80L3Ri9C8INC/0YDQvtC00YPQutGC0L7QvCDQvNC+0LbQtdGCINCy0L7RgdC/0L7Qu9GM +0LfQvtCy0LDRgtGM0YHRjyDQu9GO0LHQvtC5INC90LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQ +uNC6INCx0LXRgdC/0LvQsNGC0L3QviDQt9Cw0LPRgNGD0LfQuNCyINC10LPQviDRgSDQvtGE0LjR +htC40LDQu9GM0L3QvtCz0L4g0YHQsNC50YLQsCDQpNCd0KEg0KDQvtGB0YHQuNC4IChodHRwOi8v +d3d3Lm5hbG9nLnJ1L3JuNzcvL3Byb2dyYW0vYWxsL25hbF91bC8pLjxicj4NCjxicj4NCtCS0LXQ +tNGD0YnQsNGPOiDQlNC80LjRgtGA0LjQuSDQodGC0LDQvdC40YHQu9Cw0LLQvtCy0LjRhywg0LXR +gdGC0Ywg0YLQsNC60LDRjyDRgdC40YLRg9Cw0YbQuNGPOiDQtNC+0LHRgNC+0YHQvtCy0LXRgdGC +0L3QsNGPINC60L7QvNC/0LDQvdC40Y8g0L7RgtGA0LDQt9C40LvQsCDQsiDQtNC10LrQu9Cw0YDQ +sNGG0LjQuCDRgNGP0LQg0YHRh9C10YLQvtCyLdGE0LDQutGC0YPRgCwg0L/QviDQutC+0YLQvtGA +0YvQvCDQv9C+0YHRgtCw0LLRidC40Log0L7RgtGH0LXRgtC90L7RgdGC0Ywg0L3QtSDRgdC00LDQ +uyDQstC+0L7QsdGJ0LUuINCl0L7RgtGPINC90LAg0LzQvtC80LXQvdGCINC/0YDQvtCy0LXQtNC1 +0L3QuNGPINGB0LTQtdC70LrQuCDQutC+0L3RgtGA0LDQs9C10L3RgiDQv9C+INC00LDQvdC90YvQ +vCDQstGL0L/QuNGB0LrQuCDQuNC3INCV0JPQoNCu0Jsg0LHRi9C7INCy0L/QvtC70L3QtSDQtNC1 +0LnRgdGC0LLRg9GO0YnQuNC8LiDQmtCw0LrQuNC1INC/0L7RgdC70LXQtNGB0YLQstC40Y8g0L7Q +ttC40LTQsNGO0YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrQsCDQsiDRgtCw0LrQ +vtC8INGB0LvRg9GH0LDQtT8gPHA+DQo8YnI+DQrQodCw0YLQuNC9INCULtChLjog0JXRgdC70Lgg +0LrQvtC90YLRgNCw0LPQtdC90YIg0L3QsNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrQsCDQ +vdC1INC/0YDQtdC00YHRgtCw0LLQuNC7INC90LDQu9C+0LPQvtCy0YPRjiDQtNC10LrQu9Cw0YDQ +sNGG0LjRjiDQv9C+INCd0JTQoSDQuNC70Lgg0LIg0LXQs9C+INC/0YDQtdC00YHRgtCw0LLQu9C1 +0L3QvdC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L3QtSDQvtGC0YDQsNC20LXQvdGLINC60LDQ +utC40LUt0LvQuNCx0L4g0YHRh9C10YLQsC3RhNCw0LrRgtGD0YDRiywg0YLQviDRgtCw0LrQvtC5 +INC60L7QvdGC0YDQsNCz0LXQvdGCINGB0YLQsNC90L7QstC40YLRgdGPINC+0LHRitC10LrRgtC+ +0Lwg0LLQvdC40LzQsNC90LjRjyDRgdC+INGB0YLQvtGA0L7QvdGLINC90LDQu9C+0LPQvtCy0L7Q +s9C+INC+0YDQs9Cw0L3QsC4g0J3QsCDQv9C10YDQstC+0Lwg0Y3RgtCw0L/QtSDQsiDRgdC70YPR +h9Cw0LUg0L7RgtGB0YPRgtGB0YLQstC40Y8g0L3QsNC70L7Qs9C+0LLQvtC5INC00LXQutC70LDR +gNCw0YbQuNC4INC90LDQu9C+0LPQvtCy0YvQuSDQvtGA0LPQsNC9INCx0YPQtNC10YIg0LjQvNC1 +0YLRjCDQstC+0LfQvNC+0LbQvdC+0YHRgtGMINC/0YDQuNC+0YHRgtCw0L3QvtCy0LjRgtGMINC0 +0LLQuNC20LXQvdC40LUg0LTQtdC90LXQttC90YvRhSDRgdGA0LXQtNGB0YLQsiDQv9C+INGA0LDR +gdGH0LXRgtC90YvQvCDRgdGH0LXRgtCw0LwuINCU0LDQu9C10LUg0L/RgNC+0LLQvtC00LjRgtGB +0Y8g0YPRgdGC0LDQvdC+0LLQu9C10L3QvdGL0Lkg0LPQu9Cw0LLQvtC5IDE0INCd0LDQu9C+0LPQ +vtCy0L7Qs9C+INC60L7QtNC10LrRgdCwINCg0L7RgdGB0LjQudGB0LrQvtC5INCk0LXQtNC10YDQ +sNGG0LjQuCDQutC+0LzQv9C70LXQutGBINC60L7QvdGC0YDQvtC70YzQvdGL0YUg0LzQtdGA0L7Q +v9GA0LjRj9GC0LjQuSDQv9C+INGE0L7RgNC80LjRgNC+0LLQsNC90LjRjiDQtNC+0LrQsNC30LDR +gtC10LvRjNC90L7QuSDQsdCw0LfRiyDQvtCxINGD0LrQu9C+0L3QtdC90LjQuCDQvtGCINC90LDQ +u9C+0LPQvtC+0LHQu9C+0LbQtdC90LjRjy4g0K3RgtC+INC40YHRgtGA0LXQsdC+0LLQsNC90LjQ +tSDQuNC90YTQvtGA0LzQsNGG0LjQuCDQuCDQtNC+0LrRg9C80LXQvdGC0L7Qsiwg0LAg0L/RgNC4 +INC90LXQvtCx0YXQvtC00LjQvNC+0YHRgtC4INC00YDRg9Cz0LjQtSDQvNC10YDQvtC/0YDQuNGP +0YLQuNGPINC90LDQu9C+0LPQvtCy0L7Qs9C+INC60L7QvdGC0YDQvtC70Y8uINCf0YDQuCDRjdGC +0L7QvCDQsiDRgdC70YPRh9Cw0LUg0YPRgdGC0LDQvdC+0LLQu9C10L3QuNGPINGE0LDQutGC0LAg +0YDQtdCw0LvRjNC90L7RgdGC0Lgg0L7Qv9C10YDQsNGG0LjQuCwg0LTQu9GPINGB0LDQvNC+0LPQ +viDQvdCw0LvQvtCz0L7Qv9C70LDRgtC10LvRjNGJ0LjQutCwINC90Lgg0LrQsNC60LjRhSDQv9C+ +0YHQu9C10LTRgdGC0LLQuNC5INC90LUg0LHRg9C00LXRgi4g0Jog0L7RgtCy0LXRgtGB0YLQstC1 +0L3QvdC+0YHRgtC4INCx0YPQtNC10YIg0L/RgNC40LLQu9C10YfQtdC90L4g0YLQviDQu9C40YbQ +viwg0LrQvtGC0L7RgNC+0LUg0L3QsNGA0YPRiNC40LvQviDQvdCw0LvQvtCz0L7QstC+0LUg0LfQ +sNC60L7QvdC+0LTQsNGC0LXQu9GM0YHRgtCy0L4uPGJyPg0KPHA+DQrQn9GA0Lgg0Y3RgtC+0Lwg +0LzRiyDRgNC10LrQvtC80LXQvdC00YPQtdC8INCw0LrQutGD0YDQsNGC0L3QviDQv9C+0LTRhdC+ +0LTQuNGC0Ywg0Log0LLRi9Cx0L7RgNGDINC/0L7RgtC10L3RhtC40LDQu9GM0L3Ri9GFINC60L7Q +vdGC0YDQsNCz0LXQvdGC0L7QsiDQuCDQv9GA0L7Rj9Cy0LvRj9GC0Ywg0LTQvtC70LbQvdGD0Y4g +0L7RgdC80L7RgtGA0LjRgtC10LvRjNC90L7RgdGC0YwsINGC0LDQuiDQutCw0Log0Y3RgtC+INC9 +0LXQvtCx0YXQvtC00LjQvNC+INC00LvRjyDRgdC+0YXRgNCw0L3QtdC90LjRjyDQtNC10LvQvtCy +0L7QuSDRgNC10L/Rg9GC0LDRhtC40Lgg0Lgg0YTQvtGA0LzQuNGA0L7QstCw0L3QuNGPINC/0L7Q +u9C+0LbQuNGC0LXQu9GM0L3QvtC5INC90LDQu9C+0LPQvtCy0L7QuSDQuNGB0YLQvtGA0LjQuC48 +cD4NCjxicj4NCtCS0LXQtNGD0YnQsNGPOiDQldGB0LvQuCDQsiDRgdCy0LXQtNC10L3QuNGP0YUg +0YMg0LrQvtC90YLRgNCw0LPQtdC90YLQvtCyINCx0YPQtNGD0YIg0YDQsNGB0YXQvtC20LTQtdC9 +0LjRjywg0L3QsNC/0YDQuNC80LXRgCwg0YDQsNC30L3Ri9C1INC90L7QvNC10YDQsCDRgdGH0LXR +gtC+0LIt0YTQsNC60YLRg9GAINC4INGA0LDQt9C90YvQtSDQtNCw0YLRiywg0LrQsNC6INGN0YLQ +viDQsdGD0LTRg9GCINC+0YbQtdC90LjQstCw0YLRjCDQuNC90YHQv9C10LrRgtC+0YDRiz8g0JAg +0LXRgdC70Lgg0L7RgtC70LjRh9Cw0Y7RgtGB0Y8g0YHRg9C80LzRiyDQv9C+INC30LDRgNC10LPQ +uNGB0YLRgNC40YDQvtCy0LDQvdC90YvQvCDRgdGH0LXRgtCw0Lwt0YTQsNC60YLRg9GA0LDQvD8g +PGJyPg0KPGJyPg0K0KHQsNGC0LjQvSDQlC7QoS46INCf0YDQtdC20LTQtSDQstGB0LXQs9C+INCy +0L4g0LjQt9Cx0LXQttCw0L3QuNC1INGC0LXRhdC90LjRh9C10YHQutC40YUg0L7RiNC40LHQvtC6 +INCyINC/0YDQtdC00YHRgtCw0LLQu9C10L3QvdC+0Lkg0LTQtdC60LvQsNGA0LDRhtC40Lgg0L/Q +viDQndCU0KEg0LzRiyDRgNC10LrQvtC80LXQvdC00YPQtdC8INC/0YDQvtCy0LXRgNC40YLRjCDQ +uNC90YTQvtGA0LzQsNGG0LjRjiDQviDQutC+0L3RgtGA0LDQs9C10L3RgtCw0YUsINC60L7RgtC+ +0YDQsNGPINGB0L7QtNC10YDQttC40YLRgdGPINCyINCy0LDRiNC10Lkg0YPRh9C10YLQvdC+0Lkg +KNCx0YPRhdCz0LDQu9GC0LXRgNGB0LrQvtC5KSDRgdC40YHRgtC10LzQtSwg0L3QsCDQv9GA0LXQ +tNC80LXRgiDQv9GA0LDQstC40LvRjNC90L7RgdGC0Lgg0LfQsNC90LXRgdC10L3QuNGPINCyINGB +0LjRgdGC0LXQvNGDINCY0J3QnSDQuCDQmtCf0J8g0LrQvtC90YLRgNCw0LPQtdC90YLQvtCyLiDQ +nNC+0LbQvdC+INCy0L7RgdC/0L7Qu9GM0LfQvtCy0LDRgtGM0YHRjywg0L3QsNC/0YDQuNC80LXR +gCwg0L7QvdC70LDQudC9LdGB0LXRgNCy0LjRgdC+0LwsINGA0LDQt9C80LXRidC10L3QvdGL0Lwg +0L3QsCDQvtGE0LjRhtC40LDQu9GM0L3QvtC8INGB0LDQudGC0LUg0KTQndChINCg0L7RgdGB0LjQ +uCAod3d3Lm5hbG9nLnJ1LCBodHRwOi8vbnBjaGsubmFsb2cucnUpLjxicj4NCjxicj4NCtCV0YHQ +u9C4INGA0LDRgdGF0L7QttC00LXQvdC40Y8g0LIg0YHQstC10LTQtdC90LjRj9GFINC+0LEg0L7Q +v9C10YDQsNGG0LjRj9GFINC60L7QvdGC0YDQsNCz0LXQvdGC0L7QsiDQsdGD0LTRg9GCINCy0YHQ +tS3RgtCw0LrQuCDQstGL0Y/QstC70LXQvdGLLCDRgtC+INC80Ysg0LjRhSDQtNC10LvQuNC8INC9 +0LAg0LTQstC1INC60LDRgtC10LPQvtGA0LjQuC4g0J/QtdGA0LLQsNGPINGN0YLQviDRgtC10YXQ +vdC40YfQtdGB0LrQuNC1INC+0YjQuNCx0LrQuCwg0YLQsNC6INC90LDQt9GL0LLQsNC10LzRi9C1 +ICLQvtGI0LjQsdC60Lgg0LLQstC+0LTQsCIsINC60L7RgtC+0YDRi9C1INC90LUg0LjQvNC10Y7R +giDQstGL0YHQvtC60L7Qs9C+INC/0YDQuNC+0YDQuNGC0LXRgtCwINCyINC+0YLRgNCw0LHQvtGC +0LrQtS4g0J7RiNC40LHQutC4INCyINC90L7QvNC10YDQtSwg0LTQsNGC0LUg0YHRh9C10YLQsC3R +hNCw0LrRgtGD0YDRiyDQuNC70Lgg0LIg0LTRgNGD0LPQvtC8INGA0LXQutCy0LjQt9C40YLQtSwg +0L3QtSDQstC70LjRj9GO0YnQtdC8INC90LAg0YHRg9C80LzRgyDQvdCw0LvQvtCz0LAsINC80Ysg +0YDQsNGB0YHRh9C40YLRi9Cy0LDQtdC8INGD0YHRgtGA0LDQvdGP0YLRjCDQvdCwINGN0YLQsNC/ +0LUg0LfQsNC/0YDQvtGB0LAg0L/QvtGP0YHQvdC10L3QuNC5LiDQn9GA0Lgg0Y3RgtC+0Lwg0L3Q +sNC70L7Qs9C+0L/Qu9Cw0YLQtdC70YzRidC40LrRgyDQv9GA0LXQtNGB0YLQsNCy0LjRgtGB0Y8g +0LLQvtC30LzQvtC20L3QvtGB0YLRjCDQvdCw0L/RgNCw0LLQuNGC0Ywg0LIg0L3QsNC70L7Qs9C+ +0LLRi9C5INC+0YDQs9Cw0L0g0YTQvtGA0LzQsNC70LjQt9C+0LLQsNC90L3Ri9C1INC/0L7Rj9GB +0L3QtdC90LjRjyDQsdC10Lcg0LrQsNC60LjRhS3Qu9C40LHQviDQvdCw0LvQvtCz0L7QstGL0YUg +0L/QvtGB0LvQtdC00YHRgtCy0LjQuS4g0JTQu9GPINGN0YLQvtCz0L4g0L3QsNC00L4g0LHRg9C0 +0LXRgiDRg9C60LDQt9Cw0YLRjCDQsiDRgdC/0LXRhtC40LDQu9GM0L3QvtC5INGA0LXQutC+0LzQ +tdC90LTQvtCy0LDQvdC90L7QuSDRhNC+0YDQvNC1INC60L7RgNGA0LXQutGC0L3Ri9C1INGB0LLQ +tdC00LXQvdC40Y8g0Lgg0L3QsNC/0YDQsNCy0LjRgtGMINC40YUg0L/QviDQotCa0KEg0YfQtdGA +0LXQtyDQvtC/0LXRgNCw0YLQvtGA0LAg0Y3Qu9C10LrRgtGA0L7QvdC90L7Qs9C+INC00L7QutGD +0LzQtdC90YLQvtC+0LHQvtGA0L7RgtCwINCyINC90LDQu9C+0LPQvtCy0YvQtSDQvtGA0LPQsNC9 +0YsuINCf0YDQtdC00YHRgtCw0LLQu9GP0YLRjCDRg9GC0L7Rh9C90LXQvdC90YPRjiDQvdCw0LvQ +vtCz0L7QstGD0Y4g0LTQtdC60LvQsNGA0LDRhtC40Y4g0L/QviDQndCU0KEg0LIg0YLQsNC60L7Q +vCDRgdC70YPRh9Cw0LUg0L3QtSDRgtGA0LXQsdGD0LXRgtGB0Y8uPGJyPg0KPHA+DQrQldGB0LvQ +uCDQttC1INGA0LDRgdGF0L7QttC00LXQvdC40Y8g0LLRi9C30LLQsNC90Ysg0L3QtdCy0LXRgNC9 +0YvQvCDRgNCw0YHRh9C10YLQvtC8INGB0YPQvNC80Ysg0L3QsNC70L7Qs9CwINC40LvQuCDRgdGH +0LXRgi3RhNCw0LrRgtGD0YDQsCDQvtGC0YDQsNC20LXQvSDQvtGI0LjQsdC+0YfQvdC+LCDRgtC+ +INCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgdC+INGB0YLQsNGC0YzQtdC5IDgxINCd0LDQ +u9C+0LPQvtCy0L7Qs9C+INC60L7QtNC10LrRgdCwINCg0L7RgdGB0LjQudGB0LrQvtC5INCk0LXQ +tNC10YDQsNGG0LjQuCDQvdC10L7QsdGF0L7QtNC40LzQviDQsdGD0LTQtdGCINC/0YDQtdC00YHR +gtCw0LLQuNGC0Ywg0YPRgtC+0YfQvdC10L3QvdGD0Y4g0L3QsNC70L7Qs9C+0LLRg9GOINC00LXQ +utC70LDRgNCw0YbQuNGOINC/0L4g0J3QlNChLiDQmCDQt9C00LXRgdGMINC/0YDQtdC00YPRgdC8 +0LDRgtGA0LjQstCw0LXRgtGB0Y8g0L3QvtCy0YvQuSDQv9C+0LTRhdC+0LQuINCi0LXQv9C10YDR +jCDQtNC+0L/Rg9GB0LrQsNC10YLRgdGPINC/0YDQtdC00YHRgtCw0LLQu9C10L3QuNC1INGD0YLQ +vtGH0L3QtdC90L3QvtC5INC90LDQu9C+0LPQvtCy0L7QuSDQtNC10LrQu9Cw0YDQsNGG0LjQuCAi +0L3QsCDQtNC10LvRjNGC0YMiLiDQotC+INC10YHRgtGMINCyINGD0YLQvtGH0L3QtdC90L3QvtC5 +INC90LDQu9C+0LPQvtCy0L7QuSDQtNC10LrQu9Cw0YDQsNGG0LjQuCDQt9Cw0L/QvtC70L3Rj9GO +0YLRgdGPINGC0L7Qu9GM0LrQviDRgtC1INGA0LDQt9C00LXQu9GLLCDQsiDQutC+0YLQvtGA0YvQ +tSDQstC90LXRgdC10L3RiyDQuNGB0L/RgNCw0LLQu9C10L3QuNGPLiDQn9C+0LTRgNC+0LHQvdC1 +0LUg0L/QvtGA0Y/QtNC+0Log0LfQsNC/0L7Qu9C90LXQvdC40Y8g0L3QsNC70L7Qs9C+0LLQvtC5 +INC00LXQutC70LDRgNCw0YbQuNC4INC40LfQu9C+0LbQtdC9INCyINC/0YDQuNC60LDQt9C1INCk +0J3QoSDQoNC+0YHRgdC40Lgg0L7RgiAyOS4xMC4yMDE0IOKEliDQnNCc0JItNy0zLzU1OEAuPGJy +Pg0KPHA+DQo8cD4NCtCd0LDQu9C+0LPQvtC/0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INC4INC/ +0LvQsNGC0LXQu9GM0YnQuNC60LDQvNC4INGB0LHQvtGA0L7QsiDQv9GA0LjQt9C90LDRjtGC0YHR +jyDQvtGA0LPQsNC90LjQt9Cw0YbQuNC4INC4INGE0LjQt9C40YfQtdGB0LrQuNC1INC70LjRhtCw +LCDQvdCwINC60L7RgtC+0YDRi9GFINCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDQvdCw +0YHRgtC+0Y/RidC40Lwg0JrQvtC00LXQutGB0L7QvCDQstC+0LfQu9C+0LbQtdC90LAg0L7QsdGP +0LfQsNC90L3QvtGB0YLRjCDRg9C/0LvQsNGH0LjQstCw0YLRjCDRgdC+0L7RgtCy0LXRgtGB0YLQ +stC10L3QvdC+INC90LDQu9C+0LPQuCDQuCAo0LjQu9C4KSDRgdCx0L7RgNGLLjxicj4NCtCSINC/ +0L7RgNGP0LTQutC1LCDQv9GA0LXQtNGD0YHQvNC+0YLRgNC10L3QvdC+0Lwg0L3QsNGB0YLQvtGP +0YnQuNC8INCa0L7QtNC10LrRgdC+0LwsINGE0LjQu9C40LDQu9GLINC4INC40L3Ri9C1INC+0LHQ +vtGB0L7QsdC70LXQvdC90YvQtSDQv9C+0LTRgNCw0LfQtNC10LvQtdC90LjRjyDRgNC+0YHRgdC4 +0LnRgdC60LjRhSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC5INC40YHQv9C+0LvQvdGP0Y7RgiDQvtCx +0Y/Qt9Cw0L3QvdC+0YHRgtC4INGN0YLQuNGFINC+0YDQs9Cw0L3QuNC30LDRhtC40Lkg0L/QviDR +g9C/0LvQsNGC0LUg0L3QsNC70L7Qs9C+0LIg0Lgg0YHQsdC+0YDQvtCyINC/0L4g0LzQtdGB0YLR +gyDQvdCw0YXQvtC20LTQtdC90LjRjyDRjdGC0LjRhSDRhNC40LvQuNCw0LvQvtCyINC4INC40L3R +i9GFINC+0LHQvtGB0L7QsdC70LXQvdC90YvRhSDQv9C+0LTRgNCw0LfQtNC10LvQtdC90LjQuS48 +YnI+DQrQkiDRgdC70YPRh9Cw0Y/RhSwg0L/RgNC10LTRg9GB0LzQvtGC0YDQtdC90L3Ri9GFINC9 +0LDRgdGC0L7Rj9GJ0LjQvCDQmtC+0LTQtdC60YHQvtC8LCDQvdCw0LvQvtCz0L7Qv9C70LDRgtC1 +0LvRjNGJ0LjQutCw0LzQuCDQv9GA0LjQt9C90LDRjtGC0YHRjyDQuNC90L7RgdGC0YDQsNC90L3R +i9C1INGB0YLRgNGD0LrRgtGD0YDRiyDQsdC10Lcg0L7QsdGA0LDQt9C+0LLQsNC90LjRjyDRjtGA +0LjQtNC40YfQtdGB0LrQvtCz0L4g0LvQuNGG0LAuPHA+DQo8cD4NCtCh0YLQsNGC0YzRjyAyMC4g +0JLQt9Cw0LjQvNC+0LfQsNCy0LjRgdC40LzRi9C1INC70LjRhtCwPHA+DQoxLiDQktC30LDQuNC8 +0L7Qt9Cw0LLQuNGB0LjQvNGL0LzQuCDQu9C40YbQsNC80Lgg0LTQu9GPINGG0LXQu9C10Lkg0L3Q +sNC70L7Qs9C+0L7QsdC70L7QttC10L3QuNGPINC/0YDQuNC30L3QsNGO0YLRgdGPINGE0LjQt9C4 +0YfQtdGB0LrQuNC1INC70LjRhtCwINC4ICjQuNC70LgpINC+0YDQs9Cw0L3QuNC30LDRhtC40Lgs +INC+0YLQvdC+0YjQtdC90LjRjyDQvNC10LbQtNGDINC60L7RgtC+0YDRi9C80Lgg0LzQvtCz0YPR +giDQvtC60LDQt9GL0LLQsNGC0Ywg0LLQu9C40Y/QvdC40LUg0L3QsCDRg9GB0LvQvtCy0LjRjyDQ +uNC70Lgg0Y3QutC+0L3QvtC80LjRh9C10YHQutC40LUg0YDQtdC30YPQu9GM0YLQsNGC0Ysg0LjR +hSDQtNC10Y/RgtC10LvRjNC90L7RgdGC0Lgg0LjQu9C4INC00LXRj9GC0LXQu9GM0L3QvtGB0YLQ +uCDQv9GA0LXQtNGB0YLQsNCy0LvRj9C10LzRi9GFINC40LzQuCDQu9C40YYsINCwINC40LzQtdC9 +0L3Qvjo8YnI+DQoxKSDQvtC00L3QsCDQvtGA0LPQsNC90LjQt9Cw0YbQuNGPINC90LXQv9C+0YHR +gNC10LTRgdGC0LLQtdC90L3QviDQuCAo0LjQu9C4KSDQutC+0YHQstC10L3QvdC+INGD0YfQsNGB +0YLQstGD0LXRgiDQsiDQtNGA0YPQs9C+0Lkg0L7RgNCz0LDQvdC40LfQsNGG0LjQuCwg0Lgg0YHR +g9C80LzQsNGA0L3QsNGPINC00L7Qu9GPINGC0LDQutC+0LPQviDRg9GH0LDRgdGC0LjRjyDRgdC+ +0YHRgtCw0LLQu9GP0LXRgiDQsdC+0LvQtdC1IDIwINC/0YDQvtGG0LXQvdGC0L7Qsi4g0JTQvtC7 +0Y8g0LrQvtGB0LLQtdC90L3QvtCz0L4g0YPRh9Cw0YHRgtC40Y8g0L7QtNC90L7QuSDQvtGA0LPQ +sNC90LjQt9Cw0YbQuNC4INCyINC00YDRg9Cz0L7QuSDRh9C10YDQtdC3INC/0L7RgdC70LXQtNC+ +0LLQsNGC0LXQu9GM0L3QvtGB0YLRjCDQuNC90YvRhSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC5INC+ +0L/RgNC10LTQtdC70Y/QtdGC0YHRjyDQsiDQstC40LTQtSDQv9GA0L7QuNC30LLQtdC00LXQvdC4 +0Y8g0LTQvtC70LXQuSDQvdC10L/QvtGB0YDQtdC00YHRgtCy0LXQvdC90L7Qs9C+INGD0YfQsNGB +0YLQuNGPINC+0YDQs9Cw0L3QuNC30LDRhtC40Lkg0Y3RgtC+0Lkg0L/QvtGB0LvQtdC00L7QstCw +0YLQtdC70YzQvdC+0YHRgtC4INC+0LTQvdCwINCyINC00YDRg9Cz0L7QuTs8YnI+DQoyKSDQvtC0 +0L3QviDRhNC40LfQuNGH0LXRgdC60L7QtSDQu9C40YbQviDQv9C+0LTRh9C40L3Rj9C10YLRgdGP +INC00YDRg9Cz0L7QvNGDINGE0LjQt9C40YfQtdGB0LrQvtC80YMg0LvQuNGG0YMg0L/QviDQtNC+ +0LvQttC90L7RgdGC0L3QvtC80YMg0L/QvtC70L7QttC10L3QuNGOOzxicj4NCjMpINC70LjRhtCw +INGB0L7RgdGC0L7Rj9GCINCyINGB0L7QvtGC0LLQtdGC0YHRgtCy0LjQuCDRgSDRgdC10LzQtdC5 +0L3Ri9C8INC30LDQutC+0L3QvtC00LDRgtC10LvRjNGB0YLQstC+0Lwg0KDQvtGB0YHQuNC50YHQ +utC+0Lkg0KTQtdC00LXRgNCw0YbQuNC4INCyINCx0YDQsNGH0L3Ri9GFINC+0YLQvdC+0YjQtdC9 +0LjRj9GFLCDQvtGC0L3QvtGI0LXQvdC40Y/RhSDRgNC+0LTRgdGC0LLQsCDQuNC70Lgg0YHQstC+ +0LnRgdGC0LLQsCwg0YPRgdGL0L3QvtCy0LjRgtC10LvRjyDQuCDRg9GB0YvQvdC+0LLQu9C10L3Q +vdC+0LPQviwg0LAg0YLQsNC60LbQtSDQv9C+0L/QtdGH0LjRgtC10LvRjyDQuCDQvtC/0LXQutCw +0LXQvNC+0LPQvi48cD4NCjIuINCh0YPQtCDQvNC+0LbQtdGCINC/0YDQuNC30L3QsNGC0Ywg0LvQ +uNGG0LAg0LLQt9Cw0LjQvNC+0LfQsNCy0LjRgdC40LzRi9C80Lgg0L/QviDQuNC90YvQvCDQvtGB +0L3QvtCy0LDQvdC40Y/QvCwg0L3QtSDQv9GA0LXQtNGD0YHQvNC+0YLRgNC10L3QvdGL0Lwg0L/R +g9C90LrRgtC+0LwgMSDQvdCw0YHRgtC+0Y/RidC10Lkg0YHRgtCw0YLRjNC4LCDQtdGB0LvQuCDQ +vtGC0L3QvtGI0LXQvdC40Y8g0LzQtdC20LTRgyDRjdGC0LjQvNC4INC70LjRhtCw0LzQuCDQvNC+ +0LPRg9GCINC/0L7QstC70LjRj9GC0Ywg0L3QsCDRgNC10LfRg9C70YzRgtCw0YLRiyDRgdC00LXQ +u9C+0Log0L/QviDRgNC10LDQu9C40LfQsNGG0LjQuCDRgtC+0LLQsNGA0L7QsiAo0YDQsNCx0L7R +giwg0YPRgdC70YPQsykuPGJyPg0KPGJyPg0KPGJyPg0K0J3QtdC00LDQstC90L4gwqvRhtC40YTR +gNC+0LLQsNGPINGN0LrQvtC90L7QvNC40LrQsMK7INC+0LTQtdGA0LbQsNC70LAg0LLQsNC20L3R +g9GOINC/0L7QsdC10LTRgywg0LLRi9C60LjQvdGD0LIg0LjQtyDQv9GP0YLQtdGA0LrQuCDRgdCw +0LzRi9GFINC00L7RgNC+0LPQuNGFINC60L7QvNC/0LDQvdC40Lkg0LzQuNGA0LAg0L/QvtGB0LvQ +tdC00L3QtdCz0L4g0L/RgNC10LTRgdGC0LDQstC40YLQtdC70Y8gwqvRgNC10LDQu9GM0L3QvtCz +0L4g0YHQtdC60YLQvtGA0LDCuyBFeHhvbk1vYmlsLiDQndC+INC/0L7QutCwINCx0LjRgtCy0LAg +wqvRgdC10YDQstC10YDQsCDRgdC+INGB0YLQsNC90LrQvtC8wrsg0L/RgNC40LLQvtC00LjRgiDQ +uiDRgtC+0YDQvNC+0LbQtdC90LjRjiDRjdC60L7QvdC+0LzQuNGH0LXRgdC60L7Qs9C+INGA0L7R +gdGC0LAuINCe0YfQtdCy0LjQtNC90L4sINGH0YLQviDQtNC70Y8g0L7QutC+0L3Rh9Cw0YLQtdC7 +0YzQvdC+0LPQviDRhNC+0YDQvNC40YDQvtCy0LDQvdC40Y8g0L3QvtCy0L7Qs9C+INGN0LrQvtC9 +0L7QvNC40YfQtdGB0LrQvtCz0L4g0YPQutC70LDQtNCwINC80LjRgNGDINC/0YDQuNC00LXRgtGB +0Y8g0L/QtdGA0LXQttC40YLRjCDQtdGJ0LUg0L3QtSDQvtC00L3QviDQv9C+0YLRgNGP0YHQtdC9 +0LjQtS48cD4NCiA8YnI+DQo8cD4NCjxwPg0KPHA+DQog0J/Rj9GC0LXRgNC60YMg0LrRgNGD0L/Q +vdC10LnRiNC40YUg0L/QviDRgNGL0L3QvtGH0L3QvtC5INGB0YLQvtC40LzQvtGB0YLQuCDQutC+ +0LzQv9Cw0L3QuNC5INC+0LrQutGD0L/QuNGA0L7QstCw0LvQuCBJVC3Qs9C40LPQsNC90YLRizxi +cj4NCtCd0LXRhNGC0Y/QvdC40LrQvtCyINGB0LzQtdC90LjQu9C4INGC0LXRhdC90L7Qu9C+0LPQ +uNC4PGJyPg0K0J3QsCDRgdC80LXQvdGDINGA0LXRgdGD0YDRgdC90L7QuSDRjdC60L7QvdC+0LzQ +uNC60LUg0L/RgNC40YXQvtC00LjRgiDRgtC10YXQvdC+0LvQvtCz0LjRh9C10YHQutCw0Y8uINCV +0YHQu9C4INGA0LDQvdGM0YjQtSDQsiDRgtC+0L8tNSDQu9C40LTQtdGA0L7QsiDQv9C+INC60LDQ +v9C40YLQsNC70LjQt9Cw0YbQuNC4IElULdCz0LjQs9Cw0L3RgtGLINC/0L7Qv9Cw0LTQsNC70Lgg +0LvQuNGI0Ywg0LjQt9GA0LXQtNC60LAsINGC0L4g0YLQtdC/0LXRgNGMINCy0YHRji4uLiDihpI8 +YnI+DQrQndC+INC90Lgg0LIg0L7QtNC90L7QvCDQsdCw0L3QutC+0LzQsNGC0LUg0YHQvdGP0YLR +jCDQtNC10L3QtdCzINC90LUg0L/QvtC70YPRh9C40LvQvtGB0YwuINCi0LXQu9C10YTQvtC9INCx +0LDQvdC60LAg0L3QtSDQvtGC0LLQtdGH0LDQuywg0YHQsNC50YIg0LLQuNGB0LXQuy4g0KfQtdGA +0LXQtyDQv9C+0LvRh9Cw0YHQsCDQstGL0Y/RgdC90LjQu9C+0YHRjCwg0YfRgtC+INCx0LDQvdC6 +0L7QstGB0LrQuNC1INGB0LjRgdGC0LXQvNGLINGA0YPRhdC90YPQu9C4LCDQsCDRgdGH0LXRgtCw +INC00LXRgdGP0YLQutC+0LIg0LzQuNC70LvQuNC+0L3QvtCyINCy0LrQu9Cw0LTRh9C40LrQvtCy +INC+0LHQvdGD0LvQtdC90YsuINCU0L7Qu9C70LDRgCwg0YTRg9C90YIg0Lgg0LXQstGA0L4g0L/R +gNC10LLRgNCw0YLQuNC70LjRgdGMINCyINC/0YvQu9GMLCDQt9Cw0YLQviDRgtC1LCDQutGC0L4g +0LfQsNC/0LDRgdCw0LvRgdGPINC30L7Qu9C+0YLQvtC8INC4INC/0L7QutGD0L/QsNC7INC60YDQ +uNC/0YLQvtCy0LDQu9GO0YLRiywg0L/QvtGC0LjRgNCw0LvQuCDRgNGD0LrQuC4g0KHRgtGA0LDR +hSDQuCDRgNCw0YHRgtC10YDRj9C90L3QvtGB0YLRjCDigJQg0LLQvtGCINC00LLQsCDQs9C70LDQ +stC90YvRhSDRgdC70L7QstCwLCDQutC+0YLQvtGA0YvQtSDQsdGL0LvQuCDQsiDQt9Cw0LPQvtC7 +0L7QstC60LDRhSDQvdC+0LLQvtGB0YLQvdGL0YUg0YHQvtC+0LHRidC10L3QuNC5INCy0YHQtdGF +INC40L3RhNC+0YDQvNCw0LPQtdC90YLRgdGC0LIuINCd0L7QstGL0Lkg0LTQuNCy0L3Ri9C5INC8 +0LjRgCDQstGB0YLRgNC10YfQsNC7INGB0LLQvtC40YUg0LjRgdC/0YPQs9Cw0L3QvdGL0YUg0L7Q +sdC40YLQsNGC0LXQu9C10LnigKY8cD4NCjxicj4NCtCa0L7QvdC10YfQvdC+LCDQstC10YHRjNC8 +0LAg0LLQtdGA0L7Rj9GC0L3Qviwg0YfRgtC+INGB0YbQtdC90LDRgNC40LkgwqvRhNC40LvRjNC8 +0LAt0LrQsNGC0LDRgdGC0YDQvtGE0YvCuyDQsiDQttC40LfQvdC4INC90LjQutC+0LPQtNCwINC9 +0LUg0LHRg9C00LXRgiDRgNC10LDQu9C40LfQvtCy0LDQvS4g0J3QviDQstC+0YIg0YfRgtC+INC9 +0LDQtNC+INGH0LXRgtC60L4g0L/QvtC90LjQvNCw0YLRjDog0L3QvtCy0YvQuSDQvNC40YAsINC9 +0L7QstCw0Y8g0YbQuNGE0YDQvtCy0LDRjyDRgNC10LDQu9GM0L3QvtGB0YLRjCDRg9C20LUg0L/R +gNC40LHQuNGA0LDQtdGCINC6INGA0YPQutCw0Lwg0L7QutGA0YPQttCw0Y7RidC40Lkg0LzQuNGA +Ljxicj4NCjxwPg0K0J3QtdC00LDQstC90L4g0LLQv9C10YDQstGL0LUg0LIg0LjRgdGC0L7RgNC4 +0Lgg0L/Rj9GC0LXRgNC60LAg0YHQsNC80YvRhSDQtNC+0YDQvtCz0LjRhSDQutC+0LzQv9Cw0L3Q +uNC5INC80LjRgNCwINGB0YLQsNC70LAg0LjRgdC60LvRjtGH0LjRgtC10LvRjNC90L4gwqvRhtC4 +0YTRgNC+0LLQvtC5wrsuINCf0L7RgdC70LXQtNC90LjQuSDQv9GA0LXQtNGB0YLQsNCy0LjRgtC1 +0LvRjCDCq9GB0YLQsNGA0L7Qs9C+INC80LjRgNCwwrsg4oCUINC90LXRhNGC0Y/QvdC+0Lkg0LPQ +uNCz0LDQvdGCIEV4eG9uTW9iaWwg0LHRi9C7INCy0YvRgtC10YHQvdC10L0g0YEg0L/Rj9GC0L7Q +s9C+INC80LXRgdGC0LAg0LjQvdGC0LXRgNC90LXRgi3QvNCw0LPQsNC30LjQvdC+0LwgQW1hem9u +INC4INGB0L7RhtGB0LXRgtGM0Y4gRmFjZWJvb2suINCf0LXRgNCy0YvQtSDRgtGA0Lgg0L/QvtC3 +0LjRhtC40Lgg0L/RgNC40L3QsNC00LvQtdC20LDRgiBBcHBsZSwgQWxwaGFiZXQgKNC80LDRgtC1 +0YDQuNC90YHQutCw0Y8g0LrQvtC80L/QsNC90LjRjyBHb29nbGUpINC4IE1pY3Jvc29mdC4gwqvQ +r9Cx0LvQvtGH0L3Ri9C5wrsg0LvQuNC00LXRgCDRgdGC0L7QuNGCINC+0LrQvtC70L4gJDU3MCDQ +vNC70YDQtC48YnI+DQo8cD4NCtCf0YDQtdC40LzRg9GJ0LXRgdGC0LLQviBJVC3RgdC10LrRgtC+ +0YDQsCDQvdCw0YDQsNGB0YLQsNC10YIg0YEg0LrQvtC90YbQsCDQtNC10LLRj9C90L7RgdGC0YvR +hSDQs9C+0LTQvtCyINC/0YDQvtGI0LvQvtCz0L4g0LLQtdC60LAuINCi0L7Qs9C00LAg0L/RgNC+ +0LjQt9C+0YjQtdC7INGE0LDQu9GM0YHRgtCw0YDRgiwg0LLRi9C70LjQstGI0LjQudGB0Y8g0LIg +0LrRgNC40LfQuNGBINC00L7RgtC60L7QvNC+0LIsINGH0YPRgtGMINCx0YvQu9C+INC90LUg0L/Q +vtCz0YDRg9C30LjQstGI0LjQuSDQsNC80LXRgNC40LrQsNC90YHQutGD0Y4g0Y3QutC+0L3QvtC8 +0LjQutGDINCyINGA0LXRhtC10YHRgdC40Y4g0LIg0L3QsNGH0LDQu9C1IDIwMDAt0YUg0LPQvtC0 +0L7Qsi4g0J3QviDQuCDRgdC10LnRh9Cw0YEg0L3QvtCy0LDRjyDRhtC40YTRgNC+0LLQsNGPINGN +0YDQsCDQvdC40LrQsNC6INC90LUg0LLRi9Cy0LXQtNC10YIg0Y3QutC+0L3QvtC80LjQutGDINC9 +0LAg0YLRgNCw0LXQutGC0L7RgNC40Y4g0YPRgdGC0L7QudGH0LjQstC+0LPQviDRgNC+0YHRgtCw +LjxwPg0KPHA+DQrQkdC+0LvQtdC1INGC0L7Qs9C+LCDRgdGA0LXQtNC90LjQtSDRgtC10LzQv9GL +INGA0L7RgdGC0LAg0JLQktCfINCh0L7QtdC00LjQvdC10L3QvdGL0YUg0KjRgtCw0YLQvtCyICjQ +uNC80LXQvdC90L4g0LIg0Y3RgtC+0Lkg0YHRgtGA0LDQvdC1INCx0LDQt9C40YDRg9C10YLRgdGP +INCx0L7Qu9GM0YjQuNC90YHRgtCy0L4g0LrRgNGD0L/QvdC10LnRiNC40YUg0LLRi9GB0L7QutC+ +0YLQtdGF0L3QvtC70L7Qs9C40YfQvdGL0YUg0LrQvtGA0L/QvtGA0LDRhtC40LkpINCyIFhYSSDQ +stC10LrQtSDQvdCw0YXQvtC00Y/RgtGB0Y8g0L3QsCDQvNC40L3QuNC80LDQu9GM0L3QvtC8INGD +0YDQvtCy0L3QtSDQv9C+INGB0YDQsNCy0L3QtdC90LjRjiDRgdC+INCy0YLQvtGA0L7QuSDQv9C+ +0LvQvtCy0LjQvdC+0LkgWFgg0LLQtdC60LAuPHA+DQo8cD4NCtCSIDIwMDHigJMyMDE1INCz0L7Q +tNCw0YUg0LDQvNC10YDQuNC60LDQvdGB0LrQsNGPINGN0LrQvtC90L7QvNC40LrQsCDRgNC+0YHQ +u9CwINC90LAgMSw4JSDQsiDRgdGA0LXQtNC90LXQvCDQt9CwINCz0L7QtC4g0KDQvtGB0YIg0LIg +0L/RgNC10LTRi9C00YPRidC40LUg0LTQstC1INC/0Y/RgtC90LDQtNGG0LDRgtC40LvQtdGC0LrQ +uCDRgdC+0YHRgtCw0LLQu9GP0LsgMyw0INC4IDMsMyUsINCwINC00L4g0Y3RgtC+0LPQviDQsdGL +0Lsg0LXRidC1INCy0YvRiNC1ICjQt9C00LXRgdGMINC4INC00LDQu9C10LUg4oCUINC00LDQvdC9 +0YvQtSDQktGB0LXQvNC40YDQvdC+0LPQviDQsdCw0L3QutCwKS48YnI+DQo8cD4NCtCYINGN0YLQ +viDQvdC1INCy0YHQtSDQsNC90YLQuNGA0LXQutC+0YDQtNGLLiDQotC10LzQv9GLINGA0L7RgdGC +0LAg0LfQsCDQv9C+0YHQu9C10LTQvdC40LUg0L/Rj9GC0L3QsNC00YbQsNGC0Ywg0LvQtdGCINC9 +0Lgg0YDQsNC30YMg0L3QtSDQv9GA0LXQstGL0YjQsNC70LggNCUgKNC80LDQutGB0LjQvNGD0Lwg +4oCUIDMsOCUg4oCUINCx0YvQuyDQv9C+0LrQsNC30LDQvSDQsiAyMDA0INCz0L7QtNGDKSwg0YLQ +vtCz0LTQsCDQutCw0Log0YDQsNC90LXQtSDRjdGC0LggNCUg0LHRi9C70Lgg0L7QsdGL0YfQvdGL +0Lwg0LTQtdC70L7QvCAo0L/QuNC6INCyIDcsMjYlINCx0YvQuyDQv9C+0LrQsNC30LDQvSDQsiAx +OTg0INCz0L7QtNGDKS48cD4NCjxwPg0KPHA+DQrQk9C70YPQsdC40L3QsCDQv9Cw0LTQtdC90LjR +jyDRjdC60L7QvdC+0LzQuNC60Lgg0LIgMjAwOSDQs9C+0LTRgyAo4oCTMiw3OCUpINGB0YLQsNC7 +0LAg0LzQsNC60YHQuNC80LDQu9GM0L3QvtC5INGBIDE5NjEg0LPQvtC00LAuINCf0YDQuCDRjdGC +0L7QvCDQvNCw0YHRiNGC0LDQsdGLIMKr0L7RgtGB0LrQvtC60LDCuyDQv9C+0YHQu9C1INGB0L/Q +sNC00LAgKCsyLDUzJSDQsiAyMDEwINCz0L7QtNGDKSDQsdGL0LvQuCDQvNC40L3QuNC80LDQu9GM +0L3Ri9C80Lgg0LfQsCDQstC10YHRjCDQvdCw0LHQu9GO0LTQsNC10LzRi9C5INC/0LXRgNC40L7Q +tC4g0JIg0L7QsdGJ0LXQvCwg0L3QtSDQt9GA0Y8g0L/QvtGB0LvQtdC00L3QuNC5INC60YDQuNC3 +0LjRgSDQv9C+0LvRg9GH0LjQuyDQsiDQqNGC0LDRgtCw0YUg0L3QsNC30LLQsNC90LjQtSDCq9CS +0LXQu9C40LrQsNGPINGA0LXRhtC10YHRgdC40Y/CuyAoR3JlYXQgUmVjZXNzaW9uKS48cD4NCjxi +cj4NCiDQm9Cw0LfQtdGA0Ysg0Lgg0LTRgNC+0L3RiyDQvtGCIEZhY2Vib29rINC4INC00YDRg9Cz +0LjQtSDRgdC/0L7RgdC+0LHRiyDQvtCx0LXRgdC/0LXRh9C40YLRjCDQsdC10LTQvdGL0LUg0YHR +gtGA0LDQvdGLINC40L3RgtC10YDQvdC10YLQvtC8PGJyPg0K0JvQsNC30LXRgNGLINC90LAg0LHQ +tdC00L3QvtGB0YLRjDxicj4NCtCW0LXQu9Cw0L3QuNC1INCc0LDRgNC60LAg0KbRg9C60LXRgNCx +0LXRgNCz0LAg0L/QvtC60YDRi9GC0Ywg0LjQvdGC0LXRgNC90LXRgtC+0Lwg0LLQtdGB0Ywg0LzQ +uNGAINC90LDQsdC40YDQsNC10YIg0L3QvtCy0YvQtSDQvtCx0L7RgNC+0YLRiyDigJQg0Log0LTR +gNC+0L3QsNC8LCDQutC+0YLQvtGA0YvQtSDQsdGD0LTRg9GCINC+0LHQtdGB0L/QtdGH0LjQstCw +0YLRjCDRgdC10YLRjCDQsiDRgtGA0YPQtNC90L7QtNC+0YHRgtGD0L/QvdGL0YUuLi4g4oaSPHA+ +DQrQn9GA0LXQt9C40LTQtdC90YLRgdGC0LLQviDQkdCw0YDQsNC60LAg0J7QsdCw0LzRiyDQvNC+ +0LbQvdC+INCy0L7QvtCx0YnQtSDRgdGH0LjRgtCw0YLRjCDRgdCw0LzRi9C8INC90LXRg9C00LDR +h9C90YvQvCDRgSDRgtC+0YfQutC4INC30YDQtdC90LjRjyDRjdC60L7QvdC+0LzQuNGH0LXRgdC6 +0LjRhSDRg9GB0L/QtdGF0L7Qsi4g0JfQsCDQv9GA0LXQtNGL0LTRg9GJ0LjQtSDRgdC10LzRjCDQ +u9C10YIg0JLQktCfINCh0KjQkCDRg9Cy0LXQu9C40YfQuNCy0LDQu9GB0Y8g0YHRgNC10LTQvdC1 +0LPQvtC00L7QstGL0LzQuCDRgtC10LzQv9Cw0LzQuCAxLDQlLiDQndC4INC/0YDQuCDQvtC00L3Q +vtC8INC/0YDQtdC30LjQtNC10L3RgtC1LCDQvdCw0YfQuNC90LDRjyDRgSDQlNC20L7QvdCwINCa +0LXQvdC90LXQtNC4LCDRgtCw0Log0LzQtdC00LvQtdC90L3QviDRjdC60L7QvdC+0LzQuNC60LAg +0L3QtSDRgNC+0YHQu9CwLjxwPg0KPGJyPg0K0KLQtdC60YPRidC40Lkg0LPQvtC0INC90LUg0L/R +gNC40L3QtdGB0LXRgiDQvdC40YfQtdCz0L4g0YXQvtGA0L7RiNC10LPQvi4g0JIg0L/QtdGA0LLQ +vtC8INC60LLQsNGA0YLQsNC70LUg0JLQktCfINCy0YvRgNC+0YEg0L3QsCAxLDElINCyINCz0L7Q +tNC+0LLQvtC8INC40YHRh9C40YHQu9C10L3QuNC4LCDQstC+INCy0YLQvtGA0L7QvCwg0L/QviDQ +v9C10YDQstC+0Lkg0L7RhtC10L3QutC1LCDQvdCwIDEsMiUuINCf0YDQvtCz0L3QvtC3INC90LAg +0LLQtdGB0Ywg0LPQvtC0IOKAlCAy4oCTMiwyJSwg0YfRgtC+INCx0YPQtNC10YIg0LzQtdC90YzR +iNC1INC/0YDQvtGI0LvQvtCz0L7QtNC90LjRhSAyLDQlLjxwPg0KPHA+DQrQp9C70LXQvSDRgdC+ +0LLQtdGC0LAg0YPQv9GA0LDQstC70Y/RjtGJ0LjRhSDQpNC10LTQtdGA0LDQu9GM0L3QvtC5INGA +0LXQt9C10YDQstC90L7QuSDRgdC40YHRgtC10LzRiyDQodCo0JAg0JTQttC10YDQvtC8INCf0LDR +g9GN0LvQuyDQsiDQuNC90YLQtdGA0LLRjNGOIEZpbmFuY2lhbCBUaW1lcyDQt9Cw0Y/QstC40Lss +INGH0YLQviDQtdGB0YLRjCDRgNC40YHQuiDQstGC0Y/Qs9C40LLQsNC90LjRjyDQsiDQtNC70LjR +gtC10LvRjNC90YvQuSDQv9C10YDQuNC+0LQg0YHQu9Cw0LHQvtCz0L4g0YDQvtGB0YLQsC48YnI+ +DQo8cD4NCtCd0LDQtNC+INGC0LDQutC20LUg0YPRh9C40YLRi9Cy0LDRgtGMLCDRh9GC0L4g0Y3R +gtC+0YIg0LzQuNC90LjQvNCw0LvRjNC90YvQuSDRgNC+0YHRgiDQv9C+0YHQu9C10LTQvdC40YUg +0LvQtdGCINGB0L7Qv9GA0L7QstC+0LbQtNCw0LXRgtGB0Y8g0YPQstC10LvQuNGH0LXQvdC40LXQ +vCDQtNC+0LvQs9CwLiDQk9C+0YHRg9C00LDRgNGB0YLQstC10L3QvdGL0Lkg0LTQvtC70LMg0LLR +i9GA0L7RgSDQt9CwINCy0L7RgdC10LzRjCDQu9C10YIg0L/QvtGH0YLQuCDQsiDQtNCy0LAg0YDQ +sNC30LAg0Lgg0YHQtdC50YfQsNGBINC/0YDQtdCy0YvRiNCw0LXRgiAxMDAlINCS0JLQnyDQodCo +0JAg0Lgg0YHQvtGB0YLQsNCy0LvRj9C10YIgJDE5LDQg0YLRgNC70L0uINCSINGN0YLQvtC8INCz +0L7QtNGDLCDQv9C+INC+0YbQtdC90LrQtSDQsNC80LXRgNC40LrQsNC90YHQutC+0LPQviDQvNC4 +0L3RhNC40L3QsCwg0L7QvSDQstGL0YDQsNGB0YLQtdGCINC10YnQtSDQvdCwICQzODMg0LzQu9GA +0LQuPGJyPg0KPGJyPg0K0J7QtNC90L7QstGA0LXQvNC10L3QvdC+INCk0KDQoSDRg9C00LXRgNC2 +0LjQstCw0LXRgiDQutC70Y7Rh9C10LLRg9GOINGB0YLQsNCy0LrRgyDQvdCwINC80LjQvdC40LzQ +sNC70YzQvdC+0Lwg0YPRgNC+0LLQvdC1INC90LjQttC1IDElINGBINC00LXQutCw0LHRgNGPIDIw +MDgg0LPQvtC00LAuINCX0LAg0Y3RgtC+INCy0YDQtdC80Y8g0LHRi9C70L4g0YDQtdCw0LvQuNC3 +0L7QstCw0L3QviDRgtGA0Lgg0YDQsNGD0L3QtNCwINC/0YDQvtCz0YDQsNC80LzRiyDQutC+0LvQ +uNGH0LXRgdGC0LLQtdC90L3QvtCz0L4g0YHQvNGP0LPRh9C10L3QuNGPIChRRSksINC60L7RgtC+ +0YDQsNGPINC30LDQstC10YDRiNC40LvQsNGB0Ywg0LIg0L7QutGC0Y/QsdGA0LUgMjAxNCDQs9C+ +0LTQsCAo0LIg0YLRgNC10YLRjNC10Lwg0YDQsNGD0L3QtNC1INCyINC80LXRgdGP0YYg0KTQoNCh +INC/0L7QutGD0L/QsNC7INGDINCx0LDQvdC60L7QsiDQs9C+0YHRg9C00LDRgNGB0YLQstC10L3Q +vdGL0YUg0Lgg0LjQv9C+0YLQtdGH0L3Ri9GFINC+0LHQu9C40LPQsNGG0LjQuSDQvdCwICQ4NSDQ +vNC70YDQtCkuPGJyPg0KPHA+DQrQndC+LCDQv9C+0LTRh9C10YDQutC90LXQvCDQtdGJ0LUg0YDQ +sNC3LCDQvdC10YHQvNC+0YLRgNGPINC90LAg0LLRgdC1INGN0YLQuCDRjdC60YHRgtGA0LDQvtGA +0LTQuNC90LDRgNC90YvQtSDQvNC10YDRiywg0YLQtdC80L/RiyDRgNC+0YHRgtCwINC/0YDQtdCx +0YvQstCw0Y7RgiDQvdCwINC80L3QvtCz0L7Qu9C10YLQvdC40YUg0LzQuNC90LjQvNGD0LzQsNGF +LCDQsCDQstGB0Y8g0Y3RgtCwINC40YHRgtC+0YDQuNGPINCy0LXQu9C40LrQvtCz0L4g0YLQvtGA +0LzQvtC20LXQvdC40Y8g0L/RgNC+0LjRgdGF0L7QtNC40YIg0L3QsCDRhNC+0L3QtSDRgNC10LfQ +utC+0LPQviDRg9GB0LjQu9C10L3QuNGPINGA0L7Qu9C4IElULdGB0LXQutGC0L7RgNCwLCDQsCDR +gtCw0LrQttC1INGA0L7RgdGC0LAg0YTQuNC90LDQvdGB0L7QstGL0YUg0YDRi9C90LrQvtCyLjxw +Pg0KPHA+DQrQmtC70Y7Rh9C10LLQvtC5INCx0LjRgNC20LXQstC+0Lkg0LjQvdC00LXQutGBIFMm +UDUwMCDQvdCw0YXQvtC00LjRgtGB0Y8g0L3QsCDQuNGB0YLQvtGA0LjRh9C10YHQutC40YUg0LzQ +sNC60YHQuNC80YPQvNCw0YUuINCQ0L3QsNC70LjRgtC40LrQuCBHb2xkbWFuIFNhY2hzINC/0YDQ +tdC00YPQv9GA0LXQttC00LDRjtGCOiDQutC+0L3QtdGGINGA0LDQu9C70Lgg0LHQu9C40LfQvtC6 +LiDQn9GA0LXQtNGL0LTRg9GJ0LjQtSDQv9C10YDQuNC+0LTRiyDRgNC+0YHRgtCwICjQsiAxOTg0 +4oCTMTk4NyDQuCAxOTk04oCTMTk5OSDQs9C+0LTQsNGFKSDQt9Cw0LrQsNC90YfQuNCy0LDQu9C4 +0YHRjCDQvtCx0LLQsNC70L7QvCDQutC+0YLQuNGA0L7QstC+0LouPHA+DQo8cD4NCtCi0L4sINGH +0YLQviDQv9GA0L7QuNGB0YXQvtC00LjRgiDQsiDQodCo0JAsINCyINGC0L7QuSDQuNC70Lgg0LjQ +vdC+0Lkg0YHRgtC10L/QtdC90Lgg0YXQsNGA0LDQutGC0LXRgNC90L4g0Lgg0LTQu9GPINC00YDR +g9Cz0LjRhSDRgNCw0LfQstC40YLRi9GFINGB0YLRgNCw0L06INCy0LDQuyDQtNC+0LvQs9C+0LIs +INC90LjQt9C60LjQtSDRgtC10LzQv9GLINGA0L7RgdGC0LAsINC+0YLRgdGD0YLRgdGC0LLQuNC1 +INC40L3RhNC70Y/RhtC40LgsINC/0L7RgdGC0LXQv9C10L3QvdC+0LUg0YHQvtC60YDQsNGJ0LXQ +vdC40LUg0YHRgNC10LTQvdC10LPQviDQutC70LDRgdGB0LAg0Lgg0LrQvtC90YbQtdC90YLRgNCw +0YbQuNGPINCx0L7Qs9Cw0YLRgdGC0LLQsCDQsiDRgNGD0LrQsNGFINC+0LPRgNCw0L3QuNGH0LXQ +vdC90L7Qs9C+INC60YDRg9Cz0LAg0LvRjtC00LXQuS4g0J/RgNC4INGN0YLQvtC8INC40LfQvNC1 +0L3QtdC90LjQtSDRgdC40YLRg9Cw0YbQuNC4INGBINC/0L7QvNC+0YnRjNGOINC80LXRgCDRjdC6 +0L7QvdC+0LzQuNGH0LXRgdC60L7QuSDQuCDQtNC10L3QtdC20L3Qvi3QutGA0LXQtNC40YLQvdC+ +0Lkg0L/QvtC70LjRgtC40LrQuCDQvdC10LLQvtC30LzQvtC20L3Qvi4g0K3RgtC+INC90LUg0YHR +h9C40YLQsNGPIMKr0YfQtdGA0L3Ri9GFINC70LXQsdC10LTQtdC5wrsg0LLRgNC+0LTQtSBCcmV4 +aXQsINC60L7RgtC+0YDRi9C1INGB0LXRjtGCINGB0LzRj9GC0LXQvdC40LUg0Lgg0YXQsNC+0YEg +0L3QsCDRgNGL0L3QutCw0YUuPHA+DQo8YnI+DQrQlNC+0LvQs9C+0LUg0LLRgNC10LzRjyDQvNC4 +0YAg0LLRi9C/0LvRi9Cy0LDQuyDQt9CwINGB0YfQtdGCINCx0YPRgNC90L7Qs9C+INGA0L7RgdGC +0LAg0LIg0YHRgtGA0LDQvdCw0YUg0YLRgNC10YLRjNC10LPQviDQvNC40YDQsCwg0LrQvtGC0L7R +gNGL0LUg0Y3QutGB0L/QvtGA0YLQuNGA0L7QstCw0LvQuCDRgdGL0YDRjNC1INC/0L4g0LLRi9GB +0L7QutC40Lwg0YbQtdC90LDQvCwg0L/QvtC60YPQv9Cw0LvQuCDRgtC+0LLQsNGA0Ysg0Lgg0YLQ +tdGF0L3QvtC70L7Qs9C40Lgg0Lgg0YDQsNC30YDQtdGI0LDQu9C4INC+0YLQutGA0YvQstCw0YLR +jCDRgyDRgdC10LHRjyDQv9GA0L7QuNC30LLQvtC00YHRgtCy0LAg0YLRgNCw0L3RgdC90LDRhtC4 +0L7QvdCw0LvRjNC90YvQvCDQutC+0YDQv9C+0YDQsNGG0LjRj9C8LCDRgdC90LDQsdC20LDRjyDQ +uNGFINC00LXRiNC10LLQvtC5INGA0LDQsdC+0YfQtdC5INGB0LjQu9C+0LkuINCd0L4g0YHQtdCz +0L7QtNC90Y8g0L7QvdC4INGC0LDQutC20LUg0LIg0LrRgNC40LfQuNGB0LUuPGJyPg0KPGJyPg0K +INCV0LvQuNC30LDQstC10YLQsCDQkNC70LXQutGB0LDQvdC00YDQvtCy0LAt0JfQvtGA0LjQvdCw +INC+INGC0L7QvCwg0LPQvtGC0L7QstC+INC70Lgg0YfQtdC70L7QstC10YfQtdGB0YLQstC+INC6 +INCz0LXQvdC10YLQuNGH0LXRgdC60L7QuSDQuCDRgtC10YXQvdC+0LvQvtCz0LjRh9C10YHQutC+ +0LkgwqvRgNC10LTQsNC60YLRg9GA0LXCuzxicj4NCtCn0LXQu9C+0LLQtdC6INC90L7QstGL0Lks +INGD0LvRg9GH0YjQtdC90L3Ri9C5PHA+DQrQniDQvdC+0LLQvtC8INGH0LXQu9C+0LLQtdC60LUs +IGwnaG9tbWUgbm91dmVhdSwg0LzQtdGH0YLQsNC70Lgg0YHQtdCy0LXRgNC+0LDQvNC10YDQuNC6 +0LDQvdGB0LrQuNC1INC/0L7RgdC10LvQtdC90YbRiywg0L3QsNGG0LjRgdGC0Ysg0Lgg0LrQvtC8 +0LzRg9C90LjRgdGC0YssINCi0YDQvtGG0LrQuNC5LCDQp9C1INCT0LXQstCw0YDQsCDQuCDQvNC9 +0L7Qs9C40LUg0LTRgNGD0LPQuNC1LiDQoNC10YfRjCDRiNC70LAg0L7QsS4uLiDihpI8YnI+DQrQ +otCw0Log0L/QvtGH0LXQvNGDINC20LUgwqvRhtC40YTRgNCwwrsg0YLQsNC6INC4INC90LUg0YHR +gtCw0LvQsCDRgtC10Lwg0YHQsNC80YvQvCDQtNGA0LDQudCy0LXRgNC+0LwsINC60L7RgtC+0YDR +i9C5INCy0LXRgNC90YPQuyDQsdGLINGN0LrQvtC90L7QvNC40LrRgyDQvdCwINGC0YDQsNC10LrR +gtC+0YDQuNGOINCy0YvRgdC+0LrQvtCz0L4g0Lgg0YPRgdGC0L7QudGH0LjQstC+0LPQviDRgNC+ +0YHRgtCwPyDQrdGC0L7QvNGDINC80LXRiNCw0Y7RgiDRgdGC0LDRgNGL0LUg0LjQvdGB0YLQuNGC +0YPRgtGLINC4INC40L3RhNGA0LDRgdGC0YDRg9C60YLRg9GA0LAsINGF0L7RgNC+0YjQviDQv9GA +0LjRgdC/0L7RgdC+0LHQu9C10L3QvdGL0LUg0LTQu9GPINGC0YDQsNC00LjRhtC40L7QvdC90L7Q +uSDRjdC60L7QvdC+0LzQuNC60Lgg0YEg0LXQtSDQs9C70LDQstC10L3RgdGC0LLRg9GO0YnQtdC5 +INGA0L7Qu9GM0Y4gwqvRgNC10LDQu9GM0L3QvtCz0L4g0YHQtdC60YLQvtGA0LDCuyDQuCDQsdCw +0L3QutC+0LLRgdC60L7Qs9C+INC60YDQtdC00LjRgtCwINC60LDQuiDQtNGA0LDQudCy0LXRgNCw +INC40L3QstC10YHRgtC40YbQuNC5INC4INC/0L7RgtGA0LXQsdC70LXQvdC40Y8uPHA+DQo8cD4N +CtCd0LAg0YHQvNC10L3RgyDRgdGC0LDRgNGL0Lwg0LLQsNC70Y7RgtCw0LwsINCx0LDQvdC60LDQ +vCDQuCDQsdC40YDQttCw0LwsINGD0LPQu9C10LLQvtC00L7RgNC+0LTQvdC+0Lkg0Y3QvdC10YDQ +s9C10YLQuNC60LUg0Lgg0YLRgNCw0LTQuNGG0LjQvtC90L3Ri9C8INGE0LDQsdGA0LjQutCw0Lwg +0YPQttC1INC40LTRg9GCINC90L7QstGL0LUg0YLQtdGF0L3QvtC70L7Qs9C40LguINCd0LDQv9GA +0LjQvNC10YAsINCx0LvQvtC60YfQtdC50L0g0Lgg0LHQuNGC0LrQvtC40L3Riywg0LrQvtGC0L7R +gNGL0LUg0L/QvtC30LLQvtC70Y/RjtGCINCy0L7QvtCx0YnQtSDQstGL0LLQtdGB0YLQuCDQs9C+ +0YHRg9C00LDRgNGB0YLQstC+ICjQutCw0Log0Y3QvNC40YLQtdC90YLQsCDQstCw0LvRjtGC0Ysp +INC4INCx0LDQvdC60LggKNC60LDQuiDQuNGB0YLQvtGH0L3QuNC6INC60YDQtdC00LjRgtCwINC4 +INGE0LjQvdCw0L3RgdC+0LLRi9C1INC/0L7RgdGA0LXQtNC90LjQutC4KSDQuNC3INC40LPRgNGL +LjxwPg0KPGJyPg0K0J/RgNC10LfQuNC00LXQvdGCINCh0LHQtdGA0LHQsNC90LrQsCDQk9C10YDQ +vNCw0L0g0JPRgNC10YQg0L3QtdC+0LTQvdC+0LrRgNCw0YLQvdC+INC/0L7QtNGH0LXRgNC60LjQ +stCw0LssINGH0YLQviDRg9C20LUg0LIg0LHQu9C40LbQsNC50YjQtdC1INCy0YDQtdC80Y8g0YLR +gNCw0LTQuNGG0LjQvtC90L3Ri9C8INGE0LjQvdCw0L3RgdC+0LLRi9C8INC+0YDQs9Cw0L3QuNC3 +0LDRhtC40Y/QvCDQv9GA0LjQtNC10YLRgdGPINC60L7QvdC60YPRgNC40YDQvtCy0LDRgtGMINGB +INC40L3RgtC10YDQvdC10YIt0LjQvdC00YPRgdGC0YDQuNC10LkuPHA+DQo8cD4NCtCSINC40L3R +gtC10YDQstGM0Y4g0LbRg9GA0L3QsNC70YMgSGFydmFyZCBCdXNpbmVzcyBSZXZpZXcg0L7QvSDQ +vtGC0LzQtdGH0LDQuywg0YfRgtC+INCx0LDQvdC6IMKr0LDQsdGB0L7Qu9GO0YLQvdC+INC90LXQ +utC+0L3QutGD0YDQtdC90YLQvtGB0L/QvtGB0L7QsdC10L3CuyDQsiDQv9C10YDRgdC/0LXQutGC +0LjQstC1INC00LXRgdGP0YLQuCDQu9C10YIsINC/0L7RgdC60L7Qu9GM0LrRgyDQv9GA0L7QuNCz +0YDRi9Cy0LDQtdGCINGC0LXRhdC90L7Qu9C+0LPQuNGH0LXRgdC60LjQvCDQutC+0LzQv9Cw0L3Q +uNGP0LwgKEdvb2dsZSwgQWxpYmFiYSDQuCBBbWF6b24g0Lgg0L/RgC4pLCDQstGL0YXQvtC00Y/R +idC40Lwg0L3QsCDRgNGL0L3QvtC6INC/0YDQtdC00L7RgdGC0LDQstC70LXQvdC40Y8g0YTQuNC9 +0LDQvdGB0L7QstGL0YUg0YPRgdC70YPQsy4gwqvQntC90Lgg0L3QsNC80L3QvtCz0L4g0YHQuNC7 +0YzQvdC10LUg0L/QvtGH0YLQuCDQstC+INCy0YHQtdC8wrssIOKAlCDQv9C+0LTRh9C10YDQutC4 +0LLQsNC7INCT0YDQtdGELjxicj4NCjxicj4NCsKr0JXRgdC70Lgg0YMg0L3QsNGBIHRpbWUgdG8g +bWFya2V0ICjQstGA0LXQvNGPINCy0YvRhdC+0LTQsCDQv9GA0L7QtNGD0LrRgtCwINC90LAg0YDR +i9C90L7QuiDRgSDQvNC+0LzQtdC90YLQsCDRgdC+0LfQtNCw0L3QuNGPLiDigJQgwqvQk9Cw0LfQ +tdGC0LAuUnXCuykg0LzQtdGA0Y/QtdGC0YHRjyDQvNC90L7Qs9C40LzQuCDQvNC10YHRj9GG0LDQ +vNC4LCDQsCDRgyDQvdC40YUg0YfQsNGB0LDQvNC4LCDRgtC+INC60LDQuiDQvdCw0Lwg0LrQvtC9 +0LrRg9GA0LjRgNC+0LLQsNGC0Yw/INCd0LjQutCw0LouINCe0L3QuCDQstGB0LXQs9C00LAg0L3Q +sNGBINCx0YPQtNGD0YIg0L7Qv9C10YDQtdC20LDRgtGMLiDQp9GC0L4g0L3QsNC8INC90YPQttC9 +0L4g0YHQtNC10LvQsNGC0Yw/INCi0LDQutC+0Lkg0LbQtSB0aW1lIHRvIG1hcmtldMK7LCDigJQg +0LfQsNGP0LLQuNC7INC/0YDQtdC30LjQtNC10L3RgiDQodCx0LXRgNCx0LDQvdC60LAuPHA+DQo8 +cD4NCtCR0LXQt9GD0YHQu9C+0LLQvdC+LCDQuCDQsdC70L7QutGH0LXQudC9LCDQuCDQsdC40YLQ +utC+0LjQvdGLINC10YnQtSDQvdGD0LbQtNCw0Y7RgtGB0Y8g0LIg0LTQvtGA0LDQsdC+0YLQutC1 +LiDQndC10LTQsNCy0L3Rj9GPINGF0LDQutC10YDRgdC60LDRjyDQsNGC0LDQutCwINC90LAg0L7Q +tNC90YMg0LjQtyDQutGA0YPQv9C90LXQudGI0LjRhSDQsdC40YLQutC+0LjQvS3QsdC40YDQtiDQ +siDQk9C+0L3QutC+0L3Qs9C1INC/0YDQuNCy0LXQu9CwINC6INC/0LDQtNC10L3QuNGOINC60YPR +gNGB0LAg0LrRgNC40L/RgtC+0LLQsNC70Y7RgtGLINC90LAgMjAlLiDQpdCw0LrQtdGA0LDQvCDR +g9C00LDQu9C+0YHRjCDQv9C+0YXQuNGC0LjRgtGMIDEyMCDRgtGL0YEuINCx0LjRgtC60L7QuNC9 +0L7Qsiwg0LrQvtGC0L7RgNGL0LUg0L3QsCDRgtC+0YIg0LzQvtC80LXQvdGCINCx0YvQu9C4INGN +0LrQstC40LLQsNC70LXQvdGC0L3RiyDQv9GA0LjQvNC10YDQvdC+ICQ3MCDQvNC70L0uPGJyPg0K +PGJyPg0K0JLQv9GA0L7Rh9C10LwsINC/0L7QsdC10LTQvdGD0Y4g0L/QvtGB0YLRg9C/0Ywg0LHQ +u9C+0LrRh9C10LnQvdCwINCy0YDRj9C0INC70Lgg0YPQtNCw0YHRgtGB0Y8g0L7RgdGC0LDQvdC+ +0LLQuNGC0YwuINCQINCy0LXQtNGMINC10YHRgtGMINC10YnQtSDRgtC10YXQvdC+0LvQvtCz0LjQ +uCDQuNC3INGA0LXQsNC70YzQvdC+0LPQviDRgdC10LrRgtC+0YDQsCDigJQg0LDQu9GM0YLQtdGA +0L3QsNGC0LjQstC90LDRjyDRjdC90LXRgNCz0LXRgtC40LrQsCDQuCDRjdC70LXQutGC0YDQvtC8 +0L7QsdC40LvQuCwg0LAg0YLQsNC60LbQtSAzRC3Qv9C10YfQsNGC0YwsINGA0L7QsdC+0YLQuNC3 +0LDRhtC40Y8g0L/RgNC+0LjQt9Cy0L7QtNGB0YLQstC10L3QvdGL0YUg0L/RgNC+0YbQtdGB0YHQ +vtCyLCDRgdC40L3RgtC10Lcg0L/QuNGJ0LXQstGL0YUg0L/RgNC+0LTRg9C60YLQvtCyINC4INC8 +0L3QvtCz0L7QtSDQtNGA0YPQs9C+0LUuPHA+DQo8YnI+DQrQktC/0L7Qu9C90LUg0LLQtdGA0L7R +j9GC0L3Qviwg0YfRgtC+INC90LAg0LPQvtGA0LjQt9C+0L3RgtC1INCx0LvQuNC20LDQudGI0LjR +hSDQtNCy0YPRhS3RgtGA0LXRhSDQtNC10YHRj9GC0LjQu9C10YLQuNC5INCx0LDQt9C+0LLRi9C1 +INC/0L7RgtGA0LXQsdC90L7RgdGC0Lgg0YfQtdC70L7QstC10LrQsCDQsiDQv9C40YnQtSwg0L7Q +tNC10LbQtNC1INC4INC/0LXRgNC10LTQstC40LbQtdC90LjQuCDQsdGD0LTRg9GCINGD0LTQvtCy +0LvQtdGC0LLQvtGA0Y/RgtGM0YHRjyDQv9C+0YfRgtC4INCx0LXRgdC/0LvQsNGC0L3Qvi4g0J/Q +u9Cw0YLQuNGC0Ywg0L/RgNC40LTQtdGC0YHRjyDQsiDQvtGB0L3QvtCy0L3QvtC8INC30LAgwqvQ +utC+0L3RgtC10L3RgsK7Ljxicj4NCjxicj4NCtCR0LXQtNC90L7RgdGC0Ywg0LIg0YHRgtGA0LDQ +vdCw0YUgwqvQt9C+0LvQvtGC0L7Qs9C+INC80LjQu9C70LjQsNGA0LTQsMK7INC+0LrQvtC90YfQ +sNGC0LXQu9GM0L3QviDRg9C50LTQtdGCINCyINC/0YDQvtGI0LvQvtC1LCDQs9GA0LDQttC00LDQ +vdC1INCx0YPQtNGD0YIg0L/QvtC70YPRh9Cw0YLRjCDQs9Cw0YDQsNC90YLQuNGA0L7QstCw0L3Q +vdGL0Lkg0LTQvtGF0L7QtCDQv9GA0Y/QvNC+INGBINGA0L7QttC00LXQvdC40Y8gKNC/0YDQuNCy +0YvRh9C90YvRhSDQsdGD0LzQsNC20L3Ri9GFINC00LXQvdC10LMg0L3QtSDQsdGD0LTQtdGCLCDQ +uNGFINGBINCx0L7Qu9GM0YjQvtC5INC00L7Qu9C10Lkg0LLQtdGA0L7Rj9GC0L3QvtGB0YLQuCDQ +vtGC0LzQtdC90Y/RgiDRg9C20LUg0LTQvtCy0L7Qu9GM0L3QviDRgdC60L7RgNC+KS4g0JIg0YLQ +viDQttC1INCy0YDQtdC80Y8g0LLRi9Cx0LjRgtGM0YHRjyDQsiDRgdGA0LXQtNC90LjQuSDQutC7 +0LDRgdGBINC40LvQuCDRgNCw0LfQsdC+0LPQsNGC0LXRgtGMINGB0YLQsNC90LXRgiDQs9C+0YDQ +sNC30LTQviDRgdC70L7QttC90LXQtSwg0YfQtdC8INGB0LXQudGH0LDRgS48YnI+DQo8YnI+DQrQ +ndC+INCy0L7RgiDRh9GC0L4g0YHRgtC+0LjRgiDQv9C+0LTRh9C10YDQutC90YPRgtGMLiDQnNC4 +0YAg0LbQtNC10YIg0L3QvtCy0LDRjyDRjdC60L7QvdC+0LzQuNGH0LXRgdC60LDRjyDRgNC10LLQ +vtC70Y7RhtC40Y8sINC60L7RgtC+0YDQsNGPINCx0YPQtNC10YIg0YHQvtC/0YDQvtCy0L7QttC0 +0LDRgtGM0YHRjyDQutGA0LjQt9C40YHQvdGL0LzQuCDQv9C10YDQuNC+0LTQsNC80LguINCi0LXQ +vCDQsdC+0LvQtdC1INGH0YLQviwg0LrQsNC6INCx0YvQu9C+INC/0L7QutCw0LfQsNC90L4g0LLR +i9GI0LUsINC/0YDQtdC00L/QvtGB0YvQu9C+0Log0LTQu9GPINC90L7QstC+0LPQviDQvtCx0LLQ +sNC70LAg0LHQvtC70LXQtSDRh9C10Lwg0LTQvtGB0YLQsNGC0L7Rh9C90L4uINCS0L7Qv9GA0L7R +gSDRgtC+0LvRjNC60L4g0LIg0YLQvtC8LCDQs9C00LUg0Lgg0LrQvtCz0LTQsCDQvdCw0YfQvdC1 +0YLRgdGPINC+0LHRgNGD0YjQtdC90LjQtS48YnI+DQo8cD4NCjxicj4NCg== +--2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 +Content-Type: application/octet-stream; name="ФНС_РФ558.zip" +Content-Disposition:attachment; filename="ФНС_РФ558.zip" +Content-Transfer-Encoding: base64 + +UEsDBBQAAAAIALtmFklP5Nc/ZCIAAGMiAAAPAAAA4avjpqGglI2ROTQuemlwdXpTcCUME+yJk41t +2zrhxhvbtm3bG+PEtq2NbVsb28nGTu73P9ynW3fmYWr6aaqmqru6qhVlICAxAAAALKDTHEvKj7O8 +6ogUABhSBQCQ/kNNHUwM3NzNTB3sbJmsXWr4rOIPeJ3zdt4xZwBgfGcZMRRRoJgtRJJx0E2VX+c8 +mO/489XTfP/Xs2pioejMtfnxryejUbJoBszoYd6KOIGnxYvz+tmXSacnm+OAr48DTgGL8xLjY4eM +1ufej1cYvh6rm7m3xk80LrcVXV2xDcOrV/1mOBvmhsgxPH3cv6y5abJ8IYKn+0G7nTP3F6Ne8ehf +dyc73zGQVk3J9T76c26zo6Y5vf+wyioxWrnl3m02Dyn0jp0WTacPNBPW+H5lPfeacps/yewb8H9x +UNBlWYRJBhCVmNpDpmSlS37C2zjiVUzrZItv7TPKk8lJ/g2b23ZUt+MWw5cfE9/DQlCFrVCj4BUA +G/3g3DYP+m1gMsUx9J5hBhndQ9ZTDd1Wkbtotcnjwy/CHOhvGlEznKOP+YZyx//L7iflYBWdqqed +1SKa+yaUYK0s73Uw+XvbME9jhE42+Ua7oRSUxuC7AdtzOu5zuhiCm7T8kQopNYOrsre8nPBsBe4i +ub5Tj0j0Y2n5jAbUMqdYM4onViMD074YoVdIXUXzSY0mildI9F42F40FgoNmdeKYxTP0NJQBC8R+ +/EgA7Dfra3x/QLlowHPpHPGS9dmPqNcWTQi+kSrD/Ui5HqQNVLsejJ+J85hRMHlPoEiYfzB63t/S +Xp8DaThtXaEiQq0VLNeKrwCaSUnMbhoPgNf4oe3vIT+Z06pgz0wK7Odkv2z4gYoTZ1o4M4OCgmzl +ORwOSIhdVnOABv7aCqVY0JXmcopolsPJKxk3W6g53w0lOvIucrhngphN+Xm7v7RGMk2HJA2vuGhy +Wgm9NIbeJhmaX+Kcy2qbWAAZJ0I+4fBfxqgWIuJRf6kPm6HeUlEvHJrim3vas89oku6ASCmunn9Y +1FtzB6WJlXyWTn9d1J0381ROd5v02XwB7xz7BkddgmXnQQpnGepbovBPKl7nQtSBjZIdpz/FIU6v +AN4gdoaCuxFPcZQHchNN702eAUTwsWdZprIS15i7H7jXbWlojfZwI9U6rCYv8sIqaU/QmeEUHnMU +ZwIik7Kt2YTgpuh596C98rU6rO/M2zpbYCpda+pgGIV3xW082oFDusE+ZxmQOlO8o1/J+J1xHrjY +H/EWBE59GUDBF+xJEjA3HCJ7/avgDwkP3WDPms6IdUPiLHsldKD5+oLosztMN6zPOE5mShOkFg0J +4B3seDbmHYkpDmrsOYZ2g+dUI2K9+oSWx6e/zW/qT853WAv3Itn2LkPWtFd0caoJ8f2F7y6jv6EJ +xiYFw2iXWdOKMMvTYMhxLrvDbEb7ydSWi0vDSZnFLDJXVrxg46/7PVp4HeJrUA6KOBqxIlxr3cmy +/23e/Gs9xOztJbH5vS3T8yM2uev0x3JC9qcMxRtzoubDqmR13dlC9MSEzIXrHkRDUYinRMY2YcT9 +BH74oR7SPiLjbTgtbrJwV0Gk1k0BBOvuO3D3J/uuD/euhGmfh11++fa2SIF7Gho+rHFIuuZvwiGS +0hXSruHxZUjMeYKc3MBe7t0lyCjTPtijFFDjjef+B+nqEM9Gfvz6bWNn8wWL8FWrMPHiHPqythAy +urDjA9CJYIvXFWtucIlxPKLTLSwiMUBm3HF4Qm8vklXunA56U22qFd8BYwffHtlJUHcGNo194Ah/ +j+4cFpquWFeQUtn9PZgEmsjE2VlW2DgLXVR0UZKfyRakamC8KAvTpBJhJL35DYpsuIUhIwLekjgY +KsWqTYCKlGgRhpHY4qI+RTIAmqk1cq4rNyanPYXo5n8H0Iyxwf1NCVRs98C6W1gvScbHO1RiUe1B +1DjTRUOnU+QNkbK9RjppfCSWeFahkXIzLRmWmaDzYU65LVtbZzFv+T4Q3XBpUqU1BQL/Tds7JXrS +CLHsrWEEjB6f5n4/FrIo31hu+3n6TuZiMnHR77m2S7iv117X629+qpWOXiHTb2iyTy4Urd4wyW/C +xpYd3Af89DaeF0ypRmGcCqLRQkep1lLtPDilsFFN1Yx/RipsCGuHoHNmsazihTA5WvAhqrSlQfnm +AxaW/hGxZYWpL7AD4rhHSvTeHEHF/90gw7e2L4yO3OjGQwOf5ps135MVQeEzFPC9XIpjEGChySDf +S1D6vcLC+Pxz0Pj2g7Jxh4zR4luw9p6L9l3P3rKXSzatxH9Hzf0bxYx+h63G+uFRAZVpZ56hq8F0 +rxM2k8VNdfwHPmsQlazXIEICTnBkFldqiFeACRwA7B8S8cBbcntGO7PGhheVz9QHpv7T46zg7vDm +g6flQbyhmcgT3YqW8ZJvxoDz5kXa82WicJztLvwI3NqQMKWl+na6zPt9uW8UEgRtPo3O24is6YLg +ZfWBGCvhZ66Y8Rc/uxT0w/nYsdVxr1P5qe8+PvfizjE3vHgD7lisguf9PWxNL/WmBS49bYFnApnE +A6jwEttYcXIOMRbPW5+9+SPNIYM5d8t+I+fqXqup9ZV5wb42pZuAoRilTpBwFId2SDjVJJOqhXDo +D2dmq86n8oVnmtX7UpxpKIuM8Ou8HCvns1pDomQbCvTUyH9cu51r6lzJf/o0jx5askZmmmXSF1ZP +nJHNdMczDUEVj/pE6IdEe+WlmDQkRnvFgz9SFeKoR47HSpLairjK4hjWnXTy9uircKFQF+ely3RC +75XIzkG9mOSL4cAdf/tXgIMjJHNJNKaFB3f2oa+WwVSURbt/SRI1pTVYQ7LFcJKoIa11PTf5B74x +ExRg11ELTkHfgJUObA7ZMSEMO9yfG7zPipMg8kdYeCl1CfldlUUXwfSC3XSza6HNn+szpobYbGHL +3MPm7GO04GjXWYnBHD6mbkN7Vw8Yr5JASa/JykTE1ciJPsz84r4XtWQwyeNeNuwzQxNNFGOikn5k +4xggvKZ2DLKjyPHGJF+O0lAXuU4UsKfN+NcROjDx8N7VJmr4PFzMd3DZd2O5vPZgXz7szVGM06Wj +fPYopZ/BzKwOBFz66OL+lXqlZHzQS2SclTby+HauXDnv65aAPw5rBIpypFJD/CtCl/CPAvfcOLgx +xy+leD8wFwFcn4VUTHSCzJKEGeyF3ARVgoDk8td/XFshM6/aVGLekga6X+/UCBuA49gwtS5tmYpl +3rv610HZ/mgKXE/LQ/YyY5GYjr6klNrAGApyRV290md6e+ujLvam18dU+ouxJGl2KdX+d0skW3CD +5TFQs0NBUdKv8qxnxF3ZZTXJounwrCyDnSvO3OX07A4e8BuPP/7zAerER6k6Cw3ZzICcN+lL3+f1 +H1b82QKuK1NRPTu7PJt6erlhjuRU1xWUfJ5yLJoGvJCi76J4l9q9/l8c6AxaZecN+rUOD1qTb9jQ +JdTFlvs/Dke0Oq8vutUI2CM2fvyUZa9eZWE9VOR+5bg6tO0u8of2dFb2xJg3PJP6TLyDfKnOKA8g +VkYJdNCqSFF8HzFuUtTatmARGCSKJNHk2mioUG+lcYnrgHcr5ihwzqXDwbKteeKkRYmLt/N1vIYh +OPu1Mr19S1141YpBxfdLNX8RqAe/lxdoqT3zh8YWYnoRUG0pBJFjN2Lb0ssH02l4ceky35+JeQ71 +KtaTyb4FqSxVocr7Yx7yY4iq9NiGygm4M3bByyprTtN8b/PM2nMDv5oag0NFn/5+lZK87rmN9jrv +FgXNKEFgvM6zETZIjCatd2oSZXEwL9V0+ihnII6Ms3Q5LWc854RmsuoWghd0jUOqbH/YR/USwb5T +Vc21qHbrGGd+ZDd5O/rjajSycpZ3wenHgGhsegfMKSPjqJ3j3pwDA/i75QYELq6sYr2cDijkFqVN +d5oN2HzcB6tjAa6s34UEP7pX7d4texxUxtBbEe4cayU1cf2IfltP+MLcTIhHJtKt64BRlLvxt7U6 +LsZuTmPZXJ83c+tIN5uiWJcgUzqiGIJlcpuxYxV1pQcFVpuAobbcapl8aQ32M5F0CGU5iuiJDEE+ +qaAMEkyooJxSTeBH8LfhCQHhl4IsikQa/OSoS4PVdG3suYSPK/eEDcNH8TgXiFufbnLovP+swQ7e +SS5N+7OHQG/CW7VOw4GNLT2fSkPBucd3E5PgVweUzV+YmHqwdLMb+UYFywHUJ1AQOyXC5r+JWmxg +Ay/5gxKmGh54jwXCqGRkRaVXfWZ2ZwAin1ExUJE/FVocqpCGe6xt4NjEPQtxNT6JkhnpuJyo4TBE +nyv4NdeOlGhCNG/p6v2B5Qq9Ixc66sBrq7zg+ZX2rCE+VCnGsh/kjD8NJtREbHViHZgLDA5U4tlC +ffQNCKqFFAlS+7OrdhIlzzsQqMWjAqqu0uaocafIUlrTC91nAf7Or2iWmgEY4GPzaKnlJpnw6z4d +TxrBNCvbs6lQ2HooKcrQX8G9NvKhX5aMAO8vzEoIn6m7WzcEfOZrpCVgoAwUUhruhOlQp3WCXWzB +gqX0MEq96N9lO+9wUoW+ZcLzC7CQltC08m4aMFokdZ5ZkcT/IwLyewSAdQVd3ufmErqBp/6wYxqP +30BhIZrhDLar7qzC9VGj/rFwt6nhF75LyHZ776Thv1l4Yx+Tr0kfTv014s+nhy5e/Z250h6LDNoY +vdOzF14988bOzENvZO86TaPYE5aQp1yzFepI2yKq84SLe25BUe6vWK14DzD7fD8zaWKfuV4L9C9o +gCeyD6qGOwPtTDMl0mF0X0pQGjKLJ+MifNs2yxCqkNPl43dMaE50QFCoXVcwBWLdoGJs3Y0yjWsM +GjUjcmKOhK/jl8g0S8a9RtHsCOfm3yI3PVa/xPNSvzjdcd2WsDrrN6x5W/q59NKuHlCPHqPDCdRw +ZNCetbD6AsU0CGW3U3WPwDCxWGuisZUqpwVVPfncMxGmOnRshLjMMh0FFrWIFrIz3AgjmKUxlSwk +09toMMleRvV6fy4krUh4qpxntwjVfqnsIP69Vucio0WE2rX07o7uvELTT71kqnc3yUonVdgvs0j2 +gGjrybp9B0jEj6tSfD4WnLxEQ1FBUZfdWOwmfK/4MtaYRrWMaAob3BffYlUI7yCJ0LdbrhMspyGb +AgC0UWxsVWXCrzOWIDgVpXDTI3GnYwRjsGjyvthCMgQdn3vFDgv/VlnCkaX+fFWGU9KuHHXe9BVZ ++mA8cEtJxxM7TGPlMEkgUCNvxBp8VPh1sds0hYbkgZNoZgOygHggDr6songosZIa+13z2mFzGNMl +bDnW/TJsz0NX+zjzZisg3cGjgoQuqyZj7TIuRdhGv6d3comy6W6snUw4bWwi0Mkg37rbxlczU5W0 +790H5MjJSLO8Ogxr1i2BAErYrY5brF+InUBN0wrpC4FHcKuQduBgA0vMnS12m0U4wFFcf5Yni2tW +ZO5TRE4UMXYhOV7ITddzRJyR8BqxbjlauqYufXA/usuVGWUcXGgmJkvomGWa5uPnNVGQseoJld+a +pjxzHxbqVLRW68WfdAqoztDKKi8rKnVcpqp6K9DazAXoe65XmWpuyAGL/B74JdFI/MngUe/IIRo6 +x0wK3kaGcpmOHvWXJ63NaL9uInCiNTHA4fbRHJeE1NjdUHmnlVtf39bwViu7iuPwdJb9XtnO5Oth +yazQ2+yD0QfBZyZMAqZ3gnXcfFxketYTWAeAHP41MsM70YZoRbr/KMoNedw8xvcvkhiQOn1+kzBE +hoQwNbshwpBj3Oj9MG9hVfyFIr64EFb0By8wh9J/S16kRQSyUgXNGuvUVqGIYcCKuXLx7QNIQCN1 +sCElhyP9mNcx5pGBVS47bUp3WaTu6V5WZGGGzetdZUQIUxUZg2xeURlAUsIGAp5q7S+bxjrfrpln +gU6Q+de+g8flVExjQlCWRQjXD2FnbdstXpkCfWfrm+iY+K6WWSpGQcnlsL1mb2F7rbKuas30i5i9 +qnWeAqZXUZo6dAsBleb0EtWbBbu7ttsi0wgKslyj3Q7zmB+sohi16PExXvxj4mfDU1uvUsx1lIwq +5fDdBCK7Mf0RJaxZflZLZSePwu+nuJNdBzQCHd5bhRspySczMldZNRcdcAV+hqF89Qzii2EGjvjR +zhbrcLq4J/KIaVTKNEQUFvN/7twtUekNRztUiBBzuRbOju9bx6trMSjAH0neJGj3soZkYhxJJevG +zuzdyYb3FolyGQD59AlYRB2nNmQIulRyooqUnN2Anw7jA2rK0aVGKh1Iytp2aFmSQt6RIe3u5/eX +E4U8ldk2I2yNFxDPTco+vjTiTiXhoZ1VxfE6ZX7KgwWpZ2vJ59kWi6/smk8f5nVzbBLdZnuv8ykN +A6KPMTLY3yGL2v9ia8gqfuW3QBimJiZAMpefgUFms9tKWFbyTM7+Lr9ABOniS9iEJtzgZknbJ5fh +Zf/VnKifLSqOMTl0qwx5hIolPrVN7D43JhSIv+j0uZl9W533Fz4Sm1GTaGB6jUyxL8RifBgFh31K +d/C/Rrpa7uIub8HEVFeKLjSTCokF5fsylJzMlhOLr3f43joGEpEChJY3wsmzI95g4L+h0xvLwlG6 +xTHjsf9TmtJkHqjA80xkSjNBlJ40IzwqKmYE4i7mvPpGfeq2PBn2Orxs/bymadq64i3DykwRXpmW +SvsESz9Gi1eMLwu2ZafVznsompr1TBqLrqrjjDx02vnLZTRsPxq9lWwj0h+BWk/m837aRd0SJMc+ +U6LDgvMSv0gm0uqhKGXtAJD7ThkmJeEiMdmXBn66w/2h95Ysyxzz4ILasRZNqwPtinAuX2JO8YYT +ChoUJfigEzuQFGW4eJukm3bW5m8/Hz/NSfsagUnadtH0wnwbGcqp24qcQ3mOPMX9kMWis6VCdcA1 +M4NuiEcgQ0O35QxvbKrLOeKxtBZ4Ag1zxIqy3gAxdQPBizISCbzt70NX85Uyf57q089PNyvOpzzp +yBENiLzSoYWQtKGupSaOSXCj8YZyDx1LBfeVos1UVkJVq2t+ChdHTY7QcpbKMEVguhM0+0YyXwU/ +Ly8AjL+4LKXuAy8Fq+8/PyzjGl5NF5+fV4m2p+KGIdGvPoNM8Uhfn59OMGZD9KHd9cCmuyqYl+9S +s1H811d3wzpVSA7H4/B9t5AX7ARzwGCr8A+h5ZAhUqb42Wa9R41+fpLVhVuRasR/v1i8kRmt3o8u +7rkKrHkWVt3n0x72DiRrwX+4TBAJ6NGgCbOyBpLyoJSsWt0ViFOb+VbEqOMHaokLGTOyPAexcLep +LoIc2N88Z8Bg8UGu/l1kDqiQ+GQGz5CBeN1Sh7q8VstkCraLC/coMG28PxEOrIWIiZss2TfoIk/j +vEtsaF7ZfRKnmusxcouCru54hXzmNLkK9a5OfdFw1/WnDeRnF7gYR4Xqz0nEpLH57AGTjdjUlcg5 +EICCQzzg6x+AT15XJpVuPFgqFdg/QgdwSdZabc9q+cbRg9u8zhZ65Hi4wJ10B5raXQSF0yWWHSlT +vr8v/q7wog+cohblfJISQEekyqchK4m7QptL1iaNT0SlUX323/Feuf9uJkj6MJHRIiVVq27W3cWk +fx1T42CwIJK2d8+9x5/+xR9X2XnUmsAX+g5CsWLqOAeoaQ03sB64xHkNP/T5YCEPcoWf/tywk8IK +PClrCoVZeVOT48CCGHfreQytzkDpRtsiB5NkrmpIcnZZ3km5ngxRYJKWgjavUEvO0QXKNcpxpsNi +G0upIDcEFpgso5MJ6tv0JPo53Wkx5616iKWUr5fmJGO5KeQ8LITK0zoh5HhCN+hOB3IvBD+zMP95 +tk6JTvHv4cgRiPtdqOzLVzFa+0w3ryfdyAsd1DbELrvIPl8flomke/vAjWhMNDt/146XbdMq3q0G +V9hzF9dy3sNQks+B95T0EHWMj+Xmxr0LtFjbZfMRzxdWlbVbg3C03zXIgWaRL6vdUwkxQHzlc5UE +Lku/+bbb4YtRogPWln9KulXkqvphdg1qW4991p8+rWe1Oq8UiU/rv7Diy2KGqnG8N6QSBI9hTv0I +5LH95fJ69seDMFZGlfZBojaTPKXSI2yWAgmSeZir327Vlb8Hx417xa7O86rl5JHyir4g/43VrJPZ +zriThgDzXHEVkmvrByMV5sW018KFa/SXcdtkPP6r3YX9hKZR6XmOZH9srzxo26UCfGzTLNAMFy7S +27hlD5khEmVNskCs03MOBdrfmOnVGCWEKvDjkJppOcHSJUWk0I+2nUrsbokt3egFhzsFzk5eQGAl +Vt1TfVM0vaBDcoEab3mdtsJB2DV5cRd4hlC0aLD9wcj/wHzw2a2yuR5lOLXUNaXL9gwvnCclVAAJ +TZnu4s6ROk4Wb13XyXaDTOmoXw4Z7rztuIPyDlfW/0RaXewrmDbZIYiLwF+Fuy040gGyDtOyDl9M +YZZwXZhO7gqlLc0ES0UsuTS6Qpe7TTZgkeqBjRTXrPmF04ak+uB+G0oKa0uWqGys94jBL7Ng/wbl +9BvZWCoPEhIjPmwVkhEEQbyx6+PElHs9WelUi4CJDEdV1amY2Gj3aO29DLn0wyLgwPLq7lFqEA+X +i1KgporjqQcjZOdkENi5HwQxzGI3ez+mnXhR/lO/reYnIR3t+KtCKxZlWde8JBmcNKFIBNz5cthc +wlkCyQmWeM+OgDzVnv76fLg3L72zlNt35hqLShPi5MEqwBvb4RBz4qjGgEjseV25HRkuSRWPUs8L +l8ojwRtkYABPf0qEOKvl66PcLqYBLryrp52a4zu0KiqBFkhCJsVkHBsHL9LXnjzRbPOYqtVEgol/ +XlCPYxiJMLUWOAyYPGmSsajXd/84qQvZa1IuTDG5/ofT+kT8S6x1ednhLisR428z3Ti8LDwE0/Uf +ounjLwGv45+Y3E6tRWSMNVHWe8Uy1CfISpahlvSARwu0NV3r+8DU8/7aXI/gFvWPjy5ZjZomZa7f +4+dH6e3J7XSpAkmikcLCps4dNUF1hexT3l+svNhMMGytWJiUlYwVK/t0W46bpe1pfKIRXvDchg2B +9w22rjdF2meS7yV2Lpg0TpdPZRiyTtR6zrQcIFVicsFIDCUzAItPytcMGL0DR/2QA25cBJwGKlU3 +4IHcREckW7qcGuWO38TGWVxYDWy6PRkifWkBXoGGN/MXeELBHOA72OHOmHejBpnewTVP4QNCTZ5g +Sp25UYpjd9XJmXGGRUiDZ5OnYlkj5cKWMPwR5Bz5qhY7r7eu+4LuVICgS4q8L6/igjD6r+/R2Q2W +N8Nzp/mxVmtnijnqLevjoR90uBL+aXo0tKgLKuIdmmCq+ziSiG/k6XYnKxO3AtrQSVCq+pV3mzL4 +7P8tP+vnsRA/XSdUjYtf9zShIZJW4J9zOWtLuYn0JemXOvUeMYUCNPmc0Q8zynSTItiwjRN3l/Ox ++kUKMeRHz9C+44ALafaDjNkA5ay9ruUC9NOHTL0FuD3mNrsxIgAUvFKjoMc/9Rv54TJ8XaETwXPu +Th0Xvgjvy7VpBivJtvi7KhB90aaTG5FSZFgvK5yLL+LvqQZ8NllH9QopCwo6mL2SE5jLovbRPNh/ +4syk0Smy7KRal3O8Wz4N2X/YNs0TyWqIru9mZh79lQQTp2VujNYmErFXIOf8dba0Il6kjiOA2ftD +vMpL+izKqcZP9k5MuSNiHTLQjGthOZJ1EQcwMPtB2JDj9SRhiBKOG5CowbfX/2vuGSHl4DrJe6BU +Z9zralmsAI2AFSKmDS4cItvdsZUV6JJPTVKNs4GX5pGjpVHH721NiqVYLqM3JmDqbDMPPoo11Y4k +XywsHMGnj5mnClNUzt/9VFOorFBeKXG1a049S8H/r0P+S1ZEiZda+CvDd6/zIICO1B2crvWlEoAw +fk3xpU9XAHkmlett5A8imTSPQaLqalg7448mRQ6dm3VgIIhgFtsbWCd5mHzlKO+12IeXe33PZaWK +9B/ZmMwevm24iHAYaj8Ni86WWOub4ndRPogRGwLBzM0mNmRsC5GleiVKCRTy9RWpjVkYiVWjQ/ey +erD0lITZuZ7/C+F/cB0akttjOqWNiFblTvWCvdECUC/OTxy4ml+BnFxSCYKeqfUsqbcXufg0G0FV +H9gUqtMFUwcYPxzUqLWW5HccI3ccz7lqhdFs0auPdoXRnzsdtw0KdNq3ly3aXRPyudxR1iq/ACtd +rO1+s+DZButoaRYzxPcB7pMXZfymWTjYCt/DRN8VhclNTNQKd+/YeYWQle3aM0uPgBg2bX7zG4E0 +Uw1BqLdSgdPc5+5jtVdzwHHl9eqviafpBQx7Q5hUM1zOyaNs9RZzrht5b30W/atk8E3lGi+oO726 +w5s9lvOkzgEau2mWaAcsv2h4++lMPRnWpEmIPcQD6tDPsfRIfF/+otmY+hunGr69zExBvubDuuTg +B07ksO8IpOgifd327K34NH496BE1WvxBhkcRERW6bo7OQhzr19KioKAiUmQHXmS/PwvSdp9G2UjS +bj+Oy59MGMTIvjjGW6S5ag0kTrteVxl8vAp91/SGUf1loXjX0CKXh5M/24No/TmSNtPhWW/D/jMD +sBkUFmLT5oTFgnLq77IHgj8Gwjy8CKaPduIJLAYHRPIYn4iV9IlpV7utKzt9E63Xqh5Jx3uFiw+0 +maZv6SObDYivZmvEms9+jjZy1cbKSCPBmlcyRQw1qZCZwJzVBT8qZuUNXIq9PZ1OCGSCw+JJg9aG +MPWbf+HoH0vdvQj8IArsU5zQqC0w0DPwIWdU1GTz57Lart6xOC5VzgNl7R9Vqw4+JhS7vkkuEJfK +94+cQ6dG6uUcbUzZWGIIWOhcAMn1KIvA75d+RFEPY+TZkZYtcVQc65AepTwWre94yrQuK7d9rquL +uZLxjCQaoF76S5LxDiVAGGo9bR/emE85sgIGo3hFLwp71MRlQoUi7JLYGqKlwYRfPNiKN2DtH8s2 +YNFag6UzAVNK0q5bXHDLMdn9nIadLd0lufQvPsOdSEv/mnYShvxX6Shm4gqCiBHmq4yUo08wZq/5 +FtzlHUQ9xJ0Oj+ynRaTGbVpVR/1ov+EY9lyVdVzsjHk3uWKcry0GCbenovirDapqytCKRCubvKxl +YCeLOtd+/AkdOmPOjwfMXSq/vSsiA4tgg7tJ/iT41UVyA57If5Ur1ulh+IX6zo5hW3Wm5YmI0eos +Xbou5NT3tRSbX14ZMM1oOSh0OWuphRCqPQL/rA+50csqYQTHFxOo5O4JsMu9pj3IL3LaNXb9yB16 +iLYRvdPIxk1T4Fb+eA6DA3YmhYIh+2vx11BNc1EZ3miNWFcEowXQzah/EvlozaVtFyv7gR3FLQmu +edVf2ApsN/aplVUa/suQ0/szocWyHMIdhjMUjrI6ZcWL4/dxHKecpcc2g88ZtLYJfqLVKqvg0kzQ +Iodi8G8V4twEAT4oXeI+EwLfxJopXRC92sDyZdfGBAjV0GRVIBYKmClloiFCFuGszAqHCeq1h8kM +Ix4LvCMpT10bOX6/qsQVy6KxmD7vys4KjTlJdjIBdtrqBSRivXntSMLHivajRSC67jhOZcvJMRC+ +CxWXhEH7n4kjgpfphsz1UfK+Z2NXgLFJzTwgEh/zovApnoP6mhElkGMVXSLE+TXTSo4Q4YM9Fx2N +aJMhUco6gCwEspBK6vXzgRAYvW//HudCuX8HB7GxLgwtiGQbACcn+81ccZTrgPYJ+kflDD87iidz +C6bNq2LCjnZIBHBhDObnQ8vWs7GX0/X+1uFCAHOsUeK+mRVlwMAxIP7/kZD/W3+CAP9PQERRBgr6 +fyjYf83330Ql+9/2fwBQSwECFAMUAAAACAC7ZhZJT+TXP2QiAABjIgAADwAAAAAAAAAAAAAAtoEA +AAAA4avjpqGglI2ROTQuemlwUEsFBgAAAAABAAEAPQAAAJEiAAAAAA== +--2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7 diff --git a/tests/email_testfiles/mail_1_headers_only.eml b/tests/email_testfiles/mail_1_headers_only.eml new file mode 100644 index 0000000..e2a79ec --- /dev/null +++ b/tests/email_testfiles/mail_1_headers_only.eml @@ -0,0 +1,28 @@ +Return-Path: +Delivered-To: kinney@noth.com +Received: (qmail 11769 invoked from network); 22 Aug 2016 14:23:01 -0000 +Received: from smtprelay0207.b.hostedemail.com (HELO smtprelay.b.hostedemail.com) (64.98.42.207) + by smtp.server.net with SMTP; 22 Aug 2016 14:23:01 -0000 +Received: from filter.hostedemail.com (10.5.19.248.rfc1918.com [10.5.19.248]) + by smtprelay06.b.hostedemail.com (Postfix) with ESMTP id 2CC378D014 + for ; Mon, 22 Aug 2016 14:22:58 +0000 (UTC) +Received: from DM6PR06MB4475.namprd06.prod.outlook.com (2603:10b6:207:3d::31) + by BL0PR06MB4465.namprd06.prod.outlook.com with HTTPS id 12345 via + BL0PR02CA0054.NAMPRD02.PROD.OUTLOOK.COM; Mon, 1 Oct 2018 09:49:22 +0000 +Received: from DM3NAM03FT035.eop-NAM03.prod.protection.outlook.com + (2a01:111:f400:7e49::205) by CY4PR0601CA0051.outlook.office365.com + (2603:10b6:910:89::28) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.20.1185.23 via Frontend + Transport; Mon, 1 Oct 2018 09:49:21 +0000 +X-Session-Marker: 6A64617A657940616C6578616E646572736D6974682E636F6D +X-Spam-Summary: 69,4.5,0,,d41d8cd98f00b204,suvorov.s@nalg.ru,:,RULES_HIT:46:150:152:379:553:871:967:989:1000:1254:1260:1263:1313:1381:1516:1517:1520:1575:1594:1605:1676:1699:1730:1747:1764:1777:1792:1823:2044:2197:2199:2393:2525:2560:2563:2682:2685:2827:2859:2911:2933:2937:2939:2942:2945:2947:2951:2954:3022:3867:3872:3890:3934:3936:3938:3941:3944:3947:3950:3953:3956:3959:4425:5007:6001:6261:6506:6678:6747:6748:7281:7398:7688:8599:8824:8957:9009:9025:9388:10004:10848:11604:11638:11639:11783:11914:12043:12185:12445:12517:12519:12740:13026:14149:14381:14658:14659:14687:21080:21221:30054:30055:30065:30066,0,RBL:none,CacheIP:none,Bayesian:0.5,0.5,0.5,Netcheck:none,DomainCache:0,MSF:not bulk,SPF:fn,MSBL:0,DNSBL:none,Custom_rules:0:0:0,LFtime:5,LUA_SUMMARY:none +X-HE-Tag: print38_7083d7fd63e24 +X-Filterd-Recvd-Size: 64695 +Received: from computer_3436 (unknown [43.230.105.145]) + (Authenticated sender: jdazey@alexandersmith.com) + by omf06.b.hostedemail.com (Postfix) with ESMTPA + for ; Mon, 22 Aug 2016 14:22:52 +0000 (UTC) +From: =?UTF-8?B?0YHQu9GD0LbQsdCwINCk0J3QoSDQlNCw0L3QuNC40Lsg0KHRg9Cy0L7RgNC+0LI=?= +To: kinney@noth.com +Subject: =?UTF-8?B?0L/QuNGB0YzQvNC+INGD0LLQtdC00L7QvC3QtQ==?= +Content-Type: multipart/mixed; boundary="2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7" \ No newline at end of file diff --git a/tests/email_testfiles/mail_2.eml b/tests/email_testfiles/mail_2.eml new file mode 100644 index 0000000..4153094 --- /dev/null +++ b/tests/email_testfiles/mail_2.eml @@ -0,0 +1,32 @@ +MIME-Version: 1.0 +Date: Fri, 18 Dec 2020 21:20:15 -0500 +Message-ID: +Subject: multi-encoded-email +From: Test Testerson +To: Try Tryerson +Cc: TryTwo Tryerson +User-agent: mozilla/5.0 (windows nt 5.1; rv:11.0) gecko firefox/11.0 (via ggpht.com googleimageproxy) +X-Mailer: Apple Mail (2.3445.9.1) +Thread-Index: AQHT48oEYuOLpJgzCU+4lba9aw8zCA== +Reply-To: maliciousreply@thebadguysdomainisthisone.com +Content-Type: multipart/alternative; boundary="0000000000009cc2ea05b6c7dd56" + +--0000000000009cc2ea05b6c7dd56 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: base64 + +6YCZ5piv5LiA5YCL5paH5pys5a2X56ym5LiyDQrXlteU15Ug157Xl9eo15XXlteqINeY16fXodeY +Lg0KDQpUaGlzIGlzIHlvdXIgdGhpcmQgZW5jb2RpbmcuLi4uIG1heWJlPw0K +--0000000000009cc2ea05b6c7dd56 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
=E9=80=99=E6=98=AF=E4=B8=80=E5=80=8B=E6=96=87=E6=9C=AC=E5= +=AD=97=E7=AC=A6=E4=B8=B2
=D7=96=D7=94=D7=95 =D7=9E=D7=97=D7=A8=D7=95=D7= +=96=D7=AA =D7=98=D7=A7=D7=A1=D7=98.

<= +div style=3D"font-size:12.8px">This is your third encoding.... maybe?
= +
+ +--0000000000009cc2ea05b6c7dd56-- \ No newline at end of file diff --git a/tests/email_testfiles/mail_3.eml b/tests/email_testfiles/mail_3.eml new file mode 100644 index 0000000..7fb68e2 --- /dev/null +++ b/tests/email_testfiles/mail_3.eml @@ -0,0 +1,170 @@ +Return-Path: +To: Manuel Lemos +Subject: Testing Manuel Lemos' MIME E-mail composing and sending PHP class: HTML message +From: mlemos +Reply-To: mlemos +Sender: mlemos@acm.org +X-Mailer: http://www.phpclasses.org/mimemessage $Revision: 1.63 $ (mail) +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="652b8c4dcb00cdcdda1e16af36781caf" +Message-ID: <20050430192829.0489.mlemos@acm.org> +Date: Sat, 30 Apr 2005 19:28:29 -0300 + + +--652b8c4dcb00cdcdda1e16af36781caf +Content-Type: multipart/related; boundary="6a82fb459dcaacd40ab3404529e808dc" + + +--6a82fb459dcaacd40ab3404529e808dc +Content-Type: multipart/alternative; boundary="69c1683a3ee16ef7cf16edd700694a2f" + + +--69c1683a3ee16ef7cf16edd700694a2f +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +This is an HTML message. Please use an HTML capable mail program to read +this message. + +--69c1683a3ee16ef7cf16edd700694a2f +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + + + +Testing Manuel Lemos' MIME E-mail composing and sending PHP class: H= +TML message + + + + + + + +
+

Testing Manuel Lemos' MIME E-mail composing and sending PHP cla= +ss: HTML message

+
+

Hello Manuel,

+This message is just to let you know that the MIME E-mail message composing and sending PHP class is working as expected.

+

Here is an image embedded in a message as a separate part:

= +
+
Than= +k you,
+mlemos

+
+ + +--69c1683a3ee16ef7cf16edd700694a2f-- + +--6a82fb459dcaacd40ab3404529e808dc +Content-Type: image/gif; name="logo.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="logo.gif" +Content-ID: + +R0lGODlhlgAjAPMJAAAAAAAA/y8vLz8/P19fX19f339/f4+Pj4+Pz7+/v/////////////////// +/////yH5BAEAAAkALAAAAACWACMAQwT+MMlJq7046827/2AoHYChGAChAkBylgKgKClFyEl6xDMg +qLFBj3C5uXKplVAxIOxkA8BhdFCpDlMK1urMTrZWbAV8tVS5YsxtxmZHBVOSCcW9zaXyNhslVcto +RBp5NQYxLAYGLi8oSwoJBlE+BiSNj5E/PDQsmy4pAJWQLAKJY5+hXhZ2dDYldFWtNSFPiXssXnZR +k5+1pjpBiDMJUXG/Jo7DI4eKfMSmxsJ9GAUB1NXW19jZ2tvc3d7f4OHi2AgZN5vom1kk6F7s6u/p +m3Ab7AOIiCxOyZuBIv8AOeTJIaYQjiR/kKTr5GQNE3pYSjCJ9mUXClRUsLxaZGciC0X+OlpoOuQo +ZKdNJnIoKfnxRUQh6FLG0iLxIoYnJd0JEKISJyAQDodp3EUDC48oDnUY7HFI3wEDRjzycQJVZCQT +Ol7NK+G0qgtkAcOKHUu2rNmzYTVqRMt2bB49bHompSchqg6HcGeANSMxr8sEa2y2HexnSEUTuWri +SSbkYh7BgGVAnhB1b2REibESYaRoBgqIMYx59tFM9AvQffVG49P5NMZkMlHKhJPJb0knmSKZ6kSX +JtbeF3Am7ocok6c7cM7pU5xcXiJJETUz16qPrzEfaFgZpvzn7h86YV5r/1mxXeAUMVyEIpnVUGpN +RlG2ka9b3lP3pm2l6u7P+l/YLj3+RlEHbz1C0kRxSITQaAcilVBMEzmkkEQO8oSOBNg9SN+AX6hV +z1pjgJiAhwCRsY8ZIp6xj1ruqCgeGeKNGEZwLnIwzTg45qjjjjz2GEA5hAUp5JBEFmnkkSCoWEcZ +X8yohZNK1pFGPQS4hx0qNSLJlk9wCQORYu5QiMd7bUzGVyNlRiOHSlpuKdGEItHQ3HZ18beRRyws +YSY/waDTiHf/tWlWUBAJiMJ1/Z0XXU7N0FnREpKM4NChCgbyRDq9XYpOplaKopN9NMkDnBbG+UMC +QwLWIeaiglES6AjGARcPHCWoVAiatcTnGTABZoLPaPG1phccPv366mEvWEFSLnj+2QaonECwcJt/ +e1Zw3lJvVMmftBdVNQS3UngLCA85YHIQOy6JO9N4eZW7KJwtOUZmGwOMWqejwVW6RQzaikRHX3yI +osKhDAq8wmnKSmdMwNidSOof9ZG2DoV0RfTVmLFtGmNk+CoZna0HQnPHS3AhRbIeDpqmR09E0bsu +soeaw994z+rwQVInvqLenBftYjLOVphLFHhV9qsnez8AEUbQRgO737AxChjmyANxuEFHSGi7hFCV +4jxLst2N8sRJYU+SHiAKjlmCgz2IffbLI5aaQR71hnkxq1ZfHSfKata6YDCJDMAQwY7wOgzhjxgj +VFQnKB5uX4mr9qJ79pann+VcfcSzsSCd2mw5scqRRvlQ6TgcUelYhu75iPE4JejrsJOFQAG01277 +7bjnrvvuvPfu++/ABy887hfc6OPxyCevPDdAVoDA89BHL/301Fdv/fXYZ6/99tx3Pz0FEQAAOw== + +--6a82fb459dcaacd40ab3404529e808dc +Content-Type: image/gif; name="background.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="background.gif" +Content-ID: <4c837ed463ad29c820668e835a270e8a.gif> + +R0lGODlh+wHCAPMAAKPFzKLEy6HDyqHCyaDByJ/Ax56/xp2+xZ28xJy7w5u6wpq5wZm4wJm3v5i2 +vpe1vSwAAAAA+wHCAEME/hDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBILBqP +yKRyyWw6n9CodEqtWq+gwSHReHgfjobY8X00FIc019tIHAYS7dqcQCDm3vC4fD4QAhUBBFsMZF8O +hnkLCAYFW11tb1iTlJWWOXJdZZtmC24Eg3hgYntfbXainJ2fgBSZbG5wFAG0E6+RoAZ3CbwJCgya +p3cMbAyevQcFAgMGCcRmxr1uyszOxQq+wF4MdcPFx7zJApfk5eYhr3SSGemRsu3dc+4iAqELhZwO +0X6hkHUHCBRoGtUg0RkEAAUeKhhGAcICBQIODIPooIEBzCTmKcjGYSNd/go3VvQo65zJkyhTqlzJ +sqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CXBhhAwECaq1gPNCIwANDU +qmkMcG311apWULmyZt3alcPXAma1FgAlgCxVq2LbRt3LF0Y7hwWoEjLEDZUmff8AOjMkTB5gwYu3 +JbhIQUDEZw+4+aE1aNc0R2vcDYjoDBgpBoUDj95yzzRqbH7qgW4t5vUnAfVAoj7NwOOf1QloN7Ad +u1Xf41b+IlCNsa6rR7DWwTPccTnG5sYvCEKwgPGiZI64A9OsK/Q/BM/0YfuFz13VOwsULLhHps+f +98Hl0zeDRk0X9Qih/vLPWPjFN197aPyB3IJVBLDMdc5t4OB1A0QowYQQ0vIgdilgyGEgG1roYV0j +GufhhyBSWGF2s2yIYosqWsjgjDTWaOONOOao44489ujjj0AGKeSQRBZp5JFIJqnkkkw26eSTUMJU +llpYseXVXWGNdSGWZ6EVF5VWukUVXFdtRUCEU+bFYpRslqNcYKHgk1k8hxWWxjCM0VkdnINJRtkE +lqH3hWZ/CKJYOBBBJxppu/FWh2qzNUrcmQRE6lpvt+UWUKPD9cbIb5bWhmlxbbL5JoUywiMddHRQ +x591GWqwXXdsfJeoeMO5UZ4/AaaHKXv1xVKgfghuNuyB9fUHHYAA/u2CEIHlGbiffWuWyuSJMmKA +bXbbbtuhi9kCUOIEJY57oYsraoduuOfGWO2J6Vor77z01mvvvfjmq+++/Pbr778AByzwwAQXbPDB +CCfcZDobldLRVfLEEgerjQ1EEEemJMiioZEdkggYizSiqMQKl5wCw6qswg+rDTvc6h0Wq9KAJ5tV +oGpJF9YysXn8lCfNL8HE88xw4EyzTDNDR4MMNUhfk40mhXkDTdHimHzjzRpgDcB0MEeHswf1sCZn +GfrQDMrIAYZEkEEOJTQRQweBp5FIDTGCEUiHYWwRXHOPMpLdVgcu+OCEF2744YgnrvjijDfu+OOQ +Ry755JRXbvnl/phnrvnmnHfu+eegZ57RAqSUzptv75E+M+Bb66L6InZwZ7rpr31aLQBhb2pap548 +e7TsIX8dOr/pIIZQQphFHfGqEbtq/J2/DDrZ13Ga0jt8h/XX9TxvfRmmuPVUatb34INCplxakjtm +XOQ7aP74c+k1fE4MD7fefvxBbLEeLldsyq/4o9ZzHOOHylBFS7f4RJxQMx/8MeB4ggIDA02ziLno +wlfGoOByKnUAhZQNWfkzwAXzMEExVFB+86NJ/TDVC4SIZRzFs5Ni5OQ/p7XwLOOwQDXSswgFiYuD +Z4GMP8AjtvGgJk9aYU2davdCeyzRU2LpBwkb2KjvWCU4T/TN/u1S+BKtYUBrXFue8DYQKFoVAzXa +eJh/XiYPpZEOFhAMTnzkk8aQWQU+c7yHJkIGkGd4SkDhMJ9i5qMAOu4RAWfiYk1yxwvfaYCRA8oh +JF14x0bGhgSyaZY07JCMRDLyWWnxTOyc1UmweMaSL5zSKf/xQgnk5lA3TCWWVunCRCrylrjMpS53 +ycte+vKXwAymMIdJzGIa85jITKYyl8nMZjrzmdCMpjSnSc1qWvOa2MymvkY3u9IxMReyW92fuLm6 +2Kmum53SIgZyxx7e9C423AyeNnkUw8RsSnqumsfWKKYnCdozen6iHiGsF483gkF7PIND96oUP7KE +73zteyj8/tK3JfGVqaHkkmhYMDrPJqzwfjRUlij4hzE4ds1pdGSMxgYYjAQZEBRtSeDKSmMMEGYG +ghjU4+osGEF9ZNCEG3SEB2s6LTSIsKcl3CkKO2qEj24Sh/ucw/NmmCdXQQMbsbSlzZoGMkSSBYh5 +kWIkEhWc3aARiVc0qE+hSCklkvCbUpQgFTWYRCy+la1bZGoQvHgBMPIznyT7QBkNgsY05m+NNSQa +Lwx6ijvJsZB69IIdB5nHOjKij9twCCAVGJ7HGlKyiMyhXo0wyUtmoLS2LK0ID+XIEWRys5ycyzg+ +yQ9TtjB2lpyLbZ8qy91mVZK+ReWZVCkNVmp1tMhNrnKX/svc5jr3udCNrnSnS93qWve62M2udrfL +3e5697vgDa94x0ve8pr3vOhNr3rXy972uve98I2vfOdLXxrBS0Uv8lZGUaUh/OKXXRmAV7jMVV+X +QLK4vD0TaoHLWq1UEsEJFu0FXknLh3iyM5EssEtQlrK98ZN5QbNqyl71pwqEza752MfZEqrhljg1 +pYMKkBh3FuKTXtUX+LupMkwcETNCA40D6QNiA3tfdunXAkdOEX+1Ba68tjiqLbVOnKp60oNAam6J +fcyUvTYLAnDHOw8Jjx7Js71YTKWzxX1IV76iyayuWTCwDSIgKJxmqLI5zmp6sg5ZNdV7bkPGQWYh +0EzR/s8+A1THEt6hIrx6IbByRawKHKjfpEfExVREpUEdzKX3dJe5UaQ6UdT0p18VGCfPF2X8S4QD +QgaamI24hi1TtTxZyuVZ6AzK6gBnIbE66DmhImlzxAYouUq0XQ+oUhG039P+rAZgG7u1erYFyy6W +Tt85ddkmHak3PWVaWuePAC9F4Mh6dgdjB/A8tCqbscUxWLmumxp8jsa5A5RuY7xbwtHGtT+Phz69 +nGo0WC60DPt9u0AljxWG8kylh9hsRKw1jbiwx24cDsUKSRwYFPdIq2347NoWkSEAKnG++brnGes7 +sYH1QPVqVdDsOZZXUlN2WYO1soCA9JBoScjNQdvs/n3fKXaxYefOH9BDfD+Z5Db78Dv+WuWUd4Bj +YwPDx1bNiI03BoO7yRi9CzJBBLlQdj5tTbKIOFQqikHjruN6Bovlw5GnXZxjtMXbZ01O2NnhdawL +ASOFw8BIxpOSuutUYWfmBjW0U1S+gczhqy0Wzuhmd7Ur5RYW/01Tz3dKcpYVl/Isrs2jBSyZJ4H7 +LIq+4VYUL2NZaCMgQiY1LXSjFH09wWexvovGvvawX2q+d8/73vv+98APvvCHT/ziG//4yE++8pfP +/OY7//nQj770p0/96lv/+tjPvva3z/3ue//74A+/+MdP/vKb//zoT7/6e3Lf/3KryTDKUPvdBQIB +/q+JwOuPwYEhbFzcYDjDuPN/lARL/FdLRlcZwdUNnTRbGAZt+fcCHCYzGqd0NJZtrsYJFjFGJ2ZQ +m1A2kcZiD+gXLKNsMMZsTQdiFvg/IJUID7RjldFjhAVkGaM/6lASRfYu8KcuS6aDO4hkOfh7p7Jl +bBRlVxYSWSZlfVKDXfZltRJmADFmulJmb3BmBJhbb9YZp1RLV9hmwtUWdBZhnYeFCaZ7Rxdv/5Q8 +gKaCvNBrQ0hCZxhjLhgHXEV1PiQIjhBEkDZT6VFSmkFWhbBppMZBljZqVtZpIUGIqCNqevMYlhdf +qEYKslZ10zZibbgQDkN1IndyTkcLxiFTulZI/muYRsrjbKA4bNYwNR1nPsn2K6J4PKdYbKXYbSM3 +bSQVeWdybWwIa9Rmi0b3FwUEKAcUU+MGTr4AivP2hGSgbqDIbjDobssIb1IlbzSEbslob894gGUY +jYkxeyf3GABnhAK3jeTDYxE0J5uRcEtjdYUnaoMXHStGGxlnNxs4cYgARRt3Y8UobB5XVhhXjyTR +e0jnbfoURkGzDh+wcquACmqFUDD3iiw0LZFmczhmWTknkZ9FdK5IDH0GdArWGaB4kUXHewEpbSZH +kLX2AVA3dVPHamgjNQ8XZG0Ddl2XLF9HOmF3RPmTKGV3IGdXdWl3k2zXiPBVd3nXV3PHOkRpgk5A +lYlgg2F8Fw3WlnZW9HiCB2Q0Y3ic8k2Kl5V4JQhUiXgWFgqUh1e9h3mcpy2epxdm+XnjQ1EiMHoQ +pVtogiWuV3urBxGod4Xnw41huJfjKHvtg3t8GYKEWZiGeZiImZiKuZiM2ZiO+ZiQGZmSOZmUWZmW +eZmYmZmauZmc2ZlCEQEAOw== + +--6a82fb459dcaacd40ab3404529e808dc-- + +--652b8c4dcb00cdcdda1e16af36781caf +Content-Type: text/plain; name="attachment.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="attachment.txt" + +VGhpcyBpcyBqdXN0IGEgcGxhaW4gdGV4dCBhdHRhY2htZW50IGZpbGUgbmFtZWQgYXR0YWNobWVu +dC50eHQgLg== + +--652b8c4dcb00cdcdda1e16af36781caf-- \ No newline at end of file diff --git a/tests/email_testfiles/mail_3.msg b/tests/email_testfiles/mail_3.msg new file mode 100644 index 0000000..03a3e35 Binary files /dev/null and b/tests/email_testfiles/mail_3.msg differ diff --git a/tests/email_testfiles/mail_4.msg b/tests/email_testfiles/mail_4.msg new file mode 100644 index 0000000..8538f5d Binary files /dev/null and b/tests/email_testfiles/mail_4.msg differ diff --git a/tests/email_testfiles/mail_5.msg b/tests/email_testfiles/mail_5.msg new file mode 100644 index 0000000..15a5cb5 Binary files /dev/null and b/tests/email_testfiles/mail_5.msg differ diff --git a/tests/email_testfiles/mail_multiple_to.eml b/tests/email_testfiles/mail_multiple_to.eml new file mode 100644 index 0000000..03b9a0a --- /dev/null +++ b/tests/email_testfiles/mail_multiple_to.eml @@ -0,0 +1,15 @@ +Return-Path: +Delivered-To: kinney@noth.com +Received: (qmail 11769 invoked from network); 22 Aug 2016 14:23:01 -0000 +X-Session-Marker: 6A64617A657940616C6578616E646572736D6974682E636F6D +X-Spam-Summary: 69,4.5,0,,d41d8cd98f00b204,suvorov.s@nalg.ru,:,RULES_HIT:46:150:152:379:553:871:967:989:1000:1254:1260:1263:1313:1381:1516:1517:1520:1575:1594:1605:1676:1699:1730:1747:1764:1777:1792:1823:2044:2197:2199:2393:2525:2560:2563:2682:2685:2827:2859:2911:2933:2937:2939:2942:2945:2947:2951:2954:3022:3867:3872:3890:3934:3936:3938:3941:3944:3947:3950:3953:3956:3959:4425:5007:6001:6261:6506:6678:6747:6748:7281:7398:7688:8599:8824:8957:9009:9025:9388:10004:10848:11604:11638:11639:11783:11914:12043:12185:12445:12517:12519:12740:13026:14149:14381:14658:14659:14687:21080:21221:30054:30055:30065:30066,0,RBL:none,CacheIP:none,Bayesian:0.5,0.5,0.5,Netcheck:none,DomainCache:0,MSF:not bulk,SPF:fn,MSBL:0,DNSBL:none,Custom_rules:0:0:0,LFtime:5,LUA_SUMMARY:none +X-HE-Tag: print38_7083d7fd63e24 +X-Filterd-Recvd-Size: 64695 +Received: from computer_3436 (unknown [43.230.105.145]) + (Authenticated sender: jdazey@alexandersmith.com) + by omf06.b.hostedemail.com (Postfix) with ESMTPA + for ; Mon, 22 Aug 2016 14:22:52 +0000 (UTC) +From: =?UTF-8?B?0YHQu9GD0LbQsdCwINCk0J3QoSDQlNCw0L3QuNC40Lsg0KHRg9Cy0L7RgNC+0LI=?= +To: "Novak, Jan" , "Marek, Jan" +Subject: =?UTF-8?B?0L/QuNGB0YzQvNC+INGD0LLQtdC00L7QvC3QtQ==?= +Content-Type: multipart/mixed; boundary="2NqJR3m2cLnhEraiqXA4Q9hqnmihx7b7" diff --git a/tests/email_testfiles/source b/tests/email_testfiles/source new file mode 100644 index 0000000..48eb84e --- /dev/null +++ b/tests/email_testfiles/source @@ -0,0 +1 @@ +Testing emails come from https://github.com/SpamScope/mail-parser/ \ No newline at end of file diff --git a/tests/git-vuln-finder-quagga.json b/tests/git-vuln-finder-quagga.json new file mode 100644 index 0000000..1e33020 --- /dev/null +++ b/tests/git-vuln-finder-quagga.json @@ -0,0 +1,1493 @@ +{ + "cbffa53cc0454bcc4ab95d9363b13fb8c68301d4": { + "message": "doc/security: Security announcements for 4 issues\n\n* doc/security/Quagga-2018-0543.txt: attr_endp used for NOTIFY data\n* doc/security/Quagga-2018-1114.txt: bgpd double free\n* doc/security/Quagga-2018-1550.txt: debug overrun in notify lookup tables\n* doc/security/Quagga-2018-1975.txt: BGP capability inf. loop\n", + "language": "en", + "commit-id": "cbffa53cc0454bcc4ab95d9363b13fb8c68301d4", + "summary": "doc/security: Security announcements for 4 issues", + "stats": { + "insertions": 257, + "deletions": 0, + "lines": 257, + "files": 5 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1516554152, + "committed_date": 1517758950, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/cbffa53cc0454bcc4ab95d9363b13fb8c68301d4", + "tags": [], + "state": "under-review" + }, + "f080b436bbddf8d28dd991c967dcac5288272522": { + "message": "doc/security: Add a doc/security folder and template for announcements\n\n* doc/security: New folder to store Quagga security announcements,\n where they can be revision controlled.\n* doc/security/template.txt: Template for announcements\n", + "language": "en", + "commit-id": "f080b436bbddf8d28dd991c967dcac5288272522", + "summary": "doc/security: Add a doc/security folder and template for announcements", + "stats": { + "insertions": 39, + "deletions": 0, + "lines": 39, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1516554078, + "committed_date": 1517758950, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/f080b436bbddf8d28dd991c967dcac5288272522", + "tags": [], + "state": "under-review" + }, + "9e5251151894aefdf8e9392a2371615222119ad8": { + "message": "bgpd/security: debug print of received NOTIFY data can over-read msg array\n\nSecurity issue: Quagga-2018-1550\nSee: https://www.quagga.net/security/Quagga-2018-1550.txt\n\n* bgpd/bgp_debug.c: (struct message) Nearly every one of the NOTIFY\n code/subcode message arrays has their corresponding size variables off\n by one, as most have 1 as first index.\n\n This means (bgp_notify_print) can cause mes_lookup to overread the (struct\n message) by 1 pointer value if given an unknown index.\n\n Fix the bgp_notify_..._msg_max variables to use the compiler to calculate\n the correct sizes.\n", + "language": "en", + "commit-id": "9e5251151894aefdf8e9392a2371615222119ad8", + "summary": "bgpd/security: debug print of received NOTIFY data can over-read msg array", + "stats": { + "insertions": 12, + "deletions": 9, + "lines": 21, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1515277912, + "committed_date": 1517742933, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/9e5251151894aefdf8e9392a2371615222119ad8", + "tags": [], + "state": "under-review" + }, + "ce07207c50a3d1f05d6dd49b5294282e59749787": { + "message": "bgpd/security: fix infinite loop on certain invalid OPEN messages\n\nSecurity issue: Quagga-2018-1975\nSee: https://www.quagga.net/security/Quagga-2018-1975.txt\n\n* bgpd/bgp_packet.c: (bgp_capability_msg_parse) capability parser can infinite\n loop due to checks that issue 'continue' without bumping the input\n pointer.\n", + "language": "en", + "commit-id": "ce07207c50a3d1f05d6dd49b5294282e59749787", + "summary": "bgpd/security: fix infinite loop on certain invalid OPEN messages", + "stats": { + "insertions": 2, + "deletions": 2, + "lines": 4, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1515273651, + "committed_date": 1517742928, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/ce07207c50a3d1f05d6dd49b5294282e59749787", + "tags": [], + "state": "under-review" + }, + "e69b535f92eafb599329bf725d9b4c6fd5d7fded": { + "message": "bgpd/security: Fix double free of unknown attribute\n\nSecurity issue: Quagga-2018-1114\nSee: https://www.quagga.net/security/Quagga-2018-1114.txt\n\nIt is possible for bgpd to double-free an unknown attribute. This can happen\nvia bgp_update_receive receiving an UPDATE with an invalid unknown attribute.\nbgp_update_receive then will call bgp_attr_unintern_sub and bgp_attr_flush,\nand the latter may try free an already freed unknown attr.\n\n* bgpd/bgp_attr.c: (transit_unintern) Take a pointer to the caller's storage\n for the (struct transit *), so that transit_unintern can NULL out the\n caller's reference if the (struct transit) is freed.\n (cluster_unintern) By inspection, appears to have a similar issue.\n (bgp_attr_unintern_sub) adjust for above.\n", + "language": "en", + "commit-id": "e69b535f92eafb599329bf725d9b4c6fd5d7fded", + "summary": "bgpd/security: Fix double free of unknown attribute", + "stats": { + "insertions": 21, + "deletions": 16, + "lines": 37, + "files": 2 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1515268330, + "committed_date": 1517742615, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/e69b535f92eafb599329bf725d9b4c6fd5d7fded", + "tags": [], + "state": "under-review" + }, + "cc2e6770697e343f4af534114ab7e633d5beabec": { + "message": "bgpd/security: invalid attr length sends NOTIFY with data overrun\n\nSecurity issue: Quagga-2018-0543\n\nSee: https://www.quagga.net/security/Quagga-2018-0543.txt\n\n* bgpd/bgp_attr.c: (bgp_attr_parse) An invalid attribute length is correctly\n checked, and a NOTIFY prepared. The NOTIFY can include the incorrect\n received data with the NOTIFY, for debug purposes. Commit\n c69698704806a9ac5 modified the code to do that just, and also send the\n malformed attr with the NOTIFY. However, the invalid attribute length was\n used as the length of the data to send back.\n\n The result is a read past the end of data, which is then written to the\n NOTIFY message and sent to the peer.\n\n A configured BGP peer can use this bug to read up to 64 KiB of memory from\n the bgpd process, or crash the process if the invalid read is caught by\n some means (unmapped page and SEGV, or other mechanism) resulting in a DoS.\n\n This bug _ought_ /not/ be exploitable by anything other than the connected\n BGP peer, assuming the underlying TCP transport is secure. For no BGP\n peer should send on an UPDATE with this attribute. Quagga will not, as\n Quagga always validates the attr header length, regardless of type.\n\n However, it is possible that there are BGP implementations that do not\n check lengths on some attributes (e.g. optional/transitive ones of a type\n they do not recognise), and might pass such malformed attrs on. If such\n implementations exists and are common, then this bug might be triggerable\n by BGP speakers further hops away. Those peers will not receive the\n NOTIFY (unless they sit on a shared medium), however they might then be\n able to trigger a DoS.\n\n Fix: use the valid bound to calculate the length.\n", + "language": "en", + "commit-id": "cc2e6770697e343f4af534114ab7e633d5beabec", + "summary": "bgpd/security: invalid attr length sends NOTIFY with data overrun", + "stats": { + "insertions": 3, + "deletions": 1, + "lines": 4, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1515023853, + "committed_date": 1517742611, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/cc2e6770697e343f4af534114ab7e633d5beabec", + "tags": [], + "state": "under-review" + }, + "69f8d5df72b6bd9c39c3a262ae0ed07f2cd566e9": { + "message": "configure: Add commonly used GCC security flags\n", + "language": "en", + "commit-id": "69f8d5df72b6bd9c39c3a262ae0ed07f2cd566e9", + "summary": "configure: Add commonly used GCC security flags", + "stats": { + "insertions": 4, + "deletions": 0, + "lines": 4, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@jakma.org", + "authored_date": 1488993358, + "committed_date": 1489082635, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/69f8d5df72b6bd9c39c3a262ae0ed07f2cd566e9", + "tags": [], + "state": "under-review" + }, + "e3443a21552b6a3cd6ebdbb98336eede217a478f": { + "message": "bgpd: simplify ebgp-multihop and ttl-security handling\n\nChange to track configured value in ->ttl and ->gtsm_hops;\nnot the value set to sockopt. Instead, setting of socket's ttl\nand minttl options are now merged to one function which calculates\nit on demand. This greatly simplifies the code.\n", + "language": "en", + "commit-id": "e3443a21552b6a3cd6ebdbb98336eede217a478f", + "summary": "bgpd: simplify ebgp-multihop and ttl-security handling", + "stats": { + "insertions": 95, + "deletions": 253, + "lines": 348, + "files": 8 + }, + "author": "Timo Teräs", + "author-email": "timo.teras@iki.fi", + "authored_date": 1476882154, + "committed_date": 1485197511, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/e3443a21552b6a3cd6ebdbb98336eede217a478f", + "tags": [], + "state": "under-review" + }, + "f5a4488a0dda521f19e96f2615f4a8b134c5878b": { + "message": "vtysh: Fix, guard against NULL pointer dereference\n\ngetpwuid() may fail returning a null value leaving subsequent\ncode vulnerable to a null pointer dereference.\n\nTested-by: NetDEF CI System \n", + "language": "en", + "commit-id": "f5a4488a0dda521f19e96f2615f4a8b134c5878b", + "summary": "vtysh: Fix, guard against NULL pointer dereference", + "stats": { + "insertions": 5, + "deletions": 1, + "lines": 6, + "files": 1 + }, + "author": "Jafar Al-Gharaibeh", + "author-email": "jafar@atcorp.com", + "authored_date": 1470093278, + "committed_date": 1485192051, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "vuln" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/f5a4488a0dda521f19e96f2615f4a8b134c5878b", + "tags": [], + "state": "under-review" + }, + "cfb1fae25f8c092e0d17073eaf7bd428ce1cd546": { + "message": "zebra: stack overrun in IPv6 RA receive code (CVE-2016-1245)\n\nThe IPv6 RA code also receives ICMPv6 RS and RA messages.\nUnfortunately, by bad coding practice, the buffer size specified on\nreceiving such messages mixed up 2 constants that in fact have\ndifferent values.\n\nThe code itself has:\n #define RTADV_MSG_SIZE 4096\nWhile BUFSIZ is system-dependent, in my case (x86_64 glibc):\n /usr/include/_G_config.h:#define _G_BUFSIZ 8192\n /usr/include/libio.h:#define _IO_BUFSIZ _G_BUFSIZ\n /usr/include/stdio.h:# define BUFSIZ _IO_BUFSIZ\n\nFreeBSD, OpenBSD, NetBSD and Illumos are not affected, since all of them\nhave BUFSIZ == 1024.\n\nAs the latter is passed to the kernel on recvmsg(), it's possible to\noverwrite 4kB of stack -- with ICMPv6 packets that can be globally sent\nto any of the system's addresses (using fragmentation to get to 8k).\n\n(The socket has filters installed limiting this to RS and RA packets,\nbut does not have a filter for source address or TTL.)\n\nIssue discovered by trying to test other stuff, which randomly caused\nthe stack to be smaller than 8kB in that code location, which then\ncauses the kernel to report EFAULT (Bad address).\n\nSigned-off-by: David Lamparter \nReviewed-by: Donald Sharp \n", + "language": "en", + "commit-id": "cfb1fae25f8c092e0d17073eaf7bd428ce1cd546", + "summary": "zebra: stack overrun in IPv6 RA receive code (CVE-2016-1245)", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "David Lamparter", + "author-email": "equinox@opensourcerouting.org", + "authored_date": 1472643076, + "committed_date": 1476722496, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/cfb1fae25f8c092e0d17073eaf7bd428ce1cd546", + "tags": [], + "cve": [ + "CVE-2016-1245" + ], + "state": "cve-assigned" + }, + "2db962760426ddb9e266f9a4bc0b274584c819cc": { + "message": "lib: zclient can overflow (struct interface) hw_addr if zebra is evil\n\n* lib/zclient.c: (zebra_interface_if_set_value) The hw_addr_len field\n is used as trusted input to read off the hw_addr and write to the\n INTERFACE_HWADDR_MAX sized hw_addr field. The read from the stream is\n bounds-checked by the stream abstraction, however the write out to the\n heap can not be.\n\n Tighten the supplied length to stream_get used to do the write.\n\n Impact: a malicious zebra can overflow the heap of clients using the ZServ\n IPC. Note that zebra is already fairly trusted within Quagga.\n\nReported-by: Kostya Kortchinsky \n", + "language": "en", + "commit-id": "2db962760426ddb9e266f9a4bc0b274584c819cc", + "summary": "lib: zclient can overflow (struct interface) hw_addr if zebra is evil", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul.jakma@hpe.com", + "authored_date": 1454942788, + "committed_date": 1457459602, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "malicious" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/2db962760426ddb9e266f9a4bc0b274584c819cc", + "tags": [], + "state": "under-review" + }, + "a3bc7e9400b214a0f078fdb19596ba54214a1442": { + "message": "bgpd: Fix VU#270232, VPNv4 NLRI parser memcpys to stack on unchecked length\n\nAddress CERT vulnerability report VU#270232, memcpy to stack data structure\nbased on length field from packet data whose length field upper-bound was\nnot properly checked.\n\nThis likely allows BGP peers that are enabled to send Labeled-VPN SAFI\nroutes to Quagga bgpd to remotely exploit Quagga bgpd.\n\nMitigation: Do not enable Labeled-VPN SAFI with untrusted neighbours.\n\nImpact: Labeled-VPN SAFI is not enabled by default.\n\n* bgp_mplsvpn.c: (bgp_nlri_parse_vpnv4) The prefixlen is checked for\n lower-bound, but not for upper-bound against received data length.\n The packet data is then memcpy'd to the stack based on the prefixlen.\n\n Extend the prefixlen check to ensure it is within the bound of the NLRI\n packet data AND the on-stack prefix structure AND the maximum size for the\n address family.\n\nReported-by: Kostya Kortchinsky \n\nThis commit a joint effort between:\n\nLou Berger \nDonald Sharp \nPaul Jakma / \n", + "language": "en", + "commit-id": "a3bc7e9400b214a0f078fdb19596ba54214a1442", + "summary": "bgpd: Fix VU#270232, VPNv4 NLRI parser memcpys to stack on unchecked length", + "stats": { + "insertions": 36, + "deletions": 16, + "lines": 52, + "files": 1 + }, + "author": "Donald Sharp", + "author-email": "sharpd@cumulusnetworks.com", + "authored_date": 1453913685, + "committed_date": 1455116527, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "vuln" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/a3bc7e9400b214a0f078fdb19596ba54214a1442", + "tags": [], + "state": "under-review" + }, + "75a3cf6cf69f6ab940f8421b0f79b2b1f689b904": { + "message": "solaris: fix SMF manifest dependency model and start method\n\nResolves an issue where quagga daemons restart in an infinite loop.\nQuagga daemons declare a dependency on zebra that requires a restart\nof the daemon when zebra restarts and they explicitly restart zebra,\nwhich again triggers their own restart.\n\nRestarting zebra when other daemons are started is explicitly removed,\nleaving dependency management up to SMF rather than handling it in the\nstart method.\n\nsolaris/quagga.init.in: Remove calls to routeadm_zebra_enable, and the\n routeadm_zebra_enable function.\nsolaris/quagga.xml.in: Set dependency zebra grouping to require_all.\n\nFixes: #818\nSigned-off-by: Greg Troxel \nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "75a3cf6cf69f6ab940f8421b0f79b2b1f689b904", + "summary": "solaris: fix SMF manifest dependency model and start method", + "stats": { + "insertions": 7, + "deletions": 31, + "lines": 38, + "files": 2 + }, + "author": "Brian Bennett", + "author-email": "brian.bennett@joyent.com", + "authored_date": 1424215572, + "committed_date": 1425276045, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/75a3cf6cf69f6ab940f8421b0f79b2b1f689b904", + "tags": [], + "state": "under-review" + }, + "5d804b439a4138c77f81de30c64f923e2b5c1340": { + "message": "bgpd: support TTL-security with iBGP\n\nTraditionally, ttl-security feature has been associated with EBGP\nsessions as those identify directly connected external peers. The\nGTSM RFC (rfc 5082) does not make any restrictions on type of\npeering. In fact, it is beneficial to support ttl-security for both\nEBGP and IBGP sessions. Specifically, in data centers, there are\ndirectly connected IBGP peerings that will benefit from the protection\nttl-security provides.\n\nSigned-off-by: Dinesh G Dutt \nReviewed-by: Pradosh Mohapatra \n[DL: function refactoring split out into previous 2 patches. changes:\n - bgp_set_socket_ttl(): ret type int -> void\n - is_ebgp_multihop_configured(): stripped peer == NULL check\n - comments/whitespace]\nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "5d804b439a4138c77f81de30c64f923e2b5c1340", + "summary": "bgpd: support TTL-security with iBGP", + "stats": { + "insertions": 62, + "deletions": 26, + "lines": 88, + "files": 4 + }, + "author": "Pradosh Mohapatra", + "author-email": "pmohapat@cumulusnetworks.com", + "authored_date": 1378957027, + "committed_date": 1400534746, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/5d804b439a4138c77f81de30c64f923e2b5c1340", + "tags": [], + "state": "under-review" + }, + "8da8689d91a6436c17aca5000b1426aaea47e23c": { + "message": "bgpd: fix fast external fallover behavior\n\nISSUES\n\n1. When an interface goes down, the zclient callbacks are invoked\n in the following order: (a) address_delete() that removes the\n connected address list: ifp->connected, (b) interface_down()\n that performs \"fast external fallover\" operation. The operation\n relies on ifp->connected to look for peers that should be brought\n down. That's a cyclic dependency.\n\n2. 'ttl-security' configuration handler sets peer->ttl to\n MAXTTL (so that BGP packets are sent with TTL=255, as per the\n requirement of ttl-security). This, however, is incompatible\n with 'fast external fallover' as the fallover operation checks\n for (ttl == 1) to determine directly connected peers.\n\n3. The current fallover operation does not work for IPv6 address family.\n\nPATCH\n\n1. The patch removes the dependency on 'ifp->connected' list for fast\n fallover. The peer already contains a nexthop structure that reflects\n the peering address. The nexthop structure has a pointer to the\n interface (ifp) that peering address resolves to. Everytime the TCP\n connection succeeds, the ifp is updated. The patch uses this ifp in\n the interface_down() callback for a match for the peers that should be\n brought down.\n\n2. The evaluation for directly connected peering is enhanced as\n 'peer->ttl == 1' OR 'peer->gtsm_hops == 1'. Thus a ttl-security\n configuration on the peer with one hop is directly connected and\n should be brought down under 'fast external fallover'.\n\n3. Because of fix (1), IPv6 address family works automatically.\n\nSigned-off-by: Pradosh Mohapatra \nReviewed-by: Dinesh G Dutt \nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "8da8689d91a6436c17aca5000b1426aaea47e23c", + "summary": "bgpd: fix fast external fallover behavior", + "stats": { + "insertions": 3, + "deletions": 9, + "lines": 12, + "files": 1 + }, + "author": "Pradosh Mohapatra", + "author-email": "pmohapat@cumulusnetworks.com", + "authored_date": 1378870435, + "committed_date": 1400534739, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/8da8689d91a6436c17aca5000b1426aaea47e23c", + "tags": [], + "state": "under-review" + }, + "a11e012e8661629d665e992e765741a5eaa7d017": { + "message": "security: Fix some typos and potential NULL-deref\n\nThis patch against the git tree fixes minor typos, some of them possibily\nleading to NULL-pointer dereference in rare conditions.\n\nSigned-off-by: Remi Gacogne \nSigned-off-by: Joachim Nilsson \nAcked-by: Feng Lu \n", + "language": "en", + "commit-id": "a11e012e8661629d665e992e765741a5eaa7d017", + "summary": "security: Fix some typos and potential NULL-deref", + "stats": { + "insertions": 8, + "deletions": 4, + "lines": 12, + "files": 5 + }, + "author": "Remi Gacogne", + "author-email": "rgacogne-github@coredump.fr", + "authored_date": 1378648114, + "committed_date": 1392110883, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/a11e012e8661629d665e992e765741a5eaa7d017", + "tags": [], + "state": "under-review" + }, + "23cd8fb7133befdb84b3a918f7b2f6147161ac6e": { + "message": "ospfd: protect vs. VU#229804 (malformed Router-LSA)\n\nVU#229804 reports that, by injecting Router LSAs with the Advertising\nRouter ID different from the Link State ID, OSPF implementations can be\ntricked into retaining and using invalid information.\n\nQuagga is not vulnerable to this because it looks up Router LSAs by\n(Router-ID, LS-ID) pair. The relevant code is in ospf_lsa.c l.3140.\nNote the double \"id\" parameter at the end.\n\nStill, we can provide an improvement here by discarding such malformed\nLSAs and providing a warning to the administrator. While we cannot\nprevent such malformed LSAs from entering the OSPF domain, we can\ncertainly try to limit their distribution.\n\ncf. http://www.kb.cert.org/vuls/id/229804 for the vulnerability report.\nThis issue is a specification issue in the OSPF protocol that was\ndiscovered by Dr. Gabi Nakibly.\n\nReported-by: CERT Coordination Center \nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "23cd8fb7133befdb84b3a918f7b2f6147161ac6e", + "summary": "ospfd: protect vs. VU#229804 (malformed Router-LSA)", + "stats": { + "insertions": 21, + "deletions": 0, + "lines": 21, + "files": 1 + }, + "author": "David Lamparter", + "author-email": "equinox@diac24.net", + "authored_date": 1375428473, + "committed_date": 1375785706, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "vuln" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/23cd8fb7133befdb84b3a918f7b2f6147161ac6e", + "tags": [], + "state": "under-review" + }, + "c423d413e464913ee88c1ee700e2c4037e6bdb24": { + "message": "lib: unconditionally include stddef.h\n\nI've used offsetof() in the previous commit to paper over the security\nproblems in ospf_api.c. This blows the build on FreeBSD 7.0, missing\noffsetof(). Let's add that to zebra's generally used includes.\n\nstddef.h (and offsetof) is defined in C89 section 4.1.5 (and not\ndeprecated/removed by any later standard). If this causes problems, the\nbug report should go against the host OS/compiler...\n\nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "c423d413e464913ee88c1ee700e2c4037e6bdb24", + "summary": "lib: unconditionally include stddef.h", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "David Lamparter", + "author-email": "equinox@opensourcerouting.org", + "authored_date": 1375191386, + "committed_date": 1375200853, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/c423d413e464913ee88c1ee700e2c4037e6bdb24", + "tags": [], + "state": "under-review" + }, + "c51443f4aa6b7f0b0d6ad5409ad7d4b215092443": { + "message": "ospfd: CVE-2013-2236, stack overrun in apiserver\n\nthe OSPF API-server (exporting the LSDB and allowing announcement of\nOpaque-LSAs) writes past the end of fixed on-stack buffers. This leads\nto an exploitable stack overflow.\n\nFor this condition to occur, the following two conditions must be true:\n- Quagga is configured with --enable-opaque-lsa\n- ospfd is started with the \"-a\" command line option\n\nIf either of these does not hold, the relevant code is not executed and\nthe issue does not get triggered.\n\nSince the issue occurs on receiving large LSAs (larger than 1488 bytes),\nit is possible for this to happen during normal operation of a network.\nIn particular, if there is an OSPF router with a large number of\ninterfaces, the Router-LSA of that router may exceed 1488 bytes and\ntrigger this, leading to an ospfd crash.\n\nFor an attacker to exploit this, s/he must be able to inject valid LSAs\ninto the OSPF domain. Any best-practice protection measure (using\ncrypto authentication, restricting OSPF to internal interfaces, packet\nfiltering protocol 89, etc.) will prevent exploitation. On top of that,\nremote (not on an OSPF-speaking network segment) attackers will have\ndifficulties bringing up the adjacency needed to inject a LSA.\n\nThis patch only performs minimal changes to remove the possibility of a\nstack overrun. The OSPF API in general is quite ugly and needs a\nrewrite.\n\nReported-by: Ricky Charlet \nCc: Florian Weimer \nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "c51443f4aa6b7f0b0d6ad5409ad7d4b215092443", + "summary": "ospfd: CVE-2013-2236, stack overrun in apiserver", + "stats": { + "insertions": 18, + "deletions": 7, + "lines": 25, + "files": 1 + }, + "author": "David Lamparter", + "author-email": "equinox@opensourcerouting.org", + "authored_date": 1373317528, + "committed_date": 1375020790, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/c51443f4aa6b7f0b0d6ad5409ad7d4b215092443", + "tags": [], + "cve": [ + "CVE-2013-2236" + ], + "state": "cve-assigned" + }, + "5e728e929942d39ce5a4ab3d01c33f7b688c4e3f": { + "message": "bgpd: relax ORF capability length handling\n\ncommit fe9bb64... \"bgpd: CVE-2012-1820, DoS in bgp_capability_orf()\"\nmade the length test in bgp_capability_orf_entry() stricter and is now\ncausing us to refuse (with CEASE) ORF capabilites carrying any excess\ndata. This does not conform to the robustness principle as laid out by\nRFC1122 (\"be liberal in what you accept\").\n\nEven worse, RFC5291 is quite unclear on how to use the ORF capability\nwith multiple AFI/SAFIs. It can be interpreted as either \"use one\ninstance, stuff everything in\" but also as \"use multiple instances\".\nSo, if not for applying robustness, we end up clearing sessions from\nimplementations going by the former interpretation. (or if anyone dares\nadd a byte of padding...)\n\nCc: Denis Ovsienko \nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "5e728e929942d39ce5a4ab3d01c33f7b688c4e3f", + "summary": "bgpd: relax ORF capability length handling", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "David Lamparter", + "author-email": "equinox@opensourcerouting.org", + "authored_date": 1358916624, + "committed_date": 1359737704, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/5e728e929942d39ce5a4ab3d01c33f7b688c4e3f", + "tags": [], + "cve": [ + "CVE-2012-1820" + ], + "state": "cve-assigned" + }, + "e8aca32f312cbef1cb0b0dd9e87b7e59dc9fa251": { + "message": "isisd: address Coverity warnings\n\nthis fixes a bunch of issues found by Coverity SCAN and flagged as\n\"high\" impact -- although, they're all rather minute issues.\n\n* isisd/isis_adjacency.c: one superfluous check, one possible NULL deref\n* isisd/isis_circuit.c: two prefix memory leaks\n* isisd/isis_csm.c: one missing break\n* isisd/isis_lsp.c: one possible NULL deref\n* isisd/isis_pfpacket.c: one error-case fd leak\n* isisd/isis_route.c: one isis_route_info memory leak\n* isisd/isis_routemap.c: one... fnord\n* isisd/isis_tlv.c: one infinite loop\n\nReported-by: Coverity SCAN\nSigned-off-by: David Lamparter \n", + "language": "en", + "commit-id": "e8aca32f312cbef1cb0b0dd9e87b7e59dc9fa251", + "summary": "isisd: address Coverity warnings", + "stats": { + "insertions": 19, + "deletions": 7, + "lines": 26, + "files": 9 + }, + "author": "David Lamparter", + "author-email": "equinox@opensourcerouting.org", + "authored_date": 1353978630, + "committed_date": 1355323088, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/e8aca32f312cbef1cb0b0dd9e87b7e59dc9fa251", + "tags": [], + "state": "under-review" + }, + "fe9bb6459afe0d55e56619cdc5061d8407cd1f15": { + "message": "bgpd: CVE-2012-1820, DoS in bgp_capability_orf()\n\nAn ORF (code 3) capability TLV is defined to contain exactly one\nAFI/SAFI block. Function bgp_capability_orf(), which parses ORF\ncapability TLV, uses do-while cycle to call its helper function\nbgp_capability_orf_entry(), which actually processes the AFI/SAFI data\nblock. The call is made at least once and repeated as long as the input\nbuffer has enough data for the next call.\n\nThe helper function, bgp_capability_orf_entry(), uses \"Number of ORFs\"\nfield of the provided AFI/SAFI block to verify, if it fits the input\nbuffer. However, the check is made based on the total length of the ORF\nTLV regardless of the data already consumed by the previous helper\nfunction call(s). This way, the check condition is only valid for the\nfirst AFI/SAFI block inside an ORF capability TLV.\n\nFor the subsequent calls of the helper function, if any are made, the\ncheck condition may erroneously tell, that the current \"Number of ORFs\"\nfield fits the buffer boundary, where in fact it does not. This makes it\npossible to trigger an assertion by feeding an OPEN message with a\nspecially-crafted malformed ORF capability TLV.\n\nThis commit fixes the vulnerability by making the implementation follow\nthe spec.\n", + "language": "en", + "commit-id": "fe9bb6459afe0d55e56619cdc5061d8407cd1f15", + "summary": "bgpd: CVE-2012-1820, DoS in bgp_capability_orf()", + "stats": { + "insertions": 2, + "deletions": 24, + "lines": 26, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1334853253, + "committed_date": 1351836435, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/fe9bb6459afe0d55e56619cdc5061d8407cd1f15", + "tags": [], + "cve": [ + "CVE-2012-1820" + ], + "state": "cve-assigned" + }, + "5861739f8c38bc36ea9955e5cb2be2bf2f482d70": { + "message": "bgpd: Open option parse errors don't NOTIFY, resulting in abort & DoS\n\n* bgp_packet.c: (bgp_open_receive) Errors from bgp_open_option_parse are\n detected, and the code will stop processing the OPEN and return. However\n it does so without calling bgp_notify_send to send a NOTIFY - which means\n the peer FSM doesn't get stopped, and bgp_read will be called again later.\n Because it returns, it doesn't go through the code near the end of the\n function that removes the current message from the peer input streaam.\n Thus the next call to bgp_read will try to parse a half-parsed stream as\n if it were a new BGP message, leading to an assert later in the code when\n it tries to read stuff that isn't there. Add the required call to\n bgp_notify_send before returning.\n* bgp_open.c: (bgp_capability_as4) Be a bit stricter, check the length field\n corresponds to the only value it can be, which is the amount we're going to\n read off the stream. And make sure the capability flag gets set, so\n callers can know this capability was read, regardless.\n (peek_for_as4_capability) Let bgp_capability_as4 do the length check.\n", + "language": "en", + "commit-id": "5861739f8c38bc36ea9955e5cb2be2bf2f482d70", + "summary": "bgpd: Open option parse errors don't NOTIFY, resulting in abort & DoS", + "stats": { + "insertions": 16, + "deletions": 8, + "lines": 24, + "files": 2 + }, + "author": "Paul Jakma", + "author-email": "paul@quagga.net", + "authored_date": 1326142766, + "committed_date": 1330905302, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "DoS" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/5861739f8c38bc36ea9955e5cb2be2bf2f482d70", + "tags": [], + "state": "under-review" + }, + "70e3ca2ccedca2cae58bd91c968714cad0f9d5d6": { + "message": "ospfd: improve fix to CVE-2011-3326 (BZ#586)\n\nMake ospf_flood() propagate error returned by ospf_lsa_install() further\nto properly discard the malformed LSA, not just prevent the immediate\ncrash.\n", + "language": "en", + "commit-id": "70e3ca2ccedca2cae58bd91c968714cad0f9d5d6", + "summary": "ospfd: improve fix to CVE-2011-3326 (BZ#586)", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "Thomas Ries", + "author-email": "tries@gmx.net", + "authored_date": 1319723018, + "committed_date": 1321377770, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/70e3ca2ccedca2cae58bd91c968714cad0f9d5d6", + "tags": [], + "cve": [ + "CVE-2011-3326" + ], + "state": "cve-assigned" + }, + "4de148e5d6f6f7885b2c0952a236a3bc3ec36250": { + "message": "ospfd: improve fix to CVE-2011-3326 (BZ#586)\n\nMake ospf_flood() propagate error returned by ospf_lsa_install() further\nto properly discard the malformed LSA, not just prevent the immediate\ncrash.\n", + "language": "en", + "commit-id": "4de148e5d6f6f7885b2c0952a236a3bc3ec36250", + "summary": "ospfd: improve fix to CVE-2011-3326 (BZ#586)", + "stats": { + "insertions": 1, + "deletions": 1, + "lines": 2, + "files": 1 + }, + "author": "Thomas Ries", + "author-email": "tries@gmx.net", + "authored_date": 1319723018, + "committed_date": 1321375848, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/4de148e5d6f6f7885b2c0952a236a3bc3ec36250", + "tags": [], + "cve": [ + "CVE-2011-3326" + ], + "state": "cve-assigned" + }, + "abc7ef44ca05493500865ce81f7b84f5c4eb6594": { + "message": "ospf6d: CVE-2011-3323 (fortify packet reception)\n\nThis vulnerability (CERT-FI #514840) was reported by CROSS project.\n\nospf6d processes IPv6 prefix structures in incoming packets without\nverifying that the declared prefix length is valid. This leads to a\ncrash\ncaused by out of bounds memory access.\n\n* ospf6_abr.h: new macros for size/alignment validation\n* ospf6_asbr.h: idem\n* ospf6_intra.h: idem\n* ospf6_lsa.h: idem\n* ospf6_message.h: idem\n* ospf6_proto.h: idem\n* ospf6_message.c\n * ospf6_packet_minlen: helper array for ospf6_packet_examin()\n * ospf6_lsa_minlen: helper array for ospf6_lsa_examin()\n * ospf6_hello_recv(): do not call ospf6_header_examin(), let upper\n layer verify the input data\n * ospf6_dbdesc_recv(): idem\n * ospf6_lsreq_recv(): idem\n * ospf6_lsupdate_recv(): idem\n * ospf6_lsack_recv(): idem\n * ospf6_prefixes_examin(): new function, implements A.4.1\n * ospf6_lsa_examin(): new function, implements A.4\n * ospf6_lsaseq_examin(): new function, an interface to above\n * ospf6_packet_examin(): new function, implements A.3\n * ospf6_rxpacket_examin(): new function, replaces\n ospf6_header_examin()\n * ospf6_header_examin(): sayonara\n * ospf6_receive(): perform passive interface check earliest possible,\n employ ospf6_rxpacket_examin()\n", + "language": "en", + "commit-id": "abc7ef44ca05493500865ce81f7b84f5c4eb6594", + "summary": "ospf6d: CVE-2011-3323 (fortify packet reception)", + "stats": { + "insertions": 492, + "deletions": 73, + "lines": 565, + "files": 7 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028731, + "committed_date": 1317048436, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/abc7ef44ca05493500865ce81f7b84f5c4eb6594", + "tags": [], + "cve": [ + "CVE-2011-3323" + ], + "state": "cve-assigned" + }, + "09395e2a0e93b2cf4258cb1de91887948796bb68": { + "message": "ospf6d: CVE-2011-3324 (DD LSA assertion)\n\nThis vulnerability (CERT-FI #514839) was reported by CROSS project.\n\nWhen Database Description LSA header list contains trailing zero octets,\nospf6d tries to process this data as an LSA header. This triggers an\nassertion in the code and ospf6d shuts down.\n\n* ospf6_lsa.c\n * ospf6_lsa_is_changed(): handle header-only argument(s)\n appropriately, do not treat LSA length underrun as a fatal error.\n", + "language": "en", + "commit-id": "09395e2a0e93b2cf4258cb1de91887948796bb68", + "summary": "ospf6d: CVE-2011-3324 (DD LSA assertion)", + "stats": { + "insertions": 11, + "deletions": 1, + "lines": 12, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028716, + "committed_date": 1317048426, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/09395e2a0e93b2cf4258cb1de91887948796bb68", + "tags": [], + "cve": [ + "CVE-2011-3324" + ], + "state": "cve-assigned" + }, + "717750433839762d23a5f8d88fe0b4d57c8d490a": { + "message": "ospfd: CVE-2011-3325 part 2 (OSPF pkt type segv)\n\nThis vulnerability (CERT-FI #514838) was reported by CROSS project.\n\nThe error is reproducible only when ospfd debugging is enabled:\n * debug ospf packet all\n * debug ospf zebra\nWhen incoming packet header type field is set to 0x0a, ospfd will crash.\n\n* ospf_packet.c\n * ospf_verify_header(): add type field check\n * ospf_read(): perform input checks early\n", + "language": "en", + "commit-id": "717750433839762d23a5f8d88fe0b4d57c8d490a", + "summary": "ospfd: CVE-2011-3325 part 2 (OSPF pkt type segv)", + "stats": { + "insertions": 18, + "deletions": 14, + "lines": 32, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028682, + "committed_date": 1317048414, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/717750433839762d23a5f8d88fe0b4d57c8d490a", + "tags": [], + "cve": [ + "CVE-2011-3325" + ], + "state": "cve-assigned" + }, + "61ab0301606053192f45c188bc48afc837518770": { + "message": "ospfd: CVE-2011-3325 part 1 (OSPF header underrun)\n\nThis vulnerability (CERT-FI #514838) was reported by CROSS project.\n\nWhen only 14 first bytes of a Hello packet is delivered, ospfd crashes.\n\n* ospf_packet.c\n * ospf_read(): add size check\n", + "language": "en", + "commit-id": "61ab0301606053192f45c188bc48afc837518770", + "summary": "ospfd: CVE-2011-3325 part 1 (OSPF header underrun)", + "stats": { + "insertions": 12, + "deletions": 3, + "lines": 15, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028672, + "committed_date": 1317048402, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/61ab0301606053192f45c188bc48afc837518770", + "tags": [], + "cve": [ + "CVE-2011-3325" + ], + "state": "cve-assigned" + }, + "6b161fc12a15aba8824c84d1eb38e529aaf70769": { + "message": "ospfd: CVE-2011-3326 (uknown LSA type segfault)\n\nThis vulnerability (CERT-FI #514837) was reported by CROSS project.\nThey have also suggested a fix to the problem, which was found\nacceptable.\n\nQuagga ospfd does not seem to handle unknown LSA types in a Link State\nUpdate message correctly. If LSA type is something else than one\nsupported\nby Quagga, the default handling of unknown types leads to an error.\n\n* ospf_flood.c\n * ospf_flood(): check return value of ospf_lsa_install()\n", + "language": "en", + "commit-id": "6b161fc12a15aba8824c84d1eb38e529aaf70769", + "summary": "ospfd: CVE-2011-3326 (uknown LSA type segfault)", + "stats": { + "insertions": 2, + "deletions": 1, + "lines": 3, + "files": 1 + }, + "author": "CROSS", + "author-email": "info@codenomicon.com", + "authored_date": 1317028641, + "committed_date": 1317048388, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/6b161fc12a15aba8824c84d1eb38e529aaf70769", + "tags": [], + "cve": [ + "CVE-2011-3326" + ], + "state": "cve-assigned" + }, + "94431dbc753171b48b5c6806af97fd690813b00a": { + "message": "bgpd: CVE-2011-3327 (ext. comm. buffer overflow)\n\nThis vulnerability (CERT-FI #513254) was reported by CROSS project.\nThey have also suggested a fix to the problem, which was found\nacceptable.\n\nThe problem occurs when bgpd receives an UPDATE message containing\n255 unknown AS_PATH attributes in Path Attribute Extended Communities.\nThis causes a buffer overlow in bgpd.\n\n* bgp_ecommunity.c\n * ecommunity_ecom2str(): perform size check earlier\n", + "language": "en", + "commit-id": "94431dbc753171b48b5c6806af97fd690813b00a", + "summary": "bgpd: CVE-2011-3327 (ext. comm. buffer overflow)", + "stats": { + "insertions": 7, + "deletions": 7, + "lines": 14, + "files": 1 + }, + "author": "CROSS", + "author-email": "info@codenomicon.com", + "authored_date": 1317028625, + "committed_date": 1317048376, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/94431dbc753171b48b5c6806af97fd690813b00a", + "tags": [], + "cve": [ + "CVE-2011-3327" + ], + "state": "cve-assigned" + }, + "552563a1c443ec876edd92bf79f29ff3afe2c01e": { + "message": "ospf6d: CVE-2011-3323 (fortify packet reception)\n\nThis vulnerability (CERT-FI #514840) was reported by CROSS project.\n\nospf6d processes IPv6 prefix structures in incoming packets without\nverifying that the declared prefix length is valid. This leads to a\ncrash\ncaused by out of bounds memory access.\n\n* ospf6_abr.h: new macros for size/alignment validation\n* ospf6_asbr.h: idem\n* ospf6_intra.h: idem\n* ospf6_lsa.h: idem\n* ospf6_message.h: idem\n* ospf6_proto.h: idem\n* ospf6_message.c\n * ospf6_packet_minlen: helper array for ospf6_packet_examin()\n * ospf6_lsa_minlen: helper array for ospf6_lsa_examin()\n * ospf6_hello_recv(): do not call ospf6_header_examin(), let upper\n layer verify the input data\n * ospf6_dbdesc_recv(): idem\n * ospf6_lsreq_recv(): idem\n * ospf6_lsupdate_recv(): idem\n * ospf6_lsack_recv(): idem\n * ospf6_prefixes_examin(): new function, implements A.4.1\n * ospf6_lsa_examin(): new function, implements A.4\n * ospf6_lsaseq_examin(): new function, an interface to above\n * ospf6_packet_examin(): new function, implements A.3\n * ospf6_rxpacket_examin(): new function, replaces\n ospf6_header_examin()\n * ospf6_header_examin(): sayonara\n * ospf6_receive(): perform passive interface check earliest possible,\n employ ospf6_rxpacket_examin()\n", + "language": "en", + "commit-id": "552563a1c443ec876edd92bf79f29ff3afe2c01e", + "summary": "ospf6d: CVE-2011-3323 (fortify packet reception)", + "stats": { + "insertions": 492, + "deletions": 73, + "lines": 565, + "files": 7 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028731, + "committed_date": 1317048048, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/552563a1c443ec876edd92bf79f29ff3afe2c01e", + "tags": [], + "cve": [ + "CVE-2011-3323" + ], + "state": "cve-assigned" + }, + "308687b7d73c5cacf927a3a33efbfaea627ccc09": { + "message": "ospf6d: CVE-2011-3324 (DD LSA assertion)\n\nThis vulnerability (CERT-FI #514839) was reported by CROSS project.\n\nWhen Database Description LSA header list contains trailing zero octets,\nospf6d tries to process this data as an LSA header. This triggers an\nassertion in the code and ospf6d shuts down.\n\n* ospf6_lsa.c\n * ospf6_lsa_is_changed(): handle header-only argument(s)\n appropriately, do not treat LSA length underrun as a fatal error.\n", + "language": "en", + "commit-id": "308687b7d73c5cacf927a3a33efbfaea627ccc09", + "summary": "ospf6d: CVE-2011-3324 (DD LSA assertion)", + "stats": { + "insertions": 11, + "deletions": 1, + "lines": 12, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028716, + "committed_date": 1317048030, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/308687b7d73c5cacf927a3a33efbfaea627ccc09", + "tags": [], + "cve": [ + "CVE-2011-3324" + ], + "state": "cve-assigned" + }, + "1f54cef38dab072f1054c6cfedd9ac32af14a120": { + "message": "ospfd: CVE-2011-3325 part 2 (OSPF pkt type segv)\n\nThis vulnerability (CERT-FI #514838) was reported by CROSS project.\n\nThe error is reproducible only when ospfd debugging is enabled:\n * debug ospf packet all\n * debug ospf zebra\nWhen incoming packet header type field is set to 0x0a, ospfd will crash.\n\n* ospf_packet.c\n * ospf_verify_header(): add type field check\n * ospf_read(): perform input checks early\n", + "language": "en", + "commit-id": "1f54cef38dab072f1054c6cfedd9ac32af14a120", + "summary": "ospfd: CVE-2011-3325 part 2 (OSPF pkt type segv)", + "stats": { + "insertions": 18, + "deletions": 14, + "lines": 32, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028682, + "committed_date": 1317048019, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/1f54cef38dab072f1054c6cfedd9ac32af14a120", + "tags": [], + "cve": [ + "CVE-2011-3325" + ], + "state": "cve-assigned" + }, + "3d3380d4fda43924171bc0866746c85634952c99": { + "message": "ospfd: CVE-2011-3325 part 1 (OSPF header underrun)\n\nThis vulnerability (CERT-FI #514838) was reported by CROSS project.\n\nWhen only 14 first bytes of a Hello packet is delivered, ospfd crashes.\n\n* ospf_packet.c\n * ospf_read(): add size check\n", + "language": "en", + "commit-id": "3d3380d4fda43924171bc0866746c85634952c99", + "summary": "ospfd: CVE-2011-3325 part 1 (OSPF header underrun)", + "stats": { + "insertions": 12, + "deletions": 3, + "lines": 15, + "files": 1 + }, + "author": "Denis Ovsienko", + "author-email": "infrastation@yandex.ru", + "authored_date": 1317028672, + "committed_date": 1317048007, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/3d3380d4fda43924171bc0866746c85634952c99", + "tags": [], + "cve": [ + "CVE-2011-3325" + ], + "state": "cve-assigned" + }, + "af143a26ef96ba9be7b9c0b151b7605e1c2c74cd": { + "message": "ospfd: CVE-2011-3326 (uknown LSA type segfault)\n\nThis vulnerability (CERT-FI #514837) was reported by CROSS project.\nThey have also suggested a fix to the problem, which was found\nacceptable.\n\nQuagga ospfd does not seem to handle unknown LSA types in a Link State\nUpdate message correctly. If LSA type is something else than one\nsupported\nby Quagga, the default handling of unknown types leads to an error.\n\n* ospf_flood.c\n * ospf_flood(): check return value of ospf_lsa_install()\n", + "language": "en", + "commit-id": "af143a26ef96ba9be7b9c0b151b7605e1c2c74cd", + "summary": "ospfd: CVE-2011-3326 (uknown LSA type segfault)", + "stats": { + "insertions": 2, + "deletions": 1, + "lines": 3, + "files": 1 + }, + "author": "CROSS", + "author-email": "info@codenomicon.com", + "authored_date": 1317028641, + "committed_date": 1317047992, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/af143a26ef96ba9be7b9c0b151b7605e1c2c74cd", + "tags": [], + "cve": [ + "CVE-2011-3326" + ], + "state": "cve-assigned" + }, + "a1afbc6e1d56b06409de5e8d7d984d565817fd96": { + "message": "bgpd: CVE-2011-3327 (ext. comm. buffer overflow)\n\nThis vulnerability (CERT-FI #513254) was reported by CROSS project.\nThey have also suggested a fix to the problem, which was found\nacceptable.\n\nThe problem occurs when bgpd receives an UPDATE message containing\n255 unknown AS_PATH attributes in Path Attribute Extended Communities.\nThis causes a buffer overlow in bgpd.\n\n* bgp_ecommunity.c\n * ecommunity_ecom2str(): perform size check earlier\n", + "language": "en", + "commit-id": "a1afbc6e1d56b06409de5e8d7d984d565817fd96", + "summary": "bgpd: CVE-2011-3327 (ext. comm. buffer overflow)", + "stats": { + "insertions": 7, + "deletions": 7, + "lines": 14, + "files": 1 + }, + "author": "CROSS", + "author-email": "info@codenomicon.com", + "authored_date": 1317028625, + "committed_date": 1317047977, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "CVE" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/a1afbc6e1d56b06409de5e8d7d984d565817fd96", + "tags": [], + "cve": [ + "CVE-2011-3327" + ], + "state": "cve-assigned" + }, + "fc09716b81e67f2d06dc92ff7bcb1efdf18c4eec": { + "message": "bgpd/security: CVE-2010-1674 Fix crash due to extended-community parser error\n\n* bgp_attr.c: (bgp_attr_ext_communities) Certain extended-community attrs\n can leave attr->flag indicating ext-community is present, even though no\n extended-community object has been attached to the attr structure. Thus a\n null-pointer dereference can occur later.\n (bgp_attr_community) No bug fixed here, but tidy up flow so it has same\n form as previous.\n\n Problem and fix thanks to anonymous reporter.\n(cherry picked from commit 0c46638122f10019a12ae9668aec91691cf2e017)\n", + "language": "en", + "commit-id": "fc09716b81e67f2d06dc92ff7bcb1efdf18c4eec", + "summary": "bgpd/security: CVE-2010-1674 Fix crash due to extended-community parser error", + "stats": { + "insertions": 20, + "deletions": 12, + "lines": 32, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@quagga.net", + "authored_date": 1291569446, + "committed_date": 1309798920, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/fc09716b81e67f2d06dc92ff7bcb1efdf18c4eec", + "tags": [], + "cve": [ + "CVE-2010-1674" + ], + "state": "cve-assigned" + }, + "f5a4827db60545309d0ee378b85acac56cf7837a": { + "message": "bgpd: refine the setting up of GTSM\n\n* bgpd.h: Add error code for setting GTSM on iBGP\n* bgpd.c: (peer_ttl_security_hops_set) use previous error code and signal\n incompatibility of GTSM+iBGP to vty.\n Consider the session state when setting GTSM, and reset Open/Active peers\n to let them pick up new TTL from start.\n", + "language": "en", + "commit-id": "f5a4827db60545309d0ee378b85acac56cf7837a", + "summary": "bgpd: refine the setting up of GTSM", + "stats": { + "insertions": 33, + "deletions": 8, + "lines": 41, + "files": 3 + }, + "author": "Stephen Hemminger", + "author-email": "shemminger@vyatta.com", + "authored_date": 1300987821, + "committed_date": 1301308061, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/f5a4827db60545309d0ee378b85acac56cf7837a", + "tags": [], + "state": "under-review" + }, + "d876bdf4a84f40ac3f9bec8d5040858b3725db3e": { + "message": "lib: Add support for IPv6 ttl security\n\n* sockunion.c: (sockopt_minttl) Add IPv6 support for min hop count.\n The kernel support is Linux kernel 2.6.35 or later.\n", + "language": "en", + "commit-id": "d876bdf4a84f40ac3f9bec8d5040858b3725db3e", + "summary": "lib: Add support for IPv6 ttl security", + "stats": { + "insertions": 19, + "deletions": 11, + "lines": 30, + "files": 1 + }, + "author": "Stephen Hemminger", + "author-email": "shemminger@vyatta.com", + "authored_date": 1281029187, + "committed_date": 1300965521, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/d876bdf4a84f40ac3f9bec8d5040858b3725db3e", + "tags": [], + "state": "under-review" + }, + "89b6d1f8e2759cc38bc768067abe3a296d93f454": { + "message": "bgpd: Cleanups & fixes for minttl / GTSM\n\n* bgp_vty.c: (peer_ebgp_multihop_{un,}set_vty) tail-call cleanup.\n ({no_,}neighbor_ttl_security) ditto.\n* bgpd.c: (peer_ttl_security_hops_set) Peer group checks and TTL set only\n need to be done on transition.\n* sockunion.c: (sockopt_minttl) remove always-on debug and improve readability.\n", + "language": "en", + "commit-id": "89b6d1f8e2759cc38bc768067abe3a296d93f454", + "summary": "bgpd: Cleanups & fixes for minttl / GTSM", + "stats": { + "insertions": 41, + "deletions": 51, + "lines": 92, + "files": 3 + }, + "author": "Stephen Hemminger", + "author-email": "shemminger@vyatta.com", + "authored_date": 1300963919, + "committed_date": 1300963919, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/89b6d1f8e2759cc38bc768067abe3a296d93f454", + "tags": [], + "state": "under-review" + }, + "fa411a212b55bba650d68fd0456686f3e47b7395": { + "message": "bgpd: RFC 5082 Generalized TTL Security Mechanism support\n\n* bgpd: Add support for RFC 5082 GTSM, which allows the TTL field to be used\n to verify that incoming packets have been sent from neighbours no more\n than X IP hops away. In other words, this allows packets that were sent from\n further away (i.e. not by the neighbour with known distance, and so possibly\n a miscreant) to be filtered out.\n* lib/sockunion.{c,h}: (sockopt_minttl) new function, to set a minimum TTL\n using the IP_MINTTL socket opt.\n* bgpd.h: (BGP_ERR_NO_EBGP_MULTIHOP_WITH_TTLHACK) define for command\n error for minttl.\n (struct peer) add a config variable, to store the configured minttl.\n (peer_ttl_security_hops_{set,unset}) configuration handlers\n* bgpd.c: (peer_group_get) init gtsm_hops\n (peer_ebgp_multihop_{un,}set) check for conflicts with GTSM. Multihop and\n GTSM can't both be active for a peer at the same time.\n (peer_ttl_security_hops_set) set minttl, taking care to avoid conflicts with\n ebgp_multihop.\n (bgp_config_write_peer) write out minttl as \"neighbor .. ttl-security hops X\".\n* bgp_vty.c: (bgp_vty_return) message for\n BGP_ERR_NO_EBGP_MULTIHOP_WITH_TTLHACK\n (peer_ebgp_multihop_{un,}set_vty)\n* bgp_network.c: (bgp_accept) set minttl on accepted sockets if appropriate.\n (bgp_connect) ditto for outbound.\n", + "language": "en", + "commit-id": "fa411a212b55bba650d68fd0456686f3e47b7395", + "summary": "bgpd: RFC 5082 Generalized TTL Security Mechanism support", + "stats": { + "insertions": 256, + "deletions": 11, + "lines": 267, + "files": 6 + }, + "author": "Nick Hilliard", + "author-email": "nick@inex.ie", + "authored_date": 1300894397, + "committed_date": 1300894397, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "Security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/fa411a212b55bba650d68fd0456686f3e47b7395", + "tags": [], + "state": "under-review" + }, + "0c46638122f10019a12ae9668aec91691cf2e017": { + "message": "bgpd/security: CVE-2010-1674 Fix crash due to extended-community parser error\n\n* bgp_attr.c: (bgp_attr_ext_communities) Certain extended-community attrs\n can leave attr->flag indicating ext-community is present, even though no\n extended-community object has been attached to the attr structure. Thus a\n null-pointer dereference can occur later.\n (bgp_attr_community) No bug fixed here, but tidy up flow so it has same\n form as previous.\n\n Problem and fix thanks to anonymous reporter.\n", + "language": "en", + "commit-id": "0c46638122f10019a12ae9668aec91691cf2e017", + "summary": "bgpd/security: CVE-2010-1674 Fix crash due to extended-community parser error", + "stats": { + "insertions": 20, + "deletions": 12, + "lines": 32, + "files": 1 + }, + "author": "Paul Jakma", + "author-email": "paul@quagga.net", + "authored_date": 1291569446, + "committed_date": 1300715456, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/0c46638122f10019a12ae9668aec91691cf2e017", + "tags": [], + "cve": [ + "CVE-2010-1674" + ], + "state": "cve-assigned" + }, + "e26873fd8f0c4306eff65de94a45b4114fc81b98": { + "message": "zebra: fix infinite loop when deleting an interface\n\nWhen deleting a VLAN interface after flushing its\naddresses, zebra uses 100% CPU time and freezes.\n\n * interface.c: The while loop in line 407 that\n should clean up connected routes never hits one\n of the 2 lines \"last = node;\" and thus loops\n forever.\n\nSigned-off-by: Roman Hoog Antink \n", + "language": "en", + "commit-id": "e26873fd8f0c4306eff65de94a45b4114fc81b98", + "summary": "zebra: fix infinite loop when deleting an interface", + "stats": { + "insertions": 4, + "deletions": 0, + "lines": 4, + "files": 1 + }, + "author": "Roman Hoog Antink", + "author-email": "rha@open.ch", + "authored_date": 1273068050, + "committed_date": 1273075413, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/e26873fd8f0c4306eff65de94a45b4114fc81b98", + "tags": [], + "state": "under-review" + }, + "d023aec49f70156d2ed894a8fba65bcfa221ff02": { + "message": "bgpd: start listener on first instance\n\nStart BGP listener only after first instance is started. This helps the\nsecurity if BGP is not used but daemon is started. It also addresses some\nissues like MD5 not working on listener unless IPV6 configured (because\nlistener was not in list); as well as compiler warnings.\n\n* bgp_network.c: (bgp_listener) listen socket creation consolidated here\n (bgp_socket) Use bgp_listener\n* bgpd.c: (bgp_get) call bgp_socket on creation of first struct bgp.\n (bgp_init) remove bgp_socket call.\n* memtypes.c: Add MTYPE_BGP_LISTENER\n", + "language": "en", + "commit-id": "d023aec49f70156d2ed894a8fba65bcfa221ff02", + "summary": "bgpd: start listener on first instance", + "stats": { + "insertions": 114, + "deletions": 94, + "lines": 208, + "files": 4 + }, + "author": "Stephen Hemminger", + "author-email": "shemminger@vyatta.com", + "authored_date": 1248218841, + "committed_date": 1248771878, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/d023aec49f70156d2ed894a8fba65bcfa221ff02", + "tags": [], + "state": "under-review" + }, + "370b64a2ad38e43b4bed028960481bbf4192becd": { + "message": "[bgpd] Fix number of DoS security issues, restricted to configured peers.\n\n2007-12-22 Paul Jakma \n\n\t* Fix series of vulnerabilities reported by \"Mu Security\n\t Research Team\", where bgpd can be made to crash by sending\n\t malformed packets - requires that bgpd be configured with a\n\t session to the peer.\n\t* bgp_attr.c: (bgp_attr_as4_path) aspath_parse may fail, only\n\t set the attribute flag indicating AS4_PATH if we actually managed\n\t to parse one.\n\t (bgp_attr_munge_as4_attrs) Assert was too general, it is possible\n\t to receive AS4_AGGREGATOR before AGGREGATOR.\n\t (bgp_attr_parse) Check that we have actually received the extra\n\t byte of header for Extended-Length attributes.\n\t* bgp_attr.h: Fix BGP_ATTR_MIN_LEN to account for the length byte.\n\t* bgp_open.c: (cap_minsizes) Fix size of CAPABILITY_CODE_RESTART,\n\t incorrect -2 left in place from a development version of as4-path\n\t patch.\n\t* bgp_packet.c: (bgp_route_refresh_receive) ORF length parameter\n\t needs to be properly sanity checked.\n\t* tests/bgp_capability_test.c: Test for empty capabilities.\n", + "language": "en", + "commit-id": "370b64a2ad38e43b4bed028960481bbf4192becd", + "summary": "[bgpd] Fix number of DoS security issues, restricted to configured peers.", + "stats": { + "insertions": 87, + "deletions": 8, + "lines": 95, + "files": 7 + }, + "author": "Paul Jakma", + "author-email": "paul.jakma@sun.com", + "authored_date": 1198342192, + "committed_date": 1198342192, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "DoS" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/370b64a2ad38e43b4bed028960481bbf4192becd", + "tags": [], + "state": "under-review" + }, + "b2ceea18074ab8cca894051a3fbc30c312e3acc6": { + "message": "[bgpd] low-impact DoS: crash on malformed community with debug set\n\n2007-09-07 Paul Jakma \n\n\t* (general) bgpd can be made crash by remote peers if debug\n\t bgp updates is set, due to NULL pointer dereference.\n\t Reported by \"Mu Security Research Team\",\n\t .\n\t* bgp_attr.c: (bgp_attr_community) If community length is 0,\n\t don't set the community-present attribute bit, just return\n\t early.\n\t* bgp_debug.c: (community_str,community_com2str) Check com\n\t pointer before dereferencing.\n", + "language": "en", + "commit-id": "b2ceea18074ab8cca894051a3fbc30c312e3acc6", + "summary": "[bgpd] low-impact DoS: crash on malformed community with debug set", + "stats": { + "insertions": 22, + "deletions": 1, + "lines": 23, + "files": 3 + }, + "author": "Paul Jakma", + "author-email": "paul.jakma@sun.com", + "authored_date": 1189175095, + "committed_date": 1189175095, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "DoS" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/b2ceea18074ab8cca894051a3fbc30c312e3acc6", + "tags": [], + "state": "under-review" + }, + "5f03f141eced8bad4971fcc6ec7d7a538c227d8c": { + "message": "[docs] Update ripd docs on version and authentication, see bugs #261,#262\n\n2006-05-04 Paul Jakma \n\n\t* ripd.texi: Add Version Control as a distinct section.\n\t Expand Version Control section with overview text,\n\t touching on insecurity of RIPv1 and referencing\n\t authentication section, cleanup text of various version\n\t commands.\n\t RIP Authentication: Add overview text, refer to RIPv1 version\n\t control, which is required to completely secure RIP.\n", + "language": "en", + "commit-id": "5f03f141eced8bad4971fcc6ec7d7a538c227d8c", + "summary": "[docs] Update ripd docs on version and authentication, see bugs #261,#262", + "stats": { + "insertions": 86, + "deletions": 31, + "lines": 117, + "files": 2 + }, + "author": "Paul Jakma", + "author-email": "paul.jakma@sun.com", + "authored_date": 1146728257, + "committed_date": 1146728257, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "security" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/5f03f141eced8bad4971fcc6ec7d7a538c227d8c", + "tags": [], + "state": "under-review" + }, + "15aa6a1a732eef1049dbc64d7ede9236772cafcf": { + "message": "[bgpd] Fix infinite loop in community_str2com\n\n2006-03-30 Paul Jakma \n\n\t* bgp_community.c: (community_gettoken) Unknown token should\n\t return NULL, to give a strong indication to callers that\n\t the token no longer can be parsed, otherwise callers looping\n\t on this function may have a hard time ending their loop.\n\t (community_str2com) While loop around community_gettoken appears\n\t to have been coded thinking that break statement would break\n\t from the while{}, hence it could never exit for unknown token\n\t case. Fix it to do..while, so it can use the NULL result from\n\t community_gettoken easily.\n", + "language": "en", + "commit-id": "15aa6a1a732eef1049dbc64d7ede9236772cafcf", + "summary": "[bgpd] Fix infinite loop in community_str2com", + "stats": { + "insertions": 20, + "deletions": 6, + "lines": 26, + "files": 2 + }, + "author": "Paul Jakma", + "author-email": "paul.jakma@sun.com", + "authored_date": 1143729575, + "committed_date": 1143729575, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/15aa6a1a732eef1049dbc64d7ede9236772cafcf", + "tags": [], + "state": "under-review" + }, + "9dbc797274ca5df614d61784658b8f809bbd8e2b": { + "message": "2005-03-13 Andrew J. Schorr \n\n\t* ospf_lsa.c: (ospf_lsa_refresh_walker) If the system clock jumps\n\t backward, then current time may be less than\n\t ospf->lsa_refresher_started. This was causing invalid values\n\t for ospf->lsa_refresh_queue.index resulting in infinite loops.\n\t Problem fixed by casting the expression to unsigned before taking\n\t the modulus.\n\n\t[backport candidate]\n", + "language": "en", + "commit-id": "9dbc797274ca5df614d61784658b8f809bbd8e2b", + "summary": "2005-03-13 Andrew J. Schorr ", + "stats": { + "insertions": 15, + "deletions": 3, + "lines": 18, + "files": 2 + }, + "author": "ajs", + "author-email": "ajs", + "authored_date": 1110742042, + "committed_date": 1110742042, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/9dbc797274ca5df614d61784658b8f809bbd8e2b", + "tags": [], + "state": "under-review" + }, + "cced60dd5bf297d16ec61fad75a122deaeca9e20": { + "message": "004-07-13 David Wiggins \n\n * lib/vty.c: (vty_telnet_option) Remote DoS exists if a telnet\n end-sub-negotation is sent when no sub-negotation data has been\n sent. Return immediately if no sub-negotation is in progress.\n (vty_read) do not attempt to process options if no sub-negotation\n is in progress.\n", + "language": "en", + "commit-id": "5b8c1b0d6af736b0633309b4b3490298b9a20742", + "summary": "2003-10-15 Jay Fenlason ", + "stats": { + "insertions": 11, + "deletions": 6, + "lines": 17, + "files": 1 + }, + "author": "paul", + "author-email": "paul", + "authored_date": 1066259335, + "committed_date": 1066259335, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "DoS" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/5b8c1b0d6af736b0633309b4b3490298b9a20742", + "tags": [], + "state": "under-review" + }, + "90578521e5f332e65e97f7612485d04ace5c0ba5": { + "message": "2003-09-24 sowmini.varadhan@sun.com\n\n\t* lib/if.c: (if_cmp_func) fix infinite loop if\n\t ifp1->name == ifp2->name\n", + "language": "en", + "commit-id": "90578521e5f332e65e97f7612485d04ace5c0ba5", + "summary": "2003-09-24 sowmini.varadhan@sun.com", + "stats": { + "insertions": 6, + "deletions": 1, + "lines": 7, + "files": 1 + }, + "author": "paul", + "author-email": "paul", + "authored_date": 1064360761, + "committed_date": 1064360761, + "branches": [ + "master" + ], + "pattern-selected": "(?i)(denial of service|\\bXXE\\b|remote code execution|\\bopen redirect|OSVDB|\\bvuln|\\bCVE\\b|\\bXSS\\b|\\bReDoS\\b|\\bNVD\\b|malicious|x−frame−options|attack|cross site|exploit|malicious|directory traversal|\\bRCE\\b|\\bdos\\b|\\bXSRF \\b|\\bXSS\\b|clickjack|session.fixation|hijack|\\badvisory|\\binsecure|security|\\bcross−origin\\b|unauthori[z|s]ed|infinite loop)", + "pattern-matches": [ + "infinite loop" + ], + "origin": "https://git.savannah.nongnu.org/git/quagga.git", + "origin-github-api": "https://api.github.com/repos///git.savannah.nongnu.org/git/quagga/commits/90578521e5f332e65e97f7612485d04ace5c0ba5", + "tags": [], + "state": "under-review" + } +} diff --git a/tests/mispevent_testfiles/attribute.json b/tests/mispevent_testfiles/attribute.json index 8ad4843..c839dff 100644 --- a/tests/mispevent_testfiles/attribute.json +++ b/tests/mispevent_testfiles/attribute.json @@ -1,23 +1,21 @@ { - "Event": { - "Attribute": [ - { - "Tag": [ - { - "name": "osint" - } - ], - "category": "Payload delivery", - "disable_correlation": false, - "to_ids": true, - "type": "filename", - "value": "bar.exe" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Attribute": [ + { + "Tag": [ + { + "name": "osint" + } + ], + "category": "Payload delivery", + "disable_correlation": false, + "to_ids": true, + "type": "filename", + "value": "bar.exe" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/attribute_del.json b/tests/mispevent_testfiles/attribute_del.json index d381cfe..912cbaf 100644 --- a/tests/mispevent_testfiles/attribute_del.json +++ b/tests/mispevent_testfiles/attribute_del.json @@ -1,25 +1,23 @@ { - "Event": { - "Attribute": [ - { - "Tag": [ - { - "name": "osint" - } - ], - "category": "Payload delivery", - "deleted": true, - "disable_correlation": false, - "id": "42", - "to_ids": true, - "type": "filename", - "value": "bar.exe" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Attribute": [ + { + "Tag": [ + { + "name": "osint" + } + ], + "category": "Payload delivery", + "deleted": true, + "disable_correlation": false, + "id": "42", + "to_ids": true, + "type": "filename", + "value": "bar.exe" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/def_param.json b/tests/mispevent_testfiles/def_param.json index 9658189..de954eb 100644 --- a/tests/mispevent_testfiles/def_param.json +++ b/tests/mispevent_testfiles/def_param.json @@ -1,55 +1,53 @@ { - "Event": { - "Object": [ - { - "Attribute": [ - { - "category": "Attribution", - "disable_correlation": false, - "object_relation": "registrar", - "to_ids": false, - "type": "whois-registrar", - "value": "registar.example.com" - }, - { - "category": "Network activity", - "disable_correlation": false, - "object_relation": "domain", - "to_ids": true, - "type": "domain", - "value": "domain.example.com" - }, - { - "category": "Network activity", - "disable_correlation": true, - "object_relation": "nameserver", - "to_ids": false, - "type": "hostname", - "value": "ns1.example.com" - }, - { - "category": "External analysis", - "disable_correlation": false, - "object_relation": "nameserver", - "to_ids": true, - "type": "hostname", - "value": "ns2.example.com" - } - ], - "description": "Whois records information for a domain name or an IP address.", - "distribution": "5", - "meta-category": "network", - "name": "whois", - "sharing_group_id": "0", - "template_uuid": "429faea1-34ff-47af-8a00-7c62d3be5a6a", - "template_version": "10", - "uuid": "a" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Object": [ + { + "Attribute": [ + { + "category": "Attribution", + "disable_correlation": false, + "object_relation": "registrar", + "to_ids": false, + "type": "whois-registrar", + "value": "registar.example.com" + }, + { + "category": "Network activity", + "disable_correlation": false, + "object_relation": "domain", + "to_ids": true, + "type": "domain", + "value": "domain.example.com" + }, + { + "category": "Network activity", + "disable_correlation": true, + "object_relation": "nameserver", + "to_ids": false, + "type": "hostname", + "value": "ns1.example.com" + }, + { + "category": "External analysis", + "disable_correlation": false, + "object_relation": "nameserver", + "to_ids": true, + "type": "hostname", + "value": "ns2.example.com" + } + ], + "description": "Whois records information for a domain name or an IP address.", + "distribution": "5", + "meta-category": "network", + "name": "whois", + "sharing_group_id": "0", + "template_uuid": "429faea1-34ff-47af-8a00-7c62d3be5a6a", + "template_version": "10", + "uuid": "a" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/event.json b/tests/mispevent_testfiles/event.json index 0dcc796..0d0c7ba 100644 --- a/tests/mispevent_testfiles/event.json +++ b/tests/mispevent_testfiles/event.json @@ -1,10 +1,8 @@ { - "Event": { - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "published": true, - "threat_level_id": "1" - } + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "published": true, + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/event_obj_attr_tag.json b/tests/mispevent_testfiles/event_obj_attr_tag.json index 87ad93a..bc46d0a 100644 --- a/tests/mispevent_testfiles/event_obj_attr_tag.json +++ b/tests/mispevent_testfiles/event_obj_attr_tag.json @@ -1,59 +1,57 @@ { - "Event": { - "Object": [ - { - "Attribute": [ - { - "Tag": [ - { - "name": "blah" - } - ], - "category": "Payload delivery", - "disable_correlation": true, - "object_relation": "filename", - "to_ids": true, - "type": "filename", - "value": "bar" - } - ], - "ObjectReference": [ - { - "comment": "foo", - "object_uuid": "a", - "referenced_uuid": "b", - "relationship_type": "baz" - } - ], - "description": "File object describing a file with meta-information", - "distribution": "5", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", - "uuid": "a" - }, - { - "Attribute": [ - { - "category": "Network activity", - "disable_correlation": false, - "object_relation": "url", - "to_ids": true, - "type": "url", - "value": "https://www.circl.lu" - } - ], - "description": "url object describes an url along with its normalized field (like extracted using faup parsing library) and its metadata.", - "distribution": "5", - "meta-category": "network", - "name": "url", - "sharing_group_id": "0", - "template_uuid": "60efb77b-40b5-4c46-871b-ed1ed999fce5", - "template_version": "7", - "uuid": "b" - } - ] - } + "Object": [ + { + "Attribute": [ + { + "Tag": [ + { + "name": "blah" + } + ], + "category": "Payload delivery", + "disable_correlation": true, + "object_relation": "filename", + "to_ids": true, + "type": "filename", + "value": "bar" + } + ], + "ObjectReference": [ + { + "comment": "foo", + "object_uuid": "a", + "referenced_uuid": "b", + "relationship_type": "baz" + } + ], + "description": "File object describing a file with meta-information", + "distribution": "5", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "25", + "uuid": "a" + }, + { + "Attribute": [ + { + "category": "Network activity", + "disable_correlation": false, + "object_relation": "url", + "to_ids": true, + "type": "url", + "value": "https://www.circl.lu" + } + ], + "description": "url object describes an url along with its normalized field (like extracted using faup parsing library) and its metadata.", + "distribution": "5", + "meta-category": "network", + "name": "url", + "sharing_group_id": "0", + "template_uuid": "60efb77b-40b5-4c46-871b-ed1ed999fce5", + "template_version": "10", + "uuid": "b" + } + ] } diff --git a/tests/mispevent_testfiles/event_obj_def_param.json b/tests/mispevent_testfiles/event_obj_def_param.json index b6857e8..1519139 100644 --- a/tests/mispevent_testfiles/event_obj_def_param.json +++ b/tests/mispevent_testfiles/event_obj_def_param.json @@ -1,56 +1,62 @@ { - "Event": { - "Object": [ - { - "Attribute": [ - { - "Tag": [ - { - "name": "blah" - } - ], - "category": "Payload delivery", - "disable_correlation": true, - "object_relation": "filename", - "to_ids": true, - "type": "filename", - "value": "bar" - } - ], - "description": "File object describing a file with meta-information", - "distribution": "5", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", - "uuid": "a" - }, - { - "Attribute": [ - { - "Tag": [ - { - "name": "blah" - } - ], - "category": "Payload delivery", - "disable_correlation": true, - "object_relation": "filename", - "to_ids": true, - "type": "filename", - "value": "baz" - } - ], - "description": "File object describing a file with meta-information", - "distribution": "5", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", - "uuid": "b" - } - ] - } + "Object": [ + { + "Attribute": [ + { + "Tag": [ + { + "name": "blah" + } + ], + "category": "Payload delivery", + "disable_correlation": true, + "object_relation": "filename", + "to_ids": true, + "type": "filename", + "value": "bar" + }, + { + "category": "Artifacts dropped", + "disable_correlation": false, + "object_relation": "pattern-in-file", + "to_ids": true, + "type": "pattern-in-file", + "value": "baz" + } + ], + "description": "File object describing a file with meta-information", + "distribution": "5", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "25", + "uuid": "a" + }, + { + "Attribute": [ + { + "Tag": [ + { + "name": "blah" + } + ], + "category": "Payload delivery", + "disable_correlation": true, + "object_relation": "filename", + "to_ids": true, + "type": "filename", + "value": "baz" + } + ], + "description": "File object describing a file with meta-information", + "distribution": "5", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "25", + "uuid": "b" + } + ] } diff --git a/tests/mispevent_testfiles/event_obj_tag.json b/tests/mispevent_testfiles/event_obj_tag.json index 1542d8b..40e2098 100644 --- a/tests/mispevent_testfiles/event_obj_tag.json +++ b/tests/mispevent_testfiles/event_obj_tag.json @@ -1,31 +1,29 @@ { - "Event": { - "Object": [ - { - "Attribute": [ - { - "category": "Payload delivery", - "disable_correlation": false, - "object_relation": "filename", - "to_ids": true, - "type": "filename", - "value": "bar" - } - ], - "Tag": [ - { - "name": "osint" - } - ], - "description": "File object describing a file with meta-information", - "distribution": 5, - "meta-category": "file", - "name": "file", - "sharing_group_id": 0, - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": 9, - "uuid": "a" - } - ] - } + "Object": [ + { + "Attribute": [ + { + "category": "Payload delivery", + "disable_correlation": false, + "object_relation": "filename", + "to_ids": true, + "type": "filename", + "value": "bar" + } + ], + "Tag": [ + { + "name": "osint" + } + ], + "description": "File object describing a file with meta-information", + "distribution": 5, + "meta-category": "file", + "name": "file", + "sharing_group_id": 0, + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": 9, + "uuid": "a" + } + ] } diff --git a/tests/mispevent_testfiles/event_tags.json b/tests/mispevent_testfiles/event_tags.json index b099b7b..dd9eba3 100644 --- a/tests/mispevent_testfiles/event_tags.json +++ b/tests/mispevent_testfiles/event_tags.json @@ -1,20 +1,18 @@ { - "Event": { - "Tag": [ - { - "name": "bar" - }, - { - "name": "baz" - }, - { - "name": "foo" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Tag": [ + { + "name": "bar" + }, + { + "name": "baz" + }, + { + "name": "foo" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/existing_event.json b/tests/mispevent_testfiles/existing_event.json index 5f8eac6..a123662 100644 --- a/tests/mispevent_testfiles/existing_event.json +++ b/tests/mispevent_testfiles/existing_event.json @@ -1,4573 +1,4599 @@ { - "Event": { - "Attribute": [ - { - "Tag": [ - { - "colour": "#00223b", - "exportable": true, - "hide_tag": false, - "id": "101", - "name": "osint:source-type=\"blog-post\"", - "user_id": "0" - }, - { - "colour": "#007cd6", - "exportable": true, - "hide_tag": false, - "id": "618", - "name": "osint:certainty=\"93\"", - "user_id": "0" - } - ], - "category": "External analysis", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188757", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893921", - "to_ids": false, - "type": "link", - "uuid": "5a3c2fda-78f4-44b7-8366-46da02de0b81", - "value": "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" - }, - { - "Tag": [ - { - "colour": "#00223b", - "exportable": true, - "hide_tag": false, - "id": "101", - "name": "osint:source-type=\"blog-post\"", - "user_id": "0" - }, - { - "colour": "#007cd6", - "exportable": true, - "hide_tag": false, - "id": "618", - "name": "osint:certainty=\"93\"", - "user_id": "0" - } - ], - "category": "External analysis", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188758", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893921", - "to_ids": false, - "type": "text", - "uuid": "5a3c2fee-7c8c-438a-8f7f-465402de0b81", - "value": "The Sednit group — also known as Strontium, APT28, Fancy Bear or Sofacy — is a group of attackers operating since 2004, if not earlier, and whose main objective is to steal confidential information from specific targets.\r\n\r\nThis article is a follow-up to ESET’s presentation at BlueHat in November 2017. Late in 2016 we published a white paper covering Sednit activity between 2014 and 2016. Since then, we have continued to actively track Sednit’s operations, and today we are publishing a brief overview of what our tracking uncovered in terms of the group’s activities and updates to their toolset. The first section covers the update of their attack methodology: namely, the ways in which this group tries to compromise their targets systems. The second section covers the evolution of their tools, with a particular emphasis on a detailed analysis of a new version of their flagship malware: Xagent." - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188759", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "value": "movieultimate.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188760", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "value": "meteost.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188761", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188762", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "value": "nethostnet.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188763", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "value": "fsportal.net" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188764", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "value": "fastdataexchange.org" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188765", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - } - ], - "Galaxy": [ - { - "GalaxyCluster": [ - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Thomas Schreck", - "Timo Steffens", - "Various" - ], - "description": "The Sofacy Group (also known as APT28, Pawn Storm, Fancy Bear and Sednit) is a cyber espionage group believed to have ties to the Russian government. Likely operating since 2007, the group is known to target government, military, and security organizations. It has been characterized as an advanced persistent threat.", - "galaxy_id": "366", - "id": "45563", - "meta": { - "country": [ - "RU" - ], - "refs": [ - "https://en.wikipedia.org/wiki/Sofacy_Group", - "https://aptnotes.malwareconfig.com/web/viewer.html?file=../APTnotes/2014/apt28.pdf", - "http://www.trendmicro.com/cloud-content/us/pdfs/security-intelligence/white-papers/wp-operation-pawn-storm.pdf", - "https://www2.fireeye.com/rs/848-DID-242/images/wp-mandiant-matryoshka-mining.pdf", - "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/", - "http://researchcenter.paloaltonetworks.com/2016/06/unit42-new-sofacy-attacks-against-us-government-agency/" - ], - "synonyms": [ - "APT 28", - "APT28", - "Pawn Storm", - "Fancy Bear", - "Sednit", - "TsarTeam", - "TG-4127", - "Group-4127", - "STRONTIUM", - "TAG_0700", - "Swallowtail", - "IRON TWILIGHT", - "Group 74" - ] - }, - "source": "MISP Project", - "tag_id": "1100", - "tag_name": "misp-galaxy:threat-actor=\"Sofacy\"", - "type": "threat-actor", - "uuid": "7cdff317-a673-4474-84ec-4f1754947823", - "value": "Sofacy", - "version": "30" - } - ], - "description": "Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.", - "icon": "user-secret", - "id": "366", - "name": "Threat Actor", - "type": "threat-actor", - "uuid": "698774c7-8022-42c4-917f-8d6e4f06ada3", - "version": "2" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "Kafeine", - "Will Metcalf", - "KahuSecurity" - ], - "description": "Sednit EK is the exploit kit used by APT28", - "galaxy_id": "370", - "id": "38813", - "meta": { - "refs": [ - "http://www.welivesecurity.com/2014/10/08/sednit-espionage-group-now-using-custom-exploit-kit/", - "http://blog.trendmicro.com/trendlabs-security-intelligence/new-adobe-flash-zero-day-used-in-pawn-storm-campaign/" - ], - "status": [ - "Active" - ] - }, - "source": "MISP Project", - "tag_id": "3007", - "tag_name": "misp-galaxy:exploit-kit=\"Sednit EK\"", - "type": "exploit-kit", - "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", - "value": "Sednit EK", - "version": "5" - }, - { - "authors": [ - "Kafeine", - "Will Metcalf", - "KahuSecurity" - ], - "description": "DealersChoice is a Flash Player Exploit platform triggered by RTF", - "galaxy_id": "370", - "id": "38805", - "meta": { - "refs": [ - "http://researchcenter.paloaltonetworks.com/2016/10/unit42-dealerschoice-sofacys-flash-player-exploit-platform/", - "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-ramps-up-spear-phishing-before-zero-days-get-patched/" - ], - "status": [ - "Active" - ], - "synonyms": [ - "Sednit RTF EK" - ] - }, - "source": "MISP Project", - "tag_id": "3015", - "tag_name": "misp-galaxy:exploit-kit=\"DealersChoice\"", - "type": "exploit-kit", - "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", - "value": "DealersChoice", - "version": "5" - } - ], - "description": "Exploit-Kit is an enumeration of some exploitation kits used by adversaries. The list includes document, browser and router exploit kits.It's not meant to be totally exhaustive but aim at covering the most seen in the past 5 years", - "icon": "internet-explorer", - "id": "370", - "name": "Exploit-Kit", - "type": "exploit-kit", - "uuid": "6ab240ec-bd79-11e6-a4a6-cec0c932ce01", - "version": "3" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "backdoor", - "galaxy_id": "367", - "id": "46592", - "meta": { - "refs": [ - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" - ], - "synonyms": [ - "Sednit", - "Seduploader", - "JHUHUGIT", - "Sofacy" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "2215", - "tag_name": "misp-galaxy:tool=\"GAMEFISH\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "GAMEFISH", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "", - "galaxy_id": "367", - "id": "46670", - "meta": { - "synonyms": [ - "XTunnel" - ] - }, - "source": "MISP Project", - "tag_id": "1012", - "tag_name": "misp-galaxy:tool=\"X-Tunnel\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "X-Tunnel", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "backdoor used by apt28\n\nSedreco serves as a spying backdoor; its functionalities can be extended with dynamically loaded plugins. It is made up of two distinct components: a dropper and the persistent payload installed by this dropper. We have not seen this component since April 2016.", - "galaxy_id": "367", - "id": "46591", - "meta": { - "possible_issues": [ - "Report tells that is could be Xagent alias (Java Rat)" - ], - "refs": [ - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" - ], - "synonyms": [ - "Sedreco", - "AZZY", - "ADVSTORESHELL", - "NETUI" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "3011", - "tag_name": "misp-galaxy:tool=\"EVILTOSS\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "EVILTOSS", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "This backdoor component is known to have a modular structure featuring various espionage functionalities, such as key-logging, screen grabbing and file exfiltration. This component is available for Osx, Windows, Linux and iOS operating systems.\n\nXagent is a modular backdoor with spying functionalities such as keystroke logging and file exfiltration. Xagent is the group’s flagship backdoor and heavily used in their operations. Early versions for Linux and Windows were seen years ago, then in 2015 an iOS version came out. One year later, an Android version was discovered and finally, in the beginning of 2017, an Xagent sample for OS X was described.", - "galaxy_id": "367", - "id": "46669", - "meta": { - "refs": [ - "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-update-ios-espionage-app-found/", - "https://app.box.com/s/l7n781ig6n8wlf1aff5hgwbh4qoi5jqq", - "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" - ], - "synonyms": [ - "XAgent" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "1011", - "tag_name": "misp-galaxy:tool=\"X-Agent\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "X-Agent", - "version": "45" - } - ], - "description": "Threat actors tools is an enumeration of tools used by adversaries. The list includes malware but also common software regularly used by the adversaries.", - "icon": "optin-monster", - "id": "367", - "name": "Tool", - "type": "tool", - "uuid": "9b8037f7-bc8f-4de1-a797-37266619bc0b", - "version": "2" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "MITRE" - ], - "description": "JHUHUGIT is malware used by APT28. It is based on Carberp source code and serves as reconnaissance malware.[[Citation: Kaspersky Sofacy]][[Citation: F-Secure Sofacy 2015]][[Citation: ESET Sednit Part 1]][[Citation: FireEye APT28 January 2017]]\n\nAliases: JHUHUGIT, Seduploader, JKEYSKW, Sednit, GAMEFISH", - "galaxy_id": "365", - "id": "41618", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0044", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part1.pdf", - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", - "https://labsblog.f-secure.com/2015/09/08/sofacy-recycles-carberp-and-metasploit-code/", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "JHUHUGIT", - "Seduploader", - "JKEYSKW", - "Sednit", - "GAMEFISH" - ], - "uuid": [ - "8ae43c46-57ef-47d5-a77a-eebb35628db2" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3008", - "tag_name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "JHUHUGIT", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "XTunnel a VPN-like network proxy tool that can relay traffic between a C2 server and a victim. It was first seen in May 2013 and reportedly used by APT28 during the compromise of the Democratic National Committee.[[Citation: Crowdstrike DNC June 2016]][[Citation: Invincea XTunnel]][[Citation: ESET Sednit Part 2]]\n\nAliases: XTunnel, X-Tunnel, XAPS", - "galaxy_id": "365", - "id": "41543", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0117", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://www.invincea.com/2016/07/tunnel-of-gov-dnc-hack-and-the-russian-xtunnel/", - "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/" - ], - "synonyms": [ - "XTunnel", - "X-Tunnel", - "XAPS" - ], - "uuid": [ - "7343e208-7cab-45f2-a47b-41ba5e2f0fab" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3009", - "tag_name": "misp-galaxy:mitre-malware=\"XTunnel\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "XTunnel", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "ADVSTORESHELL is a spying backdoor that has been used by APT28 from at least 2012 to 2016. It is generally used for long-term espionage and is deployed on targets deemed interesting after a reconnaissance phase.[[Citation: Kaspersky Sofacy]][[Citation: ESET Sednit Part 2]]\n\nAliases: ADVSTORESHELL, NETUI, EVILTOSS, AZZY, Sedreco", - "galaxy_id": "365", - "id": "41582", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0045", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "ADVSTORESHELL", - "NETUI", - "EVILTOSS", - "AZZY", - "Sedreco" - ], - "uuid": [ - "fb575479-14ef-41e9-bfab-0b7cf10bec73" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3010", - "tag_name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "ADVSTORESHELL", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "USBStealer is malware that has used by APT28 since at least 2005 to extract information from air-gapped networks. It does not have the capability to communicate over the Internet and has been used in conjunction with ADVSTORESHELL.[[Citation: ESET Sednit USBStealer 2014]][[Citation: Kaspersky Sofacy]]\n\nAliases: USBStealer, USB Stealer, Win32/USBStealer", - "galaxy_id": "365", - "id": "41549", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0136", - "http://www.welivesecurity.com/2014/11/11/sednit-espionage-group-attacking-air-gapped-networks/", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "USBStealer", - "USB Stealer", - "Win32/USBStealer" - ], - "uuid": [ - "af2ad3b7-ab6a-4807-91fd-51bcaff9acbb" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3012", - "tag_name": "misp-galaxy:mitre-malware=\"USBStealer\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "USBStealer", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "is a trojan that has been used by APT28 on OS X and appears to be a port of their standard CHOPSTICK or XAgent trojan.[[Citation: XAgentOSX]]", - "galaxy_id": "365", - "id": "41551", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0161", - "https://researchcenter.paloaltonetworks.com/2017/02/unit42-xagentosx-sofacys-xagent-macos-tool/" - ], - "uuid": [ - "5930509b-7793-4db9-bdfc-4edda7709d0d" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3013", - "tag_name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "XAgentOSX", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "CHOPSTICK is malware family of modular backdoors used by APT28. It has been used from at least November 2012 to August 2016 and is usually dropped on victims as second-stage malware, though it has been used as first-stage malware in several cases.[[Citation: FireEye APT28]][[Citation: ESET Sednit Part 2]][[Citation: FireEye APT28 January 2017]]\n\nAliases: CHOPSTICK, SPLM, Xagent, X-Agent, webhp", - "galaxy_id": "365", - "id": "41559", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0023", - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/rpt-apt28.pdf" - ], - "synonyms": [ - "CHOPSTICK", - "SPLM", - "Xagent", - "X-Agent", - "webhp" - ], - "uuid": [ - "ccd61dfc-b03f-4689-8c18-7c97eab08472" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3014", - "tag_name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "CHOPSTICK", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "Downdelph is a first-stage downloader written in Delphi that has been used by APT28 in rare instances between 2013 and 2015.[[Citation: ESET Sednit Part 3]]\n\nAliases: Downdelph, Delphacy", - "galaxy_id": "365", - "id": "41504", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0134", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part3.pdf" - ], - "synonyms": [ - "Downdelph", - "Delphacy" - ], - "uuid": [ - "08d20cd2-f084-45ee-8558-fa6ef5a18519" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3016", - "tag_name": "misp-galaxy:mitre-malware=\"Downdelph\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "Downdelph", - "version": "4" - } - ], - "description": "Name of ATT&CK software", - "icon": "optin-monster", - "id": "365", - "name": "Malware", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "version": "4" - } - ], - "Object": [ - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188944", - "object_id": "1555", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936310", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd5b6-2850-435f-bd0d-4c62950d210f", - "value": "Bulletin.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188945", - "object_id": "1555", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936310", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd5b6-78a8-4e47-8333-4c62950d210f", - "value": "68064fc152e23d56e541714af52651cb4ba81aaf" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188946", - "object_id": "1555", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936310", - "to_ids": false, - "type": "text", - "uuid": "5a3cd5b6-23d8-43ba-8518-4c62950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.AX", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1555", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936310", - "uuid": "5a3cd5b6-9568-4342-b2ab-4c62950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188947", - "object_id": "1556", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936388", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd604-748c-4fc0-88bf-c170950d210f", - "value": "f3805382ae2e23ff1147301d131a06e00e4ff75f" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188948", - "object_id": "1556", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936388", - "to_ids": false, - "type": "text", - "uuid": "5a3cd604-6668-4469-a1c0-c170950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.CVE-2016-4117.A", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1556", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936388", - "uuid": "5a3cd604-e11c-4de5-bbbf-c170950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188949", - "object_id": "1557", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd693-dc40-445d-a4d7-4ae0950d210f", - "value": "OC_PSO_2017.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188950", - "object_id": "1557", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd693-8ffc-4d95-b522-4e84950d210f", - "value": "512bdfe937314ac3f195c462c395feeb36932971" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188951", - "object_id": "1557", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": false, - "type": "text", - "uuid": "5a3cd693-a8f0-4aea-a834-4097950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NUB", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1557", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936531", - "uuid": "5a3cd693-fd9c-4fcf-b69a-439c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188952", - "object_id": "1558", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd6c2-d31c-40cc-bcc1-4458950d210f", - "value": "NASAMS.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188953", - "object_id": "1558", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd6c2-6a54-4b4c-8748-4c84950d210f", - "value": "30b3e8c0f3f3cf200daa21c267ffab3cad64e68b" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188954", - "object_id": "1558", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": false, - "type": "text", - "uuid": "5a3cd6c2-1c68-45de-8325-464a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1558", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936578", - "uuid": "5a3cd6c2-d290-4787-910f-4e6d950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188955", - "object_id": "1559", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd74e-584c-45b9-8557-486d950d210f", - "value": "Programm_Details.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188956", - "object_id": "1559", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd74e-f334-4e6b-b37f-462f950d210f", - "value": "4173b29a251cd9c1cab135f67cb60acab4ace0c5" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188957", - "object_id": "1559", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": false, - "type": "text", - "uuid": "5a3cd74e-5900-4fbf-85c6-4c81950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1559", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936718", - "uuid": "5a3cd74e-1504-40ff-9a28-4501950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188958", - "object_id": "1560", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd775-e8f4-465a-aca2-4c5a950d210f", - "value": "Operation_in_Mosul.rtf" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188959", - "object_id": "1560", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd775-1190-4db7-961a-4c5a950d210f", - "value": "12a37cfdd3f3671074dd5b0f354269cec028fb52" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188960", - "object_id": "1560", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": false, - "type": "text", - "uuid": "5a3cd775-fa5c-4453-bcb0-4c5a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1560", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936757", - "uuid": "5a3cd775-e4cc-44bb-89b6-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188961", - "object_id": "1561", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd82f-b918-4520-ba8b-5165950d210f", - "value": "ARM-NATO_ENGLISH_30_NOV_2016.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188962", - "object_id": "1561", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd82f-cae4-4209-9338-5165950d210f", - "value": "15201766bd964b7c405aeb11db81457220c31e46" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188963", - "object_id": "1561", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": false, - "type": "text", - "uuid": "5a3cd82f-d91c-43af-8262-5165950d210f", - "value": "Malicious" - } - ], - "comment": "SWF/Agent.L", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1561", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936943", - "uuid": "5a3cd82f-2788-4561-bbeb-5165950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188964", - "object_id": "1562", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd847-0aa0-4b5c-aa30-5165950d210f", - "value": "Olympic-Agenda-2020-20-20-Recommendations.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188965", - "object_id": "1562", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd847-593c-4985-8756-5165950d210f", - "value": "8078e411fbe33864dfd8f87ad5105cc1fd26d62e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188966", - "object_id": "1562", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": false, - "type": "text", - "uuid": "5a3cd847-1324-4fad-af60-5165950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.BL", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1562", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936967", - "uuid": "5a3cd847-b5a0-42f7-ac4b-5165950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188967", - "object_id": "1563", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd861-9350-40c1-ac29-4771950d210f", - "value": "Merry_Christmas!.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188968", - "object_id": "1563", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd861-18ac-4cf0-b96f-4986950d210f", - "value": "33447383379ca99083442b852589111296f0c603" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188969", - "object_id": "1563", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": false, - "type": "text", - "uuid": "5a3cd861-cfbc-4096-baae-40e2950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NUG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1563", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936993", - "uuid": "5a3cd861-65c0-4b69-9429-4f37950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188970", - "object_id": "1564", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd87d-fa9c-41aa-897f-49a5950d210f", - "value": "Trump’s_Attack_on_Syria_English.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188971", - "object_id": "1564", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd87d-c630-4487-8336-4615950d210f", - "value": "d5235d136cfcadbef431eea7253d80bde414db9d" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188972", - "object_id": "1564", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": false, - "type": "text", - "uuid": "5a3cd87d-8c98-4660-9026-44de950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NWZ", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1564", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937021", - "uuid": "5a3cd87d-f514-4071-a5f7-4ec2950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188973", - "object_id": "1565", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd897-4cc0-48b0-bb2c-461f950d210f", - "value": "Hotel_Reservation_Form.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188974", - "object_id": "1565", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd897-fa64-466c-9421-49c5950d210f", - "value": "f293a2bfb728060c54efeeb03c5323893b5c80df" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188975", - "object_id": "1565", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": false, - "type": "text", - "uuid": "5a3cd897-f020-44cf-8dfc-4225950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1565", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937046", - "uuid": "5a3cd896-f6cc-4e52-bcb2-442c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188976", - "object_id": "1566", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937070", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd8ae-7194-48fd-810e-4c5a950d210f", - "value": "SB_Doc_2017-3_Implementation_of_Key_Taskings_and_Next_Steps.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188977", - "object_id": "1566", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937071", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8af-f39c-443c-bcf1-4c5a950d210f", - "value": "bb10ed5d59672fbc6178e35d0feac0562513e9f0" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188978", - "object_id": "1566", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937071", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8af-b3ec-478a-b585-4c5a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1566", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937070", - "uuid": "5a3cd8ae-54d0-46bb-adbb-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188979", - "object_id": "1567", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937083", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8bb-74d8-4d19-ae08-4043950d210f", - "value": "4873bafe44cff06845faa0ce7c270c4ce3c9f7b9" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188980", - "object_id": "1567", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937083", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8bb-77bc-4cc4-887f-429d950d210f", - "value": "Malicious" - } - ], - "comment": "", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1567", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937083", - "uuid": "5a3cd8bb-a704-4f1d-a235-444e950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188981", - "object_id": "1568", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937097", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8c9-4d2c-4145-a637-4f13950d210f", - "value": "169c8f3e3d22e192c108bc95164d362ce5437465" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188982", - "object_id": "1568", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937097", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8c9-7ff0-42f7-ae80-4eb6950d210f", - "value": "Malicious" - } - ], - "comment": "", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1568", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937097", - "uuid": "5a3cd8c9-6568-406a-853c-4862950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188983", - "object_id": "1569", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937116", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8dc-48c0-4ea0-a67d-4734950d210f", - "value": "cc7607015cd7a1a4452acd3d87adabdd7e005bd7" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188984", - "object_id": "1569", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937116", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8dc-9ed8-4a4d-9ceb-4daa950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1569", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937115", - "uuid": "5a3cd8db-2838-4466-a986-4afb950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188985", - "object_id": "1570", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd8fb-1efc-4059-ae7a-42f5950d210f", - "value": "Caucasian_Eagle_ENG.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188986", - "object_id": "1570", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8fb-9cec-4a30-8b2f-4441950d210f", - "value": "5d2c7d87995cc5b8184baba2c7a1900a48b2f42d" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188987", - "object_id": "1570", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8fb-e52c-489b-8da5-43d1950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTM", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1570", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937147", - "uuid": "5a3cd8fb-cd14-4b00-9710-430c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188988", - "object_id": "1571", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd90e-5eb4-4069-b160-5276950d210f", - "value": "World War3.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188989", - "object_id": "1571", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd90e-6d2c-4ffc-a699-5276950d210f", - "value": "7aada8bcc0d1ab8ffb1f0fae4757789c6f5546a3" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188990", - "object_id": "1571", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": false, - "type": "text", - "uuid": "5a3cd90e-28e8-410e-8033-5276950d210f", - "value": "Malicious" - } - ], - "comment": "SWF/Exploit.CVE-2017-11292.A", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1571", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937166", - "uuid": "5a3cd90e-538c-4b7e-95dc-5276950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188991", - "object_id": "1572", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd927-e810-4d22-a0e4-4057950d210f", - "value": "SaberGuardian2017.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188992", - "object_id": "1572", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd927-f284-43b9-83d1-473b950d210f", - "value": "68c2809560c7623d2307d8797691abf3eafe319a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188993", - "object_id": "1572", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": false, - "type": "text", - "uuid": "5a3cd927-b844-49f2-a1a9-4c85950d210f", - "value": "Malicious" - } - ], - "comment": "VBA/DDE.E", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1572", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937191", - "uuid": "5a3cd927-e410-489c-abfc-4b63950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188994", - "object_id": "1573", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd93c-2438-4dda-823e-463d950d210f", - "value": "IsisAttackInNewYork.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188995", - "object_id": "1573", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd93c-1ef0-4d81-9476-4655950d210f", - "value": "1c6c700ceebfbe799e115582665105caa03c5c9e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188996", - "object_id": "1573", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": false, - "type": "text", - "uuid": "5a3cd93c-949c-40ac-9094-4a4a950d210f", - "value": "Malicious" - } - ], - "comment": "VBA/DDE.L", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1573", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937212", - "uuid": "5a3cd93c-716c-4918-a00f-4671950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188997", - "object_id": "1574", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937559", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cda97-7e58-4642-aaf5-c5ed950d210f", - "value": "6f0fc0ebba3e4c8b26a69cdf519edf8d1aa2f4bb" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188998", - "object_id": "1574", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937559", - "to_ids": false, - "type": "text", - "uuid": "5a3cda97-6020-423d-9d23-c5ed950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "value": "movieultimate.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "159", - "object_id": "1574", - "object_uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f", - "referenced_id": "1188759", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513937826", - "uuid": "5a3cdba2-2fdc-4f9a-a4eb-4dae950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1574", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937826", - "uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188999", - "object_id": "1575", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937864", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdbc8-0aac-4d8a-8c1f-4c5a950d210f", - "value": "e19f753e514f6adec8f81bcdefb9117979e69627" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189000", - "object_id": "1575", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937864", - "to_ids": false, - "type": "text", - "uuid": "5a3cdbc8-e204-4606-b9ea-4c5a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "value": "meteost.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "160", - "object_id": "1575", - "object_uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f", - "referenced_id": "1188760", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938091", - "uuid": "5a3cdcab-8200-4c65-868e-42a9950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1575", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938091", - "uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189001", - "object_id": "1576", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937910", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdbf6-eca0-4c09-9bd0-4c59950d210f", - "value": "961468ddd3d0fa25beb8210c81ba620f9170ed30" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189002", - "object_id": "1576", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937910", - "to_ids": false, - "type": "text", - "uuid": "5a3cdbf6-acd8-4a36-a028-4c59950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "164", - "object_id": "1576", - "object_uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f", - "referenced_id": "1188761", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938210", - "uuid": "5a3cdd22-b7d8-4754-a108-4742950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1576", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938210", - "uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189003", - "object_id": "1577", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937929", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc09-b428-4c0b-9969-c5ed950d210f", - "value": "a0719b50265505c8432616c0a4e14ed206981e95" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189004", - "object_id": "1577", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937929", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc09-05d8-4356-ba52-c5ed950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "value": "nethostnet.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "162", - "object_id": "1577", - "object_uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f", - "referenced_id": "1188762", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938169", - "uuid": "5a3cdcf9-d5a4-4c8e-a201-45b1950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1577", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938169", - "uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189005", - "object_id": "1578", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937953", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc21-a170-4637-b139-4812950d210f", - "value": "2cf6436b99d11d9d1e0c488af518e35162ecbc9c" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189006", - "object_id": "1578", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937953", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc21-3274-4800-9e91-41e2950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "165", - "object_id": "1578", - "object_uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f", - "referenced_id": "1188761", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938226", - "uuid": "5a3cdd32-3044-4895-8f18-4d06950d210f" - } - ], - "comment": "Win64/Sednit.Y", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1578", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938226", - "uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189007", - "object_id": "1579", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937975", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc37-cee0-43d0-9e20-4db6950d210f", - "value": "fec29b4f4dccc59770c65c128dfe4564d7c13d33" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189008", - "object_id": "1579", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937976", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc38-ac24-44be-a1ed-4935950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "value": "fsportal.net" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "163", - "object_id": "1579", - "object_uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f", - "referenced_id": "1188763", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938189", - "uuid": "5a3cdd0d-d990-42ba-830d-5156950d210f" - } - ], - "comment": "Win64/Sednit.Y", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1579", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938190", - "uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189009", - "object_id": "1580", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937992", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc48-c74c-4b6e-8202-5156950d210f", - "value": "57d7f3d31c491f8aef4665ca4dd905c3c8a98795" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189010", - "object_id": "1580", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937992", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc48-55dc-420e-9b5d-5156950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "value": "fastdataexchange.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "161", - "object_id": "1580", - "object_uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f", - "referenced_id": "1188764", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938129", - "uuid": "5a3cdcd1-c6cc-43d8-a2f4-4681950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1580", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938129", - "uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189011", - "object_id": "1581", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513938011", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc5b-54a8-4e60-bc67-4c5a950d210f", - "value": "a3bf5b5cf5a5ef438a198a6f61f7225c0a4a7138" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189012", - "object_id": "1581", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513938011", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc5b-b390-4183-aec7-4c5a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "168", - "object_id": "1581", - "object_uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f", - "referenced_id": "1188765", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938280", - "uuid": "5a3cdd68-7968-40d1-a0a9-5156950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1581", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938280", - "uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189013", - "object_id": "1582", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513938034", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc72-ba30-4ecd-9d21-4654950d210f", - "value": "1958e722afd0dba266576922abc98aa505cf5f9a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189014", - "object_id": "1582", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513938034", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc72-0804-42c4-acfa-4ac5950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "167", - "object_id": "1582", - "object_uuid": "5a3cdc72-1538-4c66-af46-427b950d210f", - "referenced_id": "1188765", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938264", - "uuid": "5a3cdd58-9800-4bae-837c-4f20950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1582", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938264", - "uuid": "5a3cdc72-1538-4c66-af46-427b950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189015", - "object_id": "1583", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939882", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3aa-e104-481e-a7f4-4bc1950d210f", - "value": "9f6bed7d7f4728490117cbc85819c2e6c494251b" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189016", - "object_id": "1583", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939882", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3aa-74fc-48c7-af40-4c6a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "173", - "object_id": "1583", - "object_uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f", - "referenced_id": "1592", - "referenced_type": "1", - "referenced_uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513947459", - "uuid": "5a3d0143-c300-4118-8afe-4a2d950d210f" - } - ], - "comment": "Win32/Sednit.AX\t", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1583", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948642", - "uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189017", - "object_id": "1584", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939907", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3c3-6d9c-48f4-93db-4a61950d210f", - "value": "4bc722a9b0492a50bd86a1341f02c74c0d773db7" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189018", - "object_id": "1584", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939907", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3c3-c38c-4e30-a904-4c8f950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "188", - "object_id": "1584", - "object_uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f", - "referenced_id": "1603", - "referenced_type": "1", - "referenced_uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948518", - "uuid": "5a3d0566-34fc-4a62-b2a5-4f91950d210f" - } - ], - "comment": "Win32/Sednit.BS", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1584", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948535", - "uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189019", - "object_id": "1585", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939924", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3d4-9168-4e23-8b64-485a950d210f", - "value": "ab354807e687993fbeb1b325eb6e4ab38d428a1e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189020", - "object_id": "1585", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939924", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3d4-27e0-4366-943f-4b9a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "189", - "object_id": "1585", - "object_uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f", - "referenced_id": "1602", - "referenced_type": "1", - "referenced_uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948528", - "uuid": "5a3d0570-a86c-4264-a43a-4125950d210f" - } - ], - "comment": "Win32/Sednit.BS", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1585", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948597", - "uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189021", - "object_id": "1586", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939946", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3ea-8dbc-4cf4-997f-448b950d210f", - "value": "9c47ca3883196b3a84d67676a804ff50e22b0a9f" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189022", - "object_id": "1586", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939946", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3ea-e714-444e-ad9b-40b0950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "190", - "object_id": "1586", - "object_uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f", - "referenced_id": "1601", - "referenced_type": "1", - "referenced_uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948614", - "uuid": "5a3d05c6-0618-4520-9549-48a0950d210f" - } - ], - "comment": "Win32/Sednit.BR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1586", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948626", - "uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189023", - "object_id": "1587", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939972", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce404-7bfc-4316-bd32-55ea950d210f", - "value": "8a68f26d01372114f660e32ac4c9117e5d0577f1" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189024", - "object_id": "1587", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939972", - "to_ids": false, - "type": "text", - "uuid": "5a3ce404-7224-4525-922a-55ea950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "182", - "object_id": "1587", - "object_uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f", - "referenced_id": "1600", - "referenced_type": "1", - "referenced_uuid": "5a3ce680-90d4-478d-95db-48a6950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948044", - "uuid": "5a3d038c-1cc8-4d9c-87ab-c5ed950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1587", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948073", - "uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189025", - "object_id": "1588", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939991", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce417-62a4-4d46-9a87-55ea950d210f", - "value": "476fc1d31722ac26b46154cbf0c631d60268b28a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189026", - "object_id": "1588", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939991", - "to_ids": false, - "type": "text", - "uuid": "5a3ce417-43f0-494d-ac2e-55ea950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "187", - "object_id": "1588", - "object_uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f", - "referenced_id": "1599", - "referenced_type": "1", - "referenced_uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948483", - "uuid": "5a3d0543-8f74-4086-aafc-418a950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1588", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948498", - "uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189027", - "object_id": "1589", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940012", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce42c-836c-49e7-a9f3-4a5f950d210f", - "value": "f9fd3f1d8da4ffd6a494228b934549d09e3c59d1" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189028", - "object_id": "1589", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940012", - "to_ids": false, - "type": "text", - "uuid": "5a3ce42c-4c88-4940-94b8-4084950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "183", - "object_id": "1589", - "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", - "referenced_id": "1594", - "referenced_type": "1", - "referenced_uuid": "5a3ce60a-6db8-4212-b194-4339950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948106", - "uuid": "5a3d03ca-2398-4060-b13c-404a950d210f" - }, - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "184", - "object_id": "1589", - "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", - "referenced_id": "1595", - "referenced_type": "1", - "referenced_uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948117", - "uuid": "5a3d03d5-6d8c-4dfb-b193-4002950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1589", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948128", - "uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189029", - "object_id": "1590", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940027", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce43b-6738-4a14-a318-4d65950d210f", - "value": "e338d49c270baf64363879e5eecb8fa6bdde8ad9" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189030", - "object_id": "1590", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940027", - "to_ids": false, - "type": "text", - "uuid": "5a3ce43b-3a10-4d78-9ee2-485c950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "186", - "object_id": "1590", - "object_uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f", - "referenced_id": "1593", - "referenced_type": "1", - "referenced_uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948320", - "uuid": "5a3d04a0-9d28-47c3-a12c-465b950d210f" - } - ], - "comment": "Win32/Sednit.BG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1590", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948339", - "uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189031", - "object_id": "1591", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940042", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce44a-2ea4-4526-8bbc-c328950d210f", - "value": "6e167da3c5d887fa2e58da848a2245d11b6c5ad6" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189032", - "object_id": "1591", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940042", - "to_ids": false, - "type": "text", - "uuid": "5a3ce44a-5118-4142-97f0-c328950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "170", - "object_id": "1591", - "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", - "referenced_id": "1597", - "referenced_type": "1", - "referenced_uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513940734", - "uuid": "5a3ce6fe-b0c4-44df-a609-419a950d210f" - }, - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "171", - "object_id": "1591", - "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", - "referenced_id": "1598", - "referenced_type": "1", - "referenced_uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513940753", - "uuid": "5a3ce711-a0dc-4dbe-b59e-495a950d210f" - } - ], - "comment": "Win32/Sednit.BG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1591", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513940753", - "uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189033", - "object_id": "1592", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940362", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce58a-fcd8-48d5-8b4a-4fd9950d210f", - "value": "87.236.211.182" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189034", - "object_id": "1592", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940362", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce58a-6e14-48ea-9746-48f2950d210f", - "value": "servicecdp.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1592", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940362", - "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189035", - "object_id": "1593", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940472", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce5f8-99b4-41a2-915a-4bf8950d210f", - "value": "95.215.45.43" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189036", - "object_id": "1593", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940472", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce5f8-62c8-4f04-89c2-4aeb950d210f", - "value": "wmdmediacodecs.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1593", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940472", - "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189037", - "object_id": "1594", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940490", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce60a-cc50-4553-bfff-4ea9950d210f", - "value": "89.45.67.144" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189038", - "object_id": "1594", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940491", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce60b-e648-4667-8432-4ba8950d210f", - "value": "mvband.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1594", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940490", - "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189039", - "object_id": "1595", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940506", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce61a-4458-4c36-866e-44e9950d210f", - "value": "89.33.246.117" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189040", - "object_id": "1595", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940506", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce61a-f820-4a43-b3d9-47e5950d210f", - "value": "mvtband.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1595", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940506", - "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189041", - "object_id": "1596", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940542", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce63e-66d4-483f-bae6-44f6950d210f", - "value": "87.236.211.182" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189042", - "object_id": "1596", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940542", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce63e-0d88-405b-82a9-43b5950d210f", - "value": "servicecdp.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1596", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940542", - "uuid": "5a3ce63e-0240-46f5-b9ed-4759950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189043", - "object_id": "1597", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940558", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce64e-d7a8-4817-a132-4c72950d210f", - "value": "185.156.173.70" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189044", - "object_id": "1597", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940558", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce64e-243c-4931-b733-403c950d210f", - "value": "runvercheck.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1597", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940558", - "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189045", - "object_id": "1598", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940572", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce65c-bf78-4b78-bafd-4cf6950d210f", - "value": "191.101.31.96" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189046", - "object_id": "1598", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940572", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce65c-8140-4146-a927-45e4950d210f", - "value": "remsupport.org" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1598", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940572", - "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189047", - "object_id": "1599", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940591", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce66f-150c-43ec-a3ff-4aa5950d210f", - "value": "89.187.150.44" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189048", - "object_id": "1599", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940591", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce66f-466c-478e-8064-4b42950d210f", - "value": "viters.org" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1599", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940590", - "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189049", - "object_id": "1600", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940608", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce680-7b04-466d-b187-4301950d210f", - "value": "146.185.253.132" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189050", - "object_id": "1600", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940608", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce680-12f4-4001-9f86-4aa4950d210f", - "value": "myinvestgroup.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1600", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940608", - "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189051", - "object_id": "1601", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940621", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce68d-0108-4557-8921-4377950d210f", - "value": "86.106.131.141" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189052", - "object_id": "1601", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940622", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce68e-54d0-4c67-8c4c-4dea950d210f", - "value": "space-delivery.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1601", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940621", - "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189054", - "object_id": "1602", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940642", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce6a2-4a38-4b90-8d74-4f10950d210f", - "value": "89.34.111.160" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189055", - "object_id": "1602", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940642", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce6a2-ffa4-4afb-89ab-42a6950d210f", - "value": "satellitedeluxpanorama.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1602", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940641", - "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189056", - "object_id": "1603", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940654", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce6ae-601c-44b8-8eec-4a5f950d210f", - "value": "185.216.35.26" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189057", - "object_id": "1603", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940654", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce6ae-3b00-420a-82fd-45fb950d210f", - "value": "webviewres.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1603", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940654", - "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" - } - ], - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + "Attribute": [ + { + "Tag": [ + { + "colour": "#00223b", + "exportable": true, + "hide_tag": false, + "id": "101", + "name": "osint:source-type=\"blog-post\"", + "user_id": "0" + }, + { + "colour": "#007cd6", + "exportable": true, + "hide_tag": false, + "id": "618", + "name": "osint:certainty=\"93\"", + "user_id": "0" + } + ], + "category": "External analysis", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188757", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893921", + "to_ids": false, + "type": "link", + "uuid": "5a3c2fda-78f4-44b7-8366-46da02de0b81", + "value": "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + { + "Tag": [ + { + "colour": "#00223b", + "exportable": true, + "hide_tag": false, + "id": "101", + "name": "osint:source-type=\"blog-post\"", + "user_id": "0" + }, + { + "colour": "#007cd6", + "exportable": true, + "hide_tag": false, + "id": "618", + "name": "osint:certainty=\"93\"", + "user_id": "0" + } + ], + "category": "External analysis", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188758", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893921", + "to_ids": false, + "type": "text", + "uuid": "5a3c2fee-7c8c-438a-8f7f-465402de0b81", + "value": "The Sednit group — also known as Strontium, APT28, Fancy Bear or Sofacy — is a group of attackers operating since 2004, if not earlier, and whose main objective is to steal confidential information from specific targets.\r\n\r\nThis article is a follow-up to ESET’s presentation at BlueHat in November 2017. Late in 2016 we published a white paper covering Sednit activity between 2014 and 2016. Since then, we have continued to actively track Sednit’s operations, and today we are publishing a brief overview of what our tracking uncovered in terms of the group’s activities and updates to their toolset. The first section covers the update of their attack methodology: namely, the ways in which this group tries to compromise their targets systems. The second section covers the evolution of their tools, with a particular emphasis on a detailed analysis of a new version of their flagship malware: Xagent." }, - "RelatedEvent": [ - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-12-14", - "distribution": "3", - "id": "9616", - "info": "OSINT - Attackers Deploy New ICS Attack Framework “TRITON” and Cause Operational Disruption to Critical Infrastructure", - "org_id": "2", - "orgc_id": "2", - "published": false, - "threat_level_id": "3", - "timestamp": "1513674510", - "uuid": "5a329d19-03e0-4eaa-8b4d-4310950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-12-07", - "distribution": "3", - "id": "9552", - "info": "OSINT - Master Channel: The Boleto Mestre Campaign Targets Brazil", - "org_id": "2", - "orgc_id": "2", - "published": false, - "threat_level_id": "3", - "timestamp": "1512657975", - "uuid": "5a2943a3-c574-44bb-8e68-45de950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "0", - "date": "2017-11-27", - "distribution": "3", - "id": "9513", - "info": "OSINT - Tizi: Detecting and blocking socially engineered spyware on Android", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1512356440", - "uuid": "5a23a972-e6a0-4a05-b505-4e8f02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-11-07", - "distribution": "3", - "id": "9309", - "info": "OSINT - Threat Group APT28 Slips Office Malware into Doc Citing NYC Terror Attack", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1511385862", - "uuid": "5a021bc2-8e0c-4ac5-b048-cc3e02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "Orgc": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "analysis": "2", - "date": "2017-10-23", - "distribution": "3", - "id": "9208", - "info": "Talos: “Cyber Conflict” Decoy Document Used In Real Cyber Conflict", - "org_id": "291", - "orgc_id": "291", - "published": true, - "threat_level_id": "2", - "timestamp": "1510088616", - "uuid": "59ed9c81-6484-47a9-aab4-191d0a950b0c" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-08-11", - "distribution": "3", - "id": "8798", - "info": "OSINT - APT28 Targets Hospitality Sector, Presents Threat to Travelers", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1502460096", - "uuid": "598db7fd-47a8-45f8-9414-408b02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "231", - "name": "kingfisherops.com", - "uuid": "566ff5f4-7020-4089-9003-4374950d210f" - }, - "Orgc": { - "id": "204", - "name": "CERT-BUND", - "uuid": "56a64d7a-63dc-4471-bce9-4accc25ed029" - }, - "analysis": "0", - "date": "2017-07-25", - "distribution": "3", - "id": "8750", - "info": "European Defence Agency lure drops mssuppa.dat", - "org_id": "231", - "orgc_id": "204", - "published": true, - "threat_level_id": "2", - "timestamp": "1500967989", - "uuid": "5976f294-a844-44fe-a4f0-6c67c25ed029" - } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "analysis": "2", - "date": "2017-05-11", - "distribution": "3", - "id": "7820", - "info": "APT28-Sednit adds two zero-day exploits using ‘Trump’s attack on Syria’ as a decoy", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494824291", - "uuid": "59147a22-3100-4779-9377-360395ca48b7" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-05-09", - "distribution": "3", - "id": "7801", - "info": "OSINT - EPS Processing Zero-Days Exploited by Multiple Threat Actors", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1494354378", - "uuid": "59120865-27e0-4e6d-9b74-4a9f950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "0", - "date": "2016-12-29", - "distribution": "3", - "id": "5667", - "info": "OSINT - GRIZZLY STEPPE – Russian Malicious Cyber Activity", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1494853878", - "uuid": "58658c15-54ac-43c3-9beb-414502de0b81" - } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "analysis": "2", - "date": "2016-12-20", - "distribution": "1", - "id": "5616", - "info": "APT28-The Sofacy Group's DealersChoice Attacks Continue", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494829249", - "uuid": "58594faf-e98c-4c03-a58c-43cf95ca48b7" - } - }, - { - "Event": { - "Org": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "Orgc": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "analysis": "1", - "date": "2016-11-09", - "distribution": "3", - "id": "5348", - "info": "[APT-28/Sofacy]Pawn Storm Ramps Up [European Government] Spear-phishing Before Zero-Days Get Patched", - "org_id": "291", - "orgc_id": "291", - "published": true, - "threat_level_id": "1", - "timestamp": "1481709638", - "uuid": "582341ff-0830-4b32-aaba-08640a950b0c" - } - }, - { - "Event": { - "Org": { - "id": "74", - "name": "PwC.lu", - "uuid": "55f6ea61-4f74-40b6-a6df-4ff9950d210f" - }, - "Orgc": { - "id": "325", - "name": "CUDESO", - "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" - }, - "analysis": "2", - "date": "2016-11-09", - "distribution": "3", - "id": "5641", - "info": "Pawn Storm Ramps Up Spear-phishing Before Zero-Days Get Patched", - "org_id": "74", - "orgc_id": "325", - "published": true, - "threat_level_id": "2", - "timestamp": "1478712711", - "uuid": "58235d0e-34d4-41c1-9a2e-04dcc0a8ab16" - } - }, - { - "Event": { - "Org": { - "id": "335", - "name": "Orange CERT-CC", - "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" - }, - "Orgc": { - "id": "335", - "name": "Orange CERT-CC", - "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" - }, - "analysis": "0", - "date": "2016-10-18", + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188759", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "value": "movieultimate.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188760", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "value": "meteost.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188761", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188762", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "value": "nethostnet.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188763", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "value": "fsportal.net" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188764", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "value": "fastdataexchange.org" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188765", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + } + ], + "Galaxy": [ + { + "GalaxyCluster": [ + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Thomas Schreck", + "Timo Steffens", + "Various" + ], + "default": false, + "description": "The Sofacy Group (also known as APT28, Pawn Storm, Fancy Bear and Sednit) is a cyber espionage group believed to have ties to the Russian government. Likely operating since 2007, the group is known to target government, military, and security organizations. It has been characterized as an advanced persistent threat.", "distribution": "0", - "id": "5163", - "info": "Orange-CERT-CC Test #01", - "org_id": "335", - "orgc_id": "335", - "published": false, - "threat_level_id": "3", - "timestamp": "1476782422", - "uuid": "5805e8a5-611c-498b-839b-bd57950d210f" + "galaxy_id": "366", + "id": "45563", + "meta": { + "country": [ + "RU" + ], + "refs": [ + "https://en.wikipedia.org/wiki/Sofacy_Group", + "https://aptnotes.malwareconfig.com/web/viewer.html?file=../APTnotes/2014/apt28.pdf", + "http://www.trendmicro.com/cloud-content/us/pdfs/security-intelligence/white-papers/wp-operation-pawn-storm.pdf", + "https://www2.fireeye.com/rs/848-DID-242/images/wp-mandiant-matryoshka-mining.pdf", + "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/", + "http://researchcenter.paloaltonetworks.com/2016/06/unit42-new-sofacy-attacks-against-us-government-agency/" + ], + "synonyms": [ + "APT 28", + "APT28", + "Pawn Storm", + "Fancy Bear", + "Sednit", + "TsarTeam", + "TG-4127", + "Group-4127", + "STRONTIUM", + "TAG_0700", + "Swallowtail", + "IRON TWILIGHT", + "Group 74" + ] + }, + "source": "MISP Project", + "tag_id": "1100", + "tag_name": "misp-galaxy:threat-actor=\"Sofacy\"", + "type": "threat-actor", + "uuid": "7cdff317-a673-4474-84ec-4f1754947823", + "value": "Sofacy", + "version": "30" } - }, - { - "Event": { - "Org": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + ], + "description": "Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.", + "icon": "user-secret", + "id": "366", + "name": "Threat Actor", + "type": "threat-actor", + "uuid": "698774c7-8022-42c4-917f-8d6e4f06ada3", + "version": "2" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "Kafeine", + "Will Metcalf", + "KahuSecurity" + ], + "default": false, + "description": "Sednit EK is the exploit kit used by APT28", + "distribution": "0", + "galaxy_id": "370", + "id": "38813", + "meta": { + "refs": [ + "http://www.welivesecurity.com/2014/10/08/sednit-espionage-group-now-using-custom-exploit-kit/", + "http://blog.trendmicro.com/trendlabs-security-intelligence/new-adobe-flash-zero-day-used-in-pawn-storm-campaign/" + ], + "status": [ + "Active" + ] }, - "Orgc": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + "source": "MISP Project", + "tag_id": "3007", + "tag_name": "misp-galaxy:exploit-kit=\"Sednit EK\"", + "type": "exploit-kit", + "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", + "value": "Sednit EK", + "version": "5" + }, + { + "authors": [ + "Kafeine", + "Will Metcalf", + "KahuSecurity" + ], + "default": false, + "description": "DealersChoice is a Flash Player Exploit platform triggered by RTF", + "distribution": "0", + "galaxy_id": "370", + "id": "38805", + "meta": { + "refs": [ + "http://researchcenter.paloaltonetworks.com/2016/10/unit42-dealerschoice-sofacys-flash-player-exploit-platform/", + "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-ramps-up-spear-phishing-before-zero-days-get-patched/" + ], + "status": [ + "Active" + ], + "synonyms": [ + "Sednit RTF EK" + ] }, - "analysis": "2", - "date": "2016-10-17", - "distribution": "3", - "id": "5165", - "info": "OSINT: ‘DealersChoice’ is Sofacy’s Flash Player Exploit Platform", - "org_id": "278", - "orgc_id": "278", - "published": true, - "threat_level_id": "1", - "timestamp": "1476789563", - "uuid": "580602f6-f8b8-4ac3-9813-7bf7bce2ab96" + "source": "MISP Project", + "tag_id": "3015", + "tag_name": "misp-galaxy:exploit-kit=\"DealersChoice\"", + "type": "exploit-kit", + "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", + "value": "DealersChoice", + "version": "5" } - }, - { - "Event": { - "Org": { - "id": "412", - "name": "TS", - "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + ], + "description": "Exploit-Kit is an enumeration of some exploitation kits used by adversaries. The list includes document, browser and router exploit kits.It's not meant to be totally exhaustive but aim at covering the most seen in the past 5 years", + "icon": "internet-explorer", + "id": "370", + "name": "Exploit-Kit", + "type": "exploit-kit", + "uuid": "6ab240ec-bd79-11e6-a4a6-cec0c932ce01", + "version": "3" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "backdoor", + "distribution": "0", + "galaxy_id": "367", + "id": "46592", + "meta": { + "refs": [ + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" + ], + "synonyms": [ + "Sednit", + "Seduploader", + "JHUHUGIT", + "Sofacy" + ], + "type": [ + "Backdoor" + ] }, - "Orgc": { - "id": "412", - "name": "TS", - "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + "source": "MISP Project", + "tag_id": "2215", + "tag_name": "misp-galaxy:tool=\"GAMEFISH\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "GAMEFISH", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "", + "distribution": "0", + "galaxy_id": "367", + "id": "46670", + "meta": { + "synonyms": [ + "XTunnel" + ] }, - "analysis": "2", - "date": "2016-08-19", - "distribution": "1", - "id": "4710", - "info": "bullettin.doc sample, linked to APT28 campaign", - "org_id": "412", - "orgc_id": "412", - "published": true, - "threat_level_id": "1", - "timestamp": "1476776982", - "uuid": "57b7248f-283c-442e-8e02-2d0f5b86d7e5" + "source": "MISP Project", + "tag_id": "1012", + "tag_name": "misp-galaxy:tool=\"X-Tunnel\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "X-Tunnel", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "backdoor used by apt28\n\nSedreco serves as a spying backdoor; its functionalities can be extended with dynamically loaded plugins. It is made up of two distinct components: a dropper and the persistent payload installed by this dropper. We have not seen this component since April 2016.", + "distribution": "0", + "galaxy_id": "367", + "id": "46591", + "meta": { + "possible_issues": [ + "Report tells that is could be Xagent alias (Java Rat)" + ], + "refs": [ + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" + ], + "synonyms": [ + "Sedreco", + "AZZY", + "ADVSTORESHELL", + "NETUI" + ], + "type": [ + "Backdoor" + ] + }, + "source": "MISP Project", + "tag_id": "3011", + "tag_name": "misp-galaxy:tool=\"EVILTOSS\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "EVILTOSS", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "This backdoor component is known to have a modular structure featuring various espionage functionalities, such as key-logging, screen grabbing and file exfiltration. This component is available for Osx, Windows, Linux and iOS operating systems.\n\nXagent is a modular backdoor with spying functionalities such as keystroke logging and file exfiltration. Xagent is the group’s flagship backdoor and heavily used in their operations. Early versions for Linux and Windows were seen years ago, then in 2015 an iOS version came out. One year later, an Android version was discovered and finally, in the beginning of 2017, an Xagent sample for OS X was described.", + "distribution": "0", + "galaxy_id": "367", + "id": "46669", + "meta": { + "refs": [ + "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-update-ios-espionage-app-found/", + "https://app.box.com/s/l7n781ig6n8wlf1aff5hgwbh4qoi5jqq", + "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" + ], + "synonyms": [ + "XAgent" + ], + "type": [ + "Backdoor" + ] + }, + "source": "MISP Project", + "tag_id": "1011", + "tag_name": "misp-galaxy:tool=\"X-Agent\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "X-Agent", + "version": "45" } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + ], + "description": "Threat actors tools is an enumeration of tools used by adversaries. The list includes malware but also common software regularly used by the adversaries.", + "icon": "optin-monster", + "id": "367", + "name": "Tool", + "type": "tool", + "uuid": "9b8037f7-bc8f-4de1-a797-37266619bc0b", + "version": "2" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "JHUHUGIT is malware used by APT28. It is based on Carberp source code and serves as reconnaissance malware.[[Citation: Kaspersky Sofacy]][[Citation: F-Secure Sofacy 2015]][[Citation: ESET Sednit Part 1]][[Citation: FireEye APT28 January 2017]]\n\nAliases: JHUHUGIT, Seduploader, JKEYSKW, Sednit, GAMEFISH", + "distribution": "0", + "galaxy_id": "365", + "id": "41618", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0044", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part1.pdf", + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", + "https://labsblog.f-secure.com/2015/09/08/sofacy-recycles-carberp-and-metasploit-code/", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "JHUHUGIT", + "Seduploader", + "JKEYSKW", + "Sednit", + "GAMEFISH" + ], + "uuid": [ + "8ae43c46-57ef-47d5-a77a-eebb35628db2" + ] }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + "source": "https://github.com/mitre/cti", + "tag_id": "3008", + "tag_name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "JHUHUGIT", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "XTunnel a VPN-like network proxy tool that can relay traffic between a C2 server and a victim. It was first seen in May 2013 and reportedly used by APT28 during the compromise of the Democratic National Committee.[[Citation: Crowdstrike DNC June 2016]][[Citation: Invincea XTunnel]][[Citation: ESET Sednit Part 2]]\n\nAliases: XTunnel, X-Tunnel, XAPS", + "distribution": "0", + "galaxy_id": "365", + "id": "41543", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0117", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://www.invincea.com/2016/07/tunnel-of-gov-dnc-hack-and-the-russian-xtunnel/", + "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/" + ], + "synonyms": [ + "XTunnel", + "X-Tunnel", + "XAPS" + ], + "uuid": [ + "7343e208-7cab-45f2-a47b-41ba5e2f0fab" + ] }, - "analysis": "2", - "date": "2016-06-20", - "distribution": "3", - "id": "4172", - "info": "APT28 and APT29 - Inside the DNC Breaches", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494829231", - "uuid": "5767c102-c170-4124-ae3d-7bef95ca48b7" + "source": "https://github.com/mitre/cti", + "tag_id": "3009", + "tag_name": "misp-galaxy:mitre-malware=\"XTunnel\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "XTunnel", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "ADVSTORESHELL is a spying backdoor that has been used by APT28 from at least 2012 to 2016. It is generally used for long-term espionage and is deployed on targets deemed interesting after a reconnaissance phase.[[Citation: Kaspersky Sofacy]][[Citation: ESET Sednit Part 2]]\n\nAliases: ADVSTORESHELL, NETUI, EVILTOSS, AZZY, Sedreco", + "distribution": "0", + "galaxy_id": "365", + "id": "41582", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0045", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "ADVSTORESHELL", + "NETUI", + "EVILTOSS", + "AZZY", + "Sedreco" + ], + "uuid": [ + "fb575479-14ef-41e9-bfab-0b7cf10bec73" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3010", + "tag_name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "ADVSTORESHELL", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "USBStealer is malware that has used by APT28 since at least 2005 to extract information from air-gapped networks. It does not have the capability to communicate over the Internet and has been used in conjunction with ADVSTORESHELL.[[Citation: ESET Sednit USBStealer 2014]][[Citation: Kaspersky Sofacy]]\n\nAliases: USBStealer, USB Stealer, Win32/USBStealer", + "distribution": "0", + "galaxy_id": "365", + "id": "41549", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0136", + "http://www.welivesecurity.com/2014/11/11/sednit-espionage-group-attacking-air-gapped-networks/", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "USBStealer", + "USB Stealer", + "Win32/USBStealer" + ], + "uuid": [ + "af2ad3b7-ab6a-4807-91fd-51bcaff9acbb" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3012", + "tag_name": "misp-galaxy:mitre-malware=\"USBStealer\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "USBStealer", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "is a trojan that has been used by APT28 on OS X and appears to be a port of their standard CHOPSTICK or XAgent trojan.[[Citation: XAgentOSX]]", + "distribution": "0", + "galaxy_id": "365", + "id": "41551", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0161", + "https://researchcenter.paloaltonetworks.com/2017/02/unit42-xagentosx-sofacys-xagent-macos-tool/" + ], + "uuid": [ + "5930509b-7793-4db9-bdfc-4edda7709d0d" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3013", + "tag_name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "XAgentOSX", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "CHOPSTICK is malware family of modular backdoors used by APT28. It has been used from at least November 2012 to August 2016 and is usually dropped on victims as second-stage malware, though it has been used as first-stage malware in several cases.[[Citation: FireEye APT28]][[Citation: ESET Sednit Part 2]][[Citation: FireEye APT28 January 2017]]\n\nAliases: CHOPSTICK, SPLM, Xagent, X-Agent, webhp", + "distribution": "0", + "galaxy_id": "365", + "id": "41559", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0023", + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/rpt-apt28.pdf" + ], + "synonyms": [ + "CHOPSTICK", + "SPLM", + "Xagent", + "X-Agent", + "webhp" + ], + "uuid": [ + "ccd61dfc-b03f-4689-8c18-7c97eab08472" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3014", + "tag_name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "CHOPSTICK", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "Downdelph is a first-stage downloader written in Delphi that has been used by APT28 in rare instances between 2013 and 2015.[[Citation: ESET Sednit Part 3]]\n\nAliases: Downdelph, Delphacy", + "distribution": "0", + "galaxy_id": "365", + "id": "41504", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0134", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part3.pdf" + ], + "synonyms": [ + "Downdelph", + "Delphacy" + ], + "uuid": [ + "08d20cd2-f084-45ee-8558-fa6ef5a18519" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3016", + "tag_name": "misp-galaxy:mitre-malware=\"Downdelph\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "Downdelph", + "version": "4" } - }, - { - "Event": { - "Org": { - "id": "347", - "name": "incibe.es", - "uuid": "5720623c-129c-4989-ae9d-4a11950d210f" - }, - "Orgc": { - "id": "665", - "name": "INCIBE", - "uuid": "56fa4fe4-f528-4480-8332-1ba3c0a80a8c" - }, - "analysis": "2", - "date": "2016-06-16", - "distribution": "3", - "id": "6131", - "info": "New Sofacy (APT28) attacks against a US Government Agency", - "org_id": "347", - "orgc_id": "665", - "published": true, - "threat_level_id": "1", - "timestamp": "1488792538", - "uuid": "5762a86a-e314-4e4e-ba5a-51c5c0a80a8e" + ], + "description": "Name of ATT&CK software", + "icon": "optin-monster", + "id": "365", + "name": "Malware", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "version": "4" + } + ], + "Object": [ + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188944", + "object_id": "1555", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936310", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd5b6-2850-435f-bd0d-4c62950d210f", + "value": "Bulletin.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188945", + "object_id": "1555", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936310", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd5b6-78a8-4e47-8333-4c62950d210f", + "value": "68064fc152e23d56e541714af52651cb4ba81aaf" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188946", + "object_id": "1555", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936310", + "to_ids": false, + "type": "text", + "uuid": "5a3cd5b6-23d8-43ba-8518-4c62950d210f", + "value": "Malicious" } - }, - { - "Event": { - "Org": { - "id": "26", - "name": "CthulhuSPRL.be", - "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" - }, - "Orgc": { - "id": "26", - "name": "CthulhuSPRL.be", - "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" - }, - "analysis": "2", - "date": "2016-06-15", - "distribution": "3", - "id": "3987", - "info": "OSINT New Sofacy Attacks Against US Government Agency by Palo Alto Unit 42", - "org_id": "26", - "orgc_id": "26", - "published": true, - "threat_level_id": "1", - "timestamp": "1466000907", - "uuid": "57613790-f6b4-4895-943f-4467950d210f" + ], + "comment": "Win32/Sednit.AX", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1555", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936310", + "uuid": "5a3cd5b6-9568-4342-b2ab-4c62950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188947", + "object_id": "1556", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936388", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd604-748c-4fc0-88bf-c170950d210f", + "value": "f3805382ae2e23ff1147301d131a06e00e4ff75f" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188948", + "object_id": "1556", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936388", + "to_ids": false, + "type": "text", + "uuid": "5a3cd604-6668-4469-a1c0-c170950d210f", + "value": "Malicious" } - }, - { - "Event": { - "Org": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" - }, - "Orgc": { - "id": "325", - "name": "CUDESO", - "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" - }, - "analysis": "2", - "date": "2016-06-14", - "distribution": "3", - "id": "4183", - "info": "New Sofacy Attacks Against US Government Agency", - "org_id": "278", - "orgc_id": "325", - "published": true, - "threat_level_id": "2", - "timestamp": "1467289109", - "uuid": "57607369-2490-444a-9034-049fc0a8ab16" + ], + "comment": "Win32/Exploit.CVE-2016-4117.A", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1556", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936388", + "uuid": "5a3cd604-e11c-4de5-bbbf-c170950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188949", + "object_id": "1557", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd693-dc40-445d-a4d7-4ae0950d210f", + "value": "OC_PSO_2017.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188950", + "object_id": "1557", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd693-8ffc-4d95-b522-4e84950d210f", + "value": "512bdfe937314ac3f195c462c395feeb36932971" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188951", + "object_id": "1557", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": false, + "type": "text", + "uuid": "5a3cd693-a8f0-4aea-a834-4097950d210f", + "value": "Malicious" } + ], + "comment": "Win32/Exploit.Agent.NUB", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1557", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936531", + "uuid": "5a3cd693-fd9c-4fcf-b69a-439c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188952", + "object_id": "1558", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd6c2-d31c-40cc-bcc1-4458950d210f", + "value": "NASAMS.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188953", + "object_id": "1558", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd6c2-6a54-4b4c-8748-4c84950d210f", + "value": "30b3e8c0f3f3cf200daa21c267ffab3cad64e68b" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188954", + "object_id": "1558", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": false, + "type": "text", + "uuid": "5a3cd6c2-1c68-45de-8325-464a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1558", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936578", + "uuid": "5a3cd6c2-d290-4787-910f-4e6d950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188955", + "object_id": "1559", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd74e-584c-45b9-8557-486d950d210f", + "value": "Programm_Details.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188956", + "object_id": "1559", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd74e-f334-4e6b-b37f-462f950d210f", + "value": "4173b29a251cd9c1cab135f67cb60acab4ace0c5" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188957", + "object_id": "1559", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": false, + "type": "text", + "uuid": "5a3cd74e-5900-4fbf-85c6-4c81950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1559", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936718", + "uuid": "5a3cd74e-1504-40ff-9a28-4501950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188958", + "object_id": "1560", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd775-e8f4-465a-aca2-4c5a950d210f", + "value": "Operation_in_Mosul.rtf" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188959", + "object_id": "1560", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd775-1190-4db7-961a-4c5a950d210f", + "value": "12a37cfdd3f3671074dd5b0f354269cec028fb52" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188960", + "object_id": "1560", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": false, + "type": "text", + "uuid": "5a3cd775-fa5c-4453-bcb0-4c5a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1560", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936757", + "uuid": "5a3cd775-e4cc-44bb-89b6-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188961", + "object_id": "1561", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd82f-b918-4520-ba8b-5165950d210f", + "value": "ARM-NATO_ENGLISH_30_NOV_2016.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188962", + "object_id": "1561", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd82f-cae4-4209-9338-5165950d210f", + "value": "15201766bd964b7c405aeb11db81457220c31e46" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188963", + "object_id": "1561", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": false, + "type": "text", + "uuid": "5a3cd82f-d91c-43af-8262-5165950d210f", + "value": "Malicious" + } + ], + "comment": "SWF/Agent.L", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1561", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936943", + "uuid": "5a3cd82f-2788-4561-bbeb-5165950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188964", + "object_id": "1562", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd847-0aa0-4b5c-aa30-5165950d210f", + "value": "Olympic-Agenda-2020-20-20-Recommendations.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188965", + "object_id": "1562", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd847-593c-4985-8756-5165950d210f", + "value": "8078e411fbe33864dfd8f87ad5105cc1fd26d62e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188966", + "object_id": "1562", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": false, + "type": "text", + "uuid": "5a3cd847-1324-4fad-af60-5165950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.BL", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1562", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936967", + "uuid": "5a3cd847-b5a0-42f7-ac4b-5165950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188967", + "object_id": "1563", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd861-9350-40c1-ac29-4771950d210f", + "value": "Merry_Christmas!.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188968", + "object_id": "1563", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd861-18ac-4cf0-b96f-4986950d210f", + "value": "33447383379ca99083442b852589111296f0c603" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188969", + "object_id": "1563", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": false, + "type": "text", + "uuid": "5a3cd861-cfbc-4096-baae-40e2950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NUG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1563", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936993", + "uuid": "5a3cd861-65c0-4b69-9429-4f37950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188970", + "object_id": "1564", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd87d-fa9c-41aa-897f-49a5950d210f", + "value": "Trump’s_Attack_on_Syria_English.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188971", + "object_id": "1564", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd87d-c630-4487-8336-4615950d210f", + "value": "d5235d136cfcadbef431eea7253d80bde414db9d" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188972", + "object_id": "1564", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": false, + "type": "text", + "uuid": "5a3cd87d-8c98-4660-9026-44de950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NWZ", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1564", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937021", + "uuid": "5a3cd87d-f514-4071-a5f7-4ec2950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188973", + "object_id": "1565", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd897-4cc0-48b0-bb2c-461f950d210f", + "value": "Hotel_Reservation_Form.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188974", + "object_id": "1565", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd897-fa64-466c-9421-49c5950d210f", + "value": "f293a2bfb728060c54efeeb03c5323893b5c80df" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188975", + "object_id": "1565", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": false, + "type": "text", + "uuid": "5a3cd897-f020-44cf-8dfc-4225950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1565", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937046", + "uuid": "5a3cd896-f6cc-4e52-bcb2-442c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188976", + "object_id": "1566", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937070", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd8ae-7194-48fd-810e-4c5a950d210f", + "value": "SB_Doc_2017-3_Implementation_of_Key_Taskings_and_Next_Steps.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188977", + "object_id": "1566", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937071", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8af-f39c-443c-bcf1-4c5a950d210f", + "value": "bb10ed5d59672fbc6178e35d0feac0562513e9f0" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188978", + "object_id": "1566", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937071", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8af-b3ec-478a-b585-4c5a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1566", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937070", + "uuid": "5a3cd8ae-54d0-46bb-adbb-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188979", + "object_id": "1567", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937083", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8bb-74d8-4d19-ae08-4043950d210f", + "value": "4873bafe44cff06845faa0ce7c270c4ce3c9f7b9" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188980", + "object_id": "1567", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937083", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8bb-77bc-4cc4-887f-429d950d210f", + "value": "Malicious" + } + ], + "comment": "", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1567", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937083", + "uuid": "5a3cd8bb-a704-4f1d-a235-444e950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188981", + "object_id": "1568", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937097", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8c9-4d2c-4145-a637-4f13950d210f", + "value": "169c8f3e3d22e192c108bc95164d362ce5437465" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188982", + "object_id": "1568", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937097", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8c9-7ff0-42f7-ae80-4eb6950d210f", + "value": "Malicious" + } + ], + "comment": "", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1568", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937097", + "uuid": "5a3cd8c9-6568-406a-853c-4862950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188983", + "object_id": "1569", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937116", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8dc-48c0-4ea0-a67d-4734950d210f", + "value": "cc7607015cd7a1a4452acd3d87adabdd7e005bd7" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188984", + "object_id": "1569", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937116", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8dc-9ed8-4a4d-9ceb-4daa950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1569", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937115", + "uuid": "5a3cd8db-2838-4466-a986-4afb950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188985", + "object_id": "1570", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd8fb-1efc-4059-ae7a-42f5950d210f", + "value": "Caucasian_Eagle_ENG.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188986", + "object_id": "1570", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8fb-9cec-4a30-8b2f-4441950d210f", + "value": "5d2c7d87995cc5b8184baba2c7a1900a48b2f42d" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188987", + "object_id": "1570", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8fb-e52c-489b-8da5-43d1950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTM", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1570", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937147", + "uuid": "5a3cd8fb-cd14-4b00-9710-430c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188988", + "object_id": "1571", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd90e-5eb4-4069-b160-5276950d210f", + "value": "World War3.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188989", + "object_id": "1571", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd90e-6d2c-4ffc-a699-5276950d210f", + "value": "7aada8bcc0d1ab8ffb1f0fae4757789c6f5546a3" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188990", + "object_id": "1571", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": false, + "type": "text", + "uuid": "5a3cd90e-28e8-410e-8033-5276950d210f", + "value": "Malicious" + } + ], + "comment": "SWF/Exploit.CVE-2017-11292.A", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1571", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937166", + "uuid": "5a3cd90e-538c-4b7e-95dc-5276950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188991", + "object_id": "1572", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd927-e810-4d22-a0e4-4057950d210f", + "value": "SaberGuardian2017.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188992", + "object_id": "1572", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd927-f284-43b9-83d1-473b950d210f", + "value": "68c2809560c7623d2307d8797691abf3eafe319a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188993", + "object_id": "1572", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": false, + "type": "text", + "uuid": "5a3cd927-b844-49f2-a1a9-4c85950d210f", + "value": "Malicious" + } + ], + "comment": "VBA/DDE.E", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1572", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937191", + "uuid": "5a3cd927-e410-489c-abfc-4b63950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188994", + "object_id": "1573", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd93c-2438-4dda-823e-463d950d210f", + "value": "IsisAttackInNewYork.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188995", + "object_id": "1573", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd93c-1ef0-4d81-9476-4655950d210f", + "value": "1c6c700ceebfbe799e115582665105caa03c5c9e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188996", + "object_id": "1573", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": false, + "type": "text", + "uuid": "5a3cd93c-949c-40ac-9094-4a4a950d210f", + "value": "Malicious" + } + ], + "comment": "VBA/DDE.L", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1573", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937212", + "uuid": "5a3cd93c-716c-4918-a00f-4671950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188997", + "object_id": "1574", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937559", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cda97-7e58-4642-aaf5-c5ed950d210f", + "value": "6f0fc0ebba3e4c8b26a69cdf519edf8d1aa2f4bb" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188998", + "object_id": "1574", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937559", + "to_ids": false, + "type": "text", + "uuid": "5a3cda97-6020-423d-9d23-c5ed950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "value": "movieultimate.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "159", + "object_id": "1574", + "object_uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f", + "referenced_id": "1188759", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513937826", + "uuid": "5a3cdba2-2fdc-4f9a-a4eb-4dae950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1574", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937826", + "uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188999", + "object_id": "1575", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937864", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdbc8-0aac-4d8a-8c1f-4c5a950d210f", + "value": "e19f753e514f6adec8f81bcdefb9117979e69627" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189000", + "object_id": "1575", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937864", + "to_ids": false, + "type": "text", + "uuid": "5a3cdbc8-e204-4606-b9ea-4c5a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "value": "meteost.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "160", + "object_id": "1575", + "object_uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f", + "referenced_id": "1188760", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938091", + "uuid": "5a3cdcab-8200-4c65-868e-42a9950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1575", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938091", + "uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189001", + "object_id": "1576", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937910", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdbf6-eca0-4c09-9bd0-4c59950d210f", + "value": "961468ddd3d0fa25beb8210c81ba620f9170ed30" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189002", + "object_id": "1576", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937910", + "to_ids": false, + "type": "text", + "uuid": "5a3cdbf6-acd8-4a36-a028-4c59950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "164", + "object_id": "1576", + "object_uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f", + "referenced_id": "1188761", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938210", + "uuid": "5a3cdd22-b7d8-4754-a108-4742950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1576", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938210", + "uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189003", + "object_id": "1577", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937929", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc09-b428-4c0b-9969-c5ed950d210f", + "value": "a0719b50265505c8432616c0a4e14ed206981e95" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189004", + "object_id": "1577", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937929", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc09-05d8-4356-ba52-c5ed950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "value": "nethostnet.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "162", + "object_id": "1577", + "object_uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f", + "referenced_id": "1188762", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938169", + "uuid": "5a3cdcf9-d5a4-4c8e-a201-45b1950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1577", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938169", + "uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189005", + "object_id": "1578", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937953", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc21-a170-4637-b139-4812950d210f", + "value": "2cf6436b99d11d9d1e0c488af518e35162ecbc9c" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189006", + "object_id": "1578", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937953", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc21-3274-4800-9e91-41e2950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "165", + "object_id": "1578", + "object_uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f", + "referenced_id": "1188761", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938226", + "uuid": "5a3cdd32-3044-4895-8f18-4d06950d210f" + } + ], + "comment": "Win64/Sednit.Y", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1578", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938226", + "uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189007", + "object_id": "1579", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937975", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc37-cee0-43d0-9e20-4db6950d210f", + "value": "fec29b4f4dccc59770c65c128dfe4564d7c13d33" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189008", + "object_id": "1579", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937976", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc38-ac24-44be-a1ed-4935950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "value": "fsportal.net" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "163", + "object_id": "1579", + "object_uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f", + "referenced_id": "1188763", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938189", + "uuid": "5a3cdd0d-d990-42ba-830d-5156950d210f" + } + ], + "comment": "Win64/Sednit.Y", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1579", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938190", + "uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189009", + "object_id": "1580", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937992", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc48-c74c-4b6e-8202-5156950d210f", + "value": "57d7f3d31c491f8aef4665ca4dd905c3c8a98795" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189010", + "object_id": "1580", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937992", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc48-55dc-420e-9b5d-5156950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "value": "fastdataexchange.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "161", + "object_id": "1580", + "object_uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f", + "referenced_id": "1188764", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938129", + "uuid": "5a3cdcd1-c6cc-43d8-a2f4-4681950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1580", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938129", + "uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189011", + "object_id": "1581", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513938011", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc5b-54a8-4e60-bc67-4c5a950d210f", + "value": "a3bf5b5cf5a5ef438a198a6f61f7225c0a4a7138" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189012", + "object_id": "1581", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513938011", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc5b-b390-4183-aec7-4c5a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "168", + "object_id": "1581", + "object_uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f", + "referenced_id": "1188765", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938280", + "uuid": "5a3cdd68-7968-40d1-a0a9-5156950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1581", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938280", + "uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189013", + "object_id": "1582", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513938034", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc72-ba30-4ecd-9d21-4654950d210f", + "value": "1958e722afd0dba266576922abc98aa505cf5f9a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189014", + "object_id": "1582", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513938034", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc72-0804-42c4-acfa-4ac5950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "167", + "object_id": "1582", + "object_uuid": "5a3cdc72-1538-4c66-af46-427b950d210f", + "referenced_id": "1188765", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938264", + "uuid": "5a3cdd58-9800-4bae-837c-4f20950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1582", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938264", + "uuid": "5a3cdc72-1538-4c66-af46-427b950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189015", + "object_id": "1583", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939882", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3aa-e104-481e-a7f4-4bc1950d210f", + "value": "9f6bed7d7f4728490117cbc85819c2e6c494251b" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189016", + "object_id": "1583", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939882", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3aa-74fc-48c7-af40-4c6a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "173", + "object_id": "1583", + "object_uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f", + "referenced_id": "1592", + "referenced_type": "1", + "referenced_uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513947459", + "uuid": "5a3d0143-c300-4118-8afe-4a2d950d210f" + } + ], + "comment": "Win32/Sednit.AX", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1583", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948642", + "uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189017", + "object_id": "1584", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939907", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3c3-6d9c-48f4-93db-4a61950d210f", + "value": "4bc722a9b0492a50bd86a1341f02c74c0d773db7" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189018", + "object_id": "1584", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939907", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3c3-c38c-4e30-a904-4c8f950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "188", + "object_id": "1584", + "object_uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f", + "referenced_id": "1603", + "referenced_type": "1", + "referenced_uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948518", + "uuid": "5a3d0566-34fc-4a62-b2a5-4f91950d210f" + } + ], + "comment": "Win32/Sednit.BS", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1584", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948535", + "uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189019", + "object_id": "1585", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939924", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3d4-9168-4e23-8b64-485a950d210f", + "value": "ab354807e687993fbeb1b325eb6e4ab38d428a1e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189020", + "object_id": "1585", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939924", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3d4-27e0-4366-943f-4b9a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "189", + "object_id": "1585", + "object_uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f", + "referenced_id": "1602", + "referenced_type": "1", + "referenced_uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948528", + "uuid": "5a3d0570-a86c-4264-a43a-4125950d210f" + } + ], + "comment": "Win32/Sednit.BS", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1585", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948597", + "uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189021", + "object_id": "1586", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939946", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3ea-8dbc-4cf4-997f-448b950d210f", + "value": "9c47ca3883196b3a84d67676a804ff50e22b0a9f" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189022", + "object_id": "1586", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939946", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3ea-e714-444e-ad9b-40b0950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "190", + "object_id": "1586", + "object_uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f", + "referenced_id": "1601", + "referenced_type": "1", + "referenced_uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948614", + "uuid": "5a3d05c6-0618-4520-9549-48a0950d210f" + } + ], + "comment": "Win32/Sednit.BR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1586", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948626", + "uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189023", + "object_id": "1587", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939972", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce404-7bfc-4316-bd32-55ea950d210f", + "value": "8a68f26d01372114f660e32ac4c9117e5d0577f1" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189024", + "object_id": "1587", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939972", + "to_ids": false, + "type": "text", + "uuid": "5a3ce404-7224-4525-922a-55ea950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "182", + "object_id": "1587", + "object_uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f", + "referenced_id": "1600", + "referenced_type": "1", + "referenced_uuid": "5a3ce680-90d4-478d-95db-48a6950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948044", + "uuid": "5a3d038c-1cc8-4d9c-87ab-c5ed950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1587", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948073", + "uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189025", + "object_id": "1588", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939991", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce417-62a4-4d46-9a87-55ea950d210f", + "value": "476fc1d31722ac26b46154cbf0c631d60268b28a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189026", + "object_id": "1588", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939991", + "to_ids": false, + "type": "text", + "uuid": "5a3ce417-43f0-494d-ac2e-55ea950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "187", + "object_id": "1588", + "object_uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f", + "referenced_id": "1599", + "referenced_type": "1", + "referenced_uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948483", + "uuid": "5a3d0543-8f74-4086-aafc-418a950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1588", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948498", + "uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189027", + "object_id": "1589", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940012", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce42c-836c-49e7-a9f3-4a5f950d210f", + "value": "f9fd3f1d8da4ffd6a494228b934549d09e3c59d1" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189028", + "object_id": "1589", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940012", + "to_ids": false, + "type": "text", + "uuid": "5a3ce42c-4c88-4940-94b8-4084950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "183", + "object_id": "1589", + "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", + "referenced_id": "1594", + "referenced_type": "1", + "referenced_uuid": "5a3ce60a-6db8-4212-b194-4339950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948106", + "uuid": "5a3d03ca-2398-4060-b13c-404a950d210f" + }, + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "184", + "object_id": "1589", + "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", + "referenced_id": "1595", + "referenced_type": "1", + "referenced_uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948117", + "uuid": "5a3d03d5-6d8c-4dfb-b193-4002950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1589", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948128", + "uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189029", + "object_id": "1590", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940027", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce43b-6738-4a14-a318-4d65950d210f", + "value": "e338d49c270baf64363879e5eecb8fa6bdde8ad9" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189030", + "object_id": "1590", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940027", + "to_ids": false, + "type": "text", + "uuid": "5a3ce43b-3a10-4d78-9ee2-485c950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "186", + "object_id": "1590", + "object_uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f", + "referenced_id": "1593", + "referenced_type": "1", + "referenced_uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948320", + "uuid": "5a3d04a0-9d28-47c3-a12c-465b950d210f" + } + ], + "comment": "Win32/Sednit.BG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1590", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948339", + "uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189031", + "object_id": "1591", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940042", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce44a-2ea4-4526-8bbc-c328950d210f", + "value": "6e167da3c5d887fa2e58da848a2245d11b6c5ad6" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189032", + "object_id": "1591", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940042", + "to_ids": false, + "type": "text", + "uuid": "5a3ce44a-5118-4142-97f0-c328950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "170", + "object_id": "1591", + "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", + "referenced_id": "1597", + "referenced_type": "1", + "referenced_uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513940734", + "uuid": "5a3ce6fe-b0c4-44df-a609-419a950d210f" + }, + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "171", + "object_id": "1591", + "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", + "referenced_id": "1598", + "referenced_type": "1", + "referenced_uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513940753", + "uuid": "5a3ce711-a0dc-4dbe-b59e-495a950d210f" + } + ], + "comment": "Win32/Sednit.BG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1591", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513940753", + "uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189033", + "object_id": "1592", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940362", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce58a-fcd8-48d5-8b4a-4fd9950d210f", + "value": "87.236.211.182" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189034", + "object_id": "1592", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940362", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce58a-6e14-48ea-9746-48f2950d210f", + "value": "servicecdp.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1592", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940362", + "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189035", + "object_id": "1593", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940472", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce5f8-99b4-41a2-915a-4bf8950d210f", + "value": "95.215.45.43" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189036", + "object_id": "1593", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940472", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce5f8-62c8-4f04-89c2-4aeb950d210f", + "value": "wmdmediacodecs.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1593", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940472", + "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189037", + "object_id": "1594", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940490", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce60a-cc50-4553-bfff-4ea9950d210f", + "value": "89.45.67.144" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189038", + "object_id": "1594", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940491", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce60b-e648-4667-8432-4ba8950d210f", + "value": "mvband.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1594", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940490", + "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189039", + "object_id": "1595", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940506", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce61a-4458-4c36-866e-44e9950d210f", + "value": "89.33.246.117" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189040", + "object_id": "1595", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940506", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce61a-f820-4a43-b3d9-47e5950d210f", + "value": "mvtband.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1595", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940506", + "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189041", + "object_id": "1596", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940542", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce63e-66d4-483f-bae6-44f6950d210f", + "value": "87.236.211.182" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189042", + "object_id": "1596", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940542", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce63e-0d88-405b-82a9-43b5950d210f", + "value": "servicecdp.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1596", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940542", + "uuid": "5a3ce63e-0240-46f5-b9ed-4759950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189043", + "object_id": "1597", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940558", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce64e-d7a8-4817-a132-4c72950d210f", + "value": "185.156.173.70" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189044", + "object_id": "1597", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940558", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce64e-243c-4931-b733-403c950d210f", + "value": "runvercheck.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1597", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940558", + "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189045", + "object_id": "1598", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940572", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce65c-bf78-4b78-bafd-4cf6950d210f", + "value": "191.101.31.96" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189046", + "object_id": "1598", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940572", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce65c-8140-4146-a927-45e4950d210f", + "value": "remsupport.org" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1598", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940572", + "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189047", + "object_id": "1599", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940591", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce66f-150c-43ec-a3ff-4aa5950d210f", + "value": "89.187.150.44" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189048", + "object_id": "1599", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940591", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce66f-466c-478e-8064-4b42950d210f", + "value": "viters.org" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1599", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940590", + "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189049", + "object_id": "1600", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940608", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce680-7b04-466d-b187-4301950d210f", + "value": "146.185.253.132" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189050", + "object_id": "1600", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940608", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce680-12f4-4001-9f86-4aa4950d210f", + "value": "myinvestgroup.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1600", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940608", + "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189051", + "object_id": "1601", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940621", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce68d-0108-4557-8921-4377950d210f", + "value": "86.106.131.141" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189052", + "object_id": "1601", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940622", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce68e-54d0-4c67-8c4c-4dea950d210f", + "value": "space-delivery.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1601", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940621", + "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189054", + "object_id": "1602", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940642", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce6a2-4a38-4b90-8d74-4f10950d210f", + "value": "89.34.111.160" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189055", + "object_id": "1602", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940642", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce6a2-ffa4-4afb-89ab-42a6950d210f", + "value": "satellitedeluxpanorama.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1602", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940641", + "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189056", + "object_id": "1603", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940654", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce6ae-601c-44b8-8eec-4a5f950d210f", + "value": "185.216.35.26" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189057", + "object_id": "1603", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940654", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce6ae-3b00-420a-82fd-45fb950d210f", + "value": "webviewres.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1603", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940654", + "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" + } + ], + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "RelatedEvent": [ + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-12-14", + "distribution": "3", + "id": "9616", + "info": "OSINT - Attackers Deploy New ICS Attack Framework \"TRITON\" and Cause Operational Disruption to Critical Infrastructure", + "org_id": "2", + "orgc_id": "2", + "published": false, + "threat_level_id": "3", + "timestamp": "1513674510", + "uuid": "5a329d19-03e0-4eaa-8b4d-4310950d210f" } - ], - "Tag": [ - { - "colour": "#00d622", - "exportable": true, - "hide_tag": false, - "id": "2", - "name": "tlp:white", - "user_id": "0" - }, - { - "colour": "#ef0081", - "exportable": true, - "hide_tag": false, - "id": "2986", - "name": "workflow:state=\"incomplete\"", - "user_id": "0" - }, - { - "colour": "#810046", - "exportable": true, - "hide_tag": false, - "id": "2979", - "name": "workflow:todo=\"create-missing-misp-galaxy-cluster-values\"", - "user_id": "0" - }, - { - "colour": "#91004e", - "exportable": true, - "hide_tag": false, - "id": "2980", - "name": "workflow:todo=\"create-missing-misp-galaxy-cluster\"", - "user_id": "0" - }, - { - "colour": "#12e000", - "exportable": true, - "hide_tag": false, - "id": "1100", - "name": "misp-galaxy:threat-actor=\"Sofacy\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3007", - "name": "misp-galaxy:exploit-kit=\"Sednit EK\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "2215", - "name": "misp-galaxy:tool=\"GAMEFISH\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3008", - "name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", - "user_id": "0" - }, - { - "colour": "#0c9900", - "exportable": true, - "hide_tag": false, - "id": "1012", - "name": "misp-galaxy:tool=\"X-Tunnel\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3009", - "name": "misp-galaxy:mitre-malware=\"XTunnel\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3010", - "name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3011", - "name": "misp-galaxy:tool=\"EVILTOSS\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3012", - "name": "misp-galaxy:mitre-malware=\"USBStealer\"", - "user_id": "0" - }, - { - "colour": "#0c9800", - "exportable": true, - "hide_tag": false, - "id": "1011", - "name": "misp-galaxy:tool=\"X-Agent\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3013", - "name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3014", - "name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3015", - "name": "misp-galaxy:exploit-kit=\"DealersChoice\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3016", - "name": "misp-galaxy:mitre-malware=\"Downdelph\"", - "user_id": "0" + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-12-07", + "distribution": "3", + "id": "9552", + "info": "OSINT - Master Channel: The Boleto Mestre Campaign Targets Brazil", + "org_id": "2", + "orgc_id": "2", + "published": false, + "threat_level_id": "3", + "timestamp": "1512657975", + "uuid": "5a2943a3-c574-44bb-8e68-45de950d210f" } - ], - "analysis": "0", - "attribute_count": "122", - "date": "2017-12-21", - "disable_correlation": false, - "distribution": "3", - "event_creator_email": "alexandre.dulaunoy@circl.lu", - "id": "9747", - "info": "OSINT - Sednit update: How Fancy Bear Spent the Year", - "locked": false, - "org_id": "2", - "orgc_id": "2", - "proposal_email_lock": false, - "publish_timestamp": "0", - "published": false, - "sharing_group_id": "0", - "threat_level_id": "3", - "timestamp": "1513948642", - "uuid": "5a3c2fcd-8328-42bb-a95e-4f4402de0b81" - } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "0", + "date": "2017-11-27", + "distribution": "3", + "id": "9513", + "info": "OSINT - Tizi: Detecting and blocking socially engineered spyware on Android", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1512356440", + "uuid": "5a23a972-e6a0-4a05-b505-4e8f02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-11-07", + "distribution": "3", + "id": "9309", + "info": "OSINT - Threat Group APT28 Slips Office Malware into Doc Citing NYC Terror Attack", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1511385862", + "uuid": "5a021bc2-8e0c-4ac5-b048-cc3e02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "Orgc": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "analysis": "2", + "date": "2017-10-23", + "distribution": "3", + "id": "9208", + "info": "Talos: \"Cyber Conflict\" Decoy Document Used In Real Cyber Conflict", + "org_id": "291", + "orgc_id": "291", + "published": true, + "threat_level_id": "2", + "timestamp": "1510088616", + "uuid": "59ed9c81-6484-47a9-aab4-191d0a950b0c" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-08-11", + "distribution": "3", + "id": "8798", + "info": "OSINT - APT28 Targets Hospitality Sector, Presents Threat to Travelers", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1502460096", + "uuid": "598db7fd-47a8-45f8-9414-408b02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "231", + "name": "kingfisherops.com", + "uuid": "566ff5f4-7020-4089-9003-4374950d210f" + }, + "Orgc": { + "id": "204", + "name": "CERT-BUND", + "uuid": "56a64d7a-63dc-4471-bce9-4accc25ed029" + }, + "analysis": "0", + "date": "2017-07-25", + "distribution": "3", + "id": "8750", + "info": "European Defence Agency lure drops mssuppa.dat", + "org_id": "231", + "orgc_id": "204", + "published": true, + "threat_level_id": "2", + "timestamp": "1500967989", + "uuid": "5976f294-a844-44fe-a4f0-6c67c25ed029" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2017-05-11", + "distribution": "3", + "id": "7820", + "info": "APT28-Sednit adds two zero-day exploits using ‘Trump’s attack on Syria’ as a decoy", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494824291", + "uuid": "59147a22-3100-4779-9377-360395ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-05-09", + "distribution": "3", + "id": "7801", + "info": "OSINT - EPS Processing Zero-Days Exploited by Multiple Threat Actors", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1494354378", + "uuid": "59120865-27e0-4e6d-9b74-4a9f950d210f" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "0", + "date": "2016-12-29", + "distribution": "3", + "id": "5667", + "info": "OSINT - GRIZZLY STEPPE – Russian Malicious Cyber Activity", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1494853878", + "uuid": "58658c15-54ac-43c3-9beb-414502de0b81" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2016-12-20", + "distribution": "1", + "id": "5616", + "info": "APT28-The Sofacy Group's DealersChoice Attacks Continue", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494829249", + "uuid": "58594faf-e98c-4c03-a58c-43cf95ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "Orgc": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "analysis": "1", + "date": "2016-11-09", + "distribution": "3", + "id": "5348", + "info": "[APT-28/Sofacy]Pawn Storm Ramps Up [European Government] Spear-phishing Before Zero-Days Get Patched", + "org_id": "291", + "orgc_id": "291", + "published": true, + "threat_level_id": "1", + "timestamp": "1481709638", + "uuid": "582341ff-0830-4b32-aaba-08640a950b0c" + } + }, + { + "Event": { + "Org": { + "id": "74", + "name": "PwC.lu", + "uuid": "55f6ea61-4f74-40b6-a6df-4ff9950d210f" + }, + "Orgc": { + "id": "325", + "name": "CUDESO", + "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" + }, + "analysis": "2", + "date": "2016-11-09", + "distribution": "3", + "id": "5641", + "info": "Pawn Storm Ramps Up Spear-phishing Before Zero-Days Get Patched", + "org_id": "74", + "orgc_id": "325", + "published": true, + "threat_level_id": "2", + "timestamp": "1478712711", + "uuid": "58235d0e-34d4-41c1-9a2e-04dcc0a8ab16" + } + }, + { + "Event": { + "Org": { + "id": "335", + "name": "Orange CERT-CC", + "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" + }, + "Orgc": { + "id": "335", + "name": "Orange CERT-CC", + "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" + }, + "analysis": "0", + "date": "2016-10-18", + "distribution": "0", + "id": "5163", + "info": "Orange-CERT-CC Test #01", + "org_id": "335", + "orgc_id": "335", + "published": false, + "threat_level_id": "3", + "timestamp": "1476782422", + "uuid": "5805e8a5-611c-498b-839b-bd57950d210f" + } + }, + { + "Event": { + "Org": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "Orgc": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "analysis": "2", + "date": "2016-10-17", + "distribution": "3", + "id": "5165", + "info": "OSINT: ‘DealersChoice’ is Sofacy’s Flash Player Exploit Platform", + "org_id": "278", + "orgc_id": "278", + "published": true, + "threat_level_id": "1", + "timestamp": "1476789563", + "uuid": "580602f6-f8b8-4ac3-9813-7bf7bce2ab96" + } + }, + { + "Event": { + "Org": { + "id": "412", + "name": "TS", + "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + }, + "Orgc": { + "id": "412", + "name": "TS", + "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + }, + "analysis": "2", + "date": "2016-08-19", + "distribution": "1", + "id": "4710", + "info": "bullettin.doc sample, linked to APT28 campaign", + "org_id": "412", + "orgc_id": "412", + "published": true, + "threat_level_id": "1", + "timestamp": "1476776982", + "uuid": "57b7248f-283c-442e-8e02-2d0f5b86d7e5" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2016-06-20", + "distribution": "3", + "id": "4172", + "info": "APT28 and APT29 - Inside the DNC Breaches", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494829231", + "uuid": "5767c102-c170-4124-ae3d-7bef95ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "347", + "name": "incibe.es", + "uuid": "5720623c-129c-4989-ae9d-4a11950d210f" + }, + "Orgc": { + "id": "665", + "name": "INCIBE", + "uuid": "56fa4fe4-f528-4480-8332-1ba3c0a80a8c" + }, + "analysis": "2", + "date": "2016-06-16", + "distribution": "3", + "id": "6131", + "info": "New Sofacy (APT28) attacks against a US Government Agency", + "org_id": "347", + "orgc_id": "665", + "published": true, + "threat_level_id": "1", + "timestamp": "1488792538", + "uuid": "5762a86a-e314-4e4e-ba5a-51c5c0a80a8e" + } + }, + { + "Event": { + "Org": { + "id": "26", + "name": "CthulhuSPRL.be", + "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" + }, + "Orgc": { + "id": "26", + "name": "CthulhuSPRL.be", + "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" + }, + "analysis": "2", + "date": "2016-06-15", + "distribution": "3", + "id": "3987", + "info": "OSINT New Sofacy Attacks Against US Government Agency by Palo Alto Unit 42", + "org_id": "26", + "orgc_id": "26", + "published": true, + "threat_level_id": "1", + "timestamp": "1466000907", + "uuid": "57613790-f6b4-4895-943f-4467950d210f" + } + }, + { + "Event": { + "Org": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "Orgc": { + "id": "325", + "name": "CUDESO", + "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" + }, + "analysis": "2", + "date": "2016-06-14", + "distribution": "3", + "id": "4183", + "info": "New Sofacy Attacks Against US Government Agency", + "org_id": "278", + "orgc_id": "325", + "published": true, + "threat_level_id": "2", + "timestamp": "1467289109", + "uuid": "57607369-2490-444a-9034-049fc0a8ab16" + } + } + ], + "Tag": [ + { + "colour": "#00d622", + "exportable": true, + "hide_tag": false, + "id": "2", + "name": "tlp:white", + "user_id": "0" + }, + { + "colour": "#ef0081", + "exportable": true, + "hide_tag": false, + "id": "2986", + "name": "workflow:state=\"incomplete\"", + "user_id": "0" + }, + { + "colour": "#810046", + "exportable": true, + "hide_tag": false, + "id": "2979", + "name": "workflow:todo=\"create-missing-misp-galaxy-cluster-values\"", + "user_id": "0" + }, + { + "colour": "#91004e", + "exportable": true, + "hide_tag": false, + "id": "2980", + "name": "workflow:todo=\"create-missing-misp-galaxy-cluster\"", + "user_id": "0" + }, + { + "colour": "#12e000", + "exportable": true, + "hide_tag": false, + "id": "1100", + "name": "misp-galaxy:threat-actor=\"Sofacy\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3007", + "name": "misp-galaxy:exploit-kit=\"Sednit EK\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "2215", + "name": "misp-galaxy:tool=\"GAMEFISH\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3008", + "name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", + "user_id": "0" + }, + { + "colour": "#0c9900", + "exportable": true, + "hide_tag": false, + "id": "1012", + "name": "misp-galaxy:tool=\"X-Tunnel\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3009", + "name": "misp-galaxy:mitre-malware=\"XTunnel\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3010", + "name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3011", + "name": "misp-galaxy:tool=\"EVILTOSS\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3012", + "name": "misp-galaxy:mitre-malware=\"USBStealer\"", + "user_id": "0" + }, + { + "colour": "#0c9800", + "exportable": true, + "hide_tag": false, + "id": "1011", + "name": "misp-galaxy:tool=\"X-Agent\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3013", + "name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3014", + "name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3015", + "name": "misp-galaxy:exploit-kit=\"DealersChoice\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3016", + "name": "misp-galaxy:mitre-malware=\"Downdelph\"", + "user_id": "0" + } + ], + "analysis": "0", + "attribute_count": "122", + "date": "2017-12-21", + "disable_correlation": false, + "distribution": "3", + "event_creator_email": "alexandre.dulaunoy@circl.lu", + "id": "9747", + "info": "OSINT - Sednit update: How Fancy Bear Spent the Year", + "locked": false, + "org_id": "2", + "orgc_id": "2", + "proposal_email_lock": false, + "publish_timestamp": "0", + "published": false, + "sharing_group_id": "0", + "threat_level_id": "3", + "timestamp": "1513948642", + "uuid": "5a3c2fcd-8328-42bb-a95e-4f4402de0b81" } diff --git a/tests/mispevent_testfiles/existing_event_edited.json b/tests/mispevent_testfiles/existing_event_edited.json index 84c8f8b..da10762 100644 --- a/tests/mispevent_testfiles/existing_event_edited.json +++ b/tests/mispevent_testfiles/existing_event_edited.json @@ -1,4575 +1,4601 @@ { - "Event": { - "Attribute": [ - { - "Tag": [ - { - "colour": "#00223b", - "exportable": true, - "hide_tag": false, - "id": "101", - "name": "osint:source-type=\"blog-post\"", - "user_id": "0" - }, - { - "colour": "#007cd6", - "exportable": true, - "hide_tag": false, - "id": "618", - "name": "osint:certainty=\"93\"", - "user_id": "0" - } - ], - "category": "External analysis", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188757", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893921", - "to_ids": false, - "type": "link", - "uuid": "5a3c2fda-78f4-44b7-8366-46da02de0b81", - "value": "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" - }, - { - "Tag": [ - { - "colour": "#00223b", - "exportable": true, - "hide_tag": false, - "id": "101", - "name": "osint:source-type=\"blog-post\"", - "user_id": "0" - }, - { - "colour": "#007cd6", - "exportable": true, - "hide_tag": false, - "id": "618", - "name": "osint:certainty=\"93\"", - "user_id": "0" - } - ], - "category": "External analysis", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188758", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893921", - "to_ids": false, - "type": "text", - "uuid": "5a3c2fee-7c8c-438a-8f7f-465402de0b81", - "value": "The Sednit group — also known as Strontium, APT28, Fancy Bear or Sofacy — is a group of attackers operating since 2004, if not earlier, and whose main objective is to steal confidential information from specific targets.\r\n\r\nThis article is a follow-up to ESET’s presentation at BlueHat in November 2017. Late in 2016 we published a white paper covering Sednit activity between 2014 and 2016. Since then, we have continued to actively track Sednit’s operations, and today we are publishing a brief overview of what our tracking uncovered in terms of the group’s activities and updates to their toolset. The first section covers the update of their attack methodology: namely, the ways in which this group tries to compromise their targets systems. The second section covers the evolution of their tools, with a particular emphasis on a detailed analysis of a new version of their flagship malware: Xagent." - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188759", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "value": "movieultimate.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188760", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "value": "meteost.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188761", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188762", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "value": "nethostnet.com" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188763", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "value": "fsportal.net" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188764", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "value": "fastdataexchange.org" - }, - { - "category": "Network activity", - "comment": "Xagent Samples", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188765", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1513893957", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - } - ], - "Galaxy": [ - { - "GalaxyCluster": [ - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Thomas Schreck", - "Timo Steffens", - "Various" - ], - "description": "The Sofacy Group (also known as APT28, Pawn Storm, Fancy Bear and Sednit) is a cyber espionage group believed to have ties to the Russian government. Likely operating since 2007, the group is known to target government, military, and security organizations. It has been characterized as an advanced persistent threat.", - "galaxy_id": "366", - "id": "45563", - "meta": { - "country": [ - "RU" - ], - "refs": [ - "https://en.wikipedia.org/wiki/Sofacy_Group", - "https://aptnotes.malwareconfig.com/web/viewer.html?file=../APTnotes/2014/apt28.pdf", - "http://www.trendmicro.com/cloud-content/us/pdfs/security-intelligence/white-papers/wp-operation-pawn-storm.pdf", - "https://www2.fireeye.com/rs/848-DID-242/images/wp-mandiant-matryoshka-mining.pdf", - "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/", - "http://researchcenter.paloaltonetworks.com/2016/06/unit42-new-sofacy-attacks-against-us-government-agency/" - ], - "synonyms": [ - "APT 28", - "APT28", - "Pawn Storm", - "Fancy Bear", - "Sednit", - "TsarTeam", - "TG-4127", - "Group-4127", - "STRONTIUM", - "TAG_0700", - "Swallowtail", - "IRON TWILIGHT", - "Group 74" - ] - }, - "source": "MISP Project", - "tag_id": "1100", - "tag_name": "misp-galaxy:threat-actor=\"Sofacy\"", - "type": "threat-actor", - "uuid": "7cdff317-a673-4474-84ec-4f1754947823", - "value": "Sofacy", - "version": "30" - } - ], - "description": "Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.", - "icon": "user-secret", - "id": "366", - "name": "Threat Actor", - "type": "threat-actor", - "uuid": "698774c7-8022-42c4-917f-8d6e4f06ada3", - "version": "2" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "Kafeine", - "Will Metcalf", - "KahuSecurity" - ], - "description": "Sednit EK is the exploit kit used by APT28", - "galaxy_id": "370", - "id": "38813", - "meta": { - "refs": [ - "http://www.welivesecurity.com/2014/10/08/sednit-espionage-group-now-using-custom-exploit-kit/", - "http://blog.trendmicro.com/trendlabs-security-intelligence/new-adobe-flash-zero-day-used-in-pawn-storm-campaign/" - ], - "status": [ - "Active" - ] - }, - "source": "MISP Project", - "tag_id": "3007", - "tag_name": "misp-galaxy:exploit-kit=\"Sednit EK\"", - "type": "exploit-kit", - "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", - "value": "Sednit EK", - "version": "5" - }, - { - "authors": [ - "Kafeine", - "Will Metcalf", - "KahuSecurity" - ], - "description": "DealersChoice is a Flash Player Exploit platform triggered by RTF", - "galaxy_id": "370", - "id": "38805", - "meta": { - "refs": [ - "http://researchcenter.paloaltonetworks.com/2016/10/unit42-dealerschoice-sofacys-flash-player-exploit-platform/", - "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-ramps-up-spear-phishing-before-zero-days-get-patched/" - ], - "status": [ - "Active" - ], - "synonyms": [ - "Sednit RTF EK" - ] - }, - "source": "MISP Project", - "tag_id": "3015", - "tag_name": "misp-galaxy:exploit-kit=\"DealersChoice\"", - "type": "exploit-kit", - "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", - "value": "DealersChoice", - "version": "5" - } - ], - "description": "Exploit-Kit is an enumeration of some exploitation kits used by adversaries. The list includes document, browser and router exploit kits.It's not meant to be totally exhaustive but aim at covering the most seen in the past 5 years", - "icon": "internet-explorer", - "id": "370", - "name": "Exploit-Kit", - "type": "exploit-kit", - "uuid": "6ab240ec-bd79-11e6-a4a6-cec0c932ce01", - "version": "3" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "backdoor", - "galaxy_id": "367", - "id": "46592", - "meta": { - "refs": [ - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" - ], - "synonyms": [ - "Sednit", - "Seduploader", - "JHUHUGIT", - "Sofacy" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "2215", - "tag_name": "misp-galaxy:tool=\"GAMEFISH\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "GAMEFISH", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "", - "galaxy_id": "367", - "id": "46670", - "meta": { - "synonyms": [ - "XTunnel" - ] - }, - "source": "MISP Project", - "tag_id": "1012", - "tag_name": "misp-galaxy:tool=\"X-Tunnel\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "X-Tunnel", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "backdoor used by apt28\n\nSedreco serves as a spying backdoor; its functionalities can be extended with dynamically loaded plugins. It is made up of two distinct components: a dropper and the persistent payload installed by this dropper. We have not seen this component since April 2016.", - "galaxy_id": "367", - "id": "46591", - "meta": { - "possible_issues": [ - "Report tells that is could be Xagent alias (Java Rat)" - ], - "refs": [ - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" - ], - "synonyms": [ - "Sedreco", - "AZZY", - "ADVSTORESHELL", - "NETUI" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "3011", - "tag_name": "misp-galaxy:tool=\"EVILTOSS\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "EVILTOSS", - "version": "45" - }, - { - "authors": [ - "Alexandre Dulaunoy", - "Florian Roth", - "Timo Steffens", - "Christophe Vandeplas" - ], - "description": "This backdoor component is known to have a modular structure featuring various espionage functionalities, such as key-logging, screen grabbing and file exfiltration. This component is available for Osx, Windows, Linux and iOS operating systems.\n\nXagent is a modular backdoor with spying functionalities such as keystroke logging and file exfiltration. Xagent is the group’s flagship backdoor and heavily used in their operations. Early versions for Linux and Windows were seen years ago, then in 2015 an iOS version came out. One year later, an Android version was discovered and finally, in the beginning of 2017, an Xagent sample for OS X was described.", - "galaxy_id": "367", - "id": "46669", - "meta": { - "refs": [ - "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-update-ios-espionage-app-found/", - "https://app.box.com/s/l7n781ig6n8wlf1aff5hgwbh4qoi5jqq", - "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" - ], - "synonyms": [ - "XAgent" - ], - "type": [ - "Backdoor" - ] - }, - "source": "MISP Project", - "tag_id": "1011", - "tag_name": "misp-galaxy:tool=\"X-Agent\"", - "type": "tool", - "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", - "value": "X-Agent", - "version": "45" - } - ], - "description": "Threat actors tools is an enumeration of tools used by adversaries. The list includes malware but also common software regularly used by the adversaries.", - "icon": "optin-monster", - "id": "367", - "name": "Tool", - "type": "tool", - "uuid": "9b8037f7-bc8f-4de1-a797-37266619bc0b", - "version": "2" - }, - { - "GalaxyCluster": [ - { - "authors": [ - "MITRE" - ], - "description": "JHUHUGIT is malware used by APT28. It is based on Carberp source code and serves as reconnaissance malware.[[Citation: Kaspersky Sofacy]][[Citation: F-Secure Sofacy 2015]][[Citation: ESET Sednit Part 1]][[Citation: FireEye APT28 January 2017]]\n\nAliases: JHUHUGIT, Seduploader, JKEYSKW, Sednit, GAMEFISH", - "galaxy_id": "365", - "id": "41618", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0044", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part1.pdf", - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", - "https://labsblog.f-secure.com/2015/09/08/sofacy-recycles-carberp-and-metasploit-code/", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "JHUHUGIT", - "Seduploader", - "JKEYSKW", - "Sednit", - "GAMEFISH" - ], - "uuid": [ - "8ae43c46-57ef-47d5-a77a-eebb35628db2" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3008", - "tag_name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "JHUHUGIT", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "XTunnel a VPN-like network proxy tool that can relay traffic between a C2 server and a victim. It was first seen in May 2013 and reportedly used by APT28 during the compromise of the Democratic National Committee.[[Citation: Crowdstrike DNC June 2016]][[Citation: Invincea XTunnel]][[Citation: ESET Sednit Part 2]]\n\nAliases: XTunnel, X-Tunnel, XAPS", - "galaxy_id": "365", - "id": "41543", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0117", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://www.invincea.com/2016/07/tunnel-of-gov-dnc-hack-and-the-russian-xtunnel/", - "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/" - ], - "synonyms": [ - "XTunnel", - "X-Tunnel", - "XAPS" - ], - "uuid": [ - "7343e208-7cab-45f2-a47b-41ba5e2f0fab" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3009", - "tag_name": "misp-galaxy:mitre-malware=\"XTunnel\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "XTunnel", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "ADVSTORESHELL is a spying backdoor that has been used by APT28 from at least 2012 to 2016. It is generally used for long-term espionage and is deployed on targets deemed interesting after a reconnaissance phase.[[Citation: Kaspersky Sofacy]][[Citation: ESET Sednit Part 2]]\n\nAliases: ADVSTORESHELL, NETUI, EVILTOSS, AZZY, Sedreco", - "galaxy_id": "365", - "id": "41582", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0045", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "ADVSTORESHELL", - "NETUI", - "EVILTOSS", - "AZZY", - "Sedreco" - ], - "uuid": [ - "fb575479-14ef-41e9-bfab-0b7cf10bec73" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3010", - "tag_name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "ADVSTORESHELL", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "USBStealer is malware that has used by APT28 since at least 2005 to extract information from air-gapped networks. It does not have the capability to communicate over the Internet and has been used in conjunction with ADVSTORESHELL.[[Citation: ESET Sednit USBStealer 2014]][[Citation: Kaspersky Sofacy]]\n\nAliases: USBStealer, USB Stealer, Win32/USBStealer", - "galaxy_id": "365", - "id": "41549", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0136", - "http://www.welivesecurity.com/2014/11/11/sednit-espionage-group-attacking-air-gapped-networks/", - "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" - ], - "synonyms": [ - "USBStealer", - "USB Stealer", - "Win32/USBStealer" - ], - "uuid": [ - "af2ad3b7-ab6a-4807-91fd-51bcaff9acbb" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3012", - "tag_name": "misp-galaxy:mitre-malware=\"USBStealer\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "USBStealer", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "is a trojan that has been used by APT28 on OS X and appears to be a port of their standard CHOPSTICK or XAgent trojan.[[Citation: XAgentOSX]]", - "galaxy_id": "365", - "id": "41551", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0161", - "https://researchcenter.paloaltonetworks.com/2017/02/unit42-xagentosx-sofacys-xagent-macos-tool/" - ], - "uuid": [ - "5930509b-7793-4db9-bdfc-4edda7709d0d" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3013", - "tag_name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "XAgentOSX", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "CHOPSTICK is malware family of modular backdoors used by APT28. It has been used from at least November 2012 to August 2016 and is usually dropped on victims as second-stage malware, though it has been used as first-stage malware in several cases.[[Citation: FireEye APT28]][[Citation: ESET Sednit Part 2]][[Citation: FireEye APT28 January 2017]]\n\nAliases: CHOPSTICK, SPLM, Xagent, X-Agent, webhp", - "galaxy_id": "365", - "id": "41559", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0023", - "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", - "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/rpt-apt28.pdf" - ], - "synonyms": [ - "CHOPSTICK", - "SPLM", - "Xagent", - "X-Agent", - "webhp" - ], - "uuid": [ - "ccd61dfc-b03f-4689-8c18-7c97eab08472" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3014", - "tag_name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "CHOPSTICK", - "version": "4" - }, - { - "authors": [ - "MITRE" - ], - "description": "Downdelph is a first-stage downloader written in Delphi that has been used by APT28 in rare instances between 2013 and 2015.[[Citation: ESET Sednit Part 3]]\n\nAliases: Downdelph, Delphacy", - "galaxy_id": "365", - "id": "41504", - "meta": { - "refs": [ - "https://attack.mitre.org/wiki/Software/S0134", - "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part3.pdf" - ], - "synonyms": [ - "Downdelph", - "Delphacy" - ], - "uuid": [ - "08d20cd2-f084-45ee-8558-fa6ef5a18519" - ] - }, - "source": "https://github.com/mitre/cti", - "tag_id": "3016", - "tag_name": "misp-galaxy:mitre-malware=\"Downdelph\"", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "value": "Downdelph", - "version": "4" - } - ], - "description": "Name of ATT&CK software", - "icon": "optin-monster", - "id": "365", - "name": "Malware", - "type": "mitre-malware", - "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", - "version": "4" - } - ], - "Object": [ - { - "Attribute": [ - { - "Tag": [ - { - "name": "blah" - } - ], - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188944", - "object_id": "1555", - "object_relation": "filename", - "sharing_group_id": "0", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd5b6-2850-435f-bd0d-4c62950d210f", - "value": "Bulletin.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188945", - "object_id": "1555", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936310", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd5b6-78a8-4e47-8333-4c62950d210f", - "value": "68064fc152e23d56e541714af52651cb4ba81aaf" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188946", - "object_id": "1555", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936310", - "to_ids": false, - "type": "text", - "uuid": "5a3cd5b6-23d8-43ba-8518-4c62950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.AX", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1555", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "uuid": "5a3cd5b6-9568-4342-b2ab-4c62950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188947", - "object_id": "1556", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936388", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd604-748c-4fc0-88bf-c170950d210f", - "value": "f3805382ae2e23ff1147301d131a06e00e4ff75f" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188948", - "object_id": "1556", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936388", - "to_ids": false, - "type": "text", - "uuid": "5a3cd604-6668-4469-a1c0-c170950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.CVE-2016-4117.A", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1556", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936388", - "uuid": "5a3cd604-e11c-4de5-bbbf-c170950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188949", - "object_id": "1557", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd693-dc40-445d-a4d7-4ae0950d210f", - "value": "OC_PSO_2017.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188950", - "object_id": "1557", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd693-8ffc-4d95-b522-4e84950d210f", - "value": "512bdfe937314ac3f195c462c395feeb36932971" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188951", - "object_id": "1557", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936531", - "to_ids": false, - "type": "text", - "uuid": "5a3cd693-a8f0-4aea-a834-4097950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NUB", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1557", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936531", - "uuid": "5a3cd693-fd9c-4fcf-b69a-439c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188952", - "object_id": "1558", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd6c2-d31c-40cc-bcc1-4458950d210f", - "value": "NASAMS.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188953", - "object_id": "1558", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd6c2-6a54-4b4c-8748-4c84950d210f", - "value": "30b3e8c0f3f3cf200daa21c267ffab3cad64e68b" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188954", - "object_id": "1558", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936578", - "to_ids": false, - "type": "text", - "uuid": "5a3cd6c2-1c68-45de-8325-464a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1558", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936578", - "uuid": "5a3cd6c2-d290-4787-910f-4e6d950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188955", - "object_id": "1559", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd74e-584c-45b9-8557-486d950d210f", - "value": "Programm_Details.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188956", - "object_id": "1559", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd74e-f334-4e6b-b37f-462f950d210f", - "value": "4173b29a251cd9c1cab135f67cb60acab4ace0c5" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188957", - "object_id": "1559", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936718", - "to_ids": false, - "type": "text", - "uuid": "5a3cd74e-5900-4fbf-85c6-4c81950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1559", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936718", - "uuid": "5a3cd74e-1504-40ff-9a28-4501950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188958", - "object_id": "1560", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd775-e8f4-465a-aca2-4c5a950d210f", - "value": "Operation_in_Mosul.rtf" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188959", - "object_id": "1560", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd775-1190-4db7-961a-4c5a950d210f", - "value": "12a37cfdd3f3671074dd5b0f354269cec028fb52" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188960", - "object_id": "1560", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936757", - "to_ids": false, - "type": "text", - "uuid": "5a3cd775-fa5c-4453-bcb0-4c5a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1560", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936757", - "uuid": "5a3cd775-e4cc-44bb-89b6-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188961", - "object_id": "1561", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd82f-b918-4520-ba8b-5165950d210f", - "value": "ARM-NATO_ENGLISH_30_NOV_2016.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188962", - "object_id": "1561", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd82f-cae4-4209-9338-5165950d210f", - "value": "15201766bd964b7c405aeb11db81457220c31e46" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188963", - "object_id": "1561", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936943", - "to_ids": false, - "type": "text", - "uuid": "5a3cd82f-d91c-43af-8262-5165950d210f", - "value": "Malicious" - } - ], - "comment": "SWF/Agent.L", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1561", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936943", - "uuid": "5a3cd82f-2788-4561-bbeb-5165950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188964", - "object_id": "1562", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd847-0aa0-4b5c-aa30-5165950d210f", - "value": "Olympic-Agenda-2020-20-20-Recommendations.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188965", - "object_id": "1562", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd847-593c-4985-8756-5165950d210f", - "value": "8078e411fbe33864dfd8f87ad5105cc1fd26d62e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188966", - "object_id": "1562", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936967", - "to_ids": false, - "type": "text", - "uuid": "5a3cd847-1324-4fad-af60-5165950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.BL", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1562", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936967", - "uuid": "5a3cd847-b5a0-42f7-ac4b-5165950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188967", - "object_id": "1563", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd861-9350-40c1-ac29-4771950d210f", - "value": "Merry_Christmas!.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188968", - "object_id": "1563", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd861-18ac-4cf0-b96f-4986950d210f", - "value": "33447383379ca99083442b852589111296f0c603" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188969", - "object_id": "1563", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513936993", - "to_ids": false, - "type": "text", - "uuid": "5a3cd861-cfbc-4096-baae-40e2950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NUG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1563", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513936993", - "uuid": "5a3cd861-65c0-4b69-9429-4f37950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188970", - "object_id": "1564", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd87d-fa9c-41aa-897f-49a5950d210f", - "value": "Trump’s_Attack_on_Syria_English.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188971", - "object_id": "1564", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd87d-c630-4487-8336-4615950d210f", - "value": "d5235d136cfcadbef431eea7253d80bde414db9d" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188972", - "object_id": "1564", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937021", - "to_ids": false, - "type": "text", - "uuid": "5a3cd87d-8c98-4660-9026-44de950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NWZ", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1564", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937021", - "uuid": "5a3cd87d-f514-4071-a5f7-4ec2950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188973", - "object_id": "1565", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd897-4cc0-48b0-bb2c-461f950d210f", - "value": "Hotel_Reservation_Form.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188974", - "object_id": "1565", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd897-fa64-466c-9421-49c5950d210f", - "value": "f293a2bfb728060c54efeeb03c5323893b5c80df" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188975", - "object_id": "1565", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937047", - "to_ids": false, - "type": "text", - "uuid": "5a3cd897-f020-44cf-8dfc-4225950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1565", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937046", - "uuid": "5a3cd896-f6cc-4e52-bcb2-442c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188976", - "object_id": "1566", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937070", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd8ae-7194-48fd-810e-4c5a950d210f", - "value": "SB_Doc_2017-3_Implementation_of_Key_Taskings_and_Next_Steps.doc" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188977", - "object_id": "1566", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937071", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8af-f39c-443c-bcf1-4c5a950d210f", - "value": "bb10ed5d59672fbc6178e35d0feac0562513e9f0" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188978", - "object_id": "1566", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937071", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8af-b3ec-478a-b585-4c5a950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1566", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937070", - "uuid": "5a3cd8ae-54d0-46bb-adbb-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188979", - "object_id": "1567", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937083", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8bb-74d8-4d19-ae08-4043950d210f", - "value": "4873bafe44cff06845faa0ce7c270c4ce3c9f7b9" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188980", - "object_id": "1567", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937083", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8bb-77bc-4cc4-887f-429d950d210f", - "value": "Malicious" - } - ], - "comment": "", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1567", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937083", - "uuid": "5a3cd8bb-a704-4f1d-a235-444e950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188981", - "object_id": "1568", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937097", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8c9-4d2c-4145-a637-4f13950d210f", - "value": "169c8f3e3d22e192c108bc95164d362ce5437465" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188982", - "object_id": "1568", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937097", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8c9-7ff0-42f7-ae80-4eb6950d210f", - "value": "Malicious" - } - ], - "comment": "", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1568", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937097", - "uuid": "5a3cd8c9-6568-406a-853c-4862950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188983", - "object_id": "1569", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937116", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8dc-48c0-4ea0-a67d-4734950d210f", - "value": "cc7607015cd7a1a4452acd3d87adabdd7e005bd7" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188984", - "object_id": "1569", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937116", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8dc-9ed8-4a4d-9ceb-4daa950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1569", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937115", - "uuid": "5a3cd8db-2838-4466-a986-4afb950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188985", - "object_id": "1570", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd8fb-1efc-4059-ae7a-42f5950d210f", - "value": "Caucasian_Eagle_ENG.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188986", - "object_id": "1570", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd8fb-9cec-4a30-8b2f-4441950d210f", - "value": "5d2c7d87995cc5b8184baba2c7a1900a48b2f42d" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188987", - "object_id": "1570", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937147", - "to_ids": false, - "type": "text", - "uuid": "5a3cd8fb-e52c-489b-8da5-43d1950d210f", - "value": "Malicious" - } - ], - "comment": "Win32/Exploit.Agent.NTM", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1570", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937147", - "uuid": "5a3cd8fb-cd14-4b00-9710-430c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188988", - "object_id": "1571", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd90e-5eb4-4069-b160-5276950d210f", - "value": "World War3.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188989", - "object_id": "1571", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd90e-6d2c-4ffc-a699-5276950d210f", - "value": "7aada8bcc0d1ab8ffb1f0fae4757789c6f5546a3" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188990", - "object_id": "1571", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937166", - "to_ids": false, - "type": "text", - "uuid": "5a3cd90e-28e8-410e-8033-5276950d210f", - "value": "Malicious" - } - ], - "comment": "SWF/Exploit.CVE-2017-11292.A", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1571", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937166", - "uuid": "5a3cd90e-538c-4b7e-95dc-5276950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188991", - "object_id": "1572", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd927-e810-4d22-a0e4-4057950d210f", - "value": "SaberGuardian2017.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188992", - "object_id": "1572", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd927-f284-43b9-83d1-473b950d210f", - "value": "68c2809560c7623d2307d8797691abf3eafe319a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188993", - "object_id": "1572", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937191", - "to_ids": false, - "type": "text", - "uuid": "5a3cd927-b844-49f2-a1a9-4c85950d210f", - "value": "Malicious" - } - ], - "comment": "VBA/DDE.E", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1572", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937191", - "uuid": "5a3cd927-e410-489c-abfc-4b63950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188994", - "object_id": "1573", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": true, - "type": "filename", - "uuid": "5a3cd93c-2438-4dda-823e-463d950d210f", - "value": "IsisAttackInNewYork.docx" - }, - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188995", - "object_id": "1573", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cd93c-1ef0-4d81-9476-4655950d210f", - "value": "1c6c700ceebfbe799e115582665105caa03c5c9e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188996", - "object_id": "1573", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937212", - "to_ids": false, - "type": "text", - "uuid": "5a3cd93c-949c-40ac-9094-4a4a950d210f", - "value": "Malicious" - } - ], - "comment": "VBA/DDE.L", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1573", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937212", - "uuid": "5a3cd93c-716c-4918-a00f-4671950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188997", - "object_id": "1574", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937559", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cda97-7e58-4642-aaf5-c5ed950d210f", - "value": "6f0fc0ebba3e4c8b26a69cdf519edf8d1aa2f4bb" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1188998", - "object_id": "1574", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937559", - "to_ids": false, - "type": "text", - "uuid": "5a3cda97-6020-423d-9d23-c5ed950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "value": "movieultimate.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "159", - "object_id": "1574", - "object_uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f", - "referenced_id": "1188759", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513937826", - "uuid": "5a3cdba2-2fdc-4f9a-a4eb-4dae950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1574", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513937826", - "uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1188999", - "object_id": "1575", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937864", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdbc8-0aac-4d8a-8c1f-4c5a950d210f", - "value": "e19f753e514f6adec8f81bcdefb9117979e69627" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189000", - "object_id": "1575", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937864", - "to_ids": false, - "type": "text", - "uuid": "5a3cdbc8-e204-4606-b9ea-4c5a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "value": "meteost.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "160", - "object_id": "1575", - "object_uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f", - "referenced_id": "1188760", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938091", - "uuid": "5a3cdcab-8200-4c65-868e-42a9950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1575", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938091", - "uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189001", - "object_id": "1576", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937910", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdbf6-eca0-4c09-9bd0-4c59950d210f", - "value": "961468ddd3d0fa25beb8210c81ba620f9170ed30" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189002", - "object_id": "1576", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937910", - "to_ids": false, - "type": "text", - "uuid": "5a3cdbf6-acd8-4a36-a028-4c59950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "164", - "object_id": "1576", - "object_uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f", - "referenced_id": "1188761", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938210", - "uuid": "5a3cdd22-b7d8-4754-a108-4742950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1576", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938210", - "uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189003", - "object_id": "1577", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937929", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc09-b428-4c0b-9969-c5ed950d210f", - "value": "a0719b50265505c8432616c0a4e14ed206981e95" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189004", - "object_id": "1577", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937929", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc09-05d8-4356-ba52-c5ed950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "value": "nethostnet.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "162", - "object_id": "1577", - "object_uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f", - "referenced_id": "1188762", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-968c-4572-9f64-491502de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938169", - "uuid": "5a3cdcf9-d5a4-4c8e-a201-45b1950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1577", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938169", - "uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189005", - "object_id": "1578", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937953", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc21-a170-4637-b139-4812950d210f", - "value": "2cf6436b99d11d9d1e0c488af518e35162ecbc9c" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189006", - "object_id": "1578", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937953", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc21-3274-4800-9e91-41e2950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "value": "faststoragefiles.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "165", - "object_id": "1578", - "object_uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f", - "referenced_id": "1188761", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938226", - "uuid": "5a3cdd32-3044-4895-8f18-4d06950d210f" - } - ], - "comment": "Win64/Sednit.Y", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1578", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938226", - "uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189007", - "object_id": "1579", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937975", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc37-cee0-43d0-9e20-4db6950d210f", - "value": "fec29b4f4dccc59770c65c128dfe4564d7c13d33" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189008", - "object_id": "1579", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937976", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc38-ac24-44be-a1ed-4935950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "value": "fsportal.net" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "163", - "object_id": "1579", - "object_uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f", - "referenced_id": "1188763", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938189", - "uuid": "5a3cdd0d-d990-42ba-830d-5156950d210f" - } - ], - "comment": "Win64/Sednit.Y", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1579", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938190", - "uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189009", - "object_id": "1580", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513937992", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc48-c74c-4b6e-8202-5156950d210f", - "value": "57d7f3d31c491f8aef4665ca4dd905c3c8a98795" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189010", - "object_id": "1580", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513937992", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc48-55dc-420e-9b5d-5156950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "value": "fastdataexchange.org" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "161", - "object_id": "1580", - "object_uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f", - "referenced_id": "1188764", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938129", - "uuid": "5a3cdcd1-c6cc-43d8-a2f4-4681950d210f" - } - ], - "comment": "Win64/Sednit.Z", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1580", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938129", - "uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189011", - "object_id": "1581", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513938011", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc5b-54a8-4e60-bc67-4c5a950d210f", - "value": "a3bf5b5cf5a5ef438a198a6f61f7225c0a4a7138" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189012", - "object_id": "1581", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513938011", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc5b-b390-4183-aec7-4c5a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "168", - "object_id": "1581", - "object_uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f", - "referenced_id": "1188765", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938280", - "uuid": "5a3cdd68-7968-40d1-a0a9-5156950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1581", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938280", - "uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189013", - "object_id": "1582", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513938034", - "to_ids": true, - "type": "sha1", - "uuid": "5a3cdc72-ba30-4ecd-9d21-4654950d210f", - "value": "1958e722afd0dba266576922abc98aa505cf5f9a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189014", - "object_id": "1582", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513938034", - "to_ids": false, - "type": "text", - "uuid": "5a3cdc72-0804-42c4-acfa-4ac5950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Attribute": { - "category": "Network activity", - "distribution": "5", - "sharing_group_id": "0", - "to_ids": true, - "type": "domain", - "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "value": "newfilmts.com" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "167", - "object_id": "1582", - "object_uuid": "5a3cdc72-1538-4c66-af46-427b950d210f", - "referenced_id": "1188765", - "referenced_type": "0", - "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", - "relationship_type": "communicates-with", - "timestamp": "1513938264", - "uuid": "5a3cdd58-9800-4bae-837c-4f20950d210f" - } - ], - "comment": "Win32/Sednit.BO", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1582", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513938264", - "uuid": "5a3cdc72-1538-4c66-af46-427b950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189015", - "object_id": "1583", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939882", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3aa-e104-481e-a7f4-4bc1950d210f", - "value": "9f6bed7d7f4728490117cbc85819c2e6c494251b" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189016", - "object_id": "1583", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939882", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3aa-74fc-48c7-af40-4c6a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "173", - "object_id": "1583", - "object_uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f", - "referenced_id": "1592", - "referenced_type": "1", - "referenced_uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513947459", - "uuid": "5a3d0143-c300-4118-8afe-4a2d950d210f" - } - ], - "comment": "Win32/Sednit.AX\t", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1583", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948642", - "uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189017", - "object_id": "1584", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939907", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3c3-6d9c-48f4-93db-4a61950d210f", - "value": "4bc722a9b0492a50bd86a1341f02c74c0d773db7" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189018", - "object_id": "1584", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939907", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3c3-c38c-4e30-a904-4c8f950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "188", - "object_id": "1584", - "object_uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f", - "referenced_id": "1603", - "referenced_type": "1", - "referenced_uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948518", - "uuid": "5a3d0566-34fc-4a62-b2a5-4f91950d210f" - } - ], - "comment": "Win32/Sednit.BS", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1584", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948535", - "uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189019", - "object_id": "1585", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939924", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3d4-9168-4e23-8b64-485a950d210f", - "value": "ab354807e687993fbeb1b325eb6e4ab38d428a1e" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189020", - "object_id": "1585", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939924", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3d4-27e0-4366-943f-4b9a950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "189", - "object_id": "1585", - "object_uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f", - "referenced_id": "1602", - "referenced_type": "1", - "referenced_uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948528", - "uuid": "5a3d0570-a86c-4264-a43a-4125950d210f" - } - ], - "comment": "Win32/Sednit.BS", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1585", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948597", - "uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189021", - "object_id": "1586", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939946", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce3ea-8dbc-4cf4-997f-448b950d210f", - "value": "9c47ca3883196b3a84d67676a804ff50e22b0a9f" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189022", - "object_id": "1586", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939946", - "to_ids": false, - "type": "text", - "uuid": "5a3ce3ea-e714-444e-ad9b-40b0950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "190", - "object_id": "1586", - "object_uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f", - "referenced_id": "1601", - "referenced_type": "1", - "referenced_uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948614", - "uuid": "5a3d05c6-0618-4520-9549-48a0950d210f" - } - ], - "comment": "Win32/Sednit.BR", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1586", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948626", - "uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189023", - "object_id": "1587", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939972", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce404-7bfc-4316-bd32-55ea950d210f", - "value": "8a68f26d01372114f660e32ac4c9117e5d0577f1" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189024", - "object_id": "1587", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939972", - "to_ids": false, - "type": "text", - "uuid": "5a3ce404-7224-4525-922a-55ea950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "182", - "object_id": "1587", - "object_uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f", - "referenced_id": "1600", - "referenced_type": "1", - "referenced_uuid": "5a3ce680-90d4-478d-95db-48a6950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948044", - "uuid": "5a3d038c-1cc8-4d9c-87ab-c5ed950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1587", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948073", - "uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189025", - "object_id": "1588", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513939991", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce417-62a4-4d46-9a87-55ea950d210f", - "value": "476fc1d31722ac26b46154cbf0c631d60268b28a" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189026", - "object_id": "1588", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513939991", - "to_ids": false, - "type": "text", - "uuid": "5a3ce417-43f0-494d-ac2e-55ea950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "187", - "object_id": "1588", - "object_uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f", - "referenced_id": "1599", - "referenced_type": "1", - "referenced_uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948483", - "uuid": "5a3d0543-8f74-4086-aafc-418a950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1588", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948498", - "uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189027", - "object_id": "1589", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940012", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce42c-836c-49e7-a9f3-4a5f950d210f", - "value": "f9fd3f1d8da4ffd6a494228b934549d09e3c59d1" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189028", - "object_id": "1589", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940012", - "to_ids": false, - "type": "text", - "uuid": "5a3ce42c-4c88-4940-94b8-4084950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "183", - "object_id": "1589", - "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", - "referenced_id": "1594", - "referenced_type": "1", - "referenced_uuid": "5a3ce60a-6db8-4212-b194-4339950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948106", - "uuid": "5a3d03ca-2398-4060-b13c-404a950d210f" - }, - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "184", - "object_id": "1589", - "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", - "referenced_id": "1595", - "referenced_type": "1", - "referenced_uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948117", - "uuid": "5a3d03d5-6d8c-4dfb-b193-4002950d210f" - } - ], - "comment": "Win32/Sednit.BN", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1589", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948128", - "uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189029", - "object_id": "1590", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940027", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce43b-6738-4a14-a318-4d65950d210f", - "value": "e338d49c270baf64363879e5eecb8fa6bdde8ad9" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189030", - "object_id": "1590", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940027", - "to_ids": false, - "type": "text", - "uuid": "5a3ce43b-3a10-4d78-9ee2-485c950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "186", - "object_id": "1590", - "object_uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f", - "referenced_id": "1593", - "referenced_type": "1", - "referenced_uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513948320", - "uuid": "5a3d04a0-9d28-47c3-a12c-465b950d210f" - } - ], - "comment": "Win32/Sednit.BG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1590", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513948339", - "uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f" - }, - { - "Attribute": [ - { - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189031", - "object_id": "1591", - "object_relation": "sha1", - "sharing_group_id": "0", - "timestamp": "1513940042", - "to_ids": true, - "type": "sha1", - "uuid": "5a3ce44a-2ea4-4526-8bbc-c328950d210f", - "value": "6e167da3c5d887fa2e58da848a2245d11b6c5ad6" - }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "9747", - "id": "1189032", - "object_id": "1591", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1513940042", - "to_ids": false, - "type": "text", - "uuid": "5a3ce44a-5118-4142-97f0-c328950d210f", - "value": "Malicious" - } - ], - "ObjectReference": [ - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "170", - "object_id": "1591", - "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", - "referenced_id": "1597", - "referenced_type": "1", - "referenced_uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513940734", - "uuid": "5a3ce6fe-b0c4-44df-a609-419a950d210f" - }, - { - "Object": { - "distribution": "5", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" - }, - "comment": "", - "deleted": false, - "event_id": "9747", - "id": "171", - "object_id": "1591", - "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", - "referenced_id": "1598", - "referenced_type": "1", - "referenced_uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f", - "relationship_type": "communicates-with", - "timestamp": "1513940753", - "uuid": "5a3ce711-a0dc-4dbe-b59e-495a950d210f" - } - ], - "comment": "Win32/Sednit.BG", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "9747", - "id": "1591", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1513940753", - "uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189033", - "object_id": "1592", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940362", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce58a-fcd8-48d5-8b4a-4fd9950d210f", - "value": "87.236.211.182" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189034", - "object_id": "1592", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940362", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce58a-6e14-48ea-9746-48f2950d210f", - "value": "servicecdp.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1592", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940362", - "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189035", - "object_id": "1593", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940472", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce5f8-99b4-41a2-915a-4bf8950d210f", - "value": "95.215.45.43" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189036", - "object_id": "1593", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940472", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce5f8-62c8-4f04-89c2-4aeb950d210f", - "value": "wmdmediacodecs.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1593", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940472", - "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189037", - "object_id": "1594", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940490", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce60a-cc50-4553-bfff-4ea9950d210f", - "value": "89.45.67.144" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189038", - "object_id": "1594", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940491", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce60b-e648-4667-8432-4ba8950d210f", - "value": "mvband.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1594", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940490", - "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189039", - "object_id": "1595", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940506", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce61a-4458-4c36-866e-44e9950d210f", - "value": "89.33.246.117" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189040", - "object_id": "1595", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940506", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce61a-f820-4a43-b3d9-47e5950d210f", - "value": "mvtband.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1595", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940506", - "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189041", - "object_id": "1596", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940542", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce63e-66d4-483f-bae6-44f6950d210f", - "value": "87.236.211.182" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189042", - "object_id": "1596", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940542", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce63e-0d88-405b-82a9-43b5950d210f", - "value": "servicecdp.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1596", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940542", - "uuid": "5a3ce63e-0240-46f5-b9ed-4759950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189043", - "object_id": "1597", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940558", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce64e-d7a8-4817-a132-4c72950d210f", - "value": "185.156.173.70" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189044", - "object_id": "1597", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940558", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce64e-243c-4931-b733-403c950d210f", - "value": "runvercheck.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1597", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940558", - "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189045", - "object_id": "1598", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940572", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce65c-bf78-4b78-bafd-4cf6950d210f", - "value": "191.101.31.96" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189046", - "object_id": "1598", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940572", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce65c-8140-4146-a927-45e4950d210f", - "value": "remsupport.org" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1598", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940572", - "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189047", - "object_id": "1599", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940591", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce66f-150c-43ec-a3ff-4aa5950d210f", - "value": "89.187.150.44" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189048", - "object_id": "1599", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940591", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce66f-466c-478e-8064-4b42950d210f", - "value": "viters.org" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1599", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940590", - "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189049", - "object_id": "1600", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940608", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce680-7b04-466d-b187-4301950d210f", - "value": "146.185.253.132" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189050", - "object_id": "1600", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940608", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce680-12f4-4001-9f86-4aa4950d210f", - "value": "myinvestgroup.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1600", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940608", - "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189051", - "object_id": "1601", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940621", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce68d-0108-4557-8921-4377950d210f", - "value": "86.106.131.141" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189052", - "object_id": "1601", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940622", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce68e-54d0-4c67-8c4c-4dea950d210f", - "value": "space-delivery.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1601", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940621", - "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189054", - "object_id": "1602", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940642", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce6a2-4a38-4b90-8d74-4f10950d210f", - "value": "89.34.111.160" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189055", - "object_id": "1602", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940642", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce6a2-ffa4-4afb-89ab-42a6950d210f", - "value": "satellitedeluxpanorama.com" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1602", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940641", - "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" - }, - { - "Attribute": [ - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189056", - "object_id": "1603", - "object_relation": "ip", - "sharing_group_id": "0", - "timestamp": "1513940654", - "to_ids": true, - "type": "ip-dst", - "uuid": "5a3ce6ae-601c-44b8-8eec-4a5f950d210f", - "value": "185.216.35.26" - }, - { - "category": "Network activity", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "9747", - "id": "1189057", - "object_id": "1603", - "object_relation": "domain", - "sharing_group_id": "0", - "timestamp": "1513940654", - "to_ids": true, - "type": "domain", - "uuid": "5a3ce6ae-3b00-420a-82fd-45fb950d210f", - "value": "webviewres.net" - } - ], - "comment": "", - "deleted": false, - "description": "A domain and IP address seen as a tuple in a specific time frame.", - "distribution": "5", - "event_id": "9747", - "id": "1603", - "meta-category": "network", - "name": "domain-ip", - "sharing_group_id": "0", - "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", - "template_version": "5", - "timestamp": "1513940654", - "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" - } - ], - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + "Attribute": [ + { + "Tag": [ + { + "colour": "#00223b", + "exportable": true, + "hide_tag": false, + "id": "101", + "name": "osint:source-type=\"blog-post\"", + "user_id": "0" + }, + { + "colour": "#007cd6", + "exportable": true, + "hide_tag": false, + "id": "618", + "name": "osint:certainty=\"93\"", + "user_id": "0" + } + ], + "category": "External analysis", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188757", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893921", + "to_ids": false, + "type": "link", + "uuid": "5a3c2fda-78f4-44b7-8366-46da02de0b81", + "value": "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + { + "Tag": [ + { + "colour": "#00223b", + "exportable": true, + "hide_tag": false, + "id": "101", + "name": "osint:source-type=\"blog-post\"", + "user_id": "0" + }, + { + "colour": "#007cd6", + "exportable": true, + "hide_tag": false, + "id": "618", + "name": "osint:certainty=\"93\"", + "user_id": "0" + } + ], + "category": "External analysis", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188758", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893921", + "to_ids": false, + "type": "text", + "uuid": "5a3c2fee-7c8c-438a-8f7f-465402de0b81", + "value": "The Sednit group — also known as Strontium, APT28, Fancy Bear or Sofacy — is a group of attackers operating since 2004, if not earlier, and whose main objective is to steal confidential information from specific targets.\r\n\r\nThis article is a follow-up to ESET’s presentation at BlueHat in November 2017. Late in 2016 we published a white paper covering Sednit activity between 2014 and 2016. Since then, we have continued to actively track Sednit’s operations, and today we are publishing a brief overview of what our tracking uncovered in terms of the group’s activities and updates to their toolset. The first section covers the update of their attack methodology: namely, the ways in which this group tries to compromise their targets systems. The second section covers the evolution of their tools, with a particular emphasis on a detailed analysis of a new version of their flagship malware: Xagent." }, - "RelatedEvent": [ - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-12-14", - "distribution": "3", - "id": "9616", - "info": "OSINT - Attackers Deploy New ICS Attack Framework “TRITON” and Cause Operational Disruption to Critical Infrastructure", - "org_id": "2", - "orgc_id": "2", - "published": false, - "threat_level_id": "3", - "timestamp": "1513674510", - "uuid": "5a329d19-03e0-4eaa-8b4d-4310950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-12-07", - "distribution": "3", - "id": "9552", - "info": "OSINT - Master Channel: The Boleto Mestre Campaign Targets Brazil", - "org_id": "2", - "orgc_id": "2", - "published": false, - "threat_level_id": "3", - "timestamp": "1512657975", - "uuid": "5a2943a3-c574-44bb-8e68-45de950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "0", - "date": "2017-11-27", - "distribution": "3", - "id": "9513", - "info": "OSINT - Tizi: Detecting and blocking socially engineered spyware on Android", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1512356440", - "uuid": "5a23a972-e6a0-4a05-b505-4e8f02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-11-07", - "distribution": "3", - "id": "9309", - "info": "OSINT - Threat Group APT28 Slips Office Malware into Doc Citing NYC Terror Attack", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1511385862", - "uuid": "5a021bc2-8e0c-4ac5-b048-cc3e02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "Orgc": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "analysis": "2", - "date": "2017-10-23", - "distribution": "3", - "id": "9208", - "info": "Talos: “Cyber Conflict” Decoy Document Used In Real Cyber Conflict", - "org_id": "291", - "orgc_id": "291", - "published": true, - "threat_level_id": "2", - "timestamp": "1510088616", - "uuid": "59ed9c81-6484-47a9-aab4-191d0a950b0c" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-08-11", - "distribution": "3", - "id": "8798", - "info": "OSINT - APT28 Targets Hospitality Sector, Presents Threat to Travelers", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1502460096", - "uuid": "598db7fd-47a8-45f8-9414-408b02de0b81" - } - }, - { - "Event": { - "Org": { - "id": "231", - "name": "kingfisherops.com", - "uuid": "566ff5f4-7020-4089-9003-4374950d210f" - }, - "Orgc": { - "id": "204", - "name": "CERT-BUND", - "uuid": "56a64d7a-63dc-4471-bce9-4accc25ed029" - }, - "analysis": "0", - "date": "2017-07-25", - "distribution": "3", - "id": "8750", - "info": "European Defence Agency lure drops mssuppa.dat", - "org_id": "231", - "orgc_id": "204", - "published": true, - "threat_level_id": "2", - "timestamp": "1500967989", - "uuid": "5976f294-a844-44fe-a4f0-6c67c25ed029" - } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "analysis": "2", - "date": "2017-05-11", - "distribution": "3", - "id": "7820", - "info": "APT28-Sednit adds two zero-day exploits using ‘Trump’s attack on Syria’ as a decoy", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494824291", - "uuid": "59147a22-3100-4779-9377-360395ca48b7" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "date": "2017-05-09", - "distribution": "3", - "id": "7801", - "info": "OSINT - EPS Processing Zero-Days Exploited by Multiple Threat Actors", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1494354378", - "uuid": "59120865-27e0-4e6d-9b74-4a9f950d210f" - } - }, - { - "Event": { - "Org": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "2", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "0", - "date": "2016-12-29", - "distribution": "3", - "id": "5667", - "info": "OSINT - GRIZZLY STEPPE – Russian Malicious Cyber Activity", - "org_id": "2", - "orgc_id": "2", - "published": true, - "threat_level_id": "3", - "timestamp": "1494853878", - "uuid": "58658c15-54ac-43c3-9beb-414502de0b81" - } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" - }, - "analysis": "2", - "date": "2016-12-20", - "distribution": "1", - "id": "5616", - "info": "APT28-The Sofacy Group's DealersChoice Attacks Continue", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494829249", - "uuid": "58594faf-e98c-4c03-a58c-43cf95ca48b7" - } - }, - { - "Event": { - "Org": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "Orgc": { - "id": "291", - "name": "NCSC-NL", - "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" - }, - "analysis": "1", - "date": "2016-11-09", - "distribution": "3", - "id": "5348", - "info": "[APT-28/Sofacy]Pawn Storm Ramps Up [European Government] Spear-phishing Before Zero-Days Get Patched", - "org_id": "291", - "orgc_id": "291", - "published": true, - "threat_level_id": "1", - "timestamp": "1481709638", - "uuid": "582341ff-0830-4b32-aaba-08640a950b0c" - } - }, - { - "Event": { - "Org": { - "id": "74", - "name": "PwC.lu", - "uuid": "55f6ea61-4f74-40b6-a6df-4ff9950d210f" - }, - "Orgc": { - "id": "325", - "name": "CUDESO", - "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" - }, - "analysis": "2", - "date": "2016-11-09", - "distribution": "3", - "id": "5641", - "info": "Pawn Storm Ramps Up Spear-phishing Before Zero-Days Get Patched", - "org_id": "74", - "orgc_id": "325", - "published": true, - "threat_level_id": "2", - "timestamp": "1478712711", - "uuid": "58235d0e-34d4-41c1-9a2e-04dcc0a8ab16" - } - }, - { - "Event": { - "Org": { - "id": "335", - "name": "Orange CERT-CC", - "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" - }, - "Orgc": { - "id": "335", - "name": "Orange CERT-CC", - "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" - }, - "analysis": "0", - "date": "2016-10-18", + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188759", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "value": "movieultimate.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188760", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "value": "meteost.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188761", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188762", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "value": "nethostnet.com" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188763", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "value": "fsportal.net" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188764", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "value": "fastdataexchange.org" + }, + { + "category": "Network activity", + "comment": "Xagent Samples", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188765", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1513893957", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + } + ], + "Galaxy": [ + { + "GalaxyCluster": [ + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Thomas Schreck", + "Timo Steffens", + "Various" + ], + "default": false, + "description": "The Sofacy Group (also known as APT28, Pawn Storm, Fancy Bear and Sednit) is a cyber espionage group believed to have ties to the Russian government. Likely operating since 2007, the group is known to target government, military, and security organizations. It has been characterized as an advanced persistent threat.", "distribution": "0", - "id": "5163", - "info": "Orange-CERT-CC Test #01", - "org_id": "335", - "orgc_id": "335", - "published": false, - "threat_level_id": "3", - "timestamp": "1476782422", - "uuid": "5805e8a5-611c-498b-839b-bd57950d210f" + "galaxy_id": "366", + "id": "45563", + "meta": { + "country": [ + "RU" + ], + "refs": [ + "https://en.wikipedia.org/wiki/Sofacy_Group", + "https://aptnotes.malwareconfig.com/web/viewer.html?file=../APTnotes/2014/apt28.pdf", + "http://www.trendmicro.com/cloud-content/us/pdfs/security-intelligence/white-papers/wp-operation-pawn-storm.pdf", + "https://www2.fireeye.com/rs/848-DID-242/images/wp-mandiant-matryoshka-mining.pdf", + "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/", + "http://researchcenter.paloaltonetworks.com/2016/06/unit42-new-sofacy-attacks-against-us-government-agency/" + ], + "synonyms": [ + "APT 28", + "APT28", + "Pawn Storm", + "Fancy Bear", + "Sednit", + "TsarTeam", + "TG-4127", + "Group-4127", + "STRONTIUM", + "TAG_0700", + "Swallowtail", + "IRON TWILIGHT", + "Group 74" + ] + }, + "source": "MISP Project", + "tag_id": "1100", + "tag_name": "misp-galaxy:threat-actor=\"Sofacy\"", + "type": "threat-actor", + "uuid": "7cdff317-a673-4474-84ec-4f1754947823", + "value": "Sofacy", + "version": "30" } - }, - { - "Event": { - "Org": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + ], + "description": "Threat actors are characteristics of malicious actors (or adversaries) representing a cyber attack threat including presumed intent and historically observed behaviour.", + "icon": "user-secret", + "id": "366", + "name": "Threat Actor", + "type": "threat-actor", + "uuid": "698774c7-8022-42c4-917f-8d6e4f06ada3", + "version": "2" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "Kafeine", + "Will Metcalf", + "KahuSecurity" + ], + "default": false, + "description": "Sednit EK is the exploit kit used by APT28", + "distribution": "0", + "galaxy_id": "370", + "id": "38813", + "meta": { + "refs": [ + "http://www.welivesecurity.com/2014/10/08/sednit-espionage-group-now-using-custom-exploit-kit/", + "http://blog.trendmicro.com/trendlabs-security-intelligence/new-adobe-flash-zero-day-used-in-pawn-storm-campaign/" + ], + "status": [ + "Active" + ] }, - "Orgc": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + "source": "MISP Project", + "tag_id": "3007", + "tag_name": "misp-galaxy:exploit-kit=\"Sednit EK\"", + "type": "exploit-kit", + "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", + "value": "Sednit EK", + "version": "5" + }, + { + "authors": [ + "Kafeine", + "Will Metcalf", + "KahuSecurity" + ], + "default": false, + "description": "DealersChoice is a Flash Player Exploit platform triggered by RTF", + "distribution": "0", + "galaxy_id": "370", + "id": "38805", + "meta": { + "refs": [ + "http://researchcenter.paloaltonetworks.com/2016/10/unit42-dealerschoice-sofacys-flash-player-exploit-platform/", + "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-ramps-up-spear-phishing-before-zero-days-get-patched/" + ], + "status": [ + "Active" + ], + "synonyms": [ + "Sednit RTF EK" + ] }, - "analysis": "2", - "date": "2016-10-17", - "distribution": "3", - "id": "5165", - "info": "OSINT: ‘DealersChoice’ is Sofacy’s Flash Player Exploit Platform", - "org_id": "278", - "orgc_id": "278", - "published": true, - "threat_level_id": "1", - "timestamp": "1476789563", - "uuid": "580602f6-f8b8-4ac3-9813-7bf7bce2ab96" + "source": "MISP Project", + "tag_id": "3015", + "tag_name": "misp-galaxy:exploit-kit=\"DealersChoice\"", + "type": "exploit-kit", + "uuid": "454f4e78-bd7c-11e6-a4a6-cec0c932ce01", + "value": "DealersChoice", + "version": "5" } - }, - { - "Event": { - "Org": { - "id": "412", - "name": "TS", - "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + ], + "description": "Exploit-Kit is an enumeration of some exploitation kits used by adversaries. The list includes document, browser and router exploit kits.It's not meant to be totally exhaustive but aim at covering the most seen in the past 5 years", + "icon": "internet-explorer", + "id": "370", + "name": "Exploit-Kit", + "type": "exploit-kit", + "uuid": "6ab240ec-bd79-11e6-a4a6-cec0c932ce01", + "version": "3" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "backdoor", + "distribution": "0", + "galaxy_id": "367", + "id": "46592", + "meta": { + "refs": [ + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" + ], + "synonyms": [ + "Sednit", + "Seduploader", + "JHUHUGIT", + "Sofacy" + ], + "type": [ + "Backdoor" + ] }, - "Orgc": { - "id": "412", - "name": "TS", - "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + "source": "MISP Project", + "tag_id": "2215", + "tag_name": "misp-galaxy:tool=\"GAMEFISH\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "GAMEFISH", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "", + "distribution": "0", + "galaxy_id": "367", + "id": "46670", + "meta": { + "synonyms": [ + "XTunnel" + ] }, - "analysis": "2", - "date": "2016-08-19", - "distribution": "1", - "id": "4710", - "info": "bullettin.doc sample, linked to APT28 campaign", - "org_id": "412", - "orgc_id": "412", - "published": true, - "threat_level_id": "1", - "timestamp": "1476776982", - "uuid": "57b7248f-283c-442e-8e02-2d0f5b86d7e5" + "source": "MISP Project", + "tag_id": "1012", + "tag_name": "misp-galaxy:tool=\"X-Tunnel\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "X-Tunnel", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "backdoor used by apt28\n\nSedreco serves as a spying backdoor; its functionalities can be extended with dynamically loaded plugins. It is made up of two distinct components: a dropper and the persistent payload installed by this dropper. We have not seen this component since April 2016.", + "distribution": "0", + "galaxy_id": "367", + "id": "46591", + "meta": { + "possible_issues": [ + "Report tells that is could be Xagent alias (Java Rat)" + ], + "refs": [ + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf" + ], + "synonyms": [ + "Sedreco", + "AZZY", + "ADVSTORESHELL", + "NETUI" + ], + "type": [ + "Backdoor" + ] + }, + "source": "MISP Project", + "tag_id": "3011", + "tag_name": "misp-galaxy:tool=\"EVILTOSS\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "EVILTOSS", + "version": "45" + }, + { + "authors": [ + "Alexandre Dulaunoy", + "Florian Roth", + "Timo Steffens", + "Christophe Vandeplas" + ], + "default": false, + "description": "This backdoor component is known to have a modular structure featuring various espionage functionalities, such as key-logging, screen grabbing and file exfiltration. This component is available for Osx, Windows, Linux and iOS operating systems.\n\nXagent is a modular backdoor with spying functionalities such as keystroke logging and file exfiltration. Xagent is the group’s flagship backdoor and heavily used in their operations. Early versions for Linux and Windows were seen years ago, then in 2015 an iOS version came out. One year later, an Android version was discovered and finally, in the beginning of 2017, an Xagent sample for OS X was described.", + "distribution": "0", + "galaxy_id": "367", + "id": "46669", + "meta": { + "refs": [ + "http://blog.trendmicro.com/trendlabs-security-intelligence/pawn-storm-update-ios-espionage-app-found/", + "https://app.box.com/s/l7n781ig6n8wlf1aff5hgwbh4qoi5jqq", + "https://www.welivesecurity.com/2017/12/21/sednit-update-fancy-bear-spent-year/" + ], + "synonyms": [ + "XAgent" + ], + "type": [ + "Backdoor" + ] + }, + "source": "MISP Project", + "tag_id": "1011", + "tag_name": "misp-galaxy:tool=\"X-Agent\"", + "type": "tool", + "uuid": "0d821b68-9d82-4c6d-86a6-1071a9e0f79f", + "value": "X-Agent", + "version": "45" } - }, - { - "Event": { - "Org": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + ], + "description": "Threat actors tools is an enumeration of tools used by adversaries. The list includes malware but also common software regularly used by the adversaries.", + "icon": "optin-monster", + "id": "367", + "name": "Tool", + "type": "tool", + "uuid": "9b8037f7-bc8f-4de1-a797-37266619bc0b", + "version": "2" + }, + { + "GalaxyCluster": [ + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "JHUHUGIT is malware used by APT28. It is based on Carberp source code and serves as reconnaissance malware.[[Citation: Kaspersky Sofacy]][[Citation: F-Secure Sofacy 2015]][[Citation: ESET Sednit Part 1]][[Citation: FireEye APT28 January 2017]]\n\nAliases: JHUHUGIT, Seduploader, JKEYSKW, Sednit, GAMEFISH", + "distribution": "0", + "galaxy_id": "365", + "id": "41618", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0044", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part1.pdf", + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", + "https://labsblog.f-secure.com/2015/09/08/sofacy-recycles-carberp-and-metasploit-code/", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "JHUHUGIT", + "Seduploader", + "JKEYSKW", + "Sednit", + "GAMEFISH" + ], + "uuid": [ + "8ae43c46-57ef-47d5-a77a-eebb35628db2" + ] }, - "Orgc": { - "id": "277", - "name": "inthreat.com", - "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + "source": "https://github.com/mitre/cti", + "tag_id": "3008", + "tag_name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "JHUHUGIT", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "XTunnel a VPN-like network proxy tool that can relay traffic between a C2 server and a victim. It was first seen in May 2013 and reportedly used by APT28 during the compromise of the Democratic National Committee.[[Citation: Crowdstrike DNC June 2016]][[Citation: Invincea XTunnel]][[Citation: ESET Sednit Part 2]]\n\nAliases: XTunnel, X-Tunnel, XAPS", + "distribution": "0", + "galaxy_id": "365", + "id": "41543", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0117", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://www.invincea.com/2016/07/tunnel-of-gov-dnc-hack-and-the-russian-xtunnel/", + "https://www.crowdstrike.com/blog/bears-midst-intrusion-democratic-national-committee/" + ], + "synonyms": [ + "XTunnel", + "X-Tunnel", + "XAPS" + ], + "uuid": [ + "7343e208-7cab-45f2-a47b-41ba5e2f0fab" + ] }, - "analysis": "2", - "date": "2016-06-20", - "distribution": "3", - "id": "4172", - "info": "APT28 and APT29 - Inside the DNC Breaches", - "org_id": "277", - "orgc_id": "277", - "published": true, - "threat_level_id": "2", - "timestamp": "1494829231", - "uuid": "5767c102-c170-4124-ae3d-7bef95ca48b7" + "source": "https://github.com/mitre/cti", + "tag_id": "3009", + "tag_name": "misp-galaxy:mitre-malware=\"XTunnel\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "XTunnel", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "ADVSTORESHELL is a spying backdoor that has been used by APT28 from at least 2012 to 2016. It is generally used for long-term espionage and is deployed on targets deemed interesting after a reconnaissance phase.[[Citation: Kaspersky Sofacy]][[Citation: ESET Sednit Part 2]]\n\nAliases: ADVSTORESHELL, NETUI, EVILTOSS, AZZY, Sedreco", + "distribution": "0", + "galaxy_id": "365", + "id": "41582", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0045", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "ADVSTORESHELL", + "NETUI", + "EVILTOSS", + "AZZY", + "Sedreco" + ], + "uuid": [ + "fb575479-14ef-41e9-bfab-0b7cf10bec73" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3010", + "tag_name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "ADVSTORESHELL", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "USBStealer is malware that has used by APT28 since at least 2005 to extract information from air-gapped networks. It does not have the capability to communicate over the Internet and has been used in conjunction with ADVSTORESHELL.[[Citation: ESET Sednit USBStealer 2014]][[Citation: Kaspersky Sofacy]]\n\nAliases: USBStealer, USB Stealer, Win32/USBStealer", + "distribution": "0", + "galaxy_id": "365", + "id": "41549", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0136", + "http://www.welivesecurity.com/2014/11/11/sednit-espionage-group-attacking-air-gapped-networks/", + "https://securelist.com/blog/research/72924/sofacy-apt-hits-high-profile-targets-with-updated-toolset/" + ], + "synonyms": [ + "USBStealer", + "USB Stealer", + "Win32/USBStealer" + ], + "uuid": [ + "af2ad3b7-ab6a-4807-91fd-51bcaff9acbb" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3012", + "tag_name": "misp-galaxy:mitre-malware=\"USBStealer\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "USBStealer", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "is a trojan that has been used by APT28 on OS X and appears to be a port of their standard CHOPSTICK or XAgent trojan.[[Citation: XAgentOSX]]", + "distribution": "0", + "galaxy_id": "365", + "id": "41551", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0161", + "https://researchcenter.paloaltonetworks.com/2017/02/unit42-xagentosx-sofacys-xagent-macos-tool/" + ], + "uuid": [ + "5930509b-7793-4db9-bdfc-4edda7709d0d" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3013", + "tag_name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "XAgentOSX", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "CHOPSTICK is malware family of modular backdoors used by APT28. It has been used from at least November 2012 to August 2016 and is usually dropped on victims as second-stage malware, though it has been used as first-stage malware in several cases.[[Citation: FireEye APT28]][[Citation: ESET Sednit Part 2]][[Citation: FireEye APT28 January 2017]]\n\nAliases: CHOPSTICK, SPLM, Xagent, X-Agent, webhp", + "distribution": "0", + "galaxy_id": "365", + "id": "41559", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0023", + "https://www2.fireeye.com/rs/848-DID-242/images/APT28-Center-of-Storm-2017.pdf", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part-2.pdf", + "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/rpt-apt28.pdf" + ], + "synonyms": [ + "CHOPSTICK", + "SPLM", + "Xagent", + "X-Agent", + "webhp" + ], + "uuid": [ + "ccd61dfc-b03f-4689-8c18-7c97eab08472" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3014", + "tag_name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "CHOPSTICK", + "version": "4" + }, + { + "authors": [ + "MITRE" + ], + "default": false, + "description": "Downdelph is a first-stage downloader written in Delphi that has been used by APT28 in rare instances between 2013 and 2015.[[Citation: ESET Sednit Part 3]]\n\nAliases: Downdelph, Delphacy", + "distribution": "0", + "galaxy_id": "365", + "id": "41504", + "meta": { + "refs": [ + "https://attack.mitre.org/wiki/Software/S0134", + "http://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-part3.pdf" + ], + "synonyms": [ + "Downdelph", + "Delphacy" + ], + "uuid": [ + "08d20cd2-f084-45ee-8558-fa6ef5a18519" + ] + }, + "source": "https://github.com/mitre/cti", + "tag_id": "3016", + "tag_name": "misp-galaxy:mitre-malware=\"Downdelph\"", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "value": "Downdelph", + "version": "4" } - }, - { - "Event": { - "Org": { - "id": "347", - "name": "incibe.es", - "uuid": "5720623c-129c-4989-ae9d-4a11950d210f" - }, - "Orgc": { - "id": "665", - "name": "INCIBE", - "uuid": "56fa4fe4-f528-4480-8332-1ba3c0a80a8c" - }, - "analysis": "2", - "date": "2016-06-16", - "distribution": "3", - "id": "6131", - "info": "New Sofacy (APT28) attacks against a US Government Agency", - "org_id": "347", - "orgc_id": "665", - "published": true, - "threat_level_id": "1", - "timestamp": "1488792538", - "uuid": "5762a86a-e314-4e4e-ba5a-51c5c0a80a8e" + ], + "description": "Name of ATT&CK software", + "icon": "optin-monster", + "id": "365", + "name": "Malware", + "type": "mitre-malware", + "uuid": "d752161c-78f6-11e7-a0ea-bfa79b407ce4", + "version": "4" + } + ], + "Object": [ + { + "Attribute": [ + { + "Tag": [ + { + "name": "blah" + } + ], + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188944", + "object_id": "1555", + "object_relation": "filename", + "sharing_group_id": "0", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd5b6-2850-435f-bd0d-4c62950d210f", + "value": "Bulletin.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188945", + "object_id": "1555", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936310", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd5b6-78a8-4e47-8333-4c62950d210f", + "value": "68064fc152e23d56e541714af52651cb4ba81aaf" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188946", + "object_id": "1555", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936310", + "to_ids": false, + "type": "text", + "uuid": "5a3cd5b6-23d8-43ba-8518-4c62950d210f", + "value": "Malicious" } - }, - { - "Event": { - "Org": { - "id": "26", - "name": "CthulhuSPRL.be", - "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" - }, - "Orgc": { - "id": "26", - "name": "CthulhuSPRL.be", - "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" - }, - "analysis": "2", - "date": "2016-06-15", - "distribution": "3", - "id": "3987", - "info": "OSINT New Sofacy Attacks Against US Government Agency by Palo Alto Unit 42", - "org_id": "26", - "orgc_id": "26", - "published": true, - "threat_level_id": "1", - "timestamp": "1466000907", - "uuid": "57613790-f6b4-4895-943f-4467950d210f" + ], + "comment": "Win32/Sednit.AX", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1555", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "uuid": "5a3cd5b6-9568-4342-b2ab-4c62950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188947", + "object_id": "1556", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936388", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd604-748c-4fc0-88bf-c170950d210f", + "value": "f3805382ae2e23ff1147301d131a06e00e4ff75f" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188948", + "object_id": "1556", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936388", + "to_ids": false, + "type": "text", + "uuid": "5a3cd604-6668-4469-a1c0-c170950d210f", + "value": "Malicious" } - }, - { - "Event": { - "Org": { - "id": "278", - "name": "TDC.dk", - "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" - }, - "Orgc": { - "id": "325", - "name": "CUDESO", - "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" - }, - "analysis": "2", - "date": "2016-06-14", - "distribution": "3", - "id": "4183", - "info": "New Sofacy Attacks Against US Government Agency", - "org_id": "278", - "orgc_id": "325", - "published": true, - "threat_level_id": "2", - "timestamp": "1467289109", - "uuid": "57607369-2490-444a-9034-049fc0a8ab16" + ], + "comment": "Win32/Exploit.CVE-2016-4117.A", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1556", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936388", + "uuid": "5a3cd604-e11c-4de5-bbbf-c170950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188949", + "object_id": "1557", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd693-dc40-445d-a4d7-4ae0950d210f", + "value": "OC_PSO_2017.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188950", + "object_id": "1557", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd693-8ffc-4d95-b522-4e84950d210f", + "value": "512bdfe937314ac3f195c462c395feeb36932971" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188951", + "object_id": "1557", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936531", + "to_ids": false, + "type": "text", + "uuid": "5a3cd693-a8f0-4aea-a834-4097950d210f", + "value": "Malicious" } + ], + "comment": "Win32/Exploit.Agent.NUB", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1557", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936531", + "uuid": "5a3cd693-fd9c-4fcf-b69a-439c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188952", + "object_id": "1558", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd6c2-d31c-40cc-bcc1-4458950d210f", + "value": "NASAMS.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188953", + "object_id": "1558", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd6c2-6a54-4b4c-8748-4c84950d210f", + "value": "30b3e8c0f3f3cf200daa21c267ffab3cad64e68b" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188954", + "object_id": "1558", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936578", + "to_ids": false, + "type": "text", + "uuid": "5a3cd6c2-1c68-45de-8325-464a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1558", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936578", + "uuid": "5a3cd6c2-d290-4787-910f-4e6d950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188955", + "object_id": "1559", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd74e-584c-45b9-8557-486d950d210f", + "value": "Programm_Details.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188956", + "object_id": "1559", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd74e-f334-4e6b-b37f-462f950d210f", + "value": "4173b29a251cd9c1cab135f67cb60acab4ace0c5" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188957", + "object_id": "1559", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936718", + "to_ids": false, + "type": "text", + "uuid": "5a3cd74e-5900-4fbf-85c6-4c81950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1559", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936718", + "uuid": "5a3cd74e-1504-40ff-9a28-4501950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188958", + "object_id": "1560", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd775-e8f4-465a-aca2-4c5a950d210f", + "value": "Operation_in_Mosul.rtf" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188959", + "object_id": "1560", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd775-1190-4db7-961a-4c5a950d210f", + "value": "12a37cfdd3f3671074dd5b0f354269cec028fb52" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188960", + "object_id": "1560", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936757", + "to_ids": false, + "type": "text", + "uuid": "5a3cd775-fa5c-4453-bcb0-4c5a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1560", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936757", + "uuid": "5a3cd775-e4cc-44bb-89b6-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188961", + "object_id": "1561", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd82f-b918-4520-ba8b-5165950d210f", + "value": "ARM-NATO_ENGLISH_30_NOV_2016.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188962", + "object_id": "1561", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd82f-cae4-4209-9338-5165950d210f", + "value": "15201766bd964b7c405aeb11db81457220c31e46" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188963", + "object_id": "1561", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936943", + "to_ids": false, + "type": "text", + "uuid": "5a3cd82f-d91c-43af-8262-5165950d210f", + "value": "Malicious" + } + ], + "comment": "SWF/Agent.L", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1561", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936943", + "uuid": "5a3cd82f-2788-4561-bbeb-5165950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188964", + "object_id": "1562", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd847-0aa0-4b5c-aa30-5165950d210f", + "value": "Olympic-Agenda-2020-20-20-Recommendations.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188965", + "object_id": "1562", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd847-593c-4985-8756-5165950d210f", + "value": "8078e411fbe33864dfd8f87ad5105cc1fd26d62e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188966", + "object_id": "1562", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936967", + "to_ids": false, + "type": "text", + "uuid": "5a3cd847-1324-4fad-af60-5165950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.BL", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1562", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936967", + "uuid": "5a3cd847-b5a0-42f7-ac4b-5165950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188967", + "object_id": "1563", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd861-9350-40c1-ac29-4771950d210f", + "value": "Merry_Christmas!.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188968", + "object_id": "1563", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd861-18ac-4cf0-b96f-4986950d210f", + "value": "33447383379ca99083442b852589111296f0c603" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188969", + "object_id": "1563", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513936993", + "to_ids": false, + "type": "text", + "uuid": "5a3cd861-cfbc-4096-baae-40e2950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NUG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1563", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513936993", + "uuid": "5a3cd861-65c0-4b69-9429-4f37950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188970", + "object_id": "1564", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd87d-fa9c-41aa-897f-49a5950d210f", + "value": "Trump’s_Attack_on_Syria_English.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188971", + "object_id": "1564", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd87d-c630-4487-8336-4615950d210f", + "value": "d5235d136cfcadbef431eea7253d80bde414db9d" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188972", + "object_id": "1564", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937021", + "to_ids": false, + "type": "text", + "uuid": "5a3cd87d-8c98-4660-9026-44de950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NWZ", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1564", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937021", + "uuid": "5a3cd87d-f514-4071-a5f7-4ec2950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188973", + "object_id": "1565", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd897-4cc0-48b0-bb2c-461f950d210f", + "value": "Hotel_Reservation_Form.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188974", + "object_id": "1565", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd897-fa64-466c-9421-49c5950d210f", + "value": "f293a2bfb728060c54efeeb03c5323893b5c80df" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188975", + "object_id": "1565", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937047", + "to_ids": false, + "type": "text", + "uuid": "5a3cd897-f020-44cf-8dfc-4225950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1565", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937046", + "uuid": "5a3cd896-f6cc-4e52-bcb2-442c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188976", + "object_id": "1566", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937070", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd8ae-7194-48fd-810e-4c5a950d210f", + "value": "SB_Doc_2017-3_Implementation_of_Key_Taskings_and_Next_Steps.doc" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188977", + "object_id": "1566", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937071", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8af-f39c-443c-bcf1-4c5a950d210f", + "value": "bb10ed5d59672fbc6178e35d0feac0562513e9f0" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188978", + "object_id": "1566", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937071", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8af-b3ec-478a-b585-4c5a950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1566", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937070", + "uuid": "5a3cd8ae-54d0-46bb-adbb-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188979", + "object_id": "1567", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937083", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8bb-74d8-4d19-ae08-4043950d210f", + "value": "4873bafe44cff06845faa0ce7c270c4ce3c9f7b9" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188980", + "object_id": "1567", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937083", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8bb-77bc-4cc4-887f-429d950d210f", + "value": "Malicious" + } + ], + "comment": "", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1567", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937083", + "uuid": "5a3cd8bb-a704-4f1d-a235-444e950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188981", + "object_id": "1568", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937097", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8c9-4d2c-4145-a637-4f13950d210f", + "value": "169c8f3e3d22e192c108bc95164d362ce5437465" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188982", + "object_id": "1568", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937097", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8c9-7ff0-42f7-ae80-4eb6950d210f", + "value": "Malicious" + } + ], + "comment": "", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1568", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937097", + "uuid": "5a3cd8c9-6568-406a-853c-4862950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188983", + "object_id": "1569", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937116", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8dc-48c0-4ea0-a67d-4734950d210f", + "value": "cc7607015cd7a1a4452acd3d87adabdd7e005bd7" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188984", + "object_id": "1569", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937116", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8dc-9ed8-4a4d-9ceb-4daa950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1569", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937115", + "uuid": "5a3cd8db-2838-4466-a986-4afb950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188985", + "object_id": "1570", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd8fb-1efc-4059-ae7a-42f5950d210f", + "value": "Caucasian_Eagle_ENG.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188986", + "object_id": "1570", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd8fb-9cec-4a30-8b2f-4441950d210f", + "value": "5d2c7d87995cc5b8184baba2c7a1900a48b2f42d" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188987", + "object_id": "1570", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937147", + "to_ids": false, + "type": "text", + "uuid": "5a3cd8fb-e52c-489b-8da5-43d1950d210f", + "value": "Malicious" + } + ], + "comment": "Win32/Exploit.Agent.NTM", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1570", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937147", + "uuid": "5a3cd8fb-cd14-4b00-9710-430c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188988", + "object_id": "1571", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd90e-5eb4-4069-b160-5276950d210f", + "value": "World War3.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188989", + "object_id": "1571", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd90e-6d2c-4ffc-a699-5276950d210f", + "value": "7aada8bcc0d1ab8ffb1f0fae4757789c6f5546a3" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188990", + "object_id": "1571", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937166", + "to_ids": false, + "type": "text", + "uuid": "5a3cd90e-28e8-410e-8033-5276950d210f", + "value": "Malicious" + } + ], + "comment": "SWF/Exploit.CVE-2017-11292.A", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1571", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937166", + "uuid": "5a3cd90e-538c-4b7e-95dc-5276950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188991", + "object_id": "1572", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd927-e810-4d22-a0e4-4057950d210f", + "value": "SaberGuardian2017.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188992", + "object_id": "1572", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd927-f284-43b9-83d1-473b950d210f", + "value": "68c2809560c7623d2307d8797691abf3eafe319a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188993", + "object_id": "1572", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937191", + "to_ids": false, + "type": "text", + "uuid": "5a3cd927-b844-49f2-a1a9-4c85950d210f", + "value": "Malicious" + } + ], + "comment": "VBA/DDE.E", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1572", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937191", + "uuid": "5a3cd927-e410-489c-abfc-4b63950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188994", + "object_id": "1573", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": true, + "type": "filename", + "uuid": "5a3cd93c-2438-4dda-823e-463d950d210f", + "value": "IsisAttackInNewYork.docx" + }, + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188995", + "object_id": "1573", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cd93c-1ef0-4d81-9476-4655950d210f", + "value": "1c6c700ceebfbe799e115582665105caa03c5c9e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188996", + "object_id": "1573", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937212", + "to_ids": false, + "type": "text", + "uuid": "5a3cd93c-949c-40ac-9094-4a4a950d210f", + "value": "Malicious" + } + ], + "comment": "VBA/DDE.L", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1573", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937212", + "uuid": "5a3cd93c-716c-4918-a00f-4671950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188997", + "object_id": "1574", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937559", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cda97-7e58-4642-aaf5-c5ed950d210f", + "value": "6f0fc0ebba3e4c8b26a69cdf519edf8d1aa2f4bb" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1188998", + "object_id": "1574", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937559", + "to_ids": false, + "type": "text", + "uuid": "5a3cda97-6020-423d-9d23-c5ed950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "value": "movieultimate.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "159", + "object_id": "1574", + "object_uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f", + "referenced_id": "1188759", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-ab0c-4d38-8efe-459002de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513937826", + "uuid": "5a3cdba2-2fdc-4f9a-a4eb-4dae950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1574", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513937826", + "uuid": "5a3cda96-85c4-45a1-82ea-c5ed950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1188999", + "object_id": "1575", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937864", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdbc8-0aac-4d8a-8c1f-4c5a950d210f", + "value": "e19f753e514f6adec8f81bcdefb9117979e69627" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189000", + "object_id": "1575", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937864", + "to_ids": false, + "type": "text", + "uuid": "5a3cdbc8-e204-4606-b9ea-4c5a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "value": "meteost.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "160", + "object_id": "1575", + "object_uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f", + "referenced_id": "1188760", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-61dc-495c-ae8a-471e02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938091", + "uuid": "5a3cdcab-8200-4c65-868e-42a9950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1575", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938091", + "uuid": "5a3cdbc7-dbec-4b8c-8ba3-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189001", + "object_id": "1576", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937910", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdbf6-eca0-4c09-9bd0-4c59950d210f", + "value": "961468ddd3d0fa25beb8210c81ba620f9170ed30" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189002", + "object_id": "1576", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937910", + "to_ids": false, + "type": "text", + "uuid": "5a3cdbf6-acd8-4a36-a028-4c59950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "164", + "object_id": "1576", + "object_uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f", + "referenced_id": "1188761", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938210", + "uuid": "5a3cdd22-b7d8-4754-a108-4742950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1576", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938210", + "uuid": "5a3cdbf6-f814-491f-9f93-4c59950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189003", + "object_id": "1577", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937929", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc09-b428-4c0b-9969-c5ed950d210f", + "value": "a0719b50265505c8432616c0a4e14ed206981e95" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189004", + "object_id": "1577", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937929", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc09-05d8-4356-ba52-c5ed950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "value": "nethostnet.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "162", + "object_id": "1577", + "object_uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f", + "referenced_id": "1188762", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-968c-4572-9f64-491502de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938169", + "uuid": "5a3cdcf9-d5a4-4c8e-a201-45b1950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1577", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938169", + "uuid": "5a3cdc09-6fbc-4ca1-bfaa-c5ed950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189005", + "object_id": "1578", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937953", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc21-a170-4637-b139-4812950d210f", + "value": "2cf6436b99d11d9d1e0c488af518e35162ecbc9c" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189006", + "object_id": "1578", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937953", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc21-3274-4800-9e91-41e2950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "value": "faststoragefiles.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "165", + "object_id": "1578", + "object_uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f", + "referenced_id": "1188761", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-e354-4978-a6b4-49ad02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938226", + "uuid": "5a3cdd32-3044-4895-8f18-4d06950d210f" + } + ], + "comment": "Win64/Sednit.Y", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1578", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938226", + "uuid": "5a3cdc21-856c-48bd-a757-4f4b950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189007", + "object_id": "1579", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937975", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc37-cee0-43d0-9e20-4db6950d210f", + "value": "fec29b4f4dccc59770c65c128dfe4564d7c13d33" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189008", + "object_id": "1579", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937976", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc38-ac24-44be-a1ed-4935950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "value": "fsportal.net" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "163", + "object_id": "1579", + "object_uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f", + "referenced_id": "1188763", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-eb44-433f-a13a-44b902de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938189", + "uuid": "5a3cdd0d-d990-42ba-830d-5156950d210f" + } + ], + "comment": "Win64/Sednit.Y", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1579", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938190", + "uuid": "5a3cdc37-89e8-4a2d-823a-4af8950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189009", + "object_id": "1580", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513937992", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc48-c74c-4b6e-8202-5156950d210f", + "value": "57d7f3d31c491f8aef4665ca4dd905c3c8a98795" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189010", + "object_id": "1580", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513937992", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc48-55dc-420e-9b5d-5156950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "value": "fastdataexchange.org" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "161", + "object_id": "1580", + "object_uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f", + "referenced_id": "1188764", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-6a88-479d-b799-4d3d02de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938129", + "uuid": "5a3cdcd1-c6cc-43d8-a2f4-4681950d210f" + } + ], + "comment": "Win64/Sednit.Z", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1580", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938129", + "uuid": "5a3cdc48-b9a0-4775-a03f-5156950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189011", + "object_id": "1581", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513938011", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc5b-54a8-4e60-bc67-4c5a950d210f", + "value": "a3bf5b5cf5a5ef438a198a6f61f7225c0a4a7138" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189012", + "object_id": "1581", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513938011", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc5b-b390-4183-aec7-4c5a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "168", + "object_id": "1581", + "object_uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f", + "referenced_id": "1188765", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938280", + "uuid": "5a3cdd68-7968-40d1-a0a9-5156950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1581", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938280", + "uuid": "5a3cdc5a-8760-4efa-949a-4c5a950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189013", + "object_id": "1582", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513938034", + "to_ids": true, + "type": "sha1", + "uuid": "5a3cdc72-ba30-4ecd-9d21-4654950d210f", + "value": "1958e722afd0dba266576922abc98aa505cf5f9a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189014", + "object_id": "1582", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513938034", + "to_ids": false, + "type": "text", + "uuid": "5a3cdc72-0804-42c4-acfa-4ac5950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Attribute": { + "category": "Network activity", + "distribution": "5", + "sharing_group_id": "0", + "to_ids": true, + "type": "domain", + "uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "value": "newfilmts.com" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "167", + "object_id": "1582", + "object_uuid": "5a3cdc72-1538-4c66-af46-427b950d210f", + "referenced_id": "1188765", + "referenced_type": "0", + "referenced_uuid": "5a3c3045-7480-4831-a5c4-48c802de0b81", + "relationship_type": "communicates-with", + "timestamp": "1513938264", + "uuid": "5a3cdd58-9800-4bae-837c-4f20950d210f" + } + ], + "comment": "Win32/Sednit.BO", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1582", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513938264", + "uuid": "5a3cdc72-1538-4c66-af46-427b950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189015", + "object_id": "1583", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939882", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3aa-e104-481e-a7f4-4bc1950d210f", + "value": "9f6bed7d7f4728490117cbc85819c2e6c494251b" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189016", + "object_id": "1583", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939882", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3aa-74fc-48c7-af40-4c6a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "173", + "object_id": "1583", + "object_uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f", + "referenced_id": "1592", + "referenced_type": "1", + "referenced_uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513947459", + "uuid": "5a3d0143-c300-4118-8afe-4a2d950d210f" + } + ], + "comment": "Win32/Sednit.AX", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1583", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948642", + "uuid": "5a3ce3a9-f070-4403-a1f6-4b8c950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189017", + "object_id": "1584", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939907", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3c3-6d9c-48f4-93db-4a61950d210f", + "value": "4bc722a9b0492a50bd86a1341f02c74c0d773db7" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189018", + "object_id": "1584", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939907", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3c3-c38c-4e30-a904-4c8f950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "188", + "object_id": "1584", + "object_uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f", + "referenced_id": "1603", + "referenced_type": "1", + "referenced_uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948518", + "uuid": "5a3d0566-34fc-4a62-b2a5-4f91950d210f" + } + ], + "comment": "Win32/Sednit.BS", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1584", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948535", + "uuid": "5a3ce3c3-34b4-4e1f-b238-4399950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189019", + "object_id": "1585", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939924", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3d4-9168-4e23-8b64-485a950d210f", + "value": "ab354807e687993fbeb1b325eb6e4ab38d428a1e" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189020", + "object_id": "1585", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939924", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3d4-27e0-4366-943f-4b9a950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "189", + "object_id": "1585", + "object_uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f", + "referenced_id": "1602", + "referenced_type": "1", + "referenced_uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948528", + "uuid": "5a3d0570-a86c-4264-a43a-4125950d210f" + } + ], + "comment": "Win32/Sednit.BS", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1585", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948597", + "uuid": "5a3ce3d4-07bc-4af3-90fc-4798950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189021", + "object_id": "1586", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939946", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce3ea-8dbc-4cf4-997f-448b950d210f", + "value": "9c47ca3883196b3a84d67676a804ff50e22b0a9f" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189022", + "object_id": "1586", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939946", + "to_ids": false, + "type": "text", + "uuid": "5a3ce3ea-e714-444e-ad9b-40b0950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "190", + "object_id": "1586", + "object_uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f", + "referenced_id": "1601", + "referenced_type": "1", + "referenced_uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948614", + "uuid": "5a3d05c6-0618-4520-9549-48a0950d210f" + } + ], + "comment": "Win32/Sednit.BR", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1586", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948626", + "uuid": "5a3ce3ea-580c-477c-9b73-4e57950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189023", + "object_id": "1587", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939972", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce404-7bfc-4316-bd32-55ea950d210f", + "value": "8a68f26d01372114f660e32ac4c9117e5d0577f1" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189024", + "object_id": "1587", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939972", + "to_ids": false, + "type": "text", + "uuid": "5a3ce404-7224-4525-922a-55ea950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "182", + "object_id": "1587", + "object_uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f", + "referenced_id": "1600", + "referenced_type": "1", + "referenced_uuid": "5a3ce680-90d4-478d-95db-48a6950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948044", + "uuid": "5a3d038c-1cc8-4d9c-87ab-c5ed950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1587", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948073", + "uuid": "5a3ce404-efc0-4f15-864e-55ea950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189025", + "object_id": "1588", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513939991", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce417-62a4-4d46-9a87-55ea950d210f", + "value": "476fc1d31722ac26b46154cbf0c631d60268b28a" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189026", + "object_id": "1588", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513939991", + "to_ids": false, + "type": "text", + "uuid": "5a3ce417-43f0-494d-ac2e-55ea950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "187", + "object_id": "1588", + "object_uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f", + "referenced_id": "1599", + "referenced_type": "1", + "referenced_uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948483", + "uuid": "5a3d0543-8f74-4086-aafc-418a950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1588", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948498", + "uuid": "5a3ce417-7cd4-4c36-8a73-55ea950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189027", + "object_id": "1589", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940012", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce42c-836c-49e7-a9f3-4a5f950d210f", + "value": "f9fd3f1d8da4ffd6a494228b934549d09e3c59d1" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189028", + "object_id": "1589", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940012", + "to_ids": false, + "type": "text", + "uuid": "5a3ce42c-4c88-4940-94b8-4084950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "183", + "object_id": "1589", + "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", + "referenced_id": "1594", + "referenced_type": "1", + "referenced_uuid": "5a3ce60a-6db8-4212-b194-4339950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948106", + "uuid": "5a3d03ca-2398-4060-b13c-404a950d210f" + }, + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "184", + "object_id": "1589", + "object_uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f", + "referenced_id": "1595", + "referenced_type": "1", + "referenced_uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948117", + "uuid": "5a3d03d5-6d8c-4dfb-b193-4002950d210f" + } + ], + "comment": "Win32/Sednit.BN", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1589", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948128", + "uuid": "5a3ce42b-2e0c-4a26-b6c8-47a3950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189029", + "object_id": "1590", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940027", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce43b-6738-4a14-a318-4d65950d210f", + "value": "e338d49c270baf64363879e5eecb8fa6bdde8ad9" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189030", + "object_id": "1590", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940027", + "to_ids": false, + "type": "text", + "uuid": "5a3ce43b-3a10-4d78-9ee2-485c950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "186", + "object_id": "1590", + "object_uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f", + "referenced_id": "1593", + "referenced_type": "1", + "referenced_uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513948320", + "uuid": "5a3d04a0-9d28-47c3-a12c-465b950d210f" + } + ], + "comment": "Win32/Sednit.BG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1590", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513948339", + "uuid": "5a3ce43a-5478-4f65-95b2-4e1e950d210f" + }, + { + "Attribute": [ + { + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189031", + "object_id": "1591", + "object_relation": "sha1", + "sharing_group_id": "0", + "timestamp": "1513940042", + "to_ids": true, + "type": "sha1", + "uuid": "5a3ce44a-2ea4-4526-8bbc-c328950d210f", + "value": "6e167da3c5d887fa2e58da848a2245d11b6c5ad6" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "9747", + "id": "1189032", + "object_id": "1591", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1513940042", + "to_ids": false, + "type": "text", + "uuid": "5a3ce44a-5118-4142-97f0-c328950d210f", + "value": "Malicious" + } + ], + "ObjectReference": [ + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "170", + "object_id": "1591", + "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", + "referenced_id": "1597", + "referenced_type": "1", + "referenced_uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513940734", + "uuid": "5a3ce6fe-b0c4-44df-a609-419a950d210f" + }, + { + "Object": { + "distribution": "5", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" + }, + "comment": "", + "deleted": false, + "event_id": "9747", + "id": "171", + "object_id": "1591", + "object_uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f", + "referenced_id": "1598", + "referenced_type": "1", + "referenced_uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f", + "relationship_type": "communicates-with", + "timestamp": "1513940753", + "uuid": "5a3ce711-a0dc-4dbe-b59e-495a950d210f" + } + ], + "comment": "Win32/Sednit.BG", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "9747", + "id": "1591", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1513940753", + "uuid": "5a3ce44a-ce70-42b7-80b8-c328950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189033", + "object_id": "1592", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940362", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce58a-fcd8-48d5-8b4a-4fd9950d210f", + "value": "87.236.211.182" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189034", + "object_id": "1592", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940362", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce58a-6e14-48ea-9746-48f2950d210f", + "value": "servicecdp.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1592", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940362", + "uuid": "5a3ce58a-3198-4cb8-9d51-44e5950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189035", + "object_id": "1593", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940472", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce5f8-99b4-41a2-915a-4bf8950d210f", + "value": "95.215.45.43" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189036", + "object_id": "1593", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940472", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce5f8-62c8-4f04-89c2-4aeb950d210f", + "value": "wmdmediacodecs.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1593", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940472", + "uuid": "5a3ce5f8-3418-4f7b-ae41-4bca950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189037", + "object_id": "1594", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940490", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce60a-cc50-4553-bfff-4ea9950d210f", + "value": "89.45.67.144" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189038", + "object_id": "1594", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940491", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce60b-e648-4667-8432-4ba8950d210f", + "value": "mvband.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1594", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940490", + "uuid": "5a3ce60a-6db8-4212-b194-4339950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189039", + "object_id": "1595", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940506", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce61a-4458-4c36-866e-44e9950d210f", + "value": "89.33.246.117" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189040", + "object_id": "1595", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940506", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce61a-f820-4a43-b3d9-47e5950d210f", + "value": "mvtband.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1595", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940506", + "uuid": "5a3ce61a-c1f0-4c7c-b815-4fa9950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189041", + "object_id": "1596", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940542", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce63e-66d4-483f-bae6-44f6950d210f", + "value": "87.236.211.182" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189042", + "object_id": "1596", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940542", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce63e-0d88-405b-82a9-43b5950d210f", + "value": "servicecdp.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1596", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940542", + "uuid": "5a3ce63e-0240-46f5-b9ed-4759950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189043", + "object_id": "1597", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940558", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce64e-d7a8-4817-a132-4c72950d210f", + "value": "185.156.173.70" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189044", + "object_id": "1597", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940558", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce64e-243c-4931-b733-403c950d210f", + "value": "runvercheck.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1597", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940558", + "uuid": "5a3ce64e-8bf8-4dc6-be49-437f950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189045", + "object_id": "1598", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940572", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce65c-bf78-4b78-bafd-4cf6950d210f", + "value": "191.101.31.96" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189046", + "object_id": "1598", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940572", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce65c-8140-4146-a927-45e4950d210f", + "value": "remsupport.org" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1598", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940572", + "uuid": "5a3ce65c-fc40-4585-817e-4ca3950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189047", + "object_id": "1599", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940591", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce66f-150c-43ec-a3ff-4aa5950d210f", + "value": "89.187.150.44" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189048", + "object_id": "1599", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940591", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce66f-466c-478e-8064-4b42950d210f", + "value": "viters.org" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1599", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940590", + "uuid": "5a3ce66e-70b4-47e7-b965-46f6950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189049", + "object_id": "1600", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940608", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce680-7b04-466d-b187-4301950d210f", + "value": "146.185.253.132" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189050", + "object_id": "1600", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940608", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce680-12f4-4001-9f86-4aa4950d210f", + "value": "myinvestgroup.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1600", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940608", + "uuid": "5a3ce680-90d4-478d-95db-48a6950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189051", + "object_id": "1601", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940621", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce68d-0108-4557-8921-4377950d210f", + "value": "86.106.131.141" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189052", + "object_id": "1601", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940622", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce68e-54d0-4c67-8c4c-4dea950d210f", + "value": "space-delivery.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1601", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940621", + "uuid": "5a3ce68d-1940-4ea6-becd-44fe950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189054", + "object_id": "1602", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940642", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce6a2-4a38-4b90-8d74-4f10950d210f", + "value": "89.34.111.160" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189055", + "object_id": "1602", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940642", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce6a2-ffa4-4afb-89ab-42a6950d210f", + "value": "satellitedeluxpanorama.com" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1602", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940641", + "uuid": "5a3ce6a1-3f1c-4d5d-bac7-406d950d210f" + }, + { + "Attribute": [ + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189056", + "object_id": "1603", + "object_relation": "ip", + "sharing_group_id": "0", + "timestamp": "1513940654", + "to_ids": true, + "type": "ip-dst", + "uuid": "5a3ce6ae-601c-44b8-8eec-4a5f950d210f", + "value": "185.216.35.26" + }, + { + "category": "Network activity", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "9747", + "id": "1189057", + "object_id": "1603", + "object_relation": "domain", + "sharing_group_id": "0", + "timestamp": "1513940654", + "to_ids": true, + "type": "domain", + "uuid": "5a3ce6ae-3b00-420a-82fd-45fb950d210f", + "value": "webviewres.net" + } + ], + "comment": "", + "deleted": false, + "description": "A domain and IP address seen as a tuple in a specific time frame.", + "distribution": "5", + "event_id": "9747", + "id": "1603", + "meta-category": "network", + "name": "domain-ip", + "sharing_group_id": "0", + "template_uuid": "43b3b146-77eb-4931-b4cc-b66c60f28734", + "template_version": "5", + "timestamp": "1513940654", + "uuid": "5a3ce6ae-98d8-4270-b88f-47f2950d210f" + } + ], + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "RelatedEvent": [ + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-12-14", + "distribution": "3", + "id": "9616", + "info": "OSINT - Attackers Deploy New ICS Attack Framework \"TRITON\" and Cause Operational Disruption to Critical Infrastructure", + "org_id": "2", + "orgc_id": "2", + "published": false, + "threat_level_id": "3", + "timestamp": "1513674510", + "uuid": "5a329d19-03e0-4eaa-8b4d-4310950d210f" } - ], - "Tag": [ - { - "colour": "#00d622", - "exportable": true, - "hide_tag": false, - "id": "2", - "name": "tlp:white", - "user_id": "0" - }, - { - "colour": "#ef0081", - "exportable": true, - "hide_tag": false, - "id": "2986", - "name": "workflow:state=\"incomplete\"", - "user_id": "0" - }, - { - "colour": "#810046", - "exportable": true, - "hide_tag": false, - "id": "2979", - "name": "workflow:todo=\"create-missing-misp-galaxy-cluster-values\"", - "user_id": "0" - }, - { - "colour": "#91004e", - "exportable": true, - "hide_tag": false, - "id": "2980", - "name": "workflow:todo=\"create-missing-misp-galaxy-cluster\"", - "user_id": "0" - }, - { - "colour": "#12e000", - "exportable": true, - "hide_tag": false, - "id": "1100", - "name": "misp-galaxy:threat-actor=\"Sofacy\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3007", - "name": "misp-galaxy:exploit-kit=\"Sednit EK\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "2215", - "name": "misp-galaxy:tool=\"GAMEFISH\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3008", - "name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", - "user_id": "0" - }, - { - "colour": "#0c9900", - "exportable": true, - "hide_tag": false, - "id": "1012", - "name": "misp-galaxy:tool=\"X-Tunnel\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3009", - "name": "misp-galaxy:mitre-malware=\"XTunnel\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3010", - "name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3011", - "name": "misp-galaxy:tool=\"EVILTOSS\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3012", - "name": "misp-galaxy:mitre-malware=\"USBStealer\"", - "user_id": "0" - }, - { - "colour": "#0c9800", - "exportable": true, - "hide_tag": false, - "id": "1011", - "name": "misp-galaxy:tool=\"X-Agent\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3013", - "name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3014", - "name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3015", - "name": "misp-galaxy:exploit-kit=\"DealersChoice\"", - "user_id": "0" - }, - { - "colour": "#0088cc", - "exportable": true, - "hide_tag": false, - "id": "3016", - "name": "misp-galaxy:mitre-malware=\"Downdelph\"", - "user_id": "0" + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-12-07", + "distribution": "3", + "id": "9552", + "info": "OSINT - Master Channel: The Boleto Mestre Campaign Targets Brazil", + "org_id": "2", + "orgc_id": "2", + "published": false, + "threat_level_id": "3", + "timestamp": "1512657975", + "uuid": "5a2943a3-c574-44bb-8e68-45de950d210f" } - ], - "analysis": "0", - "attribute_count": "122", - "date": "2017-12-21", - "disable_correlation": false, - "distribution": "3", - "event_creator_email": "alexandre.dulaunoy@circl.lu", - "id": "9747", - "info": "OSINT - Sednit update: How Fancy Bear Spent the Year", - "locked": false, - "org_id": "2", - "orgc_id": "2", - "proposal_email_lock": false, - "publish_timestamp": "0", - "published": false, - "sharing_group_id": "0", - "threat_level_id": "3", - "uuid": "5a3c2fcd-8328-42bb-a95e-4f4402de0b81" - } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "0", + "date": "2017-11-27", + "distribution": "3", + "id": "9513", + "info": "OSINT - Tizi: Detecting and blocking socially engineered spyware on Android", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1512356440", + "uuid": "5a23a972-e6a0-4a05-b505-4e8f02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-11-07", + "distribution": "3", + "id": "9309", + "info": "OSINT - Threat Group APT28 Slips Office Malware into Doc Citing NYC Terror Attack", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1511385862", + "uuid": "5a021bc2-8e0c-4ac5-b048-cc3e02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "Orgc": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "analysis": "2", + "date": "2017-10-23", + "distribution": "3", + "id": "9208", + "info": "Talos: \"Cyber Conflict\" Decoy Document Used In Real Cyber Conflict", + "org_id": "291", + "orgc_id": "291", + "published": true, + "threat_level_id": "2", + "timestamp": "1510088616", + "uuid": "59ed9c81-6484-47a9-aab4-191d0a950b0c" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-08-11", + "distribution": "3", + "id": "8798", + "info": "OSINT - APT28 Targets Hospitality Sector, Presents Threat to Travelers", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1502460096", + "uuid": "598db7fd-47a8-45f8-9414-408b02de0b81" + } + }, + { + "Event": { + "Org": { + "id": "231", + "name": "kingfisherops.com", + "uuid": "566ff5f4-7020-4089-9003-4374950d210f" + }, + "Orgc": { + "id": "204", + "name": "CERT-BUND", + "uuid": "56a64d7a-63dc-4471-bce9-4accc25ed029" + }, + "analysis": "0", + "date": "2017-07-25", + "distribution": "3", + "id": "8750", + "info": "European Defence Agency lure drops mssuppa.dat", + "org_id": "231", + "orgc_id": "204", + "published": true, + "threat_level_id": "2", + "timestamp": "1500967989", + "uuid": "5976f294-a844-44fe-a4f0-6c67c25ed029" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2017-05-11", + "distribution": "3", + "id": "7820", + "info": "APT28-Sednit adds two zero-day exploits using ‘Trump’s attack on Syria’ as a decoy", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494824291", + "uuid": "59147a22-3100-4779-9377-360395ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "date": "2017-05-09", + "distribution": "3", + "id": "7801", + "info": "OSINT - EPS Processing Zero-Days Exploited by Multiple Threat Actors", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1494354378", + "uuid": "59120865-27e0-4e6d-9b74-4a9f950d210f" + } + }, + { + "Event": { + "Org": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "2", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "0", + "date": "2016-12-29", + "distribution": "3", + "id": "5667", + "info": "OSINT - GRIZZLY STEPPE – Russian Malicious Cyber Activity", + "org_id": "2", + "orgc_id": "2", + "published": true, + "threat_level_id": "3", + "timestamp": "1494853878", + "uuid": "58658c15-54ac-43c3-9beb-414502de0b81" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2016-12-20", + "distribution": "1", + "id": "5616", + "info": "APT28-The Sofacy Group's DealersChoice Attacks Continue", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494829249", + "uuid": "58594faf-e98c-4c03-a58c-43cf95ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "Orgc": { + "id": "291", + "name": "NCSC-NL", + "uuid": "5697b0c4-9474-4336-b675-28140a950b0b" + }, + "analysis": "1", + "date": "2016-11-09", + "distribution": "3", + "id": "5348", + "info": "[APT-28/Sofacy]Pawn Storm Ramps Up [European Government] Spear-phishing Before Zero-Days Get Patched", + "org_id": "291", + "orgc_id": "291", + "published": true, + "threat_level_id": "1", + "timestamp": "1481709638", + "uuid": "582341ff-0830-4b32-aaba-08640a950b0c" + } + }, + { + "Event": { + "Org": { + "id": "74", + "name": "PwC.lu", + "uuid": "55f6ea61-4f74-40b6-a6df-4ff9950d210f" + }, + "Orgc": { + "id": "325", + "name": "CUDESO", + "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" + }, + "analysis": "2", + "date": "2016-11-09", + "distribution": "3", + "id": "5641", + "info": "Pawn Storm Ramps Up Spear-phishing Before Zero-Days Get Patched", + "org_id": "74", + "orgc_id": "325", + "published": true, + "threat_level_id": "2", + "timestamp": "1478712711", + "uuid": "58235d0e-34d4-41c1-9a2e-04dcc0a8ab16" + } + }, + { + "Event": { + "Org": { + "id": "335", + "name": "Orange CERT-CC", + "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" + }, + "Orgc": { + "id": "335", + "name": "Orange CERT-CC", + "uuid": "5707ccb5-e330-4e25-a193-41d4950d210f" + }, + "analysis": "0", + "date": "2016-10-18", + "distribution": "0", + "id": "5163", + "info": "Orange-CERT-CC Test #01", + "org_id": "335", + "orgc_id": "335", + "published": false, + "threat_level_id": "3", + "timestamp": "1476782422", + "uuid": "5805e8a5-611c-498b-839b-bd57950d210f" + } + }, + { + "Event": { + "Org": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "Orgc": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "analysis": "2", + "date": "2016-10-17", + "distribution": "3", + "id": "5165", + "info": "OSINT: ‘DealersChoice’ is Sofacy’s Flash Player Exploit Platform", + "org_id": "278", + "orgc_id": "278", + "published": true, + "threat_level_id": "1", + "timestamp": "1476789563", + "uuid": "580602f6-f8b8-4ac3-9813-7bf7bce2ab96" + } + }, + { + "Event": { + "Org": { + "id": "412", + "name": "TS", + "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + }, + "Orgc": { + "id": "412", + "name": "TS", + "uuid": "57470e61-3384-491d-a56f-1bb75b86d7e5" + }, + "analysis": "2", + "date": "2016-08-19", + "distribution": "1", + "id": "4710", + "info": "bullettin.doc sample, linked to APT28 campaign", + "org_id": "412", + "orgc_id": "412", + "published": true, + "threat_level_id": "1", + "timestamp": "1476776982", + "uuid": "57b7248f-283c-442e-8e02-2d0f5b86d7e5" + } + }, + { + "Event": { + "Org": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "Orgc": { + "id": "277", + "name": "inthreat.com", + "uuid": "5697b91d-2090-441f-b153-75e895ca48b7" + }, + "analysis": "2", + "date": "2016-06-20", + "distribution": "3", + "id": "4172", + "info": "APT28 and APT29 - Inside the DNC Breaches", + "org_id": "277", + "orgc_id": "277", + "published": true, + "threat_level_id": "2", + "timestamp": "1494829231", + "uuid": "5767c102-c170-4124-ae3d-7bef95ca48b7" + } + }, + { + "Event": { + "Org": { + "id": "347", + "name": "incibe.es", + "uuid": "5720623c-129c-4989-ae9d-4a11950d210f" + }, + "Orgc": { + "id": "665", + "name": "INCIBE", + "uuid": "56fa4fe4-f528-4480-8332-1ba3c0a80a8c" + }, + "analysis": "2", + "date": "2016-06-16", + "distribution": "3", + "id": "6131", + "info": "New Sofacy (APT28) attacks against a US Government Agency", + "org_id": "347", + "orgc_id": "665", + "published": true, + "threat_level_id": "1", + "timestamp": "1488792538", + "uuid": "5762a86a-e314-4e4e-ba5a-51c5c0a80a8e" + } + }, + { + "Event": { + "Org": { + "id": "26", + "name": "CthulhuSPRL.be", + "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" + }, + "Orgc": { + "id": "26", + "name": "CthulhuSPRL.be", + "uuid": "55f6ea5f-fd34-43b8-ac1d-40cb950d210f" + }, + "analysis": "2", + "date": "2016-06-15", + "distribution": "3", + "id": "3987", + "info": "OSINT New Sofacy Attacks Against US Government Agency by Palo Alto Unit 42", + "org_id": "26", + "orgc_id": "26", + "published": true, + "threat_level_id": "1", + "timestamp": "1466000907", + "uuid": "57613790-f6b4-4895-943f-4467950d210f" + } + }, + { + "Event": { + "Org": { + "id": "278", + "name": "TDC.dk", + "uuid": "56a5d575-2ff4-4738-a2ee-59be950d210f" + }, + "Orgc": { + "id": "325", + "name": "CUDESO", + "uuid": "56c42374-fdb8-4544-a218-41ffc0a8ab16" + }, + "analysis": "2", + "date": "2016-06-14", + "distribution": "3", + "id": "4183", + "info": "New Sofacy Attacks Against US Government Agency", + "org_id": "278", + "orgc_id": "325", + "published": true, + "threat_level_id": "2", + "timestamp": "1467289109", + "uuid": "57607369-2490-444a-9034-049fc0a8ab16" + } + } + ], + "Tag": [ + { + "colour": "#00d622", + "exportable": true, + "hide_tag": false, + "id": "2", + "name": "tlp:white", + "user_id": "0" + }, + { + "colour": "#ef0081", + "exportable": true, + "hide_tag": false, + "id": "2986", + "name": "workflow:state=\"incomplete\"", + "user_id": "0" + }, + { + "colour": "#810046", + "exportable": true, + "hide_tag": false, + "id": "2979", + "name": "workflow:todo=\"create-missing-misp-galaxy-cluster-values\"", + "user_id": "0" + }, + { + "colour": "#91004e", + "exportable": true, + "hide_tag": false, + "id": "2980", + "name": "workflow:todo=\"create-missing-misp-galaxy-cluster\"", + "user_id": "0" + }, + { + "colour": "#12e000", + "exportable": true, + "hide_tag": false, + "id": "1100", + "name": "misp-galaxy:threat-actor=\"Sofacy\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3007", + "name": "misp-galaxy:exploit-kit=\"Sednit EK\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "2215", + "name": "misp-galaxy:tool=\"GAMEFISH\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3008", + "name": "misp-galaxy:mitre-malware=\"JHUHUGIT\"", + "user_id": "0" + }, + { + "colour": "#0c9900", + "exportable": true, + "hide_tag": false, + "id": "1012", + "name": "misp-galaxy:tool=\"X-Tunnel\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3009", + "name": "misp-galaxy:mitre-malware=\"XTunnel\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3010", + "name": "misp-galaxy:mitre-malware=\"ADVSTORESHELL\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3011", + "name": "misp-galaxy:tool=\"EVILTOSS\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3012", + "name": "misp-galaxy:mitre-malware=\"USBStealer\"", + "user_id": "0" + }, + { + "colour": "#0c9800", + "exportable": true, + "hide_tag": false, + "id": "1011", + "name": "misp-galaxy:tool=\"X-Agent\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3013", + "name": "misp-galaxy:mitre-malware=\"XAgentOSX\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3014", + "name": "misp-galaxy:mitre-malware=\"CHOPSTICK\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3015", + "name": "misp-galaxy:exploit-kit=\"DealersChoice\"", + "user_id": "0" + }, + { + "colour": "#0088cc", + "exportable": true, + "hide_tag": false, + "id": "3016", + "name": "misp-galaxy:mitre-malware=\"Downdelph\"", + "user_id": "0" + } + ], + "analysis": "0", + "attribute_count": "122", + "date": "2017-12-21", + "disable_correlation": false, + "distribution": "3", + "event_creator_email": "alexandre.dulaunoy@circl.lu", + "id": "9747", + "info": "OSINT - Sednit update: How Fancy Bear Spent the Year", + "locked": false, + "org_id": "2", + "orgc_id": "2", + "proposal_email_lock": false, + "publish_timestamp": "0", + "published": false, + "sharing_group_id": "0", + "threat_level_id": "3", + "uuid": "5a3c2fcd-8328-42bb-a95e-4f4402de0b81" } diff --git a/tests/mispevent_testfiles/galaxy.json b/tests/mispevent_testfiles/galaxy.json new file mode 100644 index 0000000..a2d9107 --- /dev/null +++ b/tests/mispevent_testfiles/galaxy.json @@ -0,0 +1,25 @@ +{ + "uuid": "c5f2dfb4-21a1-42d8-a452-1d3c36a204ff", + "name": "Tea Matrix", + "type": "tea-matrix", + "description": "Tea Matrix", + "namespace": "tea-matrix", + "GalaxyCluster": [ + { + "collection_uuid": "7eacd736-b093-4cc0-a56c-5f84de725dfb", + "type": "tea-matrix", + "value": "Milk in tea", + "tag_name": "misp-galaxy:tea-matrix=\"Milk in tea\"", + "description": "Milk in tea", + "uuid": "24430dc6-9c27-4b3c-a5e7-6dda478fffa0", + "distribution": "3", + "default": true, + "meta": { + "kill_chain": [ + "tea:black" + ] + }, + "relationship_type": "ennemy-of" + } + ] +} diff --git a/tests/mispevent_testfiles/malware.json b/tests/mispevent_testfiles/malware.json index b858760..8e3a423 100644 --- a/tests/mispevent_testfiles/malware.json +++ b/tests/mispevent_testfiles/malware.json @@ -1,21 +1,19 @@ { - "Event": { - "Attribute": [ - { - "category": "Payload delivery", - "data": "ewogICJFdmVudCI6IHsKICB9Cn0K", - "disable_correlation": false, - "encrypt": true, - "malware_filename": "bar.exe", - "to_ids": true, - "type": "malware-sample", - "value": "bar.exe" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Attribute": [ + { + "category": "Payload delivery", + "data": "ewp9Cg==", + "disable_correlation": false, + "encrypt": true, + "malware_filename": "bar.exe", + "to_ids": true, + "type": "malware-sample", + "value": "bar.exe" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } diff --git a/tests/mispevent_testfiles/malware_exist.json b/tests/mispevent_testfiles/malware_exist.json index 7ea80cc..013304c 100644 --- a/tests/mispevent_testfiles/malware_exist.json +++ b/tests/mispevent_testfiles/malware_exist.json @@ -1,165 +1,163 @@ {"response":[{ - "Event": { - "id": "6719", - "orgc_id": "1", - "org_id": "1", - "date": "2018-01-04", - "threat_level_id": "1", - "info": "Test existing malware PyMISP", - "published": false, - "uuid": "5a4e4fdd-1eb4-4ff3-9e87-43fa950d210f", - "attribute_count": "6", - "analysis": "0", - "timestamp": "1515081727", - "distribution": "0", - "proposal_email_lock": false, - "locked": false, - "publish_timestamp": "0", - "sharing_group_id": "0", - "disable_correlation": false, - "event_creator_email": "raphael.vinot@circl.lu", - "Org": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Attribute": [], - "ShadowAttribute": [], - "RelatedEvent": [], - "Galaxy": [], - "Object": [ - { - "id": "2279", - "name": "file", - "meta-category": "file", - "description": "File object describing a file with meta-information", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "6", - "event_id": "6719", - "uuid": "5a4e4ffe-4cb8-48b1-bd5c-48fb950d210f", - "timestamp": "1515081726", - "distribution": "5", - "sharing_group_id": "0", - "comment": "", - "deleted": false, - "ObjectReference": [], - "Attribute": [ - { - "id": "814967", - "type": "malware-sample", - "category": "Payload delivery", - "to_ids": true, - "uuid": "5a4e4fff-407c-40ff-9de5-43dc950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": false, - "object_id": "2279", - "object_relation": "malware-sample", - "value": "simple.json|7637beddacbeac59d44469b2b120b9e6", - "data": "UEsDBAoACQAAAEOAJEyjHboUIQAAABUAAAAgABwANzYzN2JlZGRhY2JlYWM1OWQ0NDQ2OWIyYjEyMGI5ZTZVVAkAA\/5PTlr+T05adXgLAAEEIQAAAAQhAAAATvzonhGOj12MyB1QeGLJ5iZhOjD+zymV4FU2+kjD4oTYUEsHCKMduhQhAAAAFQAAAFBLAwQKAAkAAABDgCRMg45UABcAAAALAAAALQAcADc2MzdiZWRkYWNiZWFjNTlkNDQ0NjliMmIxMjBiOWU2LmZpbGVuYW1lLnR4dFVUCQAD\/k9OWv5PTlp1eAsAAQQhAAAABCEAAADDgZOh6307Bduy829xtRjpivO\/xFI3KVBLBwiDjlQAFwAAAAsAAABQSwECHgMKAAkAAABDgCRMox26FCEAAAAVAAAAIAAYAAAAAAABAAAApIEAAAAANzYzN2JlZGRhY2JlYWM1OWQ0NDQ2OWIyYjEyMGI5ZTZVVAUAA\/5PTlp1eAsAAQQhAAAABCEAAABQSwECHgMKAAkAAABDgCRMg45UABcAAAALAAAALQAYAAAAAAABAAAApIGLAAAANzYzN2JlZGRhY2JlYWM1OWQ0NDQ2OWIyYjEyMGI5ZTYuZmlsZW5hbWUudHh0VVQFAAP+T05adXgLAAEEIQAAAAQhAAAAUEsFBgAAAAACAAIA2QAAABkBAAAAAA==", - "ShadowAttribute": [] - }, - { - "id": "814968", - "type": "filename", - "category": "Payload delivery", - "to_ids": false, - "uuid": "5a4e4fff-9ec0-4822-a405-4e29950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": false, - "object_id": "2279", - "object_relation": "filename", - "value": "simple.json", - "ShadowAttribute": [] - }, - { - "id": "814969", - "type": "md5", - "category": "Payload delivery", - "to_ids": true, - "uuid": "5a4e4fff-8000-49f9-8c3e-4598950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": false, - "object_id": "2279", - "object_relation": "md5", - "value": "7637beddacbeac59d44469b2b120b9e6", - "ShadowAttribute": [] - }, - { - "id": "814970", - "type": "sha1", - "category": "Payload delivery", - "to_ids": true, - "uuid": "5a4e4fff-dae0-4aa4-81ea-4899950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": false, - "object_id": "2279", - "object_relation": "sha1", - "value": "023853a4331db8d67e44553004cf338ec1b7440e", - "ShadowAttribute": [] - }, - { - "id": "814971", - "type": "sha256", - "category": "Payload delivery", - "to_ids": true, - "uuid": "5a4e4fff-03ec-4e88-b5f4-472b950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": false, - "object_id": "2279", - "object_relation": "sha256", - "value": "6ae8b0f1c7d6f3238d1fc14038018c3b4704c8cc23dac1c2bfd2c81b5a278eef", - "ShadowAttribute": [] - }, - { - "id": "814972", - "type": "size-in-bytes", - "category": "Other", - "to_ids": false, - "uuid": "5a4e4fff-b6f4-41ba-a6eb-446c950d210f", - "event_id": "6719", - "distribution": "5", - "timestamp": "1515081727", - "comment": "", - "sharing_group_id": "0", - "deleted": false, - "disable_correlation": true, - "object_id": "2279", - "object_relation": "size-in-bytes", - "value": "21", - "ShadowAttribute": [] - } - ] - } - ] - } + "id": "6719", + "orgc_id": "1", + "org_id": "1", + "date": "2018-01-04", + "threat_level_id": "1", + "info": "Test existing malware PyMISP", + "published": false, + "uuid": "5a4e4fdd-1eb4-4ff3-9e87-43fa950d210f", + "attribute_count": "6", + "analysis": "0", + "timestamp": "1515081727", + "distribution": "0", + "proposal_email_lock": false, + "locked": false, + "publish_timestamp": "0", + "sharing_group_id": "0", + "disable_correlation": false, + "event_creator_email": "raphael.vinot@circl.lu", + "Org": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Attribute": [], + "ShadowAttribute": [], + "RelatedEvent": [], + "Galaxy": [], + "Object": [ + { + "id": "2279", + "name": "file", + "meta-category": "file", + "description": "File object describing a file with meta-information", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "6", + "event_id": "6719", + "uuid": "5a4e4ffe-4cb8-48b1-bd5c-48fb950d210f", + "timestamp": "1515081726", + "distribution": "5", + "sharing_group_id": "0", + "comment": "", + "deleted": false, + "ObjectReference": [], + "Attribute": [ + { + "id": "814967", + "type": "malware-sample", + "category": "Payload delivery", + "to_ids": true, + "uuid": "5a4e4fff-407c-40ff-9de5-43dc950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": false, + "object_id": "2279", + "object_relation": "malware-sample", + "value": "simple.json|7637beddacbeac59d44469b2b120b9e6", + "data": "UEsDBAoACQAAAFYGA0/yk6nqEAAAAAQAAAAgABwANWI3NmIwZWVmOWFmOGEyMzAwNjczZTA1NTNmNjA5ZjlVVAkAA0O+RF1DvkRddXgLAAEEIQAAAAQhAAAAKjTWvfZCN4PlYePhL4s52lBLBwjyk6nqEAAAAAQAAABQSwMECgAJAAAAVgYDT4OOVAAXAAAACwAAAC0AHAA1Yjc2YjBlZWY5YWY4YTIzMDA2NzNlMDU1M2Y2MDlmOS5maWxlbmFtZS50eHRVVAkAA0O+RF1DvkRddXgLAAEEIQAAAAQhAAAAGOK3WSE/A/8NRU5Dlo6Z5J+yV17raVlQSwcIg45UABcAAAALAAAAUEsBAh4DCgAJAAAAVgYDT/KTqeoQAAAABAAAACAAGAAAAAAAAQAAAKSBAAAAADViNzZiMGVlZjlhZjhhMjMwMDY3M2UwNTUzZjYwOWY5VVQFAANDvkRddXgLAAEEIQAAAAQhAAAAUEsBAh4DCgAJAAAAVgYDT4OOVAAXAAAACwAAAC0AGAAAAAAAAQAAAKSBegAAADViNzZiMGVlZjlhZjhhMjMwMDY3M2UwNTUzZjYwOWY5LmZpbGVuYW1lLnR4dFVUBQADQ75EXXV4CwABBCEAAAAEIQAAAFBLBQYAAAAAAgACANkAAAAIAQAAAAA=", + "ShadowAttribute": [] + }, + { + "id": "814968", + "type": "filename", + "category": "Payload delivery", + "to_ids": false, + "uuid": "5a4e4fff-9ec0-4822-a405-4e29950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": false, + "object_id": "2279", + "object_relation": "filename", + "value": "simple.json", + "ShadowAttribute": [] + }, + { + "id": "814969", + "type": "md5", + "category": "Payload delivery", + "to_ids": true, + "uuid": "5a4e4fff-8000-49f9-8c3e-4598950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": false, + "object_id": "2279", + "object_relation": "md5", + "value": "7637beddacbeac59d44469b2b120b9e6", + "ShadowAttribute": [] + }, + { + "id": "814970", + "type": "sha1", + "category": "Payload delivery", + "to_ids": true, + "uuid": "5a4e4fff-dae0-4aa4-81ea-4899950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": false, + "object_id": "2279", + "object_relation": "sha1", + "value": "023853a4331db8d67e44553004cf338ec1b7440e", + "ShadowAttribute": [] + }, + { + "id": "814971", + "type": "sha256", + "category": "Payload delivery", + "to_ids": true, + "uuid": "5a4e4fff-03ec-4e88-b5f4-472b950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": false, + "object_id": "2279", + "object_relation": "sha256", + "value": "6ae8b0f1c7d6f3238d1fc14038018c3b4704c8cc23dac1c2bfd2c81b5a278eef", + "ShadowAttribute": [] + }, + { + "id": "814972", + "type": "size-in-bytes", + "category": "Other", + "to_ids": false, + "uuid": "5a4e4fff-b6f4-41ba-a6eb-446c950d210f", + "event_id": "6719", + "distribution": "5", + "timestamp": "1515081727", + "comment": "", + "sharing_group_id": "0", + "deleted": false, + "disable_correlation": true, + "object_id": "2279", + "object_relation": "size-in-bytes", + "value": "21", + "ShadowAttribute": [] + } + ] + } + ] }]} diff --git a/tests/mispevent_testfiles/misp_custom_obj.json b/tests/mispevent_testfiles/misp_custom_obj.json index 043957d..a58ae5a 100644 --- a/tests/mispevent_testfiles/misp_custom_obj.json +++ b/tests/mispevent_testfiles/misp_custom_obj.json @@ -1,40 +1,38 @@ { - "Event": { - "Object": [ - { - "Attribute": [ - { - "category": "Other", - "disable_correlation": false, - "object_relation": "member3", - "to_ids": false, - "type": "text", - "value": "foo" - }, - { - "category": "Other", - "disable_correlation": false, - "object_relation": "member1", - "to_ids": false, - "type": "text", - "value": "bar" - } - ], - "description": "TestTemplate.", - "distribution": "5", - "meta-category": "file", - "misp_objects_path_custom": "tests/mispevent_testfiles", - "name": "test_object_template", - "sharing_group_id": "0", - "template_uuid": "4ec55cc6-9e49-4c64-b794-03c25c1a6589", - "template_version": "1", - "uuid": "a" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Object": [ + { + "Attribute": [ + { + "category": "Other", + "disable_correlation": false, + "object_relation": "member3", + "to_ids": false, + "type": "text", + "value": "foo" + }, + { + "category": "Other", + "disable_correlation": false, + "object_relation": "member1", + "to_ids": false, + "type": "text", + "value": "bar" + } + ], + "description": "TestTemplate.", + "distribution": "5", + "meta-category": "file", + "name": "test_object_template", + "sharing_group_id": "0", + "template_uuid": "4ec55cc6-9e49-4c64-b794-03c25c1a6589", + "template_version": "1", + "uuid": "a" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } + diff --git a/tests/mispevent_testfiles/overwrite_file/definition.json b/tests/mispevent_testfiles/overwrite_file/definition.json new file mode 100644 index 0000000..47c16f5 --- /dev/null +++ b/tests/mispevent_testfiles/overwrite_file/definition.json @@ -0,0 +1,457 @@ +{ + "requiredOneOf": [ + "filename", + "size-in-bytes", + "authentihash", + "ssdeep", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "tlsh", + "pattern-in-file", + "certificate", + "malware-sample", + "attachment", + "path", + "fullpath" + ], + "required": [ + "test_overwrite" + ], + "attributes": { + "test_overwrite": { + "description": "Test attribute", + "misp-attribute": "text" + }, + "md5": { + "description": "[Insecure] MD5 hash (128 bits)", + "ui-priority": 1, + "misp-attribute": "md5", + "recommended": false + }, + "sha1": { + "description": "[Insecure] Secure Hash Algorithm 1 (160 bits)", + "ui-priority": 1, + "misp-attribute": "sha1", + "recommended": false + }, + "sha224": { + "description": "Secure Hash Algorithm 2 (224 bits)", + "ui-priority": 0, + "misp-attribute": "sha224", + "recommended": false + }, + "sha256": { + "description": "Secure Hash Algorithm 2 (256 bits)", + "ui-priority": 1, + "misp-attribute": "sha256" + }, + "sha384": { + "description": "Secure Hash Algorithm 2 (384 bits)", + "ui-priority": 0, + "misp-attribute": "sha384", + "recommended": false + }, + "sha512": { + "description": "Secure Hash Algorithm 2 (512 bits)", + "ui-priority": 1, + "misp-attribute": "sha512" + }, + "sha512/224": { + "description": "Secure Hash Algorithm 2 (224 bits)", + "ui-priority": 0, + "misp-attribute": "sha512/224", + "recommended": false + }, + "sha512/256": { + "description": "Secure Hash Algorithm 2 (256 bits)", + "ui-priority": 0, + "misp-attribute": "sha512/256", + "recommended": false + }, + "ssdeep": { + "description": "Fuzzy hash using context triggered piecewise hashes (CTPH)", + "ui-priority": 0, + "misp-attribute": "ssdeep" + }, + "authentihash": { + "description": "Authenticode executable signature hash", + "ui-priority": 0, + "misp-attribute": "authentihash", + "recommended": false + }, + "size-in-bytes": { + "description": "Size of the file, in bytes", + "disable_correlation": true, + "ui-priority": 0, + "misp-attribute": "size-in-bytes" + }, + "entropy": { + "description": "Entropy of the whole file", + "disable_correlation": true, + "ui-priority": 1, + "misp-attribute": "float" + }, + "pattern-in-file": { + "description": "Pattern that can be found in the file", + "categories": [ + "Artifacts dropped", + "Payload installation", + "External analysis" + ], + "ui-priority": 1, + "misp-attribute": "pattern-in-file", + "multiple": true + }, + "text": { + "description": "Free text value to attach to the file", + "disable_correlation": true, + "ui-priority": 1, + "misp-attribute": "text", + "recommended": false + }, + "malware-sample": { + "description": "The file itself (binary)", + "ui-priority": 1, + "misp-attribute": "malware-sample" + }, + "attachment": { + "description": "A non-malicious file.", + "ui-priority": 1, + "misp-attribute": "attachment" + }, + "filename": { + "description": "Filename on disk", + "disable_correlation": true, + "multiple": true, + "categories": [ + "Payload delivery", + "Artifacts dropped", + "Payload installation", + "External analysis" + ], + "ui-priority": 1, + "misp-attribute": "filename" + }, + "path": { + "description": "Path of the filename complete or partial", + "disable_correlation": true, + "multiple": true, + "ui-priority": 0, + "misp-attribute": "text" + }, + "fullpath": { + "description": "Complete path of the filename including the filename", + "multiple": true, + "ui-priority": 0, + "misp-attribute": "text" + }, + "tlsh": { + "description": "Fuzzy hash by Trend Micro: Locality Sensitive Hash", + "ui-priority": 0, + "misp-attribute": "tlsh" + }, + "certificate": { + "description": "Certificate value if the binary is signed with another authentication scheme than authenticode", + "ui-priority": 0, + "misp-attribute": "x509-fingerprint-sha1" + }, + "mimetype": { + "description": "Mime type", + "disable_correlation": true, + "ui-priority": 0, + "misp-attribute": "mime-type" + }, + "state": { + "misp-attribute": "text", + "ui-priority": 0, + "description": "State of the file", + "multiple": true, + "disable_correlation": true, + "values_list": [ + "Malicious", + "Harmless", + "Signed", + "Revoked", + "Expired", + "Trusted" + ] + }, + "file-encoding": { + "misp-attribute": "text", + "ui-priority": 0, + "description": "Encoding format of the file", + "disable_correlation": true, + "sane_default": [ + "Adobe-Standard-Encoding", + "Adobe-Symbol-Encoding", + "Amiga-1251", + "ANSI_X3.110-1983", + "ASMO_449", + "Big5", + "Big5-HKSCS", + "BOCU-1", + "BRF", + "BS_4730", + "BS_viewdata", + "CESU-8", + "CP50220", + "CP51932", + "CSA_Z243.4-1985-1", + "CSA_Z243.4-1985-2", + "CSA_Z243.4-1985-gr", + "CSN_369103", + "DEC-MCS", + "DIN_66003", + "dk-us", + "DS_2089", + "EBCDIC-AT-DE", + "EBCDIC-AT-DE-A", + "EBCDIC-CA-FR", + "EBCDIC-DK-NO", + "EBCDIC-DK-NO-A", + "EBCDIC-ES", + "EBCDIC-ES-A", + "EBCDIC-ES-S", + "EBCDIC-FI-SE", + "EBCDIC-FI-SE-A", + "EBCDIC-FR", + "EBCDIC-IT", + "EBCDIC-PT", + "EBCDIC-UK", + "EBCDIC-US", + "ECMA-cyrillic", + "ES", + "ES2", + "EUC-KR", + "Extended_UNIX_Code_Fixed_Width_for_Japanese", + "Extended_UNIX_Code_Packed_Format_for_Japanese", + "GB18030", + "GB_1988-80", + "GB2312", + "GB_2312-80", + "GBK", + "GOST_19768-74", + "greek7", + "greek7-old", + "greek-ccitt", + "HP-DeskTop", + "HP-Legal", + "HP-Math8", + "HP-Pi-font", + "hp-roman8", + "HZ-GB-2312", + "IBM00858", + "IBM00924", + "IBM01140", + "IBM01141", + "IBM01142", + "IBM01143", + "IBM01144", + "IBM01145", + "IBM01146", + "IBM01147", + "IBM01148", + "IBM01149", + "IBM037", + "IBM038", + "IBM1026", + "IBM1047", + "IBM273", + "IBM274", + "IBM275", + "IBM277", + "IBM278", + "IBM280", + "IBM281", + "IBM284", + "IBM285", + "IBM290", + "IBM297", + "IBM420", + "IBM423", + "IBM424", + "IBM437", + "IBM500", + "IBM775", + "IBM850", + "IBM851", + "IBM852", + "IBM855", + "IBM857", + "IBM860", + "IBM861", + "IBM862", + "IBM863", + "IBM864", + "IBM865", + "IBM866", + "IBM868", + "IBM869", + "IBM870", + "IBM871", + "IBM880", + "IBM891", + "IBM903", + "IBM904", + "IBM905", + "IBM918", + "IBM-Symbols", + "IBM-Thai", + "IEC_P27-1", + "INIS", + "INIS-8", + "INIS-cyrillic", + "INVARIANT", + "ISO_10367-box", + "ISO-10646-J-1", + "ISO-10646-UCS-2", + "ISO-10646-UCS-4", + "ISO-10646-UCS-Basic", + "ISO-10646-Unicode-Latin1", + "ISO-10646-UTF-1", + "ISO-11548-1", + "ISO-2022-CN", + "ISO-2022-CN-EXT", + "ISO-2022-JP", + "ISO-2022-JP-2", + "ISO-2022-KR", + "ISO_2033-1983", + "ISO_5427", + "ISO_5427:1981", + "ISO_5428:1980", + "ISO_646.basic:1983", + "ISO_646.irv:1983", + "ISO_6937-2-25", + "ISO_6937-2-add", + "ISO-8859-10", + "ISO_8859-1:1987", + "ISO-8859-13", + "ISO-8859-14", + "ISO-8859-15", + "ISO-8859-16", + "ISO-8859-1-Windows-3.0-Latin-1", + "ISO-8859-1-Windows-3.1-Latin-1", + "ISO_8859-2:1987", + "ISO-8859-2-Windows-Latin-2", + "ISO_8859-3:1988", + "ISO_8859-4:1988", + "ISO_8859-5:1988", + "ISO_8859-6:1987", + "ISO_8859-6-E", + "ISO_8859-6-I", + "ISO_8859-7:1987", + "ISO_8859-8:1988", + "ISO_8859-8-E", + "ISO_8859-8-I", + "ISO_8859-9:1989", + "ISO-8859-9-Windows-Latin-5", + "ISO_8859-supp", + "iso-ir-90", + "ISO-Unicode-IBM-1261", + "ISO-Unicode-IBM-1264", + "ISO-Unicode-IBM-1265", + "ISO-Unicode-IBM-1268", + "ISO-Unicode-IBM-1276", + "IT", + "JIS_C6220-1969-jp", + "JIS_C6220-1969-ro", + "JIS_C6226-1978", + "JIS_C6226-1983", + "JIS_C6229-1984-a", + "JIS_C6229-1984-b", + "JIS_C6229-1984-b-add", + "JIS_C6229-1984-hand", + "JIS_C6229-1984-hand-add", + "JIS_C6229-1984-kana", + "JIS_Encoding", + "JIS_X0201", + "JIS_X0212-1990", + "JUS_I.B1.002", + "JUS_I.B1.003-mac", + "JUS_I.B1.003-serb", + "KOI7-switched", + "KOI8-R", + "KOI8-U", + "KS_C_5601-1987", + "KSC5636", + "KZ-1048", + "latin-greek", + "Latin-greek-1", + "latin-lap", + "macintosh", + "Microsoft-Publishing", + "MNEM", + "MNEMONIC", + "MSZ_7795.3", + "Name", + "NATS-DANO", + "NATS-DANO-ADD", + "NATS-SEFI", + "NATS-SEFI-ADD", + "NC_NC00-10:81", + "NF_Z_62-010", + "NF_Z_62-010_(1973)", + "NS_4551-1", + "NS_4551-2", + "OSD_EBCDIC_DF03_IRV", + "OSD_EBCDIC_DF04_1", + "OSD_EBCDIC_DF04_15", + "PC8-Danish-Norwegian", + "PC8-Turkish", + "PT", + "PT2", + "PTCP154", + "SCSU", + "SEN_850200_B", + "SEN_850200_C", + "Shift_JIS", + "T.101-G2", + "T.61-7bit", + "T.61-8bit", + "TIS-620", + "TSCII", + "UNICODE-1-1", + "UNICODE-1-1-UTF-7", + "UNKNOWN-8BIT", + "US-ASCII", + "us-dk", + "UTF-16", + "UTF-16BE", + "UTF-16LE", + "UTF-32", + "UTF-32BE", + "UTF-32LE", + "UTF-7", + "UTF-8", + "Ventura-International", + "Ventura-Math", + "Ventura-US", + "videotex-suppl", + "VIQR", + "VISCII", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + "Windows-31J", + "windows-874" + ] + } + }, + "version": 1, + "description": "File object describing a file with meta-information", + "meta-category": "file", + "uuid": "688c46fb-5edb-40a3-8273-1af7923e0000", + "name": "overwrite_file" +} diff --git a/tests/mispevent_testfiles/proposals.json b/tests/mispevent_testfiles/proposals.json index e249fd6..344c5ec 100644 --- a/tests/mispevent_testfiles/proposals.json +++ b/tests/mispevent_testfiles/proposals.json @@ -1,36 +1,35 @@ { - "Event": { - "Attribute": [ - { - "ShadowAttribute": [ - { - "category": "Payload delivery", - "disable_correlation": false, - "to_ids": true, - "type": "filename", - "value": "bar.pdf" - } - ], - "category": "Payload delivery", - "disable_correlation": false, - "to_ids": true, - "type": "filename", - "value": "bar.exe" - } - ], - "ShadowAttribute": [ - { - "category": "Payload delivery", - "disable_correlation": false, - "to_ids": true, - "type": "filename", - "value": "baz.jpg" - } - ], - "analysis": "1", - "date": "2017-12-31", - "distribution": "1", - "info": "This is a test", - "threat_level_id": "1" - } + "Attribute": [ + { + "ShadowAttribute": [ + { + "category": "Payload delivery", + "disable_correlation": false, + "to_ids": true, + "type": "filename", + "value": "bar.pdf" + } + ], + "category": "Payload delivery", + "disable_correlation": false, + "to_ids": true, + "type": "filename", + "value": "bar.exe" + } + ], + "ShadowAttribute": [ + { + "category": "Payload delivery", + "disable_correlation": false, + "to_ids": true, + "type": "filename", + "value": "baz.jpg" + } + ], + "analysis": "1", + "date": "2017-12-31", + "distribution": "1", + "info": "This is a test", + "threat_level_id": "1" } + diff --git a/tests/mispevent_testfiles/shadow.json b/tests/mispevent_testfiles/shadow.json index bce2a16..61b484c 100644 --- a/tests/mispevent_testfiles/shadow.json +++ b/tests/mispevent_testfiles/shadow.json @@ -1,149 +1,148 @@ { - "Event": { - "Attribute": [ - { - "ShadowAttribute": [ - { - "Org": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "category": "Artifacts dropped", - "comment": "", - "disable_correlation": false, - "event_id": "6676", - "event_uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f", - "id": "3770", - "old_id": "811578", - "org_id": "1", - "proposal_to_delete": false, - "timestamp": "1514975846", - "to_ids": true, - "type": "filename", - "uuid": "5a4cb1c7-fa84-45fa-8d27-4822950d210f", - "value": "blah.exe.jpg" - } - ], - "category": "Artifacts dropped", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "6676", - "id": "811578", - "object_id": "0", - "sharing_group_id": "0", - "timestamp": "1514975687", - "to_ids": false, - "type": "filename", - "uuid": "5a4cb1c7-fa84-45fa-8d27-4822950d210f", - "value": "blah.exe" - } - ], - "Object": [ - { - "Attribute": [ - { - "ShadowAttribute": [ - { - "Org": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "category": "Payload delivery", - "comment": "", - "disable_correlation": false, - "event_id": "6676", - "event_uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f", - "id": "3771", - "old_id": "811579", - "org_id": "1", - "proposal_to_delete": false, - "timestamp": "1514976196", - "to_ids": true, - "type": "filename", - "uuid": "5a4cb2b8-4748-4c72-96e6-4588950d210f", - "value": "baz.png.exe" - } - ], - "category": "Payload delivery", - "comment": "", - "deleted": false, - "disable_correlation": false, - "distribution": "5", - "event_id": "6676", - "id": "811579", - "object_id": "2278", - "object_relation": "filename", - "sharing_group_id": "0", - "timestamp": "1514975928", - "to_ids": true, - "type": "filename", - "uuid": "5a4cb2b8-4748-4c72-96e6-4588950d210f", - "value": "baz.png" + "Attribute": [ + { + "ShadowAttribute": [ + { + "Org": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" }, - { - "category": "Other", - "comment": "", - "deleted": false, - "disable_correlation": true, - "distribution": "5", - "event_id": "6676", - "id": "811580", - "object_id": "2278", - "object_relation": "state", - "sharing_group_id": "0", - "timestamp": "1514975928", - "to_ids": false, - "type": "text", - "uuid": "5a4cb2b9-92b4-4d3a-82df-4e86950d210f", - "value": "Malicious" - } - ], - "comment": "", - "deleted": false, - "description": "File object describing a file with meta-information", - "distribution": "5", - "event_id": "6676", - "id": "2278", - "meta-category": "file", - "name": "file", - "sharing_group_id": "0", - "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "8", - "timestamp": "1514975928", - "uuid": "5a4cb2b8-7958-4323-852c-4d2a950d210f" - } - ], - "Org": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "Orgc": { - "id": "1", - "name": "CIRCL", - "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" - }, - "analysis": "2", - "attribute_count": "3", - "date": "2018-01-03", - "disable_correlation": false, - "distribution": "0", - "event_creator_email": "raphael.vinot@circl.lu", - "id": "6676", - "info": "Test proposals / ShadowAttributes", - "locked": false, - "org_id": "1", - "orgc_id": "1", - "proposal_email_lock": true, - "publish_timestamp": "0", - "published": false, - "sharing_group_id": "0", - "threat_level_id": "1", - "timestamp": "1514975929", - "uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f" - } + "category": "Artifacts dropped", + "comment": "", + "disable_correlation": false, + "event_id": "6676", + "event_uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f", + "id": "3770", + "old_id": "811578", + "org_id": "1", + "proposal_to_delete": false, + "timestamp": "1514975846", + "to_ids": true, + "type": "filename", + "uuid": "5a4cb1c7-fa84-45fa-8d27-4822950d210f", + "value": "blah.exe.jpg" + } + ], + "category": "Artifacts dropped", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "6676", + "id": "811578", + "object_id": "0", + "sharing_group_id": "0", + "timestamp": "1514975687", + "to_ids": false, + "type": "filename", + "uuid": "5a4cb1c7-fa84-45fa-8d27-4822950d210f", + "value": "blah.exe" + } + ], + "Object": [ + { + "Attribute": [ + { + "ShadowAttribute": [ + { + "Org": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "category": "Payload delivery", + "comment": "", + "disable_correlation": false, + "event_id": "6676", + "event_uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f", + "id": "3771", + "old_id": "811579", + "org_id": "1", + "proposal_to_delete": false, + "timestamp": "1514976196", + "to_ids": true, + "type": "filename", + "uuid": "5a4cb2b8-4748-4c72-96e6-4588950d210f", + "value": "baz.png.exe" + } + ], + "category": "Payload delivery", + "comment": "", + "deleted": false, + "disable_correlation": false, + "distribution": "5", + "event_id": "6676", + "id": "811579", + "object_id": "2278", + "object_relation": "filename", + "sharing_group_id": "0", + "timestamp": "1514975928", + "to_ids": true, + "type": "filename", + "uuid": "5a4cb2b8-4748-4c72-96e6-4588950d210f", + "value": "baz.png" + }, + { + "category": "Other", + "comment": "", + "deleted": false, + "disable_correlation": true, + "distribution": "5", + "event_id": "6676", + "id": "811580", + "object_id": "2278", + "object_relation": "state", + "sharing_group_id": "0", + "timestamp": "1514975928", + "to_ids": false, + "type": "text", + "uuid": "5a4cb2b9-92b4-4d3a-82df-4e86950d210f", + "value": "Malicious" + } + ], + "comment": "", + "deleted": false, + "description": "File object describing a file with meta-information", + "distribution": "5", + "event_id": "6676", + "id": "2278", + "meta-category": "file", + "name": "file", + "sharing_group_id": "0", + "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", + "template_version": "8", + "timestamp": "1514975928", + "uuid": "5a4cb2b8-7958-4323-852c-4d2a950d210f" + } + ], + "Org": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "Orgc": { + "id": "1", + "name": "CIRCL", + "uuid": "55f6ea5e-2c60-40e5-964f-47a8950d210f" + }, + "analysis": "2", + "attribute_count": "3", + "date": "2018-01-03", + "disable_correlation": false, + "distribution": "0", + "event_creator_email": "raphael.vinot@circl.lu", + "id": "6676", + "info": "Test proposals / ShadowAttributes", + "locked": false, + "org_id": "1", + "orgc_id": "1", + "proposal_email_lock": true, + "publish_timestamp": "0", + "published": false, + "sharing_group_id": "0", + "threat_level_id": "1", + "timestamp": "1514975929", + "uuid": "5a4cb19a-f550-437f-bd29-48ed950d210f" } + diff --git a/tests/mispevent_testfiles/simple.json b/tests/mispevent_testfiles/simple.json index 63fbfdd..2c63c08 100644 --- a/tests/mispevent_testfiles/simple.json +++ b/tests/mispevent_testfiles/simple.json @@ -1,4 +1,2 @@ { - "Event": { - } } diff --git a/tests/reportlab_testfiles/image_event.json b/tests/reportlab_testfiles/image_event.json index e619c6d..c3cd90d 100644 --- a/tests/reportlab_testfiles/image_event.json +++ b/tests/reportlab_testfiles/image_event.json @@ -715,7 +715,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "1db36cab-7b13-4758-b16a-9e9862d0973e", "timestamp": "1550871228", @@ -887,7 +887,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "3b8f6a45-0b7f-4bea-ad61-0369f01cc306", "timestamp": "1550871228", @@ -1059,7 +1059,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "8cc1ffb8-e4b2-4641-a536-ea843ff9bc7a", "timestamp": "1550871228", @@ -1231,7 +1231,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "89e0ad73-a186-4959-b978-2311ee49e4af", "timestamp": "1550871229", @@ -1403,7 +1403,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "4dbf697b-11ce-447f-85c6-cd02a2365a7f", "timestamp": "1550871229", @@ -1575,7 +1575,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "6860e975-938c-413d-b144-74cde72c25dc", "timestamp": "1550871229", @@ -1747,7 +1747,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "df5dd372-ecd6-4595-ab34-45bff1decb63", "timestamp": "1550871229", @@ -1919,7 +1919,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "3061d73f-2f4f-4c6e-8478-3d5d1e74c1bc", "timestamp": "1550871229", @@ -2091,7 +2091,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "fd57be37-61cc-4452-85b5-518d55586335", "timestamp": "1550871230", @@ -2263,7 +2263,7 @@ "meta-category": "file", "description": "File object describing a file with meta-information", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", - "template_version": "16", + "template_version": "17", "event_id": "1203", "uuid": "56b391e4-f005-4caa-ae12-a90db6664ebd", "timestamp": "1550871270", diff --git a/tests/reportlab_testfiles/mainly_objects_1.json b/tests/reportlab_testfiles/mainly_objects_1.json index 733758c..098a77b 100644 --- a/tests/reportlab_testfiles/mainly_objects_1.json +++ b/tests/reportlab_testfiles/mainly_objects_1.json @@ -70,7 +70,7 @@ "timestamp": "1543921748", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -114,7 +114,7 @@ "timestamp": "1543921750", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -158,7 +158,7 @@ "timestamp": "1543921751", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -482,7 +482,7 @@ "timestamp": "1543921755", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -875,7 +875,7 @@ "timestamp": "1543921759", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -988,7 +988,7 @@ "timestamp": "1543921762", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", diff --git a/tests/reportlab_testfiles/mainly_objects_2.json b/tests/reportlab_testfiles/mainly_objects_2.json index 3471c1f..e032a36 100644 --- a/tests/reportlab_testfiles/mainly_objects_2.json +++ b/tests/reportlab_testfiles/mainly_objects_2.json @@ -82,7 +82,7 @@ "timestamp": "1543922168", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -126,7 +126,7 @@ "timestamp": "1543922169", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -450,7 +450,7 @@ "timestamp": "1543922173", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", @@ -857,7 +857,7 @@ "timestamp": "1543922178", "description": "File object describing a file with meta-information", "distribution": "3", - "template_version": "16", + "template_version": "17", "template_uuid": "688c46fb-5edb-40a3-8273-1af7923e2215", "comment": "", "name": "file", diff --git a/tests/stix1.xml-utf8 b/tests/stix1.xml-utf8 new file mode 100644 index 0000000..4c4e149 --- /dev/null +++ b/tests/stix1.xml-utf8 @@ -0,0 +1,110 @@ + + + Export from ORGNAME MISP + Threat Report + + + + + + Export from ORGNAME MISP © YADA YADA + Threat Report + + + + Test Stix + 612 + + 2021-08-24T00:00:00 + 2021-08-24T10:53:28 + + + + ORGNAME + + + New + + + Network activity + + + + 8.8.8.8 + + + + + + + + Event Threat Level: High + + + MISP Tag: misp:tool="misp2stix" + + + + + ORGNAME + + + + + + + + diff --git a/tests/stix2.json b/tests/stix2.json new file mode 100644 index 0000000..e804494 --- /dev/null +++ b/tests/stix2.json @@ -0,0 +1 @@ +{"type": "bundle", "spec_version": "2.0", "id": "bundle--811070d5-8e95-43c1-8af3-0b5e799dc15c", "objects": [{"type": "identity", "id": "identity--5d397c4b-3474-466a-80cd-624838d4ff94", "name": "ORGNAME", "identity_class": "organization", "created": "2021-08-24T10:53:42.879Z", "modified": "2021-08-24T10:53:42.879Z"}, {"type": "report", "id": "report--f90bb8c1-8505-4d74-af34-3dcffec6b6d4", "name": "Test Stix", "created": "2021-08-24T00:00:00.000Z", "published": "2021-08-24T10:53:28Z", "modified": "2021-08-24T10:53:13.000Z", "created_by_ref": "identity--5d397c4b-3474-466a-80cd-624838d4ff94", "labels": ["Threat-Report", "misp:tool=\"misp2stix2\""], "object_refs": ["observed-data--0853d51f-0fe7-4d35-b3cb-b96bdbc1f0ee"]}, {"id": "observed-data--0853d51f-0fe7-4d35-b3cb-b96bdbc1f0ee", "type": "observed-data", "number_observed": 1, "objects": {"0": {"type": "ipv4-addr", "value": "8.8.8.8"}, "1": {"type": "network-traffic", "src_ref": "0", "protocols": ["ipv4"]}}, "created_by_ref": "identity--5d397c4b-3474-466a-80cd-624838d4ff94", "labels": ["misp:type=\"ip-src\"", "misp:category=\"Network activity\"", "misp:to_ids=\"False\""], "created": "2021-08-24T10:53:13.000Z", "modified": "2021-08-24T10:53:13.000Z", "first_observed": "2021-08-24T10:53:13Z", "last_observed": "2021-08-24T10:53:13Z"}]} diff --git a/tests/test.py b/tests/test.py deleted file mode 100755 index c0bde0d..0000000 --- a/tests/test.py +++ /dev/null @@ -1,322 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from pymisp import PyMISP, __version__ -try: - from keys import url, key -except ImportError as e: - print(e) - url = 'http://localhost:8080' - key = 'fk5BodCZw8owbscW8pQ4ykMASLeJ4NYhuAbshNjo' - -import time - -import unittest - - -class TestBasic(unittest.TestCase): - - def setUp(self): - self.maxDiff = None - self.misp = PyMISP(url, key, True, 'json') - self.live_describe_types = self.misp.get_live_describe_types() - - def _clean_event(self, event): - event['Event'].pop('orgc_id', None) - event['Event'].pop('uuid', None) - event['Event'].pop('sharing_group_id', None) - event['Event'].pop('timestamp', None) - event['Event'].pop('org_id', None) - event['Event'].pop('date', None) - event['Event'].pop('RelatedEvent', None) - event['Event'].pop('publish_timestamp', None) - if event['Event'].get('Attribute'): - for a in event['Event'].get('Attribute'): - a.pop('uuid', None) - a.pop('event_id', None) - a.pop('id', None) - a.pop('timestamp', None) - if event['Event'].get('Orgc'): - event['Event']['Orgc'].pop('uuid', None) - event['Event']['Orgc'].pop('id', None) - if event['Event'].get('Org'): - event['Event']['Org'].pop('uuid', None) - event['Event']['Org'].pop('id', None) - return event['Event'].pop('id', None) - - def new_event(self): - event = self.misp.new_event(0, 1, 0, "This is a test") - event_id = self._clean_event(event) - to_check = {u'Event': {u'info': u'This is a test', u'locked': False, - u'attribute_count': u'0', 'disable_correlation': False, u'analysis': u'0', - u'ShadowAttribute': [], u'published': False, - u'distribution': u'0', u'event_creator_email': u'admin@admin.test', u'Attribute': [], u'proposal_email_lock': False, - u'extends_uuid': '', - u'Object': [], u'Org': {u'name': u'ORGNAME'}, - u'Orgc': {u'name': u'ORGNAME'}, - u'Galaxy': [], - u'threat_level_id': u'1'}} - self.assertEqual(event, to_check, 'Failed at creating a new Event') - return int(event_id) - - def add_hashes(self, eventid): - r = self.misp.get_event(eventid) - event = r.json() - event = self.misp.add_hashes(event, - category='Payload installation', - filename='dll_installer.dll', - md5='0a209ac0de4ac033f31d6ba9191a8f7a', - sha1='1f0ae54ac3f10d533013f74f48849de4e65817a7', - sha256='003315b0aea2fcb9f77d29223dd8947d0e6792b3a0227e054be8eb2a11f443d9', - ssdeep=None, - comment='Fanny modules', - to_ids=False, - distribution=2, - proposal=False) - self._clean_event(event) - to_check = {u'Event': {u'info': u'This is a test', u'locked': False, - u'attribute_count': u'3', u'analysis': u'0', - u'ShadowAttribute': [], u'published': False, u'distribution': u'0', u'event_creator_email': u'admin@admin.test', - u'Org': {u'name': u'ORGNAME'}, - u'Orgc': {u'name': u'ORGNAME'}, - u'Galaxy': [], - u'Attribute': [ - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|0a209ac0de4ac033f31d6ba9191a8f7a', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|md5'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|1f0ae54ac3f10d533013f74f48849de4e65817a7', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha1'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|003315b0aea2fcb9f77d29223dd8947d0e6792b3a0227e054be8eb2a11f443d9', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha256'}], - u'proposal_email_lock': False, u'threat_level_id': u'1'}} - self.assertEqual(event, to_check, 'Failed at adding hashes') - - def publish(self, eventid): - r = self.misp.get_event(eventid) - event = r.json() - event = self.misp.publish(event) - self._clean_event(event) - to_check = {u'Event': {u'info': u'This is a test', u'locked': False, - u'attribute_count': u'3', u'analysis': u'0', - u'ShadowAttribute': [], u'published': True, u'distribution': u'0', u'event_creator_email': u'admin@admin.test', - u'Org': {u'name': u'ORGNAME'}, - u'Orgc': {u'name': u'ORGNAME'}, - u'Galaxy': [], - u'Attribute': [ - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|0a209ac0de4ac033f31d6ba9191a8f7a', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|md5'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|1f0ae54ac3f10d533013f74f48849de4e65817a7', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha1'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|003315b0aea2fcb9f77d29223dd8947d0e6792b3a0227e054be8eb2a11f443d9', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha256'}], - u'proposal_email_lock': False, u'threat_level_id': u'1'}} - self.assertEqual(event, to_check, 'Failed at publishing event') - - def delete(self, eventid): - event = self.misp.delete_event(eventid) - print(event) - - def delete_attr(self, attrid): - event = self.misp.delete_attribute(attrid) - print(event) - - def get(self, eventid): - event = self.misp.get_event(eventid) - print(event) - - def get_stix(self, **kwargs): - event = self.misp.get_stix(kwargs) - print(event) - - def add(self): - event = {u'Event': {u'info': u'This is a test', u'locked': False, - u'attribute_count': u'3', u'analysis': u'0', - u'ShadowAttribute': [], u'published': False, u'distribution': u'0', u'event_creator_email': u'admin@admin.test', - u'Attribute': [ - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|0a209ac0de4ac033f31d6ba9191a8f7a', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|md5'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|1f0ae54ac3f10d533013f74f48849de4e65817a7', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha1'}, - {u'category': u'Payload installation', u'comment': u'Fanny modules', - u'to_ids': False, u'value': u'dll_installer.dll|003315b0aea2fcb9f77d29223dd8947d0e6792b3a0227e054be8eb2a11f443d9', - u'ShadowAttribute': [], u'distribution': u'2', u'type': u'filename|sha256'}], - u'proposal_email_lock': False, u'threat_level_id': u'1'}} - event = self.misp.add_event(event) - print(event) - - def add_user(self): - email = 'test@misp.local' - role_id = '5' - org_id = '1' - password = 'Password1234!' - external_auth_required = False - external_auth_key = '' - enable_password = False - nids_sid = '1238717' - server_id = '1' - gpgkey = '' - certif_public = '' - autoalert = False - contactalert = False - disabled = False - change_pw = '0' - termsaccepted = False - newsread = '0' - authkey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - to_check = {'User': {'email': email, 'org_id': org_id, 'role_id': role_id, - 'password': password, 'external_auth_required': external_auth_required, - 'external_auth_key': external_auth_key, 'enable_password': enable_password, - 'nids_sid': nids_sid, 'server_id': server_id, 'gpgkey': gpgkey, - 'certif_public': certif_public, 'autoalert': autoalert, - 'contactalert': contactalert, 'disabled': disabled, - 'change_pw': change_pw, 'termsaccepted': termsaccepted, - 'newsread': newsread, 'authkey': authkey}} - user = self.misp.add_user(email=email, - role_id=role_id, - org_id=org_id, - password=password, - external_auth_required=external_auth_required, - external_auth_key=external_auth_key, - enable_password=enable_password, - nids_sid=nids_sid, - server_id=server_id, - gpgkey=gpgkey, - certif_public=certif_public, - autoalert=autoalert, - contactalert=contactalert, - disabled=disabled, - change_pw=change_pw, - termsaccepted=termsaccepted, - newsread=newsread, - authkey=authkey) - # delete user to allow reuse of test - uid = user.get('User').get('id') - self.misp.delete_user(uid) - # ---------------------------------- - # test interesting keys only (some keys are modified(password) and some keys are added (lastlogin) - tested_keys = ['email', 'org_id', 'role_id', 'server_id', 'autoalert', - 'authkey', 'gpgkey', 'certif_public', 'nids_sid', 'termsaccepted', - 'newsread', 'contactalert', 'disabled'] - for k in tested_keys: - self.assertEqual(user.get('User').get(k), to_check.get('User').get(k), "Failed to match input with output on key: {}".format(k)) - - def add_organisation(self): - name = 'Organisation tests' - description = 'This is a test organisation' - orgtype = 'Type is a string' - nationality = 'French' - sector = 'Bank sector' - uuid = '16fd2706-8baf-433b-82eb-8c7fada847da' - contacts = 'Text field with no limitations' - local = False - to_check = {'Organisation': {'name': name, 'description': description, - 'type': orgtype, 'nationality': nationality, - 'sector': sector, 'uuid': uuid, 'contacts': contacts, - 'local': local}} - org = self.misp.add_organisation(name=name, - description=description, - type=orgtype, - nationality=nationality, - sector=sector, - uuid=uuid, - contacts=contacts, - local=local, - ) - # delete organisation to allow reuse of test - oid = org.get('Organisation').get('id') - self.misp.delete_organisation(oid) - # ---------------------------------- - tested_keys = ['anonymise', 'contacts', 'description', 'local', 'name', - 'nationality', 'sector', 'type', 'uuid'] - for k in tested_keys: - self.assertEqual(org.get('Organisation').get(k), to_check.get('Organisation').get(k), "Failed to match input with output on key: {}".format(k)) - - def test_create_event(self): - eventid = self.new_event() - time.sleep(1) - self.delete(eventid) - - def test_get_event(self): - eventid = self.new_event() - time.sleep(1) - self.get(eventid) - time.sleep(1) - self.delete(eventid) - - def test_add_event(self): - self.add() - time.sleep(1) - self.delete(1) - - def test_del_attr(self): - eventid = self.new_event() - time.sleep(1) - self.delete_attr(1) - time.sleep(1) - self.delete(eventid) - - def test_one_or_more(self): - self.assertEqual(self.misp._one_or_more(1), (1,)) - self.assertEqual(self.misp._one_or_more([1]), [1]) - - def test_create_user(self): - self.add_user() - - def test_create_organisation(self): - self.add_organisation() - - def test_describeTypes_sane_default(self): - sane_default = self.live_describe_types['sane_defaults'] - self.assertEqual(sorted(sane_default.keys()), sorted(self.live_describe_types['types'])) - - def test_describeTypes_categories(self): - category_type_mappings = self.live_describe_types['category_type_mappings'] - self.assertEqual(sorted(category_type_mappings.keys()), sorted(self.live_describe_types['categories'])) - - def test_describeTypes_types_in_categories(self): - category_type_mappings = self.live_describe_types['category_type_mappings'] - for category, types in category_type_mappings.items(): - existing_types = [t for t in types if t in self.live_describe_types['types']] - self.assertEqual(sorted(existing_types), sorted(types)) - - def test_describeTypes_types_have_category(self): - category_type_mappings = self.live_describe_types['category_type_mappings'] - all_types = set() - for category, types in category_type_mappings.items(): - all_types.update(types) - self.assertEqual(sorted(list(all_types)), sorted(self.live_describe_types['types'])) - - def test_describeTypes_sane_default_valid_category(self): - sane_default = self.live_describe_types['sane_defaults'] - categories = self.live_describe_types['categories'] - for t, sd in sane_default.items(): - self.assertTrue(sd['to_ids'] in [0, 1]) - self.assertTrue(sd['default_category'] in categories) - - def test_describeTypes_uptodate(self): - local_describe = self.misp.get_local_describe_types() - for temp_key in local_describe.keys(): - if isinstance(local_describe[temp_key], list): - self.assertEqual(sorted(self.live_describe_types[temp_key]), sorted(local_describe[temp_key])) - else: - self.assertEqual(self.live_describe_types[temp_key], local_describe[temp_key]) - - def test_live_acl(self): - query_acl = self.misp.get_live_query_acl() - self.assertEqual(query_acl['response'], []) - - def test_recommended_pymisp_version(self): - response = self.misp.get_recommended_api_version() - recommended_version_tup = tuple(int(x) for x in response['version'].split('.')) - pymisp_version_tup = tuple(int(x) for x in __version__.split('.'))[:3] - self.assertEqual(recommended_version_tup, pymisp_version_tup) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_emailobject.py b/tests/test_emailobject.py new file mode 100644 index 0000000..01cb9d0 --- /dev/null +++ b/tests/test_emailobject.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +# import json +import unittest + +from email.message import EmailMessage +from io import BytesIO +from os import urandom +from pathlib import Path +from typing import TypeVar, Type +from zipfile import ZipFile + +from pymisp.tools import EMailObject +from pymisp.exceptions import PyMISPNotImplementedYet, InvalidMISPObject + +T = TypeVar('T', bound='TestEmailObject') + + +class TestEmailObject(unittest.TestCase): + + eml_1: BytesIO + + @classmethod + def setUpClass(cls: type[T]) -> None: + with ZipFile(Path("tests/email_testfiles/mail_1.eml.zip"), 'r') as myzip: + with myzip.open('mail_1.eml', pwd=b'AVs are dumb') as myfile: + cls.eml_1 = BytesIO(myfile.read()) + + def test_mail_1(self) -> None: + email_object = EMailObject(pseudofile=self.eml_1) + self.assertEqual(self._get_values(email_object, "subject")[0], "письмо уведом-е") + self.assertEqual(self._get_values(email_object, "to")[0], "kinney@noth.com") + self.assertEqual(self._get_values(email_object, "from")[0], "suvorov.s@nalg.ru") + self.assertEqual(self._get_values(email_object, "from-display-name")[0], "служба ФНС Даниил Суворов") + self.assertEqual(len(self._get_values(email_object, "email-body")), 1) + + self.assertEqual(self._get_values(email_object, "received-header-ip"), + ['64.98.42.207', '2603:10b6:207:3d::31', + '2a01:111:f400:7e49::205', '43.230.105.145']) + + self.assertIsInstance(email_object.email, EmailMessage) + for file_name, file_content in email_object.attachments: + self.assertIsInstance(file_name, str) + self.assertIsInstance(file_content, BytesIO) + + def test_mail_1_headers_only(self) -> None: + email_object = EMailObject(Path("tests/email_testfiles/mail_1_headers_only.eml")) + self.assertEqual(self._get_values(email_object, "subject")[0], "письмо уведом-е") + self.assertEqual(self._get_values(email_object, "to")[0], "kinney@noth.com") + self.assertEqual(self._get_values(email_object, "from")[0], "suvorov.s@nalg.ru") + + self.assertEqual(len(self._get_values(email_object, "email-body")), 0) + + self.assertIsInstance(email_object.email, EmailMessage) + self.assertEqual(len(email_object.attachments), 0) + + def test_mail_multiple_to(self) -> None: + email_object = EMailObject(Path("tests/email_testfiles/mail_multiple_to.eml")) + + to = self._get_values(email_object, "to") + to_display_name = self._get_values(email_object, "to-display-name") + self.assertEqual(to[0], "jan.novak@example.com") + self.assertEqual(to_display_name[0], "Novak, Jan") + self.assertEqual(to[1], "jan.marek@example.com") + self.assertEqual(to_display_name[1], "Marek, Jan") + + def test_msg(self) -> None: + # Test result of eml converted to msg is the same + eml_email_object = EMailObject(pseudofile=self.eml_1) + email_object = EMailObject(Path("tests/email_testfiles/mail_1.msg")) + + self.assertIsInstance(email_object.email, EmailMessage) + for file_name, file_content in email_object.attachments: + self.assertIsInstance(file_name, str) + self.assertIsInstance(file_content, BytesIO) + + self.assertEqual(self._get_values(email_object, "subject")[0], + self._get_values(eml_email_object, "subject")[0]) + self.assertEqual(self._get_values(email_object, "to")[0], + self._get_values(eml_email_object, "to")[0]) + self.assertEqual(self._get_values(email_object, "from")[0], + self._get_values(eml_email_object, "from")[0]) + self.assertEqual(self._get_values(email_object, "from-display-name")[0], + self._get_values(eml_email_object, "from-display-name")[0]) + self.assertEqual(len(self._get_values(email_object, "email-body")), 2) + + self.assertEqual(self._get_values(email_object, "received-header-ip"), + self._get_values(eml_email_object, "received-header-ip")) + + def test_bom_encoded(self) -> None: + """Test utf-8-sig encoded email""" + bom_email_object = EMailObject(Path("tests/email_testfiles/mail_1_bom.eml")) + eml_email_object = EMailObject(pseudofile=self.eml_1) + + self.assertIsInstance(bom_email_object.email, EmailMessage) + for file_name, file_content in bom_email_object.attachments: + self.assertIsInstance(file_name, str) + self.assertIsInstance(file_content, BytesIO) + + self.assertEqual(self._get_values(bom_email_object, "subject")[0], + self._get_values(eml_email_object, "subject")[0]) + self.assertEqual(self._get_values(bom_email_object, "to")[0], + self._get_values(eml_email_object, "to")[0]) + self.assertEqual(self._get_values(bom_email_object, "from")[0], + self._get_values(eml_email_object, "from")[0]) + self.assertEqual(self._get_values(bom_email_object, "from-display-name")[0], + self._get_values(eml_email_object, "from-display-name")[0]) + self.assertEqual(len(self._get_values(bom_email_object, "email-body")), 1) + + self.assertEqual(self._get_values(bom_email_object, "received-header-ip"), + self._get_values(eml_email_object, "received-header-ip")) + + def test_handling_of_various_email_types(self) -> None: + self._does_not_fail(Path("tests/email_testfiles/mail_2.eml"), + "ensuring all headers work") + self._does_not_fail(Path('tests/email_testfiles/mail_3.eml'), + "Check for related content in emails emls") + self._does_not_fail(Path('tests/email_testfiles/mail_3.msg'), + "Check for related content in emails msgs") + self._does_not_fail(Path('tests/email_testfiles/mail_4.msg'), + "Check that HTML without specific encoding") + self._does_not_fail(Path('tests/email_testfiles/mail_5.msg'), + "Check encapsulated HTML works") + + def _does_not_fail(self, path: Path, test_type: str="test") -> None: + found_error = None + try: + EMailObject(path) + except Exception as _e: + found_error = _e + if found_error is not None: + self.fail('Error {} raised when parsing test email {} which tests against {}. It should not have raised an error.'.format( + type(found_error), + path, + test_type)) + + def test_random_binary_blob(self) -> None: + """Email parser fails correctly on random binary blob.""" + random_data = urandom(1024) + random_blob = BytesIO(random_data) + found_error = None + try: + broken_obj = EMailObject(pseudofile=random_data) + except Exception as _e: + found_error = _e + if not isinstance(found_error, InvalidMISPObject): + self.fail("Expected InvalidMISPObject when EmailObject receives completely unknown binary input data. But, did not get that exception.") + try: + broken_obj = EMailObject(pseudofile=random_blob) + except Exception as _e: + found_error = _e + if not isinstance(found_error, InvalidMISPObject): + self.fail("Expected InvalidMISPObject when EmailObject receives completely unknown binary input data in a pseudofile. But, did not get that exception.") + + @staticmethod + def _get_values(obj: EMailObject, relation: str) -> list[str]: + return [attr.value for attr in obj.attributes if attr['object_relation'] == relation] diff --git a/tests/test_fileobject.py b/tests/test_fileobject.py new file mode 100644 index 0000000..1299b3a --- /dev/null +++ b/tests/test_fileobject.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import unittest +import json +from pymisp.tools import FileObject +import pathlib + + +class TestFileObject(unittest.TestCase): + def test_mimeType(self) -> None: + file_object = FileObject(filepath=pathlib.Path(__file__)) + attributes = json.loads(file_object.to_json())['Attribute'] + mime = next(attr for attr in attributes if attr['object_relation'] == 'mimetype') + # was "Python script, ASCII text executable" + # libmagic on linux: 'text/x-python' + # libmagic on os x: 'text/x-script.python' + self.assertEqual(mime['value'][:7], 'text/x-') + self.assertEqual(mime['value'][-6:], 'python') diff --git a/tests/test_mispevent.py b/tests/test_mispevent.py index 0b0e55a..8c9564c 100644 --- a/tests/test_mispevent.py +++ b/tests/test_mispevent.py @@ -1,206 +1,300 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import unittest import json -import sys from io import BytesIO +import glob +import hashlib +from datetime import date, datetime -from pymisp import MISPEvent, MISPSighting, MISPTag +from pymisp import (MISPAttribute, MISPEvent, MISPGalaxy, MISPObject, MISPOrganisation, + MISPSighting, MISPTag) from pymisp.exceptions import InvalidMISPObject +from pymisp.tools import GitVulnFinderObject class TestMISPEvent(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.mispevent = MISPEvent() - def init_event(self): + def init_event(self) -> None: self.mispevent.info = 'This is a test' self.mispevent.distribution = 1 self.mispevent.threat_level_id = 1 self.mispevent.analysis = 1 self.mispevent.set_date("2017-12-31") # test the set date method - def test_simple(self): - with open('tests/mispevent_testfiles/simple.json', 'r') as f: + def test_simple(self) -> None: + with open('tests/mispevent_testfiles/simple.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_event(self): + def test_event(self) -> None: self.init_event() self.mispevent.publish() - with open('tests/mispevent_testfiles/event.json', 'r') as f: + with open('tests/mispevent_testfiles/event.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_loadfile(self): + def test_loadfile(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/event.json') - with open('tests/mispevent_testfiles/event.json', 'r') as f: + with open('tests/mispevent_testfiles/event.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_event_tag(self): + def test_loadfile_validate(self) -> None: + misp_event = MISPEvent() + misp_event.load_file('tests/mispevent_testfiles/event.json', validate=True) + + def test_loadfile_validate_strict(self) -> None: + misp_event = MISPEvent(strict_validation=True) + misp_event.load_file('tests/mispevent_testfiles/event.json', validate=True) + + def test_event_tag(self) -> None: self.init_event() self.mispevent.add_tag('bar') self.mispevent.add_tag(name='baz') new_tag = MISPTag() new_tag.from_dict(name='foo') self.mispevent.add_tag(new_tag) - with open('tests/mispevent_testfiles/event_tags.json', 'r') as f: + with open('tests/mispevent_testfiles/event_tags.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_attribute(self): + def test_event_galaxy(self) -> None: self.init_event() - a = self.mispevent.add_attribute('filename', 'bar.exe') + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + self.mispevent.add_galaxy(misp_galaxy) + self.assertEqual(self.mispevent.galaxies[0].to_json(sort_keys=True, indent=2), json.dumps(galaxy, sort_keys=True, indent=2)) + + def test_attribute(self) -> None: + self.init_event() + a: MISPAttribute = self.mispevent.add_attribute('filename', 'bar.exe') # type: ignore[assignment] del a.uuid - a = self.mispevent.add_attribute_tag('osint', 'bar.exe') + a = self.mispevent.add_attribute_tag('osint', 'bar.exe') # type: ignore[assignment] attr_tags = self.mispevent.get_attribute_tag('bar.exe') self.assertEqual(self.mispevent.attributes[0].tags[0].name, 'osint') self.assertEqual(attr_tags[0].name, 'osint') - with open('tests/mispevent_testfiles/attribute.json', 'r') as f: + with open('tests/mispevent_testfiles/attribute.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) # Fake setting an attribute ID for testing self.mispevent.attributes[0].id = 42 - self.mispevent.delete_attribute(42) - with open('tests/mispevent_testfiles/attribute_del.json', 'r') as f: + self.mispevent.delete_attribute('42') + with open('tests/mispevent_testfiles/attribute_del.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_object_tag(self): + def test_attribute_galaxy(self) -> None: + self.init_event() + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + attribute = MISPAttribute() + attribute.from_dict(**{'type': 'github-username', 'value': 'adulau'}) + attribute.add_galaxy(misp_galaxy) + self.mispevent.add_attribute(**attribute) + self.assertEqual( + self.mispevent.attributes[0].galaxies[0].to_json(sort_keys=True, indent=2), + json.dumps(galaxy, sort_keys=True, indent=2) + ) + + def test_to_dict_json_format(self) -> None: + misp_event = MISPEvent() + av_signature_object = MISPObject("av-signature") + av_signature_object.add_attribute("signature", "EICAR") + av_signature_object.add_attribute("software", "ClamAv") + misp_event.add_object(av_signature_object) + + self.assertEqual(json.loads(misp_event.to_json()), misp_event.to_dict(json_format=True)) + + def test_object_tag(self) -> None: self.mispevent.add_object(name='file', strict=True) - a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) + a: MISPAttribute = self.mispevent.objects[0].add_attribute('filename', value='') # type: ignore[assignment] + self.assertEqual(a, None) + a = self.mispevent.objects[0].add_attribute('filename', value=None) # type: ignore[assignment] + self.assertEqual(a, None) + a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) # type: ignore[assignment] del a.uuid self.assertEqual(self.mispevent.objects[0].attributes[0].tags[0].name, 'blah') self.assertTrue(self.mispevent.objects[0].has_attributes_by_relation(['filename'])) self.assertEqual(len(self.mispevent.objects[0].get_attributes_by_relation('filename')), 1) self.mispevent.add_object(name='url', strict=True) - a = self.mispevent.objects[1].add_attribute('url', value='https://www.circl.lu') + a = self.mispevent.objects[1].add_attribute('url', value='https://www.circl.lu') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' self.mispevent.objects[1].uuid = 'b' - self.mispevent.objects[0].add_reference('b', 'baz', comment='foo') + reference = self.mispevent.objects[0].add_reference(self.mispevent.objects[1], 'baz', comment='foo') + del reference.uuid self.assertEqual(self.mispevent.objects[0].references[0].relationship_type, 'baz') - with open('tests/mispevent_testfiles/event_obj_attr_tag.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_attr_tag.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) @unittest.skip("Not supported on MISP: https://github.com/MISP/MISP/issues/2638 - https://github.com/MISP/PyMISP/issues/168") - def test_object_level_tag(self): + def test_object_level_tag(self) -> None: self.mispevent.add_object(name='file', strict=True) self.mispevent.objects[0].add_attribute('filename', value='bar') - self.mispevent.objects[0].add_tag('osint') + self.mispevent.objects[0].add_tag('osint') # type: ignore[attr-defined] self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/event_obj_tag.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_tag.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_malware(self): + def test_object_galaxy(self) -> None: + self.init_event() + misp_object = MISPObject('github-user') + misp_object.add_attribute('username', 'adulau') + misp_object.add_attribute('repository', 'cve-search') + self.mispevent.add_object(misp_object) + with open('tests/mispevent_testfiles/galaxy.json') as f: + galaxy = json.load(f) + misp_galaxy = MISPGalaxy() + misp_galaxy.from_dict(**galaxy) + self.mispevent.objects[0].attributes[0].add_galaxy(misp_galaxy) + self.assertEqual( + self.mispevent.objects[0].attributes[0].galaxies[0].to_json(sort_keys=True, indent=2), + json.dumps(galaxy, sort_keys=True, indent=2) + ) + + def test_malware(self) -> None: with open('tests/mispevent_testfiles/simple.json', 'rb') as f: pseudofile = BytesIO(f.read()) self.init_event() - a = self.mispevent.add_attribute('malware-sample', 'bar.exe', data=pseudofile) + a: MISPAttribute = self.mispevent.add_attribute('malware-sample', 'bar.exe', data=pseudofile) # type: ignore[assignment] del a.uuid attribute = self.mispevent.attributes[0] self.assertEqual(attribute.malware_binary, pseudofile) - with open('tests/mispevent_testfiles/malware.json', 'r') as f: + with open('tests/mispevent_testfiles/malware.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_existing_malware(self): + def test_existing_malware(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/malware_exist.json') with open('tests/mispevent_testfiles/simple.json', 'rb') as f: pseudofile = BytesIO(f.read()) - self.assertEqual( - self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary.read(), - pseudofile.read()) + self.assertTrue(self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary) + if _mb := self.mispevent.objects[0].get_attributes_by_relation('malware-sample')[0].malware_binary: + self.assertEqual(_mb.read(), pseudofile.read()) - def test_sighting(self): + def test_sighting(self) -> None: sighting = MISPSighting() sighting.from_dict(value='1', type='bar', timestamp=11111111) - with open('tests/mispevent_testfiles/sighting.json', 'r') as f: + with open('tests/mispevent_testfiles/sighting.json') as f: ref_json = json.load(f) - self.assertEqual(sighting.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(sighting.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_existing_event(self): + def test_existing_event(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') - with open('tests/mispevent_testfiles/existing_event.json', 'r') as f: + with open('tests/mispevent_testfiles/existing_event.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_shadow_attributes_existing(self): + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) + + def test_shadow_attributes_existing(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/shadow.json') - with open('tests/mispevent_testfiles/shadow.json', 'r') as f: + with open('tests/mispevent_testfiles/shadow.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_shadow_attributes(self): + @unittest.skip("Not supported on MISP.") + def test_shadow_attributes(self) -> None: self.init_event() p = self.mispevent.add_proposal(type='filename', value='baz.jpg') del p.uuid - a = self.mispevent.add_attribute('filename', 'bar.exe') + a: MISPAttribute = self.mispevent.add_attribute('filename', 'bar.exe') # type: ignore[assignment] del a.uuid p = self.mispevent.attributes[0].add_proposal(type='filename', value='bar.pdf') del p.uuid - with open('tests/mispevent_testfiles/proposals.json', 'r') as f: + with open('tests/mispevent_testfiles/proposals.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_default_attributes(self): + def test_default_attributes(self) -> None: self.mispevent.add_object(name='file', strict=True) - a = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) + a: MISPAttribute = self.mispevent.objects[0].add_attribute('filename', value='bar', Tag=[{'name': 'blah'}]) # type: ignore[assignment] + del a.uuid + a = self.mispevent.objects[0].add_attribute('pattern-in-file', value='baz') # type: ignore[assignment] + self.assertEqual(a.category, 'Artifacts dropped') del a.uuid self.mispevent.add_object(name='file', strict=False, default_attributes_parameters=self.mispevent.objects[0].attributes[0]) - a = self.mispevent.objects[1].add_attribute('filename', value='baz') + a = self.mispevent.objects[1].add_attribute('filename', value='baz') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' self.mispevent.objects[1].uuid = 'b' - with open('tests/mispevent_testfiles/event_obj_def_param.json', 'r') as f: + with open('tests/mispevent_testfiles/event_obj_def_param.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_obj_default_values(self): + def test_obj_default_values(self) -> None: self.init_event() self.mispevent.add_object(name='whois', strict=True) - a = self.mispevent.objects[0].add_attribute('registrar', value='registar.example.com') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('registrar', value='registar.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('domain', value='domain.example.com') + a = self.mispevent.objects[0].add_attribute('domain', value='domain.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('nameserver', value='ns1.example.com') + a = self.mispevent.objects[0].add_attribute('nameserver', value='ns1.example.com') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('nameserver', value='ns2.example.com', disable_correlation=False, to_ids=True, category='External analysis') + a = self.mispevent.objects[0].add_attribute('nameserver', value='ns2.example.com', disable_correlation=False, to_ids=True, category='External analysis') # type: ignore[assignment] del a.uuid self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/def_param.json', 'r') as f: + with open('tests/mispevent_testfiles/def_param.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_event_not_edited(self): + def test_obj_references_export(self) -> None: + self.init_event() + obj1 = MISPObject(name="file") + obj2 = MISPObject(name="url", standalone=False) + obj1.add_reference(obj2, "downloads") + obj2.add_reference(obj1, "downloaded-by") + self.assertFalse("ObjectReference" in obj1.jsonable()) + self.assertTrue("ObjectReference" in obj2.jsonable()) + self.mispevent.add_object(obj1) + obj2.standalone = True + self.assertTrue("ObjectReference" in obj1.jsonable()) + self.assertFalse("ObjectReference" in obj2.jsonable()) + + def test_event_not_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) - def test_event_edited(self): + def test_event_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.mispevent.info = 'blah' self.assertTrue(self.mispevent.edited) - def test_event_tag_edited(self): + def test_event_tag_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.add_tag('foo') self.assertTrue(self.mispevent.edited) - def test_event_attribute_edited(self): + def test_event_attribute_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.mispevent.attributes[0].value = 'blah' self.assertTrue(self.mispevent.attributes[0].edited) self.assertFalse(self.mispevent.attributes[1].edited) self.assertTrue(self.mispevent.edited) - def test_event_attribute_tag_edited(self): + def test_event_attribute_tag_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.attributes[0].tags[0].name = 'blah' @@ -209,14 +303,14 @@ class TestMISPEvent(unittest.TestCase): self.assertTrue(self.mispevent.attributes[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_attribute_tag_edited_second(self): + def test_event_attribute_tag_edited_second(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.attributes[0].add_tag(name='blah') self.assertTrue(self.mispevent.attributes[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_edited(self): + def test_event_object_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].comment = 'blah' @@ -224,7 +318,7 @@ class TestMISPEvent(unittest.TestCase): self.assertFalse(self.mispevent.objects[1].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_attribute_edited(self): + def test_event_object_attribute_edited(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].attributes[0].comment = 'blah' @@ -232,55 +326,147 @@ class TestMISPEvent(unittest.TestCase): self.assertTrue(self.mispevent.objects[0].edited) self.assertTrue(self.mispevent.edited) - def test_event_object_attribute_edited_tag(self): + def test_event_object_attribute_edited_tag(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') self.assertFalse(self.mispevent.edited) self.mispevent.objects[0].attributes[0].add_tag('blah') self.assertTrue(self.mispevent.objects[0].attributes[0].edited) self.assertTrue(self.mispevent.objects[0].edited) self.assertTrue(self.mispevent.edited) - with open('tests/mispevent_testfiles/existing_event_edited.json', 'r') as f: + with open('tests/mispevent_testfiles/existing_event_edited.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) - def test_obj_by_id(self): + def test_obj_by_id(self) -> None: self.mispevent.load_file('tests/mispevent_testfiles/existing_event.json') misp_obj = self.mispevent.get_object_by_id(1556) self.assertEqual(misp_obj.uuid, '5a3cd604-e11c-4de5-bbbf-c170950d210f') - def test_userdefined_object(self): + def test_userdefined_object_custom_template(self) -> None: self.init_event() - self.mispevent.add_object(name='test_object_template', strict=True, misp_objects_path_custom='tests/mispevent_testfiles') + with open('tests/mispevent_testfiles/test_object_template/definition.json') as f: + template = json.load(f) + self.mispevent.add_object(name='test_object_template', strict=True, + misp_objects_template_custom=template) with self.assertRaises(InvalidMISPObject) as e: # Fail on required - self.mispevent.to_json() - if sys.version_info >= (3, ): - self.assertEqual(e.exception.message, '{\'member3\'} are required.') - else: - # Python2 bullshit - self.assertEqual(e.exception.message, 'set([u\'member3\']) are required.') + self.mispevent.to_json(sort_keys=True, indent=2) + self.assertEqual(e.exception.message, '{\'member3\'} are required.') - a = self.mispevent.objects[0].add_attribute('member3', value='foo') + a: MISPAttribute = self.mispevent.objects[0].add_attribute('member3', value='foo') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # Fail on requiredOneOf - self.mispevent.to_json() + self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, 'At least one of the following attributes is required: member1, member2') - a = self.mispevent.objects[0].add_attribute('member1', value='bar') + a = self.mispevent.objects[0].add_attribute('member1', value='bar') # type: ignore[assignment] del a.uuid - a = self.mispevent.objects[0].add_attribute('member1', value='baz') + a = self.mispevent.objects[0].add_attribute('member1', value='baz') # type: ignore[assignment] del a.uuid with self.assertRaises(InvalidMISPObject) as e: # member1 is not a multiple - self.mispevent.to_json() + self.mispevent.to_json(sort_keys=True, indent=2) self.assertEqual(e.exception.message, 'Multiple occurrences of member1 is not allowed') self.mispevent.objects[0].attributes = self.mispevent.objects[0].attributes[:2] self.mispevent.objects[0].uuid = 'a' - with open('tests/mispevent_testfiles/misp_custom_obj.json', 'r') as f: + with open('tests/mispevent_testfiles/misp_custom_obj.json') as f: ref_json = json.load(f) - self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2)) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) + + def test_userdefined_object_custom_dir(self) -> None: + self.init_event() + self.mispevent.add_object(name='test_object_template', strict=True, misp_objects_path_custom='tests/mispevent_testfiles') + with self.assertRaises(InvalidMISPObject) as e: + # Fail on required + self.mispevent.to_json(sort_keys=True, indent=2) + self.assertEqual(e.exception.message, '{\'member3\'} are required.') + + a: MISPAttribute = self.mispevent.objects[0].add_attribute('member3', value='foo') # type: ignore[assignment] + del a.uuid + with self.assertRaises(InvalidMISPObject) as e: + # Fail on requiredOneOf + self.mispevent.to_json(sort_keys=True, indent=2) + self.assertEqual(e.exception.message, 'At least one of the following attributes is required: member1, member2') + + a = self.mispevent.objects[0].add_attribute('member1', value='bar') # type: ignore[assignment] + del a.uuid + a = self.mispevent.objects[0].add_attribute('member1', value='baz') # type: ignore[assignment] + del a.uuid + with self.assertRaises(InvalidMISPObject) as e: + # member1 is not a multiple + self.mispevent.to_json(sort_keys=True, indent=2) + self.assertEqual(e.exception.message, 'Multiple occurrences of member1 is not allowed') + + self.mispevent.objects[0].attributes = self.mispevent.objects[0].attributes[:2] + self.mispevent.objects[0].uuid = 'a' + with open('tests/mispevent_testfiles/misp_custom_obj.json') as f: + ref_json = json.load(f) + del self.mispevent.uuid + self.assertEqual(self.mispevent.to_json(sort_keys=True, indent=2), json.dumps(ref_json, sort_keys=True, indent=2)) + + def test_first_last_seen(self) -> None: + me = MISPEvent() + me.info = 'Test First and Last Seen' + me.date = '2020.01.12' # type: ignore[assignment] + self.assertEqual(me.date.day, 12) + me.add_attribute('ip-dst', '8.8.8.8', first_seen='06-21-1998', last_seen=1580213607.469571) + self.assertEqual(me.attributes[0].first_seen.year, 1998) + self.assertEqual(me.attributes[0].last_seen.year, 2020) + now = datetime.now().astimezone() + me.attributes[0].last_seen = now + today = date.today() + me.attributes[0].first_seen = today # type: ignore[assignment] + self.assertEqual(me.attributes[0].first_seen.year, today.year) + self.assertEqual(me.attributes[0].last_seen, now) + + def test_feed(self) -> None: + me = MISPEvent() + me.info = 'Test feed' + org = MISPOrganisation() + org.name = 'TestOrg' + org.uuid = '123478' + me.Orgc = org + me.add_attribute('ip-dst', '8.8.8.8') + obj = me.add_object(name='file') + obj.add_attributes('filename', *['foo.exe', 'bar.exe']) + h = hashlib.new('md5') + h.update(b'8.8.8.8') + hash_attr_val = h.hexdigest() + feed = me.to_feed(with_meta=True) + self.assertEqual(feed['Event']['_hashes'][0], hash_attr_val) + self.assertEqual(feed['Event']['_manifest'][me.uuid]['info'], 'Test feed') + self.assertEqual(len(feed['Event']['Object'][0]['Attribute']), 2) + + def test_object_templates(self) -> None: + me = MISPEvent() + for template in glob.glob(str(me.misp_objects_path / '*' / 'definition.json')): + with open(template) as f: + t_json = json.load(f) + if 'requiredOneOf' in t_json: + obj_relations = set(t_json['attributes'].keys()) + subset = set(t_json['requiredOneOf']).issubset(obj_relations) + self.assertTrue(subset, f'{t_json["name"]}') + if 'required' in t_json: + obj_relations = set(t_json['attributes'].keys()) + subset = set(t_json['required']).issubset(obj_relations) + self.assertTrue(subset, f'{t_json["name"]}') + for obj_relation, entry in t_json['attributes'].items(): + self.assertTrue(entry['misp-attribute'] in me.describe_types['types'], f'Missing type: {entry["misp-attribute"]}') + if 'categories' in entry: + subset = set(entry['categories']).issubset(me.describe_types['categories']) + self.assertTrue(subset, f'{t_json["name"]} - {obj_relation}') + + def test_git_vuln_finder(self) -> None: + with open('tests/git-vuln-finder-quagga.json') as f: + dump = json.load(f) + + for vuln in dump.values(): + author = vuln['author'] + vuln_finder = GitVulnFinderObject(vuln) + self.assertEqual(vuln_finder.get_attributes_by_relation('author')[0].value, author) if __name__ == '__main__': diff --git a/tests/test_offline.py b/tests/test_offline.py deleted file mode 100644 index 2a5178f..0000000 --- a/tests/test_offline.py +++ /dev/null @@ -1,480 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import unittest -import requests_mock -import json -import os -import sys -from io import BytesIO - -import pymisp as pm -from pymisp import PyMISP -# from pymisp import NewEventError -from pymisp import MISPEvent -from pymisp import MISPEncode - -from pymisp.tools import make_binary_objects - - -class MockPyMISP(PyMISP): - def _send_attributes(self, event, attributes, proposal=False): - return attributes - - -@requests_mock.Mocker() -class TestOffline(unittest.TestCase): - - def setUp(self): - self.maxDiff = None - self.domain = 'http://misp.local/' - self.key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - with open('tests/misp_event.json', 'r') as f: - self.event = {'Event': json.load(f)} - with open('tests/new_misp_event.json', 'r') as f: - self.new_misp_event = {'Event': json.load(f)} - self.resources_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../pymisp/data') - with open(os.path.join(self.resources_path, 'describeTypes.json'), 'r') as f: - self.types = json.load(f) - with open('tests/sharing_groups.json', 'r') as f: - self.sharing_groups = json.load(f) - self.auth_error_msg = {"name": "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header.", - "message": "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header.", - "url": "\/events\/1"} - with open('tests/search_index_result.json', 'r') as f: - self.search_index_result = json.load(f) - - def initURI(self, m): - m.register_uri('GET', self.domain + 'events/1', json=self.auth_error_msg, status_code=403) - m.register_uri('GET', self.domain + 'servers/getVersion.json', json={"version": "2.4.62"}) - m.register_uri('GET', self.domain + 'servers/getPyMISPVersion.json', json={"version": "2.4.62"}) - m.register_uri('GET', self.domain + 'sharing_groups.json', json=self.sharing_groups) - m.register_uri('GET', self.domain + 'attributes/describeTypes.json', json=self.types) - m.register_uri('GET', self.domain + 'events/2', json=self.event) - m.register_uri('POST', self.domain + 'events/5758ebf5-c898-48e6-9fe9-5665c0a83866', json=self.event) - m.register_uri('DELETE', self.domain + 'events/2', json={'message': 'Event deleted.'}) - m.register_uri('DELETE', self.domain + 'events/3', json={'errors': ['Invalid event'], 'message': 'Invalid event', 'name': 'Invalid event', 'url': '/events/3'}) - m.register_uri('GET', self.domain + 'attributes/delete/2', json={'message': 'Attribute deleted.'}) - m.register_uri('POST', self.domain + 'events/index', json=self.search_index_result) - m.register_uri('POST', self.domain + 'attributes/edit/' + self.key, json={}) - m.register_uri('GET', self.domain + 'shadow_attributes/view/None', json={}) - m.register_uri('GET', self.domain + 'shadow_attributes/view/1', json={}) - m.register_uri('POST', self.domain + 'events/freeTextImport/1', json={}) - m.register_uri('POST', self.domain + 'attributes/restSearch', json={}) - m.register_uri('POST', self.domain + 'attributes/downloadSample', json={}) - m.register_uri('GET', self.domain + 'tags', json={'Tag': 'foo'}) - m.register_uri('POST', self.domain + 'events/upload_sample/1', json={}) - m.register_uri('POST', self.domain + 'tags/attachTagToObject', json={}) - m.register_uri('POST', self.domain + 'tags/removeTagFromObject', json={}) - - def test_getEvent(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - e1 = pymisp.get_event(2) - e2 = pymisp.get(2) - self.assertEqual(e1, e2) - self.assertEqual(self.event, e2) - - def test_updateEvent(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - e0 = pymisp.update_event('5758ebf5-c898-48e6-9fe9-5665c0a83866', json.dumps(self.event)) - e1 = pymisp.update_event('5758ebf5-c898-48e6-9fe9-5665c0a83866', self.event) - self.assertEqual(e0, e1) - e2 = pymisp.update(e0) - self.assertEqual(e1, e2) - self.assertEqual(self.event, e2) - - def test_deleteEvent(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - d = pymisp.delete_event(2) - self.assertEqual(d, {'message': 'Event deleted.'}) - d = pymisp.delete_event(3) - self.assertEqual(d, {'errors': ['Invalid event'], 'message': 'Invalid event', 'name': 'Invalid event', 'url': '/events/3'}) - - def test_deleteAttribute(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - d = pymisp.delete_attribute(2) - self.assertEqual(d, {'message': 'Attribute deleted.'}) - - def test_getVersions(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - api_version = pymisp.get_api_version() - self.assertEqual(api_version, {'version': pm.__version__}) - server_version = pymisp.get_version() - self.assertEqual(server_version, {"version": "2.4.62"}) - - def test_getSharingGroups(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - sharing_groups = pymisp.get_sharing_groups() - print(sharing_groups) - self.assertEqual(sharing_groups['response'][0], self.sharing_groups[0]) - - def test_auth_error(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - error = pymisp.get(1) - response = self.auth_error_msg - response['errors'] = [response['message']] - self.assertEqual(error, response) - - def test_newEvent(self, m): - error_empty_info = {'message': 'The event could not be saved.', - 'name': 'Add event failed.', - 'errors': ['Error in info: Info cannot be empty.'], - 'url': '/events/add'} - error_empty_info_flatten = {u'message': u'The event could not be saved.', - u'name': u'Add event failed.', - u'errors': [u"Error in info: Info cannot be empty."], - u'url': u'/events/add'} - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - m.register_uri('POST', self.domain + 'events', json=error_empty_info) - # TODO Add test exception if info field isn't set - response = pymisp.new_event(0, 1, 0, 'Foo') - self.assertEqual(response, error_empty_info_flatten) - m.register_uri('POST', self.domain + 'events', json=self.new_misp_event) - response = pymisp.new_event(0, 1, 0, "This is a test.", '2016-08-26', False) - self.assertEqual(response, self.new_misp_event) - - def test_eventObject(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - misp_event = MISPEvent(pymisp.describe_types) - with open('tests/57c4445b-c548-4654-af0b-4be3950d210f.json', 'r') as f: - misp_event.load(f.read()) - json.dumps(misp_event, cls=MISPEncode) - - def test_searchIndexByTagId(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - response = pymisp.search_index(tag="1") - self.assertEqual(response['response'], self.search_index_result) - - def test_searchIndexByTagName(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - response = pymisp.search_index(tag='ecsirt:malicious-code="ransomware"') - self.assertEqual(response['response'], self.search_index_result) - - def add_hashes(self, event, mock): - """ - Regression tests for #174 - """ - hashes_fname = mock.add_hashes(event, - md5='68b329da9893e34099c7d8ad5cb9c940', - sha1='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - sha256='01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b', - filename='foobar.exe') - self.assertEqual(3, len(hashes_fname)) - for attr in hashes_fname: - self.assertTrue(isinstance(attr, pm.mispevent.MISPAttribute)) - self.assertIn("filename|", attr["type"]) - - hashes_only = mock.add_hashes(event, md5='68b329da9893e34099c7d8ad5cb9c940', - sha1='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - sha256='01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b') - self.assertEqual(3, len(hashes_only)) - for attr in hashes_only: - self.assertTrue(isinstance(attr, pm.mispevent.MISPAttribute)) - self.assertNotIn("filename|", attr["type"]) - - def add_regkeys(self, event, mock): - regkeys = { - 'HKLM\\Software\\Microsoft\\Outlook\\Addins\\foo': None, - 'HKLM\\Software\\Microsoft\\Outlook\\Addins\\bar': 'baz', - 'HKLM\\Software\\Microsoft\\Outlook\\Addins\\bae': 0, - } - reg_attr = mock.add_regkeys(event, regkeys) - self.assertEqual(3, len(reg_attr)) - for attr in reg_attr: - self.assertTrue(isinstance(attr, pm.mispevent.MISPAttribute)) - self.assertIn("regkey", attr["type"]) - - key = mock.add_regkey(event, 'HKLM\\Software\\Microsoft\\Outlook\\Addins\\foobar') - self.assertEqual(len(key), 1) - self.assertEqual(key[0]["type"], "regkey") - - key = mock.add_regkey(event, 'HKLM\\Software\\Microsoft\\Outlook\\Addins\\foobar', rvalue='foobar') - self.assertEqual(len(key), 1) - self.assertEqual(key[0]["type"], "regkey|value") - self.assertIn("foobar|foobar", key[0]["value"]) - - def test_addAttributes(self, m): - self.initURI(m) - p = MockPyMISP(self.domain, self.key) - evt = p.get(1) - - self.add_hashes(evt, p) - self.add_regkeys(evt, p) - - p.av_detection_link(evt, 'https://foocorp.com') - p.add_detection_name(evt, 'WATERMELON') - p.add_filename(evt, 'foobar.exe') - p.add_pattern(evt, '.*foobar.*', in_memory=True) - p.add_pattern(evt, '.*foobar.*', in_file=True) - p.add_mutex(evt, 'foo') - p.add_pipe(evt, 'foo') - p.add_pipe(evt, '\\.\\pipe\\foo') - - self.assertRaises(pm.PyMISPError, p.add_pattern, evt, '.*foobar.*', in_memory=False, in_file=False) - - self.assertEqual(3, len(p.add_pipe(evt, ['foo', 'bar', 'baz']))) - self.assertEqual(3, len(p.add_pipe(evt, ['foo', 'bar', '\\.\\pipe\\baz']))) - - self.assertEqual(1, len(p.add_mutex(evt, '\\BaseNamedObjects\\foo'))) - self.assertEqual(3, len(p.add_mutex(evt, ['foo', 'bar', 'baz']))) - self.assertEqual(3, len(p.add_mutex(evt, ['foo', 'bar', '\\BaseNamedObjects\\baz']))) - p.add_yara(evt, 'rule Foo {}') - self.assertEqual(2, len(p.add_yara(evt, ['rule Foo {}', 'rule Bar {}']))) - p.add_ipdst(evt, '1.2.3.4') - self.assertEqual(2, len(p.add_ipdst(evt, ['1.2.3.4', '5.6.7.8']))) - p.add_ipsrc(evt, '1.2.3.4') - self.assertEqual(2, len(p.add_ipsrc(evt, ['1.2.3.4', '5.6.7.8']))) - p.add_hostname(evt, 'a.foobar.com') - self.assertEqual(2, len(p.add_hostname(evt, ['a.foobar.com', 'a.foobaz.com']))) - p.add_domain(evt, 'foobar.com') - self.assertEqual(2, len(p.add_domain(evt, ['foobar.com', 'foobaz.com']))) - p.add_domain_ip(evt, 'foo.com', '1.2.3.4') - self.assertEqual(2, len(p.add_domain_ip(evt, 'foo.com', ['1.2.3.4', '5.6.7.8']))) - self.assertEqual(2, len(p.add_domains_ips(evt, {'foo.com': '1.2.3.4', 'bar.com': '4.5.6.7'}))) - - p.add_url(evt, 'https://example.com') - self.assertEqual(2, len(p.add_url(evt, ['https://example.com', 'http://foo.com']))) - - p.add_useragent(evt, 'Mozilla') - self.assertEqual(2, len(p.add_useragent(evt, ['Mozilla', 'Godzilla']))) - - p.add_traffic_pattern(evt, 'blabla') - p.add_snort(evt, 'blaba') - p.add_net_other(evt, 'blabla') - p.add_email_src(evt, 'foo@bar.com') - p.add_email_dst(evt, 'foo@bar.com') - p.add_email_subject(evt, 'you won the lottery') - p.add_email_attachment(evt, 'foo.doc') - p.add_target_email(evt, 'foo@bar.com') - p.add_target_user(evt, 'foo') - p.add_target_machine(evt, 'foobar') - p.add_target_org(evt, 'foobar') - p.add_target_location(evt, 'foobar') - p.add_target_external(evt, 'foobar') - p.add_threat_actor(evt, 'WATERMELON') - p.add_internal_link(evt, 'foobar') - p.add_internal_comment(evt, 'foobar') - p.add_internal_text(evt, 'foobar') - p.add_internal_other(evt, 'foobar') - p.add_attachment(evt, "testFile") - - def make_objects(self, path=None, pseudofile=None, filename=None): - to_return = {'objects': [], 'references': []} - if path: - fo, peo, seos = make_binary_objects(path) - else: - fo, peo, seos = make_binary_objects(pseudofile=pseudofile, filename=filename) - - if seos: - for s in seos: - for a in s.attributes: - del a.uuid - to_return['objects'].append(s) - if s.ObjectReference: - to_return['references'] += s.ObjectReference - - if peo: - for a in peo.attributes: - del a.uuid - to_return['objects'].append(peo) - if peo.ObjectReference: - to_return['references'] += peo.ObjectReference - - if fo: - for a in fo.attributes: - del a.uuid - to_return['objects'].append(fo) - if fo.ObjectReference: - to_return['references'] += fo.ObjectReference - - # Remove UUIDs for comparing the objects. - for o in to_return['objects']: - o.pop('uuid') - for o in to_return['references']: - o.pop('referenced_uuid') - o.pop('object_uuid') - return json.dumps(to_return, cls=MISPEncode) - - def test_objects_pseudofile(self, m): - if sys.version_info < (3, 0): - return unittest.SkipTest() - paths = ['cmd.exe', 'tmux', 'MachO-OSX-x64-ls'] - try: - for path in paths: - with open(os.path.join('tests', 'viper-test-files', 'test_files', path), 'rb') as f: - pseudo = BytesIO(f.read()) - json_blob = self.make_objects(pseudofile=pseudo, filename=path) - # Compare pseudo file / path - filepath_blob = self.make_objects(os.path.join('tests', 'viper-test-files', 'test_files', path)) - self.assertEqual(json_blob, filepath_blob) - except IOError: # Can be replaced with FileNotFoundError when support for python 2 is dropped - return unittest.SkipTest() - print(json_blob) - - def test_objects(self, m): - paths = ['cmd.exe', 'tmux', 'MachO-OSX-x64-ls'] - try: - for path in paths: - json_blob = self.make_objects(os.path.join('tests', - 'viper-test-files', 'test_files', path)) - except IOError: # Can be replaced with FileNotFoundError when support for python 2 is dropped - return unittest.SkipTest() - print(json_blob) - - def test_describeTypes_sane_default(self, m): - sane_default = self.types['result']['sane_defaults'] - self.assertEqual(sorted(sane_default.keys()), sorted(self.types['result']['types'])) - - def test_describeTypes_categories(self, m): - category_type_mappings = self.types['result']['category_type_mappings'] - self.assertEqual(sorted(category_type_mappings.keys()), sorted(self.types['result']['categories'])) - - def test_describeTypes_types_in_categories(self, m): - category_type_mappings = self.types['result']['category_type_mappings'] - for category, types in category_type_mappings.items(): - existing_types = [t for t in types if t in self.types['result']['types']] - self.assertEqual(sorted(existing_types), sorted(types)) - - def test_describeTypes_types_have_category(self, m): - category_type_mappings = self.types['result']['category_type_mappings'] - all_types = set() - for category, types in category_type_mappings.items(): - all_types.update(types) - self.assertEqual(sorted(list(all_types)), sorted(self.types['result']['types'])) - - def test_describeTypes_sane_default_valid_category(self, m): - sane_default = self.types['result']['sane_defaults'] - categories = self.types['result']['categories'] - for t, sd in sane_default.items(): - self.assertTrue(sd['to_ids'] in [0, 1]) - self.assertTrue(sd['default_category'] in categories) - - def test_flatten_error_messages_singular(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - pymisp.get(1) - response = self.auth_error_msg - response['error'] = ['foo', 'bar', 'baz'] - messages = pymisp.flatten_error_messages(response) - self.assertEqual(["foo", "bar", "baz"], messages) - - def test_flatten_error_messages_plural(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - error = pymisp.get(1) - self.assertIn("Authentication failed", error["message"]) - response = self.auth_error_msg - response['errors'] = {'foo': 42, 'bar': False, 'baz': ['oo', 'ka']} - messages = pymisp.flatten_error_messages(response) - self.assertEqual(set(['42 (foo)', 'False (bar)', 'oo', 'ka']), set(messages)) - - def test_flatten_error_messages_nested(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - error = pymisp.get(1) - self.assertIn("Authentication failed", error["message"]) - response = self.auth_error_msg - response['errors'] = { - 'fo': {'o': 42}, 'ba': {'r': True}, 'b': {'a': ['z']}, 'd': {'e': {'e': ['p']}}} - messages = pymisp.flatten_error_messages(response) - self.assertEqual(set(['Error in o: 42', 'Error in r: True', 'Error in a: z', "Error in e: {'e': ['p']}"]), set(messages)) - - def test_test_connection(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertTrue(pymisp.test_connection()) - - def test_change_toids(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual({}, pymisp.change_toids(self.key, 1)) - - def test_change_toids_invalid(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - try: - pymisp.change_toids(self.key, 42) - self.assertFalse('Exception required for off domain value') - except Exception: - pass - - def test_proposal_view_default(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual({}, pymisp.proposal_view()) - - def test_proposal_view_event_1(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual({}, pymisp.proposal_view(event_id=1)) - - def test_proposal_view_event_overdetermined(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertTrue(pymisp.proposal_view(event_id=1, proposal_id=42).get('error') is not None) - - def test_freetext(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual({}, pymisp.freetext(1, 'foo', adhereToWarninglists=True, distribution=42)) - - def test_freetext_offdomain(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - try: - pymisp.freetext(1, None, adhereToWarninglists='hard') - self.assertFalse('Exception required for off domain value') - except Exception: - pass - - def test_get_yara(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual((False, None), pymisp.get_yara(1)) - - def test_download_samples(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual((False, None), pymisp.download_samples()) - - def test_sample_upload(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - pymisp.upload_sample("tmux", "tests/viper-test-files/test_files/tmux", 1) - pymisp.upload_sample("tmux", "non_existing_file", 1) - pymisp.upload_sample("tmux", b"binblob", 1) - - def test_get_all_tags(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - self.assertEqual({'Tag': 'foo'}, pymisp.get_all_tags()) - - def test_tag_event(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - uuid = self.event["Event"]["uuid"] - pymisp.tag(uuid, "foo") - - self.assertRaises(pm.PyMISPError, pymisp.tag, "test_uuid", "foo") - self.assertRaises(pm.PyMISPError, pymisp.tag, uuid.replace("a", "z"), "foo") - - def test_untag_event(self, m): - self.initURI(m) - pymisp = PyMISP(self.domain, self.key) - uuid = self.event["Event"]["uuid"] - pymisp.untag(uuid, "foo") - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_reportlab.py b/tests/test_reportlab.py index d404b9d..d3b589b 100644 --- a/tests/test_reportlab.py +++ b/tests/test_reportlab.py @@ -1,28 +1,29 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- + +from __future__ import annotations import os import sys import time import unittest -from pymisp import MISPEvent - +import logging +logging.disable(logging.CRITICAL) manual_testing = False try: + from pymisp import MISPEvent from pymisp.tools import reportlab_generator + reportlab_missing = False except Exception: - if sys.version_info < (3, 6): - print('This test suite requires Python 3.6+, breaking.') - sys.exit(0) - else: - raise + reportlab_missing = True class TestPDFExport(unittest.TestCase): def setUp(self): + if reportlab_missing: + self.skipTest('reportlab missing, skip test.') self.maxDiff = None self.mispevent = MISPEvent() if not manual_testing: diff --git a/tests/testlive_comprehensive.py b/tests/testlive_comprehensive.py index 8bb9ebc..31990f2 100644 --- a/tests/testlive_comprehensive.py +++ b/tests/testlive_comprehensive.py @@ -1,82 +1,138 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys +from __future__ import annotations +import hashlib +import json +import logging +import os +import time import unittest -from pymisp.tools import make_binary_objects -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta, date, timezone from io import BytesIO -import re -import json - -import time +from pathlib import Path +from typing import TypeVar, Any from uuid import uuid4 -import logging -logging.disable(logging.CRITICAL) +import urllib3 + +from pymisp.tools import make_binary_objects try: - from pymisp import ExpandedPyMISP, MISPEvent, MISPOrganisation, MISPUser, Distribution, ThreatLevel, Analysis, MISPObject + from pymisp import (register_user, PyMISP, MISPEvent, MISPOrganisation, + MISPUser, Distribution, ThreatLevel, Analysis, MISPObject, + MISPAttribute, MISPSighting, MISPShadowAttribute, MISPTag, + MISPSharingGroup, MISPFeed, MISPServer, MISPUserSetting, + MISPEventReport, MISPCorrelationExclusion, MISPGalaxyCluster, + MISPGalaxy, MISPOrganisationBlocklist, MISPEventBlocklist, + MISPNote, MISPRole) + from pymisp.tools import CSVLoader, DomainIPObject, ASNObject, GenericObjectGenerator except ImportError: - if sys.version_info < (3, 6): - print('This test suite requires Python 3.6+, breaking.') - sys.exit(0) - else: - raise + raise try: - from keys import url, key + from keys import url, key # type: ignore verifycert = False - travis_run = True except ImportError as e: print(e) - url = 'http://localhost:8080' - key = 'HRizIMmaxBOXAQSzKZ874rDWUsQEk4vGAGBoljQO' + url = 'https://10.197.206.84' + key = 'OdzzuBSnH83tEjvZbf7SFejC1kC3gS11Cnj2wxLk' verifycert = False - travis_run = False + +logging.disable(logging.CRITICAL) +logger = logging.getLogger('pymisp') + +urllib3.disable_warnings() + +fast_mode = False + +test_file_path = Path('tests/viper-test-files') + +print(test_file_path, 'exists: ', test_file_path.exists()) + +if not test_file_path.exists(): + print('The test files are missing, pulling it.') + os.system('git clone https://github.com/viper-framework/viper-test-files.git tests/viper-test-files') + +T = TypeVar('T', bound='TestComprehensive') class TestComprehensive(unittest.TestCase): + admin_misp_connector: PyMISP + user_misp_connector: PyMISP + test_usr: MISPUser + test_pub: MISPUser + test_org: MISPOrganisation + test_org_delegate: MISPOrganisation + delegate_user_misp_connector: PyMISP + pub_misp_connector: PyMISP + test_usr_delegate: MISPUser + @classmethod - def setUpClass(cls): + def setUpClass(cls: type[T]) -> None: cls.maxDiff = None # Connect as admin - cls.admin_misp_connector = ExpandedPyMISP(url, key, verifycert, debug=False) + cls.admin_misp_connector = PyMISP(url, key, verifycert, debug=False) + cls.admin_misp_connector.set_server_setting('Security.allow_self_registration', True, force=True) + cls.admin_misp_connector.set_server_setting('debug', 1, force=True) + if not fast_mode: + r = cls.admin_misp_connector.update_misp() + print(r) # Creates an org - org = cls.admin_misp_connector.add_organisation(name='Test Org') - cls.test_org = MISPOrganisation() - cls.test_org.from_dict(**org) + organisation = MISPOrganisation() + organisation.name = 'Test Org' + cls.test_org = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) # type: ignore[assignment] + # Create an org to delegate to + organisation = MISPOrganisation() + organisation.name = 'Test Org - delegate' + cls.test_org_delegate = cls.admin_misp_connector.add_organisation(organisation, pythonify=True) # type: ignore[assignment] + # Set the refault role (id 3 on the VM) + cls.admin_misp_connector.set_default_role(3) # Creates a user - usr = cls.admin_misp_connector.add_user(email='testusr@user.local', org_id=cls.test_org.id, role_id=3) - cls.test_usr = MISPUser() - cls.test_usr.from_dict(**usr) - cls.user_misp_connector = ExpandedPyMISP(url, cls.test_usr.authkey, verifycert, debug=False) + user = MISPUser() + user.email = 'testusr@user.local' + user.org_id = cls.test_org.id + cls.test_usr = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] + cls.user_misp_connector = PyMISP(url, cls.test_usr.authkey, verifycert, debug=True) + cls.user_misp_connector.toggle_global_pythonify() # Creates a publisher - pub = cls.admin_misp_connector.add_user(email='testpub@user.local', org_id=cls.test_org.id, role_id=4) - cls.test_pub = MISPUser() - cls.test_pub.from_dict(**pub) - cls.pub_misp_connector = ExpandedPyMISP(url, cls.test_pub.authkey, verifycert) - # Update all json stuff - cls.admin_misp_connector.update_object_templates() - cls.admin_misp_connector.update_galaxies() - cls.admin_misp_connector.update_noticelists() - cls.admin_misp_connector.update_warninglists() - cls.admin_misp_connector.update_taxonomies() + user = MISPUser() + user.email = 'testpub@user.local' + user.org_id = cls.test_org.id + user.role_id = 4 + cls.test_pub = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] + cls.pub_misp_connector = PyMISP(url, cls.test_pub.authkey, verifycert) + # Creates a user that can accept a delegation request + user = MISPUser() + user.email = 'testusr@delegate.recipient.local' + user.org_id = cls.test_org_delegate.id + user.role_id = 2 + cls.test_usr_delegate = cls.admin_misp_connector.add_user(user, pythonify=True) # type: ignore[assignment] + cls.delegate_user_misp_connector = PyMISP(url, cls.test_usr_delegate.authkey, verifycert, debug=False) + cls.delegate_user_misp_connector.toggle_global_pythonify() + if not fast_mode: + # Update all json stuff + cls.admin_misp_connector.update_object_templates() + cls.admin_misp_connector.update_galaxies() + cls.admin_misp_connector.update_noticelists() + cls.admin_misp_connector.update_warninglists() + cls.admin_misp_connector.update_taxonomies() + cls.admin_misp_connector.load_default_feeds() @classmethod - def tearDownClass(cls): + def tearDownClass(cls) -> None: # Delete publisher - cls.admin_misp_connector.delete_user(user_id=cls.test_pub.id) + cls.admin_misp_connector.delete_user(cls.test_pub) # Delete user - cls.admin_misp_connector.delete_user(user_id=cls.test_usr.id) + cls.admin_misp_connector.delete_user(cls.test_usr) + cls.admin_misp_connector.delete_user(cls.test_usr_delegate) # Delete org - cls.admin_misp_connector.delete_organisation(org_id=cls.test_org.id) + cls.admin_misp_connector.delete_organisation(cls.test_org) + cls.admin_misp_connector.delete_organisation(cls.test_org_delegate) - def create_simple_event(self, force_timestamps=False): + def create_simple_event(self, force_timestamps: bool=False) -> MISPEvent: mispevent = MISPEvent(force_timestamps=force_timestamps) mispevent.info = 'This is a super simple test' mispevent.distribution = Distribution.your_organisation_only @@ -85,14 +141,14 @@ class TestComprehensive(unittest.TestCase): mispevent.add_attribute('text', str(uuid4())) return mispevent - def environment(self): + def environment(self) -> tuple[MISPEvent, MISPEvent, MISPEvent]: first_event = MISPEvent() first_event.info = 'First event - org only - low - completed' first_event.distribution = Distribution.your_organisation_only first_event.threat_level_id = ThreatLevel.low first_event.analysis = Analysis.completed first_event.set_date("2017-12-31") - first_event.add_attribute('text', str(uuid4())) + first_event.add_attribute('text', 'FIRST_EVENT' + str(uuid4())) first_event.attributes[0].add_tag('admin_only') first_event.attributes[0].add_tag('tlp:white___test') first_event.add_attribute('text', str(uuid4())) @@ -104,7 +160,7 @@ class TestComprehensive(unittest.TestCase): second_event.threat_level_id = ThreatLevel.medium second_event.analysis = Analysis.ongoing second_event.set_date("Aug 18 2018") - second_event.add_attribute('text', str(uuid4())) + second_event.add_attribute('text', 'SECOND_EVENT' + str(uuid4())) second_event.attributes[0].add_tag('tlp:white___test') second_event.add_attribute('ip-dst', '1.1.1.1') second_event.attributes[1].add_tag('tlp:amber___test') @@ -118,7 +174,7 @@ class TestComprehensive(unittest.TestCase): third_event.analysis = Analysis.initial third_event.set_date("Jun 25 2018") third_event.add_tag('tlp:white___test') - third_event.add_attribute('text', str(uuid4())) + third_event.add_attribute('text', 'THIRD_EVENT' + str(uuid4())) third_event.attributes[0].add_tag('tlp:amber___test') third_event.attributes[0].add_tag('foo_double___test') third_event.add_attribute('ip-src', '8.8.8.8') @@ -127,13 +183,38 @@ class TestComprehensive(unittest.TestCase): # Create first and third event as admin # usr won't be able to see the first one - first = self.admin_misp_connector.add_event(first_event) - third = self.admin_misp_connector.add_event(third_event) + first: MISPEvent = self.admin_misp_connector.add_event(first_event, pythonify=True) # type: ignore[assignment] + third: MISPEvent = self.admin_misp_connector.add_event(third_event, pythonify=True) # type: ignore[assignment] # Create second event as user - second = self.user_misp_connector.add_event(second_event) + second: MISPEvent = self.user_misp_connector.add_event(second_event) # type: ignore[assignment] return first, second, third - def test_search_value_event(self): + def test_server_settings(self) -> None: + settings = self.admin_misp_connector.server_settings() + for final_setting in settings['finalSettings']: + if final_setting['setting'] == 'MISP.max_correlations_per_event': + self.assertEqual(final_setting['value'], 5000) + break + r = self.admin_misp_connector.set_server_setting('MISP.max_correlations_per_event', 10) + self.assertEqual(r['message'], 'Field updated', r) + + setting = self.admin_misp_connector.get_server_setting('MISP.max_correlations_per_event') + self.assertEqual(setting['value'], 10) + r = self.admin_misp_connector.set_server_setting('MISP.max_correlations_per_event', 5000) + self.assertEqual(r['message'], 'Field updated', r) + + setting = self.admin_misp_connector.get_server_setting('MISP.live') + self.assertTrue(setting['value']) + r = self.admin_misp_connector.set_server_setting('MISP.live', False, force=True) + self.assertEqual(r['message'], 'Field updated', r) + setting = self.admin_misp_connector.get_server_setting('MISP.live') + self.assertFalse(setting['value']) + r = self.admin_misp_connector.set_server_setting('MISP.live', True, force=True) + self.assertEqual(r['message'], 'Field updated', r) + setting = self.admin_misp_connector.get_server_setting('MISP.live') + self.assertTrue(setting['value']) + + def test_search_value_event(self) -> None: '''Search a value on the event controller * Test ACL admin user vs normal user in an other org * Make sure we have one match @@ -141,69 +222,153 @@ class TestComprehensive(unittest.TestCase): try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search(value=first.attributes[0].value, pythonify=True) + events: list[MISPEvent] = self.admin_misp_connector.search(value=first.attributes[0].value, pythonify=True) # type: ignore[assignment] self.assertEqual(len(events), 2) for e in events: self.assertIn(e.id, [first.id, second.id]) # Search as user - events = self.user_misp_connector.search(value=first.attributes[0].value, pythonify=True) + events = self.user_misp_connector.search(value=first.attributes[0].value) # type: ignore[assignment] self.assertEqual(len(events), 1) for e in events: self.assertIn(e.id, [second.id]) # Non-existing value - events = self.user_misp_connector.search(value=str(uuid4()), pythonify=True) + events = self.user_misp_connector.search(value=str(uuid4())) # type: ignore[assignment] self.assertEqual(events, []) finally: # Delete events - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_value_attribute(self): + def test_search_value_attribute(self) -> None: '''Search value in attributes controller''' try: first, second, third = self.environment() # Search as admin - attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, pythonify=True) + attributes: list[MISPAttribute] = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, pythonify=True) # type: ignore[assignment] self.assertEqual(len(attributes), 2) for a in attributes: self.assertIn(a.event_id, [first.id, second.id]) # Search as user - attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value) # type: ignore[assignment] self.assertEqual(len(attributes), 1) for a in attributes: self.assertIn(a.event_id, [second.id]) # Non-existing value - attributes = self.user_misp_connector.search(controller='attributes', value=str(uuid4()), pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', value=str(uuid4())) # type: ignore[assignment] self.assertEqual(attributes, []) + + # Include context - search as user (can only see one event) + attributes = self.user_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) # type: ignore[assignment] + self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) + self.assertEqual(attributes[0].Event.uuid, second.uuid) + + # Include context - search as admin (can see both event) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_context=True, pythonify=True) # type: ignore[assignment] + self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) + self.assertEqual(attributes[0].Event.uuid, first.uuid) + self.assertEqual(attributes[1].Event.uuid, second.uuid) + + # Include correlations - search as admin (can see both event) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_correlations=True, pythonify=True) # type: ignore[assignment] + self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) + self.assertEqual(attributes[0].Event.uuid, first.uuid) + self.assertEqual(attributes[1].Event.uuid, second.uuid) + self.assertEqual(attributes[0].RelatedAttribute[0].Event.uuid, second.uuid) + self.assertEqual(attributes[1].RelatedAttribute[0].Event.uuid, first.uuid) + + # Include sightings - search as admin (can see both event) + s: dict[str, Any] = {'value': first.attributes[0].value} + self.admin_misp_connector.add_sighting(s) + attributes = self.admin_misp_connector.search(controller='attributes', value=first.attributes[0].value, include_sightings=True, pythonify=True) # type: ignore[assignment] + self.assertTrue(isinstance(attributes[0].Event, MISPEvent)) + self.assertEqual(attributes[0].Event.uuid, first.uuid) + self.assertEqual(attributes[1].Event.uuid, second.uuid) + self.assertTrue(isinstance(attributes[0].Sighting[0], MISPSighting)) + finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_type_event(self): + def test_search_type_event(self) -> None: '''Search multiple events, search events containing attributes with specific types''' try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search(timestamp=first.timestamp.timestamp(), pythonify=True) + if isinstance(first.timestamp, datetime): + ts = first.timestamp.timestamp() + else: + ts = first.timestamp + events: list[MISPEvent] = self.admin_misp_connector.search(timestamp=ts, pythonify=True) # type: ignore[assignment] self.assertEqual(len(events), 3) for e in events: self.assertIn(e.id, [first.id, second.id, third.id]) attributes_types_search = self.admin_misp_connector.build_complex_query(or_parameters=['ip-src', 'ip-dst']) - events = self.admin_misp_connector.search(timestamp=first.timestamp.timestamp(), + events = self.admin_misp_connector.search(timestamp=ts, # type: ignore[assignment,type-var] type_attribute=attributes_types_search, pythonify=True) self.assertEqual(len(events), 2) for e in events: self.assertIn(e.id, [second.id, third.id]) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_type_attribute(self): + def test_search_index(self) -> None: + try: + first, second, third = self.environment() + # Search as admin + if isinstance(first.timestamp, datetime): + ts = first.timestamp.timestamp() + else: + ts = first.timestamp + events: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, pythonify=True) # type: ignore[assignment] + self.assertEqual(len(events), 3) + for e in events: + self.assertIn(e.id, [first.id, second.id, third.id]) + + # Test limit and pagination + event_one: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, limit=1, page=1, pythonify=True)[0] # type: ignore[index,assignment] + event_two: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, limit=1, page=2, pythonify=True)[0] # type: ignore[index,assignment] + self.assertTrue(event_one.id != event_two.id) + two_events = self.admin_misp_connector.search_index(limit=2) + self.assertTrue(len(two_events), 2) + + # Test ordering by the Info field. Can't use timestamp as each will likely have the same + event: MISPEvent = self.admin_misp_connector.search_index(timestamp=ts, sort="info", desc=True, limit=1, pythonify=True)[0] # type: ignore[index,assignment] + # First|Second|*Third* event + self.assertEqual(event.id, third.id) + # *First*|Second|Third event + event = self.admin_misp_connector.search_index(timestamp=ts, sort="info", desc=False, limit=1, pythonify=True)[0] # type: ignore[index,assignment] + self.assertEqual(event.id, first.id) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) + + def test_search_objects(self) -> None: + '''Search for objects''' + try: + first = self.create_simple_event() + obj = MISPObject('file') + obj.add_attribute('filename', 'foo') + first.add_object(obj) + first = self.user_misp_connector.add_event(first) + logger = logging.getLogger('pymisp') + logger.setLevel(logging.DEBUG) + objects = self.user_misp_connector.search(controller='objects', + object_name='file', pythonify=True) + self.assertEqual(len(objects), 1) + self.assertEqual(objects[0].attributes[0].value, 'foo') + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_search_type_attribute(self) -> None: '''Search multiple attributes, search attributes with specific types''' try: first, second, third = self.environment() @@ -223,11 +388,11 @@ class TestComprehensive(unittest.TestCase): self.assertIn(a.event_id, [second.id, third.id]) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_tag_event(self): + def test_search_tag_event(self) -> None: '''Search Tags at events level''' try: first, second, third = self.environment() @@ -245,23 +410,23 @@ class TestComprehensive(unittest.TestCase): for e in events: self.assertIn(e.id, [first.id]) # Search as user - events = self.user_misp_connector.search(tags='tlp:white___test', pythonify=True) + events = self.user_misp_connector.search(tags='tlp:white___test') self.assertEqual(len(events), 2) for e in events: self.assertIn(e.id, [second.id, third.id]) - events = self.user_misp_connector.search(tags='tlp:amber___test', pythonify=True) + events = self.user_misp_connector.search(tags='tlp:amber___test') self.assertEqual(len(events), 2) for e in events: self.assertIn(e.id, [second.id, third.id]) - events = self.user_misp_connector.search(tags='admin_only', pythonify=True) + events = self.user_misp_connector.search(tags='admin_only') self.assertEqual(events, []) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_tag_attribute(self): + def test_search_tag_attribute(self) -> None: '''Search Tags at attributes level''' try: first, second, third = self.environment() @@ -273,22 +438,22 @@ class TestComprehensive(unittest.TestCase): attributes = self.admin_misp_connector.search(tags='admin_only', pythonify=True) self.assertEqual(len(attributes), 1) # Search as user - attributes = self.user_misp_connector.search(controller='attributes', tags='tlp:white___test', pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', tags='tlp:white___test') self.assertEqual(len(attributes), 4) - attributes = self.user_misp_connector.search(controller='attributes', tags='tlp:amber___test', pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', tags='tlp:amber___test') self.assertEqual(len(attributes), 3) - attributes = self.user_misp_connector.search(tags='admin_only', pythonify=True) + attributes = self.user_misp_connector.search(tags='admin_only') self.assertEqual(attributes, []) attributes_tags_search = self.admin_misp_connector.build_complex_query(or_parameters=['tlp:amber___test'], not_parameters=['tlp:white___test']) - attributes = self.user_misp_connector.search(controller='attributes', tags=attributes_tags_search, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', tags=attributes_tags_search) self.assertEqual(len(attributes), 1) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_tag_advanced_event(self): + def test_search_tag_advanced_event(self) -> None: '''Advanced search Tags at events level''' try: first, second, third = self.environment() @@ -314,11 +479,11 @@ class TestComprehensive(unittest.TestCase): self.assertEqual([t for t in a.tags if t.name == 'tlp:white___test'], []) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_tag_advanced_attributes(self): + def test_search_tag_advanced_attributes(self) -> None: '''Advanced search Tags at attributes level''' try: first, second, third = self.environment() @@ -333,11 +498,11 @@ class TestComprehensive(unittest.TestCase): self.assertEqual([t for t in a.tags if t.name == 'foo_double___test'], []) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_search_timestamp_event(self): + def test_search_timestamp_event(self) -> None: '''Search specific update timestamps at events level''' # Creating event 1 - timestamp 5 min ago first = self.create_simple_event(force_timestamps=True) @@ -352,28 +517,28 @@ class TestComprehensive(unittest.TestCase): second = self.user_misp_connector.add_event(second) # Search as user # # Test - last 4 min - events = self.user_misp_connector.search(timestamp='4m', pythonify=True) + events = self.user_misp_connector.search(timestamp='4m') self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) self.assertEqual(events[0].timestamp.timestamp(), int(event_creation_timestamp_second.timestamp())) # # Test timestamp of 2nd event - events = self.user_misp_connector.search(timestamp=event_creation_timestamp_second.timestamp(), pythonify=True) + events = self.user_misp_connector.search(timestamp=event_creation_timestamp_second.timestamp()) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) self.assertEqual(events[0].timestamp.timestamp(), int(event_creation_timestamp_second.timestamp())) # # Test interval -6 min -> -4 min - events = self.user_misp_connector.search(timestamp=['6m', '4m'], pythonify=True) + events = self.user_misp_connector.search(timestamp=['6m', '4m']) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) self.assertEqual(events[0].timestamp.timestamp(), int(event_creation_timestamp_first.timestamp())) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_search_timestamp_attribute(self): + def test_search_timestamp_attribute(self) -> None: '''Search specific update timestamps at attributes level''' # Creating event 1 - timestamp 5 min ago first = self.create_simple_event(force_timestamps=True) @@ -390,28 +555,28 @@ class TestComprehensive(unittest.TestCase): second = self.user_misp_connector.add_event(second) # Search as user # # Test - last 4 min - attributes = self.user_misp_connector.search(controller='attributes', timestamp='4m', pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', timestamp='4m') self.assertEqual(len(attributes), 1) self.assertEqual(attributes[0].event_id, second.id) self.assertEqual(attributes[0].timestamp.timestamp(), int(event_creation_timestamp_second.timestamp())) # # Test timestamp of 2nd event - attributes = self.user_misp_connector.search(controller='attributes', timestamp=event_creation_timestamp_second.timestamp(), pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', timestamp=event_creation_timestamp_second.timestamp()) self.assertEqual(len(attributes), 1) self.assertEqual(attributes[0].event_id, second.id) self.assertEqual(attributes[0].timestamp.timestamp(), int(event_creation_timestamp_second.timestamp())) # # Test interval -6 min -> -4 min - attributes = self.user_misp_connector.search(controller='attributes', timestamp=['6m', '4m'], pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', timestamp=['6m', '4m']) self.assertEqual(len(attributes), 1) self.assertEqual(attributes[0].event_id, first.id) self.assertEqual(attributes[0].timestamp.timestamp(), int(event_creation_timestamp_first.timestamp())) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_user_perms(self): + def test_user_perms(self) -> None: '''Test publish rights''' try: first = self.create_simple_event() @@ -421,14 +586,82 @@ class TestComprehensive(unittest.TestCase): self.assertFalse(first.published) # Add event as publisher first.publish() - first = self.pub_misp_connector.update_event(first) + first = self.pub_misp_connector.update_event(first, pythonify=True) self.assertTrue(first.published) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - # @unittest.skip("Uncomment when adding new tests, it has a 10s sleep") - def test_search_publish_timestamp(self): + def test_delete_with_update(self) -> None: + try: + first = self.create_simple_event() + obj = MISPObject('file') + obj.add_attribute('filename', 'foo') + first.add_object(obj) + first = self.user_misp_connector.add_event(first) + + first.attributes[0].deleted = True + deleted_attribute = self.user_misp_connector.update_attribute(first.attributes[0], pythonify=True) + self.assertTrue(deleted_attribute.deleted) + + first.objects[0].deleted = True + deleted_object = self.user_misp_connector.update_object(first.objects[0], pythonify=True) + self.assertTrue(deleted_object.deleted) + + # Get event with deleted entries + first = self.user_misp_connector.get_event(first, deleted=True, pythonify=True) + self.assertTrue(first.attributes[0].deleted) + self.assertTrue(first.objects[0].deleted) + + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_get_non_exists_event(self) -> None: + event = self.user_misp_connector.get_event(0) # non exists id + self.assertEqual(event['errors'][0], 404) + + event = self.user_misp_connector.get_event("ab2b6e28-fda5-4282-bf60-22b81de77851") # non exists uuid + self.assertEqual(event['errors'][0], 404) + + def test_delete_by_uuid(self) -> None: + try: + first = self.create_simple_event() + obj = MISPObject('file') + obj.add_attribute('filename', 'foo') + first.add_object(obj) + obj = MISPObject('file') + obj.add_attribute('filename', 'bar') + first.add_object(obj) + first = self.user_misp_connector.add_event(first) + r = self.user_misp_connector.delete_attribute(first.attributes[0].uuid) + self.assertEqual(r['message'], 'Attribute deleted.') + r = self.user_misp_connector.delete_object(first.objects[0].uuid) + self.assertEqual(r['message'], 'Object deleted') + # Test deleted search + r = self.user_misp_connector.search(event_id=first.id, deleted=[0, 1], pythonify=True) + self.assertTrue(isinstance(r[0], MISPEvent)) + self.assertEqual(len(r[0].objects), 2) + self.assertTrue(r[0].objects[0].deleted) + self.assertFalse(r[0].objects[1].deleted) + self.assertEqual(len(r[0].attributes), 1) + self.assertTrue(r[0].attributes[0].deleted) + # Test deleted get + r = self.user_misp_connector.get_event(first, deleted=True, pythonify=True) + self.assertTrue(isinstance(r, MISPEvent)) + self.assertEqual(len(r.objects), 2) + self.assertTrue(r.objects[0].deleted) + self.assertFalse(r.objects[1].deleted) + self.assertEqual(len(r.attributes), 1) + self.assertTrue(r.attributes[0].deleted) + + r = self.user_misp_connector.delete_event(first.uuid) + self.assertEqual(r['message'], 'Event deleted.') + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_search_publish_timestamp(self) -> None: '''Search for a specific publication timestamp, an interval, and invalid values.''' # Creating event 1 first = self.create_simple_event() @@ -437,9 +670,9 @@ class TestComprehensive(unittest.TestCase): second = self.create_simple_event() second.publish() try: - first = self.pub_misp_connector.add_event(first) + first = self.pub_misp_connector.add_event(first, pythonify=True) time.sleep(10) - second = self.pub_misp_connector.add_event(second) + second = self.pub_misp_connector.add_event(second, pythonify=True) # Test invalid query events = self.pub_misp_connector.search(publish_timestamp='5x', pythonify=True) self.assertEqual(events, []) @@ -464,13 +697,36 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(events[0].id, first.id) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_default_distribution(self): + def test_search_decay(self) -> None: + # Creating event 1 + first = self.create_simple_event() + first.add_attribute('ip-dst', '8.8.8.8') + first.publish() + try: + r = self.admin_misp_connector.update_decaying_models() + self.assertTrue(r['success'], r) + simple_decaying_model = None + models = self.admin_misp_connector.decaying_models(pythonify=True) + for model in models: + if model.name == 'NIDS Simple Decaying Model': + simple_decaying_model = model + self.assertTrue(simple_decaying_model, models) + self.admin_misp_connector.enable_decaying_model(simple_decaying_model) + # TODO: check the response, it is curently an empty list + first = self.pub_misp_connector.add_event(first, pythonify=True) + result = self.pub_misp_connector.search('attributes', to_ids=1, includeDecayScore=True, pythonify=True) + self.assertTrue(result[0].decay_score, result[0].to_json(indent=2)) + self.admin_misp_connector.disable_decaying_model(simple_decaying_model) + # TODO: check the response, it is curently a list of all the models + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_default_distribution(self) -> None: '''The default distributions on the VM are This community only for the events and Inherit from event for attr/obj)''' - if travis_run: - return first = self.create_simple_event() del first.distribution o = first.add_object(name='file') @@ -491,28 +747,64 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(first.objects[1].distribution, Distribution.inherit.value) self.assertEqual(first.objects[1].attributes[0].distribution, Distribution.inherit.value) # Attribute create - attribute = self.user_misp_connector.add_named_attribute(first, 'comment', 'bar') - # FIXME: Add helper that returns a list of MISPAttribute - self.assertEqual(attribute[0]['Attribute']['distribution'], str(Distribution.inherit.value)) + attribute = self.user_misp_connector.add_attribute(first, {'type': 'comment', 'value': 'bar'}) + self.assertEqual(attribute.value, 'bar', attribute.to_json()) + self.assertEqual(attribute.distribution, Distribution.inherit.value, attribute.to_json()) # Object - add o = MISPObject('file') o.add_attribute('filename', value='blah.exe') - new_obj = self.user_misp_connector.add_object(first.id, o.template_uuid, o) - # FIXME: Add helper that returns a MISPObject - self.assertEqual(new_obj['Object']['distribution'], str(Distribution.inherit.value)) - self.assertEqual(new_obj['Object']['Attribute'][0]['distribution'], str(Distribution.inherit.value)) + new_obj = self.user_misp_connector.add_object(first, o) + self.assertEqual(new_obj.distribution, int(Distribution.inherit.value)) + self.assertEqual(new_obj.attributes[0].distribution, int(Distribution.inherit.value)) # Object - edit - clean_obj = MISPObject(strict=True, **new_obj['Object']) - clean_obj.from_dict(**new_obj['Object']) + clean_obj = MISPObject(name=new_obj.name, strict=True) + clean_obj.from_dict(**new_obj) clean_obj.add_attribute('filename', value='blah.exe') - new_obj = self.user_misp_connector.edit_object(clean_obj) - for a in new_obj['Object']['Attribute']: - self.assertEqual(a['distribution'], str(Distribution.inherit.value)) + new_obj = self.user_misp_connector.update_object(clean_obj) + for a in new_obj.attributes: + self.assertEqual(a.distribution, int(Distribution.inherit.value)) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - def test_simple_event(self): + def test_exists(self) -> None: + """Check event, attribute and object existence""" + event = self.create_simple_event() + misp_object = MISPObject('domain-ip') + attribute = misp_object.add_attribute('domain', value='google.fr') + misp_object.add_attribute('ip', value='8.8.8.8') + event.add_object(misp_object) + + # Event, attribute and object should not exists before event deletion + self.assertFalse(self.user_misp_connector.event_exists(event)) + self.assertFalse(self.user_misp_connector.attribute_exists(attribute)) + self.assertFalse(self.user_misp_connector.object_exists(misp_object)) + + try: + event = self.user_misp_connector.add_event(event, pythonify=True) + misp_object = event.objects[0] + attribute = misp_object.attributes[0] + self.assertTrue(self.user_misp_connector.event_exists(event)) + self.assertTrue(self.user_misp_connector.event_exists(event.uuid)) + self.assertTrue(self.user_misp_connector.event_exists(event.id)) + self.assertTrue(self.user_misp_connector.attribute_exists(attribute)) + self.assertTrue(self.user_misp_connector.attribute_exists(attribute.uuid)) + self.assertTrue(self.user_misp_connector.attribute_exists(attribute.id)) + self.assertTrue(self.user_misp_connector.object_exists(misp_object)) + self.assertTrue(self.user_misp_connector.object_exists(misp_object.id)) + self.assertTrue(self.user_misp_connector.object_exists(misp_object.uuid)) + finally: + self.admin_misp_connector.delete_event(event) + + # Event, attribute and object should not exists after event deletion + self.assertFalse(self.user_misp_connector.event_exists(event)) + self.assertFalse(self.user_misp_connector.event_exists(event.id)) + self.assertFalse(self.user_misp_connector.attribute_exists(attribute)) + self.assertFalse(self.user_misp_connector.attribute_exists(attribute.id)) + self.assertFalse(self.user_misp_connector.object_exists(misp_object)) + self.assertFalse(self.user_misp_connector.object_exists(misp_object.id)) + + def test_simple_event(self) -> None: '''Search a bunch of parameters: * Value not existing * only return metadata @@ -534,6 +826,7 @@ class TestComprehensive(unittest.TestCase): # First has one text attribute second = self.create_simple_event() second.info = 'foo blah' + second.add_tag('tlp:amber___test') second.set_date('2018-09-01') second.add_attribute('ip-src', '8.8.8.8') # second has two attributes: text and ip-src @@ -542,13 +835,13 @@ class TestComprehensive(unittest.TestCase): second = self.user_misp_connector.add_event(second) timeframe = [first.timestamp.timestamp() - 5, first.timestamp.timestamp() + 5] # Search event we just created in multiple ways. Make sure it doesn't catch it when it shouldn't - events = self.user_misp_connector.search(timestamp=timeframe, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe) self.assertEqual(len(events), 2) self.assertEqual(events[0].id, first.id) self.assertEqual(events[1].id, second.id) - events = self.user_misp_connector.search(timestamp=timeframe, value='nothere', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, value='nothere') self.assertEqual(events, []) - events = self.user_misp_connector.search(timestamp=timeframe, value=first.attributes[0].value, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, value=first.attributes[0].value) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) events = self.user_misp_connector.search(timestamp=[first.timestamp.timestamp() - 50, @@ -557,34 +850,47 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(events, []) # Test return content - events = self.user_misp_connector.search(timestamp=timeframe, metadata=False, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, metadata=False) self.assertEqual(len(events), 2) self.assertEqual(len(events[0].attributes), 1) self.assertEqual(len(events[1].attributes), 2) - events = self.user_misp_connector.search(timestamp=timeframe, metadata=True, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, metadata=True) self.assertEqual(len(events), 2) self.assertEqual(len(events[0].attributes), 0) self.assertEqual(len(events[1].attributes), 0) # other things - events = self.user_misp_connector.search(timestamp=timeframe, published=True, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, published=True) self.assertEqual(events, []) - events = self.user_misp_connector.search(timestamp=timeframe, published=False, pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, published=False) self.assertEqual(len(events), 2) - events = self.user_misp_connector.search(eventid=first.id, pythonify=True) + # check publish & search + bg_processing_state = self.admin_misp_connector.get_server_setting('MISP.background_jobs')['value'] + self.admin_misp_connector.set_server_setting('MISP.background_jobs', False, force=True) + publish_result = self.admin_misp_connector.publish(second) + self.assertEqual(publish_result["success"], True) + second = self.admin_misp_connector.get_event(second, pythonify=True) + # check if the publishing succeeded + time.sleep(1) + self.assertEqual(second.published, True) + self.admin_misp_connector.set_server_setting('MISP.background_jobs', bg_processing_state, force=True) + events = self.user_misp_connector.search(timestamp=timeframe, published=False) + self.assertEqual(len(events), 1) + + events = self.user_misp_connector.search(eventid=first.id) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) - events = self.user_misp_connector.search(uuid=first.uuid, pythonify=True) + events = self.user_misp_connector.search(uuid=first.uuid) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) - events = self.user_misp_connector.search(org=first.orgc_id, pythonify=True) + events = self.user_misp_connector.search(org=first.orgc_id) self.assertEqual(len(events), 2) # test like search - events = self.user_misp_connector.search(timestamp=timeframe, value='%{}%'.format(first.attributes[0].value.split('-')[2]), pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, value='%{}%'.format(first.attributes[0].value.split('-')[2])) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) - events = self.user_misp_connector.search(timestamp=timeframe, eventinfo='%bar blah%', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, eventinfo='%bar blah%') self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) @@ -597,24 +903,24 @@ class TestComprehensive(unittest.TestCase): # self.assertEqual(events[0].id, second.id) # date_from / date_to - events = self.user_misp_connector.search(timestamp=timeframe, date_from=date.today().isoformat(), pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, date_from=date.today().isoformat()) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, first.id) - events = self.user_misp_connector.search(timestamp=timeframe, date_from='2018-09-01', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, date_from='2018-09-01') self.assertEqual(len(events), 2) - events = self.user_misp_connector.search(timestamp=timeframe, date_from='2018-09-01', date_to='2018-09-02', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, date_from='2018-09-01', date_to='2018-09-02') self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) # Category - events = self.user_misp_connector.search(timestamp=timeframe, category='Network activity', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, category='Network activity') self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) # toids - events = self.user_misp_connector.search(timestamp=timeframe, to_ids='0', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, to_ids='0') self.assertEqual(len(events), 2) - events = self.user_misp_connector.search(timestamp=timeframe, to_ids='1', pythonify=True) + events = self.user_misp_connector.search(timestamp=timeframe, to_ids='1') self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) self.assertEqual(len(events[0].attributes), 1) @@ -622,26 +928,29 @@ class TestComprehensive(unittest.TestCase): # deleted second.attributes[1].delete() self.user_misp_connector.update_event(second) - events = self.user_misp_connector.search(eventid=second.id, pythonify=True) + events = self.user_misp_connector.search(eventid=second.id) + self.assertEqual(len(events[0].attributes), 1) + events = self.user_misp_connector.search(eventid=second.id, deleted=True) self.assertEqual(len(events[0].attributes), 1) - events = self.user_misp_connector.search(eventid=second.id, deleted=True, pythonify=True) - self.assertEqual(len(events[0].attributes), 2) # include_event_uuid - attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, include_event_uuid=True, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, include_event_uuid=True) self.assertEqual(attributes[0].event_uuid, second.uuid) + # include_event_tags + attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, include_event_tags=True) + self.assertEqual(attributes[0].tags[0].name, 'tlp:amber___test') # event_timestamp time.sleep(1) second.add_attribute('ip-src', '8.8.8.9') second = self.user_misp_connector.update_event(second) - events = self.user_misp_connector.search(event_timestamp=second.timestamp.timestamp(), pythonify=True) + events = self.user_misp_connector.search(event_timestamp=second.timestamp.timestamp()) self.assertEqual(len(events), 1) # searchall second.add_attribute('text', 'This is a test for the full text search', comment='Test stuff comment') second = self.user_misp_connector.update_event(second) - events = self.user_misp_connector.search(value='%for the full text%', searchall=True, pythonify=True) + events = self.user_misp_connector.search(value='%for the full text%', searchall=True) self.assertEqual(len(events), 1) # warninglist @@ -651,30 +960,35 @@ class TestComprehensive(unittest.TestCase): second.add_attribute('ip-src', '9.9.9.9') second = self.user_misp_connector.update_event(second) - events = self.user_misp_connector.search(eventid=second.id, pythonify=True) + events = self.user_misp_connector.search(eventid=second.id) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) self.assertEqual(len(events[0].attributes), 5) - events = self.user_misp_connector.search(eventid=second.id, enforce_warninglist=False, pythonify=True) + events = self.user_misp_connector.search(eventid=second.id, enforce_warninglist=False) self.assertEqual(len(events), 1) self.assertEqual(events[0].id, second.id) self.assertEqual(len(events[0].attributes), 5) - if not travis_run: - # FIXME: This is failing on travis for no discernable reason... - events = self.user_misp_connector.search(eventid=second.id, enforce_warninglist=True, pythonify=True) - self.assertEqual(len(events), 1) - self.assertEqual(events[0].id, second.id) - self.assertEqual(len(events[0].attributes), 3) - response = self.admin_misp_connector.toggle_warninglist(warninglist_name='%dns resolv%') # disable ipv4 DNS. - self.assertDictEqual(response, {'saved': True, 'success': '3 warninglist(s) toggled'}) + events = self.user_misp_connector.search(eventid=second.id, enforce_warninglist=True) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].id, second.id) + self.assertEqual(len(events[0].attributes), 4) + + # Test PyMISP.add_attribute with enforceWarninglist enabled + _e = events[0] + _a = _e.add_attribute('ip-src', '8.8.8.8', enforceWarninglist=True) + _a = self.user_misp_connector.add_attribute(_e, _a) + self.assertTrue('trips over a warninglist and enforceWarninglist is enforced' in _a['errors'][1]['errors'], _a) + + response = self.admin_misp_connector.toggle_warninglist(warninglist_name='%dns resolv%') # disable ipv4 DNS. + self.assertDictEqual(response, {'saved': True, 'success': '3 warninglist(s) toggled'}) # Page / limit - attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, page=1, limit=3, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, page=1, limit=3) self.assertEqual(len(attributes), 3) - attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, page=2, limit=3, pythonify=True) + attributes = self.user_misp_connector.search(controller='attributes', eventid=second.id, page=2, limit=3) self.assertEqual(len(attributes), 2) time.sleep(1) # make sure the next attribute is added one at least one second later @@ -693,30 +1007,91 @@ class TestComprehensive(unittest.TestCase): self.assertEqual(len(events), 1) self.assertIs(events[0].attributes[-1].malware_binary, None) + # Search index + # # Timestamp + events = self.user_misp_connector.search_index(timestamp=first.timestamp.timestamp(), + pythonify=True) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].info, 'foo bar blah') + self.assertEqual(events[0].attributes, []) + + # # Info + complex_info = r'C:\Windows\System32\notepad.exe' + e = events[0] + e.info = complex_info + e = self.user_misp_connector.update_event(e, pythonify=True) + # Issue: https://github.com/MISP/MISP/issues/6616 + complex_info_search = r'C:\\Windows\\System32\\notepad.exe' + events = self.user_misp_connector.search_index(eventinfo=complex_info_search, + pythonify=True) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].info, complex_info) + self.assertEqual(events[0].attributes, []) + + # Contact reporter + r = self.user_misp_connector.contact_event_reporter(events[0].id, 'This is a test') + self.assertEqual(r['message'], 'Email sent to the reporter.') finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_edit_attribute(self): + def test_event_add_update_metadata(self) -> None: + event = self.create_simple_event() + event.add_attribute('ip-src', '9.9.9.9') + try: + response = self.user_misp_connector.add_event(event, metadata=True) + self.assertEqual(len(response.attributes), 0) # response should contains zero attributes + + event.info = "New name ©" + response = self.user_misp_connector.update_event(event, metadata=True) + self.assertEqual(response.info, event.info) + self.assertEqual(len(response.attributes), 0) # response should contains zero attributes + finally: # cleanup + self.admin_misp_connector.delete_event(event) + + def test_extend_event(self) -> None: + first = self.create_simple_event() + first.info = 'parent event' + first.add_tag('tlp:amber___test') + first.set_date('2018-09-01') + second = self.create_simple_event() + second.info = 'event extension' + second.add_tag('tlp:amber___test') + second.set_date('2018-09-01') + second.add_attribute('ip-src', '9.9.9.9') + try: + first = self.user_misp_connector.add_event(first) + second = self.user_misp_connector.add_event(second) + first_extended = self.user_misp_connector.update_event({'extends_uuid': second.uuid}, event_id=first, pythonify=True) + self.assertTrue(isinstance(first_extended, MISPEvent), first_extended) + self.assertEqual(first_extended.extends_uuid, second.uuid) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + + def test_edit_attribute(self) -> None: first = self.create_simple_event() try: first.attributes[0].comment = 'This is the original comment' first = self.user_misp_connector.add_event(first) first.attributes[0].comment = 'This is the modified comment' attribute = self.user_misp_connector.update_attribute(first.attributes[0]) + self.assertTrue(isinstance(attribute, MISPAttribute), attribute) self.assertEqual(attribute.comment, 'This is the modified comment') - attribute = self.user_misp_connector.change_comment(first.attributes[0].uuid, 'This is the modified comment, again') - self.assertEqual(attribute['Attribute']['comment'], 'This is the modified comment, again') - attribute = self.user_misp_connector.change_disable_correlation(first.attributes[0].uuid, True) - self.assertEqual(attribute['Attribute']['disable_correlation'], True) - attribute = self.user_misp_connector.change_disable_correlation(first.attributes[0].uuid, 0) - self.assertEqual(attribute['Attribute']['disable_correlation'], False) + attribute = self.user_misp_connector.update_attribute({'comment': 'This is the modified comment, again'}, attribute) + self.assertTrue(isinstance(attribute, MISPAttribute), attribute) + self.assertEqual(attribute.comment, 'This is the modified comment, again', attribute) + attribute = self.user_misp_connector.update_attribute({'disable_correlation': True}, attribute) + self.assertTrue(attribute.disable_correlation, attribute) + attribute = self.user_misp_connector.update_attribute({'disable_correlation': False}, attribute) + self.assertFalse(attribute.disable_correlation, attribute) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - def test_sightings(self): + def test_sightings(self) -> None: first = self.create_simple_event() second = self.create_simple_event() try: @@ -724,10 +1099,16 @@ class TestComprehensive(unittest.TestCase): second = self.user_misp_connector.add_event(second) current_ts = int(time.time()) - self.user_misp_connector.sighting(value=first.attributes[0].value) - self.user_misp_connector.sighting(value=second.attributes[0].value, - source='Testcases', - type='1') + time.sleep(5) + r = self.user_misp_connector.add_sighting({'value': first.attributes[0].value}) + self.assertEqual(int(r.attribute_id), first.attributes[0].id) + + s = MISPSighting() + s.value = second.attributes[0].value + s.source = 'Testcases' + s.type = '1' + r = self.user_misp_connector.add_sighting(s, second.attributes[0]) + self.assertEqual(r.source, 'Testcases') s = self.user_misp_connector.search_sightings(publish_timestamp=current_ts, include_attribute=True, include_event_meta=True, pythonify=True) @@ -741,7 +1122,7 @@ class TestComprehensive(unittest.TestCase): include_event_meta=True, pythonify=True) self.assertEqual(len(s), 1) - self.assertEqual(s[0]['event'].id, second.id) + self.assertEqual(s[0]['event'].id, second.id, s) self.assertEqual(s[0]['attribute'].id, second.attributes[0].id) s = self.user_misp_connector.search_sightings(publish_timestamp=current_ts, @@ -764,12 +1145,28 @@ class TestComprehensive(unittest.TestCase): pythonify=True) self.assertEqual(len(s), 1) self.assertEqual(s[0]['sighting'].attribute_id, str(second.attributes[0].id)) + + # Get sightings from event/attribute / org + s = self.user_misp_connector.sightings(first) + self.assertTrue(isinstance(s, list)) + self.assertEqual(int(s[0].attribute_id), first.attributes[0].id) + + self.admin_misp_connector.add_sighting(s, second.attributes[0]) + s = self.user_misp_connector.sightings(second.attributes[0]) + self.assertEqual(len(s), 2) + s = self.user_misp_connector.sightings(second.attributes[0], self.test_org) + self.assertEqual(len(s), 1) + self.assertEqual(s[0].org_id, self.test_org.id) + # Delete sighting + r = self.user_misp_connector.delete_sighting(s[0]) + self.assertEqual(r['message'], 'Sighting successfully deleted.') + finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_search_csv(self): + def test_search_csv(self) -> None: first = self.create_simple_event() first.attributes[0].comment = 'This is the original comment' second = self.create_simple_event() @@ -780,234 +1177,604 @@ class TestComprehensive(unittest.TestCase): first = self.user_misp_connector.add_event(first) second = self.user_misp_connector.add_event(second) - response = self.user_misp_connector.fast_publish(first.id, alert=False) + response = self.user_misp_connector.publish(first, alert=False) self.assertEqual(response['errors'][1]['message'], 'You do not have permission to use this functionality.') # Default search, attribute with to_ids == True first.attributes[0].to_ids = True first = self.user_misp_connector.update_event(first) - self.admin_misp_connector.fast_publish(first.id, alert=False) - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), pythonify=True) + self.admin_misp_connector.publish(first, alert=False) + time.sleep(5) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp()) self.assertEqual(len(csv), 1) self.assertEqual(csv[0]['value'], first.attributes[0].value) # eventid - csv = self.user_misp_connector.search(return_format='csv', eventid=first.id, pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', eventid=first.id) self.assertEqual(len(csv), 1) self.assertEqual(csv[0]['value'], first.attributes[0].value) # category - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), category='Other', pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), category='Other') self.assertEqual(len(csv), 1) self.assertEqual(csv[0]['value'], first.attributes[0].value) - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), category='Person', pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), category='Person') self.assertEqual(len(csv), 0) # type_attribute - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), type_attribute='text', pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), type_attribute='text') self.assertEqual(len(csv), 1) self.assertEqual(csv[0]['value'], first.attributes[0].value) - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), type_attribute='ip-src', pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), type_attribute='ip-src') self.assertEqual(len(csv), 0) # context - csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), include_context=True, pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp(), include_context=True) self.assertEqual(len(csv), 1) self.assertTrue('event_info' in csv[0]) # date_from date_to - csv = self.user_misp_connector.search(return_format='csv', date_from=date.today().isoformat(), pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', date_from=date.today().isoformat()) self.assertEqual(len(csv), 1) self.assertEqual(csv[0]['value'], first.attributes[0].value) - csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02', pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02') self.assertEqual(len(csv), 2) # headerless csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02', headerless=True) - # FIXME: The header is here. - # print(csv) # Expects 2 lines after removing the empty ones. - # self.assertEqual(len(csv.strip().split('\n')), 2) + self.assertEqual(len(csv.strip().split('\n')), 2) # include_context - csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02', include_context=True, pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02', include_context=True) event_context_keys = ['event_info', 'event_member_org', 'event_source_org', 'event_distribution', 'event_threat_level_id', 'event_analysis', 'event_date', 'event_tag', 'event_timestamp'] for k in event_context_keys: self.assertTrue(k in csv[0]) # requested_attributes columns = ['value', 'event_id'] - csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', date_to='2018-09-02', requested_attributes=columns, pythonify=True) + csv = self.user_misp_connector.search(return_format='csv', date_from='2018-09-01', + date_to='2018-09-02', requested_attributes=columns) self.assertEqual(len(csv[0].keys()), 2) for k in columns: self.assertTrue(k in csv[0]) finally: + # Mostly solved -> https://github.com/MISP/MISP/issues/4886 + time.sleep(5) # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_search_stix(self): + def test_search_text(self) -> None: + first = self.create_simple_event() + first.add_attribute('ip-src', '8.8.8.8') + first.publish() + try: + first = self.user_misp_connector.add_event(first) + self.admin_misp_connector.publish(first) + time.sleep(5) + text = self.user_misp_connector.search(return_format='text', eventid=first.id) + self.assertEqual('8.8.8.8', text.strip()) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_search_stix(self) -> None: first = self.create_simple_event() first.add_attribute('ip-src', '8.8.8.8') try: first = self.user_misp_connector.add_event(first) - if not travis_run: - stix = self.user_misp_connector.search(return_format='stix', eventid=first.id) - found = re.findall('8.8.8.8', stix) - self.assertTrue(found) - stix2 = self.user_misp_connector.search(return_format='stix2', eventid=first.id) - json.dumps(stix2, indent=2) - self.assertEqual(stix2['objects'][-1]['pattern'], "[network-traffic:src_ref.type = 'ipv4-addr' AND network-traffic:src_ref.value = '8.8.8.8']") + stix = self.user_misp_connector.search(return_format='stix', eventid=first.id) + self.assertTrue(stix['related_packages']['related_packages'][0]['package']['incidents'][0]['related_indicators']['indicators'][0]['indicator']['observable']['object']['properties']['address_value']['value'], '8.8.8.8') + stix2 = self.user_misp_connector.search(return_format='stix2', eventid=first.id) + self.assertEqual(stix2['objects'][-1]['pattern'], "[network-traffic:src_ref.type = 'ipv4-addr' AND network-traffic:src_ref.value = '8.8.8.8']") + stix_xml = self.user_misp_connector.search(return_format='stix-xml', eventid=first.id) + self.assertTrue('8.8.8.8' in stix_xml) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - def test_upload_sample(self): - first = self.create_simple_event() - second = self.create_simple_event() - third = self.create_simple_event() - try: - # Simple, not executable - first = self.user_misp_connector.add_event(first) - with open('tests/testlive_comprehensive.py', 'rb') as f: - response = self.user_misp_connector.upload_sample(filename='testfile.py', filepath_or_bytes=f.read(), - event_id=first.id) - self.assertEqual(response['message'], 'Success, saved all attributes.') - first = self.user_misp_connector.get_event(first.id) - self.assertEqual(len(first.objects), 1) - self.assertEqual(first.objects[0].name, 'file') - # Simple, executable - second = self.user_misp_connector.add_event(second) - with open('tests/viper-test-files/test_files/whoami.exe', 'rb') as f: - response = self.user_misp_connector.upload_sample(filename='whoami.exe', filepath_or_bytes=f.read(), - event_id=second.id) - self.assertEqual(response['message'], 'Success, saved all attributes.') - second = self.user_misp_connector.get_event(second.id) - self.assertEqual(len(second.objects), 1) - self.assertEqual(second.objects[0].name, 'file') - third = self.user_misp_connector.add_event(third) - if not travis_run: - # Advanced, executable - with open('tests/viper-test-files/test_files/whoami.exe', 'rb') as f: - response = self.user_misp_connector.upload_sample(filename='whoami.exe', filepath_or_bytes=f.read(), - event_id=third.id, advanced_extraction=True) - self.assertEqual(response['message'], 'Success, saved all attributes.') - third = self.user_misp_connector.get_event(third.id) - self.assertEqual(len(third.objects), 7) - self.assertEqual(third.objects[0].name, 'pe-section') - finally: - # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) - - def test_update_object(self): + def test_update_object(self) -> None: first = self.create_simple_event() ip_dom = MISPObject('domain-ip') ip_dom.add_attribute('domain', value='google.fr') ip_dom.add_attribute('ip', value='8.8.8.8') first.add_object(ip_dom) try: + # Update with full event first = self.user_misp_connector.add_event(first) + first.objects[0].attributes[0].to_ids = False first.objects[0].add_attribute('ip', value='8.9.9.8') + first.objects[0].add_attribute('ip', '8.9.9.10') first = self.user_misp_connector.update_event(first) + self.assertFalse(first.objects[0].attributes[0].to_ids) self.assertEqual(first.objects[0].attributes[2].value, '8.9.9.8') + self.assertEqual(first.objects[0].attributes[3].value, '8.9.9.10') + # Update object attribute with update_attribute + attr = first.objects[0].attributes[1] + attr.to_ids = False + new_attr = self.user_misp_connector.update_attribute(attr) + self.assertFalse(new_attr.to_ids) + # Update object only + misp_object = self.user_misp_connector.get_object(first.objects[0].id) + misp_object.attributes[2].value = '8.9.9.9' + misp_object.attributes[2].to_ids = False + misp_object = self.user_misp_connector.update_object(misp_object) + self.assertEqual(misp_object.attributes[2].value, '8.9.9.9') + self.assertFalse(misp_object.attributes[2].to_ids) + # Test with add_attributes + second = self.create_simple_event() + ip_dom = MISPObject('domain-ip') + ip_dom.add_attribute('domain', value='google.fr', disable_correlation=True) + ip_dom.add_attributes('ip', {'value': '10.8.8.8', 'to_ids': False}, '10.9.8.8') + ip_dom.add_attributes('ip', '11.8.8.8', '11.9.8.8') + second.add_object(ip_dom) + second = self.user_misp_connector.add_event(second) + self.assertEqual(len(second.objects[0].attributes), 5) + self.assertTrue(second.objects[0].attributes[0].disable_correlation) + self.assertFalse(second.objects[0].attributes[1].to_ids) + self.assertTrue(second.objects[0].attributes[2].to_ids) + + # Test generic Tag methods + r = self.admin_misp_connector.tag(second, 'generic_tag_test') + self.assertTrue('successfully' in r['message'].lower() and f'({second.id})' in r['message'], r['message']) + second = self.user_misp_connector.get_event(second.id, pythonify=True) + self.assertTrue('generic_tag_test' == second.tags[0].name) + # # Test local tag, shouldn't update the timestamp + old_ts = second.timestamp + r = self.admin_misp_connector.tag(second, 'generic_tag_test_local', local=True) + second = self.user_misp_connector.get_event(second.id, pythonify=True) + self.assertEqual(old_ts, second.timestamp) + + r = self.admin_misp_connector.untag(second, 'generic_tag_test') + r = self.admin_misp_connector.untag(second, 'generic_tag_test_local') + self.assertTrue(r['message'].endswith(f'successfully removed from Event({second.id}).'), r['message']) + second = self.user_misp_connector.get_event(second.id, pythonify=True) + self.assertFalse(second.tags) + # NOTE: object tagging not supported yet + # r = self.admin_misp_connector.tag(second.objects[0].uuid, 'generic_tag_test') + # self.assertTrue(r['message'].endswith(f'successfully attached to Object({second.objects[0].id}).'), r['message']) + # r = self.admin_misp_connector.untag(second.objects[0].uuid, 'generic_tag_test') + # self.assertTrue(r['message'].endswith(f'successfully removed from Object({second.objects[0].id}).'), r['message']) + r = self.admin_misp_connector.tag(second.objects[0].attributes[0].uuid, 'generic_tag_test') + self.assertTrue('successfully' in r['message'].lower() and f'({second.objects[0].attributes[0].id})' in r['message'], r['message']) + attr = self.user_misp_connector.get_attribute(second.objects[0].attributes[0].uuid, pythonify=True) + self.assertTrue('generic_tag_test' == attr.tags[0].name) + r = self.admin_misp_connector.untag(second.objects[0].attributes[0].uuid, 'generic_tag_test') + self.assertTrue(r['message'].endswith(f'successfully removed from Attribute({second.objects[0].attributes[0].id}).'), r['message']) + second = self.user_misp_connector.get_event(second.id, pythonify=True) + for tag in second.objects[0].attributes[0].tags: + self.assertFalse('generic_tag_test' == tag.name) + attr = self.user_misp_connector.get_attribute(second.objects[0].attributes[0].uuid, pythonify=True) + self.assertFalse(attr.tags) + + # Delete tag to avoid polluting the db + tags = self.admin_misp_connector.tags(pythonify=True) + for t in tags: + if t.name == 'generic_tag_test': + response = self.admin_misp_connector.delete_tag(t) + self.assertEqual(response['message'], 'Tag deleted.') + + # Test soft delete object + second.delete_object(ip_dom.uuid) + self.assertTrue(second.objects[-1].deleted) + second = self.user_misp_connector.update_event(second) + self.assertFalse(second.objects) + second = self.user_misp_connector.get_event(second, deleted=True) + self.assertTrue(second.objects[-1].deleted) + + # Test delete object + r = self.user_misp_connector.delete_object(second.objects[0]) + self.assertEqual(r['message'], 'Object deleted', r) + new_second = self.admin_misp_connector.get_event(second, deleted=[0, 1], pythonify=True) + self.assertEqual(len(new_second.objects), 1) + # Hard delete + response = self.admin_misp_connector.delete_object(second.objects[0], hard=True) + self.assertEqual(response['message'], 'Object deleted') + new_second = self.admin_misp_connector.get_event(second, deleted=[0, 1], pythonify=True) + self.assertEqual(len(new_second.objects), 0) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) - def test_object_template(self): + def test_custom_template(self) -> None: + first = self.create_simple_event() + try: + with open('tests/viper-test-files/test_files/whoami.exe', 'rb') as f: + first.add_attribute('malware-sample', value='whoami.exe', data=BytesIO(f.read()), expand='binary') + first.run_expansions() + first = self.admin_misp_connector.add_event(first, pythonify=True) + self.assertEqual(len(first.objects), 7) + file_object = first.get_objects_by_name('file')[0] + file_object.force_misp_objects_path_custom('tests/mispevent_testfiles', 'overwrite_file') + file_object.add_attribute('test_overwrite', 'blah') + obj_json = self.admin_misp_connector.update_object(file_object) + self.assertTrue('Object' in obj_json, obj_json) + self.assertTrue('name' in obj_json['Object'], obj_json) + obj = MISPObject(obj_json['Object']['name']) + obj.from_dict(**obj_json) + self.assertEqual(obj.get_attributes_by_relation('test_overwrite')[0].value, 'blah') + + # FULL object add & update with custom template + new_object = MISPObject('overwrite_file', misp_objects_path_custom='tests/mispevent_testfiles') + new_object.add_attribute('test_overwrite', 'barbaz') + new_object.add_attribute('filename', 'barbaz.exe') + new_object = self.admin_misp_connector.add_object(first, new_object, pythonify=True) + self.assertEqual(new_object.get_attributes_by_relation('test_overwrite')[0].value, 'barbaz', new_object) + + new_object.force_misp_objects_path_custom('tests/mispevent_testfiles', 'overwrite_file') + new_object.add_attribute('filename', 'foobar.exe') + new_object = self.admin_misp_connector.update_object(new_object, pythonify=True) + self.assertEqual(new_object.get_attributes_by_relation('filename')[1].value, 'foobar.exe', new_object) + + # Get existing custom object, modify it, update on MISP + existing_object = self.admin_misp_connector.get_object(new_object.uuid, pythonify=True) + # existing_object.force_misp_objects_path_custom('tests/mispevent_testfiles', 'overwrite_file') + # The existing_object is a overwrite_file object, unless we uncomment the line above, type= is required below. + existing_object.add_attribute('pattern-in-file', value='foo', type='text') + updated_existing_object = self.admin_misp_connector.update_object(existing_object, pythonify=True) + self.assertEqual(updated_existing_object.get_attributes_by_relation('pattern-in-file')[0].value, 'foo', updated_existing_object) + + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_unknown_template(self) -> None: + first = self.create_simple_event() + attributeAsDict = [{'MyCoolAttribute': {'value': 'critical thing', 'type': 'text'}}, + {'MyCoolerAttribute': {'value': 'even worse', 'type': 'text', 'disable_correlation': True}}] + misp_object = GenericObjectGenerator('my-cool-template') + misp_object.generate_attributes(attributeAsDict) + misp_object.template_uuid = uuid4() + misp_object.template_id = 1 + misp_object.description = 'bar' + setattr(misp_object, 'meta-category', 'foo') + first.add_object(misp_object) + blah_object = MISPObject('BLAH_TEST') + blah_object.template_uuid = uuid4() + blah_object.template_id = 1 + blah_object.description = 'foo' + setattr(blah_object, 'meta-category', 'bar') + blah_object.add_reference(misp_object.uuid, "test relation") + blah_object.add_attribute('transaction-number', value='foo', type="text", disable_correlation=True) + first.add_object(blah_object) + try: + first = self.user_misp_connector.add_event(first) + self.assertEqual(len(first.objects[0].attributes), 2, first.objects[0].attributes) + self.assertFalse(first.objects[0].attributes[0].disable_correlation) + self.assertTrue(first.objects[0].attributes[1].disable_correlation) + self.assertTrue(first.objects[1].attributes[0].disable_correlation) + + # test update on totally unknown template + first.objects[1].add_attribute('my relation', value='foobar', type='text', disable_correlation=True) + updated_custom = self.user_misp_connector.update_object(first.objects[1], pythonify=True) + self.assertEqual(updated_custom.attributes[1].value, 'foobar', updated_custom) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_domain_ip_object(self) -> None: + first = self.create_simple_event() + try: + dom_ip_obj = DomainIPObject({'ip': ['1.1.1.1', {'value': '2.2.2.2', 'to_ids': False}], + 'first-seen': '20190101', + 'last-seen': '2019-02-03', + 'domain': 'circl.lu'}) + first.add_object(dom_ip_obj) + first = self.user_misp_connector.add_event(first) + self.assertEqual(len(first.objects[0].attributes), 5) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_asn_object(self) -> None: + first = self.create_simple_event() + try: + dom_ip_obj = ASNObject({'asn': '12345', + 'first-seen': '20190101', + 'last-seen': '2019-02-03'}) + first.add_object(dom_ip_obj) + first = self.user_misp_connector.add_event(first) + self.assertEqual(len(first.objects[0].attributes), 3) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_object_template(self) -> None: r = self.admin_misp_connector.update_object_templates() self.assertEqual(type(r), list) - if not travis_run: - template = self.admin_misp_connector.get_object_template('688c46fb-5edb-40a3-8273-1af7923e2215') - self.assertEqual(template['ObjectTemplate']['uuid'], '688c46fb-5edb-40a3-8273-1af7923e2215') + object_templates = self.admin_misp_connector.object_templates(pythonify=True) + self.assertTrue(isinstance(object_templates, list)) + for object_template in object_templates: + if object_template.name == 'file': + break - def test_tags(self): + template = self.admin_misp_connector.get_object_template(object_template.uuid, pythonify=True) + self.assertEqual(template.name, 'file') + + raw_template = self.admin_misp_connector.get_raw_object_template('domain-ip') + raw_template['uuid'] = '4' + mo = MISPObject('domain-ip', misp_objects_template_custom=raw_template) + mo.add_attribute('ip', '8.8.8.8') + mo.add_attribute('domain', 'google.fr') + self.assertEqual(mo.template_uuid, '4') + + def test_tags(self) -> None: # Get list - tags = self.admin_misp_connector.get_tags_list() + tags = self.admin_misp_connector.tags(pythonify=True) self.assertTrue(isinstance(tags, list)) # Get tag for tag in tags: - if not tag['hide_tag']: + if not tag.hide_tag: break - tag = self.admin_misp_connector.get_tag(tags[0]['id']) + tag = self.admin_misp_connector.get_tag(tag, pythonify=True) self.assertTrue('name' in tag) - r = self.admin_misp_connector.disable_tag(tag['id']) - self.assertTrue(r['Tag']['hide_tag']) - r = self.admin_misp_connector.enable_tag(tag['id']) - self.assertFalse(r['Tag']['hide_tag']) + # Enable by MISPTag + tag = self.admin_misp_connector.disable_tag(tag, pythonify=True) + self.assertTrue(tag.hide_tag) + tag = self.admin_misp_connector.enable_tag(tag, pythonify=True) + self.assertFalse(tag.hide_tag) + # Add tag + tag = MISPTag() + tag.name = 'this is a test tag' + new_tag = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertEqual(new_tag.name, tag.name) + # Add non-exportable tag + tag = MISPTag() + tag.name = 'non-exportable tag' + tag.exportable = False + non_exportable_tag = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertFalse(non_exportable_tag.exportable) + first = self.create_simple_event() + first.attributes[0].add_tag('non-exportable tag') + # Add tag restricted to an org + tag = MISPTag() + tag.name = f'restricted to org {self.test_org.id}' + tag.org_id = self.test_org.id + tag_org_restricted = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertEqual(tag_org_restricted.org_id, tag.org_id) + # Add tag restricted to a user + tag.name = f'restricted to user {self.test_usr.id}' + tag.user_id = self.test_usr.id + tag_user_restricted = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertEqual(tag_user_restricted.user_id, tag.user_id) + try: + first = self.user_misp_connector.add_event(first) + self.assertFalse(first.attributes[0].tags) + first = self.admin_misp_connector.get_event(first, pythonify=True) + # Reference: https://github.com/MISP/MISP/issues/1394 + self.assertFalse(first.attributes[0].tags) + # Reference: https://github.com/MISP/PyMISP/issues/483 + r = self.delegate_user_misp_connector.tag(first, tag_org_restricted) + # FIXME: The error message changed and is unhelpful. + # self.assertEqual(r['errors'][1]['message'], 'Invalid Tag. This tag can only be set by a fixed organisation.') + self.assertEqual(r['errors'][1]['message'], 'Invalid Target.') + r = self.user_misp_connector.tag(first, tag_org_restricted) + self.assertTrue('successfully' in r['message'].lower() and f'({first.id})' in r['message'], r['message']) + r = self.pub_misp_connector.tag(first.attributes[0], tag_user_restricted) + self.assertIn('Invalid Tag. This tag can only be set by a fixed user.', r['errors'][1]['errors']) + r = self.user_misp_connector.tag(first.attributes[0], tag_user_restricted) + self.assertTrue('successfully' in r['message'].lower() and f'({first.attributes[0].id})' in r['message'], r['message']) + first = self.user_misp_connector.get_event(first, pythonify=True) + self.assertTrue(len(first.attributes[0].tags) == 1) + # test delete tag on attribute edit + deleted_tag = first.attributes[0].tags[0] + first.attributes[0].tags[0].delete() + attribute = self.user_misp_connector.update_attribute(first.attributes[0], pythonify=True) + for tag in attribute.tags: + self.assertTrue(tag.name != deleted_tag.name) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) - def test_add_event_with_attachment_object_controller(self): + # Search tag + # Partial search + tags = self.admin_misp_connector.search_tags(f'{new_tag.name[:5]}%', pythonify=True) + self.assertEqual(tags[0].name, 'this is a test tag') + # No tags found + tags = self.admin_misp_connector.search_tags('not a tag') + self.assertFalse(tags) + + # Update tag + non_exportable_tag.name = 'non-exportable tag - edit' + non_exportable_tag_edited = self.admin_misp_connector.update_tag(non_exportable_tag, pythonify=True) + self.assertTrue(non_exportable_tag_edited.name == 'non-exportable tag - edit', non_exportable_tag_edited.to_json(indent=2)) + + # Delete tag + response = self.admin_misp_connector.delete_tag(new_tag) + self.assertEqual(response['message'], 'Tag deleted.') + response = self.admin_misp_connector.delete_tag(non_exportable_tag) + self.assertEqual(response['message'], 'Tag deleted.') + response = self.admin_misp_connector.delete_tag(tag_org_restricted) + response = self.admin_misp_connector.delete_tag(tag_user_restricted) + + def test_add_event_with_attachment_object_controller(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) fo, peo, seos = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') for s in seos: - template_id = self.user_misp_connector.get_object_template_id(s.template_uuid) - r = self.user_misp_connector.add_object(first.id, template_id, s) - self.assertTrue('Object' in r, r) - self.assertEqual(r['Object']['name'], 'pe-section', r) + r = self.user_misp_connector.add_object(first, s) + self.assertEqual(r.name, 'pe-section', r) - template_id = self.user_misp_connector.get_object_template_id(peo.template_uuid) - r = self.user_misp_connector.add_object(first.id, template_id, peo) - self.assertTrue('Object' in r, r) - self.assertEqual(r['Object']['name'], 'pe', r) + r = self.user_misp_connector.add_object(first, peo, pythonify=True) + self.assertEqual(r.name, 'pe', r) for ref in peo.ObjectReference: r = self.user_misp_connector.add_object_reference(ref) - self.assertTrue('ObjectReference' in r, r) + self.assertEqual(r.object_uuid, peo.uuid, r.to_json()) - template_id = self.user_misp_connector.get_object_template_id(fo.template_uuid) - r = self.user_misp_connector.add_object(first.id, template_id, fo) - self.assertTrue('Object' in r, r) - self.assertEqual(r['Object']['name'], 'file', r) - for ref in fo.ObjectReference: - r = self.user_misp_connector.add_object_reference(ref) - self.assertTrue('ObjectReference' in r, r) + r = self.user_misp_connector.add_object(first, fo) + obj_attrs = r.get_attributes_by_relation('ssdeep') + self.assertEqual(len(obj_attrs), 1, obj_attrs) + self.assertEqual(r.name, 'file', r) + + # Test break_on_duplicate at object level + fo_dup, peo_dup, _ = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') + r = self.user_misp_connector.add_object(first, peo_dup, break_on_duplicate=True) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test break on duplicate with breakOnDuplicate key in object + fo_dup.breakOnDuplicate = True + r = self.user_misp_connector.add_object(first, fo_dup) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test refs + r = self.user_misp_connector.add_object_reference(fo.ObjectReference[0]) + self.assertEqual(r.object_uuid, fo.uuid, r.to_json()) + self.assertEqual(r.referenced_uuid, peo.uuid, r.to_json()) + r = self.user_misp_connector.delete_object_reference(r) + self.assertEqual(r['message'], 'ObjectReference deleted') finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - def test_add_event_with_attachment(self): + def test_add_event_with_attachment_object_controller__hard(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) + fo, peo, seos = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') + for s in seos: + r = self.user_misp_connector.add_object(first, s) + self.assertEqual(r.name, 'pe-section', r) + + r = self.user_misp_connector.add_object(first, peo, pythonify=True) + self.assertEqual(r.name, 'pe', r) + for ref in peo.ObjectReference: + r = self.user_misp_connector.add_object_reference(ref) + self.assertEqual(r.object_uuid, peo.uuid, r.to_json()) + + r = self.user_misp_connector.add_object(first, fo) + obj_attrs = r.get_attributes_by_relation('ssdeep') + self.assertEqual(len(obj_attrs), 1, obj_attrs) + self.assertEqual(r.name, 'file', r) + + # Test break_on_duplicate at object level + fo_dup, peo_dup, _ = make_binary_objects('tests/viper-test-files/test_files/whoami.exe') + r = self.user_misp_connector.add_object(first, peo_dup, break_on_duplicate=True) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test break on duplicate with breakOnDuplicate key in object + fo_dup.breakOnDuplicate = True + r = self.user_misp_connector.add_object(first, fo_dup) + self.assertTrue("Duplicate object found" in r['errors'][1]['errors'], r) + + # Test refs + r = self.user_misp_connector.add_object_reference(fo.ObjectReference[0]) + self.assertEqual(r.object_uuid, fo.uuid, r.to_json()) + self.assertEqual(r.referenced_uuid, peo.uuid, r.to_json()) + r = self.user_misp_connector.delete_object_reference(r, hard=True) + self.assertEqual(r['message'], 'ObjectReference deleted') + # TODO: verify that the reference is not soft-deleted instead + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_lief_and_sign(self) -> None: + first = self.create_simple_event() + try: + first = self.user_misp_connector.add_event(first) + fo, peo, seos = make_binary_objects('tests/viper-test-files/test_files/chromeinstall-8u31.exe') + # Make sure VT imphash is the same as the one generated by lief + vtimphash = '697c52d3bf08cccfd62da7bc503fdceb' + imphash = peo.get_attributes_by_relation('imphash')[0] + self.assertEqual(imphash.value, vtimphash) + # Make sure VT authentihash is the same as the one generated by lief + vtauthentihash = 'eb7be5a6f8ef4c2da5a183b4a3177153183e344038c56a00f5d88570a373d858' + authentihash = peo.get_attributes_by_relation('authentihash')[0] + self.assertEqual(authentihash.value, vtauthentihash) + + # The following is a duplicate of examples/add_file_object.py + if seos: + for s in seos: + self.user_misp_connector.add_object(first, s) + + if peo: + if hasattr(peo, 'certificates') and hasattr(peo, 'signers'): + # special authenticode case for PE objects + for c in peo.certificates: + self.user_misp_connector.add_object(first, c, pythonify=True) + for s in peo.signers: + self.user_misp_connector.add_object(first, s, pythonify=True) + del peo.certificates + del peo.signers + del peo.sections + self.user_misp_connector.add_object(first, peo, pythonify=True) + for ref in peo.ObjectReference: + self.user_misp_connector.add_object_reference(ref) + + if fo: + self.user_misp_connector.add_object(first, fo, pythonify=True) + for ref in fo.ObjectReference: + self.user_misp_connector.add_object_reference(ref) + + first = self.user_misp_connector.get_event(first, pythonify=True) + self.assertEqual(len(first.objects), 10, first.objects) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_add_event_with_attachment(self) -> None: + first_send = self.create_simple_event() + try: + first = self.user_misp_connector.add_event(first_send) + self.assertTrue(isinstance(first, MISPEvent), first) file_obj, bin_obj, sections = make_binary_objects('tests/viper-test-files/test_files/whoami.exe', standalone=False) first.add_object(file_obj) first.add_object(bin_obj) for s in sections: first.add_object(s) self.assertEqual(len(first.objects[0].references), 1) - self.assertEqual(first.objects[0].references[0].relationship_type, 'included-in') + self.assertEqual(first.objects[0].references[0].relationship_type, 'includes') first = self.user_misp_connector.update_event(first) self.assertEqual(len(first.objects[0].references), 1) - self.assertEqual(first.objects[0].references[0].relationship_type, 'included-in') + self.assertEqual(first.objects[0].references[0].relationship_type, 'includes') finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - def test_taxonomies(self): + def test_taxonomies(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_taxonomies() self.assertEqual(r['name'], 'All taxonomy libraries are up to date already.') # Get list - taxonomies = self.admin_misp_connector.get_taxonomies_list() + taxonomies = self.admin_misp_connector.taxonomies(pythonify=True) self.assertTrue(isinstance(taxonomies, list)) + + # Test fetching taxonomy by ID list_name_test = 'tlp' for tax in taxonomies: - if tax['Taxonomy']['namespace'] == list_name_test: + if tax.namespace == list_name_test: break - if not travis_run: - r = self.admin_misp_connector.get_taxonomy(tax['Taxonomy']['id']) - self.assertEqual(r['Taxonomy']['namespace'], list_name_test) - self.assertTrue('enabled' in r['Taxonomy']) - r = self.admin_misp_connector.enable_taxonomy(tax['Taxonomy']['id']) + r = self.admin_misp_connector.get_taxonomy(tax, pythonify=True) + self.assertEqual(r.namespace, list_name_test) + self.assertTrue('enabled' in r) + + # Test fetching taxonomy by namespace + r = self.admin_misp_connector.get_taxonomy("tlp", pythonify=True) + self.assertEqual(r.namespace, "tlp") + + r = self.admin_misp_connector.enable_taxonomy(tax) self.assertEqual(r['message'], 'Taxonomy enabled') - r = self.admin_misp_connector.disable_taxonomy(tax['Taxonomy']['id']) + + r = self.admin_misp_connector.enable_taxonomy_tags(tax) + self.assertEqual(r['name'], 'The tag(s) has been saved.') + + r = self.admin_misp_connector.disable_taxonomy(tax) self.assertEqual(r['message'], 'Taxonomy disabled') - def test_warninglists(self): + # Test toggling the required status + r = self.admin_misp_connector.set_taxonomy_required(tax, not tax.required) + self.assertEqual(r['message'], 'Taxonomy toggleRequireded') + + updatedTax = self.admin_misp_connector.get_taxonomy(tax, pythonify=True) + self.assertFalse(tax.required == updatedTax.required) + + # Return back to default required status + r = self.admin_misp_connector.set_taxonomy_required(tax, not tax.required) + + def test_warninglists(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_warninglists() self.assertTrue('name' in r, msg=r) @@ -1016,91 +1783,1952 @@ class TestComprehensive(unittest.TestCase): except Exception: print(r) # Get list - r = self.admin_misp_connector.get_warninglists() - # FIXME It returns Warninglists object instead of a list of warning lists directly. This is inconsistent. - warninglists = r['Warninglists'] + warninglists = self.admin_misp_connector.warninglists(pythonify=True) self.assertTrue(isinstance(warninglists, list)) list_name_test = 'List of known hashes with common false-positives (based on Florian Roth input list)' for wl in warninglists: - if wl['Warninglist']['name'] == list_name_test: + if wl.name == list_name_test: break - testwl = wl['Warninglist'] - r = self.admin_misp_connector.get_warninglist(testwl['id']) - self.assertEqual(r['Warninglist']['name'], list_name_test) - self.assertTrue('WarninglistEntry' in r['Warninglist']) - r = self.admin_misp_connector.enable_warninglist(testwl['id']) + testwl = wl + r = self.admin_misp_connector.get_warninglist(testwl, pythonify=True) + self.assertEqual(r.name, list_name_test) + self.assertTrue('WarninglistEntry' in r) + r = self.admin_misp_connector.enable_warninglist(testwl) self.assertEqual(r['success'], '1 warninglist(s) enabled') - r = self.admin_misp_connector.disable_warninglist(testwl['id']) + # Check if a value is in a warning list + md5_empty_file = 'd41d8cd98f00b204e9800998ecf8427e' + r = self.user_misp_connector.values_in_warninglist([md5_empty_file]) + self.assertEqual(r[md5_empty_file][0]['name'], list_name_test) + + r = self.admin_misp_connector.disable_warninglist(testwl) self.assertEqual(r['success'], '1 warninglist(s) disabled') - def test_noticelists(self): + def test_noticelists(self) -> None: # Make sure we're up-to-date r = self.admin_misp_connector.update_noticelists() self.assertEqual(r['name'], 'All noticelists are up to date already.') # Get list - noticelists = self.admin_misp_connector.get_noticelists() + noticelists = self.admin_misp_connector.noticelists(pythonify=True) self.assertTrue(isinstance(noticelists, list)) list_name_test = 'gdpr' for nl in noticelists: - if nl['Noticelist']['name'] == list_name_test: + if nl.name == list_name_test: break testnl = nl - r = self.admin_misp_connector.get_noticelist(testnl['Noticelist']['id']) - self.assertEqual(r['Noticelist']['name'], list_name_test) - self.assertTrue('NoticelistEntry' in r['Noticelist']) - r = self.admin_misp_connector.enable_noticelist(testnl['Noticelist']['id']) - self.assertTrue(r['Noticelist']['enabled']) - r = self.admin_misp_connector.disable_noticelist(testnl['Noticelist']['id']) - self.assertFalse(r['Noticelist']['enabled']) + r = self.admin_misp_connector.get_noticelist(testnl, pythonify=True) + self.assertEqual(r.name, list_name_test) + # FIXME: https://github.com/MISP/MISP/issues/4856 + self.assertTrue('NoticelistEntry' in r) + r = self.admin_misp_connector.enable_noticelist(testnl) + self.assertTrue(r['Noticelist']['enabled'], r) + r = self.admin_misp_connector.disable_noticelist(testnl) + self.assertFalse(r['Noticelist']['enabled'], r) - def test_galaxies(self): - if not travis_run: - # Make sure we're up-to-date - r = self.admin_misp_connector.update_galaxies() - self.assertEqual(r['name'], 'Galaxies updated.') - # Get list - galaxies = self.admin_misp_connector.get_galaxies() - self.assertTrue(isinstance(galaxies, list)) - list_name_test = 'Mobile Attack - Attack Pattern' - for galaxy in galaxies: - if galaxy['Galaxy']['name'] == list_name_test: - break - r = self.admin_misp_connector.get_galaxy(galaxy['Galaxy']['id']) - self.assertEqual(r['Galaxy']['name'], list_name_test) - self.assertTrue('GalaxyCluster' in r) + def test_correlation_exclusions(self) -> None: + newce = MISPCorrelationExclusion() + newce.value = "test-correlation-exclusion" + r = self.admin_misp_connector.add_correlation_exclusion(newce, pythonify=True) + self.assertEqual(r.value, newce.value) + correlation_exclusions = self.admin_misp_connector.correlation_exclusions(pythonify=True) + self.assertTrue(isinstance(correlation_exclusions, list)) + testce = correlation_exclusions[0] + r = self.admin_misp_connector.get_correlation_exclusion(testce, pythonify=True) + self.assertEqual(r.value, testce.value) + r = self.admin_misp_connector.delete_correlation_exclusion(r) + self.assertTrue(r['success']) + r = self.admin_misp_connector.clean_correlation_exclusions() + self.assertTrue(r['success']) - def test_zmq(self): + def test_galaxies(self) -> None: + # Make sure we're up-to-date + r = self.admin_misp_connector.update_galaxies() + self.assertEqual(r['name'], 'Galaxies updated.') + # Get list + galaxies = self.admin_misp_connector.galaxies(pythonify=True) + self.assertTrue(isinstance(galaxies, list)) + list_name_test = 'Mobile Attack - Attack Pattern' + for galaxy in galaxies: + if galaxy.name == list_name_test: + break + r = self.admin_misp_connector.get_galaxy(galaxy, pythonify=True) + self.assertEqual(r.name, list_name_test) + # FIXME: Fails due to https://github.com/MISP/MISP/issues/4855 + # self.assertTrue('GalaxyCluster' in r) + + def test_zmq(self) -> None: first = self.create_simple_event() try: first = self.user_misp_connector.add_event(first) - if not travis_run: - r = self.admin_misp_connector.pushEventToZMQ(first.id) - self.assertEqual(r['message'], 'Event published to ZMQ') + r = self.admin_misp_connector.push_event_to_ZMQ(first) + self.assertEqual(r['message'], 'Event published to ZMQ') finally: # Delete event - self.admin_misp_connector.delete_event(first.id) + self.admin_misp_connector.delete_event(first) - @unittest.skip("Currently failing") - def test_search_type_event_csv(self): + def test_csv_loader(self) -> None: + csv1 = CSVLoader(template_name='file', csv_path=Path('tests/csv_testfiles/valid_fieldnames.csv')) + event = MISPEvent() + event.info = 'Test event from CSV loader' + for o in csv1.load(): + event.add_object(**o) + + csv2 = CSVLoader(template_name='file', csv_path=Path('tests/csv_testfiles/invalid_fieldnames.csv'), + fieldnames=['sha1', 'filename', 'size-in-bytes'], has_fieldnames=True) + try: + first = self.user_misp_connector.add_event(event) + for o in csv2.load(): + new_object = self.user_misp_connector.add_object(first, o) + self.assertEqual(len(new_object.attributes), 3) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_user(self) -> None: + # Get list + users = self.admin_misp_connector.users(pythonify=True) + self.assertTrue(isinstance(users, list)) + users_email = 'testusr@user.local' + for user in users: + if user.email == users_email: + break + else: + raise Exception('Unable to find that user') + self.assertEqual(user.email, users_email) + # get user + user = self.user_misp_connector.get_user(pythonify=True) + # self.assertEqual(user.authkey, self.test_usr.authkey) + # Update user + user.email = 'foo@bar.de' + user = self.admin_misp_connector.update_user(user, pythonify=True) + self.assertEqual(user.email, 'foo@bar.de') + # get API key + key = self.user_misp_connector.get_new_authkey() + self.assertTrue(isinstance(key, str)) + + def test_organisation(self) -> None: + # Get list + orgs = self.admin_misp_connector.organisations(pythonify=True) + self.assertTrue(isinstance(orgs, list)) + org_name = 'ORGNAME' + for org in orgs: + if org.name == org_name: + break + self.assertEqual(org.name, org_name) + # Get org + organisation = self.user_misp_connector.get_organisation(self.test_usr.org_id) + self.assertEqual(organisation.name, 'Test Org') + # Update org + organisation.name = 'blah' + organisation = self.admin_misp_connector.update_organisation(organisation, pythonify=True) + self.assertEqual(organisation.name, 'blah', organisation) + + def test_org_search(self) -> None: + orgs = self.admin_misp_connector.organisations(pythonify=True) + org_name = 'ORGNAME' + # Search by the org name + orgs = self.admin_misp_connector.organisations(search=org_name, pythonify=True) + # There should be one org returned + self.assertTrue(len(orgs) == 1) + + # This org should have the name ORGNAME + self.assertEqual(orgs[0].name, org_name) + + def test_user_search(self) -> None: + users = self.admin_misp_connector.users(pythonify=True) + emailAddr = users[0].email + + users = self.admin_misp_connector.users(search=emailAddr) + self.assertTrue(len(users) == 1) + self.assertEqual(users[0]['User']['email'], emailAddr) + + users = self.admin_misp_connector.users( + search=emailAddr, + organisation=users[0]['Organisation']['id'], + pythonify=True + ) + self.assertTrue(len(users) == 1) + self.assertEqual(users[0].email, emailAddr) + + def test_attribute(self) -> None: + first = self.create_simple_event() + second = self.create_simple_event() + a = second.add_attribute('ip-src', '11.11.11.11') + a.add_tag('testtag_admin_created') + second.distribution = Distribution.all_communities + try: + first = self.user_misp_connector.add_event(first) + second = self.admin_misp_connector.add_event(second, pythonify=True) + # Get attribute + attribute = self.user_misp_connector.get_attribute(first.attributes[0]) + self.assertEqual(first.attributes[0].uuid, attribute.uuid) + # Add attribute + new_attribute = MISPAttribute() + new_attribute.value = '1.2.3.4' + new_attribute.type = 'ip-dst' + new_attribute = self.user_misp_connector.add_attribute(first, new_attribute) + self.assertTrue(isinstance(new_attribute, MISPAttribute), new_attribute) + self.assertEqual(new_attribute.value, '1.2.3.4', new_attribute) + # Test attribute already in event + # new_attribute.uuid = str(uuid4()) + # new_attribute = self.user_misp_connector.add_attribute(first, new_attribute) + new_similar = MISPAttribute() + new_similar.value = '1.2.3.4' + new_similar.type = 'ip-dst' + similar_error = self.user_misp_connector.add_attribute(first, new_similar) + self.assertEqual(similar_error['errors'][1]['errors']['value'][0], 'A similar attribute already exists for this event.') + + # Test add attribute break_on_duplicate=False + time.sleep(5) + new_similar = MISPAttribute() + new_similar.value = '1.2.3.4' + new_similar.type = 'ip-dst' + new_similar = self.user_misp_connector.add_attribute(first, new_similar, break_on_duplicate=False) + self.assertTrue(isinstance(new_similar, MISPAttribute), new_similar) + self.assertGreater(new_similar.timestamp, new_attribute.timestamp) + + # Test add multiple attributes at once + attr0 = MISPAttribute() + attr0.value = '0.0.0.0' + attr0.type = 'ip-dst' + response = self.user_misp_connector.add_attribute(first, [attr0]) + time.sleep(5) + self.assertTrue(isinstance(response['attributes'], list), response['attributes']) + self.assertEqual(response['attributes'][0].value, '0.0.0.0') + attr1 = MISPAttribute() + attr1.value = '1.2.3.4' + attr1.type = 'ip-dst' + attr2 = MISPAttribute() + attr2.value = '1.2.3.5' + attr2.type = 'ip-dst' + attr3 = MISPAttribute() + attr3.value = first.attributes[0].value + attr3.type = first.attributes[0].type + attr4 = MISPAttribute() + attr4.value = '1.2.3.6' + attr4.type = 'ip-dst' + attr4.add_tag('tlp:amber___test_unique_not_created') + attr4.add_tag('testtag_admin_created') + response = self.user_misp_connector.add_attribute(first, [attr1, attr2, attr3, attr4]) + time.sleep(5) + self.assertTrue(isinstance(response['attributes'], list), response['attributes']) + self.assertEqual(response['attributes'][0].value, '1.2.3.5') + self.assertEqual(response['attributes'][1].value, '1.2.3.6') + self.assertTrue(isinstance(response['attributes'][1].tags, list), response['attributes'][1].to_json()) + self.assertTrue(len(response['attributes'][1].tags), response['attributes'][1].to_json()) + self.assertEqual(response['attributes'][1].tags[0].name, 'testtag_admin_created') + self.assertEqual(response['errors']['attribute_0']['value'][0], 'A similar attribute already exists for this event.') + self.assertEqual(response['errors']['attribute_2']['value'][0], 'A similar attribute already exists for this event.') + + # Add attribute as proposal + new_proposal = MISPAttribute() + new_proposal.value = '5.2.3.4' + new_proposal.type = 'ip-dst' + new_proposal.category = 'Network activity' + new_proposal = self.user_misp_connector.add_attribute_proposal(first.id, new_proposal) + self.assertEqual(new_proposal.value, '5.2.3.4') + # Update attribute + new_attribute.value = '5.6.3.4' + new_attribute = self.user_misp_connector.update_attribute(new_attribute) + self.assertEqual(new_attribute.value, '5.6.3.4') + # Update attribute as proposal + new_proposal_update = self.user_misp_connector.update_attribute_proposal(new_attribute.id, {'to_ids': False}) + self.assertEqual(new_proposal_update.to_ids, False) + # Delete attribute as proposal + proposal_delete = self.user_misp_connector.delete_attribute_proposal(new_attribute) + self.assertTrue(proposal_delete['saved']) + # Get attribute proposal + temp_new_proposal = self.user_misp_connector.get_attribute_proposal(new_proposal) + self.assertEqual(temp_new_proposal.uuid, new_proposal.uuid) + # Get attribute proposal*S* + proposals = self.user_misp_connector.attribute_proposals() + self.assertTrue(isinstance(proposals, list)) + self.assertEqual(len(proposals), 3) + self.assertEqual(proposals[0].value, '5.2.3.4') + # Get proposals on a specific event + self.admin_misp_connector.add_attribute_proposal(second.id, {'type': 'ip-src', 'value': '123.123.123.1'}) + proposals = self.admin_misp_connector.attribute_proposals(pythonify=True) + self.assertTrue(isinstance(proposals, list)) + self.assertEqual(len(proposals), 4) + proposals = self.admin_misp_connector.attribute_proposals(second, pythonify=True) + self.assertTrue(isinstance(proposals, list)) + self.assertEqual(len(proposals), 1) + self.assertEqual(proposals[0].value, '123.123.123.1') + # Accept attribute proposal - New attribute + self.user_misp_connector.accept_attribute_proposal(new_proposal) + first = self.user_misp_connector.get_event(first) + self.assertEqual(first.attributes[-1].value, '5.2.3.4') + # Accept attribute proposal - Attribute update + response = self.user_misp_connector.accept_attribute_proposal(new_proposal_update) + self.assertEqual(response['message'], 'Proposed change accepted.') + attribute = self.user_misp_connector.get_attribute(new_attribute) + self.assertEqual(attribute.to_ids, False) + # Discard attribute proposal + new_proposal_update = self.user_misp_connector.update_attribute_proposal(new_attribute.id, {'to_ids': True}) + response = self.user_misp_connector.discard_attribute_proposal(new_proposal_update) + self.assertEqual(response['message'], 'Proposal discarded.') + attribute = self.user_misp_connector.get_attribute(new_attribute) + self.assertEqual(attribute.to_ids, False) + + # Test fallback to proposal if the user doesn't own the event + prop_attr = MISPAttribute() + prop_attr.from_dict(**{'type': 'ip-dst', 'value': '123.43.32.21'}) + # Add attribute on event owned by someone else + attribute = self.user_misp_connector.add_attribute(second.id, prop_attr) + self.assertTrue(isinstance(attribute, MISPShadowAttribute), attribute) + # Test if add proposal without category works - https://github.com/MISP/MISP/issues/4868 + attribute = self.user_misp_connector.add_attribute(second.id, {'type': 'ip-dst', 'value': '123.43.32.22'}) + self.assertTrue(isinstance(attribute, MISPShadowAttribute), attribute) + # Add attribute with the same value as an existing proposal + prop_attr.uuid = str(uuid4()) + attribute = self.admin_misp_connector.add_attribute(second, prop_attr, pythonify=True) + prop_attr.uuid = str(uuid4()) + # Add a duplicate attribute (same value) + attribute = self.admin_misp_connector.add_attribute(second, prop_attr, pythonify=True) + self.assertTrue('errors' in attribute) + # Update attribute owned by someone else + attribute = self.user_misp_connector.update_attribute({'comment': 'blah'}, second.attributes[0].id) + self.assertTrue(isinstance(attribute, MISPShadowAttribute), attribute) + self.assertEqual(attribute.value, second.attributes[0].value) + second = self.admin_misp_connector.get_event(second, pythonify=True) + self.assertEqual(len(second.attributes), 3) + # Delete attribute owned by someone else + response = self.user_misp_connector.delete_attribute(second.attributes[1]) + self.assertTrue(response['success']) + # Delete attribute owned by user + response = self.admin_misp_connector.delete_attribute(second.attributes[1]) + self.assertEqual(response['message'], 'Attribute deleted.') + # Hard delete + response = self.admin_misp_connector.delete_attribute(second.attributes[0], hard=True) + self.assertEqual(response['message'], 'Attribute deleted.') + new_second = self.admin_misp_connector.get_event(second, deleted=[0, 1], pythonify=True) + self.assertEqual(len(new_second.attributes), 2) + + # Test attribute*S* + attributes = self.admin_misp_connector.attributes() + self.assertEqual(len(attributes), 7) + # attributes = self.user_misp_connector.attributes() + # self.assertEqual(len(attributes), 5) + # Test event*S* + events = self.admin_misp_connector.events() + self.assertEqual(len(events), 2) + events = self.user_misp_connector.events() + self.assertEqual(len(events), 2) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + + def test_search_type_event_csv(self) -> None: try: first, second, third = self.environment() # Search as admin - events = self.admin_misp_connector.search(return_format='csv', timestamp=first.timestamp.timestamp()) - print(events) + events = self.admin_misp_connector.search(return_format='csv', timestamp=first.timestamp.timestamp(), pythonify=True) + self.assertTrue(isinstance(events, list)) + self.assertEqual(len(events), 8) attributes_types_search = self.admin_misp_connector.build_complex_query(or_parameters=['ip-src', 'ip-dst']) events = self.admin_misp_connector.search(return_format='csv', timestamp=first.timestamp.timestamp(), - type_attribute=attributes_types_search) - print(events) + type_attribute=attributes_types_search, pythonify=True) + self.assertTrue(isinstance(events, list)) + self.assertEqual(len(events), 6) finally: # Delete event - self.admin_misp_connector.delete_event(first.id) - self.admin_misp_connector.delete_event(second.id) - self.admin_misp_connector.delete_event(third.id) + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) - def test_live_acl(self): - missing_acls = self.admin_misp_connector.get_live_query_acl() + @unittest.skip("Not very important, skip for now.") + def test_search_logs(self) -> None: + r = self.admin_misp_connector.update_user({'email': 'testusr-changed@user.local'}, self.test_usr) + r = self.admin_misp_connector.search_logs(model='User', created=date.today(), pythonify=True) + for entry in r[-1:]: + self.assertEqual(entry.action, 'edit') + r = self.admin_misp_connector.search_logs(email='admin@admin.test', created=date.today(), pythonify=True) + for entry in r[-1:]: + self.assertEqual(entry.action, 'edit') + + self.admin_misp_connector.update_user({'email': 'testusr@user.local'}, self.test_usr) + time.sleep(5) + r = self.admin_misp_connector.search_logs(model='User', limit=1, page=1, created=date.today(), pythonify=True) + if r: + last_change = r[0] + self.assertEqual(last_change['change'], 'email (testusr-changed@user.local) => (testusr@user.local)', last_change) + else: + raise Exception('Unable to find log entry after updating the user') + + def test_db_schema(self) -> None: + diag = self.admin_misp_connector.db_schema_diagnostic() + self.assertEqual(diag['actual_db_version'], diag['expected_db_version'], diag) + + def test_live_acl(self) -> None: + missing_acls = self.admin_misp_connector.remote_acl() self.assertEqual(missing_acls, [], msg=missing_acls) + def test_roles(self) -> None: + role = self.admin_misp_connector.set_default_role(4) + self.assertEqual(role['message'], 'Default role set.') + self.admin_misp_connector.set_default_role(3) + roles = self.admin_misp_connector.roles(pythonify=True) + self.assertTrue(isinstance(roles, list)) + try: + # Create a new role + new_role = MISPRole() + new_role.name = 'testrole' + new_role = self.admin_misp_connector.add_role(new_role, pythonify=True) + self.assertFalse(new_role.perm_sighting) + new_role.perm_sighting = True + new_role.max_execution_time = 1234 + updated_role = self.admin_misp_connector.update_role(new_role, pythonify=True) + self.assertTrue(updated_role.perm_sighting) + self.assertEqual(updated_role.max_execution_time, '1234') + finally: + self.admin_misp_connector.delete_role(new_role) + + def test_describe_types(self) -> None: + remote = self.admin_misp_connector.describe_types_remote + remote_types = remote.pop('types') + remote_categories = remote.pop('categories') + remote_category_type_mappings = remote.pop('category_type_mappings') + + local = dict(self.admin_misp_connector.describe_types_local) + local_types = local.pop('types') + local_categories = local.pop('categories') + local_category_type_mappings = local.pop('category_type_mappings') + + self.assertDictEqual(remote, local) + self.assertEqual(sorted(remote_types), sorted(local_types)) + self.assertEqual(sorted(remote_categories), sorted(local_categories)) + for category, mapping in remote_category_type_mappings.items(): + self.assertEqual(sorted(local_category_type_mappings[category]), sorted(mapping)) + for typ in mapping: + self.assertIn(typ, remote_types) + + @unittest.skip("Tested elsewhere.") + def test_versions(self) -> None: + self.assertEqual(self.user_misp_connector.version, self.user_misp_connector.pymisp_version_master) + self.assertEqual(self.user_misp_connector.misp_instance_version['version'], + self.user_misp_connector.misp_instance_version_master['version']) + + def test_statistics(self) -> None: + try: + # Attributes + first, second, third = self.environment() + expected_attr_stats = {'ip-dst': '2', 'ip-src': '1', 'text': '5'} + attr_stats = self.admin_misp_connector.attributes_statistics() + self.assertDictEqual(attr_stats, expected_attr_stats) + expected_attr_stats_percent = {'ip-dst': '25%', 'ip-src': '12.5%', 'text': '62.5%'} + attr_stats = self.admin_misp_connector.attributes_statistics(percentage=True) + self.assertDictEqual(attr_stats, expected_attr_stats_percent) + expected_attr_stats_category_percent = {'Network activity': '37.5%', 'Other': '62.5%'} + attr_stats = self.admin_misp_connector.attributes_statistics(context='category', percentage=True) + self.assertDictEqual(attr_stats, expected_attr_stats_category_percent) + # Tags + to_test = {'tags': {'tlp:white___test': '1'}, 'taxonomies': []} + tags_stats = self.admin_misp_connector.tags_statistics() + self.assertDictEqual(tags_stats, to_test) + to_test = {'tags': {'tlp:white___test': '100%'}, 'taxonomies': []} + tags_stats = self.admin_misp_connector.tags_statistics(percentage=True, name_sort=True) + self.assertDictEqual(tags_stats, to_test) + # Users + users_stats = self.admin_misp_connector.users_statistics(context='data') + self.assertTrue('stats' in users_stats) + + users_stats = self.admin_misp_connector.users_statistics(context='orgs') + self.assertTrue('ORGNAME' in list(users_stats.keys())) + + users_stats = self.admin_misp_connector.users_statistics(context='users') + self.assertEqual(list(users_stats.keys()), ['user', 'org_local', 'org_external']) + + users_stats = self.admin_misp_connector.users_statistics(context='tags') + self.assertEqual(list(users_stats.keys()), ['flatData', 'treemap']) + + users_stats = self.admin_misp_connector.users_statistics(context='attributehistogram') + self.assertTrue(isinstance(users_stats, list), users_stats) + + self.user_misp_connector.add_sighting({'value': first.attributes[0].value}) + users_stats = self.user_misp_connector.users_statistics(context='sightings') + self.assertEqual(list(users_stats.keys()), ['toplist', 'eventids']) + + # FIXME this one fails on travis. + # users_stats = self.admin_misp_connector.users_statistics(context='galaxyMatrix') + # self.assertTrue('matrix' in users_stats) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + self.admin_misp_connector.delete_event(third) + + def test_direct(self) -> None: + try: + r = self.user_misp_connector.direct_call('events/add', data={'info': 'foo'}) + event = MISPEvent() + event.from_dict(**r) + r = self.user_misp_connector.direct_call(f'events/view/{event.id}') + event_get = MISPEvent() + event_get.from_dict(**r) + self.assertDictEqual(event.to_dict(), event_get.to_dict()) + r = self.user_misp_connector.direct_call('events/restSearch', data={"returnFormat": "csv", + "type": {"AND": ["campaign-name", "threat-actor"]}, + "category": "Attribution", "includeEventUuid": 1}) + self.assertTrue(r.startswith('uuid,event_id,category,type,value')) + + finally: + self.admin_misp_connector.delete_event(event) + + def test_freetext(self) -> None: + first = self.create_simple_event() + try: + self.admin_misp_connector.toggle_warninglist(warninglist_name='%dns resolv%', force_enable=True) + first = self.user_misp_connector.add_event(first) + # disable_background_processing => returns the parsed data, before insertion + r = self.user_misp_connector.freetext(first, '1.1.1.1 foo@bar.de', adhereToWarninglists=False, + distribution=2, returnMetaAttributes=False, pythonify=True, + kw_params={'disable_background_processing': 1}) + self.assertTrue(isinstance(r, list)) + self.assertEqual(r[0].value, '1.1.1.1') + r = self.user_misp_connector.freetext(first, '9.9.9.9 foo@bar.com', adhereToWarninglists='soft', + distribution=2, returnMetaAttributes=False, pythonify=True, + kw_params={'disable_background_processing': 1}) + self.assertTrue(isinstance(r, list)) + self.assertEqual(r[0].value, '9.9.9.9') + event = self.user_misp_connector.get_event(first, pythonify=True) + self.assertEqual(event.attributes[3].value, '9.9.9.9') + self.assertFalse(event.attributes[3].to_ids) + r_wl = self.user_misp_connector.freetext(first, '8.8.8.8 foo@bar.de', adhereToWarninglists=True, + distribution=2, returnMetaAttributes=False, + kw_params={'disable_background_processing': 0}) + self.assertEqual(r_wl[0].value, '8.8.8.8') + event = self.user_misp_connector.get_event(first, pythonify=True) + for attribute in event.attributes: + self.assertFalse(attribute.value == '8.8.8.8') + r = self.user_misp_connector.freetext(first, '1.1.1.1 foo@bar.de', adhereToWarninglists=True, + distribution=2, returnMetaAttributes=True) + self.assertTrue(isinstance(r, list)) + self.assertTrue(isinstance(r[0]['types'], dict)) + finally: + # Mostly solved https://github.com/MISP/MISP/issues/4886 + time.sleep(10) + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_sharing_groups(self) -> None: + # add + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = self.admin_misp_connector.add_sharing_group(sg, pythonify=True) + self.assertEqual(sharing_group.name, 'Testcases SG') + self.assertEqual(sharing_group.releasability, 'Testing') + + # Change releasability + r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated"}, sharing_group) + self.assertEqual(r['SharingGroup']['releasability'], 'Testing updated') + r = self.admin_misp_connector.update_sharing_group({"releasability": "Testing updated - 2"}, sharing_group, pythonify=True) + self.assertEqual(r.releasability, 'Testing updated - 2') + # Change name + r.name = 'Testcases SG - new name' + r = self.admin_misp_connector.update_sharing_group(r, pythonify=True) + self.assertEqual(r.name, 'Testcases SG - new name') + + # Test `sharing_group_exists` method + self.assertTrue(self.admin_misp_connector.sharing_group_exists(sharing_group)) + self.assertTrue(self.admin_misp_connector.sharing_group_exists(sharing_group.id)) + self.assertTrue(self.admin_misp_connector.sharing_group_exists(sharing_group.uuid)) + + # add org + r = self.admin_misp_connector.add_org_to_sharing_group(sharing_group, + self.test_org, extend=True) + self.assertEqual(r['name'], 'Organisation added to the sharing group.') + + # delete org + r = self.admin_misp_connector.remove_org_from_sharing_group(sharing_group, + self.test_org) + self.assertEqual(r['name'], 'Organisation removed from the sharing group.', r) + # Get list + sharing_groups = self.admin_misp_connector.sharing_groups(pythonify=True) + self.assertTrue(isinstance(sharing_groups, list)) + self.assertEqual(sharing_groups[0].name, 'Testcases SG - new name') + + # Use the SG + + first = self.create_simple_event() + o = first.add_object(name='file') + o.add_attribute('filename', value='foo2.exe') + second_object = MISPObject('file') + second_object.add_attribute("tlsh", value='92a4b4a3d342a21fe1147474c19c9ab6a01717713a0248a2bb15affce77c1c14a79b93', + category="Payload delivery", to_ids=True, distribution=4, sharing_group_id=sharing_group.id) + + try: + first = self.user_misp_connector.add_event(first) + first = self.admin_misp_connector.change_sharing_group_on_entity(first, sharing_group.id, pythonify=True) + self.assertEqual(first.SharingGroup['name'], 'Testcases SG - new name') + + first_object = self.admin_misp_connector.change_sharing_group_on_entity(first.objects[0], sharing_group.id, pythonify=True) + self.assertEqual(first_object.sharing_group_id, sharing_group.id) + first_attribute = self.admin_misp_connector.change_sharing_group_on_entity(first.attributes[0], sharing_group.id, pythonify=True) + self.assertEqual(first_attribute.distribution, 4) + self.assertEqual(first_attribute.sharing_group_id, int(sharing_group.id)) + # manual create + second_object = self.admin_misp_connector.add_object(first.id, second_object, pythonify=True) + self.assertEqual(second_object.attributes[0].sharing_group_id, int(sharing_group.id)) + # manual update + first_object.add_attribute("tlsh", value='92a4b4a3d342a21fe1147474c19c9ab6a01717713a0248a2bb15affce77c1c14a79b93', + category="Payload delivery", to_ids=True, distribution=4, sharing_group_id=sharing_group.id) + first_object = self.admin_misp_connector.update_object(first_object, pythonify=True) + self.assertEqual(first_object.attributes[-1].sharing_group_id, int(sharing_group.id)) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + # Delete sharing group + r = self.admin_misp_connector.delete_sharing_group(sharing_group.id) + self.assertEqual(r['message'], 'SharingGroup deleted') + + self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group)) + self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group.id)) + self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group.uuid)) + + def test_sharing_group(self) -> None: + # add + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = self.admin_misp_connector.add_sharing_group(sg, pythonify=True) + # Add the org to the sharing group + self.admin_misp_connector.add_org_to_sharing_group( + sharing_group, + self.test_org, extend=True + ) + try: + # Get the sharing group once again + sharing_group = self.admin_misp_connector.get_sharing_group(sharing_group, pythonify=True) + + self.assertTrue(isinstance(sharing_group, MISPSharingGroup)) + self.assertEqual(sharing_group.name, 'Testcases SG') + + # Check we have the org field present and the first org is our org + self.assertTrue(isinstance(getattr(sharing_group, "sgorgs"), list)) + self.assertEqual(sharing_group.sgorgs[0].org_id, self.test_org.id) + finally: + self.admin_misp_connector.delete_sharing_group(sharing_group.id) + self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group)) + + def test_sharing_group_search(self) -> None: + # Add sharing group + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = self.admin_misp_connector.add_sharing_group(sg, pythonify=True) + # Add the org to the sharing group + self.admin_misp_connector.add_org_to_sharing_group( + sharing_group, + self.test_org, extend=True + ) + # Add event + event = self.create_simple_event() + event.distribution = Distribution.sharing_group + event.sharing_group_id = sharing_group.id + # Create two attributes, one specifically for the sharing group, + # another which inherits the event's SG + event.add_attribute('ip-dst', '8.8.8.8', distribution=4, sharing_group_id=sharing_group.id) + event.add_attribute('ip-dst', '9.9.9.9') + event = self.user_misp_connector.add_event(event) + attribute_ids = {a.id for a in event.attributes} + try: + # Try to query for the event + events = self.user_misp_connector.search(sharinggroup=sharing_group.id, controller="events") + # There should be one event + self.assertTrue(len(events) == 1) + # This event should be the one we added + self.assertEqual(events[0].id, event.id) + # Make sure the search isn't just returning everything + events = self.user_misp_connector.search(sharinggroup=99999, controller="events") + + self.assertTrue(len(events) == 0) + + # Try to query for the attributes + attributes = self.user_misp_connector.search(sharinggroup=sharing_group.id, controller="attributes") + searched_attribute_ids = {a.id for a in attributes} + # There should be two attributes + # The extra 1 is the random UUID now created in the event + self.assertTrue(len(attributes) == 2 + 1) + # We should not be missing any of the attributes + self.assertFalse(attribute_ids.difference(searched_attribute_ids)) + finally: + self.user_misp_connector.delete_event(event.id) + self.admin_misp_connector.delete_sharing_group(sharing_group.id) + + self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group)) + + def test_feeds(self) -> None: + # Add + feed = MISPFeed() + feed.name = 'TestFeed' + feed.provider = 'TestFeed - Provider' + feed.url = 'http://example.com' + feed = self.admin_misp_connector.add_feed(feed, pythonify=True) + self.assertEqual(feed.name, 'TestFeed') + self.assertEqual(feed.url, 'http://example.com') + # Update + feed.name = 'TestFeed - Update' + feed = self.admin_misp_connector.update_feed(feed, pythonify=True) + self.assertEqual(feed.name, 'TestFeed - Update') + # Delete + r = self.admin_misp_connector.delete_feed(feed) + self.assertEqual(r['message'], 'Feed deleted.') + # List + feeds = self.admin_misp_connector.feeds(pythonify=True) + self.assertTrue(isinstance(feeds, list)) + for feed in feeds: + if feed.name == 'The Botvrij.eu Data': + break + # Get + botvrij = self.admin_misp_connector.get_feed(feed, pythonify=True) + self.assertEqual(botvrij.url, "https://www.botvrij.eu/data/feed-osint") + # Enable + # MISP OSINT + feed = self.admin_misp_connector.enable_feed(feeds[0].id, pythonify=True) + self.assertTrue(feed.enabled) + feed = self.admin_misp_connector.enable_feed_cache(feeds[0].id, pythonify=True) + self.assertTrue(feed.caching_enabled) + # Botvrij.eu + feed = self.admin_misp_connector.enable_feed(botvrij.id, pythonify=True) + self.assertTrue(feed.enabled) + feed = self.admin_misp_connector.enable_feed_cache(botvrij.id, pythonify=True) + self.assertTrue(feed.caching_enabled) + # Cache + r = self.admin_misp_connector.cache_feed(botvrij) + self.assertEqual(r['message'], 'Feed caching job initiated.') + # Fetch + # Cannot test that, it fetches all the events. + # r = self.admin_misp_connector.fetch_feed(botvrij) + # FIXME https://github.com/MISP/MISP/issues/4834#issuecomment-511889274 + # self.assertEqual(r['message'], 'Feed caching job initiated.') + + # Cache all enabled feeds + r = self.admin_misp_connector.cache_all_feeds() + self.assertEqual(r['message'], 'Feed caching job initiated.') + # Compare all enabled feeds + r = self.admin_misp_connector.compare_feeds() + # FIXME: https://github.com/MISP/MISP/issues/4834#issuecomment-511890466 + # self.assertEqual(r['message'], 'Feed caching job initiated.') + # Disable both feeds + feed = self.admin_misp_connector.disable_feed(feeds[0].id, pythonify=True) + self.assertFalse(feed.enabled) + feed = self.admin_misp_connector.disable_feed(botvrij.id, pythonify=True) + self.assertFalse(feed.enabled) + feed = self.admin_misp_connector.disable_feed_cache(feeds[0].id, pythonify=True) + self.assertFalse(feed.enabled) + feed = self.admin_misp_connector.disable_feed_cache(botvrij.id, pythonify=True) + self.assertFalse(feed.enabled) + # Test enable csv feed - https://github.com/MISP/PyMISP/issues/574 + feeds = self.admin_misp_connector.feeds(pythonify=True) + for feed in feeds: + if feed.name == 'blockrules of rules.emergingthreats.net': + e_thread_csv_feed = feed + break + updated_feed = self.admin_misp_connector.enable_feed(e_thread_csv_feed, pythonify=True) + self.assertTrue(updated_feed.enabled) + self.assertEqual(updated_feed.settings, e_thread_csv_feed.settings) + + updated_feed = self.admin_misp_connector.disable_feed(e_thread_csv_feed, pythonify=True) + self.assertFalse(updated_feed.enabled) + self.assertEqual(updated_feed.settings, e_thread_csv_feed.settings) + + # Test partial update + updated_feed = self.admin_misp_connector.enable_feed(e_thread_csv_feed.id, pythonify=True) + self.assertTrue(updated_feed.enabled) + self.assertEqual(updated_feed.settings, e_thread_csv_feed.settings) + updated_feed = self.admin_misp_connector.disable_feed(e_thread_csv_feed.id, pythonify=True) + self.assertFalse(updated_feed.enabled) + self.assertEqual(updated_feed.settings, e_thread_csv_feed.settings) + + def test_servers(self) -> None: + # add + server = MISPServer() + server.name = 'Test Server' + server.url = 'https://127.0.0.1' + server.remote_org_id = 1 + server.authkey = key + server = self.admin_misp_connector.add_server(server, pythonify=True) + self.assertEqual(server.name, 'Test Server') + # Update + server.name = 'Updated name' + server = self.admin_misp_connector.update_server(server, pythonify=True) + self.assertEqual(server.name, 'Updated name') + # List + servers = self.admin_misp_connector.servers(pythonify=True) + self.assertEqual(servers[0].name, 'Updated name') + # Delete + r = self.admin_misp_connector.delete_server(server) + self.assertEqual(r['name'], 'Server deleted') + + def test_roles_expanded(self) -> None: + '''Test all possible things regarding roles + 1. Use existing roles (ID in test VM): + * Read only (6): Can only connect via API and see events visible by its organisation + * User (3): Same as readonly + create event, tag (using existing tags), add sighting + * Publisher (4): Same as User + publish (also on zmq and kafka), and delegate + * Org Admin (2): Same as publisher + admin org, audit, create tags, templates, sharing groups + * Sync user (5): Same as publisher + sync, create tag, sharing group + * admin (1): Same as Org admin and sync user + site admin, edit regexes, edit object templates + 2. Create roles: + * No Auth key access + * Auth key (=> Read only) + * + tagger + * + sightings creator (=> User) + * + + ''' + # Creates a test user for roles + user = MISPUser() + user.email = 'testusr-roles@user.local' + user.org_id = self.test_org.id + tag = MISPTag() + tag.name = 'tlp:white___test' + try: + test_roles_user = self.admin_misp_connector.add_user(user, pythonify=True) + test_tag = self.admin_misp_connector.add_tag(tag, pythonify=True) + test_roles_user_connector = PyMISP(url, test_roles_user.authkey, verifycert, debug=False) + test_roles_user_connector.toggle_global_pythonify() + # ===== Read Only + self.admin_misp_connector.update_user({'role_id': 6}, test_roles_user) + base_event = MISPEvent() + base_event.info = 'Test Roles' + base_event.distribution = 0 + base_event.add_attribute('ip-dst', '8.8.8.8') + base_event.add_attribute('ip-dst', '9.9.9.9') + base_event.attributes[0].add_tag('tlp:white___test') + r = test_roles_user_connector.add_event(base_event) + self.assertTrue(isinstance(r['errors'], tuple), r['errors']) + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + try: + e = self.user_misp_connector.add_event(base_event, pythonify=True) + e = test_roles_user_connector.get_event(e) + self.assertEqual(e.info, 'Test Roles') + self.assertEqual(e.attributes[0].tags[0].name, 'tlp:white___test') + r = test_roles_user_connector.publish(e) + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + r = test_roles_user_connector.tag(e.attributes[1], 'tlp:white___test') + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + r = test_roles_user_connector.add_sighting({'name': 'foo'}, e.attributes[1]) + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + + self.user_misp_connector.add_sighting({'source': 'blah'}, e.attributes[0]) + sightings = test_roles_user_connector.sightings(e.attributes[0]) + self.assertEqual(sightings[0].source, 'blah') + + e = test_roles_user_connector.get_event(e) + self.assertEqual(e.attributes[0].sightings[0].source, 'blah') + # FIXME: http://github.com/MISP/MISP/issues/5022 + # a = test_roles_user_connector.get_attribute(e.attributes[0]) + # self.assertEqual(a.sightings[0].source, 'blah') + + # ===== User (the capabilities were tested just before, only testing the publisher capabilities) + self.admin_misp_connector.update_user({'role_id': 3}, test_roles_user) + r = test_roles_user_connector.publish(e) + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + r = test_roles_user_connector.delegate_event(e, self.test_org_delegate) + self.assertEqual(r['errors'][1]['message'], 'You do not have permission to use this functionality.', r) + # ===== Publisher + # Make sure the delegation is enabled + r = self.admin_misp_connector.set_server_setting('MISP.delegation', True, force=True) + self.assertEqual(r['message'], 'Field updated', r) + setting = self.admin_misp_connector.get_server_setting('MISP.delegation') + self.assertTrue(setting['value']) + # ====== + self.admin_misp_connector.update_user({'role_id': 4}, test_roles_user) + r = test_roles_user_connector.publish(e) + self.assertEqual(r['message'], 'Job queued', r) + delegation = test_roles_user_connector.delegate_event(e, self.test_org_delegate) + self.assertEqual(delegation.org_id, self.test_org_delegate.id) + self.assertEqual(delegation.requester_org_id, self.test_org.id) + r = test_roles_user_connector.accept_event_delegation(delegation.id) + self.assertEqual(r['errors'][1]['message'], 'You are not authorised to do that.', r) + # Test delegation + delegations = self.delegate_user_misp_connector.event_delegations() + self.assertEqual(delegations[0].id, delegation.id) + r = self.delegate_user_misp_connector.accept_event_delegation(delegation) + self.assertEqual(r['message'], 'Event ownership transferred.') + e = self.delegate_user_misp_connector.get_event(e) + self.assertTrue(isinstance(e, MISPEvent), e) + self.assertEqual(e.info, 'Test Roles') + self.assertEqual(e.org.name, 'Test Org - delegate') + r = self.delegate_user_misp_connector.delete_event(e) + self.assertEqual(r['message'], 'Event deleted.', r) + # Change base_event UUID do we can add it + base_event.uuid = str(uuid4()) + e = test_roles_user_connector.add_event(base_event) + delegation = test_roles_user_connector.delegate_event(e, self.test_org_delegate) + r = test_roles_user_connector.discard_event_delegation(delegation.id) + self.assertEqual(r['message'], 'Delegation request deleted.') + + e = test_roles_user_connector.get_event(e) + self.assertTrue(isinstance(e, MISPEvent), e) + self.assertEqual(e.info, 'Test Roles') + self.assertEqual(e.org_id, int(self.test_org.id)) + finally: + self.user_misp_connector.delete_event(e) + + # Publisher + self.admin_misp_connector.update_user({'role_id': 4}, test_roles_user) + # Org Admin + self.admin_misp_connector.update_user({'role_id': 2}, test_roles_user) + # Sync User + self.admin_misp_connector.update_user({'role_id': 5}, test_roles_user) + # Admin + self.admin_misp_connector.update_user({'role_id': 1}, test_roles_user) + finally: + self.admin_misp_connector.delete_user(test_roles_user) + self.admin_misp_connector.delete_tag(test_tag) + + def test_expansion(self) -> None: + first = self.create_simple_event() + try: + md5_disk = hashlib.md5() + with open('tests/viper-test-files/test_files/sample2.pe', 'rb') as f: + filecontent = f.read() + md5_disk.update(filecontent) + malware_sample_initial_attribute = first.add_attribute('malware-sample', value='Big PE sample', data=BytesIO(filecontent), expand='binary') + md5_init_attribute = hashlib.md5() + md5_init_attribute.update(malware_sample_initial_attribute.malware_binary.getvalue()) + self.assertEqual(md5_init_attribute.digest(), md5_disk.digest()) + + first.run_expansions() + first = self.admin_misp_connector.add_event(first, pythonify=True) + self.assertEqual(len(first.objects), 8, first.objects) + # Speed test + # # reference time + start = time.time() + self.admin_misp_connector.get_event(first.id, pythonify=False) + ref_time = time.time() - start + # # Speed test pythonify + start = time.time() + first = self.admin_misp_connector.get_event(first.id, pythonify=True) + pythonify_time = time.time() - start + self.assertTrue((pythonify_time - ref_time) <= 0.5, f'Pythonify too slow: {ref_time} vs. {pythonify_time}.') + + # Test on demand decrypt malware binary + file_objects = first.get_objects_by_name('file') + samples = file_objects[0].get_attributes_by_relation('malware-sample') + binary = samples[0].malware_binary + md5_from_server = hashlib.md5() + md5_from_server.update(binary.getvalue()) + self.assertEqual(md5_from_server.digest(), md5_disk.digest()) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + + def test_user_settings(self) -> None: + first = self.create_simple_event() + first.distribution = 3 + first.add_tag('test_publish_filter') + first.add_tag('test_publish_filter_not') + second = self.create_simple_event() + second.distribution = 3 + try: + # Set + setting = self.admin_misp_connector.set_user_setting('dashboard_access', 1, pythonify=True) + setting_value = {'Tag.name': 'test_publish_filter'} + setting = self.admin_misp_connector.set_user_setting('publish_alert_filter', setting_value, pythonify=True) + self.assertTrue(isinstance(setting, MISPUserSetting)) + self.assertEqual(setting.value, setting_value) + + # Get + # FIXME: https://github.com/MISP/MISP/issues/5297 + # setting = self.admin_misp_connector.get_user_setting('dashboard_access', pythonify=True) + + # Get All + user_settings = self.admin_misp_connector.user_settings(pythonify=True) + # TODO: Make that one better + self.assertTrue(isinstance(user_settings, list)) + + # Test if publish_alert_filter works + # # Enable autoalert on admin + self.admin_misp_connector._current_user.autoalert = True + self.admin_misp_connector._current_user.termsaccepted = True + admin_usr = self.admin_misp_connector.update_user(self.admin_misp_connector._current_user, pythonify=True) + self.assertTrue(admin_usr.autoalert) + + first = self.admin_misp_connector.add_event(first, pythonify=True) + second = self.admin_misp_connector.add_event(second, pythonify=True) + r = self.user_misp_connector.change_user_password('Password1234') + self.assertEqual(r['message'], 'Password Changed.') + self.test_usr.autoalert = True + self.test_usr.termsaccepted = True + user = self.user_misp_connector.update_user(self.test_usr, pythonify=True) + self.assertTrue(user.autoalert) + self.admin_misp_connector.publish(first, alert=True) + self.admin_misp_connector.publish(second, alert=True) + time.sleep(10) + # FIXME https://github.com/MISP/MISP/issues/4872 + # mail_logs = self.admin_misp_connector.search_logs(model='User', action='email', limit=2, pythonify=True) + mail_logs = self.admin_misp_connector.search_logs(model='User', action='email', created=datetime.now() - timedelta(seconds=30), pythonify=True) + if mail_logs: + # FIXME: On travis, the mails aren't working, so we stik that. + self.assertEqual(len(mail_logs), 3) + self.assertTrue(mail_logs[0].title.startswith(f'Email to {self.admin_misp_connector._current_user.email}'), mail_logs[0].title) + self.assertTrue(mail_logs[1].title.startswith(f'Email to {self.user_misp_connector._current_user.email}'), mail_logs[1].title) + self.assertTrue(mail_logs[2].title.startswith(f'Email to {self.user_misp_connector._current_user.email}'), mail_logs[2].title) + + # Delete + # FIXME: https://github.com/MISP/MISP/issues/5297 + # response = self.admin_misp_connector.delete_user_setting('publish_alert_filter') + finally: + self.test_usr.autoalert = False + self.user_misp_connector.update_user(self.test_usr) + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + + def test_communities(self) -> None: + communities = self.admin_misp_connector.communities(pythonify=True) + self.assertEqual(communities[0].name, 'CIRCL Private Sector Information Sharing Community - aka MISPPRIV') + community = self.admin_misp_connector.get_community(communities[1], pythonify=True) + self.assertEqual(community.name, 'CIRCL n/g CSIRT information sharing community - aka MISP') + # FIXME: Fails on travis for now due to GPG misconfigured + # r = self.admin_misp_connector.request_community_access(community, mock=False) + # self.assertTrue(r['message'], 'Request sent.') + # r = self.admin_misp_connector.request_community_access(community, mock=True) + # mail = email.message_from_string(r['headers'] + '\n' + r['message']) + # for k, v in mail.items(): + # if k == 'To': + # self.assertEqual(v, 'info@circl.lu') + + def test_upload_stix(self) -> None: + # FIXME https://github.com/MISP/MISP/issues/4892 + try: + r1 = self.user_misp_connector.upload_stix('tests/stix1.xml-utf8', version='1') + event_stix_one = MISPEvent() + event_stix_one.load(r1.json()) + # self.assertEqual(event_stix_one.attributes[0], '8.8.8.8') + self.admin_misp_connector.delete_event(event_stix_one) + bl = self.admin_misp_connector.delete_event_blocklist(event_stix_one.uuid) + self.assertTrue(bl['success']) + + r2 = self.user_misp_connector.upload_stix('tests/stix2.json', version='2') + event_stix_two = MISPEvent() + event_stix_two.load(r2.json()) + # FIXME: the response is buggy. + # self.assertEqual(event_stix_two.attributes[0], '8.8.8.8') + self.admin_misp_connector.delete_event(event_stix_two) + bl = self.admin_misp_connector.delete_event_blocklist(event_stix_two.uuid) + self.assertTrue(bl['success']) + finally: + try: + self.admin_misp_connector.delete_event(event_stix_one) + self.admin_misp_connector.delete_event_blocklist(event_stix_one.uuid) + except Exception: + pass + try: + self.admin_misp_connector.delete_event(event_stix_two) + self.admin_misp_connector.delete_event_blocklist(event_stix_two.uuid) + except Exception: + pass + + def test_toggle_global_pythonify(self) -> None: + first = self.create_simple_event() + second = self.create_simple_event() + try: + self.admin_misp_connector.toggle_global_pythonify() + first = self.admin_misp_connector.add_event(first) + self.assertTrue(isinstance(first, MISPEvent)) + self.admin_misp_connector.toggle_global_pythonify() + second = self.admin_misp_connector.add_event(second) + self.assertTrue(isinstance(second, dict)) + finally: + # Delete event + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_event(second) + + def test_first_last_seen(self) -> None: + event = MISPEvent() + event.info = 'Test First Last seen' + event.add_attribute('ip-dst', '8.8.8.8', first_seen='2020-01-03', last_seen='2020-01-04T12:30:34.323242+0800') + obj = event.add_object(name='file', first_seen=1580147259.268763, last_seen=1580147300) + attr = obj.add_attribute('filename', 'blah.exe', comment="blah") + attr.first_seen = '2022-01-30' + attr.last_seen = '2022-02-23' + try: + first = self.admin_misp_connector.add_event(event, pythonify=True) + # Simple attribute + self.assertEqual(first.attributes[0].first_seen, datetime(2020, 1, 3, 0, 0).astimezone()) + self.assertEqual(first.attributes[0].last_seen, datetime(2020, 1, 4, 4, 30, 34, 323242, tzinfo=timezone.utc)) + + # Object + self.assertEqual(first.objects[0].attributes[0].value, 'blah.exe') + self.assertEqual(first.objects[0].attributes[0].comment, 'blah') + self.assertEqual(first.objects[0].first_seen, datetime(2020, 1, 27, 17, 47, 39, 268763, tzinfo=timezone.utc)) + self.assertEqual(first.objects[0].last_seen, datetime(2020, 1, 27, 17, 48, 20, tzinfo=timezone.utc)) + + # Object attribute + self.assertEqual(first.objects[0].attributes[0].first_seen, datetime(2022, 1, 30, 0, 0).astimezone()) + self.assertEqual(first.objects[0].attributes[0].last_seen, datetime(2022, 2, 23, 0, 0).astimezone()) + + # Update values + # Attribute in full event + now = datetime.now().astimezone() + first.attributes[0].last_seen = now + first = self.admin_misp_connector.update_event(first, pythonify=True) + self.assertEqual(first.attributes[0].last_seen, now) + # Object only + now = datetime.now().astimezone() + obj = first.objects[0] + obj.last_seen = now + obj = self.admin_misp_connector.update_object(obj, pythonify=True) + self.assertEqual(obj.last_seen, now) + # Attribute in object only + now = datetime.now().astimezone() + attr = obj.attributes[0] + attr.first_seen = '2020-01-04' + attr.last_seen = now + attr = self.admin_misp_connector.update_attribute(attr, pythonify=True) + self.assertEqual(attr.last_seen, now) + + finally: + self.admin_misp_connector.delete_event(first) + + def test_registrations(self) -> None: + r = register_user(url, 'self_register@user.local', organisation=self.test_org, + org_name=self.test_org.name, verify=verifycert) + self.assertTrue(r['saved']) + + r = register_user(url, 'discard@tesst.de', verify=verifycert) + self.assertTrue(r['saved']) + + registrations = self.admin_misp_connector.user_registrations(pythonify=True) + self.assertTrue(len(registrations), 2) + self.assertEqual(registrations[0].data['email'], 'self_register@user.local') + self.assertEqual(registrations[0].data['org_name'], 'Test Org') + self.assertEqual(registrations[1].data['email'], 'discard@tesst.de') + + m = self.admin_misp_connector.accept_user_registration(registrations[0], unsafe_fallback=True) + self.assertTrue(m['saved']) + + # delete new user + for user in self.admin_misp_connector.users(pythonify=True): + if user.email == registrations[0].data['email']: + self.admin_misp_connector.delete_user(user) + break + + # Expected: accept registration fails because the orgname is missing + m = self.admin_misp_connector.accept_user_registration(registrations[1], unsafe_fallback=True) + self.assertEqual(m['errors'][1]['message'], 'No organisation selected. Supply an Organisation ID') + + m = self.admin_misp_connector.discard_user_registration(registrations[1].id) + self.assertEqual(m['name'], '1 registration(s) discarded.') + + def test_search_workflow(self) -> None: + first = self.create_simple_event() + first.add_attribute('domain', 'google.com') + tag = MISPTag() + tag.name = 'my_tag' + try: + # Note: attribute 0 doesn't matter + # Attribute 1 = google.com, no tag + # Init tag and event + tag = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertEqual(tag.name, 'my_tag') + first = self.user_misp_connector.add_event(first, pythonify=True) + time.sleep(10) + # Add tag to attribute 1, add attribute 2, update + first.attributes[1].add_tag(tag) + first.add_attribute('domain', 'google.fr') + # Attribute 1 = google.com, tag + # Attribute 2 = google.fr, no tag + first = self.user_misp_connector.update_event(first, pythonify=True) + self.assertEqual(first.attributes[1].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[2].tags, []) + updated_attrs = self.user_misp_connector.search(controller='attributes', eventid=first.id, timestamp='5s', pythonify=True) + # Get two attributes, 0 (google.com) has a tag, 1 (google.fr) doesn't + self.assertEqual(len(updated_attrs), 2) + self.assertEqual(updated_attrs[0].tags[0].name, 'my_tag') + self.assertEqual(updated_attrs[1].value, 'google.fr') + self.assertEqual(updated_attrs[1].tags, []) + # Get the metadata only of the event + first_meta_only = self.user_misp_connector.search(eventid=first.id, metadata=True, pythonify=True) + + # Add tag to attribute 1 (google.fr) + attr_to_update = updated_attrs[1] + attr_to_update.add_tag(tag) + # attr_to_update.pop('timestamp') + # Add new attribute to event with metadata only + first_meta_only[0].add_attribute('domain', 'google.lu') + # Add tag to new attribute + first_meta_only[0].attributes[0].add_tag('my_tag') + # Re-add attribute 1 (google.fr), newly tagged + first_meta_only[0].add_attribute(**attr_to_update) + # When we push, all the attributes should be tagged + first = self.user_misp_connector.update_event(first_meta_only[0], pythonify=True) + self.assertEqual(first.attributes[1].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[2].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[3].tags[0].name, 'my_tag') + finally: + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_tag(tag) + + def test_search_workflow_ts(self) -> None: + first = self.create_simple_event() + first.add_attribute('domain', 'google.com') + tag = MISPTag() + tag.name = 'my_tag' + try: + # Note: attribute 0 doesn't matter + # Attribute 1 = google.com, no tag + # Init tag and event + tag = self.admin_misp_connector.add_tag(tag, pythonify=True) + self.assertEqual(tag.name, 'my_tag') + first = self.user_misp_connector.add_event(first, pythonify=True) + time.sleep(10) + # Add tag to attribute 1, add attribute 2, update + first.attributes[1].add_tag(tag) + first.add_attribute('domain', 'google.fr') + # Attribute 1 = google.com, tag + # Attribute 2 = google.fr, no tag + first = self.user_misp_connector.update_event(first, pythonify=True) + self.assertEqual(first.attributes[1].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[2].tags, []) + updated_attrs = self.user_misp_connector.search(controller='attributes', eventid=first.id, timestamp=first.timestamp.timestamp(), pythonify=True) + # Get two attributes, 0 (google.com) has a tag, 1 (google.fr) doesn't + self.assertEqual(len(updated_attrs), 2) + self.assertEqual(updated_attrs[0].tags[0].name, 'my_tag') + self.assertEqual(updated_attrs[1].value, 'google.fr') + self.assertEqual(updated_attrs[1].tags, []) + # Get the metadata only of the event + first_meta_only = self.user_misp_connector.search(eventid=first.id, metadata=True, pythonify=True) + + # Add tag to attribute 1 (google.fr) + attr_to_update = updated_attrs[1] + attr_to_update.add_tag(tag) + # attr_to_update.pop('timestamp') + # Add new attribute to event with metadata only + first_meta_only[0].add_attribute('domain', 'google.lu') + # Add tag to new attribute + first_meta_only[0].attributes[0].add_tag('my_tag') + # Re-add attribute 1 (google.fr), newly tagged + first_meta_only[0].add_attribute(**attr_to_update) + # When we push, all the attributes should be tagged + first = self.user_misp_connector.update_event(first_meta_only[0], pythonify=True) + self.assertEqual(first.attributes[1].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[2].tags[0].name, 'my_tag') + self.assertEqual(first.attributes[3].tags[0].name, 'my_tag') + finally: + self.admin_misp_connector.delete_event(first) + self.admin_misp_connector.delete_tag(tag) + + def test_blocklists(self) -> None: + first = self.create_simple_event() + second = self.create_simple_event() + second.Orgc = self.test_org + to_delete: dict[str, MISPOrganisationBlocklist | MISPEventBlocklist] = {'bl_events': [], 'bl_organisations': []} + try: + # test events BL + ebl: MISPEventBlocklist = self.admin_misp_connector.add_event_blocklist(uuids=[first.uuid]) + self.assertEqual(ebl['result']['successes'][0], first.uuid, ebl) + bl_events = self.admin_misp_connector.event_blocklists(pythonify=True) + for ble in bl_events: + if ble.event_uuid == first.uuid: + to_delete['bl_events'].append(ble) + break + else: + raise Exception('Unable to find UUID in Events blocklist') + first = self.user_misp_connector.add_event(first, pythonify=True) + self.assertEqual(first['errors'][1]['message'], 'Event blocked by event blocklist.', first) + ble.comment = 'This is a test' + ble.event_info = 'foo' + ble.event_orgc = 'bar' + ble = self.admin_misp_connector.update_event_blocklist(ble, pythonify=True) + self.assertEqual(ble.comment, 'This is a test') + r = self.admin_misp_connector.delete_event_blocklist(ble) + self.assertTrue(r['success']) + + # test Org BL + obl = self.admin_misp_connector.add_organisation_blocklist(uuids=self.test_org.uuid) + self.assertEqual(obl['result']['successes'][0], self.test_org.uuid, obl) + bl_orgs = self.admin_misp_connector.organisation_blocklists(pythonify=True) + for blo in bl_orgs: + if blo.org_uuid == self.test_org.uuid: + to_delete['bl_organisations'].append(blo) + break + else: + raise Exception('Unable to find UUID in Orgs blocklist') + first = self.user_misp_connector.add_event(first, pythonify=True) + self.assertEqual(first['errors'][1]['message'], 'Event blocked by organisation blocklist.', first) + + blo.comment = 'This is a test' + blo.org_name = 'bar' + blo: MISPOrganisationBlocklist = self.admin_misp_connector.update_organisation_blocklist(blo, pythonify=True) + self.assertEqual(blo.org_name, 'bar') + r = self.admin_misp_connector.delete_organisation_blocklist(blo) + self.assertTrue(r['success']) + + finally: + for ble in to_delete['bl_events']: + self.admin_misp_connector.delete_event_blocklist(ble) + for blo in to_delete['bl_organisations']: + self.admin_misp_connector.delete_organisation_blocklist(blo) + + def test_event_report(self) -> None: + event = self.create_simple_event() + new_event_report: MISPEventReport = MISPEventReport() + new_event_report.name = "Test Event Report" + new_event_report.content = "# Example report markdown" + new_event_report.distribution = 5 # Inherit + try: + event = self.user_misp_connector.add_event(event) + new_event_report = self.user_misp_connector.add_event_report(event.id, new_event_report) # type: ignore[assignment] + # The event report should be linked by Event ID + self.assertEqual(event.id, new_event_report.event_id) + + event = self.user_misp_connector.get_event(event) + # The Event Report should be present on the event + self.assertEqual(new_event_report.id, event.event_reports[0].id) + + new_event_report.name = "Updated Event Report" + new_event_report.content = "Updated content" + new_event_report = self.user_misp_connector.update_event_report(new_event_report) # type: ignore[assignment] + # The event report should be updatable + self.assertTrue(new_event_report.name == "Updated Event Report") + self.assertTrue(new_event_report.content == "Updated content") + + event_reports: list[MISPEventReport] = self.user_misp_connector.get_event_reports(event.id) # type: ignore[assignment] + # The event report should be requestable by the Event ID + self.assertEqual(new_event_report.id, event_reports[0].id) + + response = self.user_misp_connector.delete_event_report(new_event_report) + # The event report should be soft-deletable + self.assertTrue(response['success']) + self.assertEqual(response['name'], f'Event Report {new_event_report.uuid} soft deleted') + + response = self.user_misp_connector.delete_event_report(new_event_report, True) + self.assertTrue(response['success']) + finally: + self.user_misp_connector.delete_event(event) + self.user_misp_connector.delete_event_report(new_event_report) + + def test_search_galaxy(self) -> None: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + ret = self.admin_misp_connector.search_galaxy(value=galaxy.name, pythonify=True) + self.assertEqual(len(ret), 1) + + def test_galaxy_cluster(self) -> None: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + new_galaxy_cluster: MISPGalaxyCluster = MISPGalaxyCluster() + new_galaxy_cluster.value = "Test Cluster" + new_galaxy_cluster.authors = ["MISP"] + new_galaxy_cluster.distribution = 1 + new_galaxy_cluster.description = "Example test cluster" + try: + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") + existing_galaxy_cluster = galaxy.clusters[0] + + if gid := galaxy.id: + new_galaxy_cluster = self.admin_misp_connector.add_galaxy_cluster(gid, new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") + # The new galaxy cluster should be under the selected galaxy + self.assertEqual(galaxy.id, new_galaxy_cluster.galaxy_id) + # The cluster should have the right value + self.assertEqual(new_galaxy_cluster.value, "Test Cluster") + + new_galaxy_cluster.add_cluster_element("synonyms", "Test2") + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + + # The cluster should have one element that is a synonym + self.assertEqual(len(new_galaxy_cluster.cluster_elements), 1) + element = new_galaxy_cluster.cluster_elements[0] + self.assertEqual(element.key, "synonyms") + self.assertEqual(element.value, "Test2") + + # The cluster should have the old meta as a prop + self.assertEqual(new_galaxy_cluster.elements_meta, {'synonyms': ['Test2']}) + + # The cluster element should be updatable + element.value = "Test3" + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + element = new_galaxy_cluster.cluster_elements[0] + self.assertEqual(element.value, "Test3") + + new_galaxy_cluster.add_cluster_element("synonyms", "ToDelete") + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + # The cluster should have two elements + self.assertEqual(len(new_galaxy_cluster.cluster_elements), 2) + + new_galaxy_cluster.cluster_elements = [e for e in new_galaxy_cluster.cluster_elements if e.value != "ToDelete"] + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + # The cluster elements should be deletable + self.assertEqual(len(new_galaxy_cluster.cluster_elements), 1) + + new_galaxy_cluster.add_cluster_relation(existing_galaxy_cluster, "is-tested-by") + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + # The cluster should have a relationship + self.assertEqual(len(new_galaxy_cluster.cluster_relations), 1) + relation = new_galaxy_cluster.cluster_relations[0] + self.assertEqual(relation.referenced_galaxy_cluster_type, "is-tested-by") + self.assertEqual(relation.referenced_galaxy_cluster_uuid, existing_galaxy_cluster.uuid) + + relation.add_tag("tlp:amber") + new_galaxy_cluster = self.admin_misp_connector.update_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + relation = new_galaxy_cluster.cluster_relations[0] + # The relationship should have a tag of tlp:amber + self.assertEqual(len(relation.tags), 1) + self.assertEqual(relation.tags[0].name, "tlp:amber") + + # The cluster relations should be deletable + resp = self.admin_misp_connector.delete_galaxy_cluster_relation(relation) + self.assertTrue(resp['success']) + # The cluster relation should no longer be present + new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + self.assertEqual(len(new_galaxy_cluster.cluster_relations), 0) + + resp = self.admin_misp_connector.delete_galaxy_cluster(new_galaxy_cluster) + # Galaxy clusters should be soft deletable + self.assertTrue(resp['success']) + new_galaxy_cluster = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster, pythonify=True) # type: ignore[assignment] + self.assertTrue(isinstance(new_galaxy_cluster, MISPGalaxyCluster)) + + resp = self.admin_misp_connector.delete_galaxy_cluster(new_galaxy_cluster, hard=True) + # Galaxy clusters should be hard deletable + self.assertTrue(resp['success']) + resp = self.admin_misp_connector.get_galaxy_cluster(new_galaxy_cluster) # type: ignore[assignment] + self.assertTrue("errors" in resp) + finally: + pass + + def test_event_galaxy(self) -> None: + event = self.create_simple_event() + try: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) # type: ignore[assignment] + galaxy: MISPGalaxy = galaxies[0] + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) # type: ignore[assignment] + else: + raise Exception("No galaxy found") + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[0] + event.add_tag(galaxy_cluster.tag_name) + event = self.admin_misp_connector.add_event(event, pythonify=True) + # The event should have a galaxy attached + self.assertEqual(len(event.galaxies), 1) + event_galaxy = event.galaxies[0] + # The galaxy ID should equal the galaxy from which the cluster came from + self.assertEqual(event_galaxy.id, galaxy.id) + # The galaxy cluster should equal the cluster added + self.assertEqual(event_galaxy.clusters[0].id, galaxy_cluster.id) + finally: + self.admin_misp_connector.delete_event(event) + + def test_attach_galaxy_cluster(self) -> None: + event = self.create_simple_event() + event = self.admin_misp_connector.add_event(event, pythonify=True) + try: + galaxies: list[MISPGalaxy] = self.admin_misp_connector.galaxies(pythonify=True) + galaxy: MISPGalaxy = galaxies[0] + if gid := galaxy.id: + galaxy = self.admin_misp_connector.get_galaxy(gid, withCluster=True, pythonify=True) + else: + raise Exception("No galaxy found") + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[0] + response = self.admin_misp_connector.attach_galaxy_cluster(event, galaxy_cluster) + self.assertTrue(response['saved']) + event = self.admin_misp_connector.get_event(event.id, pythonify=True) + + self.assertEqual(len(event.galaxies), 1) + event_galaxy = event.galaxies[0] + # The galaxy ID should equal the galaxy from which the cluster came from + self.assertEqual(event_galaxy.id, galaxy.id) + # The galaxy cluster should equal the cluster added + self.assertEqual(event_galaxy.clusters[0].id, galaxy_cluster.id) + + galaxy_cluster: MISPGalaxyCluster = galaxy.clusters[1] + + # Test on attribute + attribute = event.attributes[0] + response = self.admin_misp_connector.attach_galaxy_cluster(attribute, galaxy_cluster) + self.assertTrue(response['saved']) + event = self.admin_misp_connector.get_event(event.id, pythonify=True) + attribute = event.attributes[0] + self.assertEqual(len(attribute.galaxies), 1) + attribute_galaxy = attribute.galaxies[0] + # The galaxy ID should equal the galaxy from which the cluster came from + self.assertEqual(attribute_galaxy.id, galaxy.id) + # The galaxy cluster should equal the cluster added + self.assertEqual(attribute_galaxy.clusters[0].id, galaxy_cluster.id) + finally: + self.admin_misp_connector.delete_event(event) + + def test_analyst_data_CRUD(self) -> None: + event = self.create_simple_event() + try: + fake_uuid = str(uuid4()) + new_note1 = MISPNote() + new_note1.object_type = 'Event' + new_note1.object_uuid = fake_uuid + new_note1.note = 'Fake note' + new_note1 = self.user_misp_connector.add_note(new_note1) + # The Note should be linked even for non-existing data + self.assertTrue(new_note1.object_uuid == fake_uuid) + + new_note1.note = "Updated Note" + new_note1 = self.user_misp_connector.update_note(new_note1) + # The Note should be updatable + self.assertTrue(new_note1.note == "Updated Note") + + # The Note should be able to get an Opinion + new_opinion = new_note1.add_opinion(42, 'Test Opinion') + new_note1 = self.user_misp_connector.update_note(new_note1) + # Fetch newly added node + new_note1 = self.user_misp_connector.get_note(new_note1) + # The Opinion shoud be able to be created via the Note + self.assertTrue(new_note1.opinions[0].opinion == new_opinion.opinion) + + response = self.user_misp_connector.delete_note(new_note1) + # The Note should be deletable + self.assertTrue(response['success']) + self.assertEqual(response['message'], 'Note deleted.') + # The Opinion should not be deleted + opinion_resp = self.user_misp_connector.get_opinion(new_opinion) + self.assertTrue(opinion_resp.opinion == new_opinion.opinion) + + new_note: MISPNote = event.add_note(note='Test Note', language='en') + new_note.distribution = 1 # Community + event = self.user_misp_connector.add_event(event) + # The note should be linked by Event UUID + self.assertEqual(new_note.object_type, 'Event') + self.assertTrue(new_note.object_uuid == event.uuid) + + event = self.user_misp_connector.get_event(event) + # The Note should be present on the event + self.assertTrue(event.notes[0].object_uuid == event.uuid) + + finally: + self.admin_misp_connector.delete_event(event) + try: + self.admin_misp_connector.delete_opinion(new_opinion) + self.admin_misp_connector.delete_note(new_note) + self.admin_misp_connector.delete_note(new_note1) # Should already be deleted + except Exception: + pass + + def test_analyst_data_ACL(self) -> None: + event = self.create_simple_event() + event.distribution = 2 + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = self.admin_misp_connector.add_sharing_group(sg, pythonify=True) + # Chec that sharing group was created + self.assertEqual(sharing_group.name, 'Testcases SG') + + try: + new_note: MISPNote = event.add_note(note='Test Note', language='en') + new_note.distribution = 0 # Org only + event = self.admin_misp_connector.add_event(event, pythonify=True) + + # The note should be linked by Event UUID + self.assertEqual(new_note.object_type, 'Event') + self.assertEqual(event.uuid, new_note.object_uuid) + + event = self.admin_misp_connector.get_event(event, pythonify=True) + # The note should be visible for the creator + self.assertEqual(len(event.notes), 1) + self.assertTrue(new_note.note == "Test Note") + + resp = self.user_misp_connector.get_note(new_note) + # The note should not be visible to another org + self.assertTrue(len(resp), 0) + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should not be visible for another org than the creator + self.assertEqual(len(event.Note), 0) + + new_note = self.admin_misp_connector.get_note(new_note, pythonify=True) + new_note.distribution = 4 + new_note.sharing_group_id = sharing_group.id + new_note = self.admin_misp_connector.update_note(new_note, pythonify=True) + self.assertEqual(int(new_note.sharing_group_id), int(sharing_group.id)) + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should not be visible for another org not part of the sharing group + self.assertEqual(len(event.Note), 0) + + # Add org to the sharing group + r = self.admin_misp_connector.add_org_to_sharing_group(sharing_group, + self.test_org, extend=True) + self.assertEqual(r['name'], 'Organisation added to the sharing group.') + + event = self.user_misp_connector.get_event(event) + # The Note attached to the event should now be visible + self.assertEqual(len(event.Note), 1) + + new_note.note = "Updated Note" + resp = self.user_misp_connector.update_note(new_note) + # The Note should not be updatable by another organisation + self.assertTrue(resp['errors']) + + resp = self.user_misp_connector.delete_note(new_note) + # The Note should not be deletable by another organisation + self.assertTrue(resp['errors']) + + organisation = MISPOrganisation() + organisation.name = 'Fake Org' + fake_org = self.admin_misp_connector.add_organisation(organisation, pythonify=True) + new_note_2 = new_note.add_note('Test Note 2') + new_note_2.orgc_uuid = fake_org.uuid + new_note_2 = self.user_misp_connector.add_note(new_note_2) + # Regular user should not be able to create a note on behalf of another organisation + self.assertFalse(new_note_2.orgc_uuid == fake_org.uuid) + # Note should have the orgc set to the use's organisation for non-privileged users + self.assertTrue(new_note_2.orgc_uuid == self.test_org.uuid) + + finally: + self.admin_misp_connector.delete_event(event) + try: + pass + self.admin_misp_connector.delete_sharing_group(sharing_group.id) + self.admin_misp_connector.delete_organisation(fake_org) + self.admin_misp_connector.delete_note(new_note) + except Exception: + pass + + @unittest.skip("Internal use only") + def missing_methods(self) -> None: + skip = [ + "attributes/download", + "attributes/add_attachment", + "attributes/add_threatconnect", + "attributes/editField", + "attributes/viewPicture", + "attributes/restore", + "attributes/deleteSelected", + "attributes/editSelected", + "attributes/search", + "attributes/searchAlternate", + "attributes/checkComposites", + "attributes/downloadAttachment", + "attributes/returnAttributes", + "attributes/text", + "attributes/rpz", + "attributes/bro", + "attributes/reportValidationIssuesAttributes", + "attributes/generateCorrelation", + "attributes/getMassEditForm", + "attributes/fetchViewValue", + "attributes/fetchEditForm", + "attributes/attributeReplace", + "attributes/downloadSample", + "attributes/pruneOrphanedAttributes", + "attributes/checkOrphanedAttributes", + "attributes/updateAttributeValues", + "attributes/hoverEnrichment", + "attributes/addTag", + "attributes/removeTag", + "attributes/toggleCorrelation", # Use update attribute + "attributes/toggleToIDS", # Use update attribute + "attributes/checkAttachments", + "attributes/exportSearch", + 'dashboards', + 'decayingModel', + "eventBlocklists/massDelete", + "eventDelegations/view", + "eventDelegations/index", + "eventGraph/view", + "eventGraph/add", + "eventGraph/delete", + "events/filterEventIndex", + "events/viewEventAttributes", + "events/removePivot", + "events/addIOC", + "events/add_misp_export", + "events/merge", + "events/unpublish", + "events/publishSightings", + "events/automation", + "events/export", + "events/downloadExport", + "events/xml", + "events/nids", + "events/hids", + "events/csv", + "events/downloadOpenIOCEvent", + "events/proposalEventIndex", + "events/reportValidationIssuesEvents", + "events/addTag", + "events/removeTag", + "events/saveFreeText", + "events/stix2", + "events/stix", + "events/filterEventIdsForPush", + "events/checkuuid", + "events/pushProposals", + "events/exportChoice", + "events/importChoice", + "events/upload_sample", + "events/viewGraph", + "events/viewEventGraph", + "events/updateGraph", + "events/genDistributionGraph", + "events/getEventTimeline", + "events/getDistributionGraph", + "events/getEventGraphReferences", + "events/getEventGraphTags", + "events/getEventGraphGeneric", + "events/getReferenceData", + "events/getObjectTemplate", + "events/viewGalaxyMatrix", + "events/delegation_index", + "events/queryEnrichment", + "events/handleModuleResults", + "events/importModule", + "events/exportModule", + "events/toggleCorrelation", # TODO + "events/checkPublishedStatus", + "events/pushEventToKafka", + "events/getEventInfoById", + "events/enrichEvent", # TODO + "events/checkLocks", + "events/getEditStrategy", + "events/upload_analysis_file", + "events/cullEmptyEvents", + "favouriteTags/toggle", # TODO + "favouriteTags/getToggleField", # TODO + "feeds/feedCoverage", + "feeds/importFeeds", + "feeds/fetchFromAllFeeds", + "feeds/getEvent", + "feeds/previewIndex", # TODO + "feeds/previewEvent", # TODO + "feeds/enable", + "feeds/disable", + "feeds/fetchSelectedFromFreetextIndex", + "feeds/toggleSelected", # TODO + "galaxies/delete", + "galaxies/selectGalaxy", + "galaxies/selectGalaxyNamespace", + "galaxies/selectCluster", + "galaxies/attachCluster", + "galaxies/attachMultipleClusters", + "galaxies/viewGraph", + "galaxies/showGalaxies", + "galaxyClusters/index", + "galaxyClusters/view", + "galaxyClusters/attachToEvent", + "galaxyClusters/detach", + "galaxyClusters/delete", + "galaxyClusters/viewGalaxyMatrix", + "galaxyElements/index", + "jobs/index", + "jobs/getError", + "jobs/getGenerateCorrelationProgress", + "jobs/getProgress", + "jobs/cache", + "jobs/clearJobs", + "logs/event_index", + "admin/logs/search", + "logs/returnDates", + "logs/pruneUpdateLogs", + "logs/testForStolenAttributes", + "modules/queryEnrichment", + "modules/index", + "news/index", + "news/add", + "news/edit", + "news/delete", + "noticelists/toggleEnable", + "noticelists/getToggleField", + "noticelists/delete", + "objectReferences/view", + "objectTemplateElements/viewElements", + "objectTemplates/objectMetaChoice", + "objectTemplates/objectChoice", + "objectTemplates/delete", + "objectTemplates/viewElements", + "objectTemplates/activate", + "objectTemplates/getToggleField", + "objects/revise_object", + "objects/get_row", + "objects/editField", + "objects/fetchViewValue", + "objects/fetchEditForm", + "objects/quickFetchTemplateWithValidObjectAttributes", + "objects/quickAddAttributeForm", + "objects/orphanedObjectDiagnostics", + "objects/proposeObjectsFromAttributes", + "objects/groupAttributesIntoObject", + "admin/organisations/generateuuid", + "organisations/landingpage", + "organisations/fetchOrgsForSG", + "organisations/fetchSGOrgRow", + "organisations/getUUIDs", + "admin/organisations/merge", + "pages/display", + "posts/pushMessageToZMQ", + "posts/add", + "posts/edit", + "posts/delete", + "admin/regexp/add", + "admin/regexp/index", + "admin/regexp/edit", + "admin/regexp/delete", + "regexp/index", + "admin/regexp/clean", + "regexp/cleanRegexModifiers", + "restClientHistory/index", + "restClientHistory/delete", + "roles/view", + "admin/roles/add", # TODO + "admin/roles/edit", # TODO + "admin/roles/index", # TODO + "admin/roles/delete", # TODO + "servers/previewIndex", + "servers/previewEvent", + "servers/filterEventIndex", + "servers/eventBlockRule", + "servers/serverSettingsReloadSetting", + "servers/startWorker", # TODO + "servers/stopWorker", # TODO + "servers/getWorkers", # TODO + "servers/getSubmodulesStatus", # TODO, + "servers/restartDeadWorkers", # TODO + "servers/deleteFile", + "servers/uploadFile", + "servers/fetchServersForSG", + "servers/postTest", + "servers/getRemoteUser", + "servers/startZeroMQServer", + "servers/stopZeroMQServer", + "servers/statusZeroMQServer", + "servers/purgeSessions", + "servers/clearWorkerQueue", # TODO + "servers/getGit", + "servers/checkout", + "servers/ondemandAction", + "servers/updateProgress", + "servers/getSubmoduleQuickUpdateForm", + "servers/updateSubmodule", + "servers/getInstanceUUID", + "servers/getApiInfo", + "servers/cache", + "servers/updateJSON", + "servers/resetRemoteAuthKey", + "servers/changePriority", + "servers/releaseUpdateLock", + "servers/viewDeprecatedFunctionUse", + "shadowAttributes/download", + "shadowAttributes/add_attachment", + "shadowAttributes/discardSelected", + "shadowAttributes/acceptSelected", + "shadowAttributes/generateCorrelation", + "sharingGroups/edit", + "sharingGroups/view", + "sightingdb/add", + "sightingdb/edit", + "sightingdb/delete", + "sightingdb/index", + "sightingdb/requestStatus", + "sightingdb/search", + "sightings/advanced", + "sightings/quickAdd", + "sightings/quickDelete", + "sightings/viewSightings", + "sightings/bulkSaveSightings", + "tagCollections/add", + "tagCollections/import", + "tagCollections/view", + "tagCollections/edit", + "tagCollections/delete", + "tagCollections/addTag", + "tagCollections/removeTag", + "tagCollections/index", + "tagCollections/getRow", + "tags/quickAdd", + "tags/showEventTag", + "tags/showAttributeTag", + "tags/showTagControllerTag", + "tags/viewTag", + "tags/selectTaxonomy", + "tags/selectTag", + "tags/viewGraph", + "tags/search", + "tasks/index", + "tasks/setTask", + "taxonomies/hideTag", + "taxonomies/unhideTag", + "taxonomies/taxonomyMassConfirmation", + "taxonomies/taxonomyMassHide", + "taxonomies/taxonomyMassUnhide", + "taxonomies/delete", + "taxonomies/toggleRequired", + "templateElements/index", + "templateElements/templateElementAddChoices", + "templateElements/add", + "templateElements/edit", + "templateElements/delete", + "templates/index", + "templates/edit", + "templates/view", + "templates/add", + "templates/saveElementSorting", + "templates/delete", + "templates/templateChoices", + "templates/populateEventFromTemplate", + "templates/submitEventPopulation", + "templates/uploadFile", + "templates/deleteTemporaryFile", + "threads/viewEvent", + "threads/view", + "threads/index", + "userSettings/view", + "userSettings/setHomePage", + "users/request_API", + "admin/users/filterUserIndex", + "admin/users/view", + "admin/users/edit", + "users/updateLoginTime", + "users/login", + "users/routeafterlogin", + "users/logout", + "users/resetauthkey", + "users/resetAllSyncAuthKeys", + "users/histogram", + "users/terms", + "users/downloadTerms", + "users/checkAndCorrectPgps", + "admin/users/quickEmail", + "admin/users/email", + "users/initiatePasswordReset", + "users/email_otp", + "users/tagStatisticsGraph", + "users/verifyGPG", + "users/verifyCertificate", + "users/searchGpgKey", + "users/fetchGpgKey", + "users/checkIfLoggedIn", + "admin/users/monitor", + "warninglists/enableWarninglist", + "warninglists/getToggleField", + "warninglists/delete", + "admin/allowedlists/add", + "admin/allowedlists/index", + "admin/allowedlists/edit", + "admin/allowedlists/delete", + "allowedlists/index" + ] + missing = self.admin_misp_connector.get_all_functions(True) + with open('all_missing.json', 'w') as f: + json.dump(missing, f, indent=2) + final_missing = [] + for m in missing: + if any(m.startswith(s) for s in skip): + continue + final_missing.append(m) + with open('plop', 'w') as f: + json.dump(final_missing, f, indent=2) + print(final_missing) + print(len(final_missing)) + raise Exception() + if __name__ == '__main__': unittest.main() diff --git a/tests/testlive_sync.py b/tests/testlive_sync.py new file mode 100644 index 0000000..8d0e820 --- /dev/null +++ b/tests/testlive_sync.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import time +import unittest +import subprocess + +import urllib3 +import logging +logging.disable(logging.CRITICAL) + +try: + from pymisp import PyMISP, MISPOrganisation, MISPUser, MISPEvent, MISPObject, MISPSharingGroup, Distribution +except ImportError: + raise + +key = 'eYQdGTEWZJ8C2lm9EpnMqxQGwGiPNyoR75JvLdlE' +verifycert = False + + +urllib3.disable_warnings() + +''' +Static IP config + +auto eth1 +iface eth1 inet static +address 192.168.1.XXX +netmask 255.255.255.0 +network 192.168.1.0 +broadcast 192.168.1.255 +''' + +misp_instances = [ + { + 'url': 'https://localhost:8643', + 'external_baseurl': 'https://192.168.1.1', + 'key': key, + 'orgname': 'First org', + 'email_site_admin': 'first@site-admin.local', + 'email_admin': 'first@org-admin.local', + 'email_user': 'first@user.local' + }, + { + 'url': 'https://localhost:8644', + 'external_baseurl': 'https://192.168.1.2', + 'key': key, + 'orgname': 'Second org', + 'email_site_admin': 'second@site-admin.local', + 'email_admin': 'second@org-admin.local', + 'email_user': 'second@user.local' + }, + { + 'url': 'https://localhost:8645', + 'external_baseurl': 'https://192.168.1.3', + 'key': key, + 'orgname': 'Third org', + 'email_site_admin': 'third@site-admin.local', + 'email_admin': 'third@org-admin.local', + 'email_user': 'third@user.local' + }, +] + +# Assumes the VMs are already started, doesn't shut them down +fast_mode = True + + +class MISPInstance(): + + def __init__(self, params): + self.initial_user_connector = PyMISP(params['url'], params['key'], ssl=False, debug=False) + # Git pull + self.initial_user_connector.update_misp() + # Set the default role (id 3 on the VM is normal user) + self.initial_user_connector.set_default_role(3) + # Restart workers + self.initial_user_connector.restart_workers() + if not fast_mode: + # Load submodules + self.initial_user_connector.update_object_templates() + self.initial_user_connector.update_galaxies() + self.initial_user_connector.update_noticelists() + self.initial_user_connector.update_warninglists() + self.initial_user_connector.update_taxonomies() + + self.initial_user_connector.toggle_global_pythonify() + + # Create organisation + organisation = MISPOrganisation() + organisation.name = params['orgname'] + self.test_org = self.initial_user_connector.add_organisation(organisation) + print(self.test_org.name, self.test_org.uuid) + # Create Site admin in new org + user = MISPUser() + user.email = params['email_site_admin'] + user.org_id = self.test_org.id + user.role_id = 1 # Site admin + self.test_site_admin = self.initial_user_connector.add_user(user) + self.site_admin_connector = PyMISP(params['url'], self.test_site_admin.authkey, ssl=False, debug=False) + self.site_admin_connector.toggle_global_pythonify() + # Create org admin + user = MISPUser() + user.email = params['email_admin'] + user.org_id = self.test_org.id + user.role_id = 2 # Org admin + self.test_org_admin = self.site_admin_connector.add_user(user) + self.org_admin_connector = PyMISP(params['url'], self.test_org_admin.authkey, ssl=False, debug=False) + self.org_admin_connector.toggle_global_pythonify() + # Create user + user = MISPUser() + user.email = params['email_user'] + user.org_id = self.test_org.id + self.test_usr = self.org_admin_connector.add_user(user) + self.user_connector = PyMISP(params['url'], self.test_usr.authkey, ssl=False, debug=False) + self.user_connector.toggle_global_pythonify() + + # Setup external_baseurl + self.site_admin_connector.set_server_setting('MISP.external_baseurl', params['external_baseurl'], force=True) + # Setup baseurl + self.site_admin_connector.set_server_setting('MISP.baseurl', params['url'], force=True) + # Setup host org + self.site_admin_connector.set_server_setting('MISP.host_org_id', self.test_org.id) + + self.external_base_url = params['external_baseurl'] + self.sync = [] + self.sync_servers = [] + + def __repr__(self): + return f'<{self.__class__.__name__}(external={self.external_base_url})' + + def create_sync_user(self, organisation): + sync_org = self.site_admin_connector.add_organisation(organisation) + short_org_name = sync_org.name.lower().replace(' ', '-') + user = MISPUser() + user.email = f"sync_user@{short_org_name}.local" + user.org_id = sync_org.id + user.role_id = 5 # Org admin + sync_user = self.site_admin_connector.add_user(user) + sync_user_connector = PyMISP(self.site_admin_connector.root_url, sync_user.authkey, ssl=False, debug=False) + sync_server_config = sync_user_connector.get_sync_config(pythonify=True) + self.sync.append((sync_org, sync_user, sync_server_config)) + + def create_sync_server(self, name, server): + server = self.site_admin_connector.import_server(server) + server.self_signed = True + server.pull = True # Not automatic, but allows to do a pull + server = self.site_admin_connector.update_server(server) + r = self.site_admin_connector.test_server(server) + if r['status'] != 1: + raise Exception(f'Sync test failed: {r}') + self.sync_servers.append(server) + + def cleanup(self): + for org, user, _ in self.sync: + self.site_admin_connector.delete_user(user) # Delete user from other org + self.site_admin_connector.delete_organisation(org) + + # Delete sync servers + for server in self.site_admin_connector.servers(): + self.site_admin_connector.delete_server(server) + + # Delete users + self.org_admin_connector.delete_user(self.test_usr.id) + self.site_admin_connector.delete_user(self.test_org_admin.id) + self.initial_user_connector.delete_user(self.test_site_admin.id) + # Delete org + self.initial_user_connector.delete_organisation(self.test_org.id) + + # Make sure the instance is back to a clean state + if self.initial_user_connector.events(): + raise Exception(f'Events still on the instance {self.external_base_url}') + if self.initial_user_connector.attributes(): + raise Exception(f'Attributes still on the instance {self.external_base_url}') + if self.initial_user_connector.attribute_proposals(): + raise Exception(f'AttributeProposals still on the instance {self.external_base_url}') + if self.initial_user_connector.sightings(): + raise Exception(f'Sightings still on the instance {self.external_base_url}') + if self.initial_user_connector.servers(): + raise Exception(f'Servers still on the instance {self.external_base_url}') + if self.initial_user_connector.sharing_groups(): + raise Exception(f'SharingGroups still on the instance {self.external_base_url}') + if len(self.initial_user_connector.organisations()) > 1: + raise Exception(f'Organisations still on the instance {self.external_base_url}') + if len(self.initial_user_connector.users()) > 1: + raise Exception(f'Users still on the instance {self.external_base_url}') + + +class TestSync(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if not fast_mode: + subprocess.Popen(['VBoxHeadless', '-s', 'Test Sync 1']) + subprocess.Popen(['VBoxHeadless', '-s', 'Test Sync 2']) + subprocess.Popen(['VBoxHeadless', '-s', 'Test Sync 3']) + time.sleep(30) + cls.maxDiff = None + cls.instances = [] + for misp_instance in misp_instances: + mi = MISPInstance(misp_instance) + cls.instances.append(mi) + + # Create all sync users + test_orgs = [i.test_org for i in cls.instances] + + for instance in cls.instances: + for test_org in test_orgs: + if instance.test_org.name == test_org.name: + continue + instance.create_sync_user(test_org) + + # Create all sync links + sync_identifiers = [i.sync for i in cls.instances] + for instance in cls.instances: + for sync_identifier in sync_identifiers: + for org, user, sync_server_config in sync_identifier: + if org.name != instance.test_org.name: + continue + instance.create_sync_server(name=f'Sync with {sync_server_config.url}', + server=sync_server_config) + + ready = False + while not ready: + ready = True + for i in cls.instances: + settings = i.site_admin_connector.server_settings() + if (not settings['workers']['default']['ok'] + or not settings['workers']['prio']['ok']): + print(f'Not ready: {i}') + ready = False + time.sleep(1) + + @classmethod + def tearDownClass(cls): + for i in cls.instances: + i.cleanup() + if not fast_mode: + subprocess.Popen(['VBoxManage', 'controlvm', 'Test Sync 1', 'poweroff']) + subprocess.Popen(['VBoxManage', 'controlvm', 'Test Sync 2', 'poweroff']) + subprocess.Popen(['VBoxManage', 'controlvm', 'Test Sync 3', 'poweroff']) + time.sleep(20) + subprocess.Popen(['VBoxManage', 'snapshot', 'Test Sync 1', 'restore', 'WithRefresh']) + subprocess.Popen(['VBoxManage', 'snapshot', 'Test Sync 2', 'restore', 'WithRefresh']) + subprocess.Popen(['VBoxManage', 'snapshot', 'Test Sync 3', 'restore', 'WithRefresh']) + + def test_simple_sync(self): + '''Test simple event, push to one server''' + event = MISPEvent() + event.info = 'Event created on first instance - test_simple_sync' + event.distribution = Distribution.all_communities + event.add_attribute('ip-src', '1.1.1.1') + try: + source = self.instances[0] + dest = self.instances[1] + event = source.org_admin_connector.add_event(event) + source.org_admin_connector.publish(event) + source.site_admin_connector.server_push(source.sync_servers[0], event) + time.sleep(10) + dest_event = dest.org_admin_connector.get_event(event.uuid) + self.assertEqual(event.attributes[0].value, dest_event.attributes[0].value) + + finally: + source.org_admin_connector.delete_event(event) + dest.site_admin_connector.delete_event(dest_event) + + def test_sync_community(self): + '''Simple event, this community only, pull from member of the community''' + event = MISPEvent() + event.info = 'Event created on first instance - test_sync_community' + event.distribution = Distribution.this_community_only + event.add_attribute('ip-src', '1.1.1.1') + try: + source = self.instances[0] + dest = self.instances[1] + event = source.org_admin_connector.add_event(event) + source.org_admin_connector.publish(event) + dest.site_admin_connector.server_pull(dest.sync_servers[0]) + time.sleep(10) + dest_event = dest.org_admin_connector.get_event(event.uuid) + self.assertEqual(dest_event.distribution, 0) + finally: + source.org_admin_connector.delete_event(event) + dest.site_admin_connector.delete_event(dest_event) + + def test_sync_all_communities(self): + '''Simple event, all communities, enable automatic push on two sub-instances''' + event = MISPEvent() + event.info = 'Event created on first instance - test_sync_all_communities' + event.distribution = Distribution.all_communities + event.add_attribute('ip-src', '1.1.1.1') + try: + source = self.instances[0] + server = source.site_admin_connector.update_server({'push': True}, source.sync_servers[0].id) + self.assertTrue(server.push) + middle = self.instances[1] + middle.site_admin_connector.update_server({'push': True}, middle.sync_servers[1].id) # Enable automatic push to 3rd instance + last = self.instances[2] + event = source.user_connector.add_event(event) + source.org_admin_connector.publish(event) + source.site_admin_connector.server_push(source.sync_servers[0]) + time.sleep(30) + middle_event = middle.user_connector.get_event(event.uuid) + self.assertEqual(event.attributes[0].value, middle_event.attributes[0].value) + last_event = last.user_connector.get_event(event.uuid) + self.assertEqual(event.attributes[0].value, last_event.attributes[0].value) + finally: + source.org_admin_connector.delete_event(event) + middle.site_admin_connector.delete_event(middle_event) + last.site_admin_connector.delete_event(last_event) + source.site_admin_connector.update_server({'push': False}, source.sync_servers[0].id) + middle.site_admin_connector.update_server({'push': False}, middle.sync_servers[1].id) + + def create_complex_event(self): + event = MISPEvent() + event.info = 'Complex Event' + event.distribution = Distribution.all_communities + event.add_tag('tlp:white') + + event.add_attribute('ip-src', '8.8.8.8') + event.add_attribute('ip-dst', '8.8.8.9') + event.add_attribute('domain', 'google.com') + event.add_attribute('md5', '3c656da41f4645f77e3ec3281b63dd43') + + event.attributes[0].distribution = Distribution.your_organisation_only + event.attributes[1].distribution = Distribution.this_community_only + event.attributes[2].distribution = Distribution.connected_communities + + event.attributes[0].add_tag('tlp:red') + event.attributes[1].add_tag('tlp:amber') + event.attributes[2].add_tag('tlp:green') + + obj = MISPObject('file') + + obj.distribution = Distribution.connected_communities + obj.add_attribute('filename', 'testfile') + obj.add_attribute('md5', '3c656da41f4645f77e3ec3281b63dd44') + obj.attributes[0].distribution = Distribution.your_organisation_only + + event.add_object(obj) + + return event + + def test_complex_event_push_pull(self): + '''Test automatic push''' + event = self.create_complex_event() + try: + source = self.instances[0] + source.site_admin_connector.update_server({'push': True}, source.sync_servers[0].id) + middle = self.instances[1] + middle.site_admin_connector.update_server({'push': True}, middle.sync_servers[1].id) # Enable automatic push to 3rd instance + last = self.instances[2] + + event = source.org_admin_connector.add_event(event) + source.org_admin_connector.publish(event) + time.sleep(15) + event_middle = middle.user_connector.get_event(event.uuid) + event_last = last.user_connector.get_event(event.uuid) + self.assertEqual(len(event_middle.attributes), 2) # attribute 3 and 4 + self.assertEqual(len(event_middle.objects[0].attributes), 1) # attribute 2 + self.assertEqual(len(event_last.attributes), 1) # attribute 4 + self.assertFalse(event_last.objects) + # Test if event is properly sanitized + event_middle_as_site_admin = middle.site_admin_connector.get_event(event.uuid) + self.assertEqual(len(event_middle_as_site_admin.attributes), 2) # attribute 3 and 4 + self.assertEqual(len(event_middle_as_site_admin.objects[0].attributes), 1) # attribute 2 + # FIXME https://github.com/MISP/MISP/issues/4975 + # Force pull from the last one + # last.site_admin_connector.server_pull(last.sync_servers[0]) + # time.sleep(6) + # event_last = last.user_connector.get_event(event.uuid) + # self.assertEqual(len(event_last.objects[0].attributes), 1) # attribute 2 + # self.assertEqual(len(event_last.attributes), 2) # attribute 3 and 4 + # Force pull from the middle one + # middle.site_admin_connector.server_pull(last.sync_servers[0]) + # time.sleep(6) + # event_middle = middle.user_connector.get_event(event.uuid) + # self.assertEqual(len(event_middle.attributes), 3) # attribute 2, 3 and 4 + # Force pull from the last one + # last.site_admin_connector.server_pull(last.sync_servers[0]) + # time.sleep(6) + # event_last = last.user_connector.get_event(event.uuid) + # self.assertEqual(len(event_last.attributes), 2) # attribute 3 and 4 + finally: + source.org_admin_connector.delete_event(event) + middle.site_admin_connector.delete_event(event_middle) + last.site_admin_connector.delete_event(event_last) + source.site_admin_connector.update_server({'push': False}, source.sync_servers[0].id) + middle.site_admin_connector.update_server({'push': False}, middle.sync_servers[1].id) + + def test_complex_event_pull(self): + '''Test pull''' + event = self.create_complex_event() + try: + source = self.instances[0] + middle = self.instances[1] + last = self.instances[2] + + event = source.org_admin_connector.add_event(event) + source.org_admin_connector.publish(event) + middle.site_admin_connector.server_pull(middle.sync_servers[0]) + time.sleep(6) + last.site_admin_connector.server_pull(last.sync_servers[1]) + time.sleep(6) + event_middle = middle.user_connector.get_event(event.uuid) + event_last = last.user_connector.get_event(event.uuid) + self.assertEqual(len(event_middle.attributes), 3) # attribute 2, 3 and 4 + self.assertEqual(len(event_middle.objects[0].attributes), 1) # attribute 2 + self.assertEqual(len(event_last.attributes), 2) # attribute 3, 4 + self.assertEqual(len(event_last.objects[0].attributes), 1) + # Test if event is properly sanitized + event_middle_as_site_admin = middle.site_admin_connector.get_event(event.uuid) + self.assertEqual(len(event_middle_as_site_admin.attributes), 3) # attribute 2, 3 and 4 + self.assertEqual(len(event_middle_as_site_admin.objects[0].attributes), 1) # attribute 2 + finally: + source.org_admin_connector.delete_event(event) + middle.site_admin_connector.delete_event(event_middle) + last.site_admin_connector.delete_event(event_last) + + def test_sharing_group(self): + '''Test Sharing Group''' + event = self.create_complex_event() + try: + source = self.instances[0] + source.site_admin_connector.update_server({'push': True}, source.sync_servers[0].id) + middle = self.instances[1] + middle.site_admin_connector.update_server({'push': True}, middle.sync_servers[1].id) # Enable automatic push to 3rd instance + last = self.instances[2] + + sg = MISPSharingGroup() + sg.name = 'Testcases SG' + sg.releasability = 'Testing' + sharing_group = source.site_admin_connector.add_sharing_group(sg) + source.site_admin_connector.add_org_to_sharing_group(sharing_group, middle.test_org.uuid) + source.site_admin_connector.add_server_to_sharing_group(sharing_group, 0) # Add local server + # NOTE: the data on that sharing group *won't be synced anywhere* + + a = event.add_attribute('text', 'SG only attr') + a.distribution = Distribution.sharing_group + a.sharing_group_id = sharing_group.id + + event = source.org_admin_connector.add_event(event) + source.org_admin_connector.publish(event) + time.sleep(60) + + event_middle = middle.user_connector.get_event(event) + self.assertTrue(isinstance(event_middle, MISPEvent), event_middle) + self.assertEqual(len(event_middle.attributes), 2, event_middle) + self.assertEqual(len(event_middle.objects), 1, event_middle) + self.assertEqual(len(event_middle.objects[0].attributes), 1, event_middle) + + event_last = last.user_connector.get_event(event) + self.assertTrue(isinstance(event_last, MISPEvent), event_last) + self.assertEqual(len(event_last.attributes), 1) + # Test if event is properly sanitized + event_middle_as_site_admin = middle.site_admin_connector.get_event(event.uuid) + self.assertEqual(len(event_middle_as_site_admin.attributes), 2) + event_last_as_site_admin = last.site_admin_connector.get_event(event.uuid) + self.assertEqual(len(event_last_as_site_admin.attributes), 1) + # Get sharing group from middle instance + sgs = middle.site_admin_connector.sharing_groups() + self.assertEqual(len(sgs), 0) + + # TODO: Update sharing group so the attribute is pushed + # self.assertEqual(sgs[0].name, 'Testcases SG') + # middle.site_admin_connector.delete_sharing_group(sgs[0]) + finally: + source.org_admin_connector.delete_event(event) + middle.site_admin_connector.delete_event(event) + last.site_admin_connector.delete_event(event) + source.site_admin_connector.delete_sharing_group(sharing_group.id) + middle.site_admin_connector.delete_sharing_group(sharing_group.id) + source.site_admin_connector.update_server({'push': False}, source.sync_servers[0].id) + middle.site_admin_connector.update_server({'push': False}, middle.sync_servers[1].id) diff --git a/travis/install_travis.sh b/travis/install_travis.sh new file mode 100644 index 0000000..fd236e2 --- /dev/null +++ b/travis/install_travis.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +# We're in python3, installing with poetry. +pip3 install poetry +poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport -E url -E email diff --git a/travis/test_travis.sh b/travis/test_travis.sh new file mode 100644 index 0000000..7a924f4 --- /dev/null +++ b/travis/test_travis.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +poetry run nosetests-3.4 --with-coverage --cover-package=pymisp,tests --cover-tests tests/test_*.py +poetry run mypy tests/testlive_comprehensive.py tests/test_mispevent.py tests/testlive_sync.py pymisp +poetry run flake8 --ignore=E501,W503,E226,E252 pymisp