diff --git a/.coveragerc b/.coveragerc index b2758992..2fa526fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,3 @@ -[run] -omit = - opentaxii/cli/*.py +[run] +omit = + opentaxii/cli/*.py diff --git a/.dockerignore b/.dockerignore index 8841d65a..60d7dffe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,61 +1,61 @@ -# Git -.git/ - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Editor files -*.iml -.idea +# Git +.git/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Editor files +*.iml +.idea diff --git a/.flake8 b/.flake8 index 9968b3a7..39417276 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ -[flake8] -max-line-length=120 -exclude = docs/*,.tox/* -ignore = E203, W503, W504, E704 +[flake8] +max-line-length=120 +exclude = docs/*,.tox/* +ignore = E203, W503, W504, E704 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2caebff7..388315c7 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,5 +1,5 @@ -# Initial commit to format the codebase with black -e454428fdf5b3d32f8ffbe9bded3878a80e7b169 -# Initial commit to sort import on the codebase -2d69e4fb6daf037cb2f4358fde0c6c96e3f7ca16 +# Initial commit to format the codebase with black +e454428fdf5b3d32f8ffbe9bded3878a80e7b169 +# Initial commit to sort import on the codebase +2d69e4fb6daf037cb2f4358fde0c6c96e3f7ca16 \ No newline at end of file diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index 36af0c03..9bc48dcd 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -1,10 +1,10 @@ -name: Check flake8 - -on: - - pull_request -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: TrueBrain/actions-flake8@v2 +name: Check flake8 + +on: + - pull_request +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: TrueBrain/actions-flake8@v2 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index faf097ac..e67988d8 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,19 +1,19 @@ -name: Check format - -on: - - pull_request -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install deps - run: pip install -r requirements-dev.txt - - name: Run black - run: black --check opentaxii/ tests/ - - name: Run isort - run: isort --check-only opentaxii/ tests/ +name: Check format + +on: + - pull_request +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install deps + run: pip install -r requirements-dev.txt + - name: Run black + run: black --check opentaxii/ tests/ + - name: Run isort + run: isort --check-only opentaxii/ tests/ diff --git a/.github/workflows/interrogate.sh b/.github/workflows/interrogate.sh index 18b9605d..4ef05f3b 100755 --- a/.github/workflows/interrogate.sh +++ b/.github/workflows/interrogate.sh @@ -1,5 +1,5 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) - -interrogate --fail-under=0 --config "$SCRIPT_DIR/../../pyproject.toml" $1 | grep -o "actual: [0-9]*\.[0-9]*%" | grep -o "[0-9]*\.[0-9]*" +#!/usr/bin/env bash + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +interrogate --fail-under=0 --config "$SCRIPT_DIR/../../pyproject.toml" $1 | grep -o "actual: [0-9]*\.[0-9]*%" | grep -o "[0-9]*\.[0-9]*" diff --git a/.github/workflows/interrogate.yml b/.github/workflows/interrogate.yml index 21bebc51..4374363b 100644 --- a/.github/workflows/interrogate.yml +++ b/.github/workflows/interrogate.yml @@ -1,27 +1,27 @@ -name: Check docstring coverage - -on: - - pull_request -jobs: - check: - runs-on: ubuntu-latest - steps: - - name: Checkout head branch - uses: actions/checkout@v4 - with: - path: head - - name: Checkout base branch - uses: actions/checkout@v4 - with: - ref: ${{ github.base_ref }} - path: base - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install interrogate - run: pip install -r head/requirements-interrogate.txt - - name: Run interrogate on base branch for baseline - run: echo "old_cov=$(echo $(head/.github/workflows/interrogate.sh base/opentaxii) - '0.1' | bc)" >> $GITHUB_ENV - - name: Run interrogate on head branch to check coverage didn't drop - run: interrogate --fail-under=${{ env.old_cov }} head/opentaxii +name: Check docstring coverage + +on: + - pull_request +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout head branch + uses: actions/checkout@v4 + with: + path: head + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + path: base + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install interrogate + run: pip install -r head/requirements-interrogate.txt + - name: Run interrogate on base branch for baseline + run: echo "old_cov=$(echo $(head/.github/workflows/interrogate.sh base/opentaxii) - '0.1' | bc)" >> $GITHUB_ENV + - name: Run interrogate on head branch to check coverage didn't drop + run: interrogate --fail-under=${{ env.old_cov }} head/opentaxii diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index a24bfec1..dfc09665 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,17 +1,17 @@ -name: Check mypy - -on: - - pull_request -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install deps - run: pip install -r requirements.txt -r requirements-dev.txt - - name: Run mypy - run: mypy +name: Check mypy + +on: + - pull_request +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install deps + run: pip install -r requirements.txt -r requirements-dev.txt + - name: Run mypy + run: mypy diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b107d74a..53189d82 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,77 +1,77 @@ -name: Publish - -on: - push: - tags: - - '[0-9]+.[0-9]+.[0-9]+[ab]?[0-9]?' - branches: - - master - pull_request: - branches: - - 'master' - -jobs: - build-n-publish-package: - name: Build and publish to PyPI - if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - - name: Create github release - uses: ncipollo/release-action@v1 - with: - artifacts: "dist/opentaxii*.whl,dist/opentaxii*.tar.gz" - bodyFile: "CHANGES.rst" - token: ${{ secrets.GITHUB_TOKEN }} - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - build-n-publish-docker: - name: Build and publish to Docker - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v5 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: eclecticiq/opentaxii - tags: | - type=ref,event=branch - type=pep440,pattern={{version}} - - - name: Login to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} +name: Publish + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+[ab]?[0-9]?' + branches: + - master + pull_request: + branches: + - 'master' + +jobs: + build-n-publish-package: + name: Build and publish to PyPI + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + - name: Create github release + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/opentaxii*.whl,dist/opentaxii*.tar.gz" + bodyFile: "CHANGES.rst" + token: ${{ secrets.GITHUB_TOKEN }} + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + + build-n-publish-docker: + name: Build and publish to Docker + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v5 + - + name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: eclecticiq/opentaxii + tags: | + type=ref,event=branch + type=pep440,pattern={{version}} + - + name: Login to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v6 + with: + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index a2a925ba..08f65b15 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -1,70 +1,70 @@ -name: Tox - -on: - pull_request: - push: - branches: - - master -jobs: - test: - runs-on: ubuntu-22.04 - strategy: - matrix: - python-version: ["3.10", "3.11", "pypy-3.10"] - services: - mariadb: - image: mariadb:10.11 - env: - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes - MARIADB_DATABASE: test - ports: - - 3307:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - mysql: - image: mysql:8.1 - env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: test - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - postgres: - image: postgres:15.4 - env: - POSTGRES_PASSWORD: test - POSTGRES_USER: test - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install lxml dependencies - if: ${{ matrix.python-version == 'pypy-3.10' }} - run: | - sudo apt-get install libxml2-dev libxslt-dev - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Verify MariaDB connection - run: | - while ! mysqladmin ping -h"127.0.0.1" -P"3307" --silent; do - sleep 1 - done - - name: Verify mysql connection - run: | - while ! mysqladmin ping -h"127.0.0.1" -P"3306" --silent; do - sleep 1 - done - - name: Verify postgres connection - run: | - while ! pg_isready -h"127.0.0.1" --quiet; do - sleep 1 - done - - name: Test with tox +name: Tox + +on: + pull_request: + push: + branches: + - master +jobs: + test: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.10", "3.11", "pypy-3.10"] + services: + mariadb: + image: mariadb:10.11 + env: + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes + MARIADB_DATABASE: test + ports: + - 3307:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + mysql: + image: mysql:8.1 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + postgres: + image: postgres:15.4 + env: + POSTGRES_PASSWORD: test + POSTGRES_USER: test + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install lxml dependencies + if: ${{ matrix.python-version == 'pypy-3.10' }} + run: | + sudo apt-get install libxml2-dev libxslt-dev + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Verify MariaDB connection + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"3307" --silent; do + sleep 1 + done + - name: Verify mysql connection + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"3306" --silent; do + sleep 1 + done + - name: Verify postgres connection + run: | + while ! pg_isready -h"127.0.0.1" --quiet; do + sleep 1 + done + - name: Test with tox run: tox --recreate \ No newline at end of file diff --git a/.gitignore b/.gitignore index e42b1ae0..ebdce0f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,58 +1,58 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Editor files -*.iml -.idea +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Editor files +*.iml +.idea diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e473e72e..2452c33d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,16 @@ -version: 2 - -# Build from the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - builder: "dirhtml" - -# Explicitly set the version of Python and its requirements -build: - os: "ubuntu-24.04" - tools: - python: "3.12" - -python: - install: - - requirements: docs/requirements.txt +version: 2 + +# Build from the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + builder: "dirhtml" + +# Explicitly set the version of Python and its requirements +build: + os: "ubuntu-24.04" + tools: + python: "3.12" + +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGES.rst b/CHANGES.rst index 68fb7142..979c8661 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,225 +1,225 @@ -Changelog -========= - -0.10.0 (unreleased) -------------------- - -Features: - -* Add --public and --id params to the CLI opentaxii-add-api-root `#283 - `_ -* Enable TAXII 2.1 in the default configuration `#293 - `_ -* Improve get objects performance `#297 - `_ - -Bug fixes: - -* Fix UUID json serialization `#289 - `_ -* Returned ``spec_version`` was equal to the ``type`` `#237 - `_ (@meetghodasara-crest) - and `#284 `_ -* Consider ``--admin`` flag when creating an account from the CLI with - ``opentaxii-create-account`` `#237 - `_ -* TAXII 2.1 version supports now objects without a ``modified`` property. - ``created`` is used as a fallback and if missing ``01/01/1970`` date is used - instead. `#240 `_ - (@MagsenAbbeThales) -* Deleted objects were not persisted in the database `#275 - `_ (@meetghodasara-crest) -* ``can_read`` or ``can_write`` could be returned with the ``null`` value on a - TAXII2 collection `#292 `_ - -Dependencies: - -* Add Python 3.11 and 3.12 support -* Drop EOL Python 3.6, 3.7, 3.8 and 3.9 support -* Drop sqlalchemy 1.3 support -* Require gunicorn ``>=22`` for security reasons `#269 - `_ -* Remove mypy_extensions dependency - -Development: - -* Add Docker build & push action in the CI `#296 - `_ -* Add and enforce mypy type checker `#289 - `_ -* Add and enforce black and isort formatter `#290 - `_ - -0.9.3 (2022-10-11) ------------------- -* Add public write support. - -0.9.2 (2022-08-26) ------------------- -* Improve readability and navigation of docs (`#238 `_ thanks `@zed-eiq `_ for the improvement). - -0.9.1 (2022-07-11) ------------------- -* Implement `raise_unauthorized` for taxii2, this was missing and lead to 500 errors. - -0.9.0 (2022-06-13) ------------------- -* Allow custom properties. This can be disabled by config option ``allow_custom_properties`` - -0.8.0 (2022-06-05) ------------------- -* Nest details inside taxii2 job and allow counts without details - -0.7.0 (2022-05-27) ------------------- -* Nest taxii2 endpoints under `/taxii2/` - -0.6.0 (2022-05-25) ------------------- -* Add `public_discovery` option to taxii2 config -* Add support for publicly readable taxii 2 api roots - -0.5.0 (2022-05-24) ------------------- -* Add support for publicly readable taxii 2 collections - -0.4.0 (2022-05-20) ------------------- -* Move next_param handling into `OpenTAXII2PersistenceAPI` - -0.3.0 (2022-04-13) ------------------- -* Implement taxii2.1 support - -0.3.0a4 (2022-04-13) --------------------- -* Merge changes from 0.2.4 maintenance release - -0.3.0a3 (2022-01-21) --------------------- -* Fix bug that prevented booting with only taxii1 config (`#217 `_ thanks `@azurekid `_ for the report) - -0.3.0a2 (2021-12-27) --------------------- -* Merge changes from 0.2.3 maintenance release - -0.3.0a1 -------- -* Add python 3.10 support - -0.3.0a0 -------- -* Enablement for future taxii2 implementation -* Fix documentation build issues - -0.2.4 (2022-04-13) ------------------- -* Make sure werkzeug <2.1 and >=2.1 work correctly with auth system - -0.2.3 (2021-12-22) ------------------- -* Fix bug in multithreaded use of sqlite (`#210 `_ thanks `@rohits144 `_ for the report) - -0.2.2 (2021-11-05) ------------------- -* Fix readthedocs build - -0.2.1 (2021-11-03) ------------------- -* Add tests for python 3.6, 3.7, 3.8, 3.9, pypy -* Add tests for sqlite, mysql, mariadb, postgresql -* Fix bug that broke ``delete_content_blocks`` when using mysql on sqlalchemy 1.3 -* Docs: Add db schema diagram -* Docs: Clarify how to get default data in a default (development) docker instance -* Fix implicit routing in TAXII 1.1 Inboxes -* Update jwt usage to pyjwt >= 2.0 (thanks `@SanyaKapoor `_) - -0.2.0 (2020-06-30) ------------------- -* Enforce UTC usage in datetime fields in SQL DB Persistence API. -* `Fix for #114 `_: reintroduce ``opentaxii-create-account`` CLI command. -* `Fix for #153 `_: check if user can modify a collection before advertising it over inbox service. -* Multiple coding style fixes. -* Various documentation updates. - -0.1.12 (2019-03-06) -------------------- -* Remove unnecessary print statements. - -0.1.11 (2019-02-13) -------------------- -* Make JSON logging consistent when the application is run via Gunicorn. -* Set ``acceptable_destination`` key in status details instead of extended headers -* Allow passing ``engine_parameters`` to ``SQLDatabaseAPI`` for those who want to customize SQLAlchemy engine parameters. -* Require recent version of ``lxml`` for security reasons. -* Various test and Docker infrastructure improvements. - -0.1.10 (2018-06-03) -------------------- -* Replace separate service/collection/account creation process with single ``opentaxii-sync-data`` CLI command. -* Persistence and Auth APIs extended with missing CRUD methods, that are used by ``opentaxii-sync-data``. -* Read/modify collection level ACL added. -* DB models for default implementation of Persistence API and Auth API were changed. No automatic migration code is provided (sorry!), so upgrading might require manual DB migration. -* Drop python2.7 from testing scope. -* Various bug fixes and improvements. - -0.1.9 (2017-06-19) ------------------- -* `libtaxii `_ dependency upgraded to 1.1.111. -* Various bug fixes and improvements (thanks to `@bjigmp `_, `@chorsley `_, `@rjprins `_). - -0.1.8 (2017-02-21) ------------------- -* Ability to enable/disable "huge trees" support in XML parser. Configuration property ``xml_parser_supports_huge_tree`` set to ``yes`` or ``true`` will disable security restrictions and force XML parser to support very deep trees and very long text content. -* Adjust SQL Persistence API implemetation so it works smoothly with MySQL backend. -* Use Python 3.5 instead of Python 3.4 for tests. - -0.1.7 (2016-10-18) ------------------- -* Minor fixes. -* Dependencies were changed from hard-pinned to more flexible. -* Example of production DB configuration added to docs. - -0.1.6 (2016-06-01) ------------------- -* Python 3.4 compatibility of the codebase. Tox configuration extended with python 3.4 environment run. -* Flake8 full style compatibility. Flake8 check added to Tox configuration. -* SQLAlchemy session scope issue fixed (related to `#38 `_). -* `opentaxii-delete-blocks` CLI command added (related to `#45 `_). -* `delete_content_blocks` method `added `_ to Persistence API. -* Collection's name is `required `_ to be unique in default SQL DB Auth API implementation. - -0.1.5 (2016-03-15) ------------------- -* Fix for the issue with persistence backend returning ``None`` instead of ``InboxMessage`` object - -0.1.4 (2016-02-25) ------------------- -* Hard-coded dependencies in ``setup.py`` removed. - -0.1.3 (2016-02-25) ------------------- -* Versions of dependencies are pinned. -* Code adjusted for a new version of `anyconfig `_ API. -* Test for configuration loading added. - -0.1.2 (2015-07-24) ------------------- -* Docker configuration added. -* Health check endpoint added. -* Basic authentication support added. -* Temporary workaround for `Issue #191 `_. -* Method ``get_domain`` in Persistence API returns domain value configured for ``service_id``. If nothing returned, value set in configuration file will be used. -* Performance optimisations. -* Bug fixes and style improvements. - -0.1.1 (2015-04-08) ------------------- -* Alias for Root Logger added to logging configuration. -* Context object in a request scope that holds account and token added. -* Support for OPTIONS HTTP request to enable auto version negotiation added. -* Documentation improved. - -0.1.0 (2015-03-31) ------------------- -* Initial release +Changelog +========= + +0.10.0 (unreleased) +------------------- + +Features: + +* Add --public and --id params to the CLI opentaxii-add-api-root `#283 + `_ +* Enable TAXII 2.1 in the default configuration `#293 + `_ +* Improve get objects performance `#297 + `_ + +Bug fixes: + +* Fix UUID json serialization `#289 + `_ +* Returned ``spec_version`` was equal to the ``type`` `#237 + `_ (@meetghodasara-crest) + and `#284 `_ +* Consider ``--admin`` flag when creating an account from the CLI with + ``opentaxii-create-account`` `#237 + `_ +* TAXII 2.1 version supports now objects without a ``modified`` property. + ``created`` is used as a fallback and if missing ``01/01/1970`` date is used + instead. `#240 `_ + (@MagsenAbbeThales) +* Deleted objects were not persisted in the database `#275 + `_ (@meetghodasara-crest) +* ``can_read`` or ``can_write`` could be returned with the ``null`` value on a + TAXII2 collection `#292 `_ + +Dependencies: + +* Add Python 3.11 and 3.12 support +* Drop EOL Python 3.6, 3.7, 3.8 and 3.9 support +* Drop sqlalchemy 1.3 support +* Require gunicorn ``>=22`` for security reasons `#269 + `_ +* Remove mypy_extensions dependency + +Development: + +* Add Docker build & push action in the CI `#296 + `_ +* Add and enforce mypy type checker `#289 + `_ +* Add and enforce black and isort formatter `#290 + `_ + +0.9.3 (2022-10-11) +------------------ +* Add public write support. + +0.9.2 (2022-08-26) +------------------ +* Improve readability and navigation of docs (`#238 `_ thanks `@zed-eiq `_ for the improvement). + +0.9.1 (2022-07-11) +------------------ +* Implement `raise_unauthorized` for taxii2, this was missing and lead to 500 errors. + +0.9.0 (2022-06-13) +------------------ +* Allow custom properties. This can be disabled by config option ``allow_custom_properties`` + +0.8.0 (2022-06-05) +------------------ +* Nest details inside taxii2 job and allow counts without details + +0.7.0 (2022-05-27) +------------------ +* Nest taxii2 endpoints under `/taxii2/` + +0.6.0 (2022-05-25) +------------------ +* Add `public_discovery` option to taxii2 config +* Add support for publicly readable taxii 2 api roots + +0.5.0 (2022-05-24) +------------------ +* Add support for publicly readable taxii 2 collections + +0.4.0 (2022-05-20) +------------------ +* Move next_param handling into `OpenTAXII2PersistenceAPI` + +0.3.0 (2022-04-13) +------------------ +* Implement taxii2.1 support + +0.3.0a4 (2022-04-13) +-------------------- +* Merge changes from 0.2.4 maintenance release + +0.3.0a3 (2022-01-21) +-------------------- +* Fix bug that prevented booting with only taxii1 config (`#217 `_ thanks `@azurekid `_ for the report) + +0.3.0a2 (2021-12-27) +-------------------- +* Merge changes from 0.2.3 maintenance release + +0.3.0a1 +------- +* Add python 3.10 support + +0.3.0a0 +------- +* Enablement for future taxii2 implementation +* Fix documentation build issues + +0.2.4 (2022-04-13) +------------------ +* Make sure werkzeug <2.1 and >=2.1 work correctly with auth system + +0.2.3 (2021-12-22) +------------------ +* Fix bug in multithreaded use of sqlite (`#210 `_ thanks `@rohits144 `_ for the report) + +0.2.2 (2021-11-05) +------------------ +* Fix readthedocs build + +0.2.1 (2021-11-03) +------------------ +* Add tests for python 3.6, 3.7, 3.8, 3.9, pypy +* Add tests for sqlite, mysql, mariadb, postgresql +* Fix bug that broke ``delete_content_blocks`` when using mysql on sqlalchemy 1.3 +* Docs: Add db schema diagram +* Docs: Clarify how to get default data in a default (development) docker instance +* Fix implicit routing in TAXII 1.1 Inboxes +* Update jwt usage to pyjwt >= 2.0 (thanks `@SanyaKapoor `_) + +0.2.0 (2020-06-30) +------------------ +* Enforce UTC usage in datetime fields in SQL DB Persistence API. +* `Fix for #114 `_: reintroduce ``opentaxii-create-account`` CLI command. +* `Fix for #153 `_: check if user can modify a collection before advertising it over inbox service. +* Multiple coding style fixes. +* Various documentation updates. + +0.1.12 (2019-03-06) +------------------- +* Remove unnecessary print statements. + +0.1.11 (2019-02-13) +------------------- +* Make JSON logging consistent when the application is run via Gunicorn. +* Set ``acceptable_destination`` key in status details instead of extended headers +* Allow passing ``engine_parameters`` to ``SQLDatabaseAPI`` for those who want to customize SQLAlchemy engine parameters. +* Require recent version of ``lxml`` for security reasons. +* Various test and Docker infrastructure improvements. + +0.1.10 (2018-06-03) +------------------- +* Replace separate service/collection/account creation process with single ``opentaxii-sync-data`` CLI command. +* Persistence and Auth APIs extended with missing CRUD methods, that are used by ``opentaxii-sync-data``. +* Read/modify collection level ACL added. +* DB models for default implementation of Persistence API and Auth API were changed. No automatic migration code is provided (sorry!), so upgrading might require manual DB migration. +* Drop python2.7 from testing scope. +* Various bug fixes and improvements. + +0.1.9 (2017-06-19) +------------------ +* `libtaxii `_ dependency upgraded to 1.1.111. +* Various bug fixes and improvements (thanks to `@bjigmp `_, `@chorsley `_, `@rjprins `_). + +0.1.8 (2017-02-21) +------------------ +* Ability to enable/disable "huge trees" support in XML parser. Configuration property ``xml_parser_supports_huge_tree`` set to ``yes`` or ``true`` will disable security restrictions and force XML parser to support very deep trees and very long text content. +* Adjust SQL Persistence API implemetation so it works smoothly with MySQL backend. +* Use Python 3.5 instead of Python 3.4 for tests. + +0.1.7 (2016-10-18) +------------------ +* Minor fixes. +* Dependencies were changed from hard-pinned to more flexible. +* Example of production DB configuration added to docs. + +0.1.6 (2016-06-01) +------------------ +* Python 3.4 compatibility of the codebase. Tox configuration extended with python 3.4 environment run. +* Flake8 full style compatibility. Flake8 check added to Tox configuration. +* SQLAlchemy session scope issue fixed (related to `#38 `_). +* `opentaxii-delete-blocks` CLI command added (related to `#45 `_). +* `delete_content_blocks` method `added `_ to Persistence API. +* Collection's name is `required `_ to be unique in default SQL DB Auth API implementation. + +0.1.5 (2016-03-15) +------------------ +* Fix for the issue with persistence backend returning ``None`` instead of ``InboxMessage`` object + +0.1.4 (2016-02-25) +------------------ +* Hard-coded dependencies in ``setup.py`` removed. + +0.1.3 (2016-02-25) +------------------ +* Versions of dependencies are pinned. +* Code adjusted for a new version of `anyconfig `_ API. +* Test for configuration loading added. + +0.1.2 (2015-07-24) +------------------ +* Docker configuration added. +* Health check endpoint added. +* Basic authentication support added. +* Temporary workaround for `Issue #191 `_. +* Method ``get_domain`` in Persistence API returns domain value configured for ``service_id``. If nothing returned, value set in configuration file will be used. +* Performance optimisations. +* Bug fixes and style improvements. + +0.1.1 (2015-04-08) +------------------ +* Alias for Root Logger added to logging configuration. +* Context object in a request scope that holds account and token added. +* Support for OPTIONS HTTP request to enable auto version negotiation added. +* Documentation improved. + +0.1.0 (2015-03-31) +------------------ +* Initial release diff --git a/Dockerfile b/Dockerfile index d8d074e1..04f53377 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,36 @@ -FROM python:3.10 AS build -LABEL maintainer="EclecticIQ " - -RUN apt-get update \ - && apt-get install -y libmariadb-dev-compat - -RUN python3 -m venv /venv && /venv/bin/pip install -U pip setuptools - -COPY ./requirements.txt ./requirements-docker.txt /opentaxii/ -RUN /venv/bin/pip install -r /opentaxii/requirements.txt -r /opentaxii/requirements-docker.txt - -COPY . /opentaxii -RUN /venv/bin/pip install /opentaxii - - -FROM python:3.10-slim AS prod -LABEL maintainer="EclecticIQ " -COPY --from=build /venv /venv - -RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y mariadb-client postgresql-client \ - && apt-get autoremove \ - && apt-get autoclean \ - && mkdir /data /input -VOLUME ["/data", "/input"] - -COPY ./docker/entrypoint.sh /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] - -EXPOSE 9000 -ENV PATH="/venv/bin:${PATH}" -ENV PYTHONDONTWRITEBYTECODE="1" -CMD ["/venv/bin/gunicorn", "opentaxii.http:app", "--workers=2", \ - "--log-level=info", "--log-file=-", "--timeout=300", \ - "--config=python:opentaxii.http", "--bind=0.0.0.0:9000"] +FROM python:3.10 AS build +LABEL maintainer="EclecticIQ " + +RUN apt-get update \ + && apt-get install -y libmariadb-dev-compat + +RUN python3 -m venv /venv && /venv/bin/pip install -U pip setuptools + +COPY ./requirements.txt ./requirements-docker.txt /opentaxii/ +RUN /venv/bin/pip install -r /opentaxii/requirements.txt -r /opentaxii/requirements-docker.txt + +COPY . /opentaxii +RUN /venv/bin/pip install /opentaxii + + +FROM python:3.10-slim AS prod +LABEL maintainer="EclecticIQ " +COPY --from=build /venv /venv + +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y mariadb-client postgresql-client \ + && apt-get autoremove \ + && apt-get autoclean \ + && mkdir /data /input +VOLUME ["/data", "/input"] + +COPY ./docker/entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +EXPOSE 9000 +ENV PATH="/venv/bin:${PATH}" +ENV PYTHONDONTWRITEBYTECODE="1" +CMD ["/venv/bin/gunicorn", "opentaxii.http:app", "--workers=2", \ + "--log-level=info", "--log-file=-", "--timeout=300", \ + "--config=python:opentaxii.http", "--bind=0.0.0.0:9000"] diff --git a/LICENSE.rst b/LICENSE.rst index 0f6c06c0..1fc200a0 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,27 +1,27 @@ -Copyright (c) [2016], EclecticIQ -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) [2016], EclecticIQ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index 4fb86f9d..3e1b1fd0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ -include LICENSE.rst -include CHANGES.rst -include README.rst -include requirements.txt -recursive-include docs * -recursive-include opentaxii *.yml -global-exclude *.pyc +include LICENSE.rst +include CHANGES.rst +include README.rst +include requirements.txt +recursive-include docs * +recursive-include opentaxii *.yml +global-exclude *.pyc diff --git a/README.md b/README.md index 454697df..efaecd0b 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,50 @@ -# OpenTAXII - -TAXII server implementation in Python from [EclecticIQ](https://www.eclecticiq.com). - -OpenTAXII is a robust Python implementation of TAXII Services that -delivers rich feature set and friendly pythonic API built on top of well -designed application. - -OpenTAXII is guaranteed to be compatible with [Cabby](https://github.com/eclecticiq/cabby), TAXII client library. - -[Source](https://github.com/eclecticiq/OpenTAXII) | [Documentation](http://opentaxii.readthedocs.org) | [Information](http://www.eclecticiq.com) | [Download](https://pypi.python.org/pypi/opentaxii/) - - -[![Build status](https://github.com/eclecticiq/OpenTAXII/actions/workflows/tox.yml/badge.svg?branch=master)](https://github.com/eclecticiq/OpenTAXII/actions/workflows/tox.yml?query=branch%3Amaster+) -[![Coverage Status](https://coveralls.io/repos/eclecticiq/OpenTAXII/badge.svg)](https://coveralls.io/r/eclecticiq/OpenTAXII) -[![Documentation Status](https://readthedocs.org/projects/opentaxii/badge/?version=stable)](https://readthedocs.org/projects/opentaxii/) - -## State of the project - -We have made the decision to consider this project **feature-complete**. It means we still maintain it, however we focus only on bug fixes. Still, we’re very open to external contributions - if you know how to fix an issue and you can open a PR, we will be very grateful. - -## Getting started -See [the documentation](https://opentaxii.readthedocs.io/en/stable/installation.html). - -## Getting started with OpenTAXII using Docker - -OpenTAXII can also be run using docker. This guide assumes that you have -access to a local or remote docker server, and won't go into the setup -of docker. - -To get a default (development) instance using docker - -```bash -$ docker run -d -p 9000:9000 eclecticiq/opentaxii -``` - -To have the instance preloaded with example data, see [the documentation on docker volumes](https://opentaxii.readthedocs.io/en/stable/docker.html#volumes). - -> **NOTE:** -> OpenTAXII is now accessible through port 9000, with data stored -> locally in a SQLite databases optionally using services/collections/accounts defined -> in [data-configuration.yml](https://raw.githubusercontent.com/EclecticIQ/OpenTAXII/master/examples/data-configuration.yml) - -More documentation on running OpenTAXII in a container is found in the [OpenTAXII Docker Documentation](https://opentaxii.readthedocs.io/en/stable/docker.html). - -## Feedback - -You are encouraged to provide feedback by commenting on open issues or -sending us email at - +# OpenTAXII + +TAXII server implementation in Python from [EclecticIQ](https://www.eclecticiq.com). + +OpenTAXII is a robust Python implementation of TAXII Services that +delivers rich feature set and friendly pythonic API built on top of well +designed application. + +OpenTAXII is guaranteed to be compatible with [Cabby](https://github.com/eclecticiq/cabby), TAXII client library. + +[Source](https://github.com/eclecticiq/OpenTAXII) | [Documentation](http://opentaxii.readthedocs.org) | [Information](http://www.eclecticiq.com) | [Download](https://pypi.python.org/pypi/opentaxii/) + + +[![Build status](https://github.com/eclecticiq/OpenTAXII/actions/workflows/tox.yml/badge.svg?branch=master)](https://github.com/eclecticiq/OpenTAXII/actions/workflows/tox.yml?query=branch%3Amaster+) +[![Coverage Status](https://coveralls.io/repos/eclecticiq/OpenTAXII/badge.svg)](https://coveralls.io/r/eclecticiq/OpenTAXII) +[![Documentation Status](https://readthedocs.org/projects/opentaxii/badge/?version=stable)](https://readthedocs.org/projects/opentaxii/) + +## State of the project + +We have made the decision to consider this project **feature-complete**. It means we still maintain it, however we focus only on bug fixes. Still, we’re very open to external contributions - if you know how to fix an issue and you can open a PR, we will be very grateful. + +## Getting started +See [the documentation](https://opentaxii.readthedocs.io/en/stable/installation.html). + +## Getting started with OpenTAXII using Docker + +OpenTAXII can also be run using docker. This guide assumes that you have +access to a local or remote docker server, and won't go into the setup +of docker. + +To get a default (development) instance using docker + +```bash +$ docker run -d -p 9000:9000 eclecticiq/opentaxii +``` + +To have the instance preloaded with example data, see [the documentation on docker volumes](https://opentaxii.readthedocs.io/en/stable/docker.html#volumes). + +> **NOTE:** +> OpenTAXII is now accessible through port 9000, with data stored +> locally in a SQLite databases optionally using services/collections/accounts defined +> in [data-configuration.yml](https://raw.githubusercontent.com/EclecticIQ/OpenTAXII/master/examples/data-configuration.yml) + +More documentation on running OpenTAXII in a container is found in the [OpenTAXII Docker Documentation](https://opentaxii.readthedocs.io/en/stable/docker.html). + +## Feedback + +You are encouraged to provide feedback by commenting on open issues or +sending us email at + diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index bee5e073..5a706ee2 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,112 +1,112 @@ -#!/bin/bash - - -trap "echo 'Shutting down OpenTAXII' && exit" EXIT SIGINT SIGTERM - -function wait_for_port() { - (echo >/dev/tcp/$1/$2) &>/dev/null - while [ $? -ne 0 ]; - do - echo "Waiting for $1:$2 to become available" - sleep 1 - (echo >/dev/tcp/$1/$2) &>/dev/null - done - echo "$1:$2 is now open" -} - -# Default OpenTAXII Configuration -: ${OPENTAXII_DOMAIN:=localhost:9000} -: ${DOCKER_OPENTAXII_AUTH_SECRET:=notVerySecret} -: ${OPENTAXII_CONFIG:=/opentaxii.yml} -# make sure this env var is available to the main process and subprocesses! -export OPENTAXII_CONFIG - -# Database Defaults -P_URL="sqlite:////data/data.db" -A_URL="sqlite:////data/auth.db" -: ${DATABASE_USER:=""} -: ${DATABASE_PASS:=""} -: ${DATABASE_HOST:=""} -: ${DATABASE_NAME:=""} -: ${DATABASE_PORT:=5432} -: ${DATABASE_TYPE:=postgresql} -: ${AUTH_DATABASE_USER:=$DATABASE_USER} -: ${AUTH_DATABASE_PASS:=$DATABASE_PASS} -: ${AUTH_DATABASE_HOST:=$DATABASE_HOST} -: ${AUTH_DATABASE_NAME:=$DATABASE_NAME} -: ${AUTH_DATABASE_PORT:=$DATABASE_PORT} -: ${AUTH_DATABASE_TYPE:=$DATABASE_TYPE} - -SQL_TEMPL="_type_://_auth__host__port_/_db_" -if [ "${DATABASE_HOST}" ] -then - [ "$DATABASE_USER" -a "$DATABASE_PASS" ] && AUTH="${DATABASE_USER}:${DATABASE_PASS}@" - URL=${SQL_TEMPL/_auth_/${AUTH-}} - URL=${URL/_port_/:${DATABASE_PORT}} - URL=${URL/_host_/${DATABASE_HOST}} - URL=${URL/_type_/${DATABASE_TYPE}} - URL=${URL/_db_/${DATABASE_NAME}} - P_URL=$URL -fi - -if [ "${AUTH_DATABASE_HOST}" ] -then - [ "$AUTH_DATABASE_USER" -a "$AUTH_DATABASE_PASS" ] && AUTH="${AUTH_DATABASE_USER}:${AUTH_DATABASE_PASS}@" - - URL=${SQL_TEMPL/_auth_/${AUTH-}} - URL=${URL/_port_/:${AUTH_DATABASE_PORT}} - URL=${URL/_host_/${AUTH_DATABASE_HOST}} - URL=${URL/_type_/${AUTH_DATABASE_TYPE}} - URL=${URL/_db_/${AUTH_DATABASE_NAME}} - A_URL=$URL -fi - -tmpConfig='/tmp/opentaxii.yml' -cat > "$tmpConfig" <<-EOCONFIG ---- - -domain: "${OPENTAXII_DOMAIN}" - -taxii1: - persistence_api: - class: opentaxii.persistence.sqldb.SQLDatabaseAPI - parameters: - db_connection: ${P_URL} - create_tables: yes - -taxii2: - persistence_api: - class: opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI - parameters: - db_connection: ${P_URL} - create_tables: yes - title: "TAXII2 Server" - public_discovery: true - max_content_length: 209715200 - -auth_api: - class: opentaxii.auth.sqldb.SQLDatabaseAPI - parameters: - db_connection: ${A_URL} - create_tables: yes - secret: ${DOCKER_OPENTAXII_AUTH_SECRET} - -logging: - opentaxii: info - root: info - -hooks: -EOCONFIG -cp -f $tmpConfig /opentaxii.yml - -# Lets see if there is an override -[ -f /input/opentaxii.yml ] && cp -f /input/opentaxii.yml /opentaxii.yml - -# Wait for port to become available in case of SQL -[ "$DATABASE_HOST" ] && wait_for_port $DATABASE_HOST ${DATABASE_PORT-5432} -[ "$AUTH_DATABASE_HOST" ] && wait_for_port $AUTH_DATABASE_HOST ${AUTH_DATABASE_PORT-5432} - -# Sync data configuration if it is present -[ -f /input/data-configuration.yml ] && opentaxii-sync-data -f /input/data-configuration.yml - -exec "$@" +#!/bin/bash + + +trap "echo 'Shutting down OpenTAXII' && exit" EXIT SIGINT SIGTERM + +function wait_for_port() { + (echo >/dev/tcp/$1/$2) &>/dev/null + while [ $? -ne 0 ]; + do + echo "Waiting for $1:$2 to become available" + sleep 1 + (echo >/dev/tcp/$1/$2) &>/dev/null + done + echo "$1:$2 is now open" +} + +# Default OpenTAXII Configuration +: ${OPENTAXII_DOMAIN:=localhost:9000} +: ${DOCKER_OPENTAXII_AUTH_SECRET:=notVerySecret} +: ${OPENTAXII_CONFIG:=/opentaxii.yml} +# make sure this env var is available to the main process and subprocesses! +export OPENTAXII_CONFIG + +# Database Defaults +P_URL="sqlite:////data/data.db" +A_URL="sqlite:////data/auth.db" +: ${DATABASE_USER:=""} +: ${DATABASE_PASS:=""} +: ${DATABASE_HOST:=""} +: ${DATABASE_NAME:=""} +: ${DATABASE_PORT:=5432} +: ${DATABASE_TYPE:=postgresql} +: ${AUTH_DATABASE_USER:=$DATABASE_USER} +: ${AUTH_DATABASE_PASS:=$DATABASE_PASS} +: ${AUTH_DATABASE_HOST:=$DATABASE_HOST} +: ${AUTH_DATABASE_NAME:=$DATABASE_NAME} +: ${AUTH_DATABASE_PORT:=$DATABASE_PORT} +: ${AUTH_DATABASE_TYPE:=$DATABASE_TYPE} + +SQL_TEMPL="_type_://_auth__host__port_/_db_" +if [ "${DATABASE_HOST}" ] +then + [ "$DATABASE_USER" -a "$DATABASE_PASS" ] && AUTH="${DATABASE_USER}:${DATABASE_PASS}@" + URL=${SQL_TEMPL/_auth_/${AUTH-}} + URL=${URL/_port_/:${DATABASE_PORT}} + URL=${URL/_host_/${DATABASE_HOST}} + URL=${URL/_type_/${DATABASE_TYPE}} + URL=${URL/_db_/${DATABASE_NAME}} + P_URL=$URL +fi + +if [ "${AUTH_DATABASE_HOST}" ] +then + [ "$AUTH_DATABASE_USER" -a "$AUTH_DATABASE_PASS" ] && AUTH="${AUTH_DATABASE_USER}:${AUTH_DATABASE_PASS}@" + + URL=${SQL_TEMPL/_auth_/${AUTH-}} + URL=${URL/_port_/:${AUTH_DATABASE_PORT}} + URL=${URL/_host_/${AUTH_DATABASE_HOST}} + URL=${URL/_type_/${AUTH_DATABASE_TYPE}} + URL=${URL/_db_/${AUTH_DATABASE_NAME}} + A_URL=$URL +fi + +tmpConfig='/tmp/opentaxii.yml' +cat > "$tmpConfig" <<-EOCONFIG +--- + +domain: "${OPENTAXII_DOMAIN}" + +taxii1: + persistence_api: + class: opentaxii.persistence.sqldb.SQLDatabaseAPI + parameters: + db_connection: ${P_URL} + create_tables: yes + +taxii2: + persistence_api: + class: opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI + parameters: + db_connection: ${P_URL} + create_tables: yes + title: "TAXII2 Server" + public_discovery: true + max_content_length: 209715200 + +auth_api: + class: opentaxii.auth.sqldb.SQLDatabaseAPI + parameters: + db_connection: ${A_URL} + create_tables: yes + secret: ${DOCKER_OPENTAXII_AUTH_SECRET} + +logging: + opentaxii: info + root: info + +hooks: +EOCONFIG +cp -f $tmpConfig /opentaxii.yml + +# Lets see if there is an override +[ -f /input/opentaxii.yml ] && cp -f /input/opentaxii.yml /opentaxii.yml + +# Wait for port to become available in case of SQL +[ "$DATABASE_HOST" ] && wait_for_port $DATABASE_HOST ${DATABASE_PORT-5432} +[ "$AUTH_DATABASE_HOST" ] && wait_for_port $AUTH_DATABASE_HOST ${AUTH_DATABASE_PORT-5432} + +# Sync data configuration if it is present +[ -f /input/data-configuration.yml ] && opentaxii-sync-data -f /input/data-configuration.yml + +exec "$@" diff --git a/docs/Makefile b/docs/Makefile index 63c7b120..73adf40f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,177 +1,177 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/opentaxii.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/opentaxii.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/opentaxii" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/opentaxii" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/opentaxii.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/opentaxii.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/opentaxii" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/opentaxii" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 385d4f2c..5df92f1e 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,9 +1,9 @@ -

About OpenTAXII

-

Robust and customizable TAXII Services implementation.

-

Useful Links

- +

About OpenTAXII

+

Robust and customizable TAXII Services implementation.

+

Useful Links

+ diff --git a/docs/conf.py b/docs/conf.py index acec863b..8f8f1ee0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,223 +1,223 @@ -# -*- coding: utf-8 -*- -# -# opentaxii documentation build configuration file, created by -# sphinx-quickstart on Tue Mar 10 13:37:13 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import datetime -import os -import sys - -import sphinx_rtd_theme - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.extlinks', - 'sphinx_rtd_theme', - ] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'OpenTAXII' -copyright = u'2016-%s, EclecticIQ' % datetime.date.today().year - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -exec(open(os.path.join(os.path.dirname(__file__), '../opentaxii/_version.py')).read()) -version = __version__ - -# The full version, including alpha/beta/rc tags. -release = __version__ - - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -autodoc_default_flags = ['members', 'undoc-members'] -autodoc_member_order = 'bysource' - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'sphinx_rtd_theme' - -# -- sphinx-rtd-theme options ----------- - -html_theme_options = { - # 'analytics_id': 'G-XXXXXXXXXX', # Provided by Google in your dashboard - # 'analytics_anonymize_ip': False, - 'logo_only': False, - 'display_version': True, - # 'canonical_url': 'https://opentaxii.readthedocs.io/', # important when we host on multiple urls - 'prev_next_buttons_location': 'bottom', - 'style_external_links': True, - 'vcs_pageview_mode': '', - #'style_nav_header_background': 'white', - # Toc options - 'collapse_navigation': True, - 'sticky_navigation': True, - 'navigation_depth': 4, - 'includehidden': True, - 'titles_only': False, -} - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'opentaxii.tex', u'opentaxii Documentation', - u'EclecticIQ', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'opentaxii', u'opentaxii Documentation', - [u'EclecticIQ'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'opentaxii', u'opentaxii Documentation', - u'EclecticIQ', 'opentaxii', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -# use "master" branch if version is alpha else use release value -github_release = 'master' if 'a' in __version__ else __version__ - -github_release_prefix = ( - 'https://github.com/eclecticiq/OpenTAXII/blob/{version}/%s' - .format(version=github_release)) - -extlinks = {'github-file': (github_release_prefix, '')} +# -*- coding: utf-8 -*- +# +# opentaxii documentation build configuration file, created by +# sphinx-quickstart on Tue Mar 10 13:37:13 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import datetime +import os +import sys + +import sphinx_rtd_theme + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.extlinks', + 'sphinx_rtd_theme', + ] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'OpenTAXII' +copyright = u'2016-%s, EclecticIQ' % datetime.date.today().year + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +exec(open(os.path.join(os.path.dirname(__file__), '../opentaxii/_version.py')).read()) +version = __version__ + +# The full version, including alpha/beta/rc tags. +release = __version__ + + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +autodoc_default_flags = ['members', 'undoc-members'] +autodoc_member_order = 'bysource' + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'sphinx_rtd_theme' + +# -- sphinx-rtd-theme options ----------- + +html_theme_options = { + # 'analytics_id': 'G-XXXXXXXXXX', # Provided by Google in your dashboard + # 'analytics_anonymize_ip': False, + 'logo_only': False, + 'display_version': True, + # 'canonical_url': 'https://opentaxii.readthedocs.io/', # important when we host on multiple urls + 'prev_next_buttons_location': 'bottom', + 'style_external_links': True, + 'vcs_pageview_mode': '', + #'style_nav_header_background': 'white', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': True, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False, +} + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'opentaxii.tex', u'opentaxii Documentation', + u'EclecticIQ', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'opentaxii', u'opentaxii Documentation', + [u'EclecticIQ'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'opentaxii', u'opentaxii Documentation', + u'EclecticIQ', 'opentaxii', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +# use "master" branch if version is alpha else use release value +github_release = 'master' if 'a' in __version__ else __version__ + +github_release_prefix = ( + 'https://github.com/eclecticiq/OpenTAXII/blob/{version}/%s' + .format(version=github_release)) + +extlinks = {'github-file': (github_release_prefix, '')} diff --git a/docs/contribute/developer.rst b/docs/contribute/developer.rst index 89a31e0b..46c076c9 100644 --- a/docs/contribute/developer.rst +++ b/docs/contribute/developer.rst @@ -1,60 +1,60 @@ -=========================== -Contributing and developing -=========================== - -.. _OpenTAXII project page: https://github.com/EclecticIQ/OpenTAXII - - -Reporting issues -================ - -OpenTAXII uses Github's issue tracker. See the `OpenTAXII project page`_ on Github. - - -Obtaining the source code -========================= - -The OpenTAXII source code can be found on Github. See the `OpenTAXII project page`_. - -Layout -====== - -OpenTAXII repository has the following layout: - -* ``docker/`` - Docker configuration files (:doc:`OpenTAXII Docker documentation <../installation/docker>`); -* ``docs/`` - used to build the `documentation `_; -* ``examples/`` - configuration and code examples; -* ``opentaxii/`` - OpenTAXII source; -* ``tests/`` - opentaxii tests. - -Database schema -=============== -.. image:: ../db_schema_diagram.png - :width: 1000 - -Compiling from source -===================== - -After cloning the Github repo, just run this:: - - (venv) $ python setup.py install - - -Running the tests -================= - -Almost all OpenTAXII code is covered by the unit tests. OpenTAXII uses `py.test `_ and -`tox `_ for running tests. Type ``tox -r`` or ``py.test`` to run the unit tests. - - -Generating the documentation -============================ - -The documentation is written in `ReStructuredText `_ (reST) format and processed -using `Sphinx `_. To build HTML documentation, go to ``docs`` and type ``make html``. - -.. rubric:: Next steps - -Continue to :doc:`License <../license>`. - -.. vim: set spell spelllang=en: +=========================== +Contributing and developing +=========================== + +.. _OpenTAXII project page: https://github.com/EclecticIQ/OpenTAXII + + +Reporting issues +================ + +OpenTAXII uses Github's issue tracker. See the `OpenTAXII project page`_ on Github. + + +Obtaining the source code +========================= + +The OpenTAXII source code can be found on Github. See the `OpenTAXII project page`_. + +Layout +====== + +OpenTAXII repository has the following layout: + +* ``docker/`` - Docker configuration files (:doc:`OpenTAXII Docker documentation <../installation/docker>`); +* ``docs/`` - used to build the `documentation `_; +* ``examples/`` - configuration and code examples; +* ``opentaxii/`` - OpenTAXII source; +* ``tests/`` - opentaxii tests. + +Database schema +=============== +.. image:: ../db_schema_diagram.png + :width: 1000 + +Compiling from source +===================== + +After cloning the Github repo, just run this:: + + (venv) $ python setup.py install + + +Running the tests +================= + +Almost all OpenTAXII code is covered by the unit tests. OpenTAXII uses `py.test `_ and +`tox `_ for running tests. Type ``tox -r`` or ``py.test`` to run the unit tests. + + +Generating the documentation +============================ + +The documentation is written in `ReStructuredText `_ (reST) format and processed +using `Sphinx `_. To build HTML documentation, go to ``docs`` and type ``make html``. + +.. rubric:: Next steps + +Continue to :doc:`License <../license>`. + +.. vim: set spell spelllang=en: diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst index 412de65d..a67b8e3b 100644 --- a/docs/contribute/index.rst +++ b/docs/contribute/index.rst @@ -1,11 +1,11 @@ -====================== -Contribute and develop -====================== - -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - developer +====================== +Contribute and develop +====================== + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + developer running \ No newline at end of file diff --git a/docs/contribute/running.rst b/docs/contribute/running.rst index 1f3f8cbb..36b0d5f4 100644 --- a/docs/contribute/running.rst +++ b/docs/contribute/running.rst @@ -1,16 +1,16 @@ -============================= -Running Development OpenTAXII -============================= - -.. highlight:: sh - -Here we describe how to run OpenTAXII in a *development*. Development mode -activates `Flask `_ debug mode, simplifies log messages -(for humans) and runs the server in one thread. While in production mode there -are no debug messages, proper json log messages and multithreaded if configured. - -To run the server in development mode use the CLI command shipped with OpenTAXII package:: - - (venv) $ opentaxii-run-dev - +============================= +Running Development OpenTAXII +============================= + +.. highlight:: sh + +Here we describe how to run OpenTAXII in a *development*. Development mode +activates `Flask `_ debug mode, simplifies log messages +(for humans) and runs the server in one thread. While in production mode there +are no debug messages, proper json log messages and multithreaded if configured. + +To run the server in development mode use the CLI command shipped with OpenTAXII package:: + + (venv) $ opentaxii-run-dev + This will start OpenTAXII in a development mode and bind it to ``localhost:9000``. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 73922d1d..071114e7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,46 +1,46 @@ -========= -OpenTAXII -========= - -Current release is v\ |version|. (:doc:`Changelog `) - -OpenTAXII is a robust Python implementation of TAXII Services that delivers rich feature set and friendly pythonic API. - -TAXII (Trusted Automated eXchange of Indicator Information) is a collection of specifications defining a set of services and message exchanges used for sharing cyber threat intelligence information between parties. Check `TAXII homepage `_ for more information. - -**Rich feature set** - -OpenTAXII implements all TAXII services according to TAXII specification (version 1.0 and 1.1). On top of these services, it also delivers additional functionality such as; - -- customizable APIs, -- authentication, -- flexible logging. - - -**Designed to be extendable** - -OpenTAXII architecture follows TAXII specification in its idea of TTA (TAXII transport agent) and TMH (TAXII message handler) components, separating implementations of: - - - Transport layer (`Flask `_ based web app) - - TAXII logic layer (TAXII server/services/message handlers) - - Persistence layer (extendable Persistence API) - - Authentication layer (extendable Authentication API) - - -.. rubric:: Documentation contents - -.. toctree:: - :maxdepth: 1 - - installation/index - usage/index - contribute/index - license - references/index - -.. rubric:: External links - -* `Online documentation `_ (Read the docs) -* `Project page `_ with source code and issue tracker (Github) -* `Python Package Index (PyPI) page `_ with released tarballs -* `EclecticIQ `_ +========= +OpenTAXII +========= + +Current release is v\ |version|. (:doc:`Changelog `) + +OpenTAXII is a robust Python implementation of TAXII Services that delivers rich feature set and friendly pythonic API. + +TAXII (Trusted Automated eXchange of Indicator Information) is a collection of specifications defining a set of services and message exchanges used for sharing cyber threat intelligence information between parties. Check `TAXII homepage `_ for more information. + +**Rich feature set** + +OpenTAXII implements all TAXII services according to TAXII specification (version 1.0 and 1.1). On top of these services, it also delivers additional functionality such as; + +- customizable APIs, +- authentication, +- flexible logging. + + +**Designed to be extendable** + +OpenTAXII architecture follows TAXII specification in its idea of TTA (TAXII transport agent) and TMH (TAXII message handler) components, separating implementations of: + + - Transport layer (`Flask `_ based web app) + - TAXII logic layer (TAXII server/services/message handlers) + - Persistence layer (extendable Persistence API) + - Authentication layer (extendable Authentication API) + + +.. rubric:: Documentation contents + +.. toctree:: + :maxdepth: 1 + + installation/index + usage/index + contribute/index + license + references/index + +.. rubric:: External links + +* `Online documentation `_ (Read the docs) +* `Project page `_ with source code and issue tracker (Github) +* `Python Package Index (PyPI) page `_ with released tarballs +* `EclecticIQ `_ diff --git a/docs/installation/configuration.rst b/docs/installation/configuration.rst index 9ede523d..2ca10165 100644 --- a/docs/installation/configuration.rst +++ b/docs/installation/configuration.rst @@ -1,193 +1,193 @@ -============= -Configuration -============= - -.. highlight:: sh - -OpenTAXII can be configured using YAML configuration files. - - -Default configuration -===================== - -OpenTAXII has default :github-file:`configuration file ` built in. - -By default: - - Provided Persistence API and Authentication API implementations are for SQL DB backends and use sqlite3 backend. - - The sqlite3 database files will be automatically created in ``/tmp/data.db`` and ``/tmp/auth.db``. - - Services, collections and accounts are not configured automatically. This requires a manual operation (see below). - - No signal hooks are attached. - - There is only taxii1 support, taxii2 is disabled - - -Default configuration file looks like this: - -.. code-block:: yaml - - --- - domain: "localhost:9000" - - support_basic_auth: yes - return_server_error_details: no - - auth_api: - class: opentaxii.auth.sqldb.SQLDatabaseAPI - parameters: - db_connection: sqlite:////tmp/auth.db - create_tables: yes - secret: SECRET-STRING-NEEDS-TO-BE-CHANGED - token_ttl_secs: 3600 - - taxii1: - save_raw_inbox_messages: yes - xml_parser_supports_huge_tree: yes - count_blocks_in_poll_responses: no - unauthorized_status: UNAUTHORIZED - hooks: - persistence_api: - class: opentaxii.persistence.sqldb.SQLDatabaseAPI - parameters: - db_connection: sqlite:////tmp/data.db - create_tables: yes - - taxii2: - - logging: - opentaxii: info - root: info - -.. note:: - OpenTAXII uses a SQLite Database by default wich is intended only when running OpenTAXII in a development environment. Please use different SQL DB backend for running in a production environment. - -An example of custom configuration that allows OpenTAXII to connect to production-ready PostgreSQL database: - -.. code-block:: yaml - - --- - auth_api: - class: opentaxii.auth.sqldb.SQLDatabaseAPI - parameters: - db_connection: postgresql://username:P@ssword@db.example.com:5432/databasename - create_tables: yes - secret: SECRET-STRING-NEEDS-TO-BE-CHANGED - - taxii1: - persistence_api: - class: opentaxii.persistence.sqldb.SQLDatabaseAPI - parameters: - db_connection: postgresql://username:P@ssword@db.example.com:5432/databasename - create_tables: yes - - -Properties -========== - - - ``domain`` — domain that will be used in service URLs in TAXII responses. - - ``support_basic_auth`` — enable/disable Basic Authentication support. If disabled, only JWT authentication is allowed. - - ``return_server_error_details`` — allow OpenTAXII to return error details in error-status TAXII response. - - ``auth_api`` — configuration properties for Authentication API implementation. - - - ``class`` — the full import name of the class to use - - ``parameters`` — the parameters used to instantiate the class - - - ``db_connection`` — the database connetion string - - ``create_tables`` — boolean, if true, create tables on startup - - ``secret`` — the secret with which the generated tokens are encoded - - ``token_ttl_secs`` — time that generated tokens are valid - - - ``taxii1`` — taxii1-specific settings - - - ``save_raw_inbox_message`` — enable/disable storing of raw TAXII Inbox messages via Persistence API's ``create_inbox_message`` method. This is useful for bookkeeping but significantly increases storage requirements. - - ``xml_parser_supports_huge_tree`` — enable/disable security restrictions in `lxml `_ library to allow support for very deep trees and very long text content. If this is disabled, OpenTAXII will not be able to parse TAXII messages with content blocks larger than roughly 10MB. - - ``count_blocks_in_poll_responses`` — enable/disable total count in TAXII Poll responses. It is disabled by default since ``count`` operation might be `very slow `_ in some SQL DBs. - - ``unauthorized_status`` — TAXII status type for authorization error. "UNAUTHORIZED" by default. see `libtaxii.constants.ST_TYPES_11 `_ for the list of available values. - - ``hooks`` - custom python module with signal subscriptions to import. See :ref:`documentation on custom signals` and :github-file:`an example `. - - ``persistence_api`` — configuration properties for Persistence API implementation. - - - ``class`` — the full import name of the class to use - - ``parameters`` — the parameters used to instantiate the class - - - ``db_connection`` — the database connetion string - - ``create_tables`` — boolean, if true, create tables on startup - - - ``taxii2`` — taxii2-specific settings - - - ``persistence_api`` — configuration properties for Persistence API implementation. - - - ``class`` — the full import name of the class to use - - ``parameters`` — the parameters used to instantiate the class - - - ``db_connection`` — the database connetion string - - ``create_tables`` — boolean, if true, create tables on startup - - - ``max_content_length`` — the maximum size of the request body in bytes that the server can support. Required field - - ``allow_custom_properties`` — boolean, if true, allow custom stix2 properties when posting objects (default: true) - - ``public_discovery`` - boolean, if true, do not require authentication for discovery of api roots (default: false) - - ``title`` - title of the server, returned as part of the discovery of api roots. Required field - - ``contact`` - contact for the server, returned as part of the discovery of api roots - - ``description`` - description of the server, returned as part of the discovery of api roots - - - ``logging`` — logging configuration. - - -.. _custom-configuration: - -Custom configuration -==================== - -To pass custom configuration to OpenTAXII server, specify an absolute path to your -configuration file in environment variable ``OPENTAXII_CONFIG``.:: - - $ export OPENTAXII_CONFIG=/path/to/configuration/file.yml - - -This configuration file may fully or partially override default settings. - -Example custom configuration: - -.. _configuration-example: - -.. code-block:: yaml - - --- - domain: taxii.mydomain.com - support_basic_auth: no - - auth_api: - class: opentaxii.auth.sqldb.SQLDatabaseAPI - parameters: - db_connection: postgresql://scott:tiger@localhost:5432/mydatabase - create_tables: yes - secret: aueHenjitweridUcviapEbsJocdiDrelHonsyorl - - taxii1: - xml_parser_supports_huge_tree: no - hooks: mypackage.opentaxii.custom_hooks - persistence_api: - class: mypackage.opentaxii.CustomPersistenceAPI - parameters: - rest_api: http://rest.mydomain.com/api - -The built-in implementation of the Persistence and Authentication APIs support SQLite, PostgreSQL, MySQL, and other SQL databases. Check `SQLAlchemy website `_ to get the full list. - -OpenTAXII CLI tools are implemented to call corresponding API methods and support any API implementation. - -Environment variables configuration -=================================== - -You can (re)define any configuration option with environment variables. Start variable name with ``OPENTAXII_``. Use ``__`` to separate parts of the config path. Use uppercase. Specify values in YAML syntax. - -.. code-block:: bash - - export OPENTAXII_DOMAIN='taxii.mydomain.com' - export OPENTAXII_SUPPORT_BASIC_AUTH='no' - export OPENTAXII__PERSISTENCE_API__CLASS='mypackage.opentaxii.CustomPersistenceAPI' - export OPENTAXII__AUTH_API__PARAMETERS__SECRET='aueHenjitweridUcviapEbsJocdiDrelHonsyorl' - -Environment variables applied after all other configs and can be used to redefine any option. - -.. rubric:: Next steps - -Continue to the :doc:`Running OpenTAXII ` page to see how to run OpenTAXII. - -.. vim: set spell spelllang=en: +============= +Configuration +============= + +.. highlight:: sh + +OpenTAXII can be configured using YAML configuration files. + + +Default configuration +===================== + +OpenTAXII has default :github-file:`configuration file ` built in. + +By default: + - Provided Persistence API and Authentication API implementations are for SQL DB backends and use sqlite3 backend. + - The sqlite3 database files will be automatically created in ``/tmp/data.db`` and ``/tmp/auth.db``. + - Services, collections and accounts are not configured automatically. This requires a manual operation (see below). + - No signal hooks are attached. + - There is only taxii1 support, taxii2 is disabled + + +Default configuration file looks like this: + +.. code-block:: yaml + + --- + domain: "localhost:9000" + + support_basic_auth: yes + return_server_error_details: no + + auth_api: + class: opentaxii.auth.sqldb.SQLDatabaseAPI + parameters: + db_connection: sqlite:////tmp/auth.db + create_tables: yes + secret: SECRET-STRING-NEEDS-TO-BE-CHANGED + token_ttl_secs: 3600 + + taxii1: + save_raw_inbox_messages: yes + xml_parser_supports_huge_tree: yes + count_blocks_in_poll_responses: no + unauthorized_status: UNAUTHORIZED + hooks: + persistence_api: + class: opentaxii.persistence.sqldb.SQLDatabaseAPI + parameters: + db_connection: sqlite:////tmp/data.db + create_tables: yes + + taxii2: + + logging: + opentaxii: info + root: info + +.. note:: + OpenTAXII uses a SQLite Database by default wich is intended only when running OpenTAXII in a development environment. Please use different SQL DB backend for running in a production environment. + +An example of custom configuration that allows OpenTAXII to connect to production-ready PostgreSQL database: + +.. code-block:: yaml + + --- + auth_api: + class: opentaxii.auth.sqldb.SQLDatabaseAPI + parameters: + db_connection: postgresql://username:P@ssword@db.example.com:5432/databasename + create_tables: yes + secret: SECRET-STRING-NEEDS-TO-BE-CHANGED + + taxii1: + persistence_api: + class: opentaxii.persistence.sqldb.SQLDatabaseAPI + parameters: + db_connection: postgresql://username:P@ssword@db.example.com:5432/databasename + create_tables: yes + + +Properties +========== + + - ``domain`` — domain that will be used in service URLs in TAXII responses. + - ``support_basic_auth`` — enable/disable Basic Authentication support. If disabled, only JWT authentication is allowed. + - ``return_server_error_details`` — allow OpenTAXII to return error details in error-status TAXII response. + - ``auth_api`` — configuration properties for Authentication API implementation. + + - ``class`` — the full import name of the class to use + - ``parameters`` — the parameters used to instantiate the class + + - ``db_connection`` — the database connetion string + - ``create_tables`` — boolean, if true, create tables on startup + - ``secret`` — the secret with which the generated tokens are encoded + - ``token_ttl_secs`` — time that generated tokens are valid + + - ``taxii1`` — taxii1-specific settings + + - ``save_raw_inbox_message`` — enable/disable storing of raw TAXII Inbox messages via Persistence API's ``create_inbox_message`` method. This is useful for bookkeeping but significantly increases storage requirements. + - ``xml_parser_supports_huge_tree`` — enable/disable security restrictions in `lxml `_ library to allow support for very deep trees and very long text content. If this is disabled, OpenTAXII will not be able to parse TAXII messages with content blocks larger than roughly 10MB. + - ``count_blocks_in_poll_responses`` — enable/disable total count in TAXII Poll responses. It is disabled by default since ``count`` operation might be `very slow `_ in some SQL DBs. + - ``unauthorized_status`` — TAXII status type for authorization error. "UNAUTHORIZED" by default. see `libtaxii.constants.ST_TYPES_11 `_ for the list of available values. + - ``hooks`` - custom python module with signal subscriptions to import. See :ref:`documentation on custom signals` and :github-file:`an example `. + - ``persistence_api`` — configuration properties for Persistence API implementation. + + - ``class`` — the full import name of the class to use + - ``parameters`` — the parameters used to instantiate the class + + - ``db_connection`` — the database connetion string + - ``create_tables`` — boolean, if true, create tables on startup + + - ``taxii2`` — taxii2-specific settings + + - ``persistence_api`` — configuration properties for Persistence API implementation. + + - ``class`` — the full import name of the class to use + - ``parameters`` — the parameters used to instantiate the class + + - ``db_connection`` — the database connetion string + - ``create_tables`` — boolean, if true, create tables on startup + + - ``max_content_length`` — the maximum size of the request body in bytes that the server can support. Required field + - ``allow_custom_properties`` — boolean, if true, allow custom stix2 properties when posting objects (default: true) + - ``public_discovery`` - boolean, if true, do not require authentication for discovery of api roots (default: false) + - ``title`` - title of the server, returned as part of the discovery of api roots. Required field + - ``contact`` - contact for the server, returned as part of the discovery of api roots + - ``description`` - description of the server, returned as part of the discovery of api roots + + - ``logging`` — logging configuration. + + +.. _custom-configuration: + +Custom configuration +==================== + +To pass custom configuration to OpenTAXII server, specify an absolute path to your +configuration file in environment variable ``OPENTAXII_CONFIG``.:: + + $ export OPENTAXII_CONFIG=/path/to/configuration/file.yml + + +This configuration file may fully or partially override default settings. + +Example custom configuration: + +.. _configuration-example: + +.. code-block:: yaml + + --- + domain: taxii.mydomain.com + support_basic_auth: no + + auth_api: + class: opentaxii.auth.sqldb.SQLDatabaseAPI + parameters: + db_connection: postgresql://scott:tiger@localhost:5432/mydatabase + create_tables: yes + secret: aueHenjitweridUcviapEbsJocdiDrelHonsyorl + + taxii1: + xml_parser_supports_huge_tree: no + hooks: mypackage.opentaxii.custom_hooks + persistence_api: + class: mypackage.opentaxii.CustomPersistenceAPI + parameters: + rest_api: http://rest.mydomain.com/api + +The built-in implementation of the Persistence and Authentication APIs support SQLite, PostgreSQL, MySQL, and other SQL databases. Check `SQLAlchemy website `_ to get the full list. + +OpenTAXII CLI tools are implemented to call corresponding API methods and support any API implementation. + +Environment variables configuration +=================================== + +You can (re)define any configuration option with environment variables. Start variable name with ``OPENTAXII_``. Use ``__`` to separate parts of the config path. Use uppercase. Specify values in YAML syntax. + +.. code-block:: bash + + export OPENTAXII_DOMAIN='taxii.mydomain.com' + export OPENTAXII_SUPPORT_BASIC_AUTH='no' + export OPENTAXII__PERSISTENCE_API__CLASS='mypackage.opentaxii.CustomPersistenceAPI' + export OPENTAXII__AUTH_API__PARAMETERS__SECRET='aueHenjitweridUcviapEbsJocdiDrelHonsyorl' + +Environment variables applied after all other configs and can be used to redefine any option. + +.. rubric:: Next steps + +Continue to the :doc:`Running OpenTAXII ` page to see how to run OpenTAXII. + +.. vim: set spell spelllang=en: diff --git a/docs/installation/docker.rst b/docs/installation/docker.rst index 5b1112c3..6f709bb4 100644 --- a/docs/installation/docker.rst +++ b/docs/installation/docker.rst @@ -1,168 +1,168 @@ -Run OpenTAXII on Docker -======================= - -OpenTAXII can also be run using docker. This guide assumes that you have access to a local or remote docker server, and won't go into the setup of docker. - -To get a default (development) instance using docker: - -.. code-block:: shell - - $ docker run -d -p 9000:9000 eclecticiq/opentaxii - -To have the instance preloaded with example data, see :ref:`volumes` - -.. note:: - - OpenTAXII is now accessible through port 9000, with data stored locally in a SQLite database. - - -Configuration -------------- - -Configuration is done through environment variables. - -Common configuration parameters are: - -``OPENTAXII_DOMAIN`` : (optional) - This specifies under which domain the OpenTAXII server is available, default: ``localhost:9000`` - --------------------- - -Setting up authentication is done with the following two variables: - -``OPENTAXII_SECRET`` : (optional) - This is the secret with which the generated token is encoded. - ---------------------- - -If you want to use a PostgreSQL database, instead of the included SQLite database, you can use the following environment variables for configuration: - -``DATABASE_HOST`` : (required) - This is the database host to connect to - -``DATABASE_PORT`` : (optional) - Default is ``5432`` - -``DATABASE_NAME`` : (optional) - The database to use, by default uses ``postgres`` - -If you would like to use a different Database for authentication, you can also set the following variables (any variable not set, will use it's regular DB counterpart): - -``AUTH_DATABASE_HOST`` : (required) - This is the database host to connect to - -``AUTH_DATABASE_PORT`` : (optional) - Default is ``5432`` - -``AUTH_DATABASE_USER`` : (optional) - If not set, the default ``postgres`` is used. - -``AUTH_DATABASE_PASS`` : (optional) - If not set, the database can be accessed by all containers on the same host! - -``AUTH_DATABASE_NAME`` : (optional) - The database to use, by default uses ``postgres`` - - -.. _volumes: - -Volumes -------- - -This docker container exposes two volumes, which can be attached to a running instance: - -``/data`` - This volume will contain the SQLite databases used by the default instance. - -``/input/data-configuration.yml`` - If you want to pre-load the running instance with - services/collections/accounts, An example is provided with - :github-file:`data-configuration.yml `. - -``/input/opentaxii.yml`` - To overwrite the default configuration used by the docker instance. - -.. code-block:: shell - - $ pwd - /some/path/examples - $ ls /some/path/examples - data-configuration.yml - $ docker run -d -p 9000:9000 -v /some/path/examples:/input eclecticiq/opentaxii - -.. note:: - Make sure your naming is correct. It will only execute actions when files ``data-configuration.yml`` or ``opentaxii.yml`` are present. - -Extending ---------- - -If you need custom configuration, and installation of extra/custom code, it is better to extend the OpenTAXII docker image. For example, adding mysql (instead of PostgreSQL), and adding custom code, which is configured in a custom opentaxii.yml. The Dockerfile used will then look something like: - -.. code-block:: docker - - FROM eclecticiq/opentaxii:latest - MAINTAINER EclecticIQ - - RUN pip install mysql-python \ - && pip install custom-package - - COPY opentaxii.yml /input/opentaxii.yml - -And building the image is then done using: - -.. code-block:: shell - - $ docker build -t eclecticiq/opentaxii-mysql . - - -TAXII 2 instance with Compose ------------------------------ - -Checkout the configuration at: :github-file:`examples/docker-compose-taxii2.yml `. - -.. code-block:: shell - - # Start - docker compose -f examples/docker-compose-taxii2.yml up - - # To add dummy data, run this while the compose project is running - docker exec -i examples-opentaxii-1 bash < examples/taxii2/data-setup.sh - -Full Example with Compose -------------------------- - -To see a full example of running OpenTAXII against a "real" database, using the `docker-compose `_ tool, checkout the configuration at: :github-file:`examples/docker-compose.yml `. - -.. code-block:: yaml - - db: - image: postgres:9.4 - environment: - POSTGRES_USER: user - POSTGRES_PASSWORD: password - POSTGRES_DB: opentaxii - - opentaxii: - image: eclecticiq/opentaxii - environment: - DOCKER_OPENTAXII_AUTH_SECRET: secret - OPENTAXII_DOMAIN: 192.168.59.103:9000 - DATABASE_HOST: db - DATABASE_NAME: opentaxii - DATABASE_USER: user - DATABASE_PASS: password - volumes: - - ./:/input:ro - ports: - - 9000:9000 - links: - - db:db - -This configuration starts two containers: ``opentaxii`` and ``db``, and creates the given services/collections/accounts. - - -.. rubric:: Next steps - -Continue to :doc:`Configuration ` page to learn how to configure OpenTAXII. - -.. vim: set spell spelllang=en: +Run OpenTAXII on Docker +======================= + +OpenTAXII can also be run using docker. This guide assumes that you have access to a local or remote docker server, and won't go into the setup of docker. + +To get a default (development) instance using docker: + +.. code-block:: shell + + $ docker run -d -p 9000:9000 eclecticiq/opentaxii + +To have the instance preloaded with example data, see :ref:`volumes` + +.. note:: + + OpenTAXII is now accessible through port 9000, with data stored locally in a SQLite database. + + +Configuration +------------- + +Configuration is done through environment variables. + +Common configuration parameters are: + +``OPENTAXII_DOMAIN`` : (optional) + This specifies under which domain the OpenTAXII server is available, default: ``localhost:9000`` + +-------------------- + +Setting up authentication is done with the following two variables: + +``OPENTAXII_SECRET`` : (optional) + This is the secret with which the generated token is encoded. + +--------------------- + +If you want to use a PostgreSQL database, instead of the included SQLite database, you can use the following environment variables for configuration: + +``DATABASE_HOST`` : (required) + This is the database host to connect to + +``DATABASE_PORT`` : (optional) + Default is ``5432`` + +``DATABASE_NAME`` : (optional) + The database to use, by default uses ``postgres`` + +If you would like to use a different Database for authentication, you can also set the following variables (any variable not set, will use it's regular DB counterpart): + +``AUTH_DATABASE_HOST`` : (required) + This is the database host to connect to + +``AUTH_DATABASE_PORT`` : (optional) + Default is ``5432`` + +``AUTH_DATABASE_USER`` : (optional) + If not set, the default ``postgres`` is used. + +``AUTH_DATABASE_PASS`` : (optional) + If not set, the database can be accessed by all containers on the same host! + +``AUTH_DATABASE_NAME`` : (optional) + The database to use, by default uses ``postgres`` + + +.. _volumes: + +Volumes +------- + +This docker container exposes two volumes, which can be attached to a running instance: + +``/data`` + This volume will contain the SQLite databases used by the default instance. + +``/input/data-configuration.yml`` + If you want to pre-load the running instance with + services/collections/accounts, An example is provided with + :github-file:`data-configuration.yml `. + +``/input/opentaxii.yml`` + To overwrite the default configuration used by the docker instance. + +.. code-block:: shell + + $ pwd + /some/path/examples + $ ls /some/path/examples + data-configuration.yml + $ docker run -d -p 9000:9000 -v /some/path/examples:/input eclecticiq/opentaxii + +.. note:: + Make sure your naming is correct. It will only execute actions when files ``data-configuration.yml`` or ``opentaxii.yml`` are present. + +Extending +--------- + +If you need custom configuration, and installation of extra/custom code, it is better to extend the OpenTAXII docker image. For example, adding mysql (instead of PostgreSQL), and adding custom code, which is configured in a custom opentaxii.yml. The Dockerfile used will then look something like: + +.. code-block:: docker + + FROM eclecticiq/opentaxii:latest + MAINTAINER EclecticIQ + + RUN pip install mysql-python \ + && pip install custom-package + + COPY opentaxii.yml /input/opentaxii.yml + +And building the image is then done using: + +.. code-block:: shell + + $ docker build -t eclecticiq/opentaxii-mysql . + + +TAXII 2 instance with Compose +----------------------------- + +Checkout the configuration at: :github-file:`examples/docker-compose-taxii2.yml `. + +.. code-block:: shell + + # Start + docker compose -f examples/docker-compose-taxii2.yml up + + # To add dummy data, run this while the compose project is running + docker exec -i examples-opentaxii-1 bash < examples/taxii2/data-setup.sh + +Full Example with Compose +------------------------- + +To see a full example of running OpenTAXII against a "real" database, using the `docker-compose `_ tool, checkout the configuration at: :github-file:`examples/docker-compose.yml `. + +.. code-block:: yaml + + db: + image: postgres:9.4 + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: opentaxii + + opentaxii: + image: eclecticiq/opentaxii + environment: + DOCKER_OPENTAXII_AUTH_SECRET: secret + OPENTAXII_DOMAIN: 192.168.59.103:9000 + DATABASE_HOST: db + DATABASE_NAME: opentaxii + DATABASE_USER: user + DATABASE_PASS: password + volumes: + - ./:/input:ro + ports: + - 9000:9000 + links: + - db:db + +This configuration starts two containers: ``opentaxii`` and ``db``, and creates the given services/collections/accounts. + + +.. rubric:: Next steps + +Continue to :doc:`Configuration ` page to learn how to configure OpenTAXII. + +.. vim: set spell spelllang=en: diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 9967c85c..f6364b9a 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -1,13 +1,13 @@ -================= -Install OpenTAXII -================= - -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - installation - configuration - docker - running +================= +Install OpenTAXII +================= + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + installation + configuration + docker + running diff --git a/docs/installation/installation.rst b/docs/installation/installation.rst index 195aa6c8..0482c2d3 100644 --- a/docs/installation/installation.rst +++ b/docs/installation/installation.rst @@ -1,75 +1,75 @@ -============ -Installation -============ - -.. highlight:: sh - -Install Python --------------- - -OpenTAXII works with Python versions >=3.10. You can download Python -`here `_ or install it with your operating system’s -package manager. - -You can verify that Python is installed by typing ``python`` or ``python3`` in your -shell. You should see something like:: - - $ python3 - Python 3.10.1 (main, Dec 11 2021, 17:22:55) [GCC 11.1.0] on linux - Type "help", "copyright", "credits" or "license" for more information. - >>> - -Install OpenTAXII ------------------ -To sandbox the project and protect system-wide python it is recommended to install -OpenTAXII into a -`virtual environment `_ -(*virtualenv*). - -Create a virtual environment named venv:: - - $ virtualenv venv - -Where ``venv`` is a directory to place the new environment - -Activate this environment:: - - $ . venv/bin/activate - (venv) $ - -Now install the latest OpenTAXII release from the `Python -Package Index `_ (PyPI) using ``pip``:: - - (venv) $ pip install opentaxii - -Without the virtual environment it's just:: - - $ pip install opentaxii - -.. note:: - Since OpenTAXII has `libtaxii `_ as a - dependency, the system libraries `libtaxii` will be installed. Check - `libtaxii documentation - `_ for the - details. - -To install OpenTAXII from source files: download tarball, unpack it and install it -manually with:: - - $ python setup.py install - - -Versioning ----------- - -Releases of OpenTAXII are given major.minor.revision version numbers, where major and -minor correspond to the roadmap EclecticIQ has. The revision number is used to indicate -a bug fix only release. - - -.. rubric:: Next steps - -Continue to :doc:`Configuration ` page to learn how to configure -OpenTAXII. - -.. vim: set spell spelllang=en: +============ +Installation +============ + +.. highlight:: sh + +Install Python +-------------- + +OpenTAXII works with Python versions >=3.10. You can download Python +`here `_ or install it with your operating system’s +package manager. + +You can verify that Python is installed by typing ``python`` or ``python3`` in your +shell. You should see something like:: + + $ python3 + Python 3.10.1 (main, Dec 11 2021, 17:22:55) [GCC 11.1.0] on linux + Type "help", "copyright", "credits" or "license" for more information. + >>> + +Install OpenTAXII +----------------- +To sandbox the project and protect system-wide python it is recommended to install +OpenTAXII into a +`virtual environment `_ +(*virtualenv*). + +Create a virtual environment named venv:: + + $ virtualenv venv + +Where ``venv`` is a directory to place the new environment + +Activate this environment:: + + $ . venv/bin/activate + (venv) $ + +Now install the latest OpenTAXII release from the `Python +Package Index `_ (PyPI) using ``pip``:: + + (venv) $ pip install opentaxii + +Without the virtual environment it's just:: + + $ pip install opentaxii + +.. note:: + Since OpenTAXII has `libtaxii `_ as a + dependency, the system libraries `libtaxii` will be installed. Check + `libtaxii documentation + `_ for the + details. + +To install OpenTAXII from source files: download tarball, unpack it and install it +manually with:: + + $ python setup.py install + + +Versioning +---------- + +Releases of OpenTAXII are given major.minor.revision version numbers, where major and +minor correspond to the roadmap EclecticIQ has. The revision number is used to indicate +a bug fix only release. + + +.. rubric:: Next steps + +Continue to :doc:`Configuration ` page to learn how to configure +OpenTAXII. + +.. vim: set spell spelllang=en: diff --git a/docs/installation/running.rst b/docs/installation/running.rst index a6e452a6..08a46b1f 100644 --- a/docs/installation/running.rst +++ b/docs/installation/running.rst @@ -1,60 +1,60 @@ -================= -Running OpenTAXII -================= - -To run OpenTAXII in production it is recommended to use `standalone WSGI -container -`_ -that works with Flask. In this example we use `Gunicorn WSGI HTTP Server -`_. For complete logging configuration we recommend -Gunicorn 19.8 and above. - -.. note:: - Run ``pip install gunicorn`` to install gunicorn. Yes, it's that simple. - -To run OpenTAXII with Gunicorn execute:: - - (venv) $ gunicorn opentaxii.http:app \ - --bind localhost:9000 --config python:opentaxii.http - -Common practice is to wrap gunicorn execution into `supervisord `_, to be able to monitor, start, and stop it easily. - -Example supervisord configuration file: - -.. code-block:: ini - - [program:opentaxii] - - command = - /opt/eclecticiq/opentaxii-venv/bin/gunicorn opentaxii.http:app - --workers 2 - --log-level info - --log-file - - --timeout 300 - --bind localhost:9000 - --config python:opentaxii.http - environment = - OPENTAXII_CONFIG="/opt/eclecticiq/custom-opentaxii-configuration.yml" - - stdout_logfile = /var/log/opentaxii.log - redirect_stderr = true - autostart = true - autorestart = true - - -Using SSL/TLS -============= - -If you want to run OpenTAXII with SSL, you need to use a web server like `Nginx `_, that provides SSL/TLS layer. You can find details on how to run Nginx with SSL `here `_. - -Make sure you configure your TAXII services in OpenTAXII with proper protocol bindings: - - * use ``urn:taxii.mitre.org:protocol:https:1.0`` if you're planning on serving data via HTTPS. - * use ``urn:taxii.mitre.org:protocol:http:1.0`` if the server is going to support unsecure HTTP as well. - -You can use multiple protocol bindings per service. That would tell OpenTAXII you want to advertise services over both HTTPs and HTTP. TAXII services create external URLs according to their protocol bindings, using ``http://`` or ``https://`` schemas. - -Continue to :doc:`Manage OpenTAXII <../usage/index>` pages to learn how to manage the OpenTAXII instance. - -.. vim: set spell spelllang=en: - +================= +Running OpenTAXII +================= + +To run OpenTAXII in production it is recommended to use `standalone WSGI +container +`_ +that works with Flask. In this example we use `Gunicorn WSGI HTTP Server +`_. For complete logging configuration we recommend +Gunicorn 19.8 and above. + +.. note:: + Run ``pip install gunicorn`` to install gunicorn. Yes, it's that simple. + +To run OpenTAXII with Gunicorn execute:: + + (venv) $ gunicorn opentaxii.http:app \ + --bind localhost:9000 --config python:opentaxii.http + +Common practice is to wrap gunicorn execution into `supervisord `_, to be able to monitor, start, and stop it easily. + +Example supervisord configuration file: + +.. code-block:: ini + + [program:opentaxii] + + command = + /opt/eclecticiq/opentaxii-venv/bin/gunicorn opentaxii.http:app + --workers 2 + --log-level info + --log-file - + --timeout 300 + --bind localhost:9000 + --config python:opentaxii.http + environment = + OPENTAXII_CONFIG="/opt/eclecticiq/custom-opentaxii-configuration.yml" + + stdout_logfile = /var/log/opentaxii.log + redirect_stderr = true + autostart = true + autorestart = true + + +Using SSL/TLS +============= + +If you want to run OpenTAXII with SSL, you need to use a web server like `Nginx `_, that provides SSL/TLS layer. You can find details on how to run Nginx with SSL `here `_. + +Make sure you configure your TAXII services in OpenTAXII with proper protocol bindings: + + * use ``urn:taxii.mitre.org:protocol:https:1.0`` if you're planning on serving data via HTTPS. + * use ``urn:taxii.mitre.org:protocol:http:1.0`` if the server is going to support unsecure HTTP as well. + +You can use multiple protocol bindings per service. That would tell OpenTAXII you want to advertise services over both HTTPs and HTTP. TAXII services create external URLs according to their protocol bindings, using ``http://`` or ``https://`` schemas. + +Continue to :doc:`Manage OpenTAXII <../usage/index>` pages to learn how to manage the OpenTAXII instance. + +.. vim: set spell spelllang=en: + diff --git a/docs/license.rst b/docs/license.rst index 47dcb56a..1522cc2d 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -1,5 +1,5 @@ -======= -License -======= - -.. include:: ../LICENSE.rst +======= +License +======= + +.. include:: ../LICENSE.rst diff --git a/docs/references/changes.rst b/docs/references/changes.rst index d76c92b6..bd28b6fa 100644 --- a/docs/references/changes.rst +++ b/docs/references/changes.rst @@ -1 +1 @@ -.. include:: ../../CHANGES.rst +.. include:: ../../CHANGES.rst diff --git a/docs/references/index.rst b/docs/references/index.rst index 8c6e67ea..c2e91808 100644 --- a/docs/references/index.rst +++ b/docs/references/index.rst @@ -1,12 +1,12 @@ -========== -References -========== - -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - opentaxii_apis - public_apis - changes +========== +References +========== + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + opentaxii_apis + public_apis + changes diff --git a/docs/references/opentaxii_apis.rst b/docs/references/opentaxii_apis.rst index 7ef7fedc..21bcf2ec 100644 --- a/docs/references/opentaxii_apis.rst +++ b/docs/references/opentaxii_apis.rst @@ -1,101 +1,101 @@ -============= -API reference -============= - -This document is the API reference for OpenTAXII. It describes all classes, -methods, functions, and attributes that are part of the public API. - -Most of the terminology in the OpenTAXII API comes straight from the TAXII specification. -See the `TAXII documentation `_ or `TAXII2 documentation `_ for more detailed explanations. - - -Configuration -============= -.. automodule:: opentaxii.config - :members: - :undoc-members: - :show-inheritance: - -TAXII server layer -================== - -.. autoclass:: opentaxii.server.TAXIIServer - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: opentaxii.server.TAXII1Server - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: opentaxii.server.TAXII2Server - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentaxii.entities - :members: - :undoc-members: - :show-inheritance: - -HTTP layer -========== - -.. autofunction:: opentaxii.middleware.create_app - - -Version agnostic TAXII1 entities -================================ - -.. automodule:: opentaxii.taxii.entities - :members: - :undoc-members: - :show-inheritance: - - -TAXII services -============== - -.. automodule:: opentaxii.taxii.services.abstract - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentaxii.taxii.services.discovery - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentaxii.taxii.services.inbox - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentaxii.taxii.services.poll - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentaxii.taxii.services.collection_management - :members: - :undoc-members: - :show-inheritance: - - -TAXII2 entities -=============== - -.. automodule:: opentaxii.taxii2.entities - :members: - :undoc-members: - :show-inheritance: - - -TAXII2 validation -================= - -.. automodule:: opentaxii.taxii2.validation - :members: - :undoc-members: - :show-inheritance: +============= +API reference +============= + +This document is the API reference for OpenTAXII. It describes all classes, +methods, functions, and attributes that are part of the public API. + +Most of the terminology in the OpenTAXII API comes straight from the TAXII specification. +See the `TAXII documentation `_ or `TAXII2 documentation `_ for more detailed explanations. + + +Configuration +============= +.. automodule:: opentaxii.config + :members: + :undoc-members: + :show-inheritance: + +TAXII server layer +================== + +.. autoclass:: opentaxii.server.TAXIIServer + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: opentaxii.server.TAXII1Server + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: opentaxii.server.TAXII2Server + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentaxii.entities + :members: + :undoc-members: + :show-inheritance: + +HTTP layer +========== + +.. autofunction:: opentaxii.middleware.create_app + + +Version agnostic TAXII1 entities +================================ + +.. automodule:: opentaxii.taxii.entities + :members: + :undoc-members: + :show-inheritance: + + +TAXII services +============== + +.. automodule:: opentaxii.taxii.services.abstract + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentaxii.taxii.services.discovery + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentaxii.taxii.services.inbox + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentaxii.taxii.services.poll + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentaxii.taxii.services.collection_management + :members: + :undoc-members: + :show-inheritance: + + +TAXII2 entities +=============== + +.. automodule:: opentaxii.taxii2.entities + :members: + :undoc-members: + :show-inheritance: + + +TAXII2 validation +================= + +.. automodule:: opentaxii.taxii2.validation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/references/public_apis.rst b/docs/references/public_apis.rst index 1d5e0bb6..9a06744f 100644 --- a/docs/references/public_apis.rst +++ b/docs/references/public_apis.rst @@ -1,110 +1,110 @@ -====================== -Public code-level APIs -====================== - -This page describes OpenTAXII's public code-level APIs. - -Overview -======== - -OpenTAXII ships with code-level APIs that can be extended by users. Built-in implementations -use SQL database as a backend (everything that `SQLAlchemy `_ supports). - -Additionaly, OpenTAXII supports anychronous notifications and users can attach custom -listeners to the specific events. - -.. _custom-api-implementations: - -Custom API implementations -========================== - -It is possible to attach custom API implementations to OpenTAXII. - -Custom API class should inherit base class -(:py:class:`opentaxii.persistence.api.OpenTAXIIPersistenceAPI` for Persistence API and -:py:class:`opentaxii.auth.api.OpenTAXIIAuthAPI` for Authentication API) and implement all defined methods. - -Class constructor can accept any parameters. These parameters (as well as API class full name) -have to be set in OpenTAXII configuration file. See :ref:`example configuration ` for exact syntax. -OpenTAXII will load the class from the ``PYTHONPATH`` and create API instance during server's start up procedure. - -OpenTAXII APIs are documented below. - - -.. _custom-signals: - -Custom signal listeners -======================= - -Users can attach custom listeners for the events OpenTAXII emits. See :ref:`Signals ` to find -a list of supported signals. - -To attach custom signals, specify full module name as a value for ``hooks`` field in OpenTAXII configuration file. -Note that the module needs to be in OpenTAXII's ``PYTHONPATH``. - -Example of the implementation is provided in OpenTAXII GitHub repo - :github-file:`examples/hooks.py `. - - -Persistence API -=============== - -Persistence API takes care of all CRUD operations with entities and wraps backend storage layer. - -See :doc:`Configuration <../installation/configuration>` page for the details about how to attach -custom implementation of Persistence API. - -.. automodule:: opentaxii.persistence.api - :members: - :undoc-members: - :show-inheritance: - -The persistence API is backed by the sqldb api. - -.. automodule:: opentaxii.persistence.sqldb.api - :members: - :undoc-members: - :show-inheritance: - - -Authentication API -================== - -Authentication API represents an authority for authentication-related queries. - -See :doc:`Configuration <../installation/configuration>` page for the details about how to attach -custom implementation of Authentication API. - -.. automodule:: opentaxii.auth.api - :members: - :undoc-members: - :show-inheritance: - -The authentication API is backed by the sqldb api. - -.. automodule:: opentaxii.auth.sqldb.api - :members: - :undoc-members: - :show-inheritance: - - -.. _opentaxii-signals: - -Signals -======= - -Signals provide the ability for the user's code to receive asynchronous notification -for predefined signals. - -See :ref:`Custom signal listeners ` chapter for the details about how to attach -listeners for the signals. - -.. automodule:: opentaxii.signals - :members: - :undoc-members: - :show-inheritance: -.. autodata:: CONTENT_BLOCK_CREATED - :annotation: = Instance of a signal singleton. Signal emitted when new content block is created. -.. autodata:: INBOX_MESSAGE_CREATED - :annotation: = Instance of a signal singleton. Signal emitted when new inbox message is created. -.. autodata:: SUBSCRIPTION_CREATED - :annotation: = Instance of a signal singleton. Signal emitted when new subscription is created. +====================== +Public code-level APIs +====================== + +This page describes OpenTAXII's public code-level APIs. + +Overview +======== + +OpenTAXII ships with code-level APIs that can be extended by users. Built-in implementations +use SQL database as a backend (everything that `SQLAlchemy `_ supports). + +Additionaly, OpenTAXII supports anychronous notifications and users can attach custom +listeners to the specific events. + +.. _custom-api-implementations: + +Custom API implementations +========================== + +It is possible to attach custom API implementations to OpenTAXII. + +Custom API class should inherit base class +(:py:class:`opentaxii.persistence.api.OpenTAXIIPersistenceAPI` for Persistence API and +:py:class:`opentaxii.auth.api.OpenTAXIIAuthAPI` for Authentication API) and implement all defined methods. + +Class constructor can accept any parameters. These parameters (as well as API class full name) +have to be set in OpenTAXII configuration file. See :ref:`example configuration ` for exact syntax. +OpenTAXII will load the class from the ``PYTHONPATH`` and create API instance during server's start up procedure. + +OpenTAXII APIs are documented below. + + +.. _custom-signals: + +Custom signal listeners +======================= + +Users can attach custom listeners for the events OpenTAXII emits. See :ref:`Signals ` to find +a list of supported signals. + +To attach custom signals, specify full module name as a value for ``hooks`` field in OpenTAXII configuration file. +Note that the module needs to be in OpenTAXII's ``PYTHONPATH``. + +Example of the implementation is provided in OpenTAXII GitHub repo - :github-file:`examples/hooks.py `. + + +Persistence API +=============== + +Persistence API takes care of all CRUD operations with entities and wraps backend storage layer. + +See :doc:`Configuration <../installation/configuration>` page for the details about how to attach +custom implementation of Persistence API. + +.. automodule:: opentaxii.persistence.api + :members: + :undoc-members: + :show-inheritance: + +The persistence API is backed by the sqldb api. + +.. automodule:: opentaxii.persistence.sqldb.api + :members: + :undoc-members: + :show-inheritance: + + +Authentication API +================== + +Authentication API represents an authority for authentication-related queries. + +See :doc:`Configuration <../installation/configuration>` page for the details about how to attach +custom implementation of Authentication API. + +.. automodule:: opentaxii.auth.api + :members: + :undoc-members: + :show-inheritance: + +The authentication API is backed by the sqldb api. + +.. automodule:: opentaxii.auth.sqldb.api + :members: + :undoc-members: + :show-inheritance: + + +.. _opentaxii-signals: + +Signals +======= + +Signals provide the ability for the user's code to receive asynchronous notification +for predefined signals. + +See :ref:`Custom signal listeners ` chapter for the details about how to attach +listeners for the signals. + +.. automodule:: opentaxii.signals + :members: + :undoc-members: + :show-inheritance: +.. autodata:: CONTENT_BLOCK_CREATED + :annotation: = Instance of a signal singleton. Signal emitted when new content block is created. +.. autodata:: INBOX_MESSAGE_CREATED + :annotation: = Instance of a signal singleton. Signal emitted when new inbox message is created. +.. autodata:: SUBSCRIPTION_CREATED + :annotation: = Instance of a signal singleton. Signal emitted when new subscription is created. diff --git a/docs/requirements.txt b/docs/requirements.txt index f95072f5..ce69a9c3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==8.2.3 -sphinx_rtd_theme==3.0.2 -git+https://github.com/eclecticiq/sqla-graphs.git@py3 --e . +sphinx==8.2.3 +sphinx_rtd_theme==3.0.2 +git+https://github.com/eclecticiq/sqla-graphs.git@py3 +-e . diff --git a/docs/update_db_schema_diagram.py b/docs/update_db_schema_diagram.py index 09d1349a..471610ee 100755 --- a/docs/update_db_schema_diagram.py +++ b/docs/update_db_schema_diagram.py @@ -1,17 +1,17 @@ -#!/bin/env python3 -import os -import sys - -current_dir = os.path.dirname(__file__) -sys.path.append(os.path.abspath(os.path.join(current_dir, ".."))) - -from sqla_graphs import TableGrapher - -from opentaxii.persistence.sqldb.models import Base - -grapher = TableGrapher( - style={"node_table_header": {"bgcolor": "#000080"}}, - graph_options={"size": "30,30!"}, # inches, this maps to 2880px -) -graph = grapher.graph(tables=Base.metadata.tables.values()) -graph.write_png(os.path.join(current_dir, "db_schema_diagram.png"), prog="dot") +#!/bin/env python3 +import os +import sys + +current_dir = os.path.dirname(__file__) +sys.path.append(os.path.abspath(os.path.join(current_dir, ".."))) + +from sqla_graphs import TableGrapher + +from opentaxii.persistence.sqldb.models import Base + +grapher = TableGrapher( + style={"node_table_header": {"bgcolor": "#000080"}}, + graph_options={"size": "30,30!"}, # inches, this maps to 2880px +) +graph = grapher.graph(tables=Base.metadata.tables.values()) +graph.write_png(os.path.join(current_dir, "db_schema_diagram.png"), prog="dot") diff --git a/docs/usage/accounts.rst b/docs/usage/accounts.rst index d6e1dcfb..715714b6 100644 --- a/docs/usage/accounts.rst +++ b/docs/usage/accounts.rst @@ -1,30 +1,30 @@ -======== -Accounts -======== - -Accounts can be created with CLI commands or with custom code talking to a specific Persistent API implementation/backend. - -Manual commands -=============== - -.. code-block:: shell - - opentaxii-create-account --username --password --admin - - # Update password - opentaxii-update-account --username --field password --value - - # Set admin - opentaxii-update-account --username --field admin --value yes - # Reset admin - opentaxii-update-account --username --field admin --value no - -Sync data -========= - -.. code-block:: yaml - - accounts: - - username: admin - password: admin +======== +Accounts +======== + +Accounts can be created with CLI commands or with custom code talking to a specific Persistent API implementation/backend. + +Manual commands +=============== + +.. code-block:: shell + + opentaxii-create-account --username --password --admin + + # Update password + opentaxii-update-account --username --field password --value + + # Set admin + opentaxii-update-account --username --field admin --value yes + # Reset admin + opentaxii-update-account --username --field admin --value no + +Sync data +========= + +.. code-block:: yaml + + accounts: + - username: admin + password: admin is_admin: yes \ No newline at end of file diff --git a/docs/usage/auth.rst b/docs/usage/auth.rst index 998a4c15..a90a4349 100644 --- a/docs/usage/auth.rst +++ b/docs/usage/auth.rst @@ -1,78 +1,78 @@ -================================ -Authentication and authorization -================================ - -.. highlight:: sh - -Services Authentication (Client-side) -===================================== - -Authentication can be configured per service instance with ``authentication_required``. -This is a boolean service class field which defines if the client needs to be -authenticated before accessing the service. - -.. note:: - OpenTAXII supports session-less token-based authentication as primary method of authentication with fallback to HTTP Basic Authentication. - -If authentication is required, OpenTAXII expects clients to obtain a token first and -send it as ``Authorization`` HTTP header value (formatted as ``Bearer TOKENVALUE``) -with every TAXII request. - -To obtain a token, client sends a ``POST`` request with username and password to -authentication service running on ``/management/auth`` endpoint. - -Request data can be a form-encoded string or a JSON dictionary with ``username`` and -``password`` fields:: - - (venv) $ curl -d 'username=test&password=test' http://localhost:9000/management/auth - -or:: - - (venv) $ curl -H "Content-Type: application/json" -d '{"username":"test","password":"test"}' http://localhost:9000/management/auth - -A server will reply with a JSON dictionary that contains a token. For example:: - - { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoxLCJleHAiOjE0Mjc2MzgyMjN9.oKd43j4KR1Ovu8zOtwFdeaKILys_kpl3fAiECclP7_4" - } - -To query a service with authentication enabled, pass the token in a value of ``Authorization`` HTTP header. -Example request using `Cabby library `_ CLI command:: - - # using raw HTTP header - (venv) $ taxii-poll \ - --path http://localhost:9000/services/poll-a \ - --collection collection-a \ - --header Authorization:'Bearer eyJleHAiOjE0MjY3OTMwOTYsImFsZyI6IkhTMjU2IiwiaWF0IjoxNDI2Nzg1ODk2fQ.eyJ1c2VyX2lkIjoxfQ.YsZIdbrU92dL8j5G8ydVAsdWHXtx371vC0POmXrS3W8' - - # using JWT support in Cabby - (venv) $ taxii-poll \ - --host localhost \ - --port 9000 \ - --path /services/poll-a \ - --collection collection-a \ - --username test \ - --password test \ - --jwt-auth /management/auth - -An alternative to that is to use Basic Authentication: client sends username/password formatted as Basic Authentication header with every TAXII request, OpenTAXII decodes it and passes username/password pair to Auth API for authentication. - -Authentication Implementation -============================= - -The build-in Authentication API implementation uses -`JWT (JSON Web Token) `_ -to create tokens. - -Token TTL is set to 60 minutes by default but can be configured with ``token_ttl_secs`` in ``Auth API`` settings in :ref:`custom configuration `. - -Authentication API needs to be able to identify a client based on the token -(method :py:meth:`opentaxii.auth.api.OpenTAXIIAuthAPI.get_account` of the API). The built-in implementation -does that by encoding account ID inside the token. - -.. rubric:: Next steps - -Continue to :doc:`Public code-level APIs <../references/public_apis>` page for the details about OpenTAXII APIs. - -.. vim: set spell spelllang=en: - +================================ +Authentication and authorization +================================ + +.. highlight:: sh + +Services Authentication (Client-side) +===================================== + +Authentication can be configured per service instance with ``authentication_required``. +This is a boolean service class field which defines if the client needs to be +authenticated before accessing the service. + +.. note:: + OpenTAXII supports session-less token-based authentication as primary method of authentication with fallback to HTTP Basic Authentication. + +If authentication is required, OpenTAXII expects clients to obtain a token first and +send it as ``Authorization`` HTTP header value (formatted as ``Bearer TOKENVALUE``) +with every TAXII request. + +To obtain a token, client sends a ``POST`` request with username and password to +authentication service running on ``/management/auth`` endpoint. + +Request data can be a form-encoded string or a JSON dictionary with ``username`` and +``password`` fields:: + + (venv) $ curl -d 'username=test&password=test' http://localhost:9000/management/auth + +or:: + + (venv) $ curl -H "Content-Type: application/json" -d '{"username":"test","password":"test"}' http://localhost:9000/management/auth + +A server will reply with a JSON dictionary that contains a token. For example:: + + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoxLCJleHAiOjE0Mjc2MzgyMjN9.oKd43j4KR1Ovu8zOtwFdeaKILys_kpl3fAiECclP7_4" + } + +To query a service with authentication enabled, pass the token in a value of ``Authorization`` HTTP header. +Example request using `Cabby library `_ CLI command:: + + # using raw HTTP header + (venv) $ taxii-poll \ + --path http://localhost:9000/services/poll-a \ + --collection collection-a \ + --header Authorization:'Bearer eyJleHAiOjE0MjY3OTMwOTYsImFsZyI6IkhTMjU2IiwiaWF0IjoxNDI2Nzg1ODk2fQ.eyJ1c2VyX2lkIjoxfQ.YsZIdbrU92dL8j5G8ydVAsdWHXtx371vC0POmXrS3W8' + + # using JWT support in Cabby + (venv) $ taxii-poll \ + --host localhost \ + --port 9000 \ + --path /services/poll-a \ + --collection collection-a \ + --username test \ + --password test \ + --jwt-auth /management/auth + +An alternative to that is to use Basic Authentication: client sends username/password formatted as Basic Authentication header with every TAXII request, OpenTAXII decodes it and passes username/password pair to Auth API for authentication. + +Authentication Implementation +============================= + +The build-in Authentication API implementation uses +`JWT (JSON Web Token) `_ +to create tokens. + +Token TTL is set to 60 minutes by default but can be configured with ``token_ttl_secs`` in ``Auth API`` settings in :ref:`custom configuration `. + +Authentication API needs to be able to identify a client based on the token +(method :py:meth:`opentaxii.auth.api.OpenTAXIIAuthAPI.get_account` of the API). The built-in implementation +does that by encoding account ID inside the token. + +.. rubric:: Next steps + +Continue to :doc:`Public code-level APIs <../references/public_apis>` page for the details about OpenTAXII APIs. + +.. vim: set spell spelllang=en: + diff --git a/docs/usage/health_check.rst b/docs/usage/health_check.rst index 1cf10784..9ca4eb63 100644 --- a/docs/usage/health_check.rst +++ b/docs/usage/health_check.rst @@ -1,16 +1,16 @@ -============ -Health check -============ - -OpenTAXII has an endpoint that can be used to check health of the service:: - - $ curl http://localhost:9000/management/health - { - "alive": true - } - -.. rubric:: Next steps - -Continue to :doc:`Authentication <../usage/auth>` page to learn how OpenTAXII authentication process works. - -.. vim: set spell spelllang=en: +============ +Health check +============ + +OpenTAXII has an endpoint that can be used to check health of the service:: + + $ curl http://localhost:9000/management/health + { + "alive": true + } + +.. rubric:: Next steps + +Continue to :doc:`Authentication <../usage/auth>` page to learn how OpenTAXII authentication process works. + +.. vim: set spell spelllang=en: diff --git a/docs/usage/index.rst b/docs/usage/index.rst index ef7a0306..63d706ab 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -1,15 +1,15 @@ -================ -Manage OpenTAXII -================ - -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - accounts - taxii1_services_collections - taxii2_apiroot_collections - auth - sending_requests - health_check +================ +Manage OpenTAXII +================ + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + accounts + taxii1_services_collections + taxii2_apiroot_collections + auth + sending_requests + health_check diff --git a/docs/usage/sending_requests.rst b/docs/usage/sending_requests.rst index e7ee5593..c7ac953d 100644 --- a/docs/usage/sending_requests.rst +++ b/docs/usage/sending_requests.rst @@ -1,164 +1,164 @@ -============================ -Sending requests to services -============================ - -TAXII 1 -======= - -The easiest way to send requests to TAXII services is to use `Cabby library `_ CLI tools:: - - (venv) $ pip install cabby - -If you are running OpenTAXII in default configuration and you created services using :doc:`provided examples <../installation/configuration>`, you should -be able to communicate with the services. - -Assuming OpenTAXII is running on ``http://localhost:9000``, to get the list of advertised services, run:: - - (venv) $ taxii-discovery --path http://localhost:9000/services/discovery-a - -You should see the following output:: - - 2018-06-03 21:26:32,188 INFO: Sending Discovery_Request to http://localhost:9000/services/discovery-a - 2018-06-03 21:26:32,207 INFO: 7 services discovered - === Service Instance === - Service Type: INBOX - Service Version: urn:taxii.mitre.org:services:1.1 - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Service Address: http://localhost:9000/services/inbox-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Inbox Service AC: [] - Available: True - Message: Custom Inbox Service Description A - - === Service Instance === - Service Type: INBOX - Service Version: urn:taxii.mitre.org:services:1.1 - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Service Address: http://localhost:9000/services/inbox-b - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Inbox Service AC: ['urn:stix.mitre.org:xml:1.1.1', 'urn:custom.example.com:json:0.0.1'] - Available: True - Message: Custom Inbox Service Description B - - === Service Instance === - Service Type: DISCOVERY - Service Version: urn:taxii.mitre.org:services:1.1 - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Service Address: http://localhost:9000/services/discovery-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Available: True - Message: Custom Discovery Service description - - ..... snip ..... - -One of the configured services is a Collection Management Service. To get the collections list, run:: - - (venv) $ taxii-collections --path http://localhost:9000/services/collection-management-a - -You should see the following output:: - - 2018-06-03 21:30:03,315 INFO: Sending Collection_Information_Request to http://localhost:9000/services/collection-management-a - === Data Collection Information === - Collection Name: collection-a - Collection Type: DATA_SET - Available: True - Collection Description: None - Supported Content: All - === Polling Service Instance === - Poll Protocol: urn:taxii.mitre.org:protocol:http:1.0 - Poll Address: http://localhost:9000/services/poll-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Subscription Service === - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Address: http://localhost:9000/services/collection-management-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Subscription Service === - Protocol Binding: urn:taxii.mitre.org:protocol:https:1.0 - Address: https://localhost:9000/services/collection-management-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Receiving Inbox Service === - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Address: http://localhost:9000/services/inbox-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Supported Contents: All - ================================== - - - === Data Collection Information === - Collection Name: collection-b - Collection Type: DATA_FEED - Available: True - Collection Description: None - Supported Content: urn:stix.mitre.org:xml:1.1.1 - === Polling Service Instance === - Poll Protocol: urn:taxii.mitre.org:protocol:http:1.0 - Poll Address: http://localhost:9000/services/poll-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Subscription Service === - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Address: http://localhost:9000/services/collection-management-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Subscription Service === - Protocol Binding: urn:taxii.mitre.org:protocol:https:1.0 - Address: https://localhost:9000/services/collection-management-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - === Receiving Inbox Service === - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Address: http://localhost:9000/services/inbox-a - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Supported Contents: All - === Receiving Inbox Service === - Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 - Address: http://localhost:9000/services/inbox-b - Message Binding: urn:taxii.mitre.org:message:xml:1.0 - Message Binding: urn:taxii.mitre.org:message:xml:1.1 - Supported Content: urn:stix.mitre.org:xml:1.1.1 - Supported Content: urn:custom.example.com:json:0.0.1 - ================================== - - .... snip ..... - - -See `Cabby documentation `_ for more examples. - -TAXII 2 -======= - -HTTP ----- - -You can make HTTP requests directly to the server. The discovery endpoint is at -``/taxii2/``. The trailing ``/`` is important. - -Then, the other endpoints are the ones provided by the `TAXII 2.1 specs -`_. - -Python ------- - -.. code-block:: python - - import taxii2client - - # With public discovery - server = taxii2client.Server(url="http://localhost:9000/taxii2/") - - print(server.title) - for root in server.api_roots: - print(root.url) - - # With authentication - api_root = taxii2client.ApiRoot( - root.url, user="", password="" - ) +============================ +Sending requests to services +============================ + +TAXII 1 +======= + +The easiest way to send requests to TAXII services is to use `Cabby library `_ CLI tools:: + + (venv) $ pip install cabby + +If you are running OpenTAXII in default configuration and you created services using :doc:`provided examples <../installation/configuration>`, you should +be able to communicate with the services. + +Assuming OpenTAXII is running on ``http://localhost:9000``, to get the list of advertised services, run:: + + (venv) $ taxii-discovery --path http://localhost:9000/services/discovery-a + +You should see the following output:: + + 2018-06-03 21:26:32,188 INFO: Sending Discovery_Request to http://localhost:9000/services/discovery-a + 2018-06-03 21:26:32,207 INFO: 7 services discovered + === Service Instance === + Service Type: INBOX + Service Version: urn:taxii.mitre.org:services:1.1 + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Service Address: http://localhost:9000/services/inbox-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Inbox Service AC: [] + Available: True + Message: Custom Inbox Service Description A + + === Service Instance === + Service Type: INBOX + Service Version: urn:taxii.mitre.org:services:1.1 + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Service Address: http://localhost:9000/services/inbox-b + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Inbox Service AC: ['urn:stix.mitre.org:xml:1.1.1', 'urn:custom.example.com:json:0.0.1'] + Available: True + Message: Custom Inbox Service Description B + + === Service Instance === + Service Type: DISCOVERY + Service Version: urn:taxii.mitre.org:services:1.1 + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Service Address: http://localhost:9000/services/discovery-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Available: True + Message: Custom Discovery Service description + + ..... snip ..... + +One of the configured services is a Collection Management Service. To get the collections list, run:: + + (venv) $ taxii-collections --path http://localhost:9000/services/collection-management-a + +You should see the following output:: + + 2018-06-03 21:30:03,315 INFO: Sending Collection_Information_Request to http://localhost:9000/services/collection-management-a + === Data Collection Information === + Collection Name: collection-a + Collection Type: DATA_SET + Available: True + Collection Description: None + Supported Content: All + === Polling Service Instance === + Poll Protocol: urn:taxii.mitre.org:protocol:http:1.0 + Poll Address: http://localhost:9000/services/poll-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Subscription Service === + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Address: http://localhost:9000/services/collection-management-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Subscription Service === + Protocol Binding: urn:taxii.mitre.org:protocol:https:1.0 + Address: https://localhost:9000/services/collection-management-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Receiving Inbox Service === + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Address: http://localhost:9000/services/inbox-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Supported Contents: All + ================================== + + + === Data Collection Information === + Collection Name: collection-b + Collection Type: DATA_FEED + Available: True + Collection Description: None + Supported Content: urn:stix.mitre.org:xml:1.1.1 + === Polling Service Instance === + Poll Protocol: urn:taxii.mitre.org:protocol:http:1.0 + Poll Address: http://localhost:9000/services/poll-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Subscription Service === + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Address: http://localhost:9000/services/collection-management-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Subscription Service === + Protocol Binding: urn:taxii.mitre.org:protocol:https:1.0 + Address: https://localhost:9000/services/collection-management-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + === Receiving Inbox Service === + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Address: http://localhost:9000/services/inbox-a + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Supported Contents: All + === Receiving Inbox Service === + Protocol Binding: urn:taxii.mitre.org:protocol:http:1.0 + Address: http://localhost:9000/services/inbox-b + Message Binding: urn:taxii.mitre.org:message:xml:1.0 + Message Binding: urn:taxii.mitre.org:message:xml:1.1 + Supported Content: urn:stix.mitre.org:xml:1.1.1 + Supported Content: urn:custom.example.com:json:0.0.1 + ================================== + + .... snip ..... + + +See `Cabby documentation `_ for more examples. + +TAXII 2 +======= + +HTTP +---- + +You can make HTTP requests directly to the server. The discovery endpoint is at +``/taxii2/``. The trailing ``/`` is important. + +Then, the other endpoints are the ones provided by the `TAXII 2.1 specs +`_. + +Python +------ + +.. code-block:: python + + import taxii2client + + # With public discovery + server = taxii2client.Server(url="http://localhost:9000/taxii2/") + + print(server.title) + for root in server.api_roots: + print(root.url) + + # With authentication + api_root = taxii2client.ApiRoot( + root.url, user="", password="" + ) diff --git a/docs/usage/taxii1_services_collections.rst b/docs/usage/taxii1_services_collections.rst index 8e36b3fd..90bb2f93 100644 --- a/docs/usage/taxii1_services_collections.rst +++ b/docs/usage/taxii1_services_collections.rst @@ -1,67 +1,67 @@ -================================= -TAXII 1: Services and Collections -================================= - -Services and Collections can be created with CLI commands -``opentaxii-sync-data`` or with custom code talking to a specific Persistence API -implementation/backend. - -.. Note:: - The services and collections created with CLI command ``opentaxii-sync-data`` are only available with TAXII 1. - -Step 1 ------- -Create YAML file with collections/services/accounts configuration. See provided example from `OpenTAXII git repo `_ — file :github-file:`examples/data-configuration.yml ` that contains: - -Services: - * 2 Inbox Services (with ids ``inbox_a`` and ``inbox_b``), - * 1 Discovery Service (with id ``discovery_a``), - * 1 Collection Management Service (with id ``collection_management_a``), - * 1 Poll Service (with id ``poll_a``). - -.. note:: - Services have relative path in the address field, which means OpenTAXII will prepend it with domain configured in server configuration file (``localhost:9000`` in Default configuration). - -Collections: - * ``collection-a`` that has type ``DATA_SET``, accepts all content types and is attached to services - ``inbox_a``, ``collection_management_a``, and ``poll_a``. - * ``collection-b`` that accepts only content types specified in field ``content_bindings`` and is attached to services ``inbox_a``, ``inbox_b``, ``collection_management_a`` and ``poll_a``. - * ``collection-c`` that accepts not only STIX v1.1.1 content type but also custom content type ``urn:custom.bindings.com:json:0.0.1``. It is attached to services ``inbox_a``, ``collection_management_a`` and ``poll_a``. - * ``col-not-available`` that is marked as not available, even though it is attached to ``inbox_b`` and ``collection_management_a``. - -Accounts: - * account with username ``test`` and password ``test``, with ability to modify collection ``collection-a``, read ``collection-b`` and ``coll-stix-and-custom``, and unknown permission ``some`` for non-existing collection ``collection-xyz``. Incorrect settings will be ignored during sync. - * account with username ``admin`` and password ``admin`` that has admin permissions because ``is_admin`` is set to ``yes``. - -.. note:: - Without an account you can't access services with ``authentication_required`` enabled. - -Step 2 ------- -Use ``opentaxii-sync-data`` command to synchronize data configuration in provided file and in DB. - -Usage help:: - - (venv) $ opentaxii-sync-data --help - usage: opentaxii-sync-data [-h] [-f] config - - Create services/collections/accounts - - positional arguments: - config YAML file with data configuration - - optional arguments: - -h, --help show this help message and exit - -f, --force-delete force deletion of collections and their content blocks - if collection is not defined in configuration file - (default: False) - -To sync data run:: - - (venv) $ opentaxii-sync-data examples/data-configuration.yml - -.. note:: - To drop the databases, just delete sqlite3 files ``/tmp/data.db``, ``/tmp/auth.db`` and restart OpenTAXII server. - -Now OpenTAXII has services, collections and accounts configured and can function as a TAXII server. +================================= +TAXII 1: Services and Collections +================================= + +Services and Collections can be created with CLI commands +``opentaxii-sync-data`` or with custom code talking to a specific Persistence API +implementation/backend. + +.. Note:: + The services and collections created with CLI command ``opentaxii-sync-data`` are only available with TAXII 1. + +Step 1 +------ +Create YAML file with collections/services/accounts configuration. See provided example from `OpenTAXII git repo `_ — file :github-file:`examples/data-configuration.yml ` that contains: + +Services: + * 2 Inbox Services (with ids ``inbox_a`` and ``inbox_b``), + * 1 Discovery Service (with id ``discovery_a``), + * 1 Collection Management Service (with id ``collection_management_a``), + * 1 Poll Service (with id ``poll_a``). + +.. note:: + Services have relative path in the address field, which means OpenTAXII will prepend it with domain configured in server configuration file (``localhost:9000`` in Default configuration). + +Collections: + * ``collection-a`` that has type ``DATA_SET``, accepts all content types and is attached to services + ``inbox_a``, ``collection_management_a``, and ``poll_a``. + * ``collection-b`` that accepts only content types specified in field ``content_bindings`` and is attached to services ``inbox_a``, ``inbox_b``, ``collection_management_a`` and ``poll_a``. + * ``collection-c`` that accepts not only STIX v1.1.1 content type but also custom content type ``urn:custom.bindings.com:json:0.0.1``. It is attached to services ``inbox_a``, ``collection_management_a`` and ``poll_a``. + * ``col-not-available`` that is marked as not available, even though it is attached to ``inbox_b`` and ``collection_management_a``. + +Accounts: + * account with username ``test`` and password ``test``, with ability to modify collection ``collection-a``, read ``collection-b`` and ``coll-stix-and-custom``, and unknown permission ``some`` for non-existing collection ``collection-xyz``. Incorrect settings will be ignored during sync. + * account with username ``admin`` and password ``admin`` that has admin permissions because ``is_admin`` is set to ``yes``. + +.. note:: + Without an account you can't access services with ``authentication_required`` enabled. + +Step 2 +------ +Use ``opentaxii-sync-data`` command to synchronize data configuration in provided file and in DB. + +Usage help:: + + (venv) $ opentaxii-sync-data --help + usage: opentaxii-sync-data [-h] [-f] config + + Create services/collections/accounts + + positional arguments: + config YAML file with data configuration + + optional arguments: + -h, --help show this help message and exit + -f, --force-delete force deletion of collections and their content blocks + if collection is not defined in configuration file + (default: False) + +To sync data run:: + + (venv) $ opentaxii-sync-data examples/data-configuration.yml + +.. note:: + To drop the databases, just delete sqlite3 files ``/tmp/data.db``, ``/tmp/auth.db`` and restart OpenTAXII server. + +Now OpenTAXII has services, collections and accounts configured and can function as a TAXII server. Check :doc:`Use OpenTAXII <../installation/running>` to see how to use it. \ No newline at end of file diff --git a/docs/usage/taxii2_apiroot_collections.rst b/docs/usage/taxii2_apiroot_collections.rst index 98424ab6..db764d9d 100644 --- a/docs/usage/taxii2_apiroot_collections.rst +++ b/docs/usage/taxii2_apiroot_collections.rst @@ -1,15 +1,15 @@ -================================= -TAXII 2: API Root and collections -================================= - -API Root and collections can be created with CLI commands or with custom code -talking to a specific Persistent API implementation/backend. - -CLI commands -============ - -.. code-block:: shell - - opentaxii-add-api-root --title default --description "The default API root" --default - - opentaxii-add-collection --rootid --title "A collection" --public +================================= +TAXII 2: API Root and collections +================================= + +API Root and collections can be created with CLI commands or with custom code +talking to a specific Persistent API implementation/backend. + +CLI commands +============ + +.. code-block:: shell + + opentaxii-add-api-root --title default --description "The default API root" --default + + opentaxii-add-collection --rootid --title "A collection" --public diff --git a/examples/Makefile b/examples/Makefile index 250cc797..ef8d90c8 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,14 +1,14 @@ -# Example Makefile for creating the docker image - -.PHONY: build push - -REG:="eclecticiq" -IMAGE:="opentaxii" - -all: build push - -build: - docker build -t $(REG)/$(IMAGE) ../. - -push: - docker push $(REG)/$(IMAGE) ../. +# Example Makefile for creating the docker image + +.PHONY: build push + +REG:="eclecticiq" +IMAGE:="opentaxii" + +all: build push + +build: + docker build -t $(REG)/$(IMAGE) ../. + +push: + docker push $(REG)/$(IMAGE) ../. diff --git a/examples/data-configuration.yml b/examples/data-configuration.yml index 1bec7c95..dac33812 100644 --- a/examples/data-configuration.yml +++ b/examples/data-configuration.yml @@ -1,108 +1,108 @@ ---- - -services: - - id: inbox_a - type: inbox - address: /services/inbox-a - description: Custom Inbox Service Description A - destination_collection_required: yes - accept_all_content: yes - authentication_required: no - protocol_bindings: - - urn:taxii.mitre.org:protocol:http:1.0 - - - id: inbox_b - type: inbox - address: /services/inbox-b - description: Custom Inbox Service Description B - destination_collection_required: yes - accept_all_content: no - authentication_required: yes - supported_content: - - urn:stix.mitre.org:xml:1.1.1 - - urn:custom.example.com:json:0.0.1 - protocol_bindings: - - urn:taxii.mitre.org:protocol:http:1.0 - - - id: discovery_a - type: discovery - address: /services/discovery-a - description: Custom Discovery Service description - advertised_services: - - inbox_a - - inbox_b - - discovery_a - - collection_management_a - - poll_a - protocol_bindings: - - urn:taxii.mitre.org:protocol:http:1.0 - - urn:taxii.mitre.org:protocol:https:1.0 - - - id: collection_management_a - type: collection_management - address: /services/collection-management-a - description: Custom Collection Management Service description - protocol_bindings: - - urn:taxii.mitre.org:protocol:http:1.0 - - urn:taxii.mitre.org:protocol:https:1.0 - - - id: poll_a - type: poll - address: /services/poll-a - description: Custom Poll Service description - subscription_required: no - max_result_count: 100 - max_result_size: 10 - authentication_required: yes - protocol_bindings: - - urn:taxii.mitre.org:protocol:http:1.0 - -collections: - - name: collection-a - available: true - accept_all_content: true - type: DATA_SET - service_ids: - - inbox_a - - collection_management_a - - poll_a - - - name: collection-b - available: true - accept_all_content: false - supported_content: - - urn:stix.mitre.org:xml:1.1.1 - service_ids: - - inbox_a - - inbox_b - - collection_management_a - - poll_a - - - name: collection-c - available: true - accept_all_content: false - supported_content: - - urn:stix.mitre.org:xml:1.1.1 - - urn:custom.bindings.com:json:0.0.1 - service_ids: - - inbox_a - - collection_management_a - - poll_a - - - name: col-not-available - available: false - service_ids: - - inbox_b - - collection_management_a - -accounts: - - username: test - password: test - permissions: - collection-a: modify - collection-b: read - collection-c: read - collection-xyz: some - - username: admin - password: admin - is_admin: yes +--- + +services: + - id: inbox_a + type: inbox + address: /services/inbox-a + description: Custom Inbox Service Description A + destination_collection_required: yes + accept_all_content: yes + authentication_required: no + protocol_bindings: + - urn:taxii.mitre.org:protocol:http:1.0 + + - id: inbox_b + type: inbox + address: /services/inbox-b + description: Custom Inbox Service Description B + destination_collection_required: yes + accept_all_content: no + authentication_required: yes + supported_content: + - urn:stix.mitre.org:xml:1.1.1 + - urn:custom.example.com:json:0.0.1 + protocol_bindings: + - urn:taxii.mitre.org:protocol:http:1.0 + + - id: discovery_a + type: discovery + address: /services/discovery-a + description: Custom Discovery Service description + advertised_services: + - inbox_a + - inbox_b + - discovery_a + - collection_management_a + - poll_a + protocol_bindings: + - urn:taxii.mitre.org:protocol:http:1.0 + - urn:taxii.mitre.org:protocol:https:1.0 + + - id: collection_management_a + type: collection_management + address: /services/collection-management-a + description: Custom Collection Management Service description + protocol_bindings: + - urn:taxii.mitre.org:protocol:http:1.0 + - urn:taxii.mitre.org:protocol:https:1.0 + + - id: poll_a + type: poll + address: /services/poll-a + description: Custom Poll Service description + subscription_required: no + max_result_count: 100 + max_result_size: 10 + authentication_required: yes + protocol_bindings: + - urn:taxii.mitre.org:protocol:http:1.0 + +collections: + - name: collection-a + available: true + accept_all_content: true + type: DATA_SET + service_ids: + - inbox_a + - collection_management_a + - poll_a + + - name: collection-b + available: true + accept_all_content: false + supported_content: + - urn:stix.mitre.org:xml:1.1.1 + service_ids: + - inbox_a + - inbox_b + - collection_management_a + - poll_a + + - name: collection-c + available: true + accept_all_content: false + supported_content: + - urn:stix.mitre.org:xml:1.1.1 + - urn:custom.bindings.com:json:0.0.1 + service_ids: + - inbox_a + - collection_management_a + - poll_a + + - name: col-not-available + available: false + service_ids: + - inbox_b + - collection_management_a + +accounts: + - username: test + password: test + permissions: + collection-a: modify + collection-b: read + collection-c: read + collection-xyz: some + - username: admin + password: admin + is_admin: yes diff --git a/examples/docker-compose-taxii2.yml b/examples/docker-compose-taxii2.yml index 8d12d066..98f2effb 100644 --- a/examples/docker-compose-taxii2.yml +++ b/examples/docker-compose-taxii2.yml @@ -1,11 +1,11 @@ -services: - opentaxii: - image: eclecticiq/opentaxii:${IMAGE_TAG-latest} - environment: - # Required for the CLI commands to use the correct config - OPENTAXII_CONFIG: /opentaxii.yml - volumes: - - ./taxii2/opentaxii.yml:/input/opentaxii.yml:ro - ports: - - 9000:9000 - +services: + opentaxii: + image: eclecticiq/opentaxii:${IMAGE_TAG-latest} + environment: + # Required for the CLI commands to use the correct config + OPENTAXII_CONFIG: /opentaxii.yml + volumes: + - ./taxii2/opentaxii.yml:/input/opentaxii.yml:ro + ports: + - 9000:9000 + diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 8fe1687b..5ad3b2bd 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -1,50 +1,50 @@ -db: - image: postgres:9.4 - environment: - POSTGRES_USER: user - POSTGRES_PASSWORD: password - POSTGRES_DB: opentaxii - -authdb: - image: postgres:9.4 - environment: - POSTGRES_USER: user1 - POSTGRES_PASSWORD: password1 - POSTGRES_DB: opentaxii1 - -opentaxii: - image: eclecticiq/opentaxii:${IMAGE_TAG-latest} - environment: - DOCKER_OPENTAXII_AUTH_SECRET: secret - OPENTAXII_DOMAIN: 192.168.59.103:9000 - DATABASE_HOST: db - DATABASE_NAME: opentaxii - DATABASE_USER: user - DATABASE_PASS: password - AUTH_DATABASE_HOST: authdb - AUTH_DATABASE_NAME: opentaxii1 - AUTH_DATABASE_USER: user1 - AUTH_DATABASE_PASS: password1 - volumes: - - ./data-configuration.yml:/input/data-configuration.yml:ro - ports: - - 9000:9000 - links: - - db:db - - authdb:authdb - -opentaxii2: - image: eclecticiq/opentaxii:${IMAGE_TAG-latest} - environment: - DOCKER_OPENTAXII_AUTH_SECRET: secrettwo - OPENTAXII_DOMAIN: 192.168.59.103 - DATABASE_HOST: authdb - DATABASE_NAME: opentaxii1 - DATABASE_USER: user1 - DATABASE_PASS: password1 - volumes: - - ./data-configuration.yml:/input/data-configuration.yml:ro - ports: - - 9001:9000 - links: - - authdb:authdb +db: + image: postgres:9.4 + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: opentaxii + +authdb: + image: postgres:9.4 + environment: + POSTGRES_USER: user1 + POSTGRES_PASSWORD: password1 + POSTGRES_DB: opentaxii1 + +opentaxii: + image: eclecticiq/opentaxii:${IMAGE_TAG-latest} + environment: + DOCKER_OPENTAXII_AUTH_SECRET: secret + OPENTAXII_DOMAIN: 192.168.59.103:9000 + DATABASE_HOST: db + DATABASE_NAME: opentaxii + DATABASE_USER: user + DATABASE_PASS: password + AUTH_DATABASE_HOST: authdb + AUTH_DATABASE_NAME: opentaxii1 + AUTH_DATABASE_USER: user1 + AUTH_DATABASE_PASS: password1 + volumes: + - ./data-configuration.yml:/input/data-configuration.yml:ro + ports: + - 9000:9000 + links: + - db:db + - authdb:authdb + +opentaxii2: + image: eclecticiq/opentaxii:${IMAGE_TAG-latest} + environment: + DOCKER_OPENTAXII_AUTH_SECRET: secrettwo + OPENTAXII_DOMAIN: 192.168.59.103 + DATABASE_HOST: authdb + DATABASE_NAME: opentaxii1 + DATABASE_USER: user1 + DATABASE_PASS: password1 + volumes: + - ./data-configuration.yml:/input/data-configuration.yml:ro + ports: + - 9001:9000 + links: + - authdb:authdb diff --git a/examples/hooks.py b/examples/hooks.py index eec6486b..b3ebc4cd 100644 --- a/examples/hooks.py +++ b/examples/hooks.py @@ -1,27 +1,27 @@ - -from opentaxii.signals import ( - CONTENT_BLOCK_CREATED, - INBOX_MESSAGE_CREATED, - SUBSCRIPTION_CREATED, -) - - -def post_create_content_block(manager, content_block, collection_ids, - service_id): - print('Content block id={} (collections={}, service_id={}) was created' - .format(content_block.id, ', '.join(map(str, collection_ids)), - service_id)) - - -def post_create_inbox_message(manager, inbox_message): - print('Inbox message id={} was created'.format(inbox_message.id)) - - -def post_create_subscription(manager, subscription): - print('Subscription id={} (service_id={}) was created' - .format(subscription.id, subscription.service_id)) - - -CONTENT_BLOCK_CREATED.connect(post_create_content_block) -INBOX_MESSAGE_CREATED.connect(post_create_inbox_message) -SUBSCRIPTION_CREATED.connect(post_create_subscription) + +from opentaxii.signals import ( + CONTENT_BLOCK_CREATED, + INBOX_MESSAGE_CREATED, + SUBSCRIPTION_CREATED, +) + + +def post_create_content_block(manager, content_block, collection_ids, + service_id): + print('Content block id={} (collections={}, service_id={}) was created' + .format(content_block.id, ', '.join(map(str, collection_ids)), + service_id)) + + +def post_create_inbox_message(manager, inbox_message): + print('Inbox message id={} was created'.format(inbox_message.id)) + + +def post_create_subscription(manager, subscription): + print('Subscription id={} (service_id={}) was created' + .format(subscription.id, subscription.service_id)) + + +CONTENT_BLOCK_CREATED.connect(post_create_content_block) +INBOX_MESSAGE_CREATED.connect(post_create_inbox_message) +SUBSCRIPTION_CREATED.connect(post_create_subscription) diff --git a/examples/stix/stuxnet.stix.xml b/examples/stix/stuxnet.stix.xml index ccdf263b..244aa53f 100644 --- a/examples/stix/stuxnet.stix.xml +++ b/examples/stix/stuxnet.stix.xml @@ -1,231 +1,231 @@ - - - CybOX-represented Indicators Translated from OpenIOC File - - - - CybOX-represented Indicator Created from OpenIOC File - - Generic indicator for the stuxnet virus. When loaded, stuxnet spawns lsass.exe in a suspended state. The malware then maps in its own executable section and fixes up the CONTEXT to point to the newly mapped in section. This is a common task performed by malware and allows the malware to execute under the pretense of a known and trusted process. - - OpenIOC File: stuxnet.ioc.xml - - - - - - - - - .stub - - - - - - - - - - mdmcpq3.PNF - - - - - - - mdmeric3.PNF - - - - - - - oem6C.PNF - - - - - - - oem7A.PNF - - - - - - - - - - - fs_rec.sys - - - - - - - - - - - mrxsmb.sys - - - - - - - - - - - sr.sys - - - - - - - - - - - fastfat.sys - - - - - - - - - - - - - mrxcls.sys - - - - - - - - Realtek Semiconductor Corp - - - - - - - - - - - - mrxnet.sys - - - - - - - - Realtek Semiconductor Corp - - - - - - - - - - - - SYSTEM\ControlSet001\Services\MRxCls\ImagePath - HKEY_LOCAL_MACHINE - - - - - - - - - mrxcls.sys - - - - - - - - - - - - - SYSTEM\ControlSet001\Services\MRxNet\ImagePath - HKEY_LOCAL_MACHINE - - - - - - - - - mrxnet.sys - - - - - - - - - - - - - OpenIOC to STIX Utility - 0.12 - - - - - - + + + CybOX-represented Indicators Translated from OpenIOC File + + + + CybOX-represented Indicator Created from OpenIOC File + + Generic indicator for the stuxnet virus. When loaded, stuxnet spawns lsass.exe in a suspended state. The malware then maps in its own executable section and fixes up the CONTEXT to point to the newly mapped in section. This is a common task performed by malware and allows the malware to execute under the pretense of a known and trusted process. + + OpenIOC File: stuxnet.ioc.xml + + + + + + + + + .stub + + + + + + + + + + mdmcpq3.PNF + + + + + + + mdmeric3.PNF + + + + + + + oem6C.PNF + + + + + + + oem7A.PNF + + + + + + + + + + + fs_rec.sys + + + + + + + + + + + mrxsmb.sys + + + + + + + + + + + sr.sys + + + + + + + + + + + fastfat.sys + + + + + + + + + + + + + mrxcls.sys + + + + + + + + Realtek Semiconductor Corp + + + + + + + + + + + + mrxnet.sys + + + + + + + + Realtek Semiconductor Corp + + + + + + + + + + + + SYSTEM\ControlSet001\Services\MRxCls\ImagePath + HKEY_LOCAL_MACHINE + + + + + + + + + mrxcls.sys + + + + + + + + + + + + + SYSTEM\ControlSet001\Services\MRxNet\ImagePath + HKEY_LOCAL_MACHINE + + + + + + + + + mrxnet.sys + + + + + + + + + + + + + OpenIOC to STIX Utility + 0.12 + + + + + + diff --git a/examples/taxii2/data-setup.sh b/examples/taxii2/data-setup.sh index 68d732c8..ff420257 100755 --- a/examples/taxii2/data-setup.sh +++ b/examples/taxii2/data-setup.sh @@ -1,6 +1,6 @@ -#!/bin/bash -opentaxii-add-api-root --title Default --description "The default API root" \ - --default --public --id b96631e7-f1c1-43dd-b9d6-55d634f31c41 -opentaxii-add-collection --rootid b96631e7-f1c1-43dd-b9d6-55d634f31c41 \ - --title "A collection" --public +#!/bin/bash +opentaxii-add-api-root --title Default --description "The default API root" \ + --default --public --id b96631e7-f1c1-43dd-b9d6-55d634f31c41 +opentaxii-add-collection --rootid b96631e7-f1c1-43dd-b9d6-55d634f31c41 \ + --title "A collection" --public opentaxii-create-account --username admin --password admin --admin \ No newline at end of file diff --git a/examples/taxii2/opentaxii.yml b/examples/taxii2/opentaxii.yml index 8c78fdab..8843634e 100644 --- a/examples/taxii2/opentaxii.yml +++ b/examples/taxii2/opentaxii.yml @@ -1,24 +1,24 @@ -domain: "localhost:9000" - -taxii2: - persistence_api: - class: opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI - parameters: - db_connection: sqlite:////data/data.db - create_tables: yes - title: "TAXII2 Server" - public_discovery: true - max_content_length: 209715200 - -auth_api: - class: opentaxii.auth.sqldb.SQLDatabaseAPI - parameters: - db_connection: sqlite:////data/auth.db - create_tables: yes - secret: not-secret - -taxii1: - -logging: - opentaxii: info - root: info +domain: "localhost:9000" + +taxii2: + persistence_api: + class: opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI + parameters: + db_connection: sqlite:////data/data.db + create_tables: yes + title: "TAXII2 Server" + public_discovery: true + max_content_length: 209715200 + +auth_api: + class: opentaxii.auth.sqldb.SQLDatabaseAPI + parameters: + db_connection: sqlite:////data/auth.db + create_tables: yes + secret: not-secret + +taxii1: + +logging: + opentaxii: info + root: info diff --git a/opentaxii/auth/sqldb/api.py b/opentaxii/auth/sqldb/api.py index 0501c23b..cd79b7fb 100644 --- a/opentaxii/auth/sqldb/api.py +++ b/opentaxii/auth/sqldb/api.py @@ -68,7 +68,7 @@ def create_account( ) account.set_password(password) self.db.session.add(account) - self.db.session.commit() + self._commit_with_retry() return account_to_account_entity(account) def get_account(self, token: str) -> AccountEntity | None: @@ -86,7 +86,7 @@ def delete_account(self, username: str): ) if account: self.db.session.delete(account) - self.db.session.commit() + self._commit_with_retry() def get_accounts(self): return [ @@ -109,7 +109,7 @@ def update_account( account.set_password(password) account.permissions = obj.permissions account.is_admin = obj.is_admin - self.db.session.commit() + self._commit_with_retry() return account_to_account_entity(account) def _generate_token(self, account_id, ttl=None): diff --git a/opentaxii/common/sqldb.py b/opentaxii/common/sqldb.py index a77cb872..4a38a825 100644 --- a/opentaxii/common/sqldb.py +++ b/opentaxii/common/sqldb.py @@ -1,5 +1,8 @@ +import time from typing import ClassVar, Type +from sqlalchemy.exc import OperationalError + from opentaxii.sqldb_helper import SQLAlchemyDB try: @@ -27,3 +30,42 @@ def __init__(self, db_connection, create_tables=False, **engine_parameters): def init_app(self, app): self.db.init_app(app) + + def _commit_with_retry(self, max_attempts=3, base_delay=0.05): + attempts = 0 + while True: + try: + self.db.session.commit() + return + except OperationalError as error: + self.db.session.rollback() + attempts += 1 + pgcode = str(getattr(error.orig, "pgcode", "") or "") + if pgcode not in ("40001", "40P01") or attempts >= max_attempts: + raise + time.sleep(base_delay * attempts) + except Exception: + self.db.session.rollback() + raise + finally: + self.db.session.remove() + + def _run_with_retry(self, callback, max_attempts=3, base_delay=0.05): + attempts = 0 + while True: + try: + result = callback() + self.db.session.commit() + return result + except OperationalError as error: + self.db.session.rollback() + attempts += 1 + pgcode = str(getattr(error.orig, "pgcode", "") or "") + if pgcode not in ("40001", "40P01") or attempts >= max_attempts: + raise + time.sleep(base_delay * attempts) + except Exception: + self.db.session.rollback() + raise + finally: + self.db.session.remove() diff --git a/opentaxii/defaults.yml b/opentaxii/defaults.yml index e7ab04b9..f964a403 100644 --- a/opentaxii/defaults.yml +++ b/opentaxii/defaults.yml @@ -31,6 +31,7 @@ taxii2: parameters: db_connection: sqlite:////tmp/data.db create_tables: yes + upsert_batch_size: 1000 title: "TAXII2 Server" public_discovery: true max_content_length: 209715200 diff --git a/opentaxii/middleware.py b/opentaxii/middleware.py index 16c6067e..f8f1030b 100644 --- a/opentaxii/middleware.py +++ b/opentaxii/middleware.py @@ -8,6 +8,7 @@ from .exceptions import InvalidAuthHeader from .local import context, release_context from .management import management +from .sqldb_helper import bind_session_scope, unbind_session_scope from .taxii.exceptions import StatusMessageException from .taxii.http import HTTP_AUTHORIZATION from .utils import parse_basic_auth_token @@ -45,61 +46,73 @@ def create_app(server): MarshmallowValidationError, server.handle_validation_exception ) app.before_request(functools.partial(create_context_before_request, server)) - app.after_request(cleanup_context) + app.teardown_request(cleanup_context) return app def create_context_before_request(server): + context.db_session_scope_token = bind_session_scope() context.account = _authenticate(server, request.headers) context.server = server -def cleanup_context(response): +def cleanup_context(exc): + _cleanup_db_and_context() + return None + + +def _cleanup_db_and_context(): + if getattr(context, "db_cleanup_done", False): + return + context.db_cleanup_done = True + if hasattr(context, "db_session_scope_token"): + unbind_session_scope(context.db_session_scope_token) release_context() - return response def _authenticate(server, headers): + try: + auth_header = headers.get(HTTP_AUTHORIZATION) + if not auth_header: + return None - auth_header = headers.get(HTTP_AUTHORIZATION) - if not auth_header: - return None - - parts = auth_header.split(" ", 1) + parts = auth_header.split(" ", 1) - if len(parts) != 2: - log.warning("auth.header_invalid", value=auth_header) - return None + if len(parts) != 2: + log.warning("auth.header_invalid", value=auth_header) + return None - auth_type, raw_token = parts - auth_type = auth_type.lower() + auth_type, raw_token = parts + auth_type = auth_type.lower() - if auth_type == "basic": + if auth_type == "basic": - if not server.is_basic_auth_supported(): - server.raise_unauthorized() + if not server.is_basic_auth_supported(): + server.raise_unauthorized() - try: - username, password = parse_basic_auth_token(raw_token) - except InvalidAuthHeader: - log.error( - "auth.basic_auth.header_invalid", raw_token=raw_token, exc_info=True - ) - return None + try: + username, password = parse_basic_auth_token(raw_token) + except InvalidAuthHeader: + log.error( + "auth.basic_auth.header_invalid", raw_token=raw_token, exc_info=True + ) + return None - token = server.auth.authenticate(username, password) + token = server.auth.authenticate(username, password) - elif auth_type == "bearer": - token = raw_token - else: - server.raise_unauthorized() + elif auth_type == "bearer": + token = raw_token + else: + server.raise_unauthorized() - if not token: - server.raise_unauthorized() + if not token: + server.raise_unauthorized() - account = server.auth.get_account(token) + account = server.auth.get_account(token) - if not account: - server.raise_unauthorized() + if not account: + server.raise_unauthorized() - return account + return account + finally: + server.auth.api.db.session.remove() diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index f98a9da9..f190822e 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -8,6 +8,7 @@ import six import sqlalchemy import structlog +from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy import and_, func, literal, or_ from sqlalchemy.orm import Query, aliased, load_only @@ -28,11 +29,12 @@ Subscription, ) -__all__ = ["SQLDatabaseAPI"] +__all__ = ["SQLDatabaseAPI", "Taxii2SQLDatabaseAPI"] log = structlog.getLogger(__name__) YIELD_PER_SIZE = 100 +DEFAULT_UPSERT_BATCH_SIZE = 1000 class SQLDatabaseAPI(BaseSQLDatabaseAPI, OpenTAXIIPersistenceAPI): @@ -76,7 +78,7 @@ def update_service(self, obj): id=obj.id, type=obj.type, properties=obj.properties ) self.db.session.add(service) - self.db.session.commit() + self._commit_with_retry() return conv.to_service_entity(service) def create_service(self, entity): @@ -119,7 +121,7 @@ def update_collection(self, entity): collection.available = entity.available collection.accept_all_content = entity.accept_all_content collection.bindings = _bindings - self.db.session.commit() + self._commit_with_retry() return conv.to_collection_entity(collection) def delete_collection(self, collection_name): @@ -129,12 +131,12 @@ def delete_collection(self, collection_name): .one() ) self.db.session.delete(collection) - self.db.session.commit() + self._commit_with_retry() def delete_service(self, service_id): service = self.db.session.query(Service).get(service_id) self.db.session.delete(service) - self.db.session.commit() + self._commit_with_retry() def _get_content_query( self, @@ -231,7 +233,7 @@ def create_collection(self, entity): ) self.db.session.add(collection) - self.db.session.commit() + self._commit_with_retry() return conv.to_collection_entity(collection) @@ -253,7 +255,7 @@ def set_collection_services(self, collection_id, service_ids): ) collection.services = services self.db.session.add(collection) - self.db.session.commit() + self._commit_with_retry() log.debug( "collection.services_set", id=collection.id, @@ -293,7 +295,7 @@ def create_inbox_message(self, entity): ) self.db.session.add(message) - self.db.session.commit() + self._commit_with_retry() return conv.to_inbox_message_entity(message) @@ -325,7 +327,7 @@ def create_content_block(self, entity, collection_ids=None, service_id=None): ) self.db.session.add(content) - self.db.session.commit() + self._commit_with_retry() if collection_ids: self._attach_content_to_collections(content, collection_ids) @@ -350,7 +352,7 @@ def _attach_content_to_collections(self, content_block, collection_ids): collections=new_collections.count(), ) - self.db.session.commit() + self._commit_with_retry() def create_result_set(self, entity): @@ -365,7 +367,7 @@ def create_result_set(self, entity): ) self.db.session.add(result_set) - self.db.session.commit() + self._commit_with_retry() return conv.to_result_set_entity(result_set) @@ -414,7 +416,7 @@ def update_subscription(self, entity): ) self.db.session.add(subscription) - self.db.session.commit() + self._commit_with_retry() log.debug( "subscription.updated", @@ -488,7 +490,7 @@ def delete_content_blocks( .filter(DataCollection.id == collection.id) ).scalar() - self.db.session.commit() + self._commit_with_retry() return counter @@ -496,6 +498,16 @@ def delete_content_blocks( class Taxii2SQLDatabaseAPI(BaseSQLDatabaseAPI, OpenTAXII2PersistenceAPI): BASEMODEL = taxii2models.Base + def __init__( + self, + db_connection, + create_tables=False, + upsert_batch_size=DEFAULT_UPSERT_BATCH_SIZE, + **engine_parameters, + ): + super().__init__(db_connection, create_tables, **engine_parameters) + self.upsert_batch_size = max(1, int(upsert_batch_size)) + @staticmethod def get_next_param(kwargs: Dict) -> str: """ @@ -581,7 +593,7 @@ def add_api_root( is_public=is_public, ) self.db.session.add(api_root) - self.db.session.commit() + self._commit_with_retry() if default: api_root.set_default(self.db.session) return entities.ApiRoot( @@ -735,7 +747,7 @@ def add_collection( is_public_write=is_public_write, ) self.db.session.add(collection) - self.db.session.commit() + self._commit_with_retry() return entities.Collection( id=collection.id, @@ -976,64 +988,88 @@ def get_objects( def add_objects( self, api_root_id: uuid.UUID, collection_id: uuid.UUID, objects: List[Dict] ) -> entities.Job: - job = taxii2models.Job( - api_root_id=api_root_id, - status="pending", - request_timestamp=datetime.datetime.now(datetime.timezone.utc), - total_count=0, - success_count=0, - failure_count=0, - pending_count=0, - ) - self.db.session.add(job) - self.db.session.commit() - job_details = [] - for obj in objects: - version = get_object_version(obj) - if ( - not self.db.session.query(literal(True)) - .filter( - self.db.session.query(taxii2models.STIXObject) - .filter( - taxii2models.STIXObject.id == obj["id"], - taxii2models.STIXObject.collection_id == collection_id, - taxii2models.STIXObject.version == version, - ) - .exists() - ) - .scalar() - ): - self.db.session.add( - taxii2models.STIXObject( - id=obj["id"], - collection_id=collection_id, - type=obj["id"].split("--")[0], - spec_version=obj["spec_version"], - date_added=datetime.datetime.now(datetime.timezone.utc), - version=version, - serialized_data={ + def _work(): + now = datetime.datetime.now(datetime.timezone.utc) + job = taxii2models.Job( + api_root_id=api_root_id, + status="pending", + request_timestamp=now, + completed_timestamp=None, + total_count=0, + success_count=0, + failure_count=0, + pending_count=0, + ) + self.db.session.add(job) + self.db.session.flush() + total_count = 0 + success_count = 0 + job_details = [] + stix_rows = [] + stix_exclude_keys = ("id", "type", "spec_version") + for obj in objects: + version = get_object_version(obj) + stix_rows.append( + { + "id": obj["id"], + "collection_id": collection_id, + "type": obj["id"].split("--")[0], + "spec_version": obj["spec_version"], + "date_added": now, + "version": version, + "serialized_data": { key: value for (key, value) in obj.items() - if key not in ["id", "type", "spec_version"] + if key not in stix_exclude_keys }, - ) + } ) - job_detail = taxii2models.JobDetail( - job_id=job.id, - stix_id=obj["id"], - version=version, - message="", - status="success", + job_detail = taxii2models.JobDetail( + job_id=job.id, + stix_id=obj["id"], + version=version, + message="", + status="success", + ) + job_details.append(job_detail) + self.db.session.add(job_detail) + total_count += 1 + success_count += 1 + + if stix_rows: + for start in range(0, len(stix_rows), self.upsert_batch_size): + batch = stix_rows[start : start + self.upsert_batch_size] + insert_stmt = pg_insert(taxii2models.STIXObject).values(batch) + insert_stmt = insert_stmt.on_conflict_do_nothing( + index_elements=["id", "collection_id", "version"] + ) + self.db.session.execute(insert_stmt) + + completed_timestamp = now + self.db.session.query(taxii2models.Job).filter( + taxii2models.Job.id == job.id + ).update( + { + taxii2models.Job.status: "complete", + taxii2models.Job.completed_timestamp: completed_timestamp, + taxii2models.Job.total_count: total_count, + taxii2models.Job.success_count: success_count, + taxii2models.Job.failure_count: 0, + taxii2models.Job.pending_count: 0, + }, + synchronize_session=False, ) - job_details.append(job_detail) - self.db.session.add(job_detail) - job.total_count += 1 # type: ignore[operator] - job.success_count += 1 # type: ignore[operator] - job.status = "complete" - job.completed_timestamp = datetime.datetime.now(datetime.timezone.utc) - self.db.session.commit() - job_entity = self._job_and_details_to_entity(job, job_details) - return job_entity + + job.status = "complete" + job.completed_timestamp = completed_timestamp + job.total_count = total_count + job.success_count = success_count + job.failure_count = 0 + job.pending_count = 0 + self.db.session.flush() + return self._job_and_details_to_entity(job, job_details) + + return self._run_with_retry(_work) def get_object( self, @@ -1113,7 +1149,7 @@ def delete_object( ordered=False, ) query.delete("fetch") - self.db.session.commit() + self._commit_with_retry() def get_versions( self, diff --git a/opentaxii/sqldb_helper.py b/opentaxii/sqldb_helper.py index 8d18e78f..5614abaa 100644 --- a/opentaxii/sqldb_helper.py +++ b/opentaxii/sqldb_helper.py @@ -1,9 +1,32 @@ +from contextvars import ContextVar from threading import get_ident from sqlalchemy import engine, orm from sqlalchemy.orm.exc import UnmappedClassError +_session_scope_id: ContextVar[object | None] = ContextVar( + "opentaxii_session_scope_id", default=None +) + + +def get_session_scope_id(): + scope_id = _session_scope_id.get() + if scope_id is not None: + return scope_id + return get_ident() + + +def bind_session_scope(scope_id=None): + if scope_id is None: + scope_id = object() + return _session_scope_id.set(scope_id) + + +def unbind_session_scope(token): + _session_scope_id.reset(token) + + class _QueryProperty: def __init__(self, sa): self.sa = sa @@ -54,7 +77,9 @@ def create_scoped_session(self, options=None): options.setdefault('query_cls', self.Query) - return orm.scoped_session(self.create_session(options), scopefunc=get_ident) + return orm.scoped_session( + self.create_session(options), scopefunc=get_session_scope_id + ) def create_session(self, options): kwargs = { diff --git a/pyproject.toml b/pyproject.toml index f3e0451b..c1a20df8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,45 +1,45 @@ -[tool.interrogate] -ignore-init-method = true -ignore-init-module = true -ignore-magic = true -ignore-module = true -ignore-nested-functions = true -ignore-nested-classes = true -ignore-private = true -ignore-semiprivate = true -omit-covered-files = true -verbose = 0 -exclude = ["tests"] - -[tool.mypy] -plugins = ["sqlmypy"] - -ignore_missing_imports = true -no_implicit_optional = true -show_error_codes = true -warn_redundant_casts = true -warn_unused_configs = true -warn_unused_ignores = true -check_untyped_defs = true -follow_imports = "silent" - -files = [ - "opentaxii/", - "tests/", -] - -exclude = """ -(?x)( - ^opentaxii/taxii/.+ - | ^opentaxii/utils\\.py$ - | ^opentaxii/common/entities\\.py$ -) -""" - -[tool.black] -line-length = 88 -skip_string_normalization = true -target_version = ["py310", "py311"] - -[tool.isort] -profile = "black" +[tool.interrogate] +ignore-init-method = true +ignore-init-module = true +ignore-magic = true +ignore-module = true +ignore-nested-functions = true +ignore-nested-classes = true +ignore-private = true +ignore-semiprivate = true +omit-covered-files = true +verbose = 0 +exclude = ["tests"] + +[tool.mypy] +plugins = ["sqlmypy"] + +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +check_untyped_defs = true +follow_imports = "silent" + +files = [ + "opentaxii/", + "tests/", +] + +exclude = """ +(?x)( + ^opentaxii/taxii/.+ + | ^opentaxii/utils\\.py$ + | ^opentaxii/common/entities\\.py$ +) +""" + +[tool.black] +line-length = 88 +skip_string_normalization = true +target_version = ["py310", "py311"] + +[tool.isort] +profile = "black" diff --git a/pytest.ini b/pytest.ini index f3bbb73e..ce64150e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,10 +1,10 @@ -[pytest] -addopts = - --verbose - --showlocals - --cov=opentaxii - --cov-config .coveragerc - --cov-report term-missing - --cov-report html -markers = - truncate: marks tests as needing database truncate (no transactions) +[pytest] +addopts = + --verbose + --showlocals + --cov=opentaxii + --cov-config .coveragerc + --cov-report term-missing + --cov-report html +markers = + truncate: marks tests as needing database truncate (no transactions) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6444a695..663ec257 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,13 +1,13 @@ --r requirements.txt --r requirements-test.txt -flake8 -ipdb -mypy==1.19.0 -types-six==1.17.0.20251009 -sqlalchemy-stubs==0.4 -types-PyYAML==6.0.12.20250915 -types-pytz==2025.2.0.20251108 -factory-boy>=3.2.1 -black==25.11.0 -isort==7.0.0 --r requirements-interrogate.txt +-r requirements.txt +-r requirements-test.txt +flake8 +ipdb +mypy==1.19.0 +types-six==1.17.0.20251009 +sqlalchemy-stubs==0.4 +types-PyYAML==6.0.12.20250915 +types-pytz==2025.2.0.20251108 +factory-boy>=3.2.1 +black==25.11.0 +isort==7.0.0 +-r requirements-interrogate.txt diff --git a/requirements-docker.txt b/requirements-docker.txt index 4d92d5db..984129ec 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,3 +1,3 @@ -gunicorn==22.0.0 -mysqlclient>=2.0.3 -psycopg2-binary>=2.9.1 +gunicorn==22.0.0 +mysqlclient>=2.0.3 +psycopg2-binary>=2.9.1 diff --git a/requirements-interrogate.txt b/requirements-interrogate.txt index 2414ddc4..08ccd9a0 100644 --- a/requirements-interrogate.txt +++ b/requirements-interrogate.txt @@ -1 +1 @@ -interrogate>=1.5.0 +interrogate>=1.5.0 diff --git a/requirements-mysql.txt b/requirements-mysql.txt index c9dc3f58..fd38e867 100644 --- a/requirements-mysql.txt +++ b/requirements-mysql.txt @@ -1 +1 @@ -mysqlclient>=2.0.3 +mysqlclient>=2.0.3 diff --git a/requirements-postgres-pypy.txt b/requirements-postgres-pypy.txt index 102e1160..7695daf7 100644 --- a/requirements-postgres-pypy.txt +++ b/requirements-postgres-pypy.txt @@ -1 +1 @@ -psycopg2cffi>=2.9.0 +psycopg2cffi>=2.9.0 diff --git a/requirements-postgres.txt b/requirements-postgres.txt index 1197662d..8826c3a3 100644 --- a/requirements-postgres.txt +++ b/requirements-postgres.txt @@ -1 +1 @@ -psycopg2-binary>=2.9.1 +psycopg2-binary>=2.9.1 diff --git a/requirements-test.txt b/requirements-test.txt index 3c94f223..9840503d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -pytest-cov -pytest>=4.6 -pytest-pythonpath -factory-boy>=3.2.1 +pytest-cov +pytest>=4.6 +pytest-pythonpath +factory-boy>=3.2.1 diff --git a/requirements.txt b/requirements.txt index d303d576..076a1ee6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ -pytz>=2017.2 -libtaxii>=1.1.111 -lxml>=4.3.1 -pyyaml>=3.11 -flask>=0.10.1 -sqlalchemy>=1.4,<2.0 -structlog>=18.1.0 -blinker>=1.4 -pyjwt>=1.4.0 -six>=1.10.0 -stix2>=3.0.1 -marshmallow>=3.10.0 +pytz>=2017.2 +libtaxii>=1.1.111 +lxml>=4.3.1 +pyyaml>=3.11 +flask>=0.10.1 +sqlalchemy>=1.4,<2.0 +structlog>=18.1.0 +blinker>=1.4 +pyjwt>=1.4.0 +six>=1.10.0 +stix2>=3.0.1 +marshmallow>=3.10.0 diff --git a/setup.cfg b/setup.cfg index 87d1e764..610097e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ -[metadata] -description_file = README.md -license_file = LICENSE.rst +[metadata] +description_file = README.md +license_file = LICENSE.rst diff --git a/setup.py b/setup.py index 22eda993..4e6cb499 100644 --- a/setup.py +++ b/setup.py @@ -1,64 +1,64 @@ -import os - -from setuptools import find_packages, setup - -__version__ = None -exec(open('opentaxii/_version.py').read()) - - -def here(*path): - return os.path.join(os.path.dirname(__file__), *path) - - -def get_file_contents(filename): - with open(here(filename), encoding='utf8') as fp: - return fp.read() - - -# This is a quick and dirty way to include everything from -# requirements.txt as package dependencies. -install_requires = get_file_contents('requirements.txt').split() - -setup( - name='opentaxii', - description='TAXII server implementation in Python from EclecticIQ', - long_description=get_file_contents('README.md'), - long_description_content_type='text/markdown', - url='https://github.com/EclecticIQ/OpenTAXII', - author='EclecticIQ', - author_email='opentaxii@eclecticiq.com', - version=__version__, - license='BSD License', - packages=find_packages(exclude=['tests']), - include_package_data=True, - package_data={ - 'opentaxii': ['*.yml'] - }, - entry_points={ - 'console_scripts': [ - 'opentaxii-run-dev = opentaxii.cli.run:run_in_dev_mode', - 'opentaxii-create-account = opentaxii.cli.auth:create_account', - 'opentaxii-update-account = opentaxii.cli.auth:update_account', - 'opentaxii-sync-data = opentaxii.cli.persistence:sync_data_configuration', - 'opentaxii-delete-blocks = opentaxii.cli.persistence:delete_content_blocks', - 'opentaxii-add-api-root = opentaxii.cli.persistence:add_api_root', - 'opentaxii-add-collection = opentaxii.cli.persistence:add_collection', - 'opentaxii-job-cleanup = opentaxii.cli.persistence:job_cleanup', - ] - }, - install_requires=install_requires, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Internet', - 'Topic :: Security', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] -) +import os + +from setuptools import find_packages, setup + +__version__ = None +exec(open('opentaxii/_version.py').read()) + + +def here(*path): + return os.path.join(os.path.dirname(__file__), *path) + + +def get_file_contents(filename): + with open(here(filename), encoding='utf8') as fp: + return fp.read() + + +# This is a quick and dirty way to include everything from +# requirements.txt as package dependencies. +install_requires = get_file_contents('requirements.txt').split() + +setup( + name='opentaxii', + description='TAXII server implementation in Python from EclecticIQ', + long_description=get_file_contents('README.md'), + long_description_content_type='text/markdown', + url='https://github.com/EclecticIQ/OpenTAXII', + author='EclecticIQ', + author_email='opentaxii@eclecticiq.com', + version=__version__, + license='BSD License', + packages=find_packages(exclude=['tests']), + include_package_data=True, + package_data={ + 'opentaxii': ['*.yml'] + }, + entry_points={ + 'console_scripts': [ + 'opentaxii-run-dev = opentaxii.cli.run:run_in_dev_mode', + 'opentaxii-create-account = opentaxii.cli.auth:create_account', + 'opentaxii-update-account = opentaxii.cli.auth:update_account', + 'opentaxii-sync-data = opentaxii.cli.persistence:sync_data_configuration', + 'opentaxii-delete-blocks = opentaxii.cli.persistence:delete_content_blocks', + 'opentaxii-add-api-root = opentaxii.cli.persistence:add_api_root', + 'opentaxii-add-collection = opentaxii.cli.persistence:add_collection', + 'opentaxii-job-cleanup = opentaxii.cli.persistence:job_cleanup', + ] + }, + install_requires=install_requires, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Internet', + 'Topic :: Security', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] +) diff --git a/tests/conftest.py b/tests/conftest.py index 3f155ab5..7f65dd4c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,364 +1,364 @@ -import base64 -import os -from tempfile import mkstemp -from unittest.mock import patch - -import pytest -from flask.testing import FlaskClient - -from opentaxii.config import ServerConfig -from opentaxii.local import context, release_context -from opentaxii.middleware import create_app -from opentaxii.persistence.sqldb.taxii2models import ( - ApiRoot, - Collection, - Job, - JobDetail, - STIXObject, -) -from opentaxii.server import TAXIIServer -from opentaxii.taxii.converters import dict_to_service_entity -from opentaxii.taxii.http import HTTP_AUTHORIZATION -from opentaxii.utils import configure_logging -from tests.fixtures import ( - ACCOUNT, - COLLECTIONS_B, - DOMAIN, - PASSWORD, - SERVICES, - USERNAME, - VALID_TOKEN, -) -from tests.taxii2.utils import API_ROOTS, COLLECTIONS, JOBS, STIX_OBJECTS - - -class CustomClient(FlaskClient): - def __init__(self, *args, **kwargs): - self.headers = kwargs.pop("headers", {}) - super().__init__(*args, **kwargs) - - def open(self, *args, **kwargs): - headers = kwargs.pop("headers", {}) - new_headers = {**self.headers, **headers} - if new_headers: - kwargs["headers"] = new_headers - return super().open(*args, **kwargs) - - -DBTYPE = os.getenv("DBTYPE", "sqlite") -if DBTYPE == "sqlite": - - @pytest.fixture(scope="session") - def dbconn(): - filehandle, filename = mkstemp(suffix=".db") - os.close(filehandle) - try: - yield f"sqlite:///{filename}" - finally: - try: - os.remove(filename) - except FileNotFoundError: - pass - -elif DBTYPE in ("mysql", "mariadb"): - import MySQLdb - - @pytest.fixture(scope="session") - def dbconn(): - # drop db if exists to provide clean state at beginning - if DBTYPE == "mysql": - port = 3306 - elif DBTYPE == "mariadb": - port = 3307 - yield f"mysql+mysqldb://root:@127.0.0.1:{port}/test?charset=utf8" - -elif DBTYPE == "postgres": - import platform - - if platform.python_implementation() == "PyPy": - from psycopg2cffi import compat - - compat.register() - import psycopg2 - - @pytest.fixture(scope="session") - def dbconn(): - yield "postgresql+psycopg2://test:test@127.0.0.1:5432/test" - -else: - raise NotImplementedError(f"dbtype {DBTYPE} not supported") - - -@pytest.fixture(autouse=True, scope="session") -def setup_logging(): - configure_logging({"": "debug"}) - - -def prepare_test_config(dbconnection): - config = ServerConfig( - extra_configs=[ - { - "auth_api": { - "class": "opentaxii.auth.sqldb.SQLDatabaseAPI", - "parameters": { - "db_connection": dbconnection, - "create_tables": True, - "secret": "dummy-secret-string-for-tests", - }, - }, - "taxii1": { - "persistence_api": { - "class": "opentaxii.persistence.sqldb.SQLDatabaseAPI", - "parameters": { - "db_connection": dbconnection, - "create_tables": True, - }, - }, - }, - "taxii2": { - "persistence_api": { - "class": "opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI", - "parameters": { - "db_connection": dbconnection, - "create_tables": True, - }, - }, - "max_content_length": 1024, - }, - "domain": DOMAIN, - } - ] - ) - return config - - -@pytest.fixture -def anonymous_user(): - from opentaxii.server import anonymous_full_access - - context.account = anonymous_full_access - yield - release_context() - - -def clean_db(dbconn): - # drop and recreate db to provide clean state at beginning - if DBTYPE == "sqlite": - filename = dbconn[len("sqlite:///") :] - os.remove(filename) - elif DBTYPE == "postgres": - with psycopg2.connect( - dbname="test", - user="test", - password="test", - host="127.0.0.1", - port="5432", - ) as pg_conn: - with pg_conn.cursor() as pg_cur: - pg_cur.execute( - "DROP SCHEMA public CASCADE;" - "CREATE SCHEMA public;" - "GRANT ALL ON SCHEMA public TO test;" - "GRANT ALL ON SCHEMA public TO public;" - ) - pg_conn.close() # pypy + psycopg2cffi needs this, doesn't close conn otherwise - elif DBTYPE in ("mysql", "mariadb"): - dbname = "test" - if DBTYPE == "mysql": - port = 3306 - elif DBTYPE == "mariadb": - port = 3307 - connection_kwargs = { - "host": "127.0.0.1", - "user": "root", - "passwd": "", - "port": port, - } - with MySQLdb.connect(**connection_kwargs) as mysql_conn: - mysql_conn.query(f"DROP DATABASE IF EXISTS {dbname}") - mysql_conn.query( - f"CREATE DATABASE {dbname} " - f"DEFAULT CHARACTER SET utf8 " - f"DEFAULT COLLATE utf8_general_ci" - ) - - -@pytest.fixture(scope="session") -def session_taxiiserver(dbconn): - clean_db(dbconn) - yield TAXIIServer(prepare_test_config(dbconn)) - - -@pytest.fixture() -def app(request, dbconn, session_taxiiserver): - truncate = request.node.get_closest_marker("truncate") or DBTYPE == "sqlite" - if truncate: - yield from truncate_app(dbconn) - else: - yield from transaction_app(dbconn, session_taxiiserver) - - -def transaction_app(dbconn, taxiiserver): - context.server = taxiiserver - app = create_app(context.server) - app.config["TESTING"] = True - managers = [taxiiserver.auth] + [ - subserver.persistence for subserver in taxiiserver.servers - ] - transactions = [] - connections = [] - sessions = [] - for manager in managers: - connection = manager.api.db.engine.connect() - transaction = connection.begin() - manager.api.db.session_options["bind"] = connection - transactions.append(transaction) - connections.append(connection) - sessions.append(manager.api.db.session) - yield app - for transaction, connection, session, manager in zip( - transactions, connections, sessions, managers - ): - transaction.rollback() - connection.close() - session.remove() - manager.api.db._session = None - - -def truncate_app(dbconn): - clean_db(dbconn) - taxiiserver = TAXIIServer(prepare_test_config(dbconn)) - context.server = taxiiserver - app = create_app(context.server) - app.config["TESTING"] = True - yield app - taxiiserver.servers.taxii1.persistence.api.db.engine.dispose() # type: ignore[union-attr] - taxiiserver.servers.taxii2.persistence.api.db.engine.dispose() # type: ignore[union-attr] - - -@pytest.fixture() -def taxii2_sqldb_api(app): - yield app.taxii_server.servers.taxii2.persistence.api - - -@pytest.fixture() -def server(app, anonymous_user): - yield app.taxii_server - release_context() - - -@pytest.fixture() -def client(app): - app.test_client_class = CustomClient - return app.test_client() - - -def basic_auth_token(username, password): - return base64.b64encode("{}:{}".format(username, password).encode("utf-8")) - - -def MOCK_AUTHENTICATE(username, password): - if username == USERNAME and password == PASSWORD: - return VALID_TOKEN - return None - - -def MOCK_GET_ACCOUNT(token): - if token == VALID_TOKEN: - return ACCOUNT - return None - - -@pytest.fixture() -def authenticated_client(client): - basic_auth_header = "Basic {}".format( - basic_auth_token(USERNAME, PASSWORD).decode("utf-8") - ) - headers = { - HTTP_AUTHORIZATION: basic_auth_header, - } - client.headers = headers - client.account = ACCOUNT - with ( - patch.object( - client.application.taxii_server.auth.api, - "authenticate", - side_effect=MOCK_AUTHENTICATE, - ), - patch.object( - client.application.taxii_server.auth.api, - "get_account", - side_effect=MOCK_GET_ACCOUNT, - ), - ): - yield client - - -@pytest.fixture() -def services(server): - for service in SERVICES: - server.servers.taxii1.persistence.update_service( - dict_to_service_entity(service) - ) - - -@pytest.fixture() -def collections(server): - for collection in COLLECTIONS_B: - server.servers.taxii1.persistence.create_collection(collection) - - -@pytest.fixture() -def account(server): - server.auth.api.create_account(ACCOUNT.username, "mypass") - - -@pytest.fixture(scope="function") -def db_api_roots(request, taxii2_sqldb_api): - try: - api_roots = request.param - except AttributeError: - api_roots = API_ROOTS - for api_root in api_roots: - taxii2_sqldb_api.db.session.add(ApiRoot.from_entity(api_root)) - taxii2_sqldb_api.db.session.commit() - yield api_roots - - -@pytest.fixture(scope="function") -def db_jobs(request, taxii2_sqldb_api, db_api_roots): - try: - jobs = request.param - except AttributeError: - jobs = JOBS - for job in jobs: - taxii2_sqldb_api.db.session.add(Job.from_entity(job)) - for job_details in job.details: - for job_detail in job_details: - taxii2_sqldb_api.db.session.add(JobDetail.from_entity(job_detail)) - taxii2_sqldb_api.db.session.commit() - yield jobs - - -@pytest.fixture(scope="function") -def db_collections(request, taxii2_sqldb_api, db_api_roots): - try: - collections = request.param - except AttributeError: - collections = COLLECTIONS - for collection in collections: - taxii2_sqldb_api.db.session.add(Collection.from_entity(collection)) - taxii2_sqldb_api.db.session.commit() - yield collections - - -@pytest.fixture(scope="function") -def db_stix_objects(request, taxii2_sqldb_api, db_collections): - try: - stix_objects = request.param - except AttributeError: - stix_objects = STIX_OBJECTS - for stix_object in stix_objects: - taxii2_sqldb_api.db.session.add(STIXObject.from_entity(stix_object)) - taxii2_sqldb_api.db.session.commit() - yield stix_objects +import base64 +import os +from tempfile import mkstemp +from unittest.mock import patch + +import pytest +from flask.testing import FlaskClient + +from opentaxii.config import ServerConfig +from opentaxii.local import context, release_context +from opentaxii.middleware import create_app +from opentaxii.persistence.sqldb.taxii2models import ( + ApiRoot, + Collection, + Job, + JobDetail, + STIXObject, +) +from opentaxii.server import TAXIIServer +from opentaxii.taxii.converters import dict_to_service_entity +from opentaxii.taxii.http import HTTP_AUTHORIZATION +from opentaxii.utils import configure_logging +from tests.fixtures import ( + ACCOUNT, + COLLECTIONS_B, + DOMAIN, + PASSWORD, + SERVICES, + USERNAME, + VALID_TOKEN, +) +from tests.taxii2.utils import API_ROOTS, COLLECTIONS, JOBS, STIX_OBJECTS + + +class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self.headers = kwargs.pop("headers", {}) + super().__init__(*args, **kwargs) + + def open(self, *args, **kwargs): + headers = kwargs.pop("headers", {}) + new_headers = {**self.headers, **headers} + if new_headers: + kwargs["headers"] = new_headers + return super().open(*args, **kwargs) + + +DBTYPE = os.getenv("DBTYPE", "sqlite") +if DBTYPE == "sqlite": + + @pytest.fixture(scope="session") + def dbconn(): + filehandle, filename = mkstemp(suffix=".db") + os.close(filehandle) + try: + yield f"sqlite:///{filename}" + finally: + try: + os.remove(filename) + except FileNotFoundError: + pass + +elif DBTYPE in ("mysql", "mariadb"): + import MySQLdb + + @pytest.fixture(scope="session") + def dbconn(): + # drop db if exists to provide clean state at beginning + if DBTYPE == "mysql": + port = 3306 + elif DBTYPE == "mariadb": + port = 3307 + yield f"mysql+mysqldb://root:@127.0.0.1:{port}/test?charset=utf8" + +elif DBTYPE == "postgres": + import platform + + if platform.python_implementation() == "PyPy": + from psycopg2cffi import compat + + compat.register() + import psycopg2 + + @pytest.fixture(scope="session") + def dbconn(): + yield "postgresql+psycopg2://test:test@127.0.0.1:5432/test" + +else: + raise NotImplementedError(f"dbtype {DBTYPE} not supported") + + +@pytest.fixture(autouse=True, scope="session") +def setup_logging(): + configure_logging({"": "debug"}) + + +def prepare_test_config(dbconnection): + config = ServerConfig( + extra_configs=[ + { + "auth_api": { + "class": "opentaxii.auth.sqldb.SQLDatabaseAPI", + "parameters": { + "db_connection": dbconnection, + "create_tables": True, + "secret": "dummy-secret-string-for-tests", + }, + }, + "taxii1": { + "persistence_api": { + "class": "opentaxii.persistence.sqldb.SQLDatabaseAPI", + "parameters": { + "db_connection": dbconnection, + "create_tables": True, + }, + }, + }, + "taxii2": { + "persistence_api": { + "class": "opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI", + "parameters": { + "db_connection": dbconnection, + "create_tables": True, + }, + }, + "max_content_length": 1024, + }, + "domain": DOMAIN, + } + ] + ) + return config + + +@pytest.fixture +def anonymous_user(): + from opentaxii.server import anonymous_full_access + + context.account = anonymous_full_access + yield + release_context() + + +def clean_db(dbconn): + # drop and recreate db to provide clean state at beginning + if DBTYPE == "sqlite": + filename = dbconn[len("sqlite:///") :] + os.remove(filename) + elif DBTYPE == "postgres": + with psycopg2.connect( + dbname="test", + user="test", + password="test", + host="127.0.0.1", + port="5432", + ) as pg_conn: + with pg_conn.cursor() as pg_cur: + pg_cur.execute( + "DROP SCHEMA public CASCADE;" + "CREATE SCHEMA public;" + "GRANT ALL ON SCHEMA public TO test;" + "GRANT ALL ON SCHEMA public TO public;" + ) + pg_conn.close() # pypy + psycopg2cffi needs this, doesn't close conn otherwise + elif DBTYPE in ("mysql", "mariadb"): + dbname = "test" + if DBTYPE == "mysql": + port = 3306 + elif DBTYPE == "mariadb": + port = 3307 + connection_kwargs = { + "host": "127.0.0.1", + "user": "root", + "passwd": "", + "port": port, + } + with MySQLdb.connect(**connection_kwargs) as mysql_conn: + mysql_conn.query(f"DROP DATABASE IF EXISTS {dbname}") + mysql_conn.query( + f"CREATE DATABASE {dbname} " + f"DEFAULT CHARACTER SET utf8 " + f"DEFAULT COLLATE utf8_general_ci" + ) + + +@pytest.fixture(scope="session") +def session_taxiiserver(dbconn): + clean_db(dbconn) + yield TAXIIServer(prepare_test_config(dbconn)) + + +@pytest.fixture() +def app(request, dbconn, session_taxiiserver): + truncate = request.node.get_closest_marker("truncate") or DBTYPE == "sqlite" + if truncate: + yield from truncate_app(dbconn) + else: + yield from transaction_app(dbconn, session_taxiiserver) + + +def transaction_app(dbconn, taxiiserver): + context.server = taxiiserver + app = create_app(context.server) + app.config["TESTING"] = True + managers = [taxiiserver.auth] + [ + subserver.persistence for subserver in taxiiserver.servers + ] + transactions = [] + connections = [] + sessions = [] + for manager in managers: + connection = manager.api.db.engine.connect() + transaction = connection.begin() + manager.api.db.session_options["bind"] = connection + transactions.append(transaction) + connections.append(connection) + sessions.append(manager.api.db.session) + yield app + for transaction, connection, session, manager in zip( + transactions, connections, sessions, managers + ): + transaction.rollback() + connection.close() + session.remove() + manager.api.db._session = None + + +def truncate_app(dbconn): + clean_db(dbconn) + taxiiserver = TAXIIServer(prepare_test_config(dbconn)) + context.server = taxiiserver + app = create_app(context.server) + app.config["TESTING"] = True + yield app + taxiiserver.servers.taxii1.persistence.api.db.engine.dispose() # type: ignore[union-attr] + taxiiserver.servers.taxii2.persistence.api.db.engine.dispose() # type: ignore[union-attr] + + +@pytest.fixture() +def taxii2_sqldb_api(app): + yield app.taxii_server.servers.taxii2.persistence.api + + +@pytest.fixture() +def server(app, anonymous_user): + yield app.taxii_server + release_context() + + +@pytest.fixture() +def client(app): + app.test_client_class = CustomClient + return app.test_client() + + +def basic_auth_token(username, password): + return base64.b64encode("{}:{}".format(username, password).encode("utf-8")) + + +def MOCK_AUTHENTICATE(username, password): + if username == USERNAME and password == PASSWORD: + return VALID_TOKEN + return None + + +def MOCK_GET_ACCOUNT(token): + if token == VALID_TOKEN: + return ACCOUNT + return None + + +@pytest.fixture() +def authenticated_client(client): + basic_auth_header = "Basic {}".format( + basic_auth_token(USERNAME, PASSWORD).decode("utf-8") + ) + headers = { + HTTP_AUTHORIZATION: basic_auth_header, + } + client.headers = headers + client.account = ACCOUNT + with ( + patch.object( + client.application.taxii_server.auth.api, + "authenticate", + side_effect=MOCK_AUTHENTICATE, + ), + patch.object( + client.application.taxii_server.auth.api, + "get_account", + side_effect=MOCK_GET_ACCOUNT, + ), + ): + yield client + + +@pytest.fixture() +def services(server): + for service in SERVICES: + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) + + +@pytest.fixture() +def collections(server): + for collection in COLLECTIONS_B: + server.servers.taxii1.persistence.create_collection(collection) + + +@pytest.fixture() +def account(server): + server.auth.api.create_account(ACCOUNT.username, "mypass") + + +@pytest.fixture(scope="function") +def db_api_roots(request, taxii2_sqldb_api): + try: + api_roots = request.param + except AttributeError: + api_roots = API_ROOTS + for api_root in api_roots: + taxii2_sqldb_api.db.session.add(ApiRoot.from_entity(api_root)) + taxii2_sqldb_api.db.session.commit() + yield api_roots + + +@pytest.fixture(scope="function") +def db_jobs(request, taxii2_sqldb_api, db_api_roots): + try: + jobs = request.param + except AttributeError: + jobs = JOBS + for job in jobs: + taxii2_sqldb_api.db.session.add(Job.from_entity(job)) + for job_details in job.details: + for job_detail in job_details: + taxii2_sqldb_api.db.session.add(JobDetail.from_entity(job_detail)) + taxii2_sqldb_api.db.session.commit() + yield jobs + + +@pytest.fixture(scope="function") +def db_collections(request, taxii2_sqldb_api, db_api_roots): + try: + collections = request.param + except AttributeError: + collections = COLLECTIONS + for collection in collections: + taxii2_sqldb_api.db.session.add(Collection.from_entity(collection)) + taxii2_sqldb_api.db.session.commit() + yield collections + + +@pytest.fixture(scope="function") +def db_stix_objects(request, taxii2_sqldb_api, db_collections): + try: + stix_objects = request.param + except AttributeError: + stix_objects = STIX_OBJECTS + for stix_object in stix_objects: + taxii2_sqldb_api.db.session.add(STIXObject.from_entity(stix_object)) + taxii2_sqldb_api.db.session.commit() + yield stix_objects diff --git a/tests/fixtures.py b/tests/fixtures.py index 17958341..0e67a534 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,137 +1,137 @@ -from uuid import uuid4 - -from libtaxii.constants import CB_STIX_XML_111, VID_TAXII_HTTP_10, VID_TAXII_HTTPS_10 - -from opentaxii.entities import Account -from opentaxii.taxii import entities - -PROTOCOL_BINDINGS = [VID_TAXII_HTTP_10, VID_TAXII_HTTPS_10] - -CUSTOM_CONTENT_BINDING = 'custom:content:binding' -INVALID_CONTENT_BINDING = 'invalid:content:binding' - -INBOX_A: dict = dict( - id='inbox-A', - type='inbox', - description='inbox-A description', - destination_collection_required=False, - address='/relative/path/inbox-a', - accept_all_content=True, - protocol_bindings=PROTOCOL_BINDINGS, -) - -INBOX_B: dict = dict( - id='inbox-B', - type='inbox', - description='inbox-B description', - destination_collection_required='yes', - address='/relative/path/inbox-b', - supported_content=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - protocol_bindings=PROTOCOL_BINDINGS, -) - -DISCOVERY_A: dict = dict( - id='discovery-A', - type='discovery', - description='discovery-A description', - address='/relative/path/discovery-a', - advertised_services=[ - 'inbox-A', - 'inbox-B', - 'discovery-A', - 'discovery-B', - 'collection-management-A', - 'poll-A', - ], - protocol_bindings=PROTOCOL_BINDINGS, -) - -DISCOVERY_B: dict = dict( - id='discovery-B', - type='discovery', - description='External discovery-B service', - address='http://something.com/absolute/path/discovery-b', - protocol_bindings=[VID_TAXII_HTTP_10], -) - -SUBSCRIPTION_MESSAGE = 'message about subscription' - -COLLECTION_MANAGEMENT: dict = dict( - id='collection-management-A', - type='collection_management', - description='Collection management description', - address='/relative/path/collection-management', - protocol_bindings=PROTOCOL_BINDINGS, - subscription_message=SUBSCRIPTION_MESSAGE, -) - -POLL_RESULT_SIZE = 20 -POLL_MAX_COUNT = 15 - -POLL: dict = dict( - id='poll-A', - type='poll', - description='Poll service description', - address='/relative/path/poll', - protocol_bindings=PROTOCOL_BINDINGS, - max_result_size=POLL_RESULT_SIZE, - max_result_count=POLL_MAX_COUNT, -) - -DOMAIN = 'www.some-example.local' - -INTERNAL_SERVICES = [INBOX_A, INBOX_B, DISCOVERY_A, COLLECTION_MANAGEMENT, POLL] -SERVICES = INTERNAL_SERVICES + [DISCOVERY_B] - -INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) - -MESSAGE_ID = '123' -CONTENT = 'some-content' - -CONTENT_BINDINGS_ONLY_STIX = [CB_STIX_XML_111] -CONTENT_BINDINGS_STIX_AND_CUSTOM = CONTENT_BINDINGS_ONLY_STIX + [CUSTOM_CONTENT_BINDING] -CONTENT_BINDING_SUBTYPE = 'custom-subtype' - -MESSAGE = 'test-message' - - -COLLECTION_OPEN = "collection_open" -COLLECTION_ONLY_STIX = "collection_only_stix" -COLLECTION_STIX_AND_CUSTOM = "collection_stix_and_custom" -COLLECTION_DISABLED = "collection_disabled" - - -COLLECTIONS_A = [ - entities.CollectionEntity(**x) - for x in [{'name': COLLECTION_OPEN, 'available': True, 'accept_all_content': True}] -] - -COLLECTIONS_B = [ - entities.CollectionEntity(**x) - for x in [ - { - 'name': COLLECTION_OPEN, - 'available': True, - 'accept_all_content': True, - 'type': entities.CollectionEntity.TYPE_SET, - }, - { - 'name': COLLECTION_ONLY_STIX, - 'available': True, - 'accept_all_content': False, - 'supported_content': CONTENT_BINDINGS_ONLY_STIX, - }, - { - 'name': COLLECTION_STIX_AND_CUSTOM, - 'available': True, - 'accept_all_content': False, - 'supported_content': CONTENT_BINDINGS_STIX_AND_CUSTOM, - }, - {'name': COLLECTION_DISABLED, 'available': False}, - ] -] - -USERNAME = "some-username" -PASSWORD = "some-password" -ACCOUNT = Account(str(uuid4()), USERNAME, {}) -VALID_TOKEN = "valid-token" +from uuid import uuid4 + +from libtaxii.constants import CB_STIX_XML_111, VID_TAXII_HTTP_10, VID_TAXII_HTTPS_10 + +from opentaxii.entities import Account +from opentaxii.taxii import entities + +PROTOCOL_BINDINGS = [VID_TAXII_HTTP_10, VID_TAXII_HTTPS_10] + +CUSTOM_CONTENT_BINDING = 'custom:content:binding' +INVALID_CONTENT_BINDING = 'invalid:content:binding' + +INBOX_A: dict = dict( + id='inbox-A', + type='inbox', + description='inbox-A description', + destination_collection_required=False, + address='/relative/path/inbox-a', + accept_all_content=True, + protocol_bindings=PROTOCOL_BINDINGS, +) + +INBOX_B: dict = dict( + id='inbox-B', + type='inbox', + description='inbox-B description', + destination_collection_required='yes', + address='/relative/path/inbox-b', + supported_content=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + protocol_bindings=PROTOCOL_BINDINGS, +) + +DISCOVERY_A: dict = dict( + id='discovery-A', + type='discovery', + description='discovery-A description', + address='/relative/path/discovery-a', + advertised_services=[ + 'inbox-A', + 'inbox-B', + 'discovery-A', + 'discovery-B', + 'collection-management-A', + 'poll-A', + ], + protocol_bindings=PROTOCOL_BINDINGS, +) + +DISCOVERY_B: dict = dict( + id='discovery-B', + type='discovery', + description='External discovery-B service', + address='http://something.com/absolute/path/discovery-b', + protocol_bindings=[VID_TAXII_HTTP_10], +) + +SUBSCRIPTION_MESSAGE = 'message about subscription' + +COLLECTION_MANAGEMENT: dict = dict( + id='collection-management-A', + type='collection_management', + description='Collection management description', + address='/relative/path/collection-management', + protocol_bindings=PROTOCOL_BINDINGS, + subscription_message=SUBSCRIPTION_MESSAGE, +) + +POLL_RESULT_SIZE = 20 +POLL_MAX_COUNT = 15 + +POLL: dict = dict( + id='poll-A', + type='poll', + description='Poll service description', + address='/relative/path/poll', + protocol_bindings=PROTOCOL_BINDINGS, + max_result_size=POLL_RESULT_SIZE, + max_result_count=POLL_MAX_COUNT, +) + +DOMAIN = 'www.some-example.local' + +INTERNAL_SERVICES = [INBOX_A, INBOX_B, DISCOVERY_A, COLLECTION_MANAGEMENT, POLL] +SERVICES = INTERNAL_SERVICES + [DISCOVERY_B] + +INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) + +MESSAGE_ID = '123' +CONTENT = 'some-content' + +CONTENT_BINDINGS_ONLY_STIX = [CB_STIX_XML_111] +CONTENT_BINDINGS_STIX_AND_CUSTOM = CONTENT_BINDINGS_ONLY_STIX + [CUSTOM_CONTENT_BINDING] +CONTENT_BINDING_SUBTYPE = 'custom-subtype' + +MESSAGE = 'test-message' + + +COLLECTION_OPEN = "collection_open" +COLLECTION_ONLY_STIX = "collection_only_stix" +COLLECTION_STIX_AND_CUSTOM = "collection_stix_and_custom" +COLLECTION_DISABLED = "collection_disabled" + + +COLLECTIONS_A = [ + entities.CollectionEntity(**x) + for x in [{'name': COLLECTION_OPEN, 'available': True, 'accept_all_content': True}] +] + +COLLECTIONS_B = [ + entities.CollectionEntity(**x) + for x in [ + { + 'name': COLLECTION_OPEN, + 'available': True, + 'accept_all_content': True, + 'type': entities.CollectionEntity.TYPE_SET, + }, + { + 'name': COLLECTION_ONLY_STIX, + 'available': True, + 'accept_all_content': False, + 'supported_content': CONTENT_BINDINGS_ONLY_STIX, + }, + { + 'name': COLLECTION_STIX_AND_CUSTOM, + 'available': True, + 'accept_all_content': False, + 'supported_content': CONTENT_BINDINGS_STIX_AND_CUSTOM, + }, + {'name': COLLECTION_DISABLED, 'available': False}, + ] +] + +USERNAME = "some-username" +PASSWORD = "some-password" +ACCOUNT = Account(str(uuid4()), USERNAME, {}) +VALID_TOKEN = "valid-token" diff --git a/tests/services/test_collection_management.py b/tests/services/test_collection_management.py index 22323fc4..20bc0e36 100644 --- a/tests/services/test_collection_management.py +++ b/tests/services/test_collection_management.py @@ -1,199 +1,199 @@ -import pytest - -from opentaxii.taxii import entities - -from ..fixtures import ( - COLLECTION_DISABLED, - COLLECTION_ONLY_STIX, - COLLECTION_OPEN, - COLLECTION_STIX_AND_CUSTOM, - COLLECTIONS_B, - MESSAGE_ID, - SERVICES, -) -from ..utils import as_tm, persist_content, prepare_headers - -ASSIGNED_SERVICES = ['collection-management-A', 'inbox-A', 'inbox-B', 'poll-A'] - -ASSIGNED_INBOX_INSTANCES = sum( - len(s['protocol_bindings']) - for s in SERVICES - if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('inbox') -) - -ASSIGNED_SUBSCTRIPTION_INSTANCES = sum( - len(s['protocol_bindings']) - for s in SERVICES - if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('collection-') -) - - -@pytest.fixture(autouse=True) -def prepare_server(server, services): - for coll in COLLECTIONS_B: - coll = server.servers.taxii1.persistence.create_collection(coll) - server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=ASSIGNED_SERVICES - ) - - -def prepare_request(version): - module = as_tm(version) - if version == 11: - return module.CollectionInformationRequest(message_id=MESSAGE_ID) - else: - return module.FeedInformationRequest(message_id=MESSAGE_ID) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collections(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - response = service.process(headers, request) - - names = [c.name for c in COLLECTIONS_B] - - if version == 11: - assert isinstance(response, as_tm(version).CollectionInformationResponse) - assert len(response.collection_informations) == len(COLLECTIONS_B) - - for c in response.collection_informations: - assert c.collection_name in names - - else: - assert isinstance(response, as_tm(version).FeedInformationResponse) - assert len(response.feed_informations) == len(COLLECTIONS_B) - - for c in response.feed_informations: - assert c.feed_name in names - - -@pytest.mark.parametrize("https", [True, False]) -def test_collections_inboxes(server, https): - - version = 11 - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - response = service.process(headers, request) - - for coll in response.collection_informations: - inboxes = coll.receiving_inbox_services - - assert len(inboxes) == ASSIGNED_INBOX_INSTANCES - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collections_subscribe_instances(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - response = service.process(headers, request) - - if version == 11: - collections = response.collection_informations - else: - collections = response.feed_informations - - for c in collections: - assert len(c.subscription_methods) == ASSIGNED_SUBSCTRIPTION_INSTANCES - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collection_supported_content(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - response = service.process(headers, request) - - if version == 11: - - def get_coll(name): - return next( - c for c in response.collection_informations if c.collection_name == name - ) - - assert ( - get_coll(COLLECTION_OPEN).collection_type - == entities.CollectionEntity.TYPE_SET - ) - - else: - - def get_coll(name): - return next(c for c in response.feed_informations if c.feed_name == name) - - assert len(get_coll(COLLECTION_OPEN).supported_contents) == 0 - - assert len(get_coll(COLLECTION_ONLY_STIX).supported_contents) == 1 - assert len(get_coll(COLLECTION_STIX_AND_CUSTOM).supported_contents) == 2 - - assert not get_coll(COLLECTION_DISABLED).available - - -@pytest.mark.parametrize("https", [True, False]) -def test_collections_volume(server, https): - - version = 11 - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - - # querying empty collection - response = service.process(headers, request) - - collection = next( - c - for c in response.collection_informations - if c.collection_name == COLLECTION_OPEN - ) - - assert collection.collection_volume == 0 - - blocks_amount = 10 - - for i in range(blocks_amount): - persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) - - # querying filled collection - response = service.process(headers, request) - - collection = next( - c - for c in response.collection_informations - if c.collection_name == COLLECTION_OPEN - ) - - assert collection.collection_volume == blocks_amount - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collections_defined_supported_content(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - request = prepare_request(version) - response = service.process(headers, request) - - if version == 10: - coll = response.feed_informations[0] - else: - coll = response.collection_informations[0] - - # 1 poll service with 2 defined protocol bindings - assert len(coll.polling_service_instances) == 2 +import pytest + +from opentaxii.taxii import entities + +from ..fixtures import ( + COLLECTION_DISABLED, + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTION_STIX_AND_CUSTOM, + COLLECTIONS_B, + MESSAGE_ID, + SERVICES, +) +from ..utils import as_tm, persist_content, prepare_headers + +ASSIGNED_SERVICES = ['collection-management-A', 'inbox-A', 'inbox-B', 'poll-A'] + +ASSIGNED_INBOX_INSTANCES = sum( + len(s['protocol_bindings']) + for s in SERVICES + if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('inbox') +) + +ASSIGNED_SUBSCTRIPTION_INSTANCES = sum( + len(s['protocol_bindings']) + for s in SERVICES + if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('collection-') +) + + +@pytest.fixture(autouse=True) +def prepare_server(server, services): + for coll in COLLECTIONS_B: + coll = server.servers.taxii1.persistence.create_collection(coll) + server.servers.taxii1.persistence.set_collection_services( + coll.id, service_ids=ASSIGNED_SERVICES + ) + + +def prepare_request(version): + module = as_tm(version) + if version == 11: + return module.CollectionInformationRequest(message_id=MESSAGE_ID) + else: + return module.FeedInformationRequest(message_id=MESSAGE_ID) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collections(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + response = service.process(headers, request) + + names = [c.name for c in COLLECTIONS_B] + + if version == 11: + assert isinstance(response, as_tm(version).CollectionInformationResponse) + assert len(response.collection_informations) == len(COLLECTIONS_B) + + for c in response.collection_informations: + assert c.collection_name in names + + else: + assert isinstance(response, as_tm(version).FeedInformationResponse) + assert len(response.feed_informations) == len(COLLECTIONS_B) + + for c in response.feed_informations: + assert c.feed_name in names + + +@pytest.mark.parametrize("https", [True, False]) +def test_collections_inboxes(server, https): + + version = 11 + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + response = service.process(headers, request) + + for coll in response.collection_informations: + inboxes = coll.receiving_inbox_services + + assert len(inboxes) == ASSIGNED_INBOX_INSTANCES + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collections_subscribe_instances(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + response = service.process(headers, request) + + if version == 11: + collections = response.collection_informations + else: + collections = response.feed_informations + + for c in collections: + assert len(c.subscription_methods) == ASSIGNED_SUBSCTRIPTION_INSTANCES + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collection_supported_content(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + response = service.process(headers, request) + + if version == 11: + + def get_coll(name): + return next( + c for c in response.collection_informations if c.collection_name == name + ) + + assert ( + get_coll(COLLECTION_OPEN).collection_type + == entities.CollectionEntity.TYPE_SET + ) + + else: + + def get_coll(name): + return next(c for c in response.feed_informations if c.feed_name == name) + + assert len(get_coll(COLLECTION_OPEN).supported_contents) == 0 + + assert len(get_coll(COLLECTION_ONLY_STIX).supported_contents) == 1 + assert len(get_coll(COLLECTION_STIX_AND_CUSTOM).supported_contents) == 2 + + assert not get_coll(COLLECTION_DISABLED).available + + +@pytest.mark.parametrize("https", [True, False]) +def test_collections_volume(server, https): + + version = 11 + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + + # querying empty collection + response = service.process(headers, request) + + collection = next( + c + for c in response.collection_informations + if c.collection_name == COLLECTION_OPEN + ) + + assert collection.collection_volume == 0 + + blocks_amount = 10 + + for i in range(blocks_amount): + persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) + + # querying filled collection + response = service.process(headers, request) + + collection = next( + c + for c in response.collection_informations + if c.collection_name == COLLECTION_OPEN + ) + + assert collection.collection_volume == blocks_amount + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collections_defined_supported_content(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + request = prepare_request(version) + response = service.process(headers, request) + + if version == 10: + coll = response.feed_informations[0] + else: + coll = response.collection_informations[0] + + # 1 poll service with 2 defined protocol bindings + assert len(coll.polling_service_instances) == 2 diff --git a/tests/services/test_discovery.py b/tests/services/test_discovery.py index f09b8c41..e9bdf5c7 100644 --- a/tests/services/test_discovery.py +++ b/tests/services/test_discovery.py @@ -1,60 +1,60 @@ -import pytest -from libtaxii.constants import SVC_INBOX - -from ..fixtures import INBOX_A, INBOX_B, INSTANCES_CONFIGURED, MESSAGE_ID -from ..utils import as_tm, prepare_headers - - -@pytest.fixture(autouse=True) -def server_with_services(server, services): - pass - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_discovery_request(server, version, https): - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - service = server.servers.taxii1.get_service('discovery-A') - - headers = prepare_headers(version, https) - response = service.process(headers, request) - - assert len(response.service_instances) == INSTANCES_CONFIGURED - assert response.in_response_to == MESSAGE_ID - - assert isinstance(response, as_tm(version).DiscoveryResponse) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_content_bindings_present(server, version, https): - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - service = server.servers.taxii1.get_service('discovery-A') - - headers = prepare_headers(version, https) - response = service.process(headers, request) - - assert len(response.service_instances) == INSTANCES_CONFIGURED - assert response.in_response_to == MESSAGE_ID - - inboxes = [s for s in response.service_instances if s.service_type == SVC_INBOX] - - assert len(inboxes) == 4 - - address_a = INBOX_A['address'] - inboxes_a = [i for i in inboxes if i.service_address.endswith(address_a)] - # inbox_a accepts everything, so inbox_service_accepted_content is empty - assert all([len(i.inbox_service_accepted_content) == 0 for i in inboxes_a]) - - address_b = INBOX_B['address'] - inboxes_b = [i for i in inboxes if i.service_address.endswith(address_b)] - bindings = inboxes_b[0].inbox_service_accepted_content - - if version == 10: - binding_ids = bindings - else: - binding_ids = [b.binding_id for b in bindings] - - assert set(binding_ids) == set(INBOX_B['supported_content']) +import pytest +from libtaxii.constants import SVC_INBOX + +from ..fixtures import INBOX_A, INBOX_B, INSTANCES_CONFIGURED, MESSAGE_ID +from ..utils import as_tm, prepare_headers + + +@pytest.fixture(autouse=True) +def server_with_services(server, services): + pass + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_discovery_request(server, version, https): + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + service = server.servers.taxii1.get_service('discovery-A') + + headers = prepare_headers(version, https) + response = service.process(headers, request) + + assert len(response.service_instances) == INSTANCES_CONFIGURED + assert response.in_response_to == MESSAGE_ID + + assert isinstance(response, as_tm(version).DiscoveryResponse) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_content_bindings_present(server, version, https): + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + service = server.servers.taxii1.get_service('discovery-A') + + headers = prepare_headers(version, https) + response = service.process(headers, request) + + assert len(response.service_instances) == INSTANCES_CONFIGURED + assert response.in_response_to == MESSAGE_ID + + inboxes = [s for s in response.service_instances if s.service_type == SVC_INBOX] + + assert len(inboxes) == 4 + + address_a = INBOX_A['address'] + inboxes_a = [i for i in inboxes if i.service_address.endswith(address_a)] + # inbox_a accepts everything, so inbox_service_accepted_content is empty + assert all([len(i.inbox_service_accepted_content) == 0 for i in inboxes_a]) + + address_b = INBOX_B['address'] + inboxes_b = [i for i in inboxes if i.service_address.endswith(address_b)] + bindings = inboxes_b[0].inbox_service_accepted_content + + if version == 10: + binding_ids = bindings + else: + binding_ids = [b.binding_id for b in bindings] + + assert set(binding_ids) == set(INBOX_B['supported_content']) diff --git a/tests/services/test_inbox.py b/tests/services/test_inbox.py index e042251a..3427eca5 100644 --- a/tests/services/test_inbox.py +++ b/tests/services/test_inbox.py @@ -1,214 +1,214 @@ -import pytest -from libtaxii import messages_10 as tm10 -from libtaxii import messages_11 as tm11 -from libtaxii.constants import CB_STIX_XML_111, ST_SUCCESS - -from opentaxii.taxii import exceptions - -from ..fixtures import ( - COLLECTION_ONLY_STIX, - COLLECTION_OPEN, - COLLECTIONS_A, - COLLECTIONS_B, - CONTENT, - CONTENT_BINDING_SUBTYPE, - CUSTOM_CONTENT_BINDING, - INVALID_CONTENT_BINDING, - MESSAGE_ID, -) -from ..utils import as_tm, prepare_headers - - -def make_content( - version, content_binding=CUSTOM_CONTENT_BINDING, content=CONTENT, subtype=None -): - if version == 10: - return tm10.ContentBlock(content_binding, content) - - elif version == 11: - content_block = tm11.ContentBlock(tm11.ContentBinding(content_binding), content) - if subtype: - content_block.content_binding.subtype_ids.append(subtype) - - return content_block - else: - raise ValueError("Unknown TAXII message version: %s" % version) - - -def make_inbox_message(version, blocks=None, dest_collection=None): - - if version == 10: - inbox_message = tm10.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) - - elif version == 11: - inbox_message = tm11.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) - if dest_collection: - inbox_message.destination_collection_names.append(dest_collection) - else: - raise ValueError("Unknown TAXII message version: %s" % version) - - return inbox_message - - -@pytest.fixture(autouse=True) -def prepare_server(server, services): - from opentaxii.persistence.sqldb.models import DataCollection - - coll_mapping = {"inbox-A": COLLECTIONS_A, "inbox-B": COLLECTIONS_B} - - names = set() - for service, collections in coll_mapping.items(): - for coll in collections: - if coll.name not in names: - coll = server.servers.taxii1.persistence.create_collection(coll) - names.add(coll.name) - service_ids = [service] - else: - coll = ( - server.servers.taxii1.persistence.api.db.session.query( - DataCollection - ) - .filter_by(name=coll.name) - .one() - ) - service_ids = {s.id for s in coll.services} | {service} # type:ignore - - server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=service_ids - ) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_inbox_request_all_content(server, version, https): - - inbox_a = server.servers.taxii1.get_service("inbox-A") - - headers = prepare_headers(version, https) - - blocks = [ - make_content( - version, - content_binding=CUSTOM_CONTENT_BINDING, - subtype=CONTENT_BINDING_SUBTYPE, - ), - make_content(version, content_binding=INVALID_CONTENT_BINDING), - ] - inbox_message = make_inbox_message(version, blocks=blocks) - - # "inbox-A" accepts all content - response = inbox_a.process(headers, inbox_message) - - assert isinstance(response, as_tm(version).StatusMessage) - - assert response.status_type == ST_SUCCESS - assert response.in_response_to == MESSAGE_ID - - db_blocks = server.servers.taxii1.persistence.get_content_blocks(None) - assert len(db_blocks) == len(blocks) - - -@pytest.mark.parametrize("https", [True, False]) -def test_inbox_request_destination_collection(server, https): - version = 11 - - inbox_message = make_inbox_message( - version, blocks=[make_content(version)], dest_collection=None - ) - headers = prepare_headers(version, https) - - inbox = server.servers.taxii1.get_service("inbox-A") - # destination collection is not required for inbox-A - response = inbox.process(headers, inbox_message) - - assert isinstance(response, as_tm(version).StatusMessage) - assert response.status_type == ST_SUCCESS - - inbox = server.servers.taxii1.get_service("inbox-B") - # destination collection is required for inbox-B - with pytest.raises(exceptions.StatusMessageException): - response = inbox.process(headers, inbox_message) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_inbox_request_inbox_valid_content_binding(server, version, https): - - inbox = server.servers.taxii1.get_service("inbox-B") - - blocks = [ - make_content( - version, - content_binding=CUSTOM_CONTENT_BINDING, - subtype=CONTENT_BINDING_SUBTYPE, - ), - make_content(version, content_binding=CB_STIX_XML_111), - ] - - inbox_message = make_inbox_message( - version, dest_collection=COLLECTION_OPEN, blocks=blocks - ) - headers = prepare_headers(version, https) - - response = inbox.process(headers, inbox_message) - - assert isinstance(response, as_tm(version).StatusMessage) - assert response.status_type == ST_SUCCESS - assert response.in_response_to == MESSAGE_ID - - # all blocks - blocks = server.servers.taxii1.persistence.get_content_blocks(collection_id=None) - assert len(blocks) == len(blocks) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_inbox_req_inbox_invalid_inbox_content_binding(server, version, https): - - inbox = server.servers.taxii1.get_service("inbox-B") - - content = make_content(version, content_binding=INVALID_CONTENT_BINDING) - inbox_message = make_inbox_message( - version, dest_collection=COLLECTION_OPEN, blocks=[content] - ) - - headers = prepare_headers(version, https) - - response = inbox.process(headers, inbox_message) - - assert isinstance(response, as_tm(version).StatusMessage) - assert response.status_type == ST_SUCCESS - assert response.in_response_to == MESSAGE_ID - - blocks = server.servers.taxii1.persistence.get_content_blocks(None) - - # Content blocks with invalid content should be ignored - assert len(blocks) == 0 - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_inbox_req_coll_content_bindings_filtering(server, version, https): - - inbox = server.servers.taxii1.get_service("inbox-B") - headers = prepare_headers(version, https) - - blocks = [ - make_content(version, content_binding=CB_STIX_XML_111), - make_content(version, content_binding=INVALID_CONTENT_BINDING), - ] - - inbox_message = make_inbox_message( - version, dest_collection=COLLECTION_ONLY_STIX, blocks=blocks - ) - - response = inbox.process(headers, inbox_message) - - assert isinstance(response, as_tm(version).StatusMessage) - assert response.status_type == ST_SUCCESS - assert response.in_response_to == MESSAGE_ID - - blocks = server.servers.taxii1.persistence.get_content_blocks(None) - - # Content blocks with invalid content should be ignored - assert len(blocks) == 1 +import pytest +from libtaxii import messages_10 as tm10 +from libtaxii import messages_11 as tm11 +from libtaxii.constants import CB_STIX_XML_111, ST_SUCCESS + +from opentaxii.taxii import exceptions + +from ..fixtures import ( + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTIONS_A, + COLLECTIONS_B, + CONTENT, + CONTENT_BINDING_SUBTYPE, + CUSTOM_CONTENT_BINDING, + INVALID_CONTENT_BINDING, + MESSAGE_ID, +) +from ..utils import as_tm, prepare_headers + + +def make_content( + version, content_binding=CUSTOM_CONTENT_BINDING, content=CONTENT, subtype=None +): + if version == 10: + return tm10.ContentBlock(content_binding, content) + + elif version == 11: + content_block = tm11.ContentBlock(tm11.ContentBinding(content_binding), content) + if subtype: + content_block.content_binding.subtype_ids.append(subtype) + + return content_block + else: + raise ValueError("Unknown TAXII message version: %s" % version) + + +def make_inbox_message(version, blocks=None, dest_collection=None): + + if version == 10: + inbox_message = tm10.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) + + elif version == 11: + inbox_message = tm11.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) + if dest_collection: + inbox_message.destination_collection_names.append(dest_collection) + else: + raise ValueError("Unknown TAXII message version: %s" % version) + + return inbox_message + + +@pytest.fixture(autouse=True) +def prepare_server(server, services): + from opentaxii.persistence.sqldb.models import DataCollection + + coll_mapping = {"inbox-A": COLLECTIONS_A, "inbox-B": COLLECTIONS_B} + + names = set() + for service, collections in coll_mapping.items(): + for coll in collections: + if coll.name not in names: + coll = server.servers.taxii1.persistence.create_collection(coll) + names.add(coll.name) + service_ids = [service] + else: + coll = ( + server.servers.taxii1.persistence.api.db.session.query( + DataCollection + ) + .filter_by(name=coll.name) + .one() + ) + service_ids = {s.id for s in coll.services} | {service} # type:ignore + + server.servers.taxii1.persistence.set_collection_services( + coll.id, service_ids=service_ids + ) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_inbox_request_all_content(server, version, https): + + inbox_a = server.servers.taxii1.get_service("inbox-A") + + headers = prepare_headers(version, https) + + blocks = [ + make_content( + version, + content_binding=CUSTOM_CONTENT_BINDING, + subtype=CONTENT_BINDING_SUBTYPE, + ), + make_content(version, content_binding=INVALID_CONTENT_BINDING), + ] + inbox_message = make_inbox_message(version, blocks=blocks) + + # "inbox-A" accepts all content + response = inbox_a.process(headers, inbox_message) + + assert isinstance(response, as_tm(version).StatusMessage) + + assert response.status_type == ST_SUCCESS + assert response.in_response_to == MESSAGE_ID + + db_blocks = server.servers.taxii1.persistence.get_content_blocks(None) + assert len(db_blocks) == len(blocks) + + +@pytest.mark.parametrize("https", [True, False]) +def test_inbox_request_destination_collection(server, https): + version = 11 + + inbox_message = make_inbox_message( + version, blocks=[make_content(version)], dest_collection=None + ) + headers = prepare_headers(version, https) + + inbox = server.servers.taxii1.get_service("inbox-A") + # destination collection is not required for inbox-A + response = inbox.process(headers, inbox_message) + + assert isinstance(response, as_tm(version).StatusMessage) + assert response.status_type == ST_SUCCESS + + inbox = server.servers.taxii1.get_service("inbox-B") + # destination collection is required for inbox-B + with pytest.raises(exceptions.StatusMessageException): + response = inbox.process(headers, inbox_message) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_inbox_request_inbox_valid_content_binding(server, version, https): + + inbox = server.servers.taxii1.get_service("inbox-B") + + blocks = [ + make_content( + version, + content_binding=CUSTOM_CONTENT_BINDING, + subtype=CONTENT_BINDING_SUBTYPE, + ), + make_content(version, content_binding=CB_STIX_XML_111), + ] + + inbox_message = make_inbox_message( + version, dest_collection=COLLECTION_OPEN, blocks=blocks + ) + headers = prepare_headers(version, https) + + response = inbox.process(headers, inbox_message) + + assert isinstance(response, as_tm(version).StatusMessage) + assert response.status_type == ST_SUCCESS + assert response.in_response_to == MESSAGE_ID + + # all blocks + blocks = server.servers.taxii1.persistence.get_content_blocks(collection_id=None) + assert len(blocks) == len(blocks) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_inbox_req_inbox_invalid_inbox_content_binding(server, version, https): + + inbox = server.servers.taxii1.get_service("inbox-B") + + content = make_content(version, content_binding=INVALID_CONTENT_BINDING) + inbox_message = make_inbox_message( + version, dest_collection=COLLECTION_OPEN, blocks=[content] + ) + + headers = prepare_headers(version, https) + + response = inbox.process(headers, inbox_message) + + assert isinstance(response, as_tm(version).StatusMessage) + assert response.status_type == ST_SUCCESS + assert response.in_response_to == MESSAGE_ID + + blocks = server.servers.taxii1.persistence.get_content_blocks(None) + + # Content blocks with invalid content should be ignored + assert len(blocks) == 0 + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_inbox_req_coll_content_bindings_filtering(server, version, https): + + inbox = server.servers.taxii1.get_service("inbox-B") + headers = prepare_headers(version, https) + + blocks = [ + make_content(version, content_binding=CB_STIX_XML_111), + make_content(version, content_binding=INVALID_CONTENT_BINDING), + ] + + inbox_message = make_inbox_message( + version, dest_collection=COLLECTION_ONLY_STIX, blocks=blocks + ) + + response = inbox.process(headers, inbox_message) + + assert isinstance(response, as_tm(version).StatusMessage) + assert response.status_type == ST_SUCCESS + assert response.in_response_to == MESSAGE_ID + + blocks = server.servers.taxii1.persistence.get_content_blocks(None) + + # Content blocks with invalid content should be ignored + assert len(blocks) == 1 diff --git a/tests/services/test_poll.py b/tests/services/test_poll.py index 6f7bb59a..8ea9598a 100644 --- a/tests/services/test_poll.py +++ b/tests/services/test_poll.py @@ -1,380 +1,380 @@ -import pytest -from libtaxii import messages_10 as tm10 -from libtaxii import messages_11 as tm11 -from libtaxii.constants import ACT_SUBSCRIBE, CB_STIX_XML_111, RT_COUNT_ONLY, RT_FULL - -from opentaxii.taxii import exceptions - -from ..fixtures import ( - COLLECTION_DISABLED, - COLLECTION_ONLY_STIX, - COLLECTION_OPEN, - COLLECTION_STIX_AND_CUSTOM, - COLLECTIONS_B, - CUSTOM_CONTENT_BINDING, - MESSAGE_ID, - POLL_MAX_COUNT, - POLL_RESULT_SIZE, -) -from ..utils import ( - as_tm, - persist_content, - prepare_headers, - prepare_subscription_request, -) - - -@pytest.fixture(autouse=True) -def prepare_server(server, services): - services = ['poll-A', 'collection-management-A'] - for coll in COLLECTIONS_B: - coll = server.servers.taxii1.persistence.create_collection(coll) - server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=services - ) - return server - - -def prepare_request( - collection_name, version, count_only=False, bindings=[], subscription_id=None -): - - if version == 11: - content_bindings = [tm11.ContentBinding(b) for b in bindings] - if subscription_id: - poll_parameters = None - else: - poll_parameters = tm11.PollParameters( - response_type=(RT_FULL if not count_only else RT_COUNT_ONLY), - content_bindings=content_bindings, - ) - return tm11.PollRequest( - message_id=MESSAGE_ID, - collection_name=collection_name, - subscription_id=subscription_id, - poll_parameters=poll_parameters, - ) - elif version == 10: - content_bindings = bindings - return tm10.PollRequest( - message_id=MESSAGE_ID, - feed_name=collection_name, - content_bindings=content_bindings, - subscription_id=subscription_id, - ) - - -def prepare_fulfilment_request(collection_name, result_id, part_number): - return tm11.PollFulfillmentRequest( - message_id=MESSAGE_ID, - collection_name=collection_name, - result_id=result_id, - result_part_number=part_number, - ) - - -@pytest.mark.parametrize( - ("https", "version", "count_blocks"), - [ - (True, 11, True), - (False, 11, False), - (True, 10, False), - (False, 10, False), - ], -) -def test_poll_empty_response(server, version, https, count_blocks): - - server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks - - service = server.servers.taxii1.get_service('poll-A') - - headers = prepare_headers(version, https) - request = prepare_request(collection_name=COLLECTION_OPEN, version=version) - - if version == 11: - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).PollResponse) - - if count_blocks: - assert response.record_count.record_count == 0 - assert not response.record_count.partial_count - else: - assert response.record_count is None - - else: - # COLLECTION_OPEN type (SET) is not supported in TAXII 1.0 - with pytest.raises(exceptions.StatusMessageException): - response = service.process(headers, request) - - server.servers.taxii1.config['count_blocks_in_poll_responses'] = True - - -@pytest.mark.parametrize( - ("https", "version"), [(True, 11), (False, 11), (True, 10), (False, 10)] -) -def test_poll_collection_not_available(server, version, https): - - service = server.servers.taxii1.get_service('poll-A') - - headers = prepare_headers(version, https) - request = prepare_request(collection_name=COLLECTION_DISABLED, version=version) - - with pytest.raises(exceptions.StatusMessageException): - service.process(headers, request) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_poll_get_content(server, version, https): - - service = server.servers.taxii1.get_service('poll-A') - original = persist_content( - server.servers.taxii1.persistence, - COLLECTION_ONLY_STIX, - service.id, - binding=CB_STIX_XML_111, - ) - - # wrong collection - headers = prepare_headers(version, https) - request = prepare_request( - collection_name=COLLECTION_STIX_AND_CUSTOM, version=version - ) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).PollResponse) - assert len(response.content_blocks) == 0 - - # right collection - headers = prepare_headers(version, https) - request = prepare_request(collection_name=COLLECTION_ONLY_STIX, version=version) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).PollResponse) - assert len(response.content_blocks) == 1 - - block = response.content_blocks[0] - - assert original.content == block.content.encode('utf-8') - assert original.timestamp_label == block.timestamp_label - - # right collection and request with wrong content_type - headers = prepare_headers(version, https) - request = prepare_request( - collection_name=COLLECTION_ONLY_STIX, - version=version, - bindings=[CUSTOM_CONTENT_BINDING], - ) - - with pytest.raises(exceptions.StatusMessageException): - service.process(headers, request) - - -@pytest.mark.parametrize( - ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)], -) -def test_poll_get_content_count(server, https, count_blocks): - version = 11 - server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks - service = server.servers.taxii1.get_service('poll-A') - - blocks_amount = 10 - - for i in range(blocks_amount): - persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) - - headers = prepare_headers(version, https) - - # count-only request - request = prepare_request( - collection_name=COLLECTION_OPEN, count_only=True, version=version - ) - response = service.process(headers, request) - - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == blocks_amount - assert not response.record_count.partial_count - assert len(response.content_blocks) == 0 - else: - assert response.record_count is None - server.servers.taxii1.config['count_blocks_in_poll_responses'] = True - - -@pytest.mark.parametrize( - ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)], -) -def test_poll_max_count_max_size(server, https, count_blocks): - - version = 11 - server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks - - service = server.servers.taxii1.get_service('poll-A') - - blocks_amount = 30 - - for i in range(blocks_amount): - persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) - - headers = prepare_headers(version, https) - - # count-only request - request = prepare_request( - collection_name=COLLECTION_OPEN, count_only=True, version=version - ) - response = service.process(headers, request) - - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == POLL_MAX_COUNT - else: - assert response.record_count is None - assert len(response.content_blocks) == 0 - - # content request - request = prepare_request(collection_name=COLLECTION_OPEN, version=version) - response = service.process(headers, request) - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == POLL_MAX_COUNT - assert response.record_count.partial_count is True - assert len(response.content_blocks) == POLL_RESULT_SIZE - else: - assert response.record_count is None - - assert response.more is True - assert response.result_id - server.servers.taxii1.config['count_blocks_in_poll_responses'] = True - - -@pytest.mark.parametrize( - ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)], -) -def test_poll_fulfilment_request(server, https, count_blocks): - server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks - version = 11 - service = server.servers.taxii1.get_service('poll-A') - - blocks_amount = 30 - - for i in range(blocks_amount): - persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) - - headers = prepare_headers(version, https) - - # first content request - request = prepare_request(collection_name=COLLECTION_OPEN, version=version) - response = service.process(headers, request) - - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == POLL_MAX_COUNT - assert response.record_count.partial_count is True - assert len(response.content_blocks) == POLL_RESULT_SIZE - else: - assert response.record_count is None - - assert response.more is True - assert response.result_id - - # poll fullfilment request - result_id = response.result_id - part_number = 2 - request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) - response = service.process(headers, request) - - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == POLL_MAX_COUNT - assert response.record_count.partial_count is True - else: - assert response.record_count is None - - assert len(response.content_blocks) == (blocks_amount - POLL_RESULT_SIZE) - - assert not response.more - assert response.result_id == result_id - - # poll fullfilment request over the top - result_id = response.result_id - part_number = 3 - request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) - response = service.process(headers, request) - assert isinstance(response, tm11.PollResponse) - - if count_blocks: - assert response.record_count.record_count == POLL_MAX_COUNT - assert response.record_count.partial_count is True - else: - assert response.record_count is None - - assert len(response.content_blocks) == 0 - - assert not response.more - assert response.result_id == result_id - server.servers.taxii1.config['count_blocks_in_poll_responses'] = True - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_subscribe_and_poll(server, version, https): - - server.servers.taxii1.config['count_blocks_in_poll_responses'] = True - - subs_service = server.servers.taxii1.get_service('collection-management-A') - poll_service = server.servers.taxii1.get_service('poll-A') - - collection = COLLECTION_ONLY_STIX - - blocks_amount = 10 - for i in range(blocks_amount): - persist_content(server.servers.taxii1.persistence, collection, poll_service.id) - - headers = prepare_headers(version, https) - - params = dict( - response_type=RT_COUNT_ONLY, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - ) - - subs_request = prepare_subscription_request( - collection=collection, action=ACT_SUBSCRIBE, version=version, params=params - ) - - subs_response = subs_service.process(headers, subs_request) - - assert len(subs_response.subscription_instances) == 1 - - subscription = subs_response.subscription_instances[0] - assert subscription.subscription_id - - # response type (count_only==False) should be ignored - # for TAXII 1.1 requests - poll_request = prepare_request( - collection_name=collection, - count_only=False, - subscription_id=subscription.subscription_id, - version=version, - ) - - poll_response = poll_service.process(headers, poll_request) - - if version == 11: - assert poll_response.record_count.record_count == blocks_amount - assert not poll_response.record_count.partial_count - - assert len(poll_response.content_blocks) == 0 - assert poll_response.subscription_id == subscription.subscription_id - else: - assert len(poll_response.content_blocks) == blocks_amount +import pytest +from libtaxii import messages_10 as tm10 +from libtaxii import messages_11 as tm11 +from libtaxii.constants import ACT_SUBSCRIBE, CB_STIX_XML_111, RT_COUNT_ONLY, RT_FULL + +from opentaxii.taxii import exceptions + +from ..fixtures import ( + COLLECTION_DISABLED, + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTION_STIX_AND_CUSTOM, + COLLECTIONS_B, + CUSTOM_CONTENT_BINDING, + MESSAGE_ID, + POLL_MAX_COUNT, + POLL_RESULT_SIZE, +) +from ..utils import ( + as_tm, + persist_content, + prepare_headers, + prepare_subscription_request, +) + + +@pytest.fixture(autouse=True) +def prepare_server(server, services): + services = ['poll-A', 'collection-management-A'] + for coll in COLLECTIONS_B: + coll = server.servers.taxii1.persistence.create_collection(coll) + server.servers.taxii1.persistence.set_collection_services( + coll.id, service_ids=services + ) + return server + + +def prepare_request( + collection_name, version, count_only=False, bindings=[], subscription_id=None +): + + if version == 11: + content_bindings = [tm11.ContentBinding(b) for b in bindings] + if subscription_id: + poll_parameters = None + else: + poll_parameters = tm11.PollParameters( + response_type=(RT_FULL if not count_only else RT_COUNT_ONLY), + content_bindings=content_bindings, + ) + return tm11.PollRequest( + message_id=MESSAGE_ID, + collection_name=collection_name, + subscription_id=subscription_id, + poll_parameters=poll_parameters, + ) + elif version == 10: + content_bindings = bindings + return tm10.PollRequest( + message_id=MESSAGE_ID, + feed_name=collection_name, + content_bindings=content_bindings, + subscription_id=subscription_id, + ) + + +def prepare_fulfilment_request(collection_name, result_id, part_number): + return tm11.PollFulfillmentRequest( + message_id=MESSAGE_ID, + collection_name=collection_name, + result_id=result_id, + result_part_number=part_number, + ) + + +@pytest.mark.parametrize( + ("https", "version", "count_blocks"), + [ + (True, 11, True), + (False, 11, False), + (True, 10, False), + (False, 10, False), + ], +) +def test_poll_empty_response(server, version, https, count_blocks): + + server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks + + service = server.servers.taxii1.get_service('poll-A') + + headers = prepare_headers(version, https) + request = prepare_request(collection_name=COLLECTION_OPEN, version=version) + + if version == 11: + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).PollResponse) + + if count_blocks: + assert response.record_count.record_count == 0 + assert not response.record_count.partial_count + else: + assert response.record_count is None + + else: + # COLLECTION_OPEN type (SET) is not supported in TAXII 1.0 + with pytest.raises(exceptions.StatusMessageException): + response = service.process(headers, request) + + server.servers.taxii1.config['count_blocks_in_poll_responses'] = True + + +@pytest.mark.parametrize( + ("https", "version"), [(True, 11), (False, 11), (True, 10), (False, 10)] +) +def test_poll_collection_not_available(server, version, https): + + service = server.servers.taxii1.get_service('poll-A') + + headers = prepare_headers(version, https) + request = prepare_request(collection_name=COLLECTION_DISABLED, version=version) + + with pytest.raises(exceptions.StatusMessageException): + service.process(headers, request) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_poll_get_content(server, version, https): + + service = server.servers.taxii1.get_service('poll-A') + original = persist_content( + server.servers.taxii1.persistence, + COLLECTION_ONLY_STIX, + service.id, + binding=CB_STIX_XML_111, + ) + + # wrong collection + headers = prepare_headers(version, https) + request = prepare_request( + collection_name=COLLECTION_STIX_AND_CUSTOM, version=version + ) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).PollResponse) + assert len(response.content_blocks) == 0 + + # right collection + headers = prepare_headers(version, https) + request = prepare_request(collection_name=COLLECTION_ONLY_STIX, version=version) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).PollResponse) + assert len(response.content_blocks) == 1 + + block = response.content_blocks[0] + + assert original.content == block.content.encode('utf-8') + assert original.timestamp_label == block.timestamp_label + + # right collection and request with wrong content_type + headers = prepare_headers(version, https) + request = prepare_request( + collection_name=COLLECTION_ONLY_STIX, + version=version, + bindings=[CUSTOM_CONTENT_BINDING], + ) + + with pytest.raises(exceptions.StatusMessageException): + service.process(headers, request) + + +@pytest.mark.parametrize( + ("https", "count_blocks"), + [(True, True), (False, True), (True, False), (False, False)], +) +def test_poll_get_content_count(server, https, count_blocks): + version = 11 + server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks + service = server.servers.taxii1.get_service('poll-A') + + blocks_amount = 10 + + for i in range(blocks_amount): + persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) + + headers = prepare_headers(version, https) + + # count-only request + request = prepare_request( + collection_name=COLLECTION_OPEN, count_only=True, version=version + ) + response = service.process(headers, request) + + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == blocks_amount + assert not response.record_count.partial_count + assert len(response.content_blocks) == 0 + else: + assert response.record_count is None + server.servers.taxii1.config['count_blocks_in_poll_responses'] = True + + +@pytest.mark.parametrize( + ("https", "count_blocks"), + [(True, True), (False, True), (True, False), (False, False)], +) +def test_poll_max_count_max_size(server, https, count_blocks): + + version = 11 + server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks + + service = server.servers.taxii1.get_service('poll-A') + + blocks_amount = 30 + + for i in range(blocks_amount): + persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) + + headers = prepare_headers(version, https) + + # count-only request + request = prepare_request( + collection_name=COLLECTION_OPEN, count_only=True, version=version + ) + response = service.process(headers, request) + + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == POLL_MAX_COUNT + else: + assert response.record_count is None + assert len(response.content_blocks) == 0 + + # content request + request = prepare_request(collection_name=COLLECTION_OPEN, version=version) + response = service.process(headers, request) + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == POLL_MAX_COUNT + assert response.record_count.partial_count is True + assert len(response.content_blocks) == POLL_RESULT_SIZE + else: + assert response.record_count is None + + assert response.more is True + assert response.result_id + server.servers.taxii1.config['count_blocks_in_poll_responses'] = True + + +@pytest.mark.parametrize( + ("https", "count_blocks"), + [(True, True), (False, True), (True, False), (False, False)], +) +def test_poll_fulfilment_request(server, https, count_blocks): + server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks + version = 11 + service = server.servers.taxii1.get_service('poll-A') + + blocks_amount = 30 + + for i in range(blocks_amount): + persist_content(server.servers.taxii1.persistence, COLLECTION_OPEN, service.id) + + headers = prepare_headers(version, https) + + # first content request + request = prepare_request(collection_name=COLLECTION_OPEN, version=version) + response = service.process(headers, request) + + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == POLL_MAX_COUNT + assert response.record_count.partial_count is True + assert len(response.content_blocks) == POLL_RESULT_SIZE + else: + assert response.record_count is None + + assert response.more is True + assert response.result_id + + # poll fullfilment request + result_id = response.result_id + part_number = 2 + request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) + response = service.process(headers, request) + + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == POLL_MAX_COUNT + assert response.record_count.partial_count is True + else: + assert response.record_count is None + + assert len(response.content_blocks) == (blocks_amount - POLL_RESULT_SIZE) + + assert not response.more + assert response.result_id == result_id + + # poll fullfilment request over the top + result_id = response.result_id + part_number = 3 + request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) + response = service.process(headers, request) + assert isinstance(response, tm11.PollResponse) + + if count_blocks: + assert response.record_count.record_count == POLL_MAX_COUNT + assert response.record_count.partial_count is True + else: + assert response.record_count is None + + assert len(response.content_blocks) == 0 + + assert not response.more + assert response.result_id == result_id + server.servers.taxii1.config['count_blocks_in_poll_responses'] = True + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_subscribe_and_poll(server, version, https): + + server.servers.taxii1.config['count_blocks_in_poll_responses'] = True + + subs_service = server.servers.taxii1.get_service('collection-management-A') + poll_service = server.servers.taxii1.get_service('poll-A') + + collection = COLLECTION_ONLY_STIX + + blocks_amount = 10 + for i in range(blocks_amount): + persist_content(server.servers.taxii1.persistence, collection, poll_service.id) + + headers = prepare_headers(version, https) + + params = dict( + response_type=RT_COUNT_ONLY, + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + ) + + subs_request = prepare_subscription_request( + collection=collection, action=ACT_SUBSCRIBE, version=version, params=params + ) + + subs_response = subs_service.process(headers, subs_request) + + assert len(subs_response.subscription_instances) == 1 + + subscription = subs_response.subscription_instances[0] + assert subscription.subscription_id + + # response type (count_only==False) should be ignored + # for TAXII 1.1 requests + poll_request = prepare_request( + collection_name=collection, + count_only=False, + subscription_id=subscription.subscription_id, + version=version, + ) + + poll_response = poll_service.process(headers, poll_request) + + if version == 11: + assert poll_response.record_count.record_count == blocks_amount + assert not poll_response.record_count.partial_count + + assert len(poll_response.content_blocks) == 0 + assert poll_response.subscription_id == subscription.subscription_id + else: + assert len(poll_response.content_blocks) == blocks_amount diff --git a/tests/services/test_subscription_management.py b/tests/services/test_subscription_management.py index fa493e86..53afe777 100644 --- a/tests/services/test_subscription_management.py +++ b/tests/services/test_subscription_management.py @@ -1,277 +1,277 @@ -import pytest -from libtaxii.constants import ( - ACT_PAUSE, - ACT_RESUME, - ACT_SUBSCRIBE, - ACT_UNSUBSCRIBE, - CB_STIX_XML_111, - RT_FULL, - SS_ACTIVE, - SS_PAUSED, - SS_UNSUBSCRIBED, -) - -from opentaxii.taxii import exceptions - -from ..fixtures import ( - COLLECTION_OPEN, - COLLECTIONS_B, - CUSTOM_CONTENT_BINDING, - SUBSCRIPTION_MESSAGE, -) -from ..utils import as_tm, prepare_headers -from ..utils import prepare_subscription_request as prepare_request - -ASSIGNED_SERVICES = ['collection-management-A', 'poll-A'] - - -@pytest.fixture(autouse=True) -def prepare_server(server, services): - for coll in COLLECTIONS_B: - coll = server.servers.taxii1.persistence.create_collection(coll) - server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=ASSIGNED_SERVICES - ) - return server - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_subscribe(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - poll_service = server.servers.taxii1.get_service('poll-A') - - headers = prepare_headers(version, https) - - params = dict( - response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - ) - - request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params - ) - - response = service.process(headers, request) - - if version == 11: - assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) - assert response.collection_name == COLLECTION_OPEN - else: - assert isinstance(response, as_tm(version).ManageFeedSubscriptionResponse) - assert response.feed_name == COLLECTION_OPEN - - assert response.message == SUBSCRIPTION_MESSAGE - assert len(response.subscription_instances) == 1 - - subs = response.subscription_instances[0] - - assert subs.subscription_id - - # 1 poll service * 2 protocol bindings - assert len(subs.poll_instances) == 2 - assert subs.poll_instances[0].poll_address == poll_service.get_absolute_address( - subs.poll_instances[0].poll_protocol - ) - - if version == 11: - assert subs.status == SS_ACTIVE - - response_bindings = [ - b.binding_id for b in subs.subscription_parameters.content_bindings - ] - - assert response_bindings == params['content_bindings'] - assert subs.subscription_parameters.response_type == params['response_type'] - - -@pytest.mark.parametrize("https", [True, False]) -def test_subscribe_pause_resume(server, https): - - version = 11 - - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - - params = dict( - response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - ) - - # Subscribing - request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params - ) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) - assert response.collection_name == COLLECTION_OPEN - - assert len(response.subscription_instances) == 1 - - subs = response.subscription_instances[0] - - assert subs.status == SS_ACTIVE - assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status - == SS_ACTIVE - ) - - # Pausing - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_PAUSE, - subscription_id=subs.subscription_id, - version=version, - ) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) - assert response.collection_name == COLLECTION_OPEN - - assert len(response.subscription_instances) == 1 - - subs = response.subscription_instances[0] - - assert subs.subscription_id - assert subs.status == SS_PAUSED - assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status - == SS_PAUSED - ) - - # Resume - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_RESUME, - subscription_id=subs.subscription_id, - version=version, - ) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) - assert response.collection_name == COLLECTION_OPEN - - assert len(response.subscription_instances) == 1 - - subs = response.subscription_instances[0] - - assert subs.subscription_id - assert subs.status == SS_ACTIVE - assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status - == SS_ACTIVE - ) - - -@pytest.mark.parametrize("https", [True, False]) -def test_pause_resume_wrong_id(server, https): - - version = 11 - service = server.servers.taxii1.get_service('collection-management-A') - - headers = prepare_headers(version, https) - - # Subscribing - request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version - ) - - response = service.process(headers, request) - - assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) - assert response.collection_name == COLLECTION_OPEN - - assert len(response.subscription_instances) == 1 - subs = response.subscription_instances[0] - - assert subs.subscription_id - assert subs.status == SS_ACTIVE - - # Pausing with wrong subscription ID - with pytest.raises(exceptions.StatusMessageException): - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_PAUSE, - subscription_id="RANDOM-WRONG-SUBSCRIPTION", - version=version, - ) - response = service.process(headers, request) - - # Resuming with wrong subscription ID - with pytest.raises(exceptions.StatusMessageException): - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_RESUME, - subscription_id="RANDOM-WRONG-SUBSCRIPTION", - version=version, - ) - response = service.process(headers, request) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_unsubscribe(server, version, https): - - service = server.servers.taxii1.get_service('collection-management-A') - headers = prepare_headers(version, https) - - params = dict( - response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - ) - - # Subscribing - request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params - ) - - response = service.process(headers, request) - - assert len(response.subscription_instances) == 1 - - subs = response.subscription_instances[0] - assert subs.subscription_id - - subscription_id = subs.subscription_id - - # Unsubscribing with invalid subscription ID should still - # return valid response - INVALID_ID = "RANDOM-WRONG-SUBSCRIPTION" - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_UNSUBSCRIBE, - subscription_id=INVALID_ID, - version=version, - ) - response = service.process(headers, request) - - assert len(response.subscription_instances) == 1 - subs = response.subscription_instances[0] - assert subs.subscription_id == INVALID_ID - - # Unsubscribing with valid subscription ID - request = prepare_request( - collection=COLLECTION_OPEN, - action=ACT_UNSUBSCRIBE, - subscription_id=subscription_id, - version=version, - ) - response = service.process(headers, request) - - assert len(response.subscription_instances) == 1 - subs = response.subscription_instances[0] - assert subs.subscription_id == subscription_id - - if version == 11: - assert subs.status == SS_UNSUBSCRIBED - - assert ( - server.servers.taxii1.persistence.get_subscription(subscription_id).status - == SS_UNSUBSCRIBED - ) +import pytest +from libtaxii.constants import ( + ACT_PAUSE, + ACT_RESUME, + ACT_SUBSCRIBE, + ACT_UNSUBSCRIBE, + CB_STIX_XML_111, + RT_FULL, + SS_ACTIVE, + SS_PAUSED, + SS_UNSUBSCRIBED, +) + +from opentaxii.taxii import exceptions + +from ..fixtures import ( + COLLECTION_OPEN, + COLLECTIONS_B, + CUSTOM_CONTENT_BINDING, + SUBSCRIPTION_MESSAGE, +) +from ..utils import as_tm, prepare_headers +from ..utils import prepare_subscription_request as prepare_request + +ASSIGNED_SERVICES = ['collection-management-A', 'poll-A'] + + +@pytest.fixture(autouse=True) +def prepare_server(server, services): + for coll in COLLECTIONS_B: + coll = server.servers.taxii1.persistence.create_collection(coll) + server.servers.taxii1.persistence.set_collection_services( + coll.id, service_ids=ASSIGNED_SERVICES + ) + return server + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_subscribe(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + poll_service = server.servers.taxii1.get_service('poll-A') + + headers = prepare_headers(version, https) + + params = dict( + response_type=RT_FULL, + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + ) + + request = prepare_request( + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) + + response = service.process(headers, request) + + if version == 11: + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) + assert response.collection_name == COLLECTION_OPEN + else: + assert isinstance(response, as_tm(version).ManageFeedSubscriptionResponse) + assert response.feed_name == COLLECTION_OPEN + + assert response.message == SUBSCRIPTION_MESSAGE + assert len(response.subscription_instances) == 1 + + subs = response.subscription_instances[0] + + assert subs.subscription_id + + # 1 poll service * 2 protocol bindings + assert len(subs.poll_instances) == 2 + assert subs.poll_instances[0].poll_address == poll_service.get_absolute_address( + subs.poll_instances[0].poll_protocol + ) + + if version == 11: + assert subs.status == SS_ACTIVE + + response_bindings = [ + b.binding_id for b in subs.subscription_parameters.content_bindings + ] + + assert response_bindings == params['content_bindings'] + assert subs.subscription_parameters.response_type == params['response_type'] + + +@pytest.mark.parametrize("https", [True, False]) +def test_subscribe_pause_resume(server, https): + + version = 11 + + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + + params = dict( + response_type=RT_FULL, + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + ) + + # Subscribing + request = prepare_request( + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) + assert response.collection_name == COLLECTION_OPEN + + assert len(response.subscription_instances) == 1 + + subs = response.subscription_instances[0] + + assert subs.status == SS_ACTIVE + assert ( + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_ACTIVE + ) + + # Pausing + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_PAUSE, + subscription_id=subs.subscription_id, + version=version, + ) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) + assert response.collection_name == COLLECTION_OPEN + + assert len(response.subscription_instances) == 1 + + subs = response.subscription_instances[0] + + assert subs.subscription_id + assert subs.status == SS_PAUSED + assert ( + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_PAUSED + ) + + # Resume + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_RESUME, + subscription_id=subs.subscription_id, + version=version, + ) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) + assert response.collection_name == COLLECTION_OPEN + + assert len(response.subscription_instances) == 1 + + subs = response.subscription_instances[0] + + assert subs.subscription_id + assert subs.status == SS_ACTIVE + assert ( + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_ACTIVE + ) + + +@pytest.mark.parametrize("https", [True, False]) +def test_pause_resume_wrong_id(server, https): + + version = 11 + service = server.servers.taxii1.get_service('collection-management-A') + + headers = prepare_headers(version, https) + + # Subscribing + request = prepare_request( + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version + ) + + response = service.process(headers, request) + + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) + assert response.collection_name == COLLECTION_OPEN + + assert len(response.subscription_instances) == 1 + subs = response.subscription_instances[0] + + assert subs.subscription_id + assert subs.status == SS_ACTIVE + + # Pausing with wrong subscription ID + with pytest.raises(exceptions.StatusMessageException): + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_PAUSE, + subscription_id="RANDOM-WRONG-SUBSCRIPTION", + version=version, + ) + response = service.process(headers, request) + + # Resuming with wrong subscription ID + with pytest.raises(exceptions.StatusMessageException): + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_RESUME, + subscription_id="RANDOM-WRONG-SUBSCRIPTION", + version=version, + ) + response = service.process(headers, request) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_unsubscribe(server, version, https): + + service = server.servers.taxii1.get_service('collection-management-A') + headers = prepare_headers(version, https) + + params = dict( + response_type=RT_FULL, + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + ) + + # Subscribing + request = prepare_request( + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) + + response = service.process(headers, request) + + assert len(response.subscription_instances) == 1 + + subs = response.subscription_instances[0] + assert subs.subscription_id + + subscription_id = subs.subscription_id + + # Unsubscribing with invalid subscription ID should still + # return valid response + INVALID_ID = "RANDOM-WRONG-SUBSCRIPTION" + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_UNSUBSCRIBE, + subscription_id=INVALID_ID, + version=version, + ) + response = service.process(headers, request) + + assert len(response.subscription_instances) == 1 + subs = response.subscription_instances[0] + assert subs.subscription_id == INVALID_ID + + # Unsubscribing with valid subscription ID + request = prepare_request( + collection=COLLECTION_OPEN, + action=ACT_UNSUBSCRIBE, + subscription_id=subscription_id, + version=version, + ) + response = service.process(headers, request) + + assert len(response.subscription_instances) == 1 + subs = response.subscription_instances[0] + assert subs.subscription_id == subscription_id + + if version == 11: + assert subs.status == SS_UNSUBSCRIBED + + assert ( + server.servers.taxii1.persistence.get_subscription(subscription_id).status + == SS_UNSUBSCRIBED + ) diff --git a/tests/taxii2/factories.py b/tests/taxii2/factories.py index 02fec5c1..c4768004 100644 --- a/tests/taxii2/factories.py +++ b/tests/taxii2/factories.py @@ -1,24 +1,24 @@ -"""Factories for taxii2 entities.""" - -import datetime -from uuid import uuid4 - -import factory -import stix2 - -from opentaxii.taxii2.entities import STIXObject - - -class STIXObjectFactory(factory.Factory): - id = factory.LazyAttribute(lambda o: f"{o.type}--{str(uuid4())}") - collection_id = factory.Faker("uuid4") - type = factory.Faker("random_element", elements=tuple(stix2.v21.OBJ_MAP.keys())) - spec_version = factory.Faker("random_element", elements=("2.0", "2.1")) - date_added = factory.Faker("date_time", tzinfo=datetime.timezone.utc) - version = factory.Faker("date_time", tzinfo=datetime.timezone.utc) - serialized_data = factory.Faker( - "pydict" - ) # TODO replace with valid stix object data generator - - class Meta: - model = STIXObject +"""Factories for taxii2 entities.""" + +import datetime +from uuid import uuid4 + +import factory +import stix2 + +from opentaxii.taxii2.entities import STIXObject + + +class STIXObjectFactory(factory.Factory): + id = factory.LazyAttribute(lambda o: f"{o.type}--{str(uuid4())}") + collection_id = factory.Faker("uuid4") + type = factory.Faker("random_element", elements=tuple(stix2.v21.OBJ_MAP.keys())) + spec_version = factory.Faker("random_element", elements=("2.0", "2.1")) + date_added = factory.Faker("date_time", tzinfo=datetime.timezone.utc) + version = factory.Faker("date_time", tzinfo=datetime.timezone.utc) + serialized_data = factory.Faker( + "pydict" + ) # TODO replace with valid stix object data generator + + class Meta: + model = STIXObject diff --git a/tests/taxii2/test_taxii2_api_root.py b/tests/taxii2/test_taxii2_api_root.py index 3038424f..33c25b25 100644 --- a/tests/taxii2/test_taxii2_api_root.py +++ b/tests/taxii2/test_taxii2_api_root.py @@ -1,336 +1,336 @@ -import json -import uuid -from unittest.mock import patch -from uuid import uuid4 - -import pytest -from sqlalchemy import literal - -from opentaxii.persistence.sqldb import taxii2models -from tests.taxii2.utils import ( - API_ROOTS, - API_ROOTS_WITH_DEFAULT, - GET_API_ROOT_MOCK, - config_noop, - config_override, - server_mapping_noop, - server_mapping_remove_fields, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "config_override_func", - "server_mapping_override_func", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": API_ROOTS[0].title, - "description": API_ROOTS[0].description, - "versions": ["application/taxii+json;version=2.1"], - "max_content_length": 1024, - }, - id="good, first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": API_ROOTS[1].title, - "description": API_ROOTS[1].description, - "versions": ["application/taxii+json;version=2.1"], - "max_content_length": 1024, - }, - id="good, second", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[2].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": API_ROOTS[2].title, - "versions": ["application/taxii+json;version=2.1"], - "max_content_length": 1024, - }, - id="good, no description", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - config_override({"max_content_length": 1024000}), - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": API_ROOTS[0].title, - "description": API_ROOTS[0].description, - "versions": ["application/taxii+json;version=2.1"], - "max_content_length": 1024000, - }, - id="good, first, max_content_length override", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - config_noop, - server_mapping_remove_fields("taxii1"), - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii2 only config", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - config_noop, - server_mapping_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii1/2 config", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_api_root( - authenticated_client, - method, - api_root_id, - headers, - config_override_func, - server_mapping_override_func, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2, - "config", - config_override_func( - authenticated_client.application.taxii_server.servers.taxii2.config - ), - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server, - "servers", - server_mapping_override_func( - authenticated_client.application.taxii_server.servers - ), - ), - ): - func = getattr(authenticated_client, method) - response = func(f"/taxii2/{api_root_id}/", headers=headers) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_api_root_unauthenticated( - client, - method, - is_public, -): - if is_public: - api_root_id = API_ROOTS[1].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - api_root_id = API_ROOTS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ): - func = getattr(client, method) - response = func( - f"/taxii2/{api_root_id}/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code - - -@pytest.mark.parametrize( - ["title", "description", "default", "is_public", "api_root_id", "db_api_roots"], - [ - pytest.param( - "my new api root", # title - None, # description - False, # default - False, # is_public - None, # api_root_id - [], # db_api_roots - id="title only", - ), - pytest.param( - "my new api root", # title - "my description", # description - False, # default - True, # is_public - None, # api_root_id - [], # db_api_roots - id="title, description", - ), - pytest.param( - "my new api root", # title - None, # description - True, # default - False, # is_public - None, # api_root_id - [], # db_api_roots - id="title, default", - ), - pytest.param( - "my new api root", # title - None, # description - False, # default - False, # is_public - "7468eafb-585d-402e-b6b9-49fe76492f9e", # api_root_id - [], # db_api_roots - id="title, id (str)", - ), - pytest.param( - "my new api root", # title - None, # description - False, # default - False, # is_public - uuid.UUID("7468eafb-585d-402e-b6b9-49fe76492f9e"), # api_root_id - [], # db_api_roots - id="title, id (uuid)", - ), - pytest.param( - "my new api root", # title - "my description", # description - True, # default - True, # is_public - None, # api_root_id - API_ROOTS_WITH_DEFAULT, # db_api_roots - id="title, description, default, existing", - ), - ], - indirect=["db_api_roots"], -) -def test_add_api_root( - app, title, description, default, is_public, api_root_id, db_api_roots -): - api_root = app.taxii_server.servers.taxii2.persistence.api.add_api_root( - title, description, default, is_public, api_root_id - ) - if api_root_id: - assert str(api_root.id) == str(api_root_id) - else: - assert api_root.id is not None - assert api_root.title == title - assert api_root.description == description - assert api_root.default == default - assert api_root.is_public == is_public - db_api_root = ( - app.taxii_server.servers.taxii2.persistence.api.db.session.query( - taxii2models.ApiRoot - ) - .filter(taxii2models.ApiRoot.id == api_root.id) - .one() - ) - assert db_api_root.title == title - assert db_api_root.description == description - assert db_api_root.default == default - assert db_api_root.is_public == is_public - if default: - assert ( - app.taxii_server.servers.taxii2.persistence.api.db.session.query( - taxii2models.ApiRoot - ) - .filter(taxii2models.ApiRoot.default == literal(True)) - .count() - ) == 1 +import json +import uuid +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from sqlalchemy import literal + +from opentaxii.persistence.sqldb import taxii2models +from tests.taxii2.utils import ( + API_ROOTS, + API_ROOTS_WITH_DEFAULT, + GET_API_ROOT_MOCK, + config_noop, + config_override, + server_mapping_noop, + server_mapping_remove_fields, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "config_override_func", + "server_mapping_override_func", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": API_ROOTS[0].title, + "description": API_ROOTS[0].description, + "versions": ["application/taxii+json;version=2.1"], + "max_content_length": 1024, + }, + id="good, first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": API_ROOTS[1].title, + "description": API_ROOTS[1].description, + "versions": ["application/taxii+json;version=2.1"], + "max_content_length": 1024, + }, + id="good, second", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[2].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": API_ROOTS[2].title, + "versions": ["application/taxii+json;version=2.1"], + "max_content_length": 1024, + }, + id="good, no description", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + config_override({"max_content_length": 1024000}), + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": API_ROOTS[0].title, + "description": API_ROOTS[0].description, + "versions": ["application/taxii+json;version=2.1"], + "max_content_length": 1024000, + }, + id="good, first, max_content_length override", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + config_noop, + server_mapping_remove_fields("taxii1"), + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii2 only config", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + config_noop, + server_mapping_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii1/2 config", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_api_root( + authenticated_client, + method, + api_root_id, + headers, + config_override_func, + server_mapping_override_func, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2, + "config", + config_override_func( + authenticated_client.application.taxii_server.servers.taxii2.config + ), + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server, + "servers", + server_mapping_override_func( + authenticated_client.application.taxii_server.servers + ), + ), + ): + func = getattr(authenticated_client, method) + response = func(f"/taxii2/{api_root_id}/", headers=headers) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_api_root_unauthenticated( + client, + method, + is_public, +): + if is_public: + api_root_id = API_ROOTS[1].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + api_root_id = API_ROOTS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ): + func = getattr(client, method) + response = func( + f"/taxii2/{api_root_id}/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + ["title", "description", "default", "is_public", "api_root_id", "db_api_roots"], + [ + pytest.param( + "my new api root", # title + None, # description + False, # default + False, # is_public + None, # api_root_id + [], # db_api_roots + id="title only", + ), + pytest.param( + "my new api root", # title + "my description", # description + False, # default + True, # is_public + None, # api_root_id + [], # db_api_roots + id="title, description", + ), + pytest.param( + "my new api root", # title + None, # description + True, # default + False, # is_public + None, # api_root_id + [], # db_api_roots + id="title, default", + ), + pytest.param( + "my new api root", # title + None, # description + False, # default + False, # is_public + "7468eafb-585d-402e-b6b9-49fe76492f9e", # api_root_id + [], # db_api_roots + id="title, id (str)", + ), + pytest.param( + "my new api root", # title + None, # description + False, # default + False, # is_public + uuid.UUID("7468eafb-585d-402e-b6b9-49fe76492f9e"), # api_root_id + [], # db_api_roots + id="title, id (uuid)", + ), + pytest.param( + "my new api root", # title + "my description", # description + True, # default + True, # is_public + None, # api_root_id + API_ROOTS_WITH_DEFAULT, # db_api_roots + id="title, description, default, existing", + ), + ], + indirect=["db_api_roots"], +) +def test_add_api_root( + app, title, description, default, is_public, api_root_id, db_api_roots +): + api_root = app.taxii_server.servers.taxii2.persistence.api.add_api_root( + title, description, default, is_public, api_root_id + ) + if api_root_id: + assert str(api_root.id) == str(api_root_id) + else: + assert api_root.id is not None + assert api_root.title == title + assert api_root.description == description + assert api_root.default == default + assert api_root.is_public == is_public + db_api_root = ( + app.taxii_server.servers.taxii2.persistence.api.db.session.query( + taxii2models.ApiRoot + ) + .filter(taxii2models.ApiRoot.id == api_root.id) + .one() + ) + assert db_api_root.title == title + assert db_api_root.description == description + assert db_api_root.default == default + assert db_api_root.is_public == is_public + if default: + assert ( + app.taxii_server.servers.taxii2.persistence.api.db.session.query( + taxii2models.ApiRoot + ) + .filter(taxii2models.ApiRoot.default == literal(True)) + .count() + ) == 1 diff --git a/tests/taxii2/test_taxii2_collection.py b/tests/taxii2/test_taxii2_collection.py index 1c69fef5..ef88e18d 100644 --- a/tests/taxii2/test_taxii2_collection.py +++ b/tests/taxii2/test_taxii2_collection.py @@ -1,434 +1,434 @@ -import json -import uuid -from unittest.mock import patch -from uuid import uuid4 - -import pytest - -from opentaxii.entities import Account -from opentaxii.persistence.sqldb import taxii2models -from opentaxii.taxii2.entities import Collection -from tests.taxii2.utils import ( - API_ROOTS, - COLLECTIONS, - GET_API_ROOT_MOCK, - GET_COLLECTION_MOCK, -) - - -@pytest.mark.parametrize( - "method,headers,api_root_id,collection_id,expected_status,expected_headers,expected_content", - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[0].id, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(COLLECTIONS[0].id), - "title": "0Read only", - "description": "Read only description", - "can_read": True, - "can_write": False, - "media_types": ["application/stix+json;version=2.1"], - }, - id="good", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[4].id, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(COLLECTIONS[4].id), - "title": "4No description", - "can_read": True, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - id="good, no description", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(COLLECTIONS[5].id), - "title": "5With alias", - "description": "With alias description", - "alias": "this-is-an-alias", - "can_read": False, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - id="good, with description", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].alias, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(COLLECTIONS[5].id), - "title": "5With alias", - "description": "With alias description", - "alias": "this-is-an-alias", - "can_read": False, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - id="good, by alias", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - COLLECTIONS[0].id, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - COLLECTIONS[0].id, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown collection", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[0].id, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[0].id, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_collection( - authenticated_client, - method, - api_root_id, - collection_id, - headers, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write"], - }, - ), - ): - func = getattr(authenticated_client, method) - response = func( - f"/taxii2/{api_root_id}/collections/{collection_id}/", headers=headers - ) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("is_public_write", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_collection_unauthenticated( - client, - method, - is_public, - is_public_write, -): - if is_public: - collection_id = COLLECTIONS[6].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - elif is_public_write: - collection_id = COLLECTIONS[7].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - collection_id = COLLECTIONS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code - - -@pytest.mark.parametrize( - ["api_root_id", "title", "description", "alias", "is_public", "is_public_write"], - [ - pytest.param( - API_ROOTS[0].id, # api_root_id - "my new collection", # title - None, # description - None, # alias - False, # is_public - False, # is_public_write - id="api_root_id, title", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - "my new collection", # title - "my description", # description - None, # alias - True, # is_public - False, # is_public_write - id="api_root_id, title, description", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - "my new collection", # title - "my description", # description - "my-alias", # alias - False, # is_public - True, # is_public_write - id="api_root_id, title, description, alias", - ), - ], -) -def test_add_collection( - app, - api_root_id, - title, - description, - alias, - is_public, - is_public_write, - db_api_roots, - db_collections, -): - collection = app.taxii_server.servers.taxii2.persistence.api.add_collection( - api_root_id=api_root_id, - title=title, - description=description, - alias=alias, - is_public=is_public, - is_public_write=is_public_write, - ) - assert collection.id is not None - assert collection.api_root_id == api_root_id - assert collection.title == title - assert collection.description == description - assert collection.alias == alias - assert collection.is_public == is_public - assert collection.is_public_write == is_public_write - db_collection = ( - app.taxii_server.servers.taxii2.persistence.api.db.session.query( - taxii2models.Collection - ) - .filter(taxii2models.Collection.id == collection.id) - .one() - ) - assert db_collection.api_root_id == api_root_id - assert db_collection.title == title - assert db_collection.description == description - assert db_collection.alias == alias - assert db_collection.is_public == is_public - assert db_collection.is_public_write == is_public_write - - -def test_collection_can_read(): - collection = Collection( - id=uuid.UUID("f74bf902-0b63-4c76-9bed-aecb120b084d"), - api_root_id=uuid.UUID("fe6489a0-26eb-4810-8efc-ad5a46d85a35"), - title="test", - description="test", - alias=None, - is_public=False, - is_public_write=False, - ) - str_collection_id = str(collection.id) - - # Without account - assert collection.can_read(None) is False - # With account - assert collection.can_read(Account("admin", "admin", {}, is_admin=True)) is True - assert ( - collection.can_read(Account("inaccessible", "inaccessible", {}, is_admin=False)) - is False - ) - assert ( - collection.can_read( - Account( - "read_only", "read_only", {str_collection_id: ["read"]}, is_admin=False - ) - ) - is True - ) - assert ( - collection.can_read( - Account( - "write_access", - "write_access", - {str_collection_id: ["write"]}, - is_admin=False, - ) - ) - is False - ) - - # Public - collection.is_public_write = True - assert collection.can_read(None) is False - collection.is_public = True - assert collection.can_read(None) is True - - -def test_collection_can_write(): - collection = Collection( - id=uuid.UUID("f74bf902-0b63-4c76-9bed-aecb120b084d"), - api_root_id=uuid.UUID("fe6489a0-26eb-4810-8efc-ad5a46d85a35"), - title="test", - description="test", - alias=None, - is_public=False, - is_public_write=False, - ) - str_collection_id = str(collection.id) - - # Without account - assert collection.can_write(None) is False - # With account - assert collection.can_write(Account("admin", "admin", {}, is_admin=True)) is True - assert ( - collection.can_write( - Account("inaccessible", "inaccessible", {}, is_admin=False) - ) - is False - ) - assert ( - collection.can_write( - Account( - "read_only", "read_only", {str_collection_id: ["read"]}, is_admin=False - ) - ) - is False - ) - assert ( - collection.can_write( - Account( - "write_access", - "write_access", - {str_collection_id: ["write"]}, - is_admin=False, - ) - ) - is True - ) - - # Public - collection.is_public = True - assert collection.can_write(None) is False - collection.is_public_write = True - assert collection.can_write(None) is True +import json +import uuid +from unittest.mock import patch +from uuid import uuid4 + +import pytest + +from opentaxii.entities import Account +from opentaxii.persistence.sqldb import taxii2models +from opentaxii.taxii2.entities import Collection +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTION_MOCK, +) + + +@pytest.mark.parametrize( + "method,headers,api_root_id,collection_id,expected_status,expected_headers,expected_content", + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[0].id, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(COLLECTIONS[0].id), + "title": "0Read only", + "description": "Read only description", + "can_read": True, + "can_write": False, + "media_types": ["application/stix+json;version=2.1"], + }, + id="good", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[4].id, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(COLLECTIONS[4].id), + "title": "4No description", + "can_read": True, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + id="good, no description", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(COLLECTIONS[5].id), + "title": "5With alias", + "description": "With alias description", + "alias": "this-is-an-alias", + "can_read": False, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + id="good, with description", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].alias, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(COLLECTIONS[5].id), + "title": "5With alias", + "description": "With alias description", + "alias": "this-is-an-alias", + "can_read": False, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + id="good, by alias", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + COLLECTIONS[0].id, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + COLLECTIONS[0].id, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown collection", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[0].id, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[0].id, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_collection( + authenticated_client, + method, + api_root_id, + collection_id, + headers, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write"], + }, + ), + ): + func = getattr(authenticated_client, method) + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/", headers=headers + ) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("is_public_write", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_collection_unauthenticated( + client, + method, + is_public, + is_public_write, +): + if is_public: + collection_id = COLLECTIONS[6].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + elif is_public_write: + collection_id = COLLECTIONS[7].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + collection_id = COLLECTIONS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + ["api_root_id", "title", "description", "alias", "is_public", "is_public_write"], + [ + pytest.param( + API_ROOTS[0].id, # api_root_id + "my new collection", # title + None, # description + None, # alias + False, # is_public + False, # is_public_write + id="api_root_id, title", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + "my new collection", # title + "my description", # description + None, # alias + True, # is_public + False, # is_public_write + id="api_root_id, title, description", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + "my new collection", # title + "my description", # description + "my-alias", # alias + False, # is_public + True, # is_public_write + id="api_root_id, title, description, alias", + ), + ], +) +def test_add_collection( + app, + api_root_id, + title, + description, + alias, + is_public, + is_public_write, + db_api_roots, + db_collections, +): + collection = app.taxii_server.servers.taxii2.persistence.api.add_collection( + api_root_id=api_root_id, + title=title, + description=description, + alias=alias, + is_public=is_public, + is_public_write=is_public_write, + ) + assert collection.id is not None + assert collection.api_root_id == api_root_id + assert collection.title == title + assert collection.description == description + assert collection.alias == alias + assert collection.is_public == is_public + assert collection.is_public_write == is_public_write + db_collection = ( + app.taxii_server.servers.taxii2.persistence.api.db.session.query( + taxii2models.Collection + ) + .filter(taxii2models.Collection.id == collection.id) + .one() + ) + assert db_collection.api_root_id == api_root_id + assert db_collection.title == title + assert db_collection.description == description + assert db_collection.alias == alias + assert db_collection.is_public == is_public + assert db_collection.is_public_write == is_public_write + + +def test_collection_can_read(): + collection = Collection( + id=uuid.UUID("f74bf902-0b63-4c76-9bed-aecb120b084d"), + api_root_id=uuid.UUID("fe6489a0-26eb-4810-8efc-ad5a46d85a35"), + title="test", + description="test", + alias=None, + is_public=False, + is_public_write=False, + ) + str_collection_id = str(collection.id) + + # Without account + assert collection.can_read(None) is False + # With account + assert collection.can_read(Account("admin", "admin", {}, is_admin=True)) is True + assert ( + collection.can_read(Account("inaccessible", "inaccessible", {}, is_admin=False)) + is False + ) + assert ( + collection.can_read( + Account( + "read_only", "read_only", {str_collection_id: ["read"]}, is_admin=False + ) + ) + is True + ) + assert ( + collection.can_read( + Account( + "write_access", + "write_access", + {str_collection_id: ["write"]}, + is_admin=False, + ) + ) + is False + ) + + # Public + collection.is_public_write = True + assert collection.can_read(None) is False + collection.is_public = True + assert collection.can_read(None) is True + + +def test_collection_can_write(): + collection = Collection( + id=uuid.UUID("f74bf902-0b63-4c76-9bed-aecb120b084d"), + api_root_id=uuid.UUID("fe6489a0-26eb-4810-8efc-ad5a46d85a35"), + title="test", + description="test", + alias=None, + is_public=False, + is_public_write=False, + ) + str_collection_id = str(collection.id) + + # Without account + assert collection.can_write(None) is False + # With account + assert collection.can_write(Account("admin", "admin", {}, is_admin=True)) is True + assert ( + collection.can_write( + Account("inaccessible", "inaccessible", {}, is_admin=False) + ) + is False + ) + assert ( + collection.can_write( + Account( + "read_only", "read_only", {str_collection_id: ["read"]}, is_admin=False + ) + ) + is False + ) + assert ( + collection.can_write( + Account( + "write_access", + "write_access", + {str_collection_id: ["write"]}, + is_admin=False, + ) + ) + is True + ) + + # Public + collection.is_public = True + assert collection.can_write(None) is False + collection.is_public_write = True + assert collection.can_write(None) is True diff --git a/tests/taxii2/test_taxii2_collections.py b/tests/taxii2/test_taxii2_collections.py index dbfe2b74..4feef467 100644 --- a/tests/taxii2/test_taxii2_collections.py +++ b/tests/taxii2/test_taxii2_collections.py @@ -1,287 +1,287 @@ -import json -from unittest.mock import patch -from uuid import uuid4 - -import pytest - -from tests.taxii2.utils import ( - API_ROOTS, - COLLECTIONS, - GET_API_ROOT_MOCK, - GET_COLLECTIONS_MOCK, - config_noop, - server_mapping_noop, - server_mapping_remove_fields, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "config_override_func", - "server_mapping_override_func", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "collections": [ - { - "id": str(COLLECTIONS[0].id), - "title": "0Read only", - "description": "Read only description", - "can_read": True, - "can_write": False, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[1].id), - "title": "1Write only", - "description": "Write only description", - "can_read": False, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[2].id), - "title": "2Read/Write", - "description": "Read/Write description", - "can_read": True, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[3].id), - "title": "3No permissions", - "description": "No permissions description", - "can_read": False, - "can_write": False, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[4].id), - "title": "4No description", - "can_read": True, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[5].id), - "title": "5With alias", - "description": "With alias description", - "alias": "this-is-an-alias", - "can_read": False, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[6].id), - "title": "6Public", - "description": "public description", - "can_read": True, - "can_write": False, - "media_types": ["application/stix+json;version=2.1"], - }, - { - "id": str(COLLECTIONS[7].id), - "title": "7Publicwrite", - "description": "public write description", - "can_read": False, - "can_write": True, - "media_types": ["application/stix+json;version=2.1"], - }, - ] - }, - id="good, first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - {}, - id="good, second (no collections)", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - config_noop, - server_mapping_remove_fields("taxii1"), - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii2 only config", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - config_noop, - server_mapping_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii1/2 config", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - config_noop, - server_mapping_noop, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_collections( - authenticated_client, - method, - api_root_id, - headers, - config_override_func, - server_mapping_override_func, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2, - "config", - config_override_func( - authenticated_client.application.taxii_server.servers.taxii2.config - ), - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collections", - side_effect=GET_COLLECTIONS_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server, - "servers", - server_mapping_override_func( - authenticated_client.application.taxii_server.servers - ), - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write"], - }, - ), - ): - func = getattr(authenticated_client, method) - response = func(f"/taxii2/{api_root_id}/collections/", headers=headers) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_collections_unauthenticated( - client, - method, - is_public, -): - if is_public: - api_root_id = API_ROOTS[1].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - api_root_id = API_ROOTS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collections", - side_effect=GET_COLLECTIONS_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{api_root_id}/collections/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code +import json +from unittest.mock import patch +from uuid import uuid4 + +import pytest + +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTIONS_MOCK, + config_noop, + server_mapping_noop, + server_mapping_remove_fields, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "config_override_func", + "server_mapping_override_func", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "collections": [ + { + "id": str(COLLECTIONS[0].id), + "title": "0Read only", + "description": "Read only description", + "can_read": True, + "can_write": False, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[1].id), + "title": "1Write only", + "description": "Write only description", + "can_read": False, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[2].id), + "title": "2Read/Write", + "description": "Read/Write description", + "can_read": True, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[3].id), + "title": "3No permissions", + "description": "No permissions description", + "can_read": False, + "can_write": False, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[4].id), + "title": "4No description", + "can_read": True, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[5].id), + "title": "5With alias", + "description": "With alias description", + "alias": "this-is-an-alias", + "can_read": False, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[6].id), + "title": "6Public", + "description": "public description", + "can_read": True, + "can_write": False, + "media_types": ["application/stix+json;version=2.1"], + }, + { + "id": str(COLLECTIONS[7].id), + "title": "7Publicwrite", + "description": "public write description", + "can_read": False, + "can_write": True, + "media_types": ["application/stix+json;version=2.1"], + }, + ] + }, + id="good, first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + {}, + id="good, second (no collections)", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + config_noop, + server_mapping_remove_fields("taxii1"), + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii2 only config", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + config_noop, + server_mapping_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii1/2 config", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + config_noop, + server_mapping_noop, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_collections( + authenticated_client, + method, + api_root_id, + headers, + config_override_func, + server_mapping_override_func, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2, + "config", + config_override_func( + authenticated_client.application.taxii_server.servers.taxii2.config + ), + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collections", + side_effect=GET_COLLECTIONS_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server, + "servers", + server_mapping_override_func( + authenticated_client.application.taxii_server.servers + ), + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write"], + }, + ), + ): + func = getattr(authenticated_client, method) + response = func(f"/taxii2/{api_root_id}/collections/", headers=headers) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_collections_unauthenticated( + client, + method, + is_public, +): + if is_public: + api_root_id = API_ROOTS[1].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + api_root_id = API_ROOTS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collections", + side_effect=GET_COLLECTIONS_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{api_root_id}/collections/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code diff --git a/tests/taxii2/test_taxii2_discovery.py b/tests/taxii2/test_taxii2_discovery.py index 3e309d77..c60d02e2 100644 --- a/tests/taxii2/test_taxii2_discovery.py +++ b/tests/taxii2/test_taxii2_discovery.py @@ -1,192 +1,192 @@ -import json -from unittest.mock import patch - -import pytest - -from tests.taxii2.utils import ( - API_ROOTS_WITH_DEFAULT, - API_ROOTS_WITHOUT_DEFAULT, - config_noop, - config_remove_fields, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "config_override_func", - "api_roots", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - config_noop, - API_ROOTS_WITH_DEFAULT, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": "Some TAXII Server", - "description": "This TAXII Server contains a listing of...", - "contact": "string containing contact information", - "default": f"/taxii2/{API_ROOTS_WITH_DEFAULT[0].id}/", - "api_roots": [f"/taxii2/{item.id}/" for item in API_ROOTS_WITH_DEFAULT], - }, - id="good, with default api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - config_noop, - API_ROOTS_WITHOUT_DEFAULT, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": "Some TAXII Server", - "description": "This TAXII Server contains a listing of...", - "contact": "string containing contact information", - "api_roots": [ - f"/taxii2/{item.id}/" for item in API_ROOTS_WITHOUT_DEFAULT - ], - }, - id="good, without default api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - config_noop, - [], - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": "Some TAXII Server", - "description": "This TAXII Server contains a listing of...", - "contact": "string containing contact information", - "api_roots": [], - }, - id="good, no api roots configured", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - config_remove_fields("description", "contact"), - [], - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "title": "Some TAXII Server", - "api_roots": [], - }, - id="good, no api roots and no description or contact", - ), - pytest.param( - "get", - {"Accept": "xml"}, - config_noop, - [], - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - config_noop, - [], - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_discovery( - authenticated_client, - method, - headers, - config_override_func, - api_roots, - expected_status, - expected_headers, - expected_content, -): - config_defaults = { - "title": "Some TAXII Server", - "description": "This TAXII Server contains a listing of...", - "contact": "string containing contact information", - } - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2, - "config", - config_override_func( - { - **authenticated_client.application.taxii_server.servers.taxii2.config, - **config_defaults, - } - ), - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_roots", - return_value=api_roots, - ), - ): - func = getattr(authenticated_client, method) - response = func("/taxii2/", headers=headers) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - assert json.loads(response.data) == expected_content - - -@pytest.mark.parametrize("public_discovery", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_discovery_unauthenticated( - client, - method, - public_discovery, -): - if public_discovery: - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with patch.object( - client.application.taxii_server.servers.taxii2, - "config", - { - **client.application.taxii_server.servers.taxii2.config, - "title": "Some TAXII Server", - "public_discovery": public_discovery, - }, - ): - func = getattr(client, method) - response = func( - "/taxii2/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code +import json +from unittest.mock import patch + +import pytest + +from tests.taxii2.utils import ( + API_ROOTS_WITH_DEFAULT, + API_ROOTS_WITHOUT_DEFAULT, + config_noop, + config_remove_fields, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "config_override_func", + "api_roots", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + config_noop, + API_ROOTS_WITH_DEFAULT, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": "Some TAXII Server", + "description": "This TAXII Server contains a listing of...", + "contact": "string containing contact information", + "default": f"/taxii2/{API_ROOTS_WITH_DEFAULT[0].id}/", + "api_roots": [f"/taxii2/{item.id}/" for item in API_ROOTS_WITH_DEFAULT], + }, + id="good, with default api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + config_noop, + API_ROOTS_WITHOUT_DEFAULT, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": "Some TAXII Server", + "description": "This TAXII Server contains a listing of...", + "contact": "string containing contact information", + "api_roots": [ + f"/taxii2/{item.id}/" for item in API_ROOTS_WITHOUT_DEFAULT + ], + }, + id="good, without default api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + config_noop, + [], + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": "Some TAXII Server", + "description": "This TAXII Server contains a listing of...", + "contact": "string containing contact information", + "api_roots": [], + }, + id="good, no api roots configured", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + config_remove_fields("description", "contact"), + [], + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "title": "Some TAXII Server", + "api_roots": [], + }, + id="good, no api roots and no description or contact", + ), + pytest.param( + "get", + {"Accept": "xml"}, + config_noop, + [], + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + config_noop, + [], + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_discovery( + authenticated_client, + method, + headers, + config_override_func, + api_roots, + expected_status, + expected_headers, + expected_content, +): + config_defaults = { + "title": "Some TAXII Server", + "description": "This TAXII Server contains a listing of...", + "contact": "string containing contact information", + } + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2, + "config", + config_override_func( + { + **authenticated_client.application.taxii_server.servers.taxii2.config, + **config_defaults, + } + ), + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_roots", + return_value=api_roots, + ), + ): + func = getattr(authenticated_client, method) + response = func("/taxii2/", headers=headers) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + assert json.loads(response.data) == expected_content + + +@pytest.mark.parametrize("public_discovery", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_discovery_unauthenticated( + client, + method, + public_discovery, +): + if public_discovery: + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with patch.object( + client.application.taxii_server.servers.taxii2, + "config", + { + **client.application.taxii_server.servers.taxii2.config, + "title": "Some TAXII Server", + "public_discovery": public_discovery, + }, + ): + func = getattr(client, method) + response = func( + "/taxii2/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code diff --git a/tests/taxii2/test_taxii2_manifest.py b/tests/taxii2/test_taxii2_manifest.py index f67cdee9..83d2779d 100644 --- a/tests/taxii2/test_taxii2_manifest.py +++ b/tests/taxii2/test_taxii2_manifest.py @@ -1,793 +1,793 @@ -import datetime -import json -from unittest.mock import patch -from urllib.parse import urlencode -from uuid import uuid4 - -import pytest - -from opentaxii.taxii2.utils import taxii2_datetimeformat -from tests.taxii2.utils import ( - API_ROOTS, - COLLECTIONS, - GET_COLLECTION_MOCK, - GET_MANIFEST_MOCK, - GET_NEXT_PARAM, - NOW, - STIX_OBJECTS, -) - - -@pytest.mark.parametrize( - "method,headers,api_root_id,collection_id,filter_kwargs,expected_status,expected_headers,expected_content", - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].alias, - {}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, by alias", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW)}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[1:2] - ], - }, - id="good, added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, no results", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"added_after": ["Not a valid datetime."]}, - "name": "validation error", - }, - id="broken added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 1}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": True, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 2}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, limit exact", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 999}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, limit high", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"limit": ["Not a valid integer."]}, - "name": "validation error", - }, - id="broken limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ) - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[1:2] - ], - }, - id="good, next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"next": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"next": ["Not a valid value."]}, - "name": "validation error", - }, - id="broken next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[id]": STIX_OBJECTS[0].id}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, id", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[id]": ",".join([obj.id for obj in STIX_OBJECTS[:3]])}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, ids", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[type]": STIX_OBJECTS[0].type}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, type", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[type]": ",".join([obj.type for obj in STIX_OBJECTS[:3]])}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, types", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "last"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, version last", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "first"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[1:3] - ], - }, - id="good, version first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "all"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:3] - ], - }, - id="good, version all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "match[version]": ",".join( - [taxii2_datetimeformat(obj.version) for obj in STIX_OBJECTS[:3]] - ) - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:3] - ], - }, - id="good, versions", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[spec_version]": STIX_OBJECTS[0].spec_version}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, spec_version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "match[spec_version]": ",".join( - [obj.spec_version for obj in STIX_OBJECTS[:3]] - ) - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "date_added": taxii2_datetimeformat(obj.date_added), - "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, spec_versions", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[1].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="write-only collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - COLLECTIONS[5].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - COLLECTIONS[5].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown collection", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_manifest( - authenticated_client, - method, - api_root_id, - collection_id, - filter_kwargs, - headers, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_manifest", - side_effect=GET_MANIFEST_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write", "read"], - }, - ), - ): - func = getattr(authenticated_client, method) - if filter_kwargs: - querystring = f"?{urlencode(filter_kwargs)}" - else: - querystring = "" - response = func( - f"/taxii2/{api_root_id}/collections/{collection_id}/manifest/{querystring}", - headers=headers, - ) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_manifest_unauthenticated( - client, - method, - is_public, -): - if is_public: - collection_id = COLLECTIONS[6].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - collection_id = COLLECTIONS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_manifest", - side_effect=GET_MANIFEST_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/manifest/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code +import datetime +import json +from unittest.mock import patch +from urllib.parse import urlencode +from uuid import uuid4 + +import pytest + +from opentaxii.taxii2.utils import taxii2_datetimeformat +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_COLLECTION_MOCK, + GET_MANIFEST_MOCK, + GET_NEXT_PARAM, + NOW, + STIX_OBJECTS, +) + + +@pytest.mark.parametrize( + "method,headers,api_root_id,collection_id,filter_kwargs,expected_status,expected_headers,expected_content", + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].alias, + {}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, by alias", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW)}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[1:2] + ], + }, + id="good, added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, no results", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"added_after": ["Not a valid datetime."]}, + "name": "validation error", + }, + id="broken added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 1}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": True, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 2}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, limit exact", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 999}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, limit high", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"limit": ["Not a valid integer."]}, + "name": "validation error", + }, + id="broken limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ) + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[1:2] + ], + }, + id="good, next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"next": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"next": ["Not a valid value."]}, + "name": "validation error", + }, + id="broken next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[id]": STIX_OBJECTS[0].id}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, id", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[id]": ",".join([obj.id for obj in STIX_OBJECTS[:3]])}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, ids", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[type]": STIX_OBJECTS[0].type}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, type", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[type]": ",".join([obj.type for obj in STIX_OBJECTS[:3]])}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, types", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "last"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, version last", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "first"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[1:3] + ], + }, + id="good, version first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "all"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:3] + ], + }, + id="good, version all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "match[version]": ",".join( + [taxii2_datetimeformat(obj.version) for obj in STIX_OBJECTS[:3]] + ) + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:3] + ], + }, + id="good, versions", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[spec_version]": STIX_OBJECTS[0].spec_version}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, spec_version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "match[spec_version]": ",".join( + [obj.spec_version for obj in STIX_OBJECTS[:3]] + ) + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "date_added": taxii2_datetimeformat(obj.date_added), + "version": taxii2_datetimeformat(obj.version), + "media_type": f"application/stix+json;version={obj.spec_version}", + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, spec_versions", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[1].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="write-only collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + COLLECTIONS[5].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + COLLECTIONS[5].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown collection", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_manifest( + authenticated_client, + method, + api_root_id, + collection_id, + filter_kwargs, + headers, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_manifest", + side_effect=GET_MANIFEST_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write", "read"], + }, + ), + ): + func = getattr(authenticated_client, method) + if filter_kwargs: + querystring = f"?{urlencode(filter_kwargs)}" + else: + querystring = "" + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/manifest/{querystring}", + headers=headers, + ) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_manifest_unauthenticated( + client, + method, + is_public, +): + if is_public: + collection_id = COLLECTIONS[6].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + collection_id = COLLECTIONS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_manifest", + side_effect=GET_MANIFEST_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/manifest/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code diff --git a/tests/taxii2/test_taxii2_object.py b/tests/taxii2/test_taxii2_object.py index a82e4641..b11ef678 100644 --- a/tests/taxii2/test_taxii2_object.py +++ b/tests/taxii2/test_taxii2_object.py @@ -1,923 +1,923 @@ -import datetime -import json -from unittest.mock import patch -from urllib.parse import urlencode -from uuid import uuid4 - -import pytest - -from opentaxii.taxii2.utils import DATETIMEFORMAT, taxii2_datetimeformat -from tests.taxii2.utils import ( - API_ROOTS, - COLLECTIONS, - DELETE_OBJECT_MOCK, - GET_COLLECTION_MOCK, - GET_NEXT_PARAM, - GET_OBJECT_MOCK, - NOW, - STIX_OBJECTS, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "collection_id", - "object_id", - "filter_kwargs", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].alias, - STIX_OBJECTS[0].id, - {}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, by alias", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "added_after": taxii2_datetimeformat(NOW), - "match[version]": "all", - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[2]] - ], - }, - id="good, added_after, all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"added_after": taxii2_datetimeformat(NOW)}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, added_after, no results", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"added_after": ["Not a valid datetime."]}, - "name": "validation error", - }, - id="broken added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": 1}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "limit": 1, - "match[version]": "all", - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": True, - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ), - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, limit, all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "limit": 2, - "match[version]": "all", - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0], STIX_OBJECTS[2]] - ], - }, - id="good, limit exact, all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": 999}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, limit high", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"limit": ["Not a valid integer."]}, - "name": "validation error", - }, - id="broken limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ) - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ), - "match[version]": "all", - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[2]] - ], - }, - id="good, next, all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"next": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"next": ["Not a valid value."]}, - "name": "validation error", - }, - id="broken next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": "last"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, version last", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": "first"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[2]] - ], - }, - id="good, version first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": "all"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0], STIX_OBJECTS[2]] - ], - }, - id="good, version all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": "a"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, unknown version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[spec_version]": STIX_OBJECTS[0].spec_version}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, spec_version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[spec_version]": "a"}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, unknown spec_version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[1].id, - STIX_OBJECTS[2].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="write-only collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - str(uuid4()), - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown object", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - b"", - id="delete, version filter", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[spec_version]": STIX_OBJECTS[0].spec_version}, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - b"", - id="delete, spec_version filter", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - b"", - id="delete, good", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - str(uuid4()), - {}, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - b"", - id="delete, unknown object", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[3].id, - str(uuid4()), - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="delete, no read, no write", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[0].id, - str(uuid4()), - {}, - 403, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 403, - "description": ( - "You don't have the permission to access the requested " - "resource. It is either read-protected or not readable by the " - "server." - ), - "name": "Forbidden", - }, - id="delete, read, no write", - ), - pytest.param( - "delete", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[1].id, - str(uuid4()), - {}, - 403, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 403, - "description": ( - "You don't have the permission to access the requested " - "resource. It is either read-protected or not readable by the " - "server." - ), - "name": "Forbidden", - }, - id="delete, no read, write", - ), - pytest.param( - "delete", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - str(uuid4()), - {}, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="delete, wrong accept header", - ), - ], -) -def test_object( - authenticated_client, - method, - api_root_id, - collection_id, - object_id, - filter_kwargs, - headers, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_object", - side_effect=GET_OBJECT_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write", "read"], - }, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "delete_object", - side_effect=DELETE_OBJECT_MOCK, - ) as delete_object_mock, - ): - func = getattr(authenticated_client, method) - if filter_kwargs: - querystring = f"?{urlencode(filter_kwargs)}" - else: - querystring = "" - kwargs = {"headers": headers} - response = func( - f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{object_id}/{querystring}", - **kwargs, - ) - assert response.status_code == expected_status - if method == "delete" and expected_status == 200: - expected_kwargs = { - "match_version": ( - [ - datetime.datetime.strptime( - filter_kwargs["match[version]"], DATETIMEFORMAT - ).replace(tzinfo=datetime.timezone.utc) - ] - if "match[version]" in filter_kwargs - else None - ), - "match_spec_version": ( - [filter_kwargs["match[spec_version]"]] - if "match[spec_version]" in filter_kwargs - else None - ), - } - delete_object_mock.assert_called_once_with( - collection_id=COLLECTIONS[5].id, object_id=object_id, **expected_kwargs - ) - else: - delete_object_mock.assert_not_called() - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ) and response.data != b"": - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_object_unauthenticated( - client, - method, - is_public, -): - if is_public: - collection_id = COLLECTIONS[6].id - stix_id = STIX_OBJECTS[4].id - if method == "get": - expected_status_code = 200 - elif method == "delete": - expected_status_code = 401 - else: - expected_status_code = 405 - else: - collection_id = COLLECTIONS[0].id - stix_id = STIX_OBJECTS[0].id - if method == "get": - expected_status_code = 401 - elif method == "delete": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_object", - side_effect=GET_OBJECT_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/{stix_id}/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code +import datetime +import json +from unittest.mock import patch +from urllib.parse import urlencode +from uuid import uuid4 + +import pytest + +from opentaxii.taxii2.utils import DATETIMEFORMAT, taxii2_datetimeformat +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + DELETE_OBJECT_MOCK, + GET_COLLECTION_MOCK, + GET_NEXT_PARAM, + GET_OBJECT_MOCK, + NOW, + STIX_OBJECTS, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "collection_id", + "object_id", + "filter_kwargs", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].alias, + STIX_OBJECTS[0].id, + {}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, by alias", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "added_after": taxii2_datetimeformat(NOW), + "match[version]": "all", + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[2]] + ], + }, + id="good, added_after, all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"added_after": taxii2_datetimeformat(NOW)}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, added_after, no results", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"added_after": ["Not a valid datetime."]}, + "name": "validation error", + }, + id="broken added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": 1}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "limit": 1, + "match[version]": "all", + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": True, + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ), + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, limit, all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "limit": 2, + "match[version]": "all", + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0], STIX_OBJECTS[2]] + ], + }, + id="good, limit exact, all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": 999}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, limit high", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"limit": ["Not a valid integer."]}, + "name": "validation error", + }, + id="broken limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ) + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ), + "match[version]": "all", + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[2]] + ], + }, + id="good, next, all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"next": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"next": ["Not a valid value."]}, + "name": "validation error", + }, + id="broken next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": "last"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, version last", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": "first"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[2]] + ], + }, + id="good, version first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": "all"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0], STIX_OBJECTS[2]] + ], + }, + id="good, version all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": "a"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, unknown version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[spec_version]": STIX_OBJECTS[0].spec_version}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, spec_version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[spec_version]": "a"}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, unknown spec_version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[1].id, + STIX_OBJECTS[2].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="write-only collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + str(uuid4()), + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown object", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + b"", + id="delete, version filter", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[spec_version]": STIX_OBJECTS[0].spec_version}, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + b"", + id="delete, spec_version filter", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + b"", + id="delete, good", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + str(uuid4()), + {}, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + b"", + id="delete, unknown object", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[3].id, + str(uuid4()), + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="delete, no read, no write", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[0].id, + str(uuid4()), + {}, + 403, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 403, + "description": ( + "You don't have the permission to access the requested " + "resource. It is either read-protected or not readable by the " + "server." + ), + "name": "Forbidden", + }, + id="delete, read, no write", + ), + pytest.param( + "delete", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[1].id, + str(uuid4()), + {}, + 403, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 403, + "description": ( + "You don't have the permission to access the requested " + "resource. It is either read-protected or not readable by the " + "server." + ), + "name": "Forbidden", + }, + id="delete, no read, write", + ), + pytest.param( + "delete", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + str(uuid4()), + {}, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="delete, wrong accept header", + ), + ], +) +def test_object( + authenticated_client, + method, + api_root_id, + collection_id, + object_id, + filter_kwargs, + headers, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_object", + side_effect=GET_OBJECT_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write", "read"], + }, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "delete_object", + side_effect=DELETE_OBJECT_MOCK, + ) as delete_object_mock, + ): + func = getattr(authenticated_client, method) + if filter_kwargs: + querystring = f"?{urlencode(filter_kwargs)}" + else: + querystring = "" + kwargs = {"headers": headers} + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{object_id}/{querystring}", + **kwargs, + ) + assert response.status_code == expected_status + if method == "delete" and expected_status == 200: + expected_kwargs = { + "match_version": ( + [ + datetime.datetime.strptime( + filter_kwargs["match[version]"], DATETIMEFORMAT + ).replace(tzinfo=datetime.timezone.utc) + ] + if "match[version]" in filter_kwargs + else None + ), + "match_spec_version": ( + [filter_kwargs["match[spec_version]"]] + if "match[spec_version]" in filter_kwargs + else None + ), + } + delete_object_mock.assert_called_once_with( + collection_id=COLLECTIONS[5].id, object_id=object_id, **expected_kwargs + ) + else: + delete_object_mock.assert_not_called() + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ) and response.data != b"": + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_object_unauthenticated( + client, + method, + is_public, +): + if is_public: + collection_id = COLLECTIONS[6].id + stix_id = STIX_OBJECTS[4].id + if method == "get": + expected_status_code = 200 + elif method == "delete": + expected_status_code = 401 + else: + expected_status_code = 405 + else: + collection_id = COLLECTIONS[0].id + stix_id = STIX_OBJECTS[0].id + if method == "get": + expected_status_code = 401 + elif method == "delete": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_object", + side_effect=GET_OBJECT_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/{stix_id}/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code diff --git a/tests/taxii2/test_taxii2_objects.py b/tests/taxii2/test_taxii2_objects.py index 68d55cb8..b2628e6b 100644 --- a/tests/taxii2/test_taxii2_objects.py +++ b/tests/taxii2/test_taxii2_objects.py @@ -1,1348 +1,1348 @@ -import datetime -import json -from unittest.mock import patch -from urllib.parse import urlencode -from uuid import uuid4 - -import pytest - -from opentaxii.taxii2.utils import taxii2_datetimeformat -from tests.taxii2.utils import ( - ADD_OBJECTS_MOCK, - API_ROOTS, - COLLECTIONS, - GET_COLLECTION_MOCK, - GET_JOB_AND_DETAILS_MOCK, - GET_NEXT_PARAM, - GET_OBJECTS_MOCK, - JOBS, - NOW, - STIX_OBJECTS, - config_noop, - config_override, -) -from tests.utils import SKIP - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "collection_id", - "filter_kwargs", - "post_data", - "config_override_func", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].alias, - {}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, by alias", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW)}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[1:2] - ], - }, - id="good, added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - {}, - id="good, no results", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, - {}, - config_noop, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"added_after": ["Not a valid datetime."]}, - "name": "validation error", - }, - id="broken added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 1}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": True, - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ), - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 2}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, limit exact", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": 999}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, limit high", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"limit": "a"}, - {}, - config_noop, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"limit": ["Not a valid integer."]}, - "name": "validation error", - }, - id="broken limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ) - }, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[1:2] - ], - }, - id="good, next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"next": "a"}, - {}, - config_noop, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"next": ["Not a valid value."]}, - "name": "validation error", - }, - id="broken next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[id]": STIX_OBJECTS[0].id}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, id", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[id]": ",".join([obj.id for obj in STIX_OBJECTS[:3]])}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, ids", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[type]": STIX_OBJECTS[0].type}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in [STIX_OBJECTS[0]] - ], - }, - id="good, type", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[type]": ",".join([obj.type for obj in STIX_OBJECTS[:3]])}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, types", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "last"}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, version last", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "first"}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[1:3] - ], - }, - id="good, version first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[version]": "all"}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:3] - ], - }, - id="good, version all", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "match[version]": ",".join( - [taxii2_datetimeformat(obj.version) for obj in STIX_OBJECTS[:3]] - ) - }, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:3] - ], - }, - id="good, versions", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {"match[spec_version]": STIX_OBJECTS[0].spec_version}, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:1] - ], - }, - id="good, spec_version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - { - "match[spec_version]": ",".join( - [obj.spec_version for obj in STIX_OBJECTS[:3]] - ) - }, - {}, - config_noop, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=2) - ), - }, - { - "more": False, - "objects": [ - { - "id": obj.id, - "type": obj.type, - "spec_version": obj.spec_version, - **obj.serialized_data, - } - for obj in STIX_OBJECTS[:2] - ], - }, - id="good, spec_versions", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[1].id, - {}, - {}, - config_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="write-only collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - {}, - {}, - config_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown collection", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 202, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(JOBS[0].id), - "status": JOBS[0].status, - "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), - "total_count": 4, - "success_count": 1, - "successes": [ - { - "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", - "version": "2018-05-27T12:02:41.312000Z", - } - ], - "failure_count": 1, - "failures": [ - { - "id": "malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", - "version": "2018-05-28T14:03:42.543000Z", - "message": "Unable to process object", - } - ], - "pending_count": 2, - "pendings": [ - { - "id": "indicator--252c7c11-daf2-42bd-843b-be65edca9f61", - "version": "2018-05-18T20:16:21.148000Z", - }, - { - "id": "relationship--045585ad-a22f-4333-af33-bfd503a683b5", - "version": "2018-05-15T10:13:32.579000Z", - }, - ], - }, - id="post, good", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - "x_my_custom_attr": "custom value", - } - ] - }, - config_noop, - 202, - SKIP, - SKIP, - id="post, good, custom property, no config", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - "x_my_custom_attr": "custom value", - } - ] - }, - config_override({"allow_custom_properties": True}), - 202, - SKIP, - SKIP, - id="post, good, custom property, allowed", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - "x_my_custom_attr": "custom value", - } - ] - }, - config_override({"allow_custom_properties": False}), - 400, - SKIP, - SKIP, - id="post, good, custom property, not allowed", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 400, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 400, - "description": ["No objects"], - "name": "validation error", - }, - id="post, missing data", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[0].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="post, read-only collection", - ), - pytest.param( - "post", - { - "Accept": "xml", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - {}, - config_noop, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="post, wrong accept header", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 415, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 415, - "description": ( - "The server does not support the media type transmitted in the " - "request." - ), - "name": "Unsupported Media Type", - }, - id="post, missing content-type header", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "xml", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 415, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 415, - "description": ( - "The server does not support the media type transmitted in the " - "request." - ), - "name": "Unsupported Media Type", - }, - id="post, wrong content-type header", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy" * 33, - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 413, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 413, - "description": "The data value transmitted exceeds the capacity limit.", - "name": "Request Entity Too Large", - }, - id="post, too big", - ), - pytest.param( - "post", - { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - "Content-Length": 1, - }, - API_ROOTS[0].id, - COLLECTIONS[5].id, - {}, - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy" * 33, - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - }, - config_noop, - 413, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 413, - "description": "The data value transmitted exceeds the capacity limit.", - "name": "Request Entity Too Large", - }, - id="post, too big, fake content-length", - ), - ], -) -def test_objects( - authenticated_client, - method, - api_root_id, - collection_id, - filter_kwargs, - post_data, - headers, - config_override_func, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2, - "config", - config_override_func( - authenticated_client.application.taxii_server.servers.taxii2.config - ), - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_objects", - side_effect=GET_OBJECTS_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write", "read"], - }, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "add_objects", - side_effect=ADD_OBJECTS_MOCK, - ) as add_objects_mock, - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_job_and_details", - side_effect=GET_JOB_AND_DETAILS_MOCK, - ), - ): - func = getattr(authenticated_client, method) - if filter_kwargs: - querystring = f"?{urlencode(filter_kwargs)}" - else: - querystring = "" - kwargs = {"headers": headers} - if method == "post": - kwargs["json"] = post_data - response = func( - f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{querystring}", - **kwargs, - ) - assert response.status_code == expected_status - if method == "post" and expected_status == 202: - add_objects_mock.assert_called_once_with( - api_root_id=API_ROOTS[0].id, - collection_id=COLLECTIONS[5].id, - objects=post_data["objects"], - ) - else: - add_objects_mock.assert_not_called() - if expected_headers != SKIP: - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - if expected_content != SKIP: - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("is_public_write", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_objects_unauthenticated( - client, - method, - is_public, - is_public_write, -): - if is_public: - collection_id = COLLECTIONS[6].id - if method == "get": - expected_status_code = 200 - elif method == "post": - expected_status_code = 401 - else: - expected_status_code = 405 - elif is_public_write: - collection_id = COLLECTIONS[7].id - if method == "get": - expected_status_code = 401 - elif method == "post": - expected_status_code = 202 - else: - expected_status_code = 405 - else: - collection_id = COLLECTIONS[0].id - if method == "get": - expected_status_code = 401 - elif method == "post": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_objects", - side_effect=GET_OBJECTS_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "add_objects", - side_effect=ADD_OBJECTS_MOCK, - ) as add_objects_mock, - ): - kwargs: dict = { - "headers": { - "Accept": "application/taxii+json;version=2.1", - "Content-Type": "application/taxii+json;version=2.1", - } - } - if method == "post": - kwargs["json"] = { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ] - } - func = getattr(client, method) - response = func( - f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/", - **kwargs, - ) - assert response.status_code == expected_status_code - if method == "post" and expected_status_code == 202: - add_objects_mock.assert_called_once_with( - api_root_id=API_ROOTS[0].id, - collection_id=COLLECTIONS[7].id, - objects=kwargs["json"]["objects"], - ) - else: - add_objects_mock.assert_not_called() +import datetime +import json +from unittest.mock import patch +from urllib.parse import urlencode +from uuid import uuid4 + +import pytest + +from opentaxii.taxii2.utils import taxii2_datetimeformat +from tests.taxii2.utils import ( + ADD_OBJECTS_MOCK, + API_ROOTS, + COLLECTIONS, + GET_COLLECTION_MOCK, + GET_JOB_AND_DETAILS_MOCK, + GET_NEXT_PARAM, + GET_OBJECTS_MOCK, + JOBS, + NOW, + STIX_OBJECTS, + config_noop, + config_override, +) +from tests.utils import SKIP + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "collection_id", + "filter_kwargs", + "post_data", + "config_override_func", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].alias, + {}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, by alias", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW)}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[1:2] + ], + }, + id="good, added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3))}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + {}, + id="good, no results", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, + {}, + config_noop, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"added_after": ["Not a valid datetime."]}, + "name": "validation error", + }, + id="broken added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 1}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": True, + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ), + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 2}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, limit exact", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": 999}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, limit high", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"limit": "a"}, + {}, + config_noop, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"limit": ["Not a valid integer."]}, + "name": "validation error", + }, + id="broken limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ) + }, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[1:2] + ], + }, + id="good, next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"next": "a"}, + {}, + config_noop, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"next": ["Not a valid value."]}, + "name": "validation error", + }, + id="broken next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[id]": STIX_OBJECTS[0].id}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, id", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[id]": ",".join([obj.id for obj in STIX_OBJECTS[:3]])}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, ids", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[type]": STIX_OBJECTS[0].type}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in [STIX_OBJECTS[0]] + ], + }, + id="good, type", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[type]": ",".join([obj.type for obj in STIX_OBJECTS[:3]])}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, types", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": taxii2_datetimeformat(STIX_OBJECTS[0].version)}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "last"}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, version last", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "first"}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[1:3] + ], + }, + id="good, version first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[version]": "all"}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:3] + ], + }, + id="good, version all", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "match[version]": ",".join( + [taxii2_datetimeformat(obj.version) for obj in STIX_OBJECTS[:3]] + ) + }, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:3] + ], + }, + id="good, versions", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {"match[spec_version]": STIX_OBJECTS[0].spec_version}, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:1] + ], + }, + id="good, spec_version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + { + "match[spec_version]": ",".join( + [obj.spec_version for obj in STIX_OBJECTS[:3]] + ) + }, + {}, + config_noop, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=2) + ), + }, + { + "more": False, + "objects": [ + { + "id": obj.id, + "type": obj.type, + "spec_version": obj.spec_version, + **obj.serialized_data, + } + for obj in STIX_OBJECTS[:2] + ], + }, + id="good, spec_versions", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[1].id, + {}, + {}, + config_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="write-only collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + {}, + {}, + config_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown collection", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 202, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(JOBS[0].id), + "status": JOBS[0].status, + "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), + "total_count": 4, + "success_count": 1, + "successes": [ + { + "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", + "version": "2018-05-27T12:02:41.312000Z", + } + ], + "failure_count": 1, + "failures": [ + { + "id": "malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", + "version": "2018-05-28T14:03:42.543000Z", + "message": "Unable to process object", + } + ], + "pending_count": 2, + "pendings": [ + { + "id": "indicator--252c7c11-daf2-42bd-843b-be65edca9f61", + "version": "2018-05-18T20:16:21.148000Z", + }, + { + "id": "relationship--045585ad-a22f-4333-af33-bfd503a683b5", + "version": "2018-05-15T10:13:32.579000Z", + }, + ], + }, + id="post, good", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + "x_my_custom_attr": "custom value", + } + ] + }, + config_noop, + 202, + SKIP, + SKIP, + id="post, good, custom property, no config", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + "x_my_custom_attr": "custom value", + } + ] + }, + config_override({"allow_custom_properties": True}), + 202, + SKIP, + SKIP, + id="post, good, custom property, allowed", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + "x_my_custom_attr": "custom value", + } + ] + }, + config_override({"allow_custom_properties": False}), + 400, + SKIP, + SKIP, + id="post, good, custom property, not allowed", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 400, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 400, + "description": ["No objects"], + "name": "validation error", + }, + id="post, missing data", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[0].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="post, read-only collection", + ), + pytest.param( + "post", + { + "Accept": "xml", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + {}, + config_noop, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="post, wrong accept header", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 415, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 415, + "description": ( + "The server does not support the media type transmitted in the " + "request." + ), + "name": "Unsupported Media Type", + }, + id="post, missing content-type header", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "xml", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 415, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 415, + "description": ( + "The server does not support the media type transmitted in the " + "request." + ), + "name": "Unsupported Media Type", + }, + id="post, wrong content-type header", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy" * 33, + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 413, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 413, + "description": "The data value transmitted exceeds the capacity limit.", + "name": "Request Entity Too Large", + }, + id="post, too big", + ), + pytest.param( + "post", + { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + "Content-Length": 1, + }, + API_ROOTS[0].id, + COLLECTIONS[5].id, + {}, + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy" * 33, + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + }, + config_noop, + 413, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 413, + "description": "The data value transmitted exceeds the capacity limit.", + "name": "Request Entity Too Large", + }, + id="post, too big, fake content-length", + ), + ], +) +def test_objects( + authenticated_client, + method, + api_root_id, + collection_id, + filter_kwargs, + post_data, + headers, + config_override_func, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2, + "config", + config_override_func( + authenticated_client.application.taxii_server.servers.taxii2.config + ), + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_objects", + side_effect=GET_OBJECTS_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write", "read"], + }, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "add_objects", + side_effect=ADD_OBJECTS_MOCK, + ) as add_objects_mock, + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_job_and_details", + side_effect=GET_JOB_AND_DETAILS_MOCK, + ), + ): + func = getattr(authenticated_client, method) + if filter_kwargs: + querystring = f"?{urlencode(filter_kwargs)}" + else: + querystring = "" + kwargs = {"headers": headers} + if method == "post": + kwargs["json"] = post_data + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{querystring}", + **kwargs, + ) + assert response.status_code == expected_status + if method == "post" and expected_status == 202: + add_objects_mock.assert_called_once_with( + api_root_id=API_ROOTS[0].id, + collection_id=COLLECTIONS[5].id, + objects=post_data["objects"], + ) + else: + add_objects_mock.assert_not_called() + if expected_headers != SKIP: + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + if expected_content != SKIP: + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("is_public_write", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_objects_unauthenticated( + client, + method, + is_public, + is_public_write, +): + if is_public: + collection_id = COLLECTIONS[6].id + if method == "get": + expected_status_code = 200 + elif method == "post": + expected_status_code = 401 + else: + expected_status_code = 405 + elif is_public_write: + collection_id = COLLECTIONS[7].id + if method == "get": + expected_status_code = 401 + elif method == "post": + expected_status_code = 202 + else: + expected_status_code = 405 + else: + collection_id = COLLECTIONS[0].id + if method == "get": + expected_status_code = 401 + elif method == "post": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_objects", + side_effect=GET_OBJECTS_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "add_objects", + side_effect=ADD_OBJECTS_MOCK, + ) as add_objects_mock, + ): + kwargs: dict = { + "headers": { + "Accept": "application/taxii+json;version=2.1", + "Content-Type": "application/taxii+json;version=2.1", + } + } + if method == "post": + kwargs["json"] = { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ] + } + func = getattr(client, method) + response = func( + f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/", + **kwargs, + ) + assert response.status_code == expected_status_code + if method == "post" and expected_status_code == 202: + add_objects_mock.assert_called_once_with( + api_root_id=API_ROOTS[0].id, + collection_id=COLLECTIONS[7].id, + objects=kwargs["json"]["objects"], + ) + else: + add_objects_mock.assert_not_called() diff --git a/tests/taxii2/test_taxii2_sqldb.py b/tests/taxii2/test_taxii2_sqldb.py index 2b490350..3bf09d98 100644 --- a/tests/taxii2/test_taxii2_sqldb.py +++ b/tests/taxii2/test_taxii2_sqldb.py @@ -1,1463 +1,1463 @@ -import datetime -from uuid import uuid4 - -import pytest - -from opentaxii.persistence.sqldb.api import Taxii2SQLDatabaseAPI -from opentaxii.persistence.sqldb.taxii2models import Job, JobDetail, STIXObject -from opentaxii.taxii2 import entities -from opentaxii.taxii2.utils import get_object_version -from tests.taxii2.utils import ( - API_ROOTS, - API_ROOTS_WITH_DEFAULT, - API_ROOTS_WITHOUT_DEFAULT, - COLLECTIONS, - GET_API_ROOT_MOCK, - GET_COLLECTION_MOCK, - GET_COLLECTIONS_MOCK, - GET_JOB_AND_DETAILS_MOCK, - GET_MANIFEST_MOCK, - GET_OBJECT_MOCK, - GET_OBJECTS_MOCK, - GET_VERSIONS_MOCK, - JOBS, - NOW, - STIX_OBJECTS, -) - - -@pytest.mark.parametrize( - ["db_api_roots"], - [ - pytest.param( - API_ROOTS_WITHOUT_DEFAULT, # db_api_roots - id="without default", - ), - pytest.param( - API_ROOTS_WITH_DEFAULT, # db_api_roots - id="with default", - ), - ], - indirect=["db_api_roots"], -) -def test_get_api_roots(taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots): - response = taxii2_sqldb_api.get_api_roots() - assert response == [api_root for api_root in db_api_roots] - - -@pytest.mark.parametrize( - ["api_root_id"], - [ - pytest.param( - API_ROOTS[0].id, # api_root_id - id="first", - ), - pytest.param( - API_ROOTS[1].id, # api_root_id - id="second", - ), - pytest.param( - str(uuid4()), # api_root_id - id="unknown", - ), - ], -) -def test_get_api_root( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots, api_root_id -): - response = taxii2_sqldb_api.get_api_root(api_root_id) - assert response == GET_API_ROOT_MOCK(api_root_id) - - -@pytest.mark.parametrize( - ["api_root_id", "job_id"], - [ - pytest.param( - API_ROOTS[0].id, # api_root_id - JOBS[0].id, # job_id - id="first", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - JOBS[1].id, # job_id - id="second", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - JOBS[2].id, # job_id - id="wrong api root", - ), - pytest.param( - str(uuid4()), # api_root_id - JOBS[0].id, # job_id - id="unknown api root", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - str(uuid4()), # job_id - id="unknown job id", - ), - ], -) -def test_get_job_and_details( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_jobs, api_root_id, job_id -): - response = taxii2_sqldb_api.get_job_and_details(api_root_id, job_id) - assert response == GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id) - - -@pytest.mark.parametrize( - ["api_root_id"], - [ - pytest.param( - API_ROOTS[0].id, - id="first", - ), - pytest.param( - API_ROOTS[1].id, - id="second", - ), - pytest.param( - str(uuid4()), - id="unknown", - ), - ], -) -def test_get_collections( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_collections, api_root_id -): - response = taxii2_sqldb_api.get_collections(api_root_id) - assert response == GET_COLLECTIONS_MOCK(api_root_id) - - -@pytest.mark.parametrize( - ["api_root_id", "collection_id_or_alias"], - [ - pytest.param( - API_ROOTS[0].id, - COLLECTIONS[0].id, - id="first", - ), - pytest.param( - API_ROOTS[0].id, - COLLECTIONS[1].id, - id="second", - ), - pytest.param( - API_ROOTS[0].id, - COLLECTIONS[5].alias, - id="alias", - ), - pytest.param( - API_ROOTS[1].id, - COLLECTIONS[0].id, - id="wrong api root", - ), - pytest.param( - uuid4(), - COLLECTIONS[0].id, - id="unknown api root", - ), - pytest.param( - API_ROOTS[0].id, - uuid4(), - id="unknown collection id", - ), - ], -) -def test_get_collection( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_collections, - api_root_id, - collection_id_or_alias, -): - response = taxii2_sqldb_api.get_collection(api_root_id, str(collection_id_or_alias)) - assert response == GET_COLLECTION_MOCK(api_root_id, str(collection_id_or_alias)) - - -@pytest.mark.parametrize( - [ - "collection_id", - "limit", - "added_after", - "next_kwargs", - "match_id", - "match_type", - "match_version", - "match_spec_version", - ], - [ - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="default", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 1, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit low", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 2, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit exact", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 999, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit high", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - NOW, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="added_after", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - { - "id": STIX_OBJECTS[0].id, - "date_added": STIX_OBJECTS[0].date_added, - }, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="next_kwargs", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[0].id], # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="match_id", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - [obj.id for obj in STIX_OBJECTS[:3]], # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="match_id multiple", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - [STIX_OBJECTS[0].type], # match_type - None, # match_version - None, # match_spec_version - id="match_type", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - [obj.type for obj in STIX_OBJECTS[:3]], # match_type - None, # match_version - None, # match_spec_version - id="match_type multiple", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[0].version], # match_version - None, # match_spec_version - id="version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[1].version], # match_version - None, # match_spec_version - id="version [1]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [0, 2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["first"], # match_version - None, # match_spec_version - id="version first", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["last"], # match_version - None, # match_spec_version - id="version last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["all"], # match_version - None, # match_spec_version - id="version all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["first", "last"], # match_version - None, # match_spec_version - id="version first, last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[2].version, "last"], # match_version - None, # match_spec_version - id="version [2], last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [STIX_OBJECTS[0].spec_version], # match_spec_version - id="spec_version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [STIX_OBJECTS[1].spec_version], # match_spec_version - id="spec_version [1]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [ - STIX_OBJECTS[0].spec_version, - STIX_OBJECTS[1].spec_version, - ], # match_spec_version - id="spec_version [0, 1]", - ), - ], -) -def test_get_manifest( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - collection_id, - limit, - added_after, - next_kwargs, - match_id, - match_type, - match_version, - match_spec_version, -): - response = taxii2_sqldb_api.get_manifest( - collection_id=collection_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_id=match_id, - match_type=match_type, - match_version=match_version, - match_spec_version=match_spec_version, - ) - assert response == GET_MANIFEST_MOCK( - collection_id=collection_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_id=match_id, - match_type=match_type, - match_version=match_version, - match_spec_version=match_spec_version, - ) - - -@pytest.mark.parametrize( - [ - "collection_id", - "limit", - "added_after", - "next_kwargs", - "match_id", - "match_type", - "match_version", - "match_spec_version", - ], - [ - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="default", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 1, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit low", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 2, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit exact", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - 999, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="limit high", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - NOW, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="added_after", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - { - "id": STIX_OBJECTS[0].id, - "date_added": STIX_OBJECTS[0].date_added, - }, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="next_kwargs", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[0].id], # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="match_id", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - [obj.id for obj in STIX_OBJECTS[:3]], # match_id - None, # match_type - None, # match_version - None, # match_spec_version - id="match_id multiple", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - [STIX_OBJECTS[0].type], # match_type - None, # match_version - None, # match_spec_version - id="match_type", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - [obj.type for obj in STIX_OBJECTS[:3]], # match_type - None, # match_version - None, # match_spec_version - id="match_type multiple", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[0].version], # match_version - None, # match_spec_version - id="version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[1].version], # match_version - None, # match_spec_version - id="version [1]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [0, 2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["first"], # match_version - None, # match_spec_version - id="version first", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["last"], # match_version - None, # match_spec_version - id="version last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["all"], # match_version - None, # match_spec_version - id="version all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - ["first", "last"], # match_version - None, # match_spec_version - id="version first, last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - [STIX_OBJECTS[2].version, "last"], # match_version - None, # match_spec_version - id="version [2], last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [STIX_OBJECTS[0].spec_version], # match_spec_version - id="spec_version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [STIX_OBJECTS[1].spec_version], # match_spec_version - id="spec_version [1]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_id - None, # match_type - None, # match_version - [ - STIX_OBJECTS[0].spec_version, - STIX_OBJECTS[1].spec_version, - ], # match_spec_version - id="spec_version [0, 1]", - ), - ], -) -def test_get_objects( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - collection_id, - limit, - added_after, - next_kwargs, - match_id, - match_type, - match_version, - match_spec_version, -): - response = taxii2_sqldb_api.get_objects( - collection_id=collection_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_id=match_id, - match_type=match_type, - match_version=match_version, - match_spec_version=match_spec_version, - ) - assert response == GET_OBJECTS_MOCK( - collection_id=collection_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_id=match_id, - match_type=match_type, - match_version=match_version, - match_spec_version=match_spec_version, - ) - - -@pytest.mark.parametrize( - [ - "api_root_id", - "collection_id", - "objects", - ], - [ - pytest.param( - API_ROOTS[0].id, # api_root_id - COLLECTIONS[5].id, # collection_id - [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - } - ], # objects - id="single-object-version-modified", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - COLLECTIONS[5].id, # collection_id - [ - { - "definition": { - "statement": "Copyright 2015-2025, The MITRE Corporation. [...]" - }, - "id": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", - "type": "marking-definition", - "created": "2017-06-01T00:00:00.000Z", - "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - "definition_type": "statement", - "spec_version": "2.1", - } - ], # objects - id="single-object-version-created", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - COLLECTIONS[5].id, # collection_id - [ - { - "type": "x-no-version", - "id": "x-no-version--5e113376-8a13-432d-b711-92f566ebbd92", - "spec_version": "2.1", - } - ], # objects - id="single-object-version-created", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - COLLECTIONS[5].id, # collection_id - [ - { - "type": "relationship", - "spec_version": "2.1", - "id": "relationship--44298a74-ba52-4f0c-87a3-1824e67d7fad", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:06:37.000Z", - "modified": "2016-04-06T20:06:37.000Z", - "relationship_type": "indicates", - "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", - }, - { - "type": "malware", - "spec_version": "2.1", - "id": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", - "is_family": True, - "created": "2016-04-06T20:07:09.000Z", - "modified": "2016-04-06T20:07:09.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "name": "Poison Ivy", - "malware_types": ["trojan"], - }, - ], # objects - id="multiple objects", - ), - pytest.param( - API_ROOTS[0].id, # api_root_id - COLLECTIONS[5].id, # collection_id - [ - { - "id": STIX_OBJECTS[0].id, - "type": STIX_OBJECTS[0].type, - "spec_version": STIX_OBJECTS[0].spec_version, - **STIX_OBJECTS[0].serialized_data, - } - ], # objects - id="existing object", - ), - ], -) -def test_add_objects( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - api_root_id, - collection_id, - objects, -): - job = taxii2_sqldb_api.add_objects( - api_root_id=api_root_id, - collection_id=collection_id, - objects=objects, - ) - # Check response entities - assert job == entities.Job( - id=job.id, - api_root_id=api_root_id, - status="complete", - request_timestamp=job.request_timestamp, - completed_timestamp=job.completed_timestamp, - total_count=len(objects), - success_count=len(objects), - failure_count=0, - pending_count=0, - details=entities.JobDetails( - [ - entities.JobDetail( - id=job_detail.id, - job_id=job.id, - stix_id=obj["id"], - version=get_object_version(obj), - message="", - status="success", - ) - for (job_detail, obj) in zip(job.details.success, objects) - ], - [], - [], - ), - ) - assert isinstance(job.request_timestamp, datetime.datetime) - assert isinstance(job.completed_timestamp, datetime.datetime) - # Check database state - db_job = taxii2_sqldb_api.db.session.query(Job).one() - assert db_job.api_root_id == api_root_id - assert db_job.status == "complete" - assert isinstance(db_job.request_timestamp, datetime.datetime) - assert isinstance(db_job.completed_timestamp, datetime.datetime) - for obj in objects: - db_obj = ( - taxii2_sqldb_api.db.session.query(STIXObject) - .filter( - STIXObject.id == obj["id"], - STIXObject.version == get_object_version(obj), - ) - .one() - ) - assert db_obj.id == obj["id"] - assert db_obj.collection_id == collection_id - assert db_obj.type == obj["type"] - assert db_obj.spec_version == obj["spec_version"] - assert isinstance(db_obj.date_added, datetime.datetime) - assert db_obj.version == get_object_version(obj) - assert db_obj.serialized_data == { - key: value - for (key, value) in obj.items() - if key not in ["id", "type", "spec_version"] - } - db_job_detail = ( - taxii2_sqldb_api.db.session.query(JobDetail) - .filter(JobDetail.stix_id == obj["id"]) - .one() - ) - assert db_job_detail.job_id == db_job.id - assert db_job_detail.stix_id == obj["id"] - assert db_job_detail.version == get_object_version(obj) - assert db_job_detail.message == "" - assert db_job_detail.status == "success" - - -@pytest.mark.parametrize( - [ - "collection_id", - "object_id", - "limit", - "added_after", - "next_kwargs", - "match_version", - "match_spec_version", - ], - [ - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="default", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - str(uuid4()), # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="unknown object", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - NOW, # added_after - None, # next_kwargs - ["all"], # match_version - None, # match_spec_version - id="added_after, all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - NOW, # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="added_after", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - NOW + datetime.timedelta(seconds=3), # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="added_after, no results", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 1, # limit - None, # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="limit", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 1, # limit - None, # added_after - None, # next_kwargs - ["all"], # match_version - None, # match_spec_version - id="limit, all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 2, # limit - None, # added_after - None, # next_kwargs - ["all"], # match_version - None, # match_spec_version - id="limit exact, all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 999, # limit - None, # added_after - None, # next_kwargs - None, # match_version - None, # match_spec_version - id="limit high", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - { - "id": STIX_OBJECTS[0].id, - "date_added": STIX_OBJECTS[0].date_added, - }, # next_kwargs - None, # match_version - None, # match_spec_version - id="next", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - { - "id": STIX_OBJECTS[0].id, - "date_added": STIX_OBJECTS[0].date_added, - }, # next_kwargs - ["all"], # match_version - None, # match_spec_version - id="next, all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[0].version], # match_version - None, # match_spec_version - id="version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[1].version], # match_version - None, # match_spec_version - id="version [1]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version - None, # match_spec_version - id="version [0, 2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - ["first"], # match_version - None, # match_spec_version - id="version first", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - ["last"], # match_version - None, # match_spec_version - id="version last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - ["all"], # match_version - None, # match_spec_version - id="version all", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - ["first", "last"], # match_version - None, # match_spec_version - id="version first, last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[2].version, "last"], # match_version - None, # match_spec_version - id="version [2], last", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_version - [STIX_OBJECTS[0].spec_version], # match_spec_version - id="spec_version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_version - [STIX_OBJECTS[2].spec_version], # match_spec_version - id="spec_version [2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_version - [ - STIX_OBJECTS[0].spec_version, - STIX_OBJECTS[2].spec_version, - ], # match_spec_version - id="spec_version [0, 2]", - ), - ], -) -def test_get_object( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - collection_id, - object_id, - limit, - added_after, - next_kwargs, - match_version, - match_spec_version, -): - response = taxii2_sqldb_api.get_object( - collection_id=collection_id, - object_id=object_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_version=match_version, - match_spec_version=match_spec_version, - ) - assert response == GET_OBJECT_MOCK( - collection_id=collection_id, - object_id=object_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_version=match_version, - match_spec_version=match_spec_version, - ) - - -@pytest.mark.parametrize( - [ - "collection_id", - "object_id", - "match_version", - "match_spec_version", - "expected_objects", - ], - [ - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # match_version - None, # match_spec_version - [STIX_OBJECTS[1], STIX_OBJECTS[3], STIX_OBJECTS[4]], # expected_objects - id="default", - ), - pytest.param( - COLLECTIONS[4].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # match_version - None, # match_spec_version - STIX_OBJECTS, # expected_objects - id="wrong collection", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - [STIX_OBJECTS[0].version], # match_version - None, # match_spec_version - STIX_OBJECTS[1:], # expected_objects - id="version", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # match_version - [STIX_OBJECTS[0].spec_version], # match_spec_version - STIX_OBJECTS[1:], # expected_objects - id="version", - ), - ], -) -def test_delete_object( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - collection_id, - object_id, - match_version, - match_spec_version, - expected_objects, -): - taxii2_sqldb_api.delete_object( - collection_id=collection_id, - object_id=object_id, - match_version=match_version, - match_spec_version=match_spec_version, - ) - # Verify the previous function committed - taxii2_sqldb_api.db.session.rollback() - assert set( - (db_obj.collection_id, db_obj.id, db_obj.version) - for db_obj in taxii2_sqldb_api.db.session.query(STIXObject).all() - ) == set((obj.collection_id, obj.id, obj.version) for obj in expected_objects) - - -@pytest.mark.parametrize( - [ - "collection_id", - "object_id", - "limit", - "added_after", - "next_kwargs", - "match_spec_version", - ], - [ - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_spec_version - id="default", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - str(uuid4()), # object_id - None, # limit - None, # added_after - None, # next_kwargs - None, # match_spec_version - id="unknown object", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - NOW, # added_after - None, # next_kwargs - None, # match_spec_version - id="added_after", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - NOW + datetime.timedelta(seconds=3), # added_after - None, # next_kwargs - None, # match_spec_version - id="added_after, no results", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 1, # limit - None, # added_after - None, # next_kwargs - None, # match_spec_version - id="limit", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - 999, # limit - None, # added_after - None, # next_kwargs - None, # match_spec_version - id="limit high", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - { - "id": STIX_OBJECTS[0].id, - "date_added": STIX_OBJECTS[0].date_added, - }, # next_kwargs - None, # match_spec_version - id="next", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[0].spec_version], # match_spec_version - id="spec_version [0]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [STIX_OBJECTS[2].spec_version], # match_spec_version - id="spec_version [2]", - ), - pytest.param( - COLLECTIONS[5].id, # collection_id - STIX_OBJECTS[0].id, # object_id - None, # limit - None, # added_after - None, # next_kwargs - [ - STIX_OBJECTS[0].spec_version, - STIX_OBJECTS[2].spec_version, - ], # match_spec_version - id="spec_version [0, 2]", - ), - ], -) -def test_get_versions( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, - db_stix_objects, - collection_id, - object_id, - limit, - added_after, - next_kwargs, - match_spec_version, -): - response = taxii2_sqldb_api.get_versions( - collection_id=collection_id, - object_id=object_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_spec_version=match_spec_version, - ) - assert response == GET_VERSIONS_MOCK( - collection_id=collection_id, - object_id=object_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_spec_version=match_spec_version, - ) - - -@pytest.mark.parametrize( - "stix_id, date_added, next_param", - [ - pytest.param( - "indicator--fa641b92-94d7-42dd-aa0e-63cfe1ee148a", - datetime.datetime( - 2022, 2, 4, 18, 40, 6, 297204, tzinfo=datetime.timezone.utc - ), - ( - "MjAyMi0wMi0wNFQxODo0MDowNi4yOTcyMDQrMDA6MDB8aW5kaWNhdG9yLS1mYTY0M" - "WI5Mi05NGQ3LTQyZGQtYWEwZS02M2NmZTFlZTE0OGE=" - ), - id="simple", - ), - ], -) -def test_next_param( - taxii2_sqldb_api: Taxii2SQLDatabaseAPI, stix_id, date_added, next_param -): - assert ( - taxii2_sqldb_api.get_next_param({"id": stix_id, "date_added": date_added}) - == next_param - ) - assert taxii2_sqldb_api.parse_next_param(next_param) == { - "id": stix_id, - "date_added": date_added, - } +import datetime +from uuid import uuid4 + +import pytest + +from opentaxii.persistence.sqldb.api import Taxii2SQLDatabaseAPI +from opentaxii.persistence.sqldb.taxii2models import Job, JobDetail, STIXObject +from opentaxii.taxii2 import entities +from opentaxii.taxii2.utils import get_object_version +from tests.taxii2.utils import ( + API_ROOTS, + API_ROOTS_WITH_DEFAULT, + API_ROOTS_WITHOUT_DEFAULT, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTION_MOCK, + GET_COLLECTIONS_MOCK, + GET_JOB_AND_DETAILS_MOCK, + GET_MANIFEST_MOCK, + GET_OBJECT_MOCK, + GET_OBJECTS_MOCK, + GET_VERSIONS_MOCK, + JOBS, + NOW, + STIX_OBJECTS, +) + + +@pytest.mark.parametrize( + ["db_api_roots"], + [ + pytest.param( + API_ROOTS_WITHOUT_DEFAULT, # db_api_roots + id="without default", + ), + pytest.param( + API_ROOTS_WITH_DEFAULT, # db_api_roots + id="with default", + ), + ], + indirect=["db_api_roots"], +) +def test_get_api_roots(taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots): + response = taxii2_sqldb_api.get_api_roots() + assert response == [api_root for api_root in db_api_roots] + + +@pytest.mark.parametrize( + ["api_root_id"], + [ + pytest.param( + API_ROOTS[0].id, # api_root_id + id="first", + ), + pytest.param( + API_ROOTS[1].id, # api_root_id + id="second", + ), + pytest.param( + str(uuid4()), # api_root_id + id="unknown", + ), + ], +) +def test_get_api_root( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots, api_root_id +): + response = taxii2_sqldb_api.get_api_root(api_root_id) + assert response == GET_API_ROOT_MOCK(api_root_id) + + +@pytest.mark.parametrize( + ["api_root_id", "job_id"], + [ + pytest.param( + API_ROOTS[0].id, # api_root_id + JOBS[0].id, # job_id + id="first", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + JOBS[1].id, # job_id + id="second", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + JOBS[2].id, # job_id + id="wrong api root", + ), + pytest.param( + str(uuid4()), # api_root_id + JOBS[0].id, # job_id + id="unknown api root", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + str(uuid4()), # job_id + id="unknown job id", + ), + ], +) +def test_get_job_and_details( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_jobs, api_root_id, job_id +): + response = taxii2_sqldb_api.get_job_and_details(api_root_id, job_id) + assert response == GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id) + + +@pytest.mark.parametrize( + ["api_root_id"], + [ + pytest.param( + API_ROOTS[0].id, + id="first", + ), + pytest.param( + API_ROOTS[1].id, + id="second", + ), + pytest.param( + str(uuid4()), + id="unknown", + ), + ], +) +def test_get_collections( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_collections, api_root_id +): + response = taxii2_sqldb_api.get_collections(api_root_id) + assert response == GET_COLLECTIONS_MOCK(api_root_id) + + +@pytest.mark.parametrize( + ["api_root_id", "collection_id_or_alias"], + [ + pytest.param( + API_ROOTS[0].id, + COLLECTIONS[0].id, + id="first", + ), + pytest.param( + API_ROOTS[0].id, + COLLECTIONS[1].id, + id="second", + ), + pytest.param( + API_ROOTS[0].id, + COLLECTIONS[5].alias, + id="alias", + ), + pytest.param( + API_ROOTS[1].id, + COLLECTIONS[0].id, + id="wrong api root", + ), + pytest.param( + uuid4(), + COLLECTIONS[0].id, + id="unknown api root", + ), + pytest.param( + API_ROOTS[0].id, + uuid4(), + id="unknown collection id", + ), + ], +) +def test_get_collection( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_collections, + api_root_id, + collection_id_or_alias, +): + response = taxii2_sqldb_api.get_collection(api_root_id, str(collection_id_or_alias)) + assert response == GET_COLLECTION_MOCK(api_root_id, str(collection_id_or_alias)) + + +@pytest.mark.parametrize( + [ + "collection_id", + "limit", + "added_after", + "next_kwargs", + "match_id", + "match_type", + "match_version", + "match_spec_version", + ], + [ + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="default", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 1, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit low", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 2, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit exact", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 999, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit high", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + NOW, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="added_after", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + { + "id": STIX_OBJECTS[0].id, + "date_added": STIX_OBJECTS[0].date_added, + }, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="next_kwargs", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[0].id], # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="match_id", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + [obj.id for obj in STIX_OBJECTS[:3]], # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="match_id multiple", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + [STIX_OBJECTS[0].type], # match_type + None, # match_version + None, # match_spec_version + id="match_type", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + [obj.type for obj in STIX_OBJECTS[:3]], # match_type + None, # match_version + None, # match_spec_version + id="match_type multiple", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[0].version], # match_version + None, # match_spec_version + id="version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[1].version], # match_version + None, # match_spec_version + id="version [1]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [0, 2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["first"], # match_version + None, # match_spec_version + id="version first", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["last"], # match_version + None, # match_spec_version + id="version last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["all"], # match_version + None, # match_spec_version + id="version all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["first", "last"], # match_version + None, # match_spec_version + id="version first, last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[2].version, "last"], # match_version + None, # match_spec_version + id="version [2], last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [STIX_OBJECTS[0].spec_version], # match_spec_version + id="spec_version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [STIX_OBJECTS[1].spec_version], # match_spec_version + id="spec_version [1]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [ + STIX_OBJECTS[0].spec_version, + STIX_OBJECTS[1].spec_version, + ], # match_spec_version + id="spec_version [0, 1]", + ), + ], +) +def test_get_manifest( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + collection_id, + limit, + added_after, + next_kwargs, + match_id, + match_type, + match_version, + match_spec_version, +): + response = taxii2_sqldb_api.get_manifest( + collection_id=collection_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_id=match_id, + match_type=match_type, + match_version=match_version, + match_spec_version=match_spec_version, + ) + assert response == GET_MANIFEST_MOCK( + collection_id=collection_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_id=match_id, + match_type=match_type, + match_version=match_version, + match_spec_version=match_spec_version, + ) + + +@pytest.mark.parametrize( + [ + "collection_id", + "limit", + "added_after", + "next_kwargs", + "match_id", + "match_type", + "match_version", + "match_spec_version", + ], + [ + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="default", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 1, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit low", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 2, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit exact", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + 999, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="limit high", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + NOW, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="added_after", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + { + "id": STIX_OBJECTS[0].id, + "date_added": STIX_OBJECTS[0].date_added, + }, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="next_kwargs", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[0].id], # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="match_id", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + [obj.id for obj in STIX_OBJECTS[:3]], # match_id + None, # match_type + None, # match_version + None, # match_spec_version + id="match_id multiple", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + [STIX_OBJECTS[0].type], # match_type + None, # match_version + None, # match_spec_version + id="match_type", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + [obj.type for obj in STIX_OBJECTS[:3]], # match_type + None, # match_version + None, # match_spec_version + id="match_type multiple", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[0].version], # match_version + None, # match_spec_version + id="version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[1].version], # match_version + None, # match_spec_version + id="version [1]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [0, 2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["first"], # match_version + None, # match_spec_version + id="version first", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["last"], # match_version + None, # match_spec_version + id="version last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["all"], # match_version + None, # match_spec_version + id="version all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + ["first", "last"], # match_version + None, # match_spec_version + id="version first, last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + [STIX_OBJECTS[2].version, "last"], # match_version + None, # match_spec_version + id="version [2], last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [STIX_OBJECTS[0].spec_version], # match_spec_version + id="spec_version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [STIX_OBJECTS[1].spec_version], # match_spec_version + id="spec_version [1]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_id + None, # match_type + None, # match_version + [ + STIX_OBJECTS[0].spec_version, + STIX_OBJECTS[1].spec_version, + ], # match_spec_version + id="spec_version [0, 1]", + ), + ], +) +def test_get_objects( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + collection_id, + limit, + added_after, + next_kwargs, + match_id, + match_type, + match_version, + match_spec_version, +): + response = taxii2_sqldb_api.get_objects( + collection_id=collection_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_id=match_id, + match_type=match_type, + match_version=match_version, + match_spec_version=match_spec_version, + ) + assert response == GET_OBJECTS_MOCK( + collection_id=collection_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_id=match_id, + match_type=match_type, + match_version=match_version, + match_spec_version=match_spec_version, + ) + + +@pytest.mark.parametrize( + [ + "api_root_id", + "collection_id", + "objects", + ], + [ + pytest.param( + API_ROOTS[0].id, # api_root_id + COLLECTIONS[5].id, # collection_id + [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + } + ], # objects + id="single-object-version-modified", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + COLLECTIONS[5].id, # collection_id + [ + { + "definition": { + "statement": "Copyright 2015-2025, The MITRE Corporation. [...]" + }, + "id": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", + "type": "marking-definition", + "created": "2017-06-01T00:00:00.000Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "definition_type": "statement", + "spec_version": "2.1", + } + ], # objects + id="single-object-version-created", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + COLLECTIONS[5].id, # collection_id + [ + { + "type": "x-no-version", + "id": "x-no-version--5e113376-8a13-432d-b711-92f566ebbd92", + "spec_version": "2.1", + } + ], # objects + id="single-object-version-created", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + COLLECTIONS[5].id, # collection_id + [ + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--44298a74-ba52-4f0c-87a3-1824e67d7fad", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", + }, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", + "is_family": True, + "created": "2016-04-06T20:07:09.000Z", + "modified": "2016-04-06T20:07:09.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "name": "Poison Ivy", + "malware_types": ["trojan"], + }, + ], # objects + id="multiple objects", + ), + pytest.param( + API_ROOTS[0].id, # api_root_id + COLLECTIONS[5].id, # collection_id + [ + { + "id": STIX_OBJECTS[0].id, + "type": STIX_OBJECTS[0].type, + "spec_version": STIX_OBJECTS[0].spec_version, + **STIX_OBJECTS[0].serialized_data, + } + ], # objects + id="existing object", + ), + ], +) +def test_add_objects( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + api_root_id, + collection_id, + objects, +): + job = taxii2_sqldb_api.add_objects( + api_root_id=api_root_id, + collection_id=collection_id, + objects=objects, + ) + # Check response entities + assert job == entities.Job( + id=job.id, + api_root_id=api_root_id, + status="complete", + request_timestamp=job.request_timestamp, + completed_timestamp=job.completed_timestamp, + total_count=len(objects), + success_count=len(objects), + failure_count=0, + pending_count=0, + details=entities.JobDetails( + [ + entities.JobDetail( + id=job_detail.id, + job_id=job.id, + stix_id=obj["id"], + version=get_object_version(obj), + message="", + status="success", + ) + for (job_detail, obj) in zip(job.details.success, objects) + ], + [], + [], + ), + ) + assert isinstance(job.request_timestamp, datetime.datetime) + assert isinstance(job.completed_timestamp, datetime.datetime) + # Check database state + db_job = taxii2_sqldb_api.db.session.query(Job).one() + assert db_job.api_root_id == api_root_id + assert db_job.status == "complete" + assert isinstance(db_job.request_timestamp, datetime.datetime) + assert isinstance(db_job.completed_timestamp, datetime.datetime) + for obj in objects: + db_obj = ( + taxii2_sqldb_api.db.session.query(STIXObject) + .filter( + STIXObject.id == obj["id"], + STIXObject.version == get_object_version(obj), + ) + .one() + ) + assert db_obj.id == obj["id"] + assert db_obj.collection_id == collection_id + assert db_obj.type == obj["type"] + assert db_obj.spec_version == obj["spec_version"] + assert isinstance(db_obj.date_added, datetime.datetime) + assert db_obj.version == get_object_version(obj) + assert db_obj.serialized_data == { + key: value + for (key, value) in obj.items() + if key not in ["id", "type", "spec_version"] + } + db_job_detail = ( + taxii2_sqldb_api.db.session.query(JobDetail) + .filter(JobDetail.stix_id == obj["id"]) + .one() + ) + assert db_job_detail.job_id == db_job.id + assert db_job_detail.stix_id == obj["id"] + assert db_job_detail.version == get_object_version(obj) + assert db_job_detail.message == "" + assert db_job_detail.status == "success" + + +@pytest.mark.parametrize( + [ + "collection_id", + "object_id", + "limit", + "added_after", + "next_kwargs", + "match_version", + "match_spec_version", + ], + [ + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="default", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + str(uuid4()), # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="unknown object", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + NOW, # added_after + None, # next_kwargs + ["all"], # match_version + None, # match_spec_version + id="added_after, all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + NOW, # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="added_after", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + NOW + datetime.timedelta(seconds=3), # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="added_after, no results", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 1, # limit + None, # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="limit", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 1, # limit + None, # added_after + None, # next_kwargs + ["all"], # match_version + None, # match_spec_version + id="limit, all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 2, # limit + None, # added_after + None, # next_kwargs + ["all"], # match_version + None, # match_spec_version + id="limit exact, all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 999, # limit + None, # added_after + None, # next_kwargs + None, # match_version + None, # match_spec_version + id="limit high", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + { + "id": STIX_OBJECTS[0].id, + "date_added": STIX_OBJECTS[0].date_added, + }, # next_kwargs + None, # match_version + None, # match_spec_version + id="next", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + { + "id": STIX_OBJECTS[0].id, + "date_added": STIX_OBJECTS[0].date_added, + }, # next_kwargs + ["all"], # match_version + None, # match_spec_version + id="next, all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[0].version], # match_version + None, # match_spec_version + id="version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[1].version], # match_version + None, # match_spec_version + id="version [1]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[0].version, STIX_OBJECTS[2].version], # match_version + None, # match_spec_version + id="version [0, 2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + ["first"], # match_version + None, # match_spec_version + id="version first", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + ["last"], # match_version + None, # match_spec_version + id="version last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + ["all"], # match_version + None, # match_spec_version + id="version all", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + ["first", "last"], # match_version + None, # match_spec_version + id="version first, last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[2].version, "last"], # match_version + None, # match_spec_version + id="version [2], last", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_version + [STIX_OBJECTS[0].spec_version], # match_spec_version + id="spec_version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_version + [STIX_OBJECTS[2].spec_version], # match_spec_version + id="spec_version [2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_version + [ + STIX_OBJECTS[0].spec_version, + STIX_OBJECTS[2].spec_version, + ], # match_spec_version + id="spec_version [0, 2]", + ), + ], +) +def test_get_object( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + collection_id, + object_id, + limit, + added_after, + next_kwargs, + match_version, + match_spec_version, +): + response = taxii2_sqldb_api.get_object( + collection_id=collection_id, + object_id=object_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_version=match_version, + match_spec_version=match_spec_version, + ) + assert response == GET_OBJECT_MOCK( + collection_id=collection_id, + object_id=object_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_version=match_version, + match_spec_version=match_spec_version, + ) + + +@pytest.mark.parametrize( + [ + "collection_id", + "object_id", + "match_version", + "match_spec_version", + "expected_objects", + ], + [ + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # match_version + None, # match_spec_version + [STIX_OBJECTS[1], STIX_OBJECTS[3], STIX_OBJECTS[4]], # expected_objects + id="default", + ), + pytest.param( + COLLECTIONS[4].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # match_version + None, # match_spec_version + STIX_OBJECTS, # expected_objects + id="wrong collection", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + [STIX_OBJECTS[0].version], # match_version + None, # match_spec_version + STIX_OBJECTS[1:], # expected_objects + id="version", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # match_version + [STIX_OBJECTS[0].spec_version], # match_spec_version + STIX_OBJECTS[1:], # expected_objects + id="version", + ), + ], +) +def test_delete_object( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + collection_id, + object_id, + match_version, + match_spec_version, + expected_objects, +): + taxii2_sqldb_api.delete_object( + collection_id=collection_id, + object_id=object_id, + match_version=match_version, + match_spec_version=match_spec_version, + ) + # Verify the previous function committed + taxii2_sqldb_api.db.session.rollback() + assert set( + (db_obj.collection_id, db_obj.id, db_obj.version) + for db_obj in taxii2_sqldb_api.db.session.query(STIXObject).all() + ) == set((obj.collection_id, obj.id, obj.version) for obj in expected_objects) + + +@pytest.mark.parametrize( + [ + "collection_id", + "object_id", + "limit", + "added_after", + "next_kwargs", + "match_spec_version", + ], + [ + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_spec_version + id="default", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + str(uuid4()), # object_id + None, # limit + None, # added_after + None, # next_kwargs + None, # match_spec_version + id="unknown object", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + NOW, # added_after + None, # next_kwargs + None, # match_spec_version + id="added_after", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + NOW + datetime.timedelta(seconds=3), # added_after + None, # next_kwargs + None, # match_spec_version + id="added_after, no results", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 1, # limit + None, # added_after + None, # next_kwargs + None, # match_spec_version + id="limit", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + 999, # limit + None, # added_after + None, # next_kwargs + None, # match_spec_version + id="limit high", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + { + "id": STIX_OBJECTS[0].id, + "date_added": STIX_OBJECTS[0].date_added, + }, # next_kwargs + None, # match_spec_version + id="next", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[0].spec_version], # match_spec_version + id="spec_version [0]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [STIX_OBJECTS[2].spec_version], # match_spec_version + id="spec_version [2]", + ), + pytest.param( + COLLECTIONS[5].id, # collection_id + STIX_OBJECTS[0].id, # object_id + None, # limit + None, # added_after + None, # next_kwargs + [ + STIX_OBJECTS[0].spec_version, + STIX_OBJECTS[2].spec_version, + ], # match_spec_version + id="spec_version [0, 2]", + ), + ], +) +def test_get_versions( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_stix_objects, + collection_id, + object_id, + limit, + added_after, + next_kwargs, + match_spec_version, +): + response = taxii2_sqldb_api.get_versions( + collection_id=collection_id, + object_id=object_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_spec_version=match_spec_version, + ) + assert response == GET_VERSIONS_MOCK( + collection_id=collection_id, + object_id=object_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_spec_version=match_spec_version, + ) + + +@pytest.mark.parametrize( + "stix_id, date_added, next_param", + [ + pytest.param( + "indicator--fa641b92-94d7-42dd-aa0e-63cfe1ee148a", + datetime.datetime( + 2022, 2, 4, 18, 40, 6, 297204, tzinfo=datetime.timezone.utc + ), + ( + "MjAyMi0wMi0wNFQxODo0MDowNi4yOTcyMDQrMDA6MDB8aW5kaWNhdG9yLS1mYTY0M" + "WI5Mi05NGQ3LTQyZGQtYWEwZS02M2NmZTFlZTE0OGE=" + ), + id="simple", + ), + ], +) +def test_next_param( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, stix_id, date_added, next_param +): + assert ( + taxii2_sqldb_api.get_next_param({"id": stix_id, "date_added": date_added}) + == next_param + ) + assert taxii2_sqldb_api.parse_next_param(next_param) == { + "id": stix_id, + "date_added": date_added, + } diff --git a/tests/taxii2/test_taxii2_status.py b/tests/taxii2/test_taxii2_status.py index 3cbacbc8..550d5b3e 100644 --- a/tests/taxii2/test_taxii2_status.py +++ b/tests/taxii2/test_taxii2_status.py @@ -1,347 +1,347 @@ -import json -from unittest.mock import patch -from uuid import uuid4 - -import pytest - -from opentaxii.persistence.sqldb import taxii2models -from opentaxii.taxii2.utils import taxii2_datetimeformat -from tests.taxii2.utils import ( - API_ROOTS, - GET_API_ROOT_MOCK, - GET_JOB_AND_DETAILS_MOCK, - JOBS, - config_noop, - server_mapping_noop, - server_mapping_remove_fields, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "job_id", - "config_override_func", - "server_mapping_override_func", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - JOBS[0].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(JOBS[0].id), - "status": JOBS[0].status, - "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), - "total_count": 4, - "success_count": 1, - "successes": [ - { - "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", - "version": "2018-05-27T12:02:41.312000Z", - } - ], - "failure_count": 1, - "failures": [ - { - "id": "malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", - "version": "2018-05-28T14:03:42.543000Z", - "message": "Unable to process object", - } - ], - "pending_count": 2, - "pendings": [ - { - "id": "indicator--252c7c11-daf2-42bd-843b-be65edca9f61", - "version": "2018-05-18T20:16:21.148000Z", - }, - { - "id": "relationship--045585ad-a22f-4333-af33-bfd503a683b5", - "version": "2018-05-15T10:13:32.579000Z", - }, - ], - }, - id="good, first, first", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - JOBS[3].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(JOBS[3].id), - "status": JOBS[3].status, - "request_timestamp": taxii2_datetimeformat(JOBS[3].request_timestamp), - "total_count": 0, - "success_count": 0, - "failure_count": 0, - "pending_count": 0, - }, - id="good, second, second", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - JOBS[6].id, - config_noop, - server_mapping_noop, - 200, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "id": str(JOBS[6].id), - "status": JOBS[6].status, - "request_timestamp": taxii2_datetimeformat(JOBS[6].request_timestamp), - "total_count": 6, - "success_count": 1, - "failure_count": 2, - "pending_count": 3, - }, - id="good, no details", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - JOBS[3].id, - config_noop, - server_mapping_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api id", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - config_noop, - server_mapping_remove_fields("taxii1"), - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown job id, taxii2 only config", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - config_noop, - server_mapping_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown job id, taxii1/2 config", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - JOBS[0].id, - config_noop, - server_mapping_remove_fields("taxii1"), - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii2 only config", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - JOBS[0].id, - config_noop, - server_mapping_noop, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root, taxii1/2 config", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - JOBS[0].id, - config_noop, - server_mapping_noop, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - JOBS[0].id, - config_noop, - server_mapping_noop, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_status( - authenticated_client, - method, - api_root_id, - job_id, - headers, - config_override_func, - server_mapping_override_func, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2, - "config", - config_override_func( - authenticated_client.application.taxii_server.servers.taxii2.config - ), - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_roots", - return_value=API_ROOTS, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_job_and_details", - side_effect=GET_JOB_AND_DETAILS_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server, - "servers", - server_mapping_override_func( - authenticated_client.application.taxii_server.servers - ), - ), - ): - func = getattr(authenticated_client, method) - response = func(f"/taxii2/{api_root_id}/status/{job_id}/", headers=headers) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ): - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_status_unauthenticated( - client, - method, - is_public, -): - if is_public: - api_root_id = API_ROOTS[1].id - job_id = JOBS[2].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - api_root_id = API_ROOTS[0].id - job_id = JOBS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_api_root", - side_effect=GET_API_ROOT_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_job_and_details", - side_effect=GET_JOB_AND_DETAILS_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{api_root_id}/status/{job_id}/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code - - -def test_job_cleanup(app, db_jobs): - number_removed = app.taxii_server.servers.taxii2.persistence.api.job_cleanup() - assert number_removed == 3 - assert ( - app.taxii_server.servers.taxii2.persistence.api.db.session.query( - taxii2models.Job - ).count() - == 4 - ) +import json +from unittest.mock import patch +from uuid import uuid4 + +import pytest + +from opentaxii.persistence.sqldb import taxii2models +from opentaxii.taxii2.utils import taxii2_datetimeformat +from tests.taxii2.utils import ( + API_ROOTS, + GET_API_ROOT_MOCK, + GET_JOB_AND_DETAILS_MOCK, + JOBS, + config_noop, + server_mapping_noop, + server_mapping_remove_fields, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "job_id", + "config_override_func", + "server_mapping_override_func", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + JOBS[0].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(JOBS[0].id), + "status": JOBS[0].status, + "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), + "total_count": 4, + "success_count": 1, + "successes": [ + { + "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", + "version": "2018-05-27T12:02:41.312000Z", + } + ], + "failure_count": 1, + "failures": [ + { + "id": "malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", + "version": "2018-05-28T14:03:42.543000Z", + "message": "Unable to process object", + } + ], + "pending_count": 2, + "pendings": [ + { + "id": "indicator--252c7c11-daf2-42bd-843b-be65edca9f61", + "version": "2018-05-18T20:16:21.148000Z", + }, + { + "id": "relationship--045585ad-a22f-4333-af33-bfd503a683b5", + "version": "2018-05-15T10:13:32.579000Z", + }, + ], + }, + id="good, first, first", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + JOBS[3].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(JOBS[3].id), + "status": JOBS[3].status, + "request_timestamp": taxii2_datetimeformat(JOBS[3].request_timestamp), + "total_count": 0, + "success_count": 0, + "failure_count": 0, + "pending_count": 0, + }, + id="good, second, second", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + JOBS[6].id, + config_noop, + server_mapping_noop, + 200, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "id": str(JOBS[6].id), + "status": JOBS[6].status, + "request_timestamp": taxii2_datetimeformat(JOBS[6].request_timestamp), + "total_count": 6, + "success_count": 1, + "failure_count": 2, + "pending_count": 3, + }, + id="good, no details", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + JOBS[3].id, + config_noop, + server_mapping_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api id", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + config_noop, + server_mapping_remove_fields("taxii1"), + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown job id, taxii2 only config", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + config_noop, + server_mapping_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown job id, taxii1/2 config", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + JOBS[0].id, + config_noop, + server_mapping_remove_fields("taxii1"), + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii2 only config", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + JOBS[0].id, + config_noop, + server_mapping_noop, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root, taxii1/2 config", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + JOBS[0].id, + config_noop, + server_mapping_noop, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + JOBS[0].id, + config_noop, + server_mapping_noop, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_status( + authenticated_client, + method, + api_root_id, + job_id, + headers, + config_override_func, + server_mapping_override_func, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2, + "config", + config_override_func( + authenticated_client.application.taxii_server.servers.taxii2.config + ), + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_roots", + return_value=API_ROOTS, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_job_and_details", + side_effect=GET_JOB_AND_DETAILS_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server, + "servers", + server_mapping_override_func( + authenticated_client.application.taxii_server.servers + ), + ), + ): + func = getattr(authenticated_client, method) + response = func(f"/taxii2/{api_root_id}/status/{job_id}/", headers=headers) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ): + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_status_unauthenticated( + client, + method, + is_public, +): + if is_public: + api_root_id = API_ROOTS[1].id + job_id = JOBS[2].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + api_root_id = API_ROOTS[0].id + job_id = JOBS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_api_root", + side_effect=GET_API_ROOT_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_job_and_details", + side_effect=GET_JOB_AND_DETAILS_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{api_root_id}/status/{job_id}/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code + + +def test_job_cleanup(app, db_jobs): + number_removed = app.taxii_server.servers.taxii2.persistence.api.job_cleanup() + assert number_removed == 3 + assert ( + app.taxii_server.servers.taxii2.persistence.api.db.session.query( + taxii2models.Job + ).count() + == 4 + ) diff --git a/tests/taxii2/test_taxii2_utils.py b/tests/taxii2/test_taxii2_utils.py index 75d2bc5b..fadde7b1 100644 --- a/tests/taxii2/test_taxii2_utils.py +++ b/tests/taxii2/test_taxii2_utils.py @@ -1,90 +1,90 @@ -import datetime - -import pytest - -from opentaxii.taxii2.utils import get_object_version, taxii2_datetimeformat - - -@pytest.mark.parametrize( - ["input_value", "expected"], - [ - pytest.param( - datetime.datetime(2022, 1, 1, 12, 0, 0, 0, tzinfo=datetime.timezone.utc), - "2022-01-01T12:00:00.000000Z", - id="utc", - ), - pytest.param( - datetime.datetime( - 2022, - 1, - 1, - 12, - 0, - 0, - 0, - tzinfo=datetime.timezone(datetime.timedelta(hours=1)), - ), - "2022-01-01T11:00:00.000000Z", - id="utc+1", - ), - pytest.param( - datetime.datetime( - 2022, - 1, - 1, - 12, - 0, - 0, - 0, - tzinfo=datetime.timezone(datetime.timedelta(hours=1, minutes=30)), - ), - "2022-01-01T10:30:00.000000Z", - id="utc+1.5", - ), - pytest.param( - datetime.datetime( - 2022, - 1, - 1, - 12, - 0, - 0, - 0, - tzinfo=datetime.timezone(datetime.timedelta(hours=-1)), - ), - "2022-01-01T13:00:00.000000Z", - id="utc+1", - ), - pytest.param( - datetime.datetime( - 2022, - 1, - 1, - 12, - 0, - 0, - 0, - tzinfo=datetime.timezone(datetime.timedelta(hours=-1, minutes=-30)), - ), - "2022-01-01T13:30:00.000000Z", - id="utc+1.5", - ), - ], -) -def test_taxii2_datetimeformat(input_value, expected): - assert taxii2_datetimeformat(input_value) == expected - - -def test_get_object_version(): - assert get_object_version( - { - "modified": "2022-01-01T13:30:00.000000Z", - "created": "2016-04-06T20:06:37.000Z", - } - ) == datetime.datetime(2022, 1, 1, 13, 30, 00, 0, datetime.timezone.utc) - assert get_object_version( - {"created": "2016-04-06T20:06:37.000Z"} - ) == datetime.datetime(2016, 4, 6, 20, 6, 37, 0, datetime.timezone.utc) - assert get_object_version({}) == datetime.datetime.fromtimestamp( - 0, tz=datetime.timezone.utc - ) +import datetime + +import pytest + +from opentaxii.taxii2.utils import get_object_version, taxii2_datetimeformat + + +@pytest.mark.parametrize( + ["input_value", "expected"], + [ + pytest.param( + datetime.datetime(2022, 1, 1, 12, 0, 0, 0, tzinfo=datetime.timezone.utc), + "2022-01-01T12:00:00.000000Z", + id="utc", + ), + pytest.param( + datetime.datetime( + 2022, + 1, + 1, + 12, + 0, + 0, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=1)), + ), + "2022-01-01T11:00:00.000000Z", + id="utc+1", + ), + pytest.param( + datetime.datetime( + 2022, + 1, + 1, + 12, + 0, + 0, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=1, minutes=30)), + ), + "2022-01-01T10:30:00.000000Z", + id="utc+1.5", + ), + pytest.param( + datetime.datetime( + 2022, + 1, + 1, + 12, + 0, + 0, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=-1)), + ), + "2022-01-01T13:00:00.000000Z", + id="utc+1", + ), + pytest.param( + datetime.datetime( + 2022, + 1, + 1, + 12, + 0, + 0, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=-1, minutes=-30)), + ), + "2022-01-01T13:30:00.000000Z", + id="utc+1.5", + ), + ], +) +def test_taxii2_datetimeformat(input_value, expected): + assert taxii2_datetimeformat(input_value) == expected + + +def test_get_object_version(): + assert get_object_version( + { + "modified": "2022-01-01T13:30:00.000000Z", + "created": "2016-04-06T20:06:37.000Z", + } + ) == datetime.datetime(2022, 1, 1, 13, 30, 00, 0, datetime.timezone.utc) + assert get_object_version( + {"created": "2016-04-06T20:06:37.000Z"} + ) == datetime.datetime(2016, 4, 6, 20, 6, 37, 0, datetime.timezone.utc) + assert get_object_version({}) == datetime.datetime.fromtimestamp( + 0, tz=datetime.timezone.utc + ) diff --git a/tests/taxii2/test_taxii2_versions.py b/tests/taxii2/test_taxii2_versions.py index c6eef016..195432f1 100644 --- a/tests/taxii2/test_taxii2_versions.py +++ b/tests/taxii2/test_taxii2_versions.py @@ -1,476 +1,476 @@ -import datetime -import json -from unittest.mock import patch -from urllib.parse import urlencode -from uuid import uuid4 - -import pytest - -from opentaxii.taxii2.utils import taxii2_datetimeformat -from tests.taxii2.utils import ( - API_ROOTS, - COLLECTIONS, - GET_COLLECTION_MOCK, - GET_NEXT_PARAM, - GET_VERSIONS_MOCK, - NOW, - STIX_OBJECTS, -) - - -@pytest.mark.parametrize( - [ - "method", - "headers", - "api_root_id", - "collection_id", - "object_id", - "filter_kwargs", - "expected_status", - "expected_headers", - "expected_content", - ], - [ - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[0].version), - taxii2_datetimeformat(STIX_OBJECTS[2].version), - ], - }, - id="good", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"added_after": taxii2_datetimeformat(NOW)}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[2].version), - ], - }, - id="good, added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"added_after": ["Not a valid datetime."]}, - "name": "validation error", - }, - id="broken added_after", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": 1}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": True, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[0].version), - ], - }, - id="good, limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": 2}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[0].version), - taxii2_datetimeformat(STIX_OBJECTS[2].version), - ], - }, - id="good, limit exact", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": 999}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[0].version), - taxii2_datetimeformat(STIX_OBJECTS[2].version), - ], - }, - id="good, limit high", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"limit": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"limit": ["Not a valid integer."]}, - "name": "validation error", - }, - id="broken limit", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - { - "next": GET_NEXT_PARAM( - {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} - ) - }, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat( - NOW + datetime.timedelta(seconds=3) - ), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[2].version), - ], - }, - id="good, next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"next": "a"}, - 400, - { - "Content-Type": "application/taxii+json;version=2.1", - }, - { - "code": 400, - "description": {"next": ["Not a valid value."]}, - "name": "validation error", - }, - id="broken next", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {"match[spec_version]": STIX_OBJECTS[0].spec_version}, - 200, - { - "Content-Type": "application/taxii+json;version=2.1", - "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), - "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), - }, - { - "more": False, - "versions": [ - taxii2_datetimeformat(STIX_OBJECTS[0].version), - ], - }, - id="good, spec_version", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[1].id, - STIX_OBJECTS[2].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="write-only collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[1].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="wrong api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - str(uuid4()), - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown api root", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - str(uuid4()), - STIX_OBJECTS[0].id, - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown collection", - ), - pytest.param( - "get", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - str(uuid4()), - {}, - 404, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 404, - "description": "The requested URL was not found on the server. If you entered " - "the URL manually please check your spelling and try again.", - "name": "Not Found", - }, - id="unknown object", - ), - pytest.param( - "get", - {"Accept": "xml"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 406, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 406, - "name": "Not Acceptable", - "description": ( - "The resource identified by the request is only capable of generating response entities which" - " have content characteristics not acceptable according to the accept headers sent in the" - " request." - ), - }, - id="wrong accept header", - ), - pytest.param( - "post", - {"Accept": "application/taxii+json;version=2.1"}, - API_ROOTS[0].id, - COLLECTIONS[5].id, - STIX_OBJECTS[0].id, - {}, - 405, - {"Content-Type": "application/taxii+json;version=2.1"}, - { - "code": 405, - "description": "The method is not allowed for the requested URL.", - "name": "Method Not Allowed", - }, - id="wrong method", - ), - ], -) -def test_versions( - authenticated_client, - method, - api_root_id, - collection_id, - object_id, - filter_kwargs, - headers, - expected_status, - expected_headers, - expected_content, -): - with ( - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_versions", - side_effect=GET_VERSIONS_MOCK, - ), - patch.object( - authenticated_client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - patch.object( - authenticated_client.account, - "permissions", - { - str(COLLECTIONS[0].id): ["read"], - str(COLLECTIONS[1].id): ["write"], - str(COLLECTIONS[2].id): ["read", "write"], - str(COLLECTIONS[4].id): ["read", "write"], - str(COLLECTIONS[5].id): ["write", "read"], - }, - ), - ): - func = getattr(authenticated_client, method) - if filter_kwargs: - querystring = f"?{urlencode(filter_kwargs)}" - else: - querystring = "" - kwargs = {"headers": headers} - response = func( - f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{object_id}/versions/{querystring}", - **kwargs, - ) - assert response.status_code == expected_status - assert { - key: response.headers.get(key) for key in expected_headers - } == expected_headers - if ( - response.headers.get("Content-Type", "application/taxii+json;version=2.1") - == "application/taxii+json;version=2.1" - ) and response.data != b"": - content = json.loads(response.data) - else: - content = response.data - assert content == expected_content - - -@pytest.mark.parametrize("is_public", [True, False]) -@pytest.mark.parametrize("method", ["get", "post", "delete"]) -def test_versions_unauthenticated( - client, - method, - is_public, -): - if is_public: - collection_id = COLLECTIONS[6].id - stix_id = STIX_OBJECTS[4].id - if method == "get": - expected_status_code = 200 - else: - expected_status_code = 405 - else: - collection_id = COLLECTIONS[0].id - stix_id = STIX_OBJECTS[0].id - if method == "get": - expected_status_code = 401 - else: - expected_status_code = 405 - with ( - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_versions", - side_effect=GET_VERSIONS_MOCK, - ), - patch.object( - client.application.taxii_server.servers.taxii2.persistence.api, - "get_collection", - side_effect=GET_COLLECTION_MOCK, - ), - ): - func = getattr(client, method) - response = func( - f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/{stix_id}/versions/", - headers={"Accept": "application/taxii+json;version=2.1"}, - ) - assert response.status_code == expected_status_code +import datetime +import json +from unittest.mock import patch +from urllib.parse import urlencode +from uuid import uuid4 + +import pytest + +from opentaxii.taxii2.utils import taxii2_datetimeformat +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_COLLECTION_MOCK, + GET_NEXT_PARAM, + GET_VERSIONS_MOCK, + NOW, + STIX_OBJECTS, +) + + +@pytest.mark.parametrize( + [ + "method", + "headers", + "api_root_id", + "collection_id", + "object_id", + "filter_kwargs", + "expected_status", + "expected_headers", + "expected_content", + ], + [ + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[0].version), + taxii2_datetimeformat(STIX_OBJECTS[2].version), + ], + }, + id="good", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"added_after": taxii2_datetimeformat(NOW)}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[2].version), + ], + }, + id="good, added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"added_after": taxii2_datetimeformat(NOW).replace("Z", "+00:00")}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"added_after": ["Not a valid datetime."]}, + "name": "validation error", + }, + id="broken added_after", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": 1}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": True, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[0].version), + ], + }, + id="good, limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": 2}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[0].version), + taxii2_datetimeformat(STIX_OBJECTS[2].version), + ], + }, + id="good, limit exact", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": 999}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[0].version), + taxii2_datetimeformat(STIX_OBJECTS[2].version), + ], + }, + id="good, limit high", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"limit": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"limit": ["Not a valid integer."]}, + "name": "validation error", + }, + id="broken limit", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + { + "next": GET_NEXT_PARAM( + {"id": STIX_OBJECTS[0].id, "date_added": STIX_OBJECTS[0].date_added} + ) + }, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat( + NOW + datetime.timedelta(seconds=3) + ), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[2].version), + ], + }, + id="good, next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"next": "a"}, + 400, + { + "Content-Type": "application/taxii+json;version=2.1", + }, + { + "code": 400, + "description": {"next": ["Not a valid value."]}, + "name": "validation error", + }, + id="broken next", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {"match[spec_version]": STIX_OBJECTS[0].spec_version}, + 200, + { + "Content-Type": "application/taxii+json;version=2.1", + "X-TAXII-Date-Added-First": taxii2_datetimeformat(NOW), + "X-TAXII-Date-Added-Last": taxii2_datetimeformat(NOW), + }, + { + "more": False, + "versions": [ + taxii2_datetimeformat(STIX_OBJECTS[0].version), + ], + }, + id="good, spec_version", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[1].id, + STIX_OBJECTS[2].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="write-only collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[1].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="wrong api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + str(uuid4()), + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown api root", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + str(uuid4()), + STIX_OBJECTS[0].id, + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown collection", + ), + pytest.param( + "get", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + str(uuid4()), + {}, + 404, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 404, + "description": "The requested URL was not found on the server. If you entered " + "the URL manually please check your spelling and try again.", + "name": "Not Found", + }, + id="unknown object", + ), + pytest.param( + "get", + {"Accept": "xml"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 406, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 406, + "name": "Not Acceptable", + "description": ( + "The resource identified by the request is only capable of generating response entities which" + " have content characteristics not acceptable according to the accept headers sent in the" + " request." + ), + }, + id="wrong accept header", + ), + pytest.param( + "post", + {"Accept": "application/taxii+json;version=2.1"}, + API_ROOTS[0].id, + COLLECTIONS[5].id, + STIX_OBJECTS[0].id, + {}, + 405, + {"Content-Type": "application/taxii+json;version=2.1"}, + { + "code": 405, + "description": "The method is not allowed for the requested URL.", + "name": "Method Not Allowed", + }, + id="wrong method", + ), + ], +) +def test_versions( + authenticated_client, + method, + api_root_id, + collection_id, + object_id, + filter_kwargs, + headers, + expected_status, + expected_headers, + expected_content, +): + with ( + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_versions", + side_effect=GET_VERSIONS_MOCK, + ), + patch.object( + authenticated_client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + patch.object( + authenticated_client.account, + "permissions", + { + str(COLLECTIONS[0].id): ["read"], + str(COLLECTIONS[1].id): ["write"], + str(COLLECTIONS[2].id): ["read", "write"], + str(COLLECTIONS[4].id): ["read", "write"], + str(COLLECTIONS[5].id): ["write", "read"], + }, + ), + ): + func = getattr(authenticated_client, method) + if filter_kwargs: + querystring = f"?{urlencode(filter_kwargs)}" + else: + querystring = "" + kwargs = {"headers": headers} + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/objects/{object_id}/versions/{querystring}", + **kwargs, + ) + assert response.status_code == expected_status + assert { + key: response.headers.get(key) for key in expected_headers + } == expected_headers + if ( + response.headers.get("Content-Type", "application/taxii+json;version=2.1") + == "application/taxii+json;version=2.1" + ) and response.data != b"": + content = json.loads(response.data) + else: + content = response.data + assert content == expected_content + + +@pytest.mark.parametrize("is_public", [True, False]) +@pytest.mark.parametrize("method", ["get", "post", "delete"]) +def test_versions_unauthenticated( + client, + method, + is_public, +): + if is_public: + collection_id = COLLECTIONS[6].id + stix_id = STIX_OBJECTS[4].id + if method == "get": + expected_status_code = 200 + else: + expected_status_code = 405 + else: + collection_id = COLLECTIONS[0].id + stix_id = STIX_OBJECTS[0].id + if method == "get": + expected_status_code = 401 + else: + expected_status_code = 405 + with ( + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_versions", + side_effect=GET_VERSIONS_MOCK, + ), + patch.object( + client.application.taxii_server.servers.taxii2.persistence.api, + "get_collection", + side_effect=GET_COLLECTION_MOCK, + ), + ): + func = getattr(client, method) + response = func( + f"/taxii2/{API_ROOTS[0].id}/collections/{collection_id}/objects/{stix_id}/versions/", + headers={"Accept": "application/taxii+json;version=2.1"}, + ) + assert response.status_code == expected_status_code diff --git a/tests/taxii2/test_validation.py b/tests/taxii2/test_validation.py index 99a5de14..ebd49bb8 100644 --- a/tests/taxii2/test_validation.py +++ b/tests/taxii2/test_validation.py @@ -1,105 +1,105 @@ -import json -import platform - -import pytest - -from opentaxii.taxii2.exceptions import ValidationError -from opentaxii.taxii2.validation import validate_envelope -from tests.utils import conditional - -BROKEN_INDICATOR = json.dumps( - { - "objects": [ - { - "type": "indicator", - "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", - } - ] - } -) -GOOD_INDICATOR_WITH_CONTEXT = json.dumps( - { - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": "2016-04-06T20:03:48.000Z", - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - }, - { - "type": "relationship", - "spec_version": "2.1", - "id": "relationship--44298a74-ba52-4f0c-87a3-1824e67d7fad", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:06:37.000Z", - "modified": "2016-04-06T20:06:37.000Z", - "relationship_type": "indicates", - "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", - }, - { - "type": "malware", - "spec_version": "2.1", - "id": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", - "is_family": True, - "created": "2016-04-06T20:07:09.000Z", - "modified": "2016-04-06T20:07:09.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "name": "Poison Ivy", - "malware_types": ["trojan"], - }, - ] - } -) -CUSTOM_TYPE = json.dumps({"objects": [{"type": "mytype"}]}) - -if platform.python_implementation() == "PyPy": - JSON_ERROR_MESSAGE = "Invalid json: Unexpected 'a': line 1 column 1 (char 0)" -else: - JSON_ERROR_MESSAGE = "Invalid json: Expecting value: line 1 column 1 (char 0)" - - -@pytest.mark.parametrize( - ["json_data", "allow_custom", "raises", "message"], - [ - (None, False, True, "No data"), - ("", False, True, "No data"), - ("{}", False, True, "No objects"), - ("abcd", False, True, JSON_ERROR_MESSAGE), - ( - BROKEN_INDICATOR, - False, - True, - 'Invalid stix object: {"type": "indicator", "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf"};' - " No values for required properties for Indicator: (labels, pattern).", - ), - (GOOD_INDICATOR_WITH_CONTEXT, False, False, ""), - ( - CUSTOM_TYPE, - False, - True, - 'Invalid stix object: {"type": "mytype"}; Can\'t parse unknown object type' - " 'mytype'! For custom types, use the CustomObject decorator.", - ), - ( - CUSTOM_TYPE, - True, - False, - "", - ), - ], -) -def test_validate_envelope(json_data, allow_custom, raises, message): - with conditional(raises, pytest.raises(ValidationError)) as exception: - validate_envelope(json_data, allow_custom) - if raises: - assert str(exception.value) == message +import json +import platform + +import pytest + +from opentaxii.taxii2.exceptions import ValidationError +from opentaxii.taxii2.validation import validate_envelope +from tests.utils import conditional + +BROKEN_INDICATOR = json.dumps( + { + "objects": [ + { + "type": "indicator", + "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf", + } + ] + } +) +GOOD_INDICATOR_WITH_CONTEXT = json.dumps( + { + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + }, + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--44298a74-ba52-4f0c-87a3-1824e67d7fad", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", + }, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", + "is_family": True, + "created": "2016-04-06T20:07:09.000Z", + "modified": "2016-04-06T20:07:09.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "name": "Poison Ivy", + "malware_types": ["trojan"], + }, + ] + } +) +CUSTOM_TYPE = json.dumps({"objects": [{"type": "mytype"}]}) + +if platform.python_implementation() == "PyPy": + JSON_ERROR_MESSAGE = "Invalid json: Unexpected 'a': line 1 column 1 (char 0)" +else: + JSON_ERROR_MESSAGE = "Invalid json: Expecting value: line 1 column 1 (char 0)" + + +@pytest.mark.parametrize( + ["json_data", "allow_custom", "raises", "message"], + [ + (None, False, True, "No data"), + ("", False, True, "No data"), + ("{}", False, True, "No objects"), + ("abcd", False, True, JSON_ERROR_MESSAGE), + ( + BROKEN_INDICATOR, + False, + True, + 'Invalid stix object: {"type": "indicator", "id": "indicator--c410e480-e42b-47d1-9476-85307c12bcbf"};' + " No values for required properties for Indicator: (labels, pattern).", + ), + (GOOD_INDICATOR_WITH_CONTEXT, False, False, ""), + ( + CUSTOM_TYPE, + False, + True, + 'Invalid stix object: {"type": "mytype"}; Can\'t parse unknown object type' + " 'mytype'! For custom types, use the CustomObject decorator.", + ), + ( + CUSTOM_TYPE, + True, + False, + "", + ), + ], +) +def test_validate_envelope(json_data, allow_custom, raises, message): + with conditional(raises, pytest.raises(ValidationError)) as exception: + validate_envelope(json_data, allow_custom) + if raises: + assert str(exception.value) == message diff --git a/tests/taxii2/utils.py b/tests/taxii2/utils.py index 733b1c1a..b86affe0 100644 --- a/tests/taxii2/utils.py +++ b/tests/taxii2/utils.py @@ -1,554 +1,554 @@ -import base64 -import datetime -from typing import Dict, List, Optional, Tuple -from uuid import UUID, uuid4 - -from opentaxii.server import ServerMapping -from opentaxii.taxii2.entities import ( - ApiRoot, - Collection, - Job, - JobDetail, - ManifestRecord, - STIXObject, - VersionRecord, -) -from opentaxii.taxii2.utils import DATETIMEFORMAT, taxii2_datetimeformat - -API_ROOTS_WITH_DEFAULT = ( - ApiRoot(uuid4(), True, "first title", "first description", False), - ApiRoot(uuid4(), False, "second title", "second description", True), -) -API_ROOTS_WITHOUT_DEFAULT = ( - ApiRoot(uuid4(), False, "first title", "first description", False), - ApiRoot(uuid4(), False, "second title", "second description", True), - ApiRoot(uuid4(), False, "third title", None, False), -) -API_ROOTS = API_ROOTS_WITHOUT_DEFAULT -NOW = datetime.datetime.now(datetime.timezone.utc) -JOBS: Tuple[Job, ...] = tuple() -for api_root in API_ROOTS: - JOBS = JOBS + ( - Job( - uuid4(), - api_root.id, - "complete", - NOW, - NOW - datetime.timedelta(hours=24, minutes=1), - ), - Job( - uuid4(), - api_root.id, - "pending", - NOW, - None, - ), - ) -JOBS = JOBS + ( - Job( - uuid4(), - API_ROOTS[0].id, - "pending", - NOW, - None, - 6, - 1, - 2, - 3, - ), -) - -JOBS[0].details.success.extend( - [ - JobDetail( - id=uuid4(), - job_id=JOBS[0].id, - stix_id="indicator--c410e480-e42b-47d1-9476-85307c12bcbf", - version=datetime.datetime.strptime( - "2018-05-27T12:02:41.312Z", DATETIMEFORMAT - ).replace(tzinfo=datetime.timezone.utc), - message="", - status="success", - ) - ] -) -JOBS[0].success_count = 1 -JOBS[0].details.failure.extend( - [ - JobDetail( - id=uuid4(), - job_id=JOBS[0].id, - stix_id="malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", - version=datetime.datetime.strptime( - "2018-05-28T14:03:42.543Z", DATETIMEFORMAT - ).replace(tzinfo=datetime.timezone.utc), - message="Unable to process object", - status="failure", - ) - ] -) -JOBS[0].failure_count = 1 -JOBS[0].details.pending.extend( - [ - JobDetail( - id=uuid4(), - job_id=JOBS[0].id, - stix_id="indicator--252c7c11-daf2-42bd-843b-be65edca9f61", - version=datetime.datetime.strptime( - "2018-05-18T20:16:21.148Z", DATETIMEFORMAT - ).replace(tzinfo=datetime.timezone.utc), - message="", - status="pending", - ), - JobDetail( - id=uuid4(), - job_id=JOBS[0].id, - stix_id="relationship--045585ad-a22f-4333-af33-bfd503a683b5", - version=datetime.datetime.strptime( - "2018-05-15T10:13:32.579Z", DATETIMEFORMAT - ).replace(tzinfo=datetime.timezone.utc), - message="", - status="pending", - ), - ] -) -JOBS[0].pending_count = 2 -JOBS[0].total_count = 4 - -COLLECTIONS = ( - Collection( - uuid4(), - API_ROOTS[0].id, - "0Read only", - "Read only description", - None, - False, - False, - ), - Collection( - uuid4(), - API_ROOTS[0].id, - "1Write only", - "Write only description", - None, - False, - False, - ), - Collection( - uuid4(), - API_ROOTS[0].id, - "2Read/Write", - "Read/Write description", - None, - False, - False, - ), - Collection( - uuid4(), - API_ROOTS[0].id, - "3No permissions", - "No permissions description", - None, - False, - False, - ), - Collection(uuid4(), API_ROOTS[0].id, "4No description", "", None, False, False), - Collection( - uuid4(), - API_ROOTS[0].id, - "5With alias", - "With alias description", - "this-is-an-alias", - False, - False, - ), - Collection( - uuid4(), - API_ROOTS[0].id, - "6Public", - "public description", - "", - True, - False, - ), - Collection( - uuid4(), - API_ROOTS[0].id, - "7Publicwrite", - "public write description", - None, - False, - True, - ), -) -STIX_OBJECTS: tuple[STIXObject, ...] = ( - STIXObject( - f"indicator--{str(uuid4())}", - COLLECTIONS[5].id, - "indicator", - "2.0", - NOW, - NOW + datetime.timedelta(seconds=1), - { - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - }, - ), - STIXObject( - f"relationship--{str(uuid4())}", - COLLECTIONS[5].id, - "relationship", - "2.1", - NOW + datetime.timedelta(seconds=2), - NOW + datetime.timedelta(seconds=3), - { - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:06:37.000Z", - "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3)), - "relationship_type": "indicates", - "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", - }, - ), -) -STIX_OBJECTS = STIX_OBJECTS + ( - STIXObject( - STIX_OBJECTS[0].id, - COLLECTIONS[5].id, - "indicator", - "2.1", - NOW + datetime.timedelta(seconds=3), - NOW + datetime.timedelta(seconds=-1), - { - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=-1)), - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - }, - ), - STIXObject( - f"indicator--{str(uuid4())}", - COLLECTIONS[1].id, - "indicator", - "2.1", - NOW, - NOW + datetime.timedelta(seconds=1), - { - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - }, - ), - STIXObject( - f"indicator--{str(uuid4())}", - COLLECTIONS[6].id, - "indicator", - "2.0", - NOW, - NOW + datetime.timedelta(seconds=1), - { - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "created": "2016-04-06T20:03:48.000Z", - "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), - "indicator_types": ["malicious-activity"], - "name": "Poison Ivy Malware", - "description": "This file is part of Poison Ivy", - "pattern": "[ file:hashes.'SHA-256' = " - "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", - "pattern_type": "stix", - "valid_from": "2016-01-01T00:00:00Z", - }, - ), -) - - -def process_match_version(match_version): - if match_version is None: - match_version = ["last"] - versions_per_id: dict = {} - for stix_obj in STIX_OBJECTS: - if stix_obj.id not in versions_per_id: - versions_per_id[stix_obj.id] = [] - versions_per_id[stix_obj.id].append(stix_obj.version) - id_version_combos = [] - for value in match_version: - if value == "last": - for obj_id, versions in versions_per_id.items(): - id_version_combos.append((obj_id, max(versions))) - elif value == "first": - for obj_id, versions in versions_per_id.items(): - id_version_combos.append((obj_id, min(versions))) - elif value == "all": - for obj_id, versions in versions_per_id.items(): - for version in versions: - id_version_combos.append((obj_id, version)) - else: - for obj_id in versions_per_id: - id_version_combos.append((obj_id, value)) - return id_version_combos - - -def GET_API_ROOT_MOCK(api_root_id: UUID): - for api_root in API_ROOTS: - if api_root.id == api_root_id: - return api_root - return None - - -def GET_JOB_AND_DETAILS_MOCK(api_root_id: UUID, job_id: UUID): - job_response = None - for job in JOBS: - if job.api_root_id == api_root_id and job.id == job_id: - job_response = job - break - return job_response - - -def GET_COLLECTIONS_MOCK(api_root_id: UUID): - response = [] - for collection in COLLECTIONS: - if collection.api_root_id == api_root_id: - response.append(collection) - return response - - -def GET_COLLECTION_MOCK(api_root_id: UUID, collection_id_or_alias: str): - for collection in COLLECTIONS: - if collection.api_root_id == api_root_id and ( - str(collection.id) == collection_id_or_alias - or collection.alias == collection_id_or_alias - ): - return collection - return None - - -def STIX_OBJECT_FROM_MANIFEST(stix_id): - for obj in STIX_OBJECTS: - if obj.id == stix_id: - return obj - - -def GET_MANIFEST_MOCK( - collection_id: UUID, - limit: Optional[int] = None, - added_after: Optional[datetime.datetime] = None, - next_kwargs: Optional[Dict] = None, - match_id: Optional[List[str]] = None, - match_type: Optional[List[str]] = None, - match_version: Optional[List[str]] = None, - match_spec_version: Optional[List[str]] = None, -): - stix_objects, more, _ = GET_OBJECTS_MOCK( - collection_id=collection_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_id=match_id, - match_type=match_type, - match_version=match_version, - match_spec_version=match_spec_version, - ) - response = [ - ManifestRecord(obj.id, obj.date_added, obj.version, obj.spec_version) - for obj in stix_objects - ] - return response, more - - -def GET_NEXT_PARAM(kwargs: Dict) -> str: - return base64.b64encode( - f"{kwargs['date_added'].isoformat()}|{kwargs['id']}".encode("utf-8") - ).decode() - - -def GET_OBJECTS_MOCK( - collection_id: UUID, - limit: Optional[int] = None, - added_after: Optional[datetime.datetime] = None, - next_kwargs: Optional[Dict] = None, - match_id: Optional[List[str]] = None, - match_type: Optional[List[str]] = None, - match_version: Optional[List[str]] = None, - match_spec_version: Optional[List[str]] = None, -): - id_version_combos = process_match_version(match_version) - response: List = [] - more = False - for stix_object in STIX_OBJECTS: - if ( - stix_object.collection_id == collection_id - and (stix_object.id, stix_object.version) in id_version_combos - ): - if limit is not None and limit == len(response): - more = True - break - if added_after is not None: - if stix_object.date_added <= added_after: - continue - if next_kwargs is not None: - if stix_object.date_added < next_kwargs["date_added"] or ( - stix_object.date_added == next_kwargs["date_added"] - and stix_object.id == next_kwargs["id"] - ): - continue - if match_id is not None and stix_object.id not in match_id: - continue - if match_type is not None and stix_object.type not in match_type: - continue - if ( - match_spec_version is not None - and stix_object.spec_version not in match_spec_version - ): - continue - response.append(stix_object) - if more: - next_param = GET_NEXT_PARAM( - {"id": response[-1].id, "date_added": response[-1].date_added} - ) - else: - next_param = None - return response, more, next_param - - -def GET_OBJECT_MOCK( - collection_id: UUID, - object_id: str, - limit: Optional[int] = None, - added_after: Optional[datetime.datetime] = None, - next_kwargs: Optional[Dict] = None, - match_version: Optional[List[str]] = None, - match_spec_version: Optional[List[str]] = None, -): - id_version_combos = process_match_version(match_version) - response: List = [] - more = False - at_least_one = False - for stix_object in STIX_OBJECTS: - if stix_object.collection_id == collection_id and stix_object.id == object_id: - at_least_one = True - if ( - stix_object.id, - stix_object.version, - ) not in id_version_combos: - continue - if limit is not None and limit == len(response): - more = True - break - if added_after is not None: - if stix_object.date_added <= added_after: - continue - if next_kwargs is not None: - if stix_object.date_added < next_kwargs["date_added"] or ( - stix_object.date_added == next_kwargs["date_added"] - and stix_object.id == next_kwargs["id"] - ): - continue - if ( - match_spec_version is not None - and stix_object.spec_version not in match_spec_version - ): - continue - response.append(stix_object) - if more: - next_param = GET_NEXT_PARAM( - {"id": response[-1].id, "date_added": response[-1].date_added} - ) - else: - next_param = None - if not at_least_one: - response = None # type: ignore[assignment] - return response, more, next_param - - -def ADD_OBJECTS_MOCK(api_root_id: UUID, collection_id: str, objects: List[Dict]): - return JOBS[0] - - -def DELETE_OBJECT_MOCK( - collection_id: UUID, - object_id: str, - match_version: Optional[List[str]] = None, - match_spec_version: Optional[List[str]] = None, -): - return - - -def GET_VERSIONS_MOCK( - collection_id: UUID, - object_id: str, - limit: Optional[int] = None, - added_after: Optional[datetime.datetime] = None, - next_kwargs: Optional[Dict] = None, - match_spec_version: Optional[List[str]] = None, -): - versions, more, _ = GET_OBJECT_MOCK( - collection_id=collection_id, - object_id=object_id, - limit=limit, - added_after=added_after, - next_kwargs=next_kwargs, - match_spec_version=match_spec_version, - match_version=["all"], - ) - return ( - ( - [VersionRecord(obj.date_added, obj.version) for obj in versions] - if versions is not None - else None - ), - more, - ) - - -def config_noop(config): - return config - - -def config_remove_fields(*fields): - def inner(config): - for field in fields: - del config[field] - return config - - return inner - - -def config_override(override): - def inner(config): - return {**config, **override} - - return inner - - -def server_mapping_remove_fields(*fields): - def inner(original): - override = {field: None for field in fields} - kwargs = {**dict(original._asdict()), **override} - return ServerMapping(**kwargs) - - return inner - - -def server_mapping_noop(original): - return original +import base64 +import datetime +from typing import Dict, List, Optional, Tuple +from uuid import UUID, uuid4 + +from opentaxii.server import ServerMapping +from opentaxii.taxii2.entities import ( + ApiRoot, + Collection, + Job, + JobDetail, + ManifestRecord, + STIXObject, + VersionRecord, +) +from opentaxii.taxii2.utils import DATETIMEFORMAT, taxii2_datetimeformat + +API_ROOTS_WITH_DEFAULT = ( + ApiRoot(uuid4(), True, "first title", "first description", False), + ApiRoot(uuid4(), False, "second title", "second description", True), +) +API_ROOTS_WITHOUT_DEFAULT = ( + ApiRoot(uuid4(), False, "first title", "first description", False), + ApiRoot(uuid4(), False, "second title", "second description", True), + ApiRoot(uuid4(), False, "third title", None, False), +) +API_ROOTS = API_ROOTS_WITHOUT_DEFAULT +NOW = datetime.datetime.now(datetime.timezone.utc) +JOBS: Tuple[Job, ...] = tuple() +for api_root in API_ROOTS: + JOBS = JOBS + ( + Job( + uuid4(), + api_root.id, + "complete", + NOW, + NOW - datetime.timedelta(hours=24, minutes=1), + ), + Job( + uuid4(), + api_root.id, + "pending", + NOW, + None, + ), + ) +JOBS = JOBS + ( + Job( + uuid4(), + API_ROOTS[0].id, + "pending", + NOW, + None, + 6, + 1, + 2, + 3, + ), +) + +JOBS[0].details.success.extend( + [ + JobDetail( + id=uuid4(), + job_id=JOBS[0].id, + stix_id="indicator--c410e480-e42b-47d1-9476-85307c12bcbf", + version=datetime.datetime.strptime( + "2018-05-27T12:02:41.312Z", DATETIMEFORMAT + ).replace(tzinfo=datetime.timezone.utc), + message="", + status="success", + ) + ] +) +JOBS[0].success_count = 1 +JOBS[0].details.failure.extend( + [ + JobDetail( + id=uuid4(), + job_id=JOBS[0].id, + stix_id="malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", + version=datetime.datetime.strptime( + "2018-05-28T14:03:42.543Z", DATETIMEFORMAT + ).replace(tzinfo=datetime.timezone.utc), + message="Unable to process object", + status="failure", + ) + ] +) +JOBS[0].failure_count = 1 +JOBS[0].details.pending.extend( + [ + JobDetail( + id=uuid4(), + job_id=JOBS[0].id, + stix_id="indicator--252c7c11-daf2-42bd-843b-be65edca9f61", + version=datetime.datetime.strptime( + "2018-05-18T20:16:21.148Z", DATETIMEFORMAT + ).replace(tzinfo=datetime.timezone.utc), + message="", + status="pending", + ), + JobDetail( + id=uuid4(), + job_id=JOBS[0].id, + stix_id="relationship--045585ad-a22f-4333-af33-bfd503a683b5", + version=datetime.datetime.strptime( + "2018-05-15T10:13:32.579Z", DATETIMEFORMAT + ).replace(tzinfo=datetime.timezone.utc), + message="", + status="pending", + ), + ] +) +JOBS[0].pending_count = 2 +JOBS[0].total_count = 4 + +COLLECTIONS = ( + Collection( + uuid4(), + API_ROOTS[0].id, + "0Read only", + "Read only description", + None, + False, + False, + ), + Collection( + uuid4(), + API_ROOTS[0].id, + "1Write only", + "Write only description", + None, + False, + False, + ), + Collection( + uuid4(), + API_ROOTS[0].id, + "2Read/Write", + "Read/Write description", + None, + False, + False, + ), + Collection( + uuid4(), + API_ROOTS[0].id, + "3No permissions", + "No permissions description", + None, + False, + False, + ), + Collection(uuid4(), API_ROOTS[0].id, "4No description", "", None, False, False), + Collection( + uuid4(), + API_ROOTS[0].id, + "5With alias", + "With alias description", + "this-is-an-alias", + False, + False, + ), + Collection( + uuid4(), + API_ROOTS[0].id, + "6Public", + "public description", + "", + True, + False, + ), + Collection( + uuid4(), + API_ROOTS[0].id, + "7Publicwrite", + "public write description", + None, + False, + True, + ), +) +STIX_OBJECTS: tuple[STIXObject, ...] = ( + STIXObject( + f"indicator--{str(uuid4())}", + COLLECTIONS[5].id, + "indicator", + "2.0", + NOW, + NOW + datetime.timedelta(seconds=1), + { + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + }, + ), + STIXObject( + f"relationship--{str(uuid4())}", + COLLECTIONS[5].id, + "relationship", + "2.1", + NOW + datetime.timedelta(seconds=2), + NOW + datetime.timedelta(seconds=3), + { + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:06:37.000Z", + "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=3)), + "relationship_type": "indicates", + "source_ref": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "target_ref": "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b", + }, + ), +) +STIX_OBJECTS = STIX_OBJECTS + ( + STIXObject( + STIX_OBJECTS[0].id, + COLLECTIONS[5].id, + "indicator", + "2.1", + NOW + datetime.timedelta(seconds=3), + NOW + datetime.timedelta(seconds=-1), + { + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=-1)), + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + }, + ), + STIXObject( + f"indicator--{str(uuid4())}", + COLLECTIONS[1].id, + "indicator", + "2.1", + NOW, + NOW + datetime.timedelta(seconds=1), + { + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + }, + ), + STIXObject( + f"indicator--{str(uuid4())}", + COLLECTIONS[6].id, + "indicator", + "2.0", + NOW, + NOW + datetime.timedelta(seconds=1), + { + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": taxii2_datetimeformat(NOW + datetime.timedelta(seconds=1)), + "indicator_types": ["malicious-activity"], + "name": "Poison Ivy Malware", + "description": "This file is part of Poison Ivy", + "pattern": "[ file:hashes.'SHA-256' = " + "'4bac27393bdd9777ce02453256c5577cd02275510b2227f473d03f533924f877' ]", + "pattern_type": "stix", + "valid_from": "2016-01-01T00:00:00Z", + }, + ), +) + + +def process_match_version(match_version): + if match_version is None: + match_version = ["last"] + versions_per_id: dict = {} + for stix_obj in STIX_OBJECTS: + if stix_obj.id not in versions_per_id: + versions_per_id[stix_obj.id] = [] + versions_per_id[stix_obj.id].append(stix_obj.version) + id_version_combos = [] + for value in match_version: + if value == "last": + for obj_id, versions in versions_per_id.items(): + id_version_combos.append((obj_id, max(versions))) + elif value == "first": + for obj_id, versions in versions_per_id.items(): + id_version_combos.append((obj_id, min(versions))) + elif value == "all": + for obj_id, versions in versions_per_id.items(): + for version in versions: + id_version_combos.append((obj_id, version)) + else: + for obj_id in versions_per_id: + id_version_combos.append((obj_id, value)) + return id_version_combos + + +def GET_API_ROOT_MOCK(api_root_id: UUID): + for api_root in API_ROOTS: + if api_root.id == api_root_id: + return api_root + return None + + +def GET_JOB_AND_DETAILS_MOCK(api_root_id: UUID, job_id: UUID): + job_response = None + for job in JOBS: + if job.api_root_id == api_root_id and job.id == job_id: + job_response = job + break + return job_response + + +def GET_COLLECTIONS_MOCK(api_root_id: UUID): + response = [] + for collection in COLLECTIONS: + if collection.api_root_id == api_root_id: + response.append(collection) + return response + + +def GET_COLLECTION_MOCK(api_root_id: UUID, collection_id_or_alias: str): + for collection in COLLECTIONS: + if collection.api_root_id == api_root_id and ( + str(collection.id) == collection_id_or_alias + or collection.alias == collection_id_or_alias + ): + return collection + return None + + +def STIX_OBJECT_FROM_MANIFEST(stix_id): + for obj in STIX_OBJECTS: + if obj.id == stix_id: + return obj + + +def GET_MANIFEST_MOCK( + collection_id: UUID, + limit: Optional[int] = None, + added_after: Optional[datetime.datetime] = None, + next_kwargs: Optional[Dict] = None, + match_id: Optional[List[str]] = None, + match_type: Optional[List[str]] = None, + match_version: Optional[List[str]] = None, + match_spec_version: Optional[List[str]] = None, +): + stix_objects, more, _ = GET_OBJECTS_MOCK( + collection_id=collection_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_id=match_id, + match_type=match_type, + match_version=match_version, + match_spec_version=match_spec_version, + ) + response = [ + ManifestRecord(obj.id, obj.date_added, obj.version, obj.spec_version) + for obj in stix_objects + ] + return response, more + + +def GET_NEXT_PARAM(kwargs: Dict) -> str: + return base64.b64encode( + f"{kwargs['date_added'].isoformat()}|{kwargs['id']}".encode("utf-8") + ).decode() + + +def GET_OBJECTS_MOCK( + collection_id: UUID, + limit: Optional[int] = None, + added_after: Optional[datetime.datetime] = None, + next_kwargs: Optional[Dict] = None, + match_id: Optional[List[str]] = None, + match_type: Optional[List[str]] = None, + match_version: Optional[List[str]] = None, + match_spec_version: Optional[List[str]] = None, +): + id_version_combos = process_match_version(match_version) + response: List = [] + more = False + for stix_object in STIX_OBJECTS: + if ( + stix_object.collection_id == collection_id + and (stix_object.id, stix_object.version) in id_version_combos + ): + if limit is not None and limit == len(response): + more = True + break + if added_after is not None: + if stix_object.date_added <= added_after: + continue + if next_kwargs is not None: + if stix_object.date_added < next_kwargs["date_added"] or ( + stix_object.date_added == next_kwargs["date_added"] + and stix_object.id == next_kwargs["id"] + ): + continue + if match_id is not None and stix_object.id not in match_id: + continue + if match_type is not None and stix_object.type not in match_type: + continue + if ( + match_spec_version is not None + and stix_object.spec_version not in match_spec_version + ): + continue + response.append(stix_object) + if more: + next_param = GET_NEXT_PARAM( + {"id": response[-1].id, "date_added": response[-1].date_added} + ) + else: + next_param = None + return response, more, next_param + + +def GET_OBJECT_MOCK( + collection_id: UUID, + object_id: str, + limit: Optional[int] = None, + added_after: Optional[datetime.datetime] = None, + next_kwargs: Optional[Dict] = None, + match_version: Optional[List[str]] = None, + match_spec_version: Optional[List[str]] = None, +): + id_version_combos = process_match_version(match_version) + response: List = [] + more = False + at_least_one = False + for stix_object in STIX_OBJECTS: + if stix_object.collection_id == collection_id and stix_object.id == object_id: + at_least_one = True + if ( + stix_object.id, + stix_object.version, + ) not in id_version_combos: + continue + if limit is not None and limit == len(response): + more = True + break + if added_after is not None: + if stix_object.date_added <= added_after: + continue + if next_kwargs is not None: + if stix_object.date_added < next_kwargs["date_added"] or ( + stix_object.date_added == next_kwargs["date_added"] + and stix_object.id == next_kwargs["id"] + ): + continue + if ( + match_spec_version is not None + and stix_object.spec_version not in match_spec_version + ): + continue + response.append(stix_object) + if more: + next_param = GET_NEXT_PARAM( + {"id": response[-1].id, "date_added": response[-1].date_added} + ) + else: + next_param = None + if not at_least_one: + response = None # type: ignore[assignment] + return response, more, next_param + + +def ADD_OBJECTS_MOCK(api_root_id: UUID, collection_id: str, objects: List[Dict]): + return JOBS[0] + + +def DELETE_OBJECT_MOCK( + collection_id: UUID, + object_id: str, + match_version: Optional[List[str]] = None, + match_spec_version: Optional[List[str]] = None, +): + return + + +def GET_VERSIONS_MOCK( + collection_id: UUID, + object_id: str, + limit: Optional[int] = None, + added_after: Optional[datetime.datetime] = None, + next_kwargs: Optional[Dict] = None, + match_spec_version: Optional[List[str]] = None, +): + versions, more, _ = GET_OBJECT_MOCK( + collection_id=collection_id, + object_id=object_id, + limit=limit, + added_after=added_after, + next_kwargs=next_kwargs, + match_spec_version=match_spec_version, + match_version=["all"], + ) + return ( + ( + [VersionRecord(obj.date_added, obj.version) for obj in versions] + if versions is not None + else None + ), + more, + ) + + +def config_noop(config): + return config + + +def config_remove_fields(*fields): + def inner(config): + for field in fields: + del config[field] + return config + + return inner + + +def config_override(override): + def inner(config): + return {**config, **override} + + return inner + + +def server_mapping_remove_fields(*fields): + def inner(original): + override = {field: None for field in fields} + kwargs = {**dict(original._asdict()), **override} + return ServerMapping(**kwargs) + + return inner + + +def server_mapping_noop(original): + return original diff --git a/tests/test_auth.py b/tests/test_auth.py index 81a4bfb8..cfc469b1 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,492 +1,492 @@ -import base64 -import json - -import pytest -from libtaxii import messages_10 as tm10 -from libtaxii import messages_11 as tm11 -from libtaxii.constants import CB_STIX_XML_111, RT_FULL, ST_BAD_MESSAGE, ST_UNAUTHORIZED - -from opentaxii.taxii.http import HTTP_AUTHORIZATION -from opentaxii.utils import sync_conf_dict_into_db - -from .fixtures import VID_TAXII_HTTP_10 -from .utils import as_tm, is_headers_valid, prepare_headers - -INBOX_OPEN = dict( - id='inbox-A', - type='inbox', - description='inboxA description', - address='/path/inbox', - destination_collection_required=True, - authentication_required=False, -) - -INBOX_CLOSED = dict( - id='inbox-A', - type='inbox', - description='inboxA description', - address='/path/inbox', - destination_collection_required=True, - authentication_required=True, -) - -DISCOVERY = dict( - id='discovery-A', - type='discovery', - description='discoveryA description', - address='/path/discovery', - advertised_services=['inbox-A', 'poll-A'], - protocol_bindings=[VID_TAXII_HTTP_10], - authentication_required=True, -) - -POLL_CLOSED = dict( - id='poll-A', - type='poll', - description='Poll service description', - address='/path/poll-a', - protocol_bindings=[VID_TAXII_HTTP_10], - authentication_required=True, - max_result_size=100, - max_result_count=10, -) - -POLL_OPEN = dict( - id='poll-B', - type='poll', - description='Poll service description', - address='/path/poll-b', - protocol_bindings=[VID_TAXII_HTTP_10], - authentication_required=False, # <- open for all - max_result_size=100, - max_result_count=10, -) - -CONTENT = 'inbox-message-content' - -COLLECTIONS = [ - { - 'name': 'collection-1', - 'available': True, - 'accept_all_content': True, - 'type': 'DATA_FEED', - 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], - }, - { - 'name': 'collection-2', - 'available': True, - 'accept_all_content': True, - 'type': 'DATA_FEED', - 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], - }, -] - -USERNAME = 'some-username' -PASSWORD = 'some-password' - -ACCOUNTS = [ - { - 'username': 'johnny', - 'password': 'johnny', - 'permissions': {'collection-1': 'read', 'collection-2': 'modify'}, - }, - { - 'username': 'billy', - 'password': 'billy', - 'permissions': {'collection-1': 'modify'}, - }, - {'username': 'wally', 'password': 'wally', 'is_admin': True}, - {'username': USERNAME, 'password': PASSWORD}, -] - -MESSAGE_ID = '123' - -AUTH_PATH = '/management/auth' - - -@pytest.fixture(autouse=True) -def auth_fixtures(server): - sync_conf_dict_into_db( - server, - config={ - 'services': [INBOX_OPEN, INBOX_CLOSED, DISCOVERY, POLL_OPEN, POLL_CLOSED], - 'collections': COLLECTIONS, - 'accounts': ACCOUNTS, - }, - ) - - assert len(server.servers.taxii1.persistence.get_services()) == 4 - assert len(server.servers.taxii1.persistence.get_collections()) == len(COLLECTIONS) - - -@pytest.fixture() -def test_account(server): - from opentaxii.entities import Account - - account = Account(id=None, username=USERNAME, permissions={}) - server.auth.update_account(account, PASSWORD) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_unauthorized_request(app, client, version, https): - base_url = '%s://localhost' % ('https' if https else 'http') - response = client.post( - INBOX_OPEN['address'], - data='invalid-body', - headers=prepare_headers(version, https), - base_url=base_url, - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == ST_UNAUTHORIZED - - from opentaxii import context - - assert not hasattr(context, 'account') - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_get_token(client, version, https): - base_url = '%s://localhost' % ('https' if https else 'http') - # Invalid credentials - response = client.post( - AUTH_PATH, data={'username': 'dummy', 'password': 'wrong'}, base_url=base_url - ) - assert response.status_code == 401 - - # Invalid auth data - response = client.post(AUTH_PATH, data={'other': 'somethind'}, base_url=base_url) - assert response.status_code == 400 - - # Valid credentials as form data - response = client.post( - AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url - ) - - assert response.status_code == 200 - - data = json.loads(response.get_data(as_text=True)) - assert data.get('token') - - # Valid credentials as JSON blob - response = client.post( - AUTH_PATH, - data=json.dumps({'username': USERNAME, 'password': PASSWORD}), - base_url=base_url, - content_type='application/json', - ) - - assert response.status_code == 200 - - data = json.loads(response.get_data(as_text=True)) - assert data.get('token') - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_get_token_and_send_request(client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - - # Get valid token - response = client.post( - AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url - ) - - assert response.status_code == 200 - - data = json.loads(response.get_data(as_text=True)) - token = data.get('token') - - assert token - - headers = prepare_headers(version, https) - headers[HTTP_AUTHORIZATION] = 'Bearer %s' % token - - # Get correct response for invalid body - response = client.post( - INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == ST_BAD_MESSAGE - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - headers = prepare_headers(version, https) - headers[HTTP_AUTHORIZATION] = 'Bearer %s' % token - - # Get correct response for valid request - response = client.post( - DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version=version, https=https) - - message = as_tm(version).get_message_from_xml(response.data) - - assert isinstance(message, as_tm(version).DiscoveryResponse) - - from opentaxii import context - - assert not hasattr(context, 'account') - - -def basic_auth_token(username, password): - return base64.b64encode('{}:{}'.format(username, password).encode('utf-8')) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_request_with_basic_auth(client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - basic_auth_header = 'Basic {}'.format( - basic_auth_token(USERNAME, PASSWORD).decode('utf-8') - ) - - headers = prepare_headers(version, https) - headers[HTTP_AUTHORIZATION] = basic_auth_header - - # Get correct response for invalid body - response = client.post( - INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == ST_BAD_MESSAGE - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - headers = prepare_headers(version, https) - headers[HTTP_AUTHORIZATION] = basic_auth_header - - # Get correct response for valid request - response = client.post( - DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version=version, https=https) - - message = as_tm(version).get_message_from_xml(response.data) - - assert isinstance(message, as_tm(version).DiscoveryResponse) - - from opentaxii import context - - assert not hasattr(context, 'account') - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_invalid_basic_auth_request(client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - - headers = prepare_headers(version, https) - headers[HTTP_AUTHORIZATION] = 'Basic somevalue' - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - response = client.post( - DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == ST_UNAUTHORIZED - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_invalid_auth_header_request(client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - - headers = prepare_headers(version, https) - - basic_auth_header = 'Foo {}'.format(basic_auth_token(USERNAME, PASSWORD)) - headers[HTTP_AUTHORIZATION] = basic_auth_header - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - response = client.post( - DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == ST_UNAUTHORIZED - - -def prepare_url_headers(version, https, username, password): - base_url = '%s://localhost' % ('https' if https else 'http') - headers = prepare_headers(version, https) - basic_auth_header = 'Basic {}'.format( - basic_auth_token(username, password).decode('utf-8') - ) - headers[HTTP_AUTHORIZATION] = basic_auth_header - return base_url, headers - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collection_access_private_poll(client, version, https): - - # POLL_CLOSED collection allowed read access - url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') - request = prepare_poll_request('collection-1', version, bindings=[CB_STIX_XML_111]) - - response = client.post( - POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.message_type == 'Poll_Response' - - # POLL_CLOSED collection disallowed read access - url, headers = prepare_url_headers(version, https, 'billy', 'billy') - request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) - - response = client.post( - POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.status_type == 'NOT_FOUND' - - # POLL_CLOSED collection admin access - url, headers = prepare_url_headers(version, https, 'wally', 'wally') - request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) - - response = client.post( - POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.message_type == 'Poll_Response' - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_collection_access_private_inbox(client, version, https): - # INBOX read-only collection access - url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') - request = prepare_inbox_message( - version, dest_collection='collection-1', blocks=[make_inbox_content(version)] - ) - - response = client.post( - INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.message_type == 'Status_Message' - - if version == 11: - assert message.status_type == 'UNAUTHORIZED' - assert message.message == 'User can not write to collection collection-1' - else: - # Because in TAXII 1.0 destination collection can not be specified - # so it impossible to verify access - assert message.status_type == 'SUCCESS' - - # INBOX modify collection access - request = prepare_inbox_message( - version, dest_collection='collection-2', blocks=[make_inbox_content(version)] - ) - - response = client.post( - INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.message_type == 'Status_Message' - assert message.status_type == 'SUCCESS' - - # INBOX modify collection access - url, headers = prepare_url_headers(version, https, 'wally', 'wally') - request = prepare_inbox_message( - version, dest_collection='collection-2', blocks=[make_inbox_content(version)] - ) - - response = client.post( - INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url - ) - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - message = as_tm(version).get_message_from_xml(response.data) - assert message.message_type == 'Status_Message' - assert message.status_type == 'SUCCESS' - - -def prepare_poll_request(collection_name, version, bindings=[], subscription_id=None): - - if version == 11: - content_bindings = [tm11.ContentBinding(b) for b in bindings] - if subscription_id: - poll_parameters = None - else: - poll_parameters = tm11.PollParameters( - response_type=RT_FULL, content_bindings=content_bindings - ) - return tm11.PollRequest( - message_id=MESSAGE_ID, - collection_name=collection_name, - subscription_id=subscription_id, - poll_parameters=poll_parameters, - ) - elif version == 10: - content_bindings = bindings - return tm10.PollRequest( - message_id=MESSAGE_ID, - feed_name=collection_name, - content_bindings=content_bindings, - subscription_id=subscription_id, - ) - - -def make_inbox_content(version, content_binding=CB_STIX_XML_111, content=CONTENT): - if version == 10: - return tm10.ContentBlock(content_binding, content) - - elif version == 11: - return tm11.ContentBlock(tm11.ContentBinding(content_binding), content) - else: - raise ValueError('Unknown TAXII message version: %s' % version) - - -def prepare_inbox_message(version, blocks=None, dest_collection=None): - if version == 10: - inbox_message = tm10.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) - elif version == 11: - inbox_message = tm11.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) - if dest_collection: - inbox_message.destination_collection_names.append(dest_collection) - else: - raise ValueError('Unknown TAXII message version: %s' % version) - return inbox_message - - -def test_broken_bearer_token_taxii2(client): - headers = {HTTP_AUTHORIZATION: "Bearer broken"} - response = client.get("/taxii2/", headers=headers) - assert response.status_code == 401 +import base64 +import json + +import pytest +from libtaxii import messages_10 as tm10 +from libtaxii import messages_11 as tm11 +from libtaxii.constants import CB_STIX_XML_111, RT_FULL, ST_BAD_MESSAGE, ST_UNAUTHORIZED + +from opentaxii.taxii.http import HTTP_AUTHORIZATION +from opentaxii.utils import sync_conf_dict_into_db + +from .fixtures import VID_TAXII_HTTP_10 +from .utils import as_tm, is_headers_valid, prepare_headers + +INBOX_OPEN = dict( + id='inbox-A', + type='inbox', + description='inboxA description', + address='/path/inbox', + destination_collection_required=True, + authentication_required=False, +) + +INBOX_CLOSED = dict( + id='inbox-A', + type='inbox', + description='inboxA description', + address='/path/inbox', + destination_collection_required=True, + authentication_required=True, +) + +DISCOVERY = dict( + id='discovery-A', + type='discovery', + description='discoveryA description', + address='/path/discovery', + advertised_services=['inbox-A', 'poll-A'], + protocol_bindings=[VID_TAXII_HTTP_10], + authentication_required=True, +) + +POLL_CLOSED = dict( + id='poll-A', + type='poll', + description='Poll service description', + address='/path/poll-a', + protocol_bindings=[VID_TAXII_HTTP_10], + authentication_required=True, + max_result_size=100, + max_result_count=10, +) + +POLL_OPEN = dict( + id='poll-B', + type='poll', + description='Poll service description', + address='/path/poll-b', + protocol_bindings=[VID_TAXII_HTTP_10], + authentication_required=False, # <- open for all + max_result_size=100, + max_result_count=10, +) + +CONTENT = 'inbox-message-content' + +COLLECTIONS = [ + { + 'name': 'collection-1', + 'available': True, + 'accept_all_content': True, + 'type': 'DATA_FEED', + 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], + }, + { + 'name': 'collection-2', + 'available': True, + 'accept_all_content': True, + 'type': 'DATA_FEED', + 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], + }, +] + +USERNAME = 'some-username' +PASSWORD = 'some-password' + +ACCOUNTS = [ + { + 'username': 'johnny', + 'password': 'johnny', + 'permissions': {'collection-1': 'read', 'collection-2': 'modify'}, + }, + { + 'username': 'billy', + 'password': 'billy', + 'permissions': {'collection-1': 'modify'}, + }, + {'username': 'wally', 'password': 'wally', 'is_admin': True}, + {'username': USERNAME, 'password': PASSWORD}, +] + +MESSAGE_ID = '123' + +AUTH_PATH = '/management/auth' + + +@pytest.fixture(autouse=True) +def auth_fixtures(server): + sync_conf_dict_into_db( + server, + config={ + 'services': [INBOX_OPEN, INBOX_CLOSED, DISCOVERY, POLL_OPEN, POLL_CLOSED], + 'collections': COLLECTIONS, + 'accounts': ACCOUNTS, + }, + ) + + assert len(server.servers.taxii1.persistence.get_services()) == 4 + assert len(server.servers.taxii1.persistence.get_collections()) == len(COLLECTIONS) + + +@pytest.fixture() +def test_account(server): + from opentaxii.entities import Account + + account = Account(id=None, username=USERNAME, permissions={}) + server.auth.update_account(account, PASSWORD) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_unauthorized_request(app, client, version, https): + base_url = '%s://localhost' % ('https' if https else 'http') + response = client.post( + INBOX_OPEN['address'], + data='invalid-body', + headers=prepare_headers(version, https), + base_url=base_url, + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == ST_UNAUTHORIZED + + from opentaxii import context + + assert not hasattr(context, 'account') + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_get_token(client, version, https): + base_url = '%s://localhost' % ('https' if https else 'http') + # Invalid credentials + response = client.post( + AUTH_PATH, data={'username': 'dummy', 'password': 'wrong'}, base_url=base_url + ) + assert response.status_code == 401 + + # Invalid auth data + response = client.post(AUTH_PATH, data={'other': 'somethind'}, base_url=base_url) + assert response.status_code == 400 + + # Valid credentials as form data + response = client.post( + AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url + ) + + assert response.status_code == 200 + + data = json.loads(response.get_data(as_text=True)) + assert data.get('token') + + # Valid credentials as JSON blob + response = client.post( + AUTH_PATH, + data=json.dumps({'username': USERNAME, 'password': PASSWORD}), + base_url=base_url, + content_type='application/json', + ) + + assert response.status_code == 200 + + data = json.loads(response.get_data(as_text=True)) + assert data.get('token') + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_get_token_and_send_request(client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + + # Get valid token + response = client.post( + AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url + ) + + assert response.status_code == 200 + + data = json.loads(response.get_data(as_text=True)) + token = data.get('token') + + assert token + + headers = prepare_headers(version, https) + headers[HTTP_AUTHORIZATION] = 'Bearer %s' % token + + # Get correct response for invalid body + response = client.post( + INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == ST_BAD_MESSAGE + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + headers = prepare_headers(version, https) + headers[HTTP_AUTHORIZATION] = 'Bearer %s' % token + + # Get correct response for valid request + response = client.post( + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version=version, https=https) + + message = as_tm(version).get_message_from_xml(response.data) + + assert isinstance(message, as_tm(version).DiscoveryResponse) + + from opentaxii import context + + assert not hasattr(context, 'account') + + +def basic_auth_token(username, password): + return base64.b64encode('{}:{}'.format(username, password).encode('utf-8')) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_request_with_basic_auth(client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + basic_auth_header = 'Basic {}'.format( + basic_auth_token(USERNAME, PASSWORD).decode('utf-8') + ) + + headers = prepare_headers(version, https) + headers[HTTP_AUTHORIZATION] = basic_auth_header + + # Get correct response for invalid body + response = client.post( + INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == ST_BAD_MESSAGE + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + headers = prepare_headers(version, https) + headers[HTTP_AUTHORIZATION] = basic_auth_header + + # Get correct response for valid request + response = client.post( + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version=version, https=https) + + message = as_tm(version).get_message_from_xml(response.data) + + assert isinstance(message, as_tm(version).DiscoveryResponse) + + from opentaxii import context + + assert not hasattr(context, 'account') + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_invalid_basic_auth_request(client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + + headers = prepare_headers(version, https) + headers[HTTP_AUTHORIZATION] = 'Basic somevalue' + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + response = client.post( + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == ST_UNAUTHORIZED + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_invalid_auth_header_request(client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + + headers = prepare_headers(version, https) + + basic_auth_header = 'Foo {}'.format(basic_auth_token(USERNAME, PASSWORD)) + headers[HTTP_AUTHORIZATION] = basic_auth_header + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + response = client.post( + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == ST_UNAUTHORIZED + + +def prepare_url_headers(version, https, username, password): + base_url = '%s://localhost' % ('https' if https else 'http') + headers = prepare_headers(version, https) + basic_auth_header = 'Basic {}'.format( + basic_auth_token(username, password).decode('utf-8') + ) + headers[HTTP_AUTHORIZATION] = basic_auth_header + return base_url, headers + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collection_access_private_poll(client, version, https): + + # POLL_CLOSED collection allowed read access + url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') + request = prepare_poll_request('collection-1', version, bindings=[CB_STIX_XML_111]) + + response = client.post( + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.message_type == 'Poll_Response' + + # POLL_CLOSED collection disallowed read access + url, headers = prepare_url_headers(version, https, 'billy', 'billy') + request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) + + response = client.post( + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.status_type == 'NOT_FOUND' + + # POLL_CLOSED collection admin access + url, headers = prepare_url_headers(version, https, 'wally', 'wally') + request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) + + response = client.post( + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.message_type == 'Poll_Response' + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_collection_access_private_inbox(client, version, https): + # INBOX read-only collection access + url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') + request = prepare_inbox_message( + version, dest_collection='collection-1', blocks=[make_inbox_content(version)] + ) + + response = client.post( + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.message_type == 'Status_Message' + + if version == 11: + assert message.status_type == 'UNAUTHORIZED' + assert message.message == 'User can not write to collection collection-1' + else: + # Because in TAXII 1.0 destination collection can not be specified + # so it impossible to verify access + assert message.status_type == 'SUCCESS' + + # INBOX modify collection access + request = prepare_inbox_message( + version, dest_collection='collection-2', blocks=[make_inbox_content(version)] + ) + + response = client.post( + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.message_type == 'Status_Message' + assert message.status_type == 'SUCCESS' + + # INBOX modify collection access + url, headers = prepare_url_headers(version, https, 'wally', 'wally') + request = prepare_inbox_message( + version, dest_collection='collection-2', blocks=[make_inbox_content(version)] + ) + + response = client.post( + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + message = as_tm(version).get_message_from_xml(response.data) + assert message.message_type == 'Status_Message' + assert message.status_type == 'SUCCESS' + + +def prepare_poll_request(collection_name, version, bindings=[], subscription_id=None): + + if version == 11: + content_bindings = [tm11.ContentBinding(b) for b in bindings] + if subscription_id: + poll_parameters = None + else: + poll_parameters = tm11.PollParameters( + response_type=RT_FULL, content_bindings=content_bindings + ) + return tm11.PollRequest( + message_id=MESSAGE_ID, + collection_name=collection_name, + subscription_id=subscription_id, + poll_parameters=poll_parameters, + ) + elif version == 10: + content_bindings = bindings + return tm10.PollRequest( + message_id=MESSAGE_ID, + feed_name=collection_name, + content_bindings=content_bindings, + subscription_id=subscription_id, + ) + + +def make_inbox_content(version, content_binding=CB_STIX_XML_111, content=CONTENT): + if version == 10: + return tm10.ContentBlock(content_binding, content) + + elif version == 11: + return tm11.ContentBlock(tm11.ContentBinding(content_binding), content) + else: + raise ValueError('Unknown TAXII message version: %s' % version) + + +def prepare_inbox_message(version, blocks=None, dest_collection=None): + if version == 10: + inbox_message = tm10.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) + elif version == 11: + inbox_message = tm11.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) + if dest_collection: + inbox_message.destination_collection_names.append(dest_collection) + else: + raise ValueError('Unknown TAXII message version: %s' % version) + return inbox_message + + +def test_broken_bearer_token_taxii2(client): + headers = {HTTP_AUTHORIZATION: "Bearer broken"} + response = client.get("/taxii2/", headers=headers) + assert response.status_code == 401 diff --git a/tests/test_auth_sqldb.py b/tests/test_auth_sqldb.py index cb21bdbd..66086a14 100644 --- a/tests/test_auth_sqldb.py +++ b/tests/test_auth_sqldb.py @@ -1,34 +1,34 @@ -import pytest - -from opentaxii.auth.sqldb.models import Account - - -def test_account_permissions(app): - account = Account() - - account.permissions = { - "taxii1-read": "read", - "taxii1-modify": "modify", - "taxii2-both": ["read", "write"], - "taxii2-read": ["read"], - "taxii2-write": ["write"], - } - assert account.permissions == { - "taxii1-read": "read", - "taxii1-modify": "modify", - "taxii2-both": ["read", "write"], - "taxii2-read": ["read"], - "taxii2-write": ["write"], - } - - with pytest.raises( - ValueError, - match=r"Unknown TAXII1 permission 'bad' specified for collection 'taxii1-fail'", - ): - account.permissions = {"taxii1-fail": "bad"} - - with pytest.raises( - ValueError, - match=r"Unknown TAXII2 permission '\['read', 'bad'\]' specified for collection 'taxii2-fail'", - ): - account.permissions = {"taxii2-fail": ["read", "bad"]} +import pytest + +from opentaxii.auth.sqldb.models import Account + + +def test_account_permissions(app): + account = Account() + + account.permissions = { + "taxii1-read": "read", + "taxii1-modify": "modify", + "taxii2-both": ["read", "write"], + "taxii2-read": ["read"], + "taxii2-write": ["write"], + } + assert account.permissions == { + "taxii1-read": "read", + "taxii1-modify": "modify", + "taxii2-both": ["read", "write"], + "taxii2-read": ["read"], + "taxii2-write": ["write"], + } + + with pytest.raises( + ValueError, + match=r"Unknown TAXII1 permission 'bad' specified for collection 'taxii1-fail'", + ): + account.permissions = {"taxii1-fail": "bad"} + + with pytest.raises( + ValueError, + match=r"Unknown TAXII2 permission '\['read', 'bad'\]' specified for collection 'taxii2-fail'", + ): + account.permissions = {"taxii2-fail": ["read", "bad"]} diff --git a/tests/test_cli.py b/tests/test_cli.py index fd73d3e9..09aff595 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,631 +1,631 @@ -import os -from unittest import mock - -import pytest - -from opentaxii.cli.auth import create_account, update_account -from opentaxii.cli.persistence import ( - add_api_root, - add_collection, - delete_content_blocks, - job_cleanup, - sync_data_configuration, -) -from tests.fixtures import ACCOUNT, COLLECTION_OPEN -from tests.taxii2.utils import API_ROOTS -from tests.utils import assert_str_equal_no_formatting, conditional_raises - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr"], - [ - pytest.param( - [os.path.join("examples", "data-configuration.yml")], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - id="default", - ), - pytest.param( - [os.path.join("examples", "data-configuration.yml"), "-f"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - id="default, -f", - ), - pytest.param( - [ - os.path.join("examples", "data-configuration.yml"), - "--force-delete", - ], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - id="default, --force-delete", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "[-f]", - "config", - ":error: the following arguments are required: config", - ] - ), - id="no args", - ), - pytest.param( - ["this_file_does_not_exist"], # argv - FileNotFoundError, # raises - "[Errno 2] No such file or directory: 'this_file_does_not_exist'", # message - "", # stdout - "", # stderr - id="missing config", - ), - pytest.param( - ["this_file_does_not_exist", "-f"], # argv - FileNotFoundError, # raises - "[Errno 2] No such file or directory: 'this_file_does_not_exist'", # message - "", # stdout - "", # stderr - id="missing config, with -f", - ), - ], -) -def test_sync_data_configuration(app, capsys, argv, raises, message, stdout, stderr): - with ( - mock.patch("opentaxii.cli.persistence.app", app), - mock.patch("sys.argv", [""] + argv), - ): - with conditional_raises(raises) as exception: - sync_data_configuration() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr"], - [ - pytest.param( - ["-c", COLLECTION_OPEN, "--begin", "2000-01-01"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - id="good", - ), - pytest.param( - ["-c", "collectiondoesnotexist", "--begin", "2000-01-01"], # argv - ValueError, # raises - "Collection with name 'collectiondoesnotexist' does not exist", # message - "", # stdout - "", # stderr - id="collection does not exist", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-c COLLECTION", - "[-m]", - "--begin BEGIN", - "[--end END]", - ": error: the following arguments are required: -c/--collection, --begin", - ] - ), - id="no args", - ), - ], -) -def test_delete_content_blocks( - app, collections, capsys, argv, raises, message, stdout, stderr -): - with ( - mock.patch("opentaxii.cli.persistence.app", app), - mock.patch("sys.argv", [""] + argv), - ): - with conditional_raises(raises) as exception: - delete_content_blocks() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr", "expected_call"], - [ - pytest.param( - ["-u", "myuser", "-p", "mypass"], # argv - False, # raises - None, # message - "token: JWT_TOKEN", # stdout - "", # stderr - { # expected_call - "username": "myuser", - "password": "mypass", - "is_admin": False, - }, - id="regular-user", - ), - pytest.param( - ["-u", "myuser", "-p", "mypass", "-a"], # argv - False, # raises - None, # message - "token: JWT_TOKEN", # stdout - "", # stderr - { # expected_call - "username": "myuser", - "password": "mypass", - "is_admin": True, - }, - id="admin", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-u USERNAME", - "-p PASSWORD", - "[-a]", - ": error: the following arguments are required: -u/--username, -p/--password", - ] - ), - None, # expected_call - id="no args", - ), - ], -) -def test_create_account( - app, capsys, argv, raises, message, stdout, stderr, expected_call -): - with ( - mock.patch("opentaxii.cli.auth.app", app), - mock.patch("sys.argv", [""] + argv), - mock.patch.object( - app.taxii_server.auth.api, - "create_account", - wraps=app.taxii_server.auth.api.create_account, - ) as mock_create_account, - ): - with conditional_raises(raises) as exception: - create_account() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - if expected_call is None: - mock_create_account.assert_not_called() - else: - mock_create_account.assert_called_once_with(**expected_call) - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr"], - [ - pytest.param( - ["-u", ACCOUNT.username, "-f", "admin", "-v", "y"], # argv - False, # raises - None, # message - "now user is admin", # stdout - "", # stderr - id="make admin", - ), - pytest.param( - ["-u", ACCOUNT.username, "-f", "admin", "-v", "n"], # argv - False, # raises - None, # message - "now user is mortal", # stdout - "", # stderr - id="drop admin", - ), - pytest.param( - ["-u", ACCOUNT.username, "-f", "password", "-v", "newpass"], # argv - False, # raises - None, # message - "password has been changed", # stdout - "", # stderr - id="change password", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-u USERNAME", - "-f {password,admin}", - "-v VALUE", - ": error: the following arguments are required: -u/--username, -f/--field, -v/--value", - ] - ), - id="no args", - ), - ], -) -def test_update_account(app, account, capsys, argv, raises, message, stdout, stderr): - with mock.patch("opentaxii.cli.auth.app", app), mock.patch("sys.argv", [""] + argv): - with conditional_raises(raises) as exception: - update_account() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr", "expected_call"], - [ - pytest.param( - ["-t", "my new api root"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": None, - "default": False, - "is_public": False, - "api_root_id": None, - }, # expected_call - id="title only", - ), - pytest.param( - ["-t", "my new api root", "-d", "my description"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": "my description", - "default": False, - "is_public": False, - "api_root_id": None, - }, # expected_call - id="title, description", - ), - pytest.param( - ["-t", "my new api root", "--default"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": None, - "default": True, - "is_public": False, - "api_root_id": None, - }, # expected_call - id="title, default", - ), - pytest.param( - ["-t", "my new api root", "-d", "my description", "--default"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": "my description", - "default": True, - "is_public": False, - "api_root_id": None, - }, # expected_call - id="title, description, default", - ), - pytest.param( - ["-t", "my new api root", "--public"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": None, - "default": False, - "is_public": True, - "api_root_id": None, - }, # expected_call - id="title, public", - ), - pytest.param( - [ - "-t", - "my new api root", - "--id", - "7468eafb-585d-402e-b6b9-49fe76492f9e", - ], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "title": "my new api root", - "description": None, - "default": False, - "is_public": False, - "api_root_id": "7468eafb-585d-402e-b6b9-49fe76492f9e", - }, # expected_call - id="title, id", - ), - pytest.param( - ["-t", "my new api root", "--id", "7468eafb-585d-402e-b6b9"], # argv - ValueError, # raises - "badly formed hexadecimal UUID string", # message - "", # stdout - "", # stderr - None, # expected_call - id="title, id (bad)", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-t TITLE", - "[-d DESCRIPTION]", - "[--default]", - "[--public]", - "[-i ID]", - ": error: the following arguments are required: -t/--title", - ] - ), - None, # expected_call - id="no args", - ), - ], -) -def test_add_api_root( - app, capsys, argv, raises, message, stdout, stderr, expected_call -): - with ( - mock.patch("opentaxii.cli.persistence.app", app), - mock.patch("sys.argv", [""] + argv), - mock.patch.object( - app.taxii_server.servers.taxii2.persistence.api, - "add_api_root", - autospec=True, - ) as mock_add_api_root, - ): - with conditional_raises(raises) as exception: - add_api_root() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - if expected_call is None: - mock_add_api_root.assert_not_called() - else: - mock_add_api_root.assert_called_once_with(**expected_call) - - -@pytest.mark.parametrize( - ["argv", "raises", "message", "stdout", "stderr", "expected_call"], - [ - pytest.param( - ["-r", str(API_ROOTS[0].id), "-t", "my new collection"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "api_root_id": str(API_ROOTS[0].id), - "title": "my new collection", - "description": None, - "alias": None, - "is_public": False, - "is_public_write": False, - }, # expected_call - id="rootid, title only", - ), - pytest.param( - [ - "-r", - str(API_ROOTS[0].id), - "-t", - "my new collection", - "-d", - "my description", - ], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "api_root_id": str(API_ROOTS[0].id), - "title": "my new collection", - "description": "my description", - "alias": None, - "is_public": False, - "is_public_write": False, - }, # expected_call - id="rootid, title, description", - ), - pytest.param( - [ - "-r", - str(API_ROOTS[0].id), - "-t", - "my new collection", - "-d", - "my description", - "-a", - "my-alias", - ], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "api_root_id": str(API_ROOTS[0].id), - "title": "my new collection", - "description": "my description", - "alias": "my-alias", - "is_public": False, - "is_public_write": False, - }, # expected_call - id="rootid, title, description, alias", - ), - pytest.param( - ["-r", str(API_ROOTS[0].id), "-t", "my new collection", "--public"], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "api_root_id": str(API_ROOTS[0].id), - "title": "my new collection", - "description": None, - "alias": None, - "is_public": True, - "is_public_write": False, - }, # expected_call - id="rootid, titlei, public", - ), - pytest.param( - [ - "-r", - str(API_ROOTS[0].id), - "-t", - "my new collection", - "--public-write", - ], # argv - False, # raises - None, # message - "", # stdout - "", # stderr - { - "api_root_id": str(API_ROOTS[0].id), - "title": "my new collection", - "description": None, - "alias": None, - "is_public": False, - "is_public_write": True, - }, # expected_call - id="rootid, titlei, publicwrite", - ), - pytest.param( - ["-r", "fake-uuid", "-t", "my new collection"], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-r {ROOTIDS}", - "-t TITLE", - "[-d DESCRIPTION]", - "[-a ALIAS]", - "[--public]", - "[--public-write]", - ": error: argument -r/--rootid: invalid choice: 'fake-uuid'", - "(choose from WRAPPED_ROOTIDS)", - ] - ), - None, # expected_call - id="unknown api root", - ), - pytest.param( - [], # argv - SystemExit, # raises - "2", # message - "", # stdout - "".join( # stderr - [ - "usage:", - "[-h]", - "-r {ROOTIDS}", - "-t TITLE", - "[-d DESCRIPTION]", - "[-a ALIAS]", - "[--public]", - "[--public-write]", - ": error: the following arguments are required: -r/--rootid, -t/--title", - ] - ), - None, # expected_call - id="no args", - ), - ], -) -def test_add_collection( - app, db_api_roots, capsys, argv, raises, message, stdout, stderr, expected_call -): - stderr = stderr.replace( - "WRAPPED_ROOTIDS", - ",".join([f"'{api_root.id}'" for api_root in db_api_roots]), - ) - stderr = stderr.replace( - "ROOTIDS", - ",".join([str(api_root.id) for api_root in db_api_roots]), - ) - with ( - mock.patch("opentaxii.cli.persistence.app", app), - mock.patch("sys.argv", [""] + argv), - mock.patch.object( - app.taxii_server.servers.taxii2.persistence.api, "add_collection" - ) as mock_add_collection, - ): - with conditional_raises(raises) as exception: - add_collection() - if raises: - assert str(exception.value) == message - captured = capsys.readouterr() - assert_str_equal_no_formatting(captured.out, stdout) - assert_str_equal_no_formatting(captured.err, stderr) - if expected_call is None: - mock_add_collection.assert_not_called() - else: - mock_add_collection.assert_called_once_with(**expected_call) - - -def test_job_cleanup(app, capsys): - with ( - mock.patch("opentaxii.cli.persistence.app", app), - mock.patch.object( - app.taxii_server.servers.taxii2.persistence.api, - "job_cleanup", - return_value=2, - ) as mock_cleanup, - ): - job_cleanup() - mock_cleanup.assert_called_once_with() - captured = capsys.readouterr() - assert captured.out == "2 removed\n" - assert captured.err == "" +import os +from unittest import mock + +import pytest + +from opentaxii.cli.auth import create_account, update_account +from opentaxii.cli.persistence import ( + add_api_root, + add_collection, + delete_content_blocks, + job_cleanup, + sync_data_configuration, +) +from tests.fixtures import ACCOUNT, COLLECTION_OPEN +from tests.taxii2.utils import API_ROOTS +from tests.utils import assert_str_equal_no_formatting, conditional_raises + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr"], + [ + pytest.param( + [os.path.join("examples", "data-configuration.yml")], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + id="default", + ), + pytest.param( + [os.path.join("examples", "data-configuration.yml"), "-f"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + id="default, -f", + ), + pytest.param( + [ + os.path.join("examples", "data-configuration.yml"), + "--force-delete", + ], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + id="default, --force-delete", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "[-f]", + "config", + ":error: the following arguments are required: config", + ] + ), + id="no args", + ), + pytest.param( + ["this_file_does_not_exist"], # argv + FileNotFoundError, # raises + "[Errno 2] No such file or directory: 'this_file_does_not_exist'", # message + "", # stdout + "", # stderr + id="missing config", + ), + pytest.param( + ["this_file_does_not_exist", "-f"], # argv + FileNotFoundError, # raises + "[Errno 2] No such file or directory: 'this_file_does_not_exist'", # message + "", # stdout + "", # stderr + id="missing config, with -f", + ), + ], +) +def test_sync_data_configuration(app, capsys, argv, raises, message, stdout, stderr): + with ( + mock.patch("opentaxii.cli.persistence.app", app), + mock.patch("sys.argv", [""] + argv), + ): + with conditional_raises(raises) as exception: + sync_data_configuration() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr"], + [ + pytest.param( + ["-c", COLLECTION_OPEN, "--begin", "2000-01-01"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + id="good", + ), + pytest.param( + ["-c", "collectiondoesnotexist", "--begin", "2000-01-01"], # argv + ValueError, # raises + "Collection with name 'collectiondoesnotexist' does not exist", # message + "", # stdout + "", # stderr + id="collection does not exist", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-c COLLECTION", + "[-m]", + "--begin BEGIN", + "[--end END]", + ": error: the following arguments are required: -c/--collection, --begin", + ] + ), + id="no args", + ), + ], +) +def test_delete_content_blocks( + app, collections, capsys, argv, raises, message, stdout, stderr +): + with ( + mock.patch("opentaxii.cli.persistence.app", app), + mock.patch("sys.argv", [""] + argv), + ): + with conditional_raises(raises) as exception: + delete_content_blocks() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr", "expected_call"], + [ + pytest.param( + ["-u", "myuser", "-p", "mypass"], # argv + False, # raises + None, # message + "token: JWT_TOKEN", # stdout + "", # stderr + { # expected_call + "username": "myuser", + "password": "mypass", + "is_admin": False, + }, + id="regular-user", + ), + pytest.param( + ["-u", "myuser", "-p", "mypass", "-a"], # argv + False, # raises + None, # message + "token: JWT_TOKEN", # stdout + "", # stderr + { # expected_call + "username": "myuser", + "password": "mypass", + "is_admin": True, + }, + id="admin", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-u USERNAME", + "-p PASSWORD", + "[-a]", + ": error: the following arguments are required: -u/--username, -p/--password", + ] + ), + None, # expected_call + id="no args", + ), + ], +) +def test_create_account( + app, capsys, argv, raises, message, stdout, stderr, expected_call +): + with ( + mock.patch("opentaxii.cli.auth.app", app), + mock.patch("sys.argv", [""] + argv), + mock.patch.object( + app.taxii_server.auth.api, + "create_account", + wraps=app.taxii_server.auth.api.create_account, + ) as mock_create_account, + ): + with conditional_raises(raises) as exception: + create_account() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + if expected_call is None: + mock_create_account.assert_not_called() + else: + mock_create_account.assert_called_once_with(**expected_call) + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr"], + [ + pytest.param( + ["-u", ACCOUNT.username, "-f", "admin", "-v", "y"], # argv + False, # raises + None, # message + "now user is admin", # stdout + "", # stderr + id="make admin", + ), + pytest.param( + ["-u", ACCOUNT.username, "-f", "admin", "-v", "n"], # argv + False, # raises + None, # message + "now user is mortal", # stdout + "", # stderr + id="drop admin", + ), + pytest.param( + ["-u", ACCOUNT.username, "-f", "password", "-v", "newpass"], # argv + False, # raises + None, # message + "password has been changed", # stdout + "", # stderr + id="change password", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-u USERNAME", + "-f {password,admin}", + "-v VALUE", + ": error: the following arguments are required: -u/--username, -f/--field, -v/--value", + ] + ), + id="no args", + ), + ], +) +def test_update_account(app, account, capsys, argv, raises, message, stdout, stderr): + with mock.patch("opentaxii.cli.auth.app", app), mock.patch("sys.argv", [""] + argv): + with conditional_raises(raises) as exception: + update_account() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr", "expected_call"], + [ + pytest.param( + ["-t", "my new api root"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": None, + "default": False, + "is_public": False, + "api_root_id": None, + }, # expected_call + id="title only", + ), + pytest.param( + ["-t", "my new api root", "-d", "my description"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": "my description", + "default": False, + "is_public": False, + "api_root_id": None, + }, # expected_call + id="title, description", + ), + pytest.param( + ["-t", "my new api root", "--default"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": None, + "default": True, + "is_public": False, + "api_root_id": None, + }, # expected_call + id="title, default", + ), + pytest.param( + ["-t", "my new api root", "-d", "my description", "--default"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": "my description", + "default": True, + "is_public": False, + "api_root_id": None, + }, # expected_call + id="title, description, default", + ), + pytest.param( + ["-t", "my new api root", "--public"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": None, + "default": False, + "is_public": True, + "api_root_id": None, + }, # expected_call + id="title, public", + ), + pytest.param( + [ + "-t", + "my new api root", + "--id", + "7468eafb-585d-402e-b6b9-49fe76492f9e", + ], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "title": "my new api root", + "description": None, + "default": False, + "is_public": False, + "api_root_id": "7468eafb-585d-402e-b6b9-49fe76492f9e", + }, # expected_call + id="title, id", + ), + pytest.param( + ["-t", "my new api root", "--id", "7468eafb-585d-402e-b6b9"], # argv + ValueError, # raises + "badly formed hexadecimal UUID string", # message + "", # stdout + "", # stderr + None, # expected_call + id="title, id (bad)", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-t TITLE", + "[-d DESCRIPTION]", + "[--default]", + "[--public]", + "[-i ID]", + ": error: the following arguments are required: -t/--title", + ] + ), + None, # expected_call + id="no args", + ), + ], +) +def test_add_api_root( + app, capsys, argv, raises, message, stdout, stderr, expected_call +): + with ( + mock.patch("opentaxii.cli.persistence.app", app), + mock.patch("sys.argv", [""] + argv), + mock.patch.object( + app.taxii_server.servers.taxii2.persistence.api, + "add_api_root", + autospec=True, + ) as mock_add_api_root, + ): + with conditional_raises(raises) as exception: + add_api_root() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + if expected_call is None: + mock_add_api_root.assert_not_called() + else: + mock_add_api_root.assert_called_once_with(**expected_call) + + +@pytest.mark.parametrize( + ["argv", "raises", "message", "stdout", "stderr", "expected_call"], + [ + pytest.param( + ["-r", str(API_ROOTS[0].id), "-t", "my new collection"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "api_root_id": str(API_ROOTS[0].id), + "title": "my new collection", + "description": None, + "alias": None, + "is_public": False, + "is_public_write": False, + }, # expected_call + id="rootid, title only", + ), + pytest.param( + [ + "-r", + str(API_ROOTS[0].id), + "-t", + "my new collection", + "-d", + "my description", + ], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "api_root_id": str(API_ROOTS[0].id), + "title": "my new collection", + "description": "my description", + "alias": None, + "is_public": False, + "is_public_write": False, + }, # expected_call + id="rootid, title, description", + ), + pytest.param( + [ + "-r", + str(API_ROOTS[0].id), + "-t", + "my new collection", + "-d", + "my description", + "-a", + "my-alias", + ], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "api_root_id": str(API_ROOTS[0].id), + "title": "my new collection", + "description": "my description", + "alias": "my-alias", + "is_public": False, + "is_public_write": False, + }, # expected_call + id="rootid, title, description, alias", + ), + pytest.param( + ["-r", str(API_ROOTS[0].id), "-t", "my new collection", "--public"], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "api_root_id": str(API_ROOTS[0].id), + "title": "my new collection", + "description": None, + "alias": None, + "is_public": True, + "is_public_write": False, + }, # expected_call + id="rootid, titlei, public", + ), + pytest.param( + [ + "-r", + str(API_ROOTS[0].id), + "-t", + "my new collection", + "--public-write", + ], # argv + False, # raises + None, # message + "", # stdout + "", # stderr + { + "api_root_id": str(API_ROOTS[0].id), + "title": "my new collection", + "description": None, + "alias": None, + "is_public": False, + "is_public_write": True, + }, # expected_call + id="rootid, titlei, publicwrite", + ), + pytest.param( + ["-r", "fake-uuid", "-t", "my new collection"], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-r {ROOTIDS}", + "-t TITLE", + "[-d DESCRIPTION]", + "[-a ALIAS]", + "[--public]", + "[--public-write]", + ": error: argument -r/--rootid: invalid choice: 'fake-uuid'", + "(choose from WRAPPED_ROOTIDS)", + ] + ), + None, # expected_call + id="unknown api root", + ), + pytest.param( + [], # argv + SystemExit, # raises + "2", # message + "", # stdout + "".join( # stderr + [ + "usage:", + "[-h]", + "-r {ROOTIDS}", + "-t TITLE", + "[-d DESCRIPTION]", + "[-a ALIAS]", + "[--public]", + "[--public-write]", + ": error: the following arguments are required: -r/--rootid, -t/--title", + ] + ), + None, # expected_call + id="no args", + ), + ], +) +def test_add_collection( + app, db_api_roots, capsys, argv, raises, message, stdout, stderr, expected_call +): + stderr = stderr.replace( + "WRAPPED_ROOTIDS", + ",".join([f"'{api_root.id}'" for api_root in db_api_roots]), + ) + stderr = stderr.replace( + "ROOTIDS", + ",".join([str(api_root.id) for api_root in db_api_roots]), + ) + with ( + mock.patch("opentaxii.cli.persistence.app", app), + mock.patch("sys.argv", [""] + argv), + mock.patch.object( + app.taxii_server.servers.taxii2.persistence.api, "add_collection" + ) as mock_add_collection, + ): + with conditional_raises(raises) as exception: + add_collection() + if raises: + assert str(exception.value) == message + captured = capsys.readouterr() + assert_str_equal_no_formatting(captured.out, stdout) + assert_str_equal_no_formatting(captured.err, stderr) + if expected_call is None: + mock_add_collection.assert_not_called() + else: + mock_add_collection.assert_called_once_with(**expected_call) + + +def test_job_cleanup(app, capsys): + with ( + mock.patch("opentaxii.cli.persistence.app", app), + mock.patch.object( + app.taxii_server.servers.taxii2.persistence.api, + "job_cleanup", + return_value=2, + ) as mock_cleanup, + ): + job_cleanup() + mock_cleanup.assert_called_once_with() + captured = capsys.readouterr() + assert captured.out == "2 removed\n" + assert captured.err == "" diff --git a/tests/test_config.py b/tests/test_config.py index d2a6f4ce..93851d53 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,271 +1,271 @@ -import os -import tempfile -from typing import Tuple - -import pytest - -from opentaxii.config import ServerConfig - -BACKWARDS_COMPAT_CONFIG = """ ---- -dummy: some -persistence_api: - class: some.test.PersistenceClass - parameters: - a: 1 - b: 2 -auth_api: - class: other.test.AuthClass - parameters: - c: 3 -""" -COMBINED_CONFIG = """ ---- -dummy: some -auth_api: - class: other.test.AuthClass - parameters: - c: 3 -taxii1: - persistence_api: - class: some.test.PersistenceClass - parameters: - a: 1 - b: 2 -taxii2: - persistence_api: - class: some.test.Taxii2PersistenceClass - parameters: - a: 1 - b: 2 - max_content_length: 1024 - allow_custom_properties: true - public_discovery: true -""" -TAXII2_CONFIG = """ ---- -dummy: some -persistence_api: - class: some.test.PersistenceClass -auth_api: - class: other.test.AuthClass - parameters: - c: 3 -taxii1: -taxii2: - persistence_api: - class: some.test.Taxii2PersistenceClass - parameters: - a: 1 - b: 2 - max_content_length: 1024 - allow_custom_properties: true - public_discovery: true -""" -DEFAULT_BASE_VALUES = { - "domain": "localhost:9000", - "support_basic_auth": True, - "return_server_error_details": False, - "logging": {"opentaxii": "info", "root": "info"}, - "auth_api": { - "class": "other.test.AuthClass", - "parameters": { - "c": 3, - "create_tables": True, - "db_connection": "sqlite:////tmp/auth.db", - "secret": "SECRET-STRING-NEEDS-TO-BE-CHANGED", - "token_ttl_secs": 3600, - }, - }, -} -DEFAULT_TAXII1_VALUES = { - "save_raw_inbox_messages": True, - "xml_parser_supports_huge_tree": True, - "unauthorized_status": "UNAUTHORIZED", - "hooks": None, - "count_blocks_in_poll_responses": False, -} -DEFAULT_TAXII2_VALUES = { - "persistence_api": { - "class": "opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI", - "parameters": { - "create_tables": True, - "db_connection": "sqlite:////tmp/data.db", - }, - }, - "title": "TAXII2 Server", - "public_discovery": True, - "max_content_length": 209715200, -} -TAXII1_VALUES = { - "persistence_api": { - "class": "some.test.PersistenceClass", - "parameters": { - "a": 1, - "b": 2, - "create_tables": True, - "db_connection": "sqlite:////tmp/data.db", - }, - }, -} -TAXII2_VALUES = { - "persistence_api": { - "class": "some.test.Taxii2PersistenceClass", - "parameters": { - "a": 1, - "b": 2, - "create_tables": True, - "db_connection": "sqlite:////tmp/data.db", - }, - }, - "max_content_length": 1024, - "public_discovery": True, - "allow_custom_properties": True, -} -EXPECTED_VALUES = { - BACKWARDS_COMPAT_CONFIG: { - **DEFAULT_BASE_VALUES, - "taxii1": { - **DEFAULT_TAXII1_VALUES, - **TAXII1_VALUES, - }, - "taxii2": {**DEFAULT_TAXII2_VALUES}, - }, - COMBINED_CONFIG: { - **DEFAULT_BASE_VALUES, - "taxii1": { - **DEFAULT_TAXII1_VALUES, - **TAXII1_VALUES, - }, - "taxii2": { - **DEFAULT_TAXII2_VALUES, - **TAXII2_VALUES, - }, - }, - TAXII2_CONFIG: { - **DEFAULT_BASE_VALUES, - "taxii1": None, - "taxii2": { - **DEFAULT_TAXII2_VALUES, - **TAXII2_VALUES, - }, - }, -} -DEPRECATION_WARNING = { - BACKWARDS_COMPAT_CONFIG: True, - COMBINED_CONFIG: False, - TAXII2_CONFIG: False, -} -TAXII2_ONLY_WARNING = { - BACKWARDS_COMPAT_CONFIG: False, - COMBINED_CONFIG: False, - TAXII2_CONFIG: True, -} - - -@pytest.fixture( - scope="module", - params=[BACKWARDS_COMPAT_CONFIG, COMBINED_CONFIG, TAXII2_CONFIG], - ids=["BACKWARDS_COMPAT_CONFIG", "COMBINED_CONFIG", "TAXII2_CONFIG"], -) -def config_file_name_expected_value(request): - config = request.param - f = tempfile.NamedTemporaryFile(delete=False) - f.write(config.encode("utf-8")) - f.close() - yield f.name, EXPECTED_VALUES[config], DEPRECATION_WARNING[ - config - ], TAXII2_ONLY_WARNING[config] - os.unlink(f.name) - - -def test_custom_config_file(config_file_name_expected_value): - ( - config_file_name, - expected_value, - deprecation_warning, - taxii2_only_warning, - ) = config_file_name_expected_value - warning_classes: Tuple = (UserWarning,) - if deprecation_warning or taxii2_only_warning: - warning_classes += (DeprecationWarning,) - expected_warnings = {"Ignoring invalid configuration item 'dummy'."} - if deprecation_warning: - expected_warnings |= { - f"Setting taxii1 attributes at top level is deprecated. Please nest '{key}' inside 'taxii1'." - for key in ["persistence_api"] - } - if taxii2_only_warning: - expected_warnings |= { - f"Running in taxii2-only mode. Dropping deprecated top level taxii1 attribute '{key}'." - for key in ["persistence_api"] - } - with pytest.warns(warning_classes) as warnings: - config = ServerConfig(extra_configs=[config_file_name]) - assert dict(config) == expected_value - assert set(str(warning.message) for warning in warnings) == expected_warnings - - -BACKWARDS_COMPAT_ENVVARS = { - "input": { - "OPENTAXII_DOMAIN": "hostname:1337", - "OPENTAXII__SUPPORT_BASIC_AUTH": "yes", - "OPENTAXII__PERSISTENCE_API__CLASS": "something.Else", - "OPENTAXII__PERSISTENCE_API__OTHER": "1", - }, - "expected": { - "domain": "hostname:1337", - "support_basic_auth": True, - "taxii1": {"persistence_api": {"class": "something.Else", "other": 1}}, - }, -} -COMBINED_ENVVARS = { - "input": { - "OPENTAXII__TAXII1__PERSISTENCE_API__CLASS": "something.Else", - "OPENTAXII__TAXII1__PERSISTENCE_API__OTHER": "1", - "OPENTAXII__TAXII2__PERSISTENCE_API__CLASS": "something.Else2", - "OPENTAXII__TAXII2__PERSISTENCE_API__OTHER": "2", - "OPENTAXII__TAXII2__MAX_CONTENT_LENGTH": "1024", - "OPENTAXII__TAXII2__ALLOW_CUSTOM_PROPERTIES": "true", - }, - "expected": { - "taxii1": {"persistence_api": {"class": "something.Else", "other": 1}}, - "taxii2": { - "persistence_api": {"class": "something.Else2", "other": 2}, - "max_content_length": 1024, - "allow_custom_properties": True, - }, - }, -} -TAXII2_ENVVARS = { - "input": { - "OPENTAXII__TAXII2__PERSISTENCE_API__CLASS": "something.Else2", - "OPENTAXII__TAXII2__PERSISTENCE_API__OTHER": "2", - "OPENTAXII__TAXII2__MAX_CONTENT_LENGTH": "1024", - "OPENTAXII__TAXII2__ALLOW_CUSTOM_PROPERTIES": "true", - }, - "expected": { - "taxii2": { - "persistence_api": {"class": "something.Else2", "other": 2}, - "max_content_length": 1024, - "allow_custom_properties": True, - }, - }, -} - - -@pytest.fixture( - scope="module", - params=[BACKWARDS_COMPAT_ENVVARS, COMBINED_ENVVARS, TAXII2_ENVVARS], - ids=["BACKWARDS_COMPAT_ENVVARS", "COMBINED_ENVVARS", "TAXII2_ENVVARS"], -) -def envvars_expected_value(request): - yield request.param["input"], request.param["expected"] - - -def test_env_vars_config(envvars_expected_value): - envvars, expected_value = envvars_expected_value - assert ( - ServerConfig._clean_options(ServerConfig._get_env_config(env=envvars)) - == expected_value - ) +import os +import tempfile +from typing import Tuple + +import pytest + +from opentaxii.config import ServerConfig + +BACKWARDS_COMPAT_CONFIG = """ +--- +dummy: some +persistence_api: + class: some.test.PersistenceClass + parameters: + a: 1 + b: 2 +auth_api: + class: other.test.AuthClass + parameters: + c: 3 +""" +COMBINED_CONFIG = """ +--- +dummy: some +auth_api: + class: other.test.AuthClass + parameters: + c: 3 +taxii1: + persistence_api: + class: some.test.PersistenceClass + parameters: + a: 1 + b: 2 +taxii2: + persistence_api: + class: some.test.Taxii2PersistenceClass + parameters: + a: 1 + b: 2 + max_content_length: 1024 + allow_custom_properties: true + public_discovery: true +""" +TAXII2_CONFIG = """ +--- +dummy: some +persistence_api: + class: some.test.PersistenceClass +auth_api: + class: other.test.AuthClass + parameters: + c: 3 +taxii1: +taxii2: + persistence_api: + class: some.test.Taxii2PersistenceClass + parameters: + a: 1 + b: 2 + max_content_length: 1024 + allow_custom_properties: true + public_discovery: true +""" +DEFAULT_BASE_VALUES = { + "domain": "localhost:9000", + "support_basic_auth": True, + "return_server_error_details": False, + "logging": {"opentaxii": "info", "root": "info"}, + "auth_api": { + "class": "other.test.AuthClass", + "parameters": { + "c": 3, + "create_tables": True, + "db_connection": "sqlite:////tmp/auth.db", + "secret": "SECRET-STRING-NEEDS-TO-BE-CHANGED", + "token_ttl_secs": 3600, + }, + }, +} +DEFAULT_TAXII1_VALUES = { + "save_raw_inbox_messages": True, + "xml_parser_supports_huge_tree": True, + "unauthorized_status": "UNAUTHORIZED", + "hooks": None, + "count_blocks_in_poll_responses": False, +} +DEFAULT_TAXII2_VALUES = { + "persistence_api": { + "class": "opentaxii.persistence.sqldb.Taxii2SQLDatabaseAPI", + "parameters": { + "create_tables": True, + "db_connection": "sqlite:////tmp/data.db", + }, + }, + "title": "TAXII2 Server", + "public_discovery": True, + "max_content_length": 209715200, +} +TAXII1_VALUES = { + "persistence_api": { + "class": "some.test.PersistenceClass", + "parameters": { + "a": 1, + "b": 2, + "create_tables": True, + "db_connection": "sqlite:////tmp/data.db", + }, + }, +} +TAXII2_VALUES = { + "persistence_api": { + "class": "some.test.Taxii2PersistenceClass", + "parameters": { + "a": 1, + "b": 2, + "create_tables": True, + "db_connection": "sqlite:////tmp/data.db", + }, + }, + "max_content_length": 1024, + "public_discovery": True, + "allow_custom_properties": True, +} +EXPECTED_VALUES = { + BACKWARDS_COMPAT_CONFIG: { + **DEFAULT_BASE_VALUES, + "taxii1": { + **DEFAULT_TAXII1_VALUES, + **TAXII1_VALUES, + }, + "taxii2": {**DEFAULT_TAXII2_VALUES}, + }, + COMBINED_CONFIG: { + **DEFAULT_BASE_VALUES, + "taxii1": { + **DEFAULT_TAXII1_VALUES, + **TAXII1_VALUES, + }, + "taxii2": { + **DEFAULT_TAXII2_VALUES, + **TAXII2_VALUES, + }, + }, + TAXII2_CONFIG: { + **DEFAULT_BASE_VALUES, + "taxii1": None, + "taxii2": { + **DEFAULT_TAXII2_VALUES, + **TAXII2_VALUES, + }, + }, +} +DEPRECATION_WARNING = { + BACKWARDS_COMPAT_CONFIG: True, + COMBINED_CONFIG: False, + TAXII2_CONFIG: False, +} +TAXII2_ONLY_WARNING = { + BACKWARDS_COMPAT_CONFIG: False, + COMBINED_CONFIG: False, + TAXII2_CONFIG: True, +} + + +@pytest.fixture( + scope="module", + params=[BACKWARDS_COMPAT_CONFIG, COMBINED_CONFIG, TAXII2_CONFIG], + ids=["BACKWARDS_COMPAT_CONFIG", "COMBINED_CONFIG", "TAXII2_CONFIG"], +) +def config_file_name_expected_value(request): + config = request.param + f = tempfile.NamedTemporaryFile(delete=False) + f.write(config.encode("utf-8")) + f.close() + yield f.name, EXPECTED_VALUES[config], DEPRECATION_WARNING[ + config + ], TAXII2_ONLY_WARNING[config] + os.unlink(f.name) + + +def test_custom_config_file(config_file_name_expected_value): + ( + config_file_name, + expected_value, + deprecation_warning, + taxii2_only_warning, + ) = config_file_name_expected_value + warning_classes: Tuple = (UserWarning,) + if deprecation_warning or taxii2_only_warning: + warning_classes += (DeprecationWarning,) + expected_warnings = {"Ignoring invalid configuration item 'dummy'."} + if deprecation_warning: + expected_warnings |= { + f"Setting taxii1 attributes at top level is deprecated. Please nest '{key}' inside 'taxii1'." + for key in ["persistence_api"] + } + if taxii2_only_warning: + expected_warnings |= { + f"Running in taxii2-only mode. Dropping deprecated top level taxii1 attribute '{key}'." + for key in ["persistence_api"] + } + with pytest.warns(warning_classes) as warnings: + config = ServerConfig(extra_configs=[config_file_name]) + assert dict(config) == expected_value + assert set(str(warning.message) for warning in warnings) == expected_warnings + + +BACKWARDS_COMPAT_ENVVARS = { + "input": { + "OPENTAXII_DOMAIN": "hostname:1337", + "OPENTAXII__SUPPORT_BASIC_AUTH": "yes", + "OPENTAXII__PERSISTENCE_API__CLASS": "something.Else", + "OPENTAXII__PERSISTENCE_API__OTHER": "1", + }, + "expected": { + "domain": "hostname:1337", + "support_basic_auth": True, + "taxii1": {"persistence_api": {"class": "something.Else", "other": 1}}, + }, +} +COMBINED_ENVVARS = { + "input": { + "OPENTAXII__TAXII1__PERSISTENCE_API__CLASS": "something.Else", + "OPENTAXII__TAXII1__PERSISTENCE_API__OTHER": "1", + "OPENTAXII__TAXII2__PERSISTENCE_API__CLASS": "something.Else2", + "OPENTAXII__TAXII2__PERSISTENCE_API__OTHER": "2", + "OPENTAXII__TAXII2__MAX_CONTENT_LENGTH": "1024", + "OPENTAXII__TAXII2__ALLOW_CUSTOM_PROPERTIES": "true", + }, + "expected": { + "taxii1": {"persistence_api": {"class": "something.Else", "other": 1}}, + "taxii2": { + "persistence_api": {"class": "something.Else2", "other": 2}, + "max_content_length": 1024, + "allow_custom_properties": True, + }, + }, +} +TAXII2_ENVVARS = { + "input": { + "OPENTAXII__TAXII2__PERSISTENCE_API__CLASS": "something.Else2", + "OPENTAXII__TAXII2__PERSISTENCE_API__OTHER": "2", + "OPENTAXII__TAXII2__MAX_CONTENT_LENGTH": "1024", + "OPENTAXII__TAXII2__ALLOW_CUSTOM_PROPERTIES": "true", + }, + "expected": { + "taxii2": { + "persistence_api": {"class": "something.Else2", "other": 2}, + "max_content_length": 1024, + "allow_custom_properties": True, + }, + }, +} + + +@pytest.fixture( + scope="module", + params=[BACKWARDS_COMPAT_ENVVARS, COMBINED_ENVVARS, TAXII2_ENVVARS], + ids=["BACKWARDS_COMPAT_ENVVARS", "COMBINED_ENVVARS", "TAXII2_ENVVARS"], +) +def envvars_expected_value(request): + yield request.param["input"], request.param["expected"] + + +def test_env_vars_config(envvars_expected_value): + envvars, expected_value = envvars_expected_value + assert ( + ServerConfig._clean_options(ServerConfig._get_env_config(env=envvars)) + == expected_value + ) diff --git a/tests/test_converters.py b/tests/test_converters.py index e11cd90c..91d42051 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -1,25 +1,25 @@ -import pytest -from libtaxii import messages_10 as tm10 -from libtaxii import messages_11 as tm11 -from libtaxii.constants import VID_TAXII_XML_10, VID_TAXII_XML_11 - -from opentaxii.taxii import exceptions -from opentaxii.taxii.utils import parse_message - -MESSAGE_ID = '123' - - -@pytest.mark.parametrize("content_type", [VID_TAXII_XML_10, VID_TAXII_XML_11]) -def test_parse_message(content_type): - - with pytest.raises(exceptions.BadMessageStatus): - parse_message(content_type, 'invalid-body', do_validate=True) - - tm = tm10 if content_type == VID_TAXII_XML_10 else tm11 - - parsed = parse_message( - content_type, tm.DiscoveryRequest(MESSAGE_ID).to_xml(), do_validate=True - ) - - assert isinstance(parsed, tm.DiscoveryRequest) - assert parsed.message_id == MESSAGE_ID +import pytest +from libtaxii import messages_10 as tm10 +from libtaxii import messages_11 as tm11 +from libtaxii.constants import VID_TAXII_XML_10, VID_TAXII_XML_11 + +from opentaxii.taxii import exceptions +from opentaxii.taxii.utils import parse_message + +MESSAGE_ID = '123' + + +@pytest.mark.parametrize("content_type", [VID_TAXII_XML_10, VID_TAXII_XML_11]) +def test_parse_message(content_type): + + with pytest.raises(exceptions.BadMessageStatus): + parse_message(content_type, 'invalid-body', do_validate=True) + + tm = tm10 if content_type == VID_TAXII_XML_10 else tm11 + + parsed = parse_message( + content_type, tm.DiscoveryRequest(MESSAGE_ID).to_xml(), do_validate=True + ) + + assert isinstance(parsed, tm.DiscoveryRequest) + assert parsed.message_id == MESSAGE_ID diff --git a/tests/test_delete_content_blocks.py b/tests/test_delete_content_blocks.py index 0538ea85..76d44c14 100644 --- a/tests/test_delete_content_blocks.py +++ b/tests/test_delete_content_blocks.py @@ -1,15 +1,15 @@ -import datetime - -import pytest - -from .fixtures import COLLECTION_OPEN, COLLECTIONS_A - - -@pytest.mark.parametrize("with_messages", [True, False]) -def test_delete_content_blocks(app, with_messages): - app.taxii_server.servers.taxii1.persistence.create_collection(COLLECTIONS_A[0]) - app.taxii_server.servers.taxii1.persistence.delete_content_blocks( - COLLECTION_OPEN, - start_time=datetime.datetime.now(datetime.timezone.utc), - with_messages=with_messages, - ) +import datetime + +import pytest + +from .fixtures import COLLECTION_OPEN, COLLECTIONS_A + + +@pytest.mark.parametrize("with_messages", [True, False]) +def test_delete_content_blocks(app, with_messages): + app.taxii_server.servers.taxii1.persistence.create_collection(COLLECTIONS_A[0]) + app.taxii_server.servers.taxii1.persistence.delete_content_blocks( + COLLECTION_OPEN, + start_time=datetime.datetime.now(datetime.timezone.utc), + with_messages=with_messages, + ) diff --git a/tests/test_health.py b/tests/test_health.py index 5303e0f6..2cf34d9b 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,17 +1,17 @@ -import json - -import pytest - -HEALTH_PATH = '/management/health' - - -@pytest.mark.parametrize("https", [True, False]) -def test_get_health(client, https): - base_url = '%s://localhost' % ('https' if https else 'http') - - # Invalid credentials - response = client.get(HEALTH_PATH, base_url=base_url) - assert response.status_code == 200 - - data = json.loads(response.get_data(as_text=True)) - assert data.get('alive') +import json + +import pytest + +HEALTH_PATH = '/management/health' + + +@pytest.mark.parametrize("https", [True, False]) +def test_get_health(client, https): + base_url = '%s://localhost' % ('https' if https else 'http') + + # Invalid credentials + response = client.get(HEALTH_PATH, base_url=base_url) + assert response.status_code == 200 + + data = json.loads(response.get_data(as_text=True)) + assert data.get('alive') diff --git a/tests/test_http.py b/tests/test_http.py index 57682198..00c024c9 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,156 +1,156 @@ -import pytest -from libtaxii.constants import ST_BAD_MESSAGE, ST_FAILURE - -from opentaxii.taxii.converters import dict_to_service_entity -from opentaxii.taxii.http import HTTP_X_TAXII_SERVICES - -from .utils import as_tm, is_headers_valid, prepare_headers - -INBOX = dict( - id='inbox-A', - type='inbox', - description='inboxA description', - destination_collection_required=True, - address='/relative/path', - accept_all_content=True, - protocol_bindings=[ - 'urn:taxii.mitre.org:protocol:http:1.0', - 'urn:taxii.mitre.org:protocol:https:1.0', - ], -) - -DISCOVERY = dict( - id='discovery-A', - type='discovery', - description='discoveryA description', - address='/relative/discovery', - advertised_services=['inbox-A', 'discovery-A', 'discovery-B'], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], -) - -DISCOVERY_NOT_AVAILABLE = dict( - id='discovery-B', - type='discovery', - description='discoveryA description', - address='/relative/discovery-b', - advertised_services=['inbox-A', 'discovery-A'], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], - available=False, -) - -SERVICES = [INBOX, DISCOVERY, DISCOVERY_NOT_AVAILABLE] -INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) # type: ignore -MESSAGE_ID = '123' - - -@pytest.fixture(autouse=True) -def local_services(server): - for service in SERVICES: - server.servers.taxii1.persistence.update_service( - dict_to_service_entity(service) - ) - - -def test_root_get(client): - assert client.get('/').status_code == 404 - - -def test_get_not_allowed(client): - response = client.get(INBOX['address']) - assert response.status_code == 405 - - -def test_not_accepted_without_headers(client): - response = client.post(INBOX['address']) - assert response.status_code == 406 - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_status_message_response(client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - - response = client.post( - INBOX['address'], - data='invalid-body', - headers=prepare_headers(version, https), - base_url=base_url, - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version, https) - - message = as_tm(version).get_message_from_xml(response.data) - - assert message.status_type == ST_BAD_MESSAGE - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_successful_response(client, version, https): - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - base_url = '%s://localhost' % ('https' if https else 'http') - - response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=prepare_headers(version=version, https=https), - base_url=base_url, - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version=version, https=https) - - message = as_tm(version).get_message_from_xml(response.data) - - assert isinstance(message, as_tm(version).DiscoveryResponse) - assert len(message.service_instances) == INSTANCES_CONFIGURED - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_post_parse_verification(client, version, https): - - headers = prepare_headers(version, https) - headers[HTTP_X_TAXII_SERVICES] = 'invalid-services-spec' - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - base_url = '%s://localhost' % ('https' if https else 'http') - - response = client.post( - DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url - ) - - assert response.status_code == 200 - assert is_headers_valid(response.headers, version=version, https=https) - - message = as_tm(version).get_message_from_xml(response.data) - - assert message.status_type == ST_FAILURE - assert message.in_response_to == MESSAGE_ID - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_services_available(client, version, https): - - headers = prepare_headers(version, https) - - request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) - base_url = '%s://localhost' % ('https' if https else 'http') - - response = client.post( - DISCOVERY_NOT_AVAILABLE['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url, - ) - - assert response.status_code == 200 - - message = as_tm(version).get_message_from_xml(response.data) - - assert isinstance(message, as_tm(version).StatusMessage) - assert message.status_type == ST_FAILURE +import pytest +from libtaxii.constants import ST_BAD_MESSAGE, ST_FAILURE + +from opentaxii.taxii.converters import dict_to_service_entity +from opentaxii.taxii.http import HTTP_X_TAXII_SERVICES + +from .utils import as_tm, is_headers_valid, prepare_headers + +INBOX = dict( + id='inbox-A', + type='inbox', + description='inboxA description', + destination_collection_required=True, + address='/relative/path', + accept_all_content=True, + protocol_bindings=[ + 'urn:taxii.mitre.org:protocol:http:1.0', + 'urn:taxii.mitre.org:protocol:https:1.0', + ], +) + +DISCOVERY = dict( + id='discovery-A', + type='discovery', + description='discoveryA description', + address='/relative/discovery', + advertised_services=['inbox-A', 'discovery-A', 'discovery-B'], + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], +) + +DISCOVERY_NOT_AVAILABLE = dict( + id='discovery-B', + type='discovery', + description='discoveryA description', + address='/relative/discovery-b', + advertised_services=['inbox-A', 'discovery-A'], + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], + available=False, +) + +SERVICES = [INBOX, DISCOVERY, DISCOVERY_NOT_AVAILABLE] +INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) # type: ignore +MESSAGE_ID = '123' + + +@pytest.fixture(autouse=True) +def local_services(server): + for service in SERVICES: + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) + + +def test_root_get(client): + assert client.get('/').status_code == 404 + + +def test_get_not_allowed(client): + response = client.get(INBOX['address']) + assert response.status_code == 405 + + +def test_not_accepted_without_headers(client): + response = client.post(INBOX['address']) + assert response.status_code == 406 + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_status_message_response(client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + + response = client.post( + INBOX['address'], + data='invalid-body', + headers=prepare_headers(version, https), + base_url=base_url, + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version, https) + + message = as_tm(version).get_message_from_xml(response.data) + + assert message.status_type == ST_BAD_MESSAGE + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_successful_response(client, version, https): + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + base_url = '%s://localhost' % ('https' if https else 'http') + + response = client.post( + DISCOVERY['address'], + data=request.to_xml(), + headers=prepare_headers(version=version, https=https), + base_url=base_url, + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version=version, https=https) + + message = as_tm(version).get_message_from_xml(response.data) + + assert isinstance(message, as_tm(version).DiscoveryResponse) + assert len(message.service_instances) == INSTANCES_CONFIGURED + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_post_parse_verification(client, version, https): + + headers = prepare_headers(version, https) + headers[HTTP_X_TAXII_SERVICES] = 'invalid-services-spec' + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + base_url = '%s://localhost' % ('https' if https else 'http') + + response = client.post( + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) + + assert response.status_code == 200 + assert is_headers_valid(response.headers, version=version, https=https) + + message = as_tm(version).get_message_from_xml(response.data) + + assert message.status_type == ST_FAILURE + assert message.in_response_to == MESSAGE_ID + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_services_available(client, version, https): + + headers = prepare_headers(version, https) + + request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) + base_url = '%s://localhost' % ('https' if https else 'http') + + response = client.post( + DISCOVERY_NOT_AVAILABLE['address'], + data=request.to_xml(), + headers=headers, + base_url=base_url, + ) + + assert response.status_code == 200 + + message = as_tm(version).get_message_from_xml(response.data) + + assert isinstance(message, as_tm(version).StatusMessage) + assert message.status_type == ST_FAILURE diff --git a/tests/test_options.py b/tests/test_options.py index 9a34bdfe..9336c797 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,43 +1,43 @@ -import pytest - -from opentaxii.taxii.converters import dict_to_service_entity -from opentaxii.taxii.http import HTTP_X_TAXII_CONTENT_TYPES - -DISCOVERY = dict( - id='discovery-A', - type='discovery', - description='discoveryA description', - address='/path/discovery', - advertised_services=[], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], - authentication_required=False, -) - - -@pytest.fixture(autouse=True) -def local_services(server): - for service in [DISCOVERY]: - server.servers.taxii1.persistence.update_service( - dict_to_service_entity(service) - ) - - -@pytest.mark.parametrize("https", [True, False]) -@pytest.mark.parametrize("version", [11, 10]) -def test_options_request(server, client, version, https): - - base_url = '%s://localhost' % ('https' if https else 'http') - - response = client.options(DISCOVERY['address'], base_url=base_url) - - assert response.status_code == 200 - assert HTTP_X_TAXII_CONTENT_TYPES in response.headers - - value = response.headers[HTTP_X_TAXII_CONTENT_TYPES] - - versions = value.split(',') - - service = server.servers.taxii1.get_service(DISCOVERY['id']) - service_bindings = service.supported_message_bindings - - assert len(versions) == len(service_bindings) +import pytest + +from opentaxii.taxii.converters import dict_to_service_entity +from opentaxii.taxii.http import HTTP_X_TAXII_CONTENT_TYPES + +DISCOVERY = dict( + id='discovery-A', + type='discovery', + description='discoveryA description', + address='/path/discovery', + advertised_services=[], + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], + authentication_required=False, +) + + +@pytest.fixture(autouse=True) +def local_services(server): + for service in [DISCOVERY]: + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) + + +@pytest.mark.parametrize("https", [True, False]) +@pytest.mark.parametrize("version", [11, 10]) +def test_options_request(server, client, version, https): + + base_url = '%s://localhost' % ('https' if https else 'http') + + response = client.options(DISCOVERY['address'], base_url=base_url) + + assert response.status_code == 200 + assert HTTP_X_TAXII_CONTENT_TYPES in response.headers + + value = response.headers[HTTP_X_TAXII_CONTENT_TYPES] + + versions = value.split(',') + + service = server.servers.taxii1.get_service(DISCOVERY['id']) + service_bindings = service.supported_message_bindings + + assert len(versions) == len(service_bindings) diff --git a/tests/test_server.py b/tests/test_server.py index 86a19f22..8cccf0e0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,90 +1,90 @@ -import concurrent.futures - -import pytest - -from opentaxii.persistence import OpenTAXII2PersistenceAPI, Taxii2PersistenceManager -from opentaxii.persistence.sqldb import Taxii2SQLDatabaseAPI -from opentaxii.server import TAXII2Server -from opentaxii.taxii.converters import dict_to_service_entity - -from .fixtures import DOMAIN - -INBOX = dict( - id='inbox-A', - type='inbox', - description='inboxA description', - destination_collection_required='yes', - address='/relative/path', - accept_all_content='yes', - protocol_bindings=[ - 'urn:taxii.mitre.org:protocol:http:1.0', - 'urn:taxii.mitre.org:protocol:https:1.0', - ], -) - -DISCOVERY = dict( - id='discovery-A', - type='discovery', - description='discoveryA description', - address='/relative/discovery', - advertised_services=['inboxA', 'discoveryA'], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], -) - -DISCOVERY_EXTERNAL = dict( - id='discovery-C', - type='discovery', - description='discoveryB description', - address='http://example.com/a/b/c', - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], -) - -INTERNAL_SERVICES = [INBOX, DISCOVERY] -SERVICES = INTERNAL_SERVICES + [DISCOVERY_EXTERNAL] - - -@pytest.fixture() -def local_services(server): - for service in SERVICES: - server.servers.taxii1.persistence.update_service( - dict_to_service_entity(service) - ) - - -def test_services_configured(server, local_services): - assert len(server.servers.taxii1.get_services()) == len(SERVICES) - - with_paths = [s for s in server.servers.taxii1.get_services() if s.path] - - assert len(with_paths) == len(INTERNAL_SERVICES) - assert all([p.address.startswith(DOMAIN) for p in with_paths]) - - -def test_taxii2_configured(server): - assert server.servers.taxii2 is not None - assert isinstance(server.servers.taxii2, TAXII2Server) - assert isinstance( - server.servers.taxii2.persistence, - Taxii2PersistenceManager, - ) - assert isinstance( - server.servers.taxii2.persistence.api, - Taxii2SQLDatabaseAPI, - ) - assert isinstance( - server.servers.taxii2.persistence.api, - OpenTAXII2PersistenceAPI, - ) - - -@pytest.mark.truncate() -def test_multithreaded_access(server, local_services): - - def testfunc(): - server.servers.taxii1.get_services() - server.servers.taxii1.persistence.api.db.session.commit() - - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - results = [executor.submit(testfunc) for _ in range(2)] - for result in concurrent.futures.as_completed(results): - assert not result.exception(timeout=5) +import concurrent.futures + +import pytest + +from opentaxii.persistence import OpenTAXII2PersistenceAPI, Taxii2PersistenceManager +from opentaxii.persistence.sqldb import Taxii2SQLDatabaseAPI +from opentaxii.server import TAXII2Server +from opentaxii.taxii.converters import dict_to_service_entity + +from .fixtures import DOMAIN + +INBOX = dict( + id='inbox-A', + type='inbox', + description='inboxA description', + destination_collection_required='yes', + address='/relative/path', + accept_all_content='yes', + protocol_bindings=[ + 'urn:taxii.mitre.org:protocol:http:1.0', + 'urn:taxii.mitre.org:protocol:https:1.0', + ], +) + +DISCOVERY = dict( + id='discovery-A', + type='discovery', + description='discoveryA description', + address='/relative/discovery', + advertised_services=['inboxA', 'discoveryA'], + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], +) + +DISCOVERY_EXTERNAL = dict( + id='discovery-C', + type='discovery', + description='discoveryB description', + address='http://example.com/a/b/c', + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], +) + +INTERNAL_SERVICES = [INBOX, DISCOVERY] +SERVICES = INTERNAL_SERVICES + [DISCOVERY_EXTERNAL] + + +@pytest.fixture() +def local_services(server): + for service in SERVICES: + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) + + +def test_services_configured(server, local_services): + assert len(server.servers.taxii1.get_services()) == len(SERVICES) + + with_paths = [s for s in server.servers.taxii1.get_services() if s.path] + + assert len(with_paths) == len(INTERNAL_SERVICES) + assert all([p.address.startswith(DOMAIN) for p in with_paths]) + + +def test_taxii2_configured(server): + assert server.servers.taxii2 is not None + assert isinstance(server.servers.taxii2, TAXII2Server) + assert isinstance( + server.servers.taxii2.persistence, + Taxii2PersistenceManager, + ) + assert isinstance( + server.servers.taxii2.persistence.api, + Taxii2SQLDatabaseAPI, + ) + assert isinstance( + server.servers.taxii2.persistence.api, + OpenTAXII2PersistenceAPI, + ) + + +@pytest.mark.truncate() +def test_multithreaded_access(server, local_services): + + def testfunc(): + server.servers.taxii1.get_services() + server.servers.taxii1.persistence.api.db.session.commit() + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + results = [executor.submit(testfunc) for _ in range(2)] + for result in concurrent.futures.as_completed(results): + assert not result.exception(timeout=5) diff --git a/tests/utils.py b/tests/utils.py index da3e5dc8..b01f17bd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,172 +1,172 @@ -import re - -import pytest -from libtaxii import messages_10 as tm10 -from libtaxii import messages_11 as tm11 - -from opentaxii.taxii import entities -from opentaxii.taxii.http import ( - HTTP_ACCEPT, - HTTP_CONTENT_XML, - TAXII_10_HTTP_HEADERS, - TAXII_10_HTTPS_HEADERS, - TAXII_11_HTTP_HEADERS, - TAXII_11_HTTPS_HEADERS, -) -from opentaxii.taxii.utils import get_utc_now - -from .fixtures import CB_STIX_XML_111, CONTENT, MESSAGE, MESSAGE_ID - -JWT_RE = re.compile(r'[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*') - - -def as_tm(version): - if version == 10: - return tm10 - elif version == 11: - return tm11 - else: - raise ValueError("Unknown TAXII message version: %s" % version) - - -def prepare_headers(version, https): - headers = dict() - if version == 10: - if https: - headers.update(TAXII_10_HTTPS_HEADERS) - else: - headers.update(TAXII_10_HTTP_HEADERS) - elif version == 11: - if https: - headers.update(TAXII_11_HTTPS_HEADERS) - else: - headers.update(TAXII_11_HTTP_HEADERS) - else: - raise ValueError("Unknown TAXII message version: %s" % version) - - headers[HTTP_ACCEPT] = HTTP_CONTENT_XML - return headers - - -def persist_content( - manager, - collection_name, - service_id, - timestamp=None, - binding=CB_STIX_XML_111, - subtypes=[], -): - - timestamp = timestamp or get_utc_now() - - content_binding = entities.ContentBindingEntity(binding=binding, subtypes=subtypes) - - content = entities.ContentBlockEntity( - content=CONTENT, - timestamp_label=timestamp, - message=MESSAGE, - content_binding=content_binding, - ) - - collection = manager.get_collection(collection_name, service_id) - - if not collection: - raise ValueError("No collection with name {}".format(collection_name)) - - content = manager.create_content(content, collections=[collection]) - - return content - - -def prepare_subscription_request( - collection, action, version, subscription_id=None, params=None -): - - data = dict( - action=action, - message_id=MESSAGE_ID, - subscription_id=subscription_id, - ) - - mod = as_tm(version) - - if version == 11: - cls = mod.ManageCollectionSubscriptionRequest - data.update( - dict( - collection_name=collection, - subscription_parameters=( - mod.SubscriptionParameters(**params) if params else None - ), - ) - ) - else: - cls = mod.ManageFeedSubscriptionRequest - data["feed_name"] = collection - - return cls(**data) - - -def includes(superset, subset): - return all(item in superset.items() for item in subset.items()) - - -def is_headers_valid(headers, version, https): - if version == 10: - if https: - return includes(headers, TAXII_10_HTTPS_HEADERS) - else: - return includes(headers, TAXII_10_HTTP_HEADERS) - elif version == 11: - if https: - return includes(headers, TAXII_11_HTTPS_HEADERS) - else: - return includes(headers, TAXII_11_HTTP_HEADERS) - else: - raise ValueError("Unknown TAXII message version: %s" % version) - - -class conditional: - """ - Wrap another context manager and enter it only if condition is true. - """ - - def __init__(self, condition, contextmanager): - self.condition = condition - self.contextmanager = contextmanager - - def __enter__(self): - if self.condition: - return self.contextmanager.__enter__() - - def __exit__(self, *args): - if self.condition: - return self.contextmanager.__exit__(*args) - - -class conditional_raises(conditional): - """ - Assert if wrapped code raises, but only when given an exception class - """ - - def __init__(self, condition): - if condition: - contextmanager = pytest.raises(condition) - else: - contextmanager = None - super().__init__(condition, contextmanager) - - -def assert_str_equal_no_formatting(str1, str2): - if "JWT_TOKEN" in str2: - jwt_token = JWT_RE.findall(str1)[0] - str2 = str2.replace("JWT_TOKEN", jwt_token) - assert "".join([part.strip() for part in str1.split()]) == "".join( - [part.strip() for part in str2.split()] - ) - - -class SKIP: - """Used as signalling value to skip check""" - - pass +import re + +import pytest +from libtaxii import messages_10 as tm10 +from libtaxii import messages_11 as tm11 + +from opentaxii.taxii import entities +from opentaxii.taxii.http import ( + HTTP_ACCEPT, + HTTP_CONTENT_XML, + TAXII_10_HTTP_HEADERS, + TAXII_10_HTTPS_HEADERS, + TAXII_11_HTTP_HEADERS, + TAXII_11_HTTPS_HEADERS, +) +from opentaxii.taxii.utils import get_utc_now + +from .fixtures import CB_STIX_XML_111, CONTENT, MESSAGE, MESSAGE_ID + +JWT_RE = re.compile(r'[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*') + + +def as_tm(version): + if version == 10: + return tm10 + elif version == 11: + return tm11 + else: + raise ValueError("Unknown TAXII message version: %s" % version) + + +def prepare_headers(version, https): + headers = dict() + if version == 10: + if https: + headers.update(TAXII_10_HTTPS_HEADERS) + else: + headers.update(TAXII_10_HTTP_HEADERS) + elif version == 11: + if https: + headers.update(TAXII_11_HTTPS_HEADERS) + else: + headers.update(TAXII_11_HTTP_HEADERS) + else: + raise ValueError("Unknown TAXII message version: %s" % version) + + headers[HTTP_ACCEPT] = HTTP_CONTENT_XML + return headers + + +def persist_content( + manager, + collection_name, + service_id, + timestamp=None, + binding=CB_STIX_XML_111, + subtypes=[], +): + + timestamp = timestamp or get_utc_now() + + content_binding = entities.ContentBindingEntity(binding=binding, subtypes=subtypes) + + content = entities.ContentBlockEntity( + content=CONTENT, + timestamp_label=timestamp, + message=MESSAGE, + content_binding=content_binding, + ) + + collection = manager.get_collection(collection_name, service_id) + + if not collection: + raise ValueError("No collection with name {}".format(collection_name)) + + content = manager.create_content(content, collections=[collection]) + + return content + + +def prepare_subscription_request( + collection, action, version, subscription_id=None, params=None +): + + data = dict( + action=action, + message_id=MESSAGE_ID, + subscription_id=subscription_id, + ) + + mod = as_tm(version) + + if version == 11: + cls = mod.ManageCollectionSubscriptionRequest + data.update( + dict( + collection_name=collection, + subscription_parameters=( + mod.SubscriptionParameters(**params) if params else None + ), + ) + ) + else: + cls = mod.ManageFeedSubscriptionRequest + data["feed_name"] = collection + + return cls(**data) + + +def includes(superset, subset): + return all(item in superset.items() for item in subset.items()) + + +def is_headers_valid(headers, version, https): + if version == 10: + if https: + return includes(headers, TAXII_10_HTTPS_HEADERS) + else: + return includes(headers, TAXII_10_HTTP_HEADERS) + elif version == 11: + if https: + return includes(headers, TAXII_11_HTTPS_HEADERS) + else: + return includes(headers, TAXII_11_HTTP_HEADERS) + else: + raise ValueError("Unknown TAXII message version: %s" % version) + + +class conditional: + """ + Wrap another context manager and enter it only if condition is true. + """ + + def __init__(self, condition, contextmanager): + self.condition = condition + self.contextmanager = contextmanager + + def __enter__(self): + if self.condition: + return self.contextmanager.__enter__() + + def __exit__(self, *args): + if self.condition: + return self.contextmanager.__exit__(*args) + + +class conditional_raises(conditional): + """ + Assert if wrapped code raises, but only when given an exception class + """ + + def __init__(self, condition): + if condition: + contextmanager = pytest.raises(condition) + else: + contextmanager = None + super().__init__(condition, contextmanager) + + +def assert_str_equal_no_formatting(str1, str2): + if "JWT_TOKEN" in str2: + jwt_token = JWT_RE.findall(str1)[0] + str2 = str2.replace("JWT_TOKEN", jwt_token) + assert "".join([part.strip() for part in str1.split()]) == "".join( + [part.strip() for part in str2.split()] + ) + + +class SKIP: + """Used as signalling value to skip check""" + + pass diff --git a/tox.ini b/tox.ini index 49c718da..b829bc2b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,33 +1,33 @@ -[tox] -skipsdist = True -envlist = py{310,311,py3}-sqlalchemy14-werkzeug{lt21,gte21}-{sqlite,mysql,mariadb,postgres} - -[gh-actions] -python = - 3.10: py310 - 3.11: py311 - pypy-3.10: pypy3 - -[testenv] -commands = - py.test --cov {envsitepackagesdir}/opentaxii {posargs} -deps = - !pypy3: -rrequirements-dev.txt - # does not support recent mypy - # https://github.com/python/mypy/issues/20329 - pypy3: -rrequirements-test.txt - pypy3: -rrequirements.txt - mysql,mariadb: -rrequirements-mysql.txt - postgres-!pypy3: -rrequirements-postgres.txt - postgres-pypy3: -rrequirements-postgres-pypy.txt - sqlalchemy14: sqlalchemy>=1.4,<1.5 - werkzeuglt21: werkzeug<2.1 - werkzeuggte21: werkzeug>=2.1 -setenv = - LC_ALL=en_US.UTF-8 - sqlite: DBTYPE=sqlite - mysql: DBTYPE=mysql - mariadb: DBTYPE=mariadb - postgres: DBTYPE=postgres - pypy3: PYTEST_ADDOPTS="--no-cov" -usedevelop = True +[tox] +skipsdist = True +envlist = py{310,311,py3}-sqlalchemy14-werkzeug{lt21,gte21}-{sqlite,mysql,mariadb,postgres} + +[gh-actions] +python = + 3.10: py310 + 3.11: py311 + pypy-3.10: pypy3 + +[testenv] +commands = + py.test --cov {envsitepackagesdir}/opentaxii {posargs} +deps = + !pypy3: -rrequirements-dev.txt + # does not support recent mypy + # https://github.com/python/mypy/issues/20329 + pypy3: -rrequirements-test.txt + pypy3: -rrequirements.txt + mysql,mariadb: -rrequirements-mysql.txt + postgres-!pypy3: -rrequirements-postgres.txt + postgres-pypy3: -rrequirements-postgres-pypy.txt + sqlalchemy14: sqlalchemy>=1.4,<1.5 + werkzeuglt21: werkzeug<2.1 + werkzeuggte21: werkzeug>=2.1 +setenv = + LC_ALL=en_US.UTF-8 + sqlite: DBTYPE=sqlite + mysql: DBTYPE=mysql + mariadb: DBTYPE=mariadb + postgres: DBTYPE=postgres + pypy3: PYTEST_ADDOPTS="--no-cov" +usedevelop = True