├── .circleci └── config.yml ├── .drone.yml ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── auto-set-labels.yml │ └── codeql-analysis.yml ├── .gitignore ├── .grenrc.js ├── .isort.cfg ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .yamllint.yaml ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── client_usage │ └── getting_started.rst ├── conf.py ├── index.rst ├── make.bat ├── pycti │ ├── pycti.api.opencti_api_client.rst │ ├── pycti.api.opencti_api_connector.rst │ ├── pycti.api.opencti_api_job.rst │ ├── pycti.api.rst │ ├── pycti.connector.opencti_connector.rst │ ├── pycti.connector.opencti_connector_helper.rst │ ├── pycti.connector.rst │ ├── pycti.entities.opencti_attack_pattern.rst │ ├── pycti.entities.opencti_campaign.rst │ ├── pycti.entities.opencti_capability.rst │ ├── pycti.entities.opencti_course_of_action.rst │ ├── pycti.entities.opencti_external_reference.rst │ ├── pycti.entities.opencti_group.rst │ ├── pycti.entities.opencti_identity.rst │ ├── pycti.entities.opencti_incident.rst │ ├── pycti.entities.opencti_indicator.rst │ ├── pycti.entities.opencti_intrusion_set.rst │ ├── pycti.entities.opencti_kill_chain_phase.rst │ ├── pycti.entities.opencti_malware.rst │ ├── pycti.entities.opencti_marking_definition.rst │ ├── pycti.entities.opencti_note.rst │ ├── pycti.entities.opencti_opinion.rst │ ├── pycti.entities.opencti_report.rst │ ├── pycti.entities.opencti_role.rst │ ├── pycti.entities.opencti_settings.rst │ ├── pycti.entities.opencti_stix_domain_entity.rst │ ├── pycti.entities.opencti_stix_entity.rst │ ├── pycti.entities.opencti_stix_observable.rst │ ├── pycti.entities.opencti_stix_observable_relation.rst │ ├── pycti.entities.opencti_stix_relation.rst │ ├── pycti.entities.opencti_stix_sighting.rst │ ├── pycti.entities.opencti_tag.rst │ ├── pycti.entities.opencti_threat_actor.rst │ ├── pycti.entities.opencti_tool.rst │ ├── pycti.entities.opencti_user.rst │ ├── pycti.entities.opencti_vulnerability.rst │ ├── pycti.entities.rst │ ├── pycti.rst │ ├── pycti.utils.constants.rst │ ├── pycti.utils.opencti_stix2.rst │ └── pycti.utils.rst └── requirements.txt ├── examples ├── add_external_reference_to_report.py ├── add_label_to_malware.py ├── add_label_to_observable.py ├── add_organization_to_sector.py ├── add_tool_usage_to_intrusion-set.py ├── ask_enrichment_of_observable.py ├── cmd_line_tag_latest_indicators_of_threat.py ├── create_campaign_attributed-to_intrusion_set.py ├── create_file_with_hashes.py ├── create_incident_with_ttps_and_indicators.py ├── create_indicator_of_campaign.py ├── create_intrusion_set.py ├── create_ip_domain_resolution.py ├── create_marking_definition.py ├── create_observable_relationships.py ├── create_process_observable.py ├── create_report_with_author.py ├── delete_intrusion_set.py ├── delete_relation.py ├── export_async_of_indicators.py ├── export_async_of_malware.py ├── export_incident_stix2.py ├── export_incidents_stix2.py ├── export_intrusion_set_stix2.py ├── export_report_stix2.py ├── get_all_indicators_using_pagination.py ├── get_all_reports_using_pagination.py ├── get_attack_pattern_by_mitre_id.py ├── get_entity_by_name_or_alias.py ├── get_malwares_of_intrusion_set.py ├── get_marking_definitions.py ├── get_observable_exact_match.py ├── get_observables_search.py ├── get_reports_about_intrusion_set.py ├── import_stix2_file.py ├── promote_observable_to_indicator.py ├── run_all.sh ├── search_attack_pattern.py ├── search_malware.py ├── update_entity_attribute.py ├── update_observable_attributes.py ├── upload_artifacts.py ├── upload_file.py ├── upload_file_example.pdf └── upload_file_to_intrusion_set.py ├── pycti ├── __init__.py ├── api │ ├── __init__.py │ ├── opencti_api_client.py │ ├── opencti_api_connector.py │ ├── opencti_api_playbook.py │ └── opencti_api_work.py ├── connector │ ├── __init__.py │ ├── opencti_connector.py │ ├── opencti_connector_helper.py │ └── opencti_metric_handler.py ├── entities │ ├── __init__.py │ ├── indicator │ │ ├── __init__.py │ │ └── opencti_indicator_properties.py │ ├── opencti_attack_pattern.py │ ├── opencti_campaign.py │ ├── opencti_capability.py │ ├── opencti_case_incident.py │ ├── opencti_case_rfi.py │ ├── opencti_case_rft.py │ ├── opencti_channel.py │ ├── opencti_course_of_action.py │ ├── opencti_data_component.py │ ├── opencti_data_source.py │ ├── opencti_event.py │ ├── opencti_external_reference.py │ ├── opencti_feedback.py │ ├── opencti_group.py │ ├── opencti_grouping.py │ ├── opencti_identity.py │ ├── opencti_incident.py │ ├── opencti_indicator.py │ ├── opencti_infrastructure.py │ ├── opencti_intrusion_set.py │ ├── opencti_kill_chain_phase.py │ ├── opencti_label.py │ ├── opencti_language.py │ ├── opencti_location.py │ ├── opencti_malware.py │ ├── opencti_malware_analysis.py │ ├── opencti_marking_definition.py │ ├── opencti_narrative.py │ ├── opencti_note.py │ ├── opencti_observed_data.py │ ├── opencti_opinion.py │ ├── opencti_report.py │ ├── opencti_role.py │ ├── opencti_settings.py │ ├── opencti_stix.py │ ├── opencti_stix_core_object.py │ ├── opencti_stix_core_relationship.py │ ├── opencti_stix_cyber_observable.py │ ├── opencti_stix_domain_object.py │ ├── opencti_stix_nested_ref_relationship.py │ ├── opencti_stix_object_or_stix_relationship.py │ ├── opencti_stix_sighting_relationship.py │ ├── opencti_task.py │ ├── opencti_threat_actor.py │ ├── opencti_threat_actor_group.py │ ├── opencti_threat_actor_individual.py │ ├── opencti_tool.py │ ├── opencti_user.py │ ├── opencti_vocabulary.py │ ├── opencti_vulnerability.py │ └── stix_cyber_observable │ │ ├── __init__.py │ │ ├── opencti_stix_cyber_observable_deprecated.py │ │ └── opencti_stix_cyber_observable_properties.py └── utils │ ├── __init__.py │ ├── constants.py │ ├── opencti_logger.py │ ├── opencti_stix2.py │ ├── opencti_stix2_identifier.py │ ├── opencti_stix2_splitter.py │ ├── opencti_stix2_update.py │ └── opencti_stix2_utils.py ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── scripts └── clone-opencti.sh ├── setup.cfg ├── test-requirements.txt ├── test.json └── tests ├── 01-unit ├── metric │ ├── __init__.py │ └── test_opencti_metric_handler.py ├── stix │ ├── __init__.py │ └── test_bundle_ids_rewrite.py └── utils │ ├── __init__.py │ ├── test_constants.py │ ├── test_opencti_stix2.py │ └── test_opencti_stix2_splitter.py ├── 02-integration ├── __init__.py ├── connector │ ├── __init__.py │ └── test_connector.py ├── entities │ ├── __init__.py │ ├── test_entity_crud.py │ ├── test_entity_filter.py │ ├── test_group.py │ ├── test_indicators.py │ ├── test_malware.py │ ├── test_observables.py │ ├── test_role.py │ ├── test_settings.py │ └── test_user.py └── utils │ └── test_stix_crud.py ├── __init__.py ├── cases ├── __init__.py ├── connectors.py └── entities.py ├── conftest.py ├── data ├── DATA-TEST-STIX2_v2.json ├── basicMalwareWithSample.json ├── bundle_ids_sample.json ├── bundle_with_internal_ids.json ├── cyclic-bundle.json ├── enterprise-attack.json ├── external_import_config.yml ├── indicator_stix.json ├── internal_enrichment_config.yml ├── internal_import_config.yml ├── internal_import_data.txt ├── missing_refs.json ├── mitre_att_capec.json ├── mono-bundle-entity.json ├── mono-bundle-relationship.json └── sectors.json └── utils.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | slack: circleci/slack@4.15.0 5 | jobs: 6 | ensure_formatting: 7 | docker: 8 | - image: cimg/python:3.13 9 | working_directory: ~/repo 10 | steps: 11 | - checkout 12 | - run: 13 | name: install dependencies 14 | command: pip3 install -r requirements.txt --user 15 | - run: 16 | name: install test-dependencies 17 | command: pip3 install -r test-requirements.txt --user 18 | - run: 19 | name: confirm black version 20 | command: black --version 21 | - run: 22 | name: run isort check 23 | command: isort --check-only . 24 | - run: 25 | name: run black check 26 | command: black --check . 27 | - slack/notify: 28 | branch_pattern: master 29 | event: fail 30 | template: basic_fail_1 31 | linter: 32 | docker: 33 | - image: alpine/flake8 34 | working_directory: ~/repo 35 | steps: 36 | - checkout 37 | - run: 38 | name: flake8 39 | command: flake8 --ignore=E,W ~/repo 40 | - slack/notify: 41 | branch_pattern: master 42 | event: fail 43 | template: basic_fail_1 44 | build: 45 | working_directory: ~/opencti-client 46 | docker: 47 | - image: cimg/python:3.13 48 | steps: 49 | - checkout 50 | - run: 51 | name: install dependencies 52 | command: > 53 | pip3 install --user -r requirements.txt -r test-requirements.txt 54 | - run: 55 | name: check version 56 | command: | 57 | package_version="$( 58 | python3 -c 'import pycti; print(pycti.__version__)' 59 | )" 60 | [ "${CIRCLE_TAG}" = "${package_version}" ] \ 61 | || printf "Version mismatch: %s is not equal to %s\n" \ 62 | "${CIRCLE_TAG}" "${package_version}" 63 | - run: 64 | name: build 65 | command: > 66 | SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" python3 -m build 67 | - slack/notify: 68 | branch_pattern: master 69 | event: fail 70 | template: basic_fail_1 71 | - persist_to_workspace: 72 | root: ~/opencti-client 73 | paths: 74 | - dist 75 | deploy: 76 | working_directory: ~/opencti-client 77 | docker: 78 | - image: cimg/python:3.13 79 | steps: 80 | - checkout 81 | - attach_workspace: 82 | at: ~/opencti-client 83 | - run: 84 | name: install dependencies 85 | command: pip3 install -r requirements.txt --user 86 | - run: 87 | name: install twine 88 | command: pip3 install twine 89 | - run: 90 | name: init .pypirc 91 | command: > 92 | printf "[pypi]\nusername = __token__\npassword = %s\n" 93 | "${PYPI_PASSWORD}" >> ~/.pypirc 94 | - run: 95 | name: upload to pypi 96 | command: twine upload dist/* 97 | - slack/notify: 98 | branch_pattern: master 99 | event: fail 100 | template: basic_fail_1 101 | notify_rolling: 102 | docker: 103 | - image: "cimg/base:stable" 104 | steps: 105 | - slack/notify: 106 | event: pass 107 | template: basic_success_1 108 | notify: 109 | docker: 110 | - image: "cimg/base:stable" 111 | steps: 112 | - slack/notify: 113 | event: pass 114 | template: basic_success_1 115 | 116 | workflows: 117 | version: 2 118 | opencti_client_python: 119 | jobs: 120 | - ensure_formatting: 121 | filters: 122 | tags: 123 | only: /.*/ 124 | - linter: 125 | filters: 126 | tags: 127 | only: /.*/ 128 | - build: 129 | filters: 130 | tags: 131 | only: /[0-9]+(\.[0-9]+)+(\.[0-9]+)?\.?(\w)*/ 132 | branches: 133 | ignore: /.*/ 134 | requires: 135 | - ensure_formatting 136 | - linter 137 | - deploy: 138 | requires: 139 | - build 140 | filters: 141 | tags: 142 | only: /[0-9]+(\.[0-9]+)+(\.[0-9]+)?\.?(\w)*/ 143 | branches: 144 | ignore: /.*/ 145 | - notify_rolling: 146 | requires: 147 | - build 148 | - notify: 149 | requires: 150 | - deploy 151 | filters: 152 | tags: 153 | only: /[0-9]+(\.[0-9]+)+(\.[0-9]+)?\.?(\w)*/ 154 | branches: 155 | ignore: /.*/ 156 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: client-python-tests 4 | 5 | steps: 6 | - name: sleep-for-opencti 7 | image: python:3.11 8 | commands: 9 | - sleep 180 10 | - name: client-test-39 11 | image: python:3.9 12 | commands: 13 | - pip3 install -r requirements.txt --user 14 | - pip3 install -r test-requirements.txt --user 15 | - python3 -m pytest --no-header -vv --disable-warnings --cov=pycti --drone 16 | - name: client-test-310 17 | image: python:3.10 18 | commands: 19 | - pip3 install -r requirements.txt --user 20 | - pip3 install -r test-requirements.txt --user 21 | - python3 -m pytest --no-header -vv --disable-warnings --cov=pycti --drone 22 | - name: client-test-311 23 | image: python:3.11 24 | commands: 25 | - pip3 install -r requirements.txt --user 26 | - pip3 install -r test-requirements.txt --user 27 | - python3 -m pytest --no-header -vv --disable-warnings --cov=pycti --drone 28 | - name: client-test-312 29 | image: python:3.12 30 | commands: 31 | - pip3 install -r requirements.txt --user 32 | - pip3 install -r test-requirements.txt --user 33 | - python3 -m pytest --no-header -vv --disable-warnings --cov=pycti --drone 34 | 35 | # always run the examples last since they don't clean up 36 | - name: example-tests 37 | image: python:3.12 38 | commands: 39 | - pip3 install -r requirements.txt --user 40 | - pip3 install . 41 | - cd examples/ 42 | - /bin/bash run_all.sh 43 | 44 | - name: slack 45 | image: plugins/slack 46 | settings: 47 | webhook: 48 | from_secret: slack_webhook 49 | username: drone 50 | icon_url: https://avatars.githubusercontent.com/oa/1284929 51 | channel: notifications 52 | when: 53 | status: [ success, failure ] 54 | event: 55 | exclude: 56 | - pull_request 57 | 58 | services: 59 | - name: redis 60 | image: redis:7.4.3 61 | - name: elastic 62 | image: docker.elastic.co/elasticsearch/elasticsearch:8.18.1 63 | environment: 64 | discovery.type: single-node 65 | xpack.security.enabled: false 66 | ES_JAVA_OPTS: -Xms2g -Xmx2g 67 | - name: minio 68 | image: minio/minio:RELEASE.2022-11-11T03-44-20Z 69 | environment: 70 | MINIO_ROOT_USER: ChangeMe 71 | MINIO_ROOT_PASSWORD: ChangeMe 72 | command: [ server, /data ] 73 | - name: rabbitmq 74 | image: rabbitmq:4.1-management 75 | - name: opencti 76 | image: nikolaik/python-nodejs:python3.10-nodejs18-alpine 77 | environment: 78 | APP__ADMIN__PASSWORD: admin 79 | APP__ADMIN__TOKEN: bfa014e0-e02e-4aa6-a42b-603b19dcf159 80 | REDIS__HOSTNAME: redis 81 | REDIS__NAMESPACE: raw-start 82 | ELASTICSEARCH__URL: http://elastic:9200 83 | ELASTICSEARCH__INDEX_PREFIX: raw-start 84 | MINIO__ENDPOINT: minio 85 | MINIO__BUCKET_NAME: raw-start-bucket 86 | RABBITMQ__HOSTNAME: rabbitmq 87 | EXPIRATION_SCHEDULER__ENABLED: false 88 | SUBSCRIPTION_SCHEDULER__ENABLED: false 89 | GITHUB_TOKEN: 90 | from_secret: github_token 91 | commands: 92 | - apk add build-base git libffi-dev cargo github-cli 93 | - chmod 777 scripts/* 94 | - pip3 install . --upgrade --force 95 | - echo "DRONE_SOURCE_BRANCH:${DRONE_SOURCE_BRANCH}, DRONE_PULL_REQUEST:${DRONE_PULL_REQUEST}" 96 | - ./scripts/clone-opencti.sh "${DRONE_SOURCE_BRANCH}" "${DRONE_TARGET_BRANCH}" "$(pwd)" "${DRONE_PULL_REQUEST}" 97 | - ls -lart 98 | - cd opencti/opencti-platform/opencti-graphql 99 | - yarn install 100 | - sed -i '/^pycti==/d' src/python/requirements.txt 101 | - yarn install:python 102 | - NODE_OPTIONS=--max_old_space_size=8192 yarn start 103 | 104 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403, F401 3 | max-line-length = 89 4 | select = B,C,E,F,W,T4,B9 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve OpenCTI 4 | title: '' 5 | labels: bug, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please replace every line in curly brackets { like this } with an appropriate answer, and remove this line. 11 | 12 | ## Description 13 | 14 | { Please provide a clear and concise description of the bug. } 15 | 16 | ## Environment 17 | 18 | 1. OS (where OpenCTI server runs): { e.g. Mac OS 10, Windows 10, Ubuntu 16.4, etc. } 19 | 2. OpenCTI version: { e.g. OpenCTI 1.0.2 } 20 | 3. Other environment details: 21 | 22 | ## Reproducible Steps 23 | 24 | Steps to create the smallest reproducible scenario: 25 | 1. { e.g. Run ... } 26 | 2. { e.g. Click ... } 27 | 3. { e.g. Error ... } 28 | 29 | ## Expected Output 30 | 31 | { Please describe what you expected to happen. } 32 | 33 | ## Actual Output 34 | 35 | { Please describe what actually happened. } 36 | 37 | ## Additional information 38 | 39 | { Any additional information, including logs or screenshots if you have any. } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Ask for a new feature to be implemented in OpenCTI 4 | title: '' 5 | labels: feature, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please replace every line in curly brackets { like this } with appropriate answers, and remove this line. 11 | 12 | ## Problem to Solve 13 | 14 | { Please describe the problem you would like to solve. } 15 | 16 | ## Current Workaround 17 | 18 | { Please describe how you currently solve or work around this problem, given OpenCTI's limitation. } 19 | 20 | ## Proposed Solution 21 | 22 | { Please describe the solution you would like OpenCTI to provide, to solve the problem above. } 23 | 24 | ## Additional Information 25 | 26 | { Any additional information, including logs or screenshots if you have any. } 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### Proposed changes 9 | 10 | * 11 | * 12 | 13 | ### Related issues 14 | 15 | * 16 | * 17 | 18 | ### Checklist 19 | 20 | 25 | 26 | - [ ] I consider the submitted work as finished 27 | - [ ] I tested the code for its functionality 28 | - [ ] I wrote test cases for the relevant uses case 29 | - [ ] I added/update the relevant documentation (either on github or on notion) 30 | - [ ] Where necessary I refactored code to improve the overall quality 31 | 32 | 33 | 34 | 35 | ### Further comments 36 | 37 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 38 | -------------------------------------------------------------------------------- /.github/workflows/auto-set-labels.yml: -------------------------------------------------------------------------------- 1 | name: Assign label Filigran team on PR from an organization 2 | on: pull_request 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - name: Setting labels 9 | uses: FiligranHQ/auto-label@1.0.0 10 | with: 11 | labels_by_organization: "{\"FiligranHQ\":[\"filigran team\"]}" -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For most projects, this workflow file will not need changing; you simply need 3 | # to commit it to your repository. 4 | # 5 | # You may wish to alter this file to override the set of languages analyzed, 6 | # or to provide custom queries or build logic. 7 | # 8 | # ******** NOTE ******** 9 | # We have attempted to detect the languages in your repository. Please check 10 | # the `language` matrix defined below to confirm you have the correct set of 11 | # supported CodeQL languages. 12 | # ******** NOTE ******** 13 | 14 | name: "CodeQL" 15 | 16 | on: 17 | push: 18 | branches: 19 | - master 20 | pull_request: 21 | # The branches below must be a subset of the branches above 22 | branches: 23 | - master 24 | schedule: 25 | - cron: "21 11 * * 0" 26 | 27 | jobs: 28 | analyze: 29 | name: Analyze 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: 36 | - python 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | # Initializes the CodeQL tools for scanning. 43 | - name: Initialize CodeQL 44 | uses: github/codeql-action/init@v3 45 | with: 46 | languages: ${{ matrix.language }} 47 | # If you wish to specify custom queries, you can do so here or in a 48 | # config file. By default, queries listed here will override any 49 | # specified in a config file. Prefix the list here with "+" to use 50 | # these queries and those in the config file. queries: 51 | # ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or 54 | # Java). If this step fails, then you should remove it and run the build 55 | # manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 63 | # three lines and modify them (or add more) to build your code if your 64 | # project uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pycti 2 | config.yml 3 | exports 4 | logs 5 | test.py 6 | examples/*.json 7 | examples/*.pdf 8 | Pipfile* 9 | .DS_Store 10 | 11 | # IntelliJ Platform) | JetBrains 12 | .idea 13 | *.iml 14 | 15 | # VScode 16 | .vscode/* 17 | # Local History for Visual Studio Code 18 | .history/ 19 | 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | share/python-wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | *.py,cover 69 | .hypothesis/ 70 | .pytest_cache/ 71 | cover/ 72 | 73 | # Translations 74 | *.mo 75 | *.pot 76 | 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # pytype static type analyzer 132 | .pytype/ 133 | 134 | # Cython debug symbols 135 | cython_debug/ 136 | -------------------------------------------------------------------------------- /.grenrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "prefix": "Version ", 3 | "data-source": "milestones", 4 | "milestone-match": "Release {{tag_name}}", 5 | "ignoreIssuesWith": [ 6 | "duplicate", 7 | "wontfix", 8 | "invalid", 9 | "help wanted" 10 | ], 11 | "template": { 12 | "issue": "- [{{text}}]({{url}}) {{name}}" 13 | }, 14 | "groupBy": { 15 | "Enhancements:": ["feature", "internal", "build", "documentation"], 16 | "Bug Fixes:": ["bug"] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output = 3 3 | include_trailing_comma = True 4 | force_grid_wrap = 0 5 | use_parentheses = True 6 | ensure_newline_before_comments = True 7 | line_length = 88 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/PyCQA/isort 4 | rev: 5.12.0 5 | hooks: 6 | - id: isort 7 | args: 8 | - "--profile" 9 | - "black" 10 | - repo: https://github.com/psf/black 11 | rev: 22.3.0 12 | hooks: 13 | - id: black 14 | - repo: https://github.com/adrienverge/yamllint 15 | rev: v1.26.3 16 | hooks: 17 | - id: yamllint 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v4.1.0 20 | hooks: 21 | - id: check-ast 22 | - id: check-json 23 | - id: check-toml 24 | - id: check-xml 25 | - id: check-yaml 26 | - id: end-of-file-fixer 27 | - id: requirements-txt-fixer 28 | - id: trailing-whitespace 29 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # .readthedocs.yml 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | # Required 7 | version: 2 8 | 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Build documentation with MkDocs 19 | #mkdocs: 20 | # configuration: mkdocs.yml 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | formats: all 24 | 25 | # Optionally set the version of Python and requirements required to build your docs 26 | python: 27 | install: 28 | - requirements: requirements.txt 29 | - requirements: docs/requirements.txt 30 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | indentation: 5 | spaces: 2 6 | indent-sequences: consistent 7 | line-length: 8 | level: warning 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | SOURCE_DATE_EPOCH=$(shell git log -1 --pretty=%ct) python3 -m build 3 | 4 | clean: 5 | rm -rf *.egg-info/ build/ dist/ 6 | 7 | clean.all: clean 8 | find . -type d -name .mypy_cache -o -name __pycache__ -exec rm -rf "{}" \+ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenCTI client for Python 2 | 3 | [![Website](https://img.shields.io/badge/website-opencti.io-blue.svg)](https://opencti.io) 4 | [![CircleCI](https://circleci.com/gh/OpenCTI-Platform/client-python.svg?style=shield)](https://circleci.com/gh/OpenCTI-Platform/client-python/tree/master) 5 | [![readthedocs](https://readthedocs.org/projects/opencti-client-for-python/badge/?style=flat)](https://opencti-client-for-python.readthedocs.io/en/latest/) 6 | [![GitHub release](https://img.shields.io/github/release/OpenCTI-Platform/client-python.svg)](https://github.com/OpenCTI-Platform/client-python/releases/latest) 7 | [![Number of PyPI downloads](https://img.shields.io/pypi/dm/pycti.svg)](https://pypi.python.org/pypi/pycti/) 8 | [![Slack Status](https://img.shields.io/badge/slack-3K%2B%20members-4A154B)](https://community.filigran.io) 9 | 10 | The official OpenCTI Python client helps developers to use the OpenCTI API by providing easy to use methods and utils. 11 | This client is also used by some OpenCTI components. 12 | 13 | ## Install 14 | 15 | To install the latest Python client library, please use `pip`: 16 | 17 | ```bash 18 | $ pip3 install pycti 19 | ``` 20 | 21 | ## Local development 22 | 23 | ```bash 24 | # Fork the current repository, then clone your fork 25 | $ git clone https://github.com/YOUR-USERNAME/client-python 26 | $ cd client-python 27 | $ git remote add upstream https://github.com/OpenCTI-Platform/client-python.git 28 | # Create a branch for your feature/fix 29 | $ git checkout -b [branch-name] 30 | # Create a virtualenv 31 | $ python3 -m venv .venv 32 | $ source .venv/bin/activate 33 | # Install the client-python and dependencies for the development and the documentation 34 | $ python3 -m pip install -e .[dev,doc] 35 | # Set up the git hook scripts 36 | $ pre-commit install 37 | # Create your feature/fix 38 | # Create tests for your changes 39 | $ pytest 40 | # Push you feature/fix on Github 41 | $ git add [file(s)] 42 | $ git commit -m "[descriptive message]" 43 | $ git push origin [branch-name] 44 | # Open a pull request 45 | ``` 46 | 47 | ### Install the package locally 48 | 49 | ```bash 50 | $ pip install -e . 51 | ``` 52 | 53 | ## Documentation 54 | 55 | ### Client usage 56 | 57 | To learn about how to use the OpenCTI Python client and read some examples and cases, refer to [the client documentation](https://opencti-client-for-python.readthedocs.io/en/latest/client_usage/getting_started.html). 58 | 59 | ### API reference 60 | 61 | To learn about the methods available for executing queries and retrieving their answers, refer to [the client API Reference](https://opencti-client-for-python.readthedocs.io/en/latest/pycti/pycti.html). 62 | 63 | ## Tests 64 | 65 | ### Install dependencies 66 | 67 | ```bash 68 | $ pip install -r ./test-requirements.txt 69 | ``` 70 | 71 | [pytest](https://docs.pytest.org/en/7.2.x/) is used to launch the tests. 72 | 73 | ### Launch tests 74 | 75 | #### Prerequisite 76 | 77 | Your OpenCTI API should be running. 78 | Your conftest.py should be configured with your API url, your token, and if applicable, your mTLS cert/key. 79 | 80 | #### Launching 81 | 82 | Unit tests 83 | ```bash 84 | $ pytest ./tests/01-unit/ 85 | ``` 86 | 87 | Integration testing 88 | ```bash 89 | $ pytest ./tests/02-integration/ 90 | ``` 91 | 92 | ## About 93 | 94 | OpenCTI is a product designed and developed by the company [Filigran](https://filigran.io). 95 | 96 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/client_usage/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Installation 5 | ************ 6 | 7 | Please install the latest pycti version available from PyPI:: 8 | 9 | $ pip3 install pycti 10 | 11 | Using the helper functions 12 | ************************** 13 | 14 | The main class :class:`OpenCTIApiClient` contains all what you need to interact 15 | with the platform, you just have to initialize it. If your OpenCTI instance requires mTLS, 16 | you can specify either the file path of the SSL .pem file, or provide a key-value pair representing 17 | the file paths to your cert and private key in a tuple with the `cert` parameter when 18 | initializing the `OpenCTIApiClient`. If you need to require the verification of the TLS certificate at the server, 19 | you can provide a boolean value for the `ssl_verify` parameter. 20 | 21 | The following example shows how you create an indicator in OpenCTI using the python library 22 | with TLP marking and OpenCTI compatible date format. 23 | 24 | .. code-block:: python 25 | 26 | from dateutil.parser import parse 27 | from pycti import OpenCTIApiClient 28 | from stix2 import TLP_GREEN 29 | 30 | # OpenCTI API client initialization 31 | opencti_api_client = OpenCTIApiClient("https://myopencti.server", "mysupersecrettoken") 32 | 33 | # Define an OpenCTI compatible date 34 | date = parse("2019-12-01").strftime("%Y-%m-%dT%H:%M:%SZ") 35 | 36 | # Get the OpenCTI marking for stix2 TLP_GREEN 37 | TLP_GREEN_CTI = opencti_api_client.marking_definition.read(id=TLP_GREEN["id"]) 38 | 39 | # Use the client to create an indicator in OpenCTI 40 | indicator = opencti_api_client.indicator.create( 41 | name="C2 server of the new campaign", 42 | description="This is the C2 server of the campaign", 43 | pattern_type="stix", 44 | pattern="[domain-name:value = 'www.5z8.info']", 45 | x_opencti_main_observable_type="IPv4-Addr", 46 | valid_from=date, 47 | update=True, 48 | markingDefinitions=[TLP_GREEN_CTI["id"]], 49 | ) 50 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "OpenCTI client for Python" 22 | copyright = "2025, Filigran" 23 | author = "OpenCTI Project" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "6.0.7" 27 | 28 | master_doc = "index" 29 | 30 | autoapi_modules = {"pycti": {"prune": True}} 31 | 32 | pygments_style = "sphinx" 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | "sphinx.ext.autodoc", 41 | "sphinx.ext.inheritance_diagram", 42 | "autoapi.sphinx", 43 | "sphinx_autodoc_typehints", 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ["_templates"] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = "sphinx_rtd_theme" 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = ["_static"] 66 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | OpenCTI client for Python 2 | ========================= 3 | 4 | The pycti library is designed to help OpenCTI users and developers to interact 5 | with the OpenCTI platform GraphQL API. 6 | 7 | The Python library requires Python >= 3. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | client_usage/getting_started.rst 14 | pycti/pycti 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pycti/pycti.api.opencti_api_client.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | ``pycti.api.opencti_api_client`` 3 | ================================ 4 | 5 | .. automodule:: pycti.api.opencti_api_client 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.api.opencti_api_client 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.api.opencti_api_connector.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | ``pycti.api.opencti_api_connector`` 3 | =================================== 4 | 5 | .. automodule:: pycti.api.opencti_api_connector 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.api.opencti_api_connector 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.api.opencti_api_job.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | ``pycti.api.opencti_api_job`` 3 | ============================= 4 | 5 | .. automodule:: pycti.api.opencti_api_job 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.api.opencti_api_job 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.api.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | ``pycti.api`` 3 | ============= 4 | 5 | .. automodule:: pycti.api 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | Submodules 13 | ========== 14 | 15 | .. toctree:: 16 | 17 | pycti.api.opencti_api_client 18 | pycti.api.opencti_api_connector 19 | pycti.api.opencti_api_job 20 | 21 | .. currentmodule:: pycti.api 22 | -------------------------------------------------------------------------------- /docs/pycti/pycti.connector.opencti_connector.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | ``pycti.connector.opencti_connector`` 3 | ===================================== 4 | 5 | .. automodule:: pycti.connector.opencti_connector 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.connector.opencti_connector 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.connector.opencti_connector_helper.rst: -------------------------------------------------------------------------------- 1 | ============================================ 2 | ``pycti.connector.opencti_connector_helper`` 3 | ============================================ 4 | 5 | .. automodule:: pycti.connector.opencti_connector_helper 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.connector.opencti_connector_helper 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.connector.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | ``pycti.connector`` 3 | =================== 4 | 5 | .. automodule:: pycti.connector 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | Submodules 13 | ========== 14 | 15 | .. toctree:: 16 | 17 | pycti.connector.opencti_connector 18 | pycti.connector.opencti_connector_helper 19 | 20 | .. currentmodule:: pycti.connector 21 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_attack_pattern.rst: -------------------------------------------------------------------------------- 1 | ========================================= 2 | ``pycti.entities.opencti_attack_pattern`` 3 | ========================================= 4 | 5 | .. automodule:: pycti.entities.opencti_attack_pattern 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_attack_pattern 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_campaign.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | ``pycti.entities.opencti_campaign`` 3 | =================================== 4 | 5 | .. automodule:: pycti.entities.opencti_campaign 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_campaign 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_capability.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_capability`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_capability 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_capability 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_course_of_action.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | ``pycti.entities.opencti_course_of_action`` 3 | =========================================== 4 | 5 | .. automodule:: pycti.entities.opencti_course_of_action 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_course_of_action 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_external_reference.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``pycti.entities.opencti_external_reference`` 3 | ============================================= 4 | 5 | .. automodule:: pycti.entities.opencti_external_reference 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_external_reference 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_group.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_group`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_group 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_group 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_identity.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | ``pycti.entities.opencti_identity`` 3 | =================================== 4 | 5 | .. automodule:: pycti.entities.opencti_identity 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_identity 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_incident.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | ``pycti.entities.opencti_incident`` 3 | =================================== 4 | 5 | .. automodule:: pycti.entities.opencti_incident 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_incident 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_indicator.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_indicator`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_indicator 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_indicator 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_intrusion_set.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | ``pycti.entities.opencti_intrusion_set`` 3 | ======================================== 4 | 5 | .. automodule:: pycti.entities.opencti_intrusion_set 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_intrusion_set 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_kill_chain_phase.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | ``pycti.entities.opencti_kill_chain_phase`` 3 | =========================================== 4 | 5 | .. automodule:: pycti.entities.opencti_kill_chain_phase 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_kill_chain_phase 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_malware.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | ``pycti.entities.opencti_malware`` 3 | ================================== 4 | 5 | .. automodule:: pycti.entities.opencti_malware 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_malware 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_marking_definition.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``pycti.entities.opencti_marking_definition`` 3 | ============================================= 4 | 5 | .. automodule:: pycti.entities.opencti_marking_definition 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_marking_definition 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_note.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | ``pycti.entities.opencti_note`` 3 | =============================== 4 | 5 | .. automodule:: pycti.entities.opencti_note 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_note 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_opinion.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | ``pycti.entities.opencti_opinion`` 3 | ================================== 4 | 5 | .. automodule:: pycti.entities.opencti_opinion 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_opinion 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_report.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | ``pycti.entities.opencti_report`` 3 | ================================= 4 | 5 | .. automodule:: pycti.entities.opencti_report 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_report 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_role.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_role`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_role 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_role 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_settings.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_settings`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_settings 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_settings 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_domain_entity.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``pycti.entities.opencti_stix_domain_object`` 3 | ============================================= 4 | 5 | .. automodule:: pycti.entities.opencti_stix_domain_object 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_domain_object 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_entity.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | ``pycti.entities.opencti_stix_entity`` 3 | ====================================== 4 | 5 | .. automodule:: pycti.entities.opencti_stix_entity 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_entity 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_observable.rst: -------------------------------------------------------------------------------- 1 | ========================================== 2 | ``pycti.entities.opencti_stix_observable`` 3 | ========================================== 4 | 5 | .. automodule:: pycti.entities.opencti_stix_observable 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_observable 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_observable_relation.rst: -------------------------------------------------------------------------------- 1 | =================================================== 2 | ``pycti.entities.opencti_stix_observable_relationship`` 3 | =================================================== 4 | 5 | .. automodule:: pycti.entities.opencti_stix_observable_relationship 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_observable_relationship 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_relation.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | ``pycti.entities.opencti_stix_relation`` 3 | ======================================== 4 | 5 | .. automodule:: pycti.entities.opencti_stix_relation 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_relation 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_stix_sighting.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | ``pycti.entities.opencti_stix_sighting`` 3 | ======================================== 4 | 5 | .. automodule:: pycti.entities.opencti_stix_sighting 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_stix_sighting 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_tag.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | ``pycti.entities.opencti_tag`` 3 | ============================== 4 | 5 | .. automodule:: pycti.entities.opencti_tag 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_tag 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_threat_actor.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | ``pycti.entities.opencti_threat_actor`` 3 | ======================================= 4 | 5 | .. automodule:: pycti.entities.opencti_threat_actor 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_threat_actor 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_tool.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | ``pycti.entities.opencti_tool`` 3 | =============================== 4 | 5 | .. automodule:: pycti.entities.opencti_tool 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_tool 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_user.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | ``pycti.entities.opencti_user`` 3 | ==================================== 4 | 5 | .. automodule:: pycti.entities.opencti_user 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_user 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.opencti_vulnerability.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | ``pycti.entities.opencti_vulnerability`` 3 | ======================================== 4 | 5 | .. automodule:: pycti.entities.opencti_vulnerability 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.entities.opencti_vulnerability 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.entities.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | ``pycti.entities`` 3 | ================== 4 | 5 | .. automodule:: pycti.entities 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | Submodules 13 | ========== 14 | 15 | .. toctree:: 16 | 17 | pycti.entities.opencti_attack_pattern 18 | pycti.entities.opencti_campaign 19 | pycti.entities.opencti_course_of_action 20 | pycti.entities.opencti_external_reference 21 | pycti.entities.opencti_identity 22 | pycti.entities.opencti_incident 23 | pycti.entities.opencti_indicator 24 | pycti.entities.opencti_intrusion_set 25 | pycti.entities.opencti_kill_chain_phase 26 | pycti.entities.opencti_malware 27 | pycti.entities.opencti_marking_definition 28 | pycti.entities.opencti_note 29 | pycti.entities.opencti_opinion 30 | pycti.entities.opencti_report 31 | pycti.entities.opencti_stix_domain_object 32 | pycti.entities.opencti_stix_entity 33 | pycti.entities.opencti_stix_observable 34 | pycti.entities.opencti_stix_observable_relationship 35 | pycti.entities.opencti_stix_relation 36 | pycti.entities.opencti_stix_sighting 37 | pycti.entities.opencti_tag 38 | pycti.entities.opencti_threat_actor 39 | pycti.entities.opencti_tool 40 | pycti.entities.opencti_vulnerability 41 | pycti.entities.opencti_capability 42 | pycti.entities.opencti_role 43 | pycti.entities.opencti_group 44 | pycti.entities.opencti_user 45 | pycti.entities.opencti_settings 46 | 47 | .. currentmodule:: pycti.entities 48 | -------------------------------------------------------------------------------- /docs/pycti/pycti.utils.constants.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | ``pycti.utils.constants`` 3 | ========================= 4 | 5 | .. automodule:: pycti.utils.constants 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.utils.constants 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.utils.opencti_stix2.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | ``pycti.utils.opencti_stix2`` 3 | ============================= 4 | 5 | .. automodule:: pycti.utils.opencti_stix2 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: pycti.utils.opencti_stix2 12 | -------------------------------------------------------------------------------- /docs/pycti/pycti.utils.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | ``pycti.utils`` 3 | =============== 4 | 5 | .. automodule:: pycti.utils 6 | :members: 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | Submodules 13 | ========== 14 | 15 | .. toctree:: 16 | 17 | pycti.utils.constants 18 | pycti.utils.opencti_stix2 19 | 20 | .. currentmodule:: pycti.utils 21 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | autoapi==2.0.1 2 | sphinx==8.2.3 3 | sphinx-autodoc-typehints==3.2.0 4 | sphinx_rtd_theme==3.0.2 5 | -------------------------------------------------------------------------------- /examples/add_external_reference_to_report.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from dateutil.parser import parse 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Define the date 15 | date = parse("2019-12-01").strftime("%Y-%m-%dT%H:%M:%SZ") 16 | 17 | # Create the report 18 | report = opencti_api_client.report.create( 19 | name="My test report", 20 | description="A new threat report.", 21 | published=date, 22 | report_class="Threat Report", 23 | ) 24 | 25 | # Create the external reference 26 | external_reference = opencti_api_client.external_reference.create( 27 | source_name="Wikipedia", url="https://en.wikipedia.org/wiki/Fancy_Bear" 28 | ) 29 | 30 | # Add the external reference to the report 31 | opencti_api_client.stix_domain_object.add_external_reference( 32 | id=report["id"], external_reference_id=external_reference["id"] 33 | ) 34 | 35 | # Get the report 36 | report = opencti_api_client.report.read(id=report["id"]) 37 | 38 | # Print 39 | print(report) 40 | -------------------------------------------------------------------------------- /examples/add_label_to_malware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Create the malware 13 | malware = opencti_api_client.malware.create( 14 | name="My new malware", description="A new evil tool." 15 | ) 16 | 17 | # Create the tag (if not exists) 18 | label = opencti_api_client.label.create( 19 | value="Ransomware", 20 | color="#ffa500", 21 | ) 22 | 23 | # Add the tag 24 | opencti_api_client.stix_domain_object.add_label(id=malware["id"], label_id=label["id"]) 25 | 26 | # Print 27 | malware = opencti_api_client.malware.read(id=malware["id"]) 28 | print(malware) 29 | -------------------------------------------------------------------------------- /examples/add_label_to_observable.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Create the observable 13 | url = opencti_api_client.stix_cyber_observable.create( 14 | observableData={"type": "url", "value": "http://johndoe.com"} 15 | ) 16 | # Create the tag (if not exists) 17 | label = opencti_api_client.label.create( 18 | value="Suspicious", 19 | color="#ffa500", 20 | ) 21 | 22 | # Add the tag 23 | opencti_api_client.stix_cyber_observable.add_label(id=url["id"], label_id=label["id"]) 24 | 25 | # Read the observable 26 | obs = opencti_api_client.stix_cyber_observable.read(id=url["id"]) 27 | print(obs) 28 | -------------------------------------------------------------------------------- /examples/add_organization_to_sector.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get the sector 13 | sector = opencti_api_client.identity.create( 14 | type="Sector", name="Banking institutions", description="Banks" 15 | ) 16 | 17 | # Create the organization 18 | organization = opencti_api_client.identity.create( 19 | type="Organization", name="BNP Paribas", description="A french bank." 20 | ) 21 | 22 | # Create the relation 23 | relation = opencti_api_client.stix_core_relationship.create( 24 | fromType="Organization", 25 | fromId=organization["id"], 26 | toType="Sector", 27 | toId=sector["id"], 28 | relationship_type="part-of", 29 | description="BNP Paribas is part of the sector Banking institutions.", 30 | ) 31 | 32 | # Print 33 | print(relation) 34 | -------------------------------------------------------------------------------- /examples/add_tool_usage_to_intrusion-set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | tool = opencti_api_client.tool.create( 13 | name="powashell.exe", description="A new evil tool." 14 | ) 15 | 16 | print(tool) 17 | 18 | intrusion_set = opencti_api_client.intrusion_set.create(name="APT_EVIL") 19 | 20 | print(intrusion_set) 21 | 22 | # Create the relation 23 | relation = opencti_api_client.stix_core_relationship.create( 24 | fromType="IntrusionSet", 25 | fromId=intrusion_set["id"], 26 | toType="Tool", 27 | toId=tool["id"], 28 | relationship_type="uses", 29 | description="APT_EVIL uses the tool powashell.exe", 30 | ) 31 | 32 | print(relation) 33 | -------------------------------------------------------------------------------- /examples/ask_enrichment_of_observable.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from pycti import OpenCTIApiClient 3 | 4 | # Variables 5 | api_url = "http://opencti:4000" 6 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 7 | # Define name of INTERNAL_ENRICHMENT Connector which can enrich IPv4 addresses 8 | connector_name = "AbuseIPDB" 9 | 10 | # OpenCTI initialization 11 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 12 | 13 | # Create the observable 14 | observable = opencti_api_client.stix_cyber_observable.create( 15 | **{ 16 | "simple_observable_key": "IPv4-Addr.value", 17 | "simple_observable_value": "8.8.4.4", 18 | } 19 | ) 20 | 21 | # Get connector id for defined connector name 22 | connector_list = opencti_api_client.connector.list() 23 | connector_names = [] 24 | connector_id = "" 25 | for connector in connector_list: 26 | connector_names.append(connector["name"]) 27 | if connector["name"] == connector_name: 28 | connector_id = connector["id"] 29 | 30 | if connector_id == "": 31 | print(f"Connector with name '{connector_name}' could not be found") 32 | print(f"Running connectors: {connector_names}") 33 | exit(0) 34 | 35 | print("Asking for enrichment... (this might take a bit to finish)") 36 | # Ask for enrichment 37 | work_id = opencti_api_client.stix_cyber_observable.ask_for_enrichment( 38 | id=observable["id"], connector_id=connector_id 39 | ) 40 | # Wait for connector to finish 41 | opencti_api_client.work.wait_for_work_to_finish(work_id) 42 | 43 | # Read the observable 44 | obs = opencti_api_client.stix_cyber_observable.read(id=observable["id"]) 45 | print(obs) 46 | -------------------------------------------------------------------------------- /examples/cmd_line_tag_latest_indicators_of_threat.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import argparse 3 | 4 | from dateutil.parser import parse 5 | 6 | from pycti import OpenCTIApiClient 7 | 8 | # Variables 9 | api_url = "http://opencti:4000" 10 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 11 | 12 | # OpenCTI initialization 13 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 14 | 15 | 16 | def main(): 17 | # Parameters 18 | parser = argparse.ArgumentParser(description="Mandatory arguments.") 19 | parser.add_argument( 20 | "--entity-type", 21 | dest="entity_type", 22 | default="Intrusion-Set", 23 | required=True, 24 | help="Type of the threat (Threat-Actor, Intrusion-Set, Campaign, X-OpenCTI,-Incident, Malware, Tool, Attack-Pattern)", 25 | ) 26 | parser.add_argument( 27 | "--name", 28 | dest="name", 29 | required=True, 30 | help="Name of the threat", 31 | ) 32 | parser.add_argument( 33 | "--created-after", 34 | dest="createdAfter", 35 | help="Indicator created before (ISO date)", 36 | ) 37 | parser.add_argument( 38 | "--created-before", 39 | dest="createdBefore", 40 | help="Indicator created after (ISO date)", 41 | ) 42 | parser.add_argument( 43 | "--tags", 44 | dest="tags", 45 | required=True, 46 | help="Tags to add or remove (separated by ,)", 47 | ) 48 | parser.add_argument( 49 | "--operation", 50 | dest="operation", 51 | required=True, 52 | default="add", 53 | help="Operation (add/remove)", 54 | ) 55 | args = parser.parse_args() 56 | 57 | entity_type = args.entity_type 58 | name = args.name 59 | created_after = parse(args.createdAfter).strftime("%Y-%m-%dT%H:%M:%SZ") 60 | created_before = parse(args.createdBefore).strftime("%Y-%m-%dT%H:%M:%SZ") 61 | tags = args.tags.split(",") 62 | operation = args.operation 63 | 64 | # Resolve the entity 65 | threat = opencti_api_client.stix_domain_object.read( 66 | types=[entity_type], 67 | filters={ 68 | "mode": "and", 69 | "filters": [{"key": "name", "values": [name]}], 70 | "filterGroups": [], 71 | }, 72 | ) 73 | 74 | if not threat: 75 | raise ValueError("Cannot find the entity with the name " + name) 76 | 77 | # Resolve all tags 78 | labels = [] 79 | for tag in tags: 80 | labels.append(opencti_api_client.label.create(value=tag)) 81 | 82 | # Get indicators 83 | custom_attributes = """ 84 | id 85 | created_at 86 | """ 87 | 88 | data = {"pagination": {"hasNextPage": True, "endCursor": None}} 89 | while data["pagination"]["hasNextPage"]: 90 | after = data["pagination"]["endCursor"] 91 | data = opencti_api_client.indicator.list( 92 | first=50, 93 | after=after, 94 | customAttributes=custom_attributes, 95 | filters={ 96 | "mode": "and", 97 | "filters": [ 98 | {"key": "indicates", "values": [threat["id"]]}, 99 | {"key": "created_at", "values": [created_after], "operator": "gt"}, 100 | {"key": "created_at", "values": [created_before], "operator": "lt"}, 101 | ], 102 | "filterGroups": [], 103 | }, 104 | orderBy="created_at", 105 | orderMode="asc", 106 | withPagination=True, 107 | ) 108 | for indicator in data["entities"]: 109 | print("[" + indicator["created_at"] + "] " + indicator["id"]) 110 | if operation == "add": 111 | for label in labels: 112 | opencti_api_client.stix_domain_object.add_label( 113 | id=indicator["id"], label_id=label["id"] 114 | ) 115 | elif operation == "remove": 116 | for label in labels: 117 | opencti_api_client.stix_domain_object.remove_label( 118 | id=indicator["id"], label_id=label["id"] 119 | ) 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /examples/create_campaign_attributed-to_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from dateutil.parser import parse 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Define the date 15 | date = parse("2019-12-01").strftime("%Y-%m-%dT%H:%M:%SZ") 16 | 17 | # Create the Intrusion Set 18 | intrusion_set = opencti_api_client.intrusion_set.create( 19 | name="My new Intrusion Set", 20 | description="Evil Cluster", 21 | first_seen=date, 22 | last_seen=date, 23 | update=True, 24 | ) 25 | print(intrusion_set) 26 | 27 | # Create the Campaign 28 | campaign = opencti_api_client.campaign.create( 29 | name="My new Campaign", 30 | description="Large SpearPhishing and intrusions followed by ransomware", 31 | objective="Financial gain", 32 | first_seen=date, 33 | last_seen=date, 34 | update=True, 35 | ) 36 | print(campaign) 37 | 38 | # Attribute the Campaign to the Intrusion Set 39 | relation = opencti_api_client.stix_core_relationship.create( 40 | fromType="Campaign", 41 | fromId=campaign["id"], 42 | toType="Intrusion-Set", 43 | toId=intrusion_set["id"], 44 | relationship_type="attributed-to", 45 | first_seen=date, 46 | last_seen=date, 47 | description="My new campaign is attributed to my new Intrusion Set, the evil cluster.", 48 | ) 49 | 50 | # Print 51 | print(relation) 52 | -------------------------------------------------------------------------------- /examples/create_file_with_hashes.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Create observable 13 | observable = opencti_api_client.stix_cyber_observable.create( 14 | observableData={ 15 | "type": "file", 16 | "hashes": { 17 | "md5": "fcd76de79819813b631d949b18b1e996", 18 | "sha-1": "e08b42d92fa579c095834909b893d49259b158be", 19 | "sha-256": "7cca822e0fdfeca033762213bf16a3f04d7cac8c345f84a0d740324d97f671c0", 20 | }, 21 | } 22 | ) 23 | 24 | print(observable) 25 | -------------------------------------------------------------------------------- /examples/create_indicator_of_campaign.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from dateutil.parser import parse 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Define the date 15 | date = parse("2019-12-01").strftime("%Y-%m-%dT%H:%M:%SZ") 16 | 17 | # Create the Campaign 18 | campaign = opencti_api_client.campaign.create( 19 | name="My new Campaign", 20 | description="Large SpearPhishing and intrusions followed by ransomware", 21 | objective="Financial gain", 22 | first_seen=date, 23 | last_seen=date, 24 | update=True, 25 | ) 26 | print(campaign) 27 | 28 | # Create the indicator 29 | indicator = opencti_api_client.indicator.create( 30 | name="C2 server of the new campaign", 31 | description="This is the C2 server of the campaign", 32 | pattern_type="stix", 33 | pattern="[domain-name:value = 'www.5z8.info' AND domain-name:resolves_to_refs[*].value = '198.51.100.1/32']", 34 | x_opencti_main_observable_type="IPv4-Addr", 35 | valid_from=date, 36 | ) 37 | print(indicator) 38 | 39 | # Create the relation 40 | relation = opencti_api_client.stix_core_relationship.create( 41 | fromType="Indicator", 42 | fromId=indicator["id"], 43 | toType="Campaign", 44 | toId=campaign["id"], 45 | relationship_type="indicates", 46 | first_seen=date, 47 | last_seen=date, 48 | description="This is the C2 server of the campaign.", 49 | ) 50 | print(relation) 51 | 52 | # Create the observables (optional) 53 | observable_1 = opencti_api_client.stix_cyber_observable.create( 54 | simple_observable_key="Domain-Name.value", simple_observable_value="www.5z8.info" 55 | ) 56 | observable_2 = opencti_api_client.stix_cyber_observable.create( 57 | simple_observable_key="IPv4-Addr.value", simple_observable_value="198.51.100.1" 58 | ) 59 | # Create the relation between observables and the indicator 60 | opencti_api_client.indicator.add_stix_cyber_observable( 61 | id=indicator["id"], stix_cyber_observable_id=observable_1["id"] 62 | ) 63 | opencti_api_client.indicator.add_stix_cyber_observable( 64 | id=indicator["id"], stix_cyber_observable_id=observable_2["id"] 65 | ) 66 | -------------------------------------------------------------------------------- /examples/create_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Create the Intrusion Set 15 | intrusion_set = opencti_api_client.intrusion_set.create( 16 | name="My new Intrusion Set", 17 | description="Evil Cluster", 18 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 19 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 20 | update=True, 21 | ) 22 | 23 | # Print 24 | print(intrusion_set) 25 | -------------------------------------------------------------------------------- /examples/create_ip_domain_resolution.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | observable_domain = opencti_api_client.stix_cyber_observable.create( 13 | observableData={ 14 | "type": "domain-name", 15 | "value": "dns.google", 16 | } 17 | ) 18 | 19 | observable_ip = opencti_api_client.stix_cyber_observable.create( 20 | observableData={ 21 | "type": "ipv4-addr", 22 | "value": "8.8.8.8", 23 | } 24 | ) 25 | 26 | opencti_api_client.stix_nested_ref_relationship.create( 27 | fromId=observable_domain["id"], 28 | toId=observable_ip["id"], 29 | relationship_type="resolves-to", 30 | ) 31 | 32 | relationships = opencti_api_client.stix_nested_ref_relationship.list( 33 | elementId=observable_domain["id"] 34 | ) 35 | 36 | print(relationships) 37 | -------------------------------------------------------------------------------- /examples/create_marking_definition.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Create the marking definition 13 | marking_definition = opencti_api_client.marking_definition.create( 14 | definition_type="TLP", 15 | definition="TLP:BLACK", 16 | x_opencti_order=10, 17 | x_opencti_color="#000000", 18 | ) 19 | 20 | # Print 21 | print(marking_definition) 22 | -------------------------------------------------------------------------------- /examples/create_observable_relationships.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | observable = opencti_api_client.stix_cyber_observable.create( 13 | observableData={ 14 | "type": "file", 15 | "hashes": { 16 | "md5": "16b3f663d0f0371a4706642c6ac04e42", 17 | "sha-1": "3a1f908941311fc357051b5c35fd2a4e0c834e37", 18 | "sha-256": "bcc70a49fab005b4cdbe0cbd87863ec622c6b2c656987d201adbb0e05ec03e56", 19 | }, 20 | } 21 | ) 22 | 23 | process = opencti_api_client.stix_cyber_observable.create( 24 | observableData={ 25 | "type": "Process", 26 | "x_opencti_description": "A process", 27 | "cwd": "C:\Process.exe", 28 | "pid": 19000, 29 | "command_line": "--run exe", 30 | "x_opencti_score": 90, 31 | } 32 | ) 33 | 34 | author = opencti_api_client.identity.create( 35 | name="John's Work", 36 | description="Automated Toolkit", 37 | type="Organization", 38 | ) 39 | 40 | opencti_api_client.stix_core_relationship.create( 41 | toId=observable["id"], 42 | fromId=process["id"], 43 | confidence=90, 44 | createdBy=author["id"], 45 | relationship_type="related-to", 46 | description="Relation between the File and Process objects", 47 | ) 48 | -------------------------------------------------------------------------------- /examples/create_process_observable.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | process = opencti_api_client.stix_cyber_observable.create( 13 | observableData={ 14 | "type": "Process", 15 | "x_opencti_description": "A process", 16 | "cwd": "C:\Process.exe", 17 | "pid": 19000, 18 | "command_line": "--run exe", 19 | "x_opencti_score": 90, 20 | } 21 | ) 22 | 23 | print(process) 24 | -------------------------------------------------------------------------------- /examples/create_report_with_author.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from dateutil.parser import parse 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Define the date 15 | date = parse("2019-12-01").strftime("%Y-%m-%dT%H:%M:%SZ") 16 | 17 | # Create the author (if not exists) 18 | organization = opencti_api_client.identity.create( 19 | type="Organization", 20 | name="My organization", 21 | alias=["my-organization"], 22 | description="A new organization.", 23 | ) 24 | 25 | # Create the report 26 | report = opencti_api_client.report.create( 27 | name="My new report of my organization", 28 | description="A report wrote by my organization", 29 | published=date, 30 | report_types=["internal-report"], 31 | createdBy=organization["id"], 32 | ) 33 | 34 | # Print 35 | print(report) 36 | -------------------------------------------------------------------------------- /examples/delete_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | 4 | from pycti import OpenCTIApiClient 5 | 6 | # Variables 7 | api_url = "http://opencti:4000" 8 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 9 | 10 | # OpenCTI initialization 11 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 12 | 13 | opencti_api_client.intrusion_set.create( 14 | name="EvilSET123", 15 | description="Evil Cluster", 16 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 17 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 18 | update=True, 19 | ) 20 | 21 | # Get the intrusion set APT28 22 | intrusion_set = opencti_api_client.intrusion_set.read( 23 | filters={ 24 | "mode": "and", 25 | "filters": [{"key": "name", "values": ["EvilSET123"]}], 26 | "filterGroups": [], 27 | } 28 | ) 29 | 30 | # Delete the intrusion set 31 | opencti_api_client.stix_domain_object.delete(id=intrusion_set["id"]) 32 | -------------------------------------------------------------------------------- /examples/delete_relation.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | intrusion_set = opencti_api_client.intrusion_set.create(name="EvilSET123") 13 | 14 | malware = opencti_api_client.malware.create( 15 | name="TheWorm", description="A new evil worm." 16 | ) 17 | 18 | opencti_api_client.stix_core_relationship.create( 19 | fromId=intrusion_set["id"], 20 | fromTypes=["Intrusion-Set"], 21 | toId=malware["id"], 22 | toTypes=["Malware"], 23 | relationship_type="uses", 24 | ) 25 | 26 | # Get the relations between APT28 and DealersChoice 27 | relations = opencti_api_client.stix_core_relationship.list( 28 | fromId=intrusion_set["id"], 29 | fromTypes=["Intrusion-Set"], 30 | toId=malware["id"], 31 | toTypes=["Malware"], 32 | relationship_type="uses", 33 | ) 34 | 35 | # Delete the relations 36 | for relation in relations: 37 | opencti_api_client.stix_core_relationship.delete(id=relation["id"]) 38 | -------------------------------------------------------------------------------- /examples/export_async_of_indicators.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | -------------------------------------------------------------------------------- /examples/export_async_of_malware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | -------------------------------------------------------------------------------- /examples/export_incident_stix2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | opencti_api_client.incident.create(name="My new incident") 15 | 16 | # Get the incident created in the create_incident_with_ttps_and_indicators.py 17 | incident = opencti_api_client.incident.read( 18 | filters={ 19 | "mode": "and", 20 | "filters": [{"key": "name", "values": ["My new incident"]}], 21 | "filterGroups": [], 22 | } 23 | ) 24 | 25 | # Create the bundle 26 | bundle = opencti_api_client.stix2.get_stix_bundle_or_object_from_entity_id( 27 | entity_type="Incident", entity_id=incident["id"], mode="full" 28 | ) 29 | json_bundle = json.dumps(bundle, indent=4) 30 | 31 | # Write the bundle 32 | f = open("My new incident.json", "w") 33 | f.write(json_bundle) 34 | f.close() 35 | -------------------------------------------------------------------------------- /examples/export_incidents_stix2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Create the bundle 15 | bundle = opencti_api_client.stix2.export_list("Incident") 16 | json_bundle = json.dumps(bundle, indent=4) 17 | 18 | # Write the bundle 19 | f = open("Incidents.json", "w") 20 | f.write(json_bundle) 21 | f.close() 22 | -------------------------------------------------------------------------------- /examples/export_intrusion_set_stix2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Create the bundle 15 | bundle = opencti_api_client.stix2.get_stix_bundle_or_object_from_entity_id( 16 | entity_type="Intrusion-Set", 17 | entity_id="4ecc2f52-d10a-4e10-bb9b-1ab5df2b282e", 18 | mode="full", 19 | ) 20 | json_bundle = json.dumps(bundle, indent=4) 21 | 22 | # Write the bundle 23 | f = open("intrusion-set.json", "w") 24 | f.write(json_bundle) 25 | f.close() 26 | -------------------------------------------------------------------------------- /examples/export_report_stix2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Create the bundle 15 | bundle = opencti_api_client.stix2.get_stix_bundle_or_object_from_entity_id( 16 | entity_type="Report", 17 | entity_id="report--2dc2b918-a0a3-569e-a305-f784486003c2", 18 | mode="full", 19 | ) 20 | json_bundle = json.dumps(bundle, indent=4) 21 | 22 | # Write the bundle 23 | f = open("report.json", "w") 24 | f.write(json_bundle) 25 | f.close() 26 | -------------------------------------------------------------------------------- /examples/get_all_indicators_using_pagination.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get all reports using the pagination 13 | custom_attributes = """ 14 | id 15 | pattern_type 16 | created 17 | """ 18 | 19 | final_indicators = [] 20 | data = {"pagination": {"hasNextPage": True, "endCursor": None}} 21 | while data["pagination"]["hasNextPage"]: 22 | after = data["pagination"]["endCursor"] 23 | if after: 24 | print("Listing indicators after " + after) 25 | data = opencti_api_client.indicator.list( 26 | first=50, 27 | after=after, 28 | customAttributes=custom_attributes, 29 | withPagination=True, 30 | orderBy="created_at", 31 | orderMode="asc", 32 | ) 33 | final_indicators += data["entities"] 34 | 35 | for indicator in final_indicators: 36 | print("[" + indicator["created"] + "] " + indicator["id"]) 37 | -------------------------------------------------------------------------------- /examples/get_all_reports_using_pagination.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get all reports using the pagination 13 | custom_attributes = """ 14 | id 15 | name 16 | published 17 | description 18 | """ 19 | 20 | final_reports = [] 21 | data = {"pagination": {"hasNextPage": True, "endCursor": None}} 22 | while data["pagination"]["hasNextPage"]: 23 | after = data["pagination"]["endCursor"] 24 | if after: 25 | print("Listing reports after " + after) 26 | data = opencti_api_client.report.list( 27 | first=50, 28 | after=after, 29 | customAttributes=custom_attributes, 30 | withPagination=True, 31 | orderBy="created_at", 32 | orderMode="asc", 33 | ) 34 | final_reports = final_reports + data["entities"] 35 | 36 | # Print 37 | for report in final_reports: 38 | print("[" + report["published"] + "] " + report["name"]) 39 | -------------------------------------------------------------------------------- /examples/get_attack_pattern_by_mitre_id.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get the Attack-Pattern T1514 13 | attack_pattern = opencti_api_client.attack_pattern.read( 14 | filters={ 15 | "mode": "and", 16 | "filters": [{"key": "x_mitre_id", "values": ["T1514"]}], 17 | "filterGroups": [], 18 | } 19 | ) 20 | 21 | # Print 22 | print(attack_pattern) 23 | -------------------------------------------------------------------------------- /examples/get_entity_by_name_or_alias.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get the ANSSI entity 13 | anssi = opencti_api_client.stix_domain_object.get_by_stix_id_or_name(name="ANSSI") 14 | 15 | # Print 16 | print(anssi) 17 | -------------------------------------------------------------------------------- /examples/get_malwares_of_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | 4 | from pycti import OpenCTIApiClient 5 | 6 | # Variables 7 | api_url = "http://opencti:4000" 8 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 9 | 10 | # OpenCTI initialization 11 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 12 | 13 | # Create the Intrusion Set 14 | opencti_api_client.intrusion_set.create( 15 | name="APT28", 16 | description="Evil hackers", 17 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 18 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 19 | update=True, 20 | ) 21 | 22 | # Get the intrusion set APT28 23 | intrusion_set = opencti_api_client.intrusion_set.read( 24 | filters={ 25 | "mode": "and", 26 | "filters": [{"key": "name", "values": ["APT28"]}], 27 | "filterGroups": [], 28 | } 29 | ) 30 | 31 | # Get the relations from APT28 to malwares 32 | stix_relations = opencti_api_client.stix_core_relationship.list( 33 | fromId=intrusion_set["id"], toTypes=["Malware"] 34 | ) 35 | 36 | # Print 37 | for stix_relation in stix_relations: 38 | print("[" + stix_relation["to"]["stix_id"] + "] " + stix_relation["to"]["name"]) 39 | -------------------------------------------------------------------------------- /examples/get_marking_definitions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Get all marking definitions 13 | marking_definitions = opencti_api_client.marking_definition.list() 14 | 15 | # Print 16 | for marking_definition in marking_definitions: 17 | print( 18 | "[" 19 | + marking_definition["definition_type"] 20 | + "] " 21 | + marking_definition["definition"] 22 | ) 23 | -------------------------------------------------------------------------------- /examples/get_observable_exact_match.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Exact IP match 13 | opencti_api_client.stix_cyber_observable.create( 14 | simple_observable_key="IPv4-Addr.value", simple_observable_value="110.172.180.180" 15 | ) 16 | print("IP ADDRESS") 17 | observable = opencti_api_client.stix_cyber_observable.read( 18 | filters={ 19 | "mode": "and", 20 | "filters": [{"key": "value", "values": ["110.172.180.180"]}], 21 | "filterGroups": [], 22 | } 23 | ) 24 | print(observable) 25 | 26 | # Exact File name match 27 | opencti_api_client.stix_cyber_observable.create( 28 | simple_observable_key="File.name", simple_observable_value="activeds.dll" 29 | ) 30 | print("FILE NAME") 31 | observable = opencti_api_client.stix_cyber_observable.read( 32 | filters={ 33 | "mode": "and", 34 | "filters": [{"key": "name", "values": ["activeds.dll"]}], 35 | "filterGroups": [], 36 | } 37 | ) 38 | print(observable) 39 | 40 | # Exact File name match 41 | opencti_api_client.stix_cyber_observable.create( 42 | simple_observable_key="File.hashes.MD5", 43 | simple_observable_value="3aad33e025303dbae12c12b4ec5258c1", 44 | ) 45 | print("FILE MD5") 46 | observable = opencti_api_client.stix_cyber_observable.read( 47 | filters={ 48 | "mode": "and", 49 | "filters": [ 50 | {"key": "hashes.MD5", "values": ["3aad33e025303dbae12c12b4ec5258c1"]} 51 | ], 52 | "filterGroups": [], 53 | } 54 | ) 55 | print(observable) 56 | -------------------------------------------------------------------------------- /examples/get_observables_search.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Search 13 | opencti_api_client.stix_cyber_observable.create( 14 | simple_observable_key="IPv4-Addr.value", simple_observable_value="65.89.87.4" 15 | ) 16 | observables = opencti_api_client.stix_cyber_observable.list(search="65.89.87.4") 17 | 18 | print(observables) 19 | -------------------------------------------------------------------------------- /examples/get_reports_about_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | 4 | from pycti import OpenCTIApiClient 5 | 6 | # Variables 7 | api_url = "http://opencti:4000" 8 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 9 | 10 | # OpenCTI initialization 11 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 12 | 13 | 14 | # Create the Intrusion Set 15 | opencti_api_client.intrusion_set.create( 16 | name="Sandworm Team", 17 | description="Evil hackers", 18 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 19 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 20 | update=True, 21 | ) 22 | 23 | # Get the intrusion set Sandworm 24 | intrusion_set = opencti_api_client.intrusion_set.read( 25 | filters={ 26 | "mode": "and", 27 | "filters": [{"key": "name", "values": ["Sandworm Team"]}], 28 | "filterGroups": [], 29 | } 30 | ) 31 | 32 | # Get all reports 33 | reports = opencti_api_client.report.list( 34 | filters={ 35 | "mode": "and", 36 | "filters": [{"key": "objects", "values": [intrusion_set["id"]]}], 37 | "filterGroups": [], 38 | }, 39 | orderBy="published", 40 | orderMode="asc", 41 | ) 42 | 43 | # Print 44 | if not reports: 45 | print(f"No {intrusion_set['name']} reports available") 46 | else: 47 | for report in reports: 48 | print( 49 | "[" 50 | + report["standard_id"] 51 | + "] " 52 | + report["name"] 53 | + " (" 54 | + report["published"] 55 | + ")" 56 | ) 57 | -------------------------------------------------------------------------------- /examples/import_stix2_file.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # File to import 13 | file_to_import = "./test.json" 14 | 15 | # Import the bundle 16 | opencti_api_client.stix2.import_bundle_from_file(file_to_import, True) 17 | -------------------------------------------------------------------------------- /examples/promote_observable_to_indicator.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | obs = opencti_api_client.stix_cyber_observable.create( 13 | simple_observable_key="IPv4-Addr.value", simple_observable_value="55.55.55.99" 14 | ) 15 | indicator = opencti_api_client.stix_cyber_observable.promote_to_indicator_v2( 16 | id=obs.get("id") 17 | ) 18 | print("promoted observable, new indicator is", indicator) 19 | -------------------------------------------------------------------------------- /examples/run_all.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | for script in ./*.py; do 3 | if [[ $script == *"cmd_line_tag_latest_indicators_of_threat.py"* || $script == *"upload_artifacts.py"* ]]; then 4 | # TODO special execution for cmd tools 5 | continue 6 | fi 7 | python3 $script; 8 | done -------------------------------------------------------------------------------- /examples/search_attack_pattern.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Search 13 | attack_patterns = opencti_api_client.attack_pattern.list(search="localgroup") 14 | 15 | # Print 16 | print(attack_patterns) 17 | -------------------------------------------------------------------------------- /examples/search_malware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Search 13 | malwares = opencti_api_client.malware.list(search="windows") 14 | 15 | # Print 16 | print(malwares) 17 | -------------------------------------------------------------------------------- /examples/update_entity_attribute.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | 4 | from pycti import OpenCTIApiClient 5 | 6 | # Variables 7 | api_url = "http://opencti:4000" 8 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 9 | 10 | # OpenCTI initialization 11 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 12 | 13 | # Create the Intrusion Set 14 | opencti_api_client.intrusion_set.create( 15 | name="APT28", 16 | description="Evil hackers", 17 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 18 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 19 | update=True, 20 | ) 21 | 22 | # Get the intrusion set APT28 23 | intrusion_set = opencti_api_client.intrusion_set.read( 24 | filters={ 25 | "mode": "and", 26 | "filters": [{"key": "name", "values": ["APT28"]}], 27 | "filterGroups": [], 28 | } 29 | ) 30 | 31 | # Update the description 32 | opencti_api_client.stix_domain_object.update_field( 33 | id=intrusion_set["id"], input={"key": "description", "value": "This is APT28!"} 34 | ) 35 | -------------------------------------------------------------------------------- /examples/update_observable_attributes.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from pycti import OpenCTIApiClient 4 | 5 | # Variables 6 | api_url = "http://opencti:4000" 7 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 8 | 9 | # OpenCTI initialization 10 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 11 | 12 | # Create an observable 13 | observable = opencti_api_client.stix_cyber_observable.create( 14 | observableData={ 15 | "type": "file", 16 | "x_opencti_description": "A malicious file", 17 | "hashes": { 18 | "MD5": "348aefbb6142d4fff8cf26fc5dc97f8a", 19 | "SHA-1": "486e7e66c3a098c1c8f42e26c78f259d6b3108a6", 20 | "SHA-256": "42c5e1fe01e689e550ba700b3c5dd4a04a84798c1868ba53c02abcbe21491515", 21 | }, 22 | # "x_opencti_score": "90", 23 | } 24 | ) 25 | 26 | # Update the fields 27 | 28 | reference = opencti_api_client.external_reference.create( 29 | source_name="Jen", url="https://janedoe.com", description="Sample Report" 30 | ) 31 | 32 | opencti_api_client.stix_cyber_observable.add_external_reference( 33 | id=observable["id"], external_reference_id=reference["id"] 34 | ) 35 | 36 | label = opencti_api_client.label.create( 37 | value="Suspicious", 38 | color="#ffa500", 39 | ) 40 | 41 | opencti_api_client.stix_cyber_observable.add_label( 42 | id=observable["id"], marking_definition_id=label["id"] 43 | ) 44 | 45 | author = opencti_api_client.identity.create( 46 | name="John's Work", 47 | description="Automated Toolkit", 48 | type="Organization", 49 | ) 50 | 51 | opencti_api_client.stix_cyber_observable.update_created_by( 52 | id=observable["id"], identity_id=author["id"] 53 | ) 54 | -------------------------------------------------------------------------------- /examples/upload_artifacts.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import magic 5 | 6 | from pycti import OpenCTIApiClient 7 | 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI instantiation 12 | OPENCTI_API_CLIENT = OpenCTIApiClient(api_url, api_token) 13 | 14 | 15 | def main(): 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument( 18 | "-f", "--file", required=True, help="The path of the Artifact(s) to upload." 19 | ) 20 | parser.add_argument( 21 | "-d", "--description", default="", help="The description for the Artifact." 22 | ) 23 | parser.add_argument( 24 | "-l", "--label", default="", help="Comma separated labels for the Artifact." 25 | ) 26 | parser.add_argument( 27 | "-r", 28 | "--related", 29 | default=None, 30 | help="Standard id of an object related to the Artifact.", 31 | ) 32 | 33 | args = parser.parse_args() 34 | 35 | if os.path.isdir(args.file): 36 | for currentpath, folders, files in os.walk(args.file): 37 | for filep in files: 38 | upload( 39 | os.path.join(currentpath, filep), 40 | args.description, 41 | args.label, 42 | args.related, 43 | ) 44 | 45 | else: 46 | upload(args.file, args.description, args.label, args.related) 47 | 48 | 49 | def upload(file_path, description, labels, related_standard_id): 50 | file_data = b"" 51 | with open(file_path, "rb") as f: 52 | file_data = f.read() 53 | 54 | mime_type = magic.from_buffer(file_data, mime=True) 55 | 56 | # Upload the file, returns the query response for the file upload 57 | kwargs = { 58 | "file_name": os.path.basename(file_path), 59 | "data": file_data, 60 | "mime_type": mime_type, 61 | "x_opencti_description": "", 62 | } 63 | 64 | if description: 65 | kwargs["x_opencti_description"] = description 66 | 67 | response = OPENCTI_API_CLIENT.stix_cyber_observable.upload_artifact(**kwargs) 68 | print(response) 69 | 70 | for label_str in labels.split(","): 71 | if label_str: 72 | label = OPENCTI_API_CLIENT.label.create(value=label_str) 73 | OPENCTI_API_CLIENT.stix_cyber_observable.add_label( 74 | id=response["id"], label_id=label["id"] 75 | ) 76 | 77 | if related_standard_id: 78 | OPENCTI_API_CLIENT.stix_core_relationship.create( 79 | fromId=related_standard_id, 80 | toId=response["standard_id"], 81 | relationship_type="related-to", 82 | description=f"Related to {related_standard_id}", 83 | ) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /examples/upload_file.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from stix2 import TLP_GREEN 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Get a marking 15 | TLP_GREEN_CTI = opencti_api_client.marking_definition.read(id=TLP_GREEN["id"]) 16 | 17 | # Upload the file (note that markings are optional) 18 | file = opencti_api_client.upload_file( 19 | file_name="./upload_file_example.pdf", file_markings=[TLP_GREEN["id"]] 20 | ) 21 | print(file) 22 | -------------------------------------------------------------------------------- /examples/upload_file_example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/examples/upload_file_example.pdf -------------------------------------------------------------------------------- /examples/upload_file_to_intrusion_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | 5 | from pycti import OpenCTIApiClient 6 | 7 | # Variables 8 | api_url = "http://opencti:4000" 9 | api_token = "bfa014e0-e02e-4aa6-a42b-603b19dcf159" 10 | 11 | # OpenCTI initialization 12 | opencti_api_client = OpenCTIApiClient(api_url, api_token) 13 | 14 | # Create the Intrusion Set 15 | intrusion_set = opencti_api_client.intrusion_set.create( 16 | name="My new Intrusion Set", 17 | description="Evil Cluster", 18 | first_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 19 | last_seen=datetime.date.today().strftime("%Y-%m-%dT%H:%M:%S+00:00"), 20 | update=True, 21 | ) 22 | 23 | # Print 24 | print(intrusion_set) 25 | 26 | # Upload the file 27 | file = opencti_api_client.stix_domain_object.add_file( 28 | id=intrusion_set["id"], 29 | file_name="./upload_file_example.pdf", 30 | ) 31 | print(file) 32 | -------------------------------------------------------------------------------- /pycti/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = "6.6.14" 3 | 4 | from .api.opencti_api_client import OpenCTIApiClient 5 | from .api.opencti_api_connector import OpenCTIApiConnector 6 | from .api.opencti_api_work import OpenCTIApiWork 7 | from .connector.opencti_connector import ConnectorType, OpenCTIConnector 8 | from .connector.opencti_connector_helper import ( 9 | OpenCTIConnectorHelper, 10 | get_config_variable, 11 | ) 12 | from .connector.opencti_metric_handler import OpenCTIMetricHandler 13 | from .entities.opencti_attack_pattern import AttackPattern 14 | from .entities.opencti_campaign import Campaign 15 | 16 | # Administrative entities 17 | from .entities.opencti_capability import Capability 18 | from .entities.opencti_case_incident import CaseIncident 19 | from .entities.opencti_case_rfi import CaseRfi 20 | from .entities.opencti_case_rft import CaseRft 21 | from .entities.opencti_channel import Channel 22 | from .entities.opencti_course_of_action import CourseOfAction 23 | from .entities.opencti_data_component import DataComponent 24 | from .entities.opencti_data_source import DataSource 25 | from .entities.opencti_external_reference import ExternalReference 26 | from .entities.opencti_feedback import Feedback 27 | from .entities.opencti_group import Group 28 | from .entities.opencti_grouping import Grouping 29 | from .entities.opencti_identity import Identity 30 | from .entities.opencti_incident import Incident 31 | from .entities.opencti_indicator import Indicator 32 | from .entities.opencti_infrastructure import Infrastructure 33 | from .entities.opencti_intrusion_set import IntrusionSet 34 | from .entities.opencti_kill_chain_phase import KillChainPhase 35 | from .entities.opencti_label import Label 36 | from .entities.opencti_location import Location 37 | from .entities.opencti_malware import Malware 38 | from .entities.opencti_malware_analysis import MalwareAnalysis 39 | from .entities.opencti_marking_definition import MarkingDefinition 40 | from .entities.opencti_note import Note 41 | from .entities.opencti_observed_data import ObservedData 42 | from .entities.opencti_opinion import Opinion 43 | from .entities.opencti_report import Report 44 | from .entities.opencti_role import Role 45 | from .entities.opencti_settings import Settings 46 | from .entities.opencti_stix_core_relationship import StixCoreRelationship 47 | from .entities.opencti_stix_cyber_observable import StixCyberObservable 48 | from .entities.opencti_stix_domain_object import StixDomainObject 49 | from .entities.opencti_stix_nested_ref_relationship import StixNestedRefRelationship 50 | from .entities.opencti_stix_object_or_stix_relationship import ( 51 | StixObjectOrStixRelationship, 52 | ) 53 | from .entities.opencti_stix_sighting_relationship import StixSightingRelationship 54 | from .entities.opencti_task import Task 55 | from .entities.opencti_threat_actor import ThreatActor 56 | from .entities.opencti_threat_actor_group import ThreatActorGroup 57 | from .entities.opencti_threat_actor_individual import ThreatActorIndividual 58 | from .entities.opencti_tool import Tool 59 | from .entities.opencti_user import User 60 | from .entities.opencti_vulnerability import Vulnerability 61 | from .utils.constants import ( 62 | CustomObjectCaseIncident, 63 | CustomObjectChannel, 64 | CustomObjectTask, 65 | CustomObservableBankAccount, 66 | CustomObservableCredential, 67 | CustomObservableCryptocurrencyWallet, 68 | CustomObservableCryptographicKey, 69 | CustomObservableHostname, 70 | CustomObservableMediaContent, 71 | CustomObservablePaymentCard, 72 | CustomObservablePersona, 73 | CustomObservablePhoneNumber, 74 | CustomObservableText, 75 | CustomObservableTrackingNumber, 76 | CustomObservableUserAgent, 77 | MultipleRefRelationship, 78 | StixCyberObservableTypes, 79 | StixMetaTypes, 80 | ) 81 | from .utils.opencti_stix2 import ( 82 | STIX_EXT_MITRE, 83 | STIX_EXT_OCTI, 84 | STIX_EXT_OCTI_SCO, 85 | OpenCTIStix2, 86 | ) 87 | from .utils.opencti_stix2_splitter import OpenCTIStix2Splitter 88 | from .utils.opencti_stix2_update import OpenCTIStix2Update 89 | from .utils.opencti_stix2_utils import OpenCTIStix2Utils 90 | 91 | __all__ = [ 92 | "AttackPattern", 93 | "Campaign", 94 | "CaseIncident", 95 | "CaseRfi", 96 | "CaseRft", 97 | "Channel", 98 | "Task", 99 | "ConnectorType", 100 | "CourseOfAction", 101 | "DataComponent", 102 | "DataSource", 103 | "ExternalReference", 104 | "Feedback", 105 | "Grouping", 106 | "Identity", 107 | "Incident", 108 | "Indicator", 109 | "Infrastructure", 110 | "IntrusionSet", 111 | "KillChainPhase", 112 | "Label", 113 | "Location", 114 | "Malware", 115 | "MalwareAnalysis", 116 | "MarkingDefinition", 117 | "Note", 118 | "ObservedData", 119 | "OpenCTIApiClient", 120 | "OpenCTIApiConnector", 121 | "OpenCTIApiWork", 122 | "OpenCTIConnector", 123 | "OpenCTIConnectorHelper", 124 | "OpenCTIMetricHandler", 125 | "OpenCTIStix2", 126 | "OpenCTIStix2Splitter", 127 | "OpenCTIStix2Update", 128 | "OpenCTIStix2Utils", 129 | "Opinion", 130 | "Report", 131 | "StixCoreRelationship", 132 | "StixCyberObservable", 133 | "StixNestedRefRelationship", 134 | "StixCyberObservableTypes", 135 | "StixDomainObject", 136 | "StixMetaTypes", 137 | "MultipleRefRelationship", 138 | "StixObjectOrStixRelationship", 139 | "StixSightingRelationship", 140 | "ThreatActor", 141 | "ThreatActorGroup", 142 | "ThreatActorIndividual", 143 | "Tool", 144 | "Vulnerability", 145 | "get_config_variable", 146 | "CustomObjectCaseIncident", 147 | "CustomObjectTask", 148 | "CustomObjectChannel", 149 | "StixCyberObservableTypes", 150 | "CustomObservableCredential", 151 | "CustomObservableHostname", 152 | "CustomObservableUserAgent", 153 | "CustomObservableBankAccount", 154 | "CustomObservableCryptographicKey", 155 | "CustomObservableCryptocurrencyWallet", 156 | "CustomObservablePaymentCard", 157 | "CustomObservablePersona", 158 | "CustomObservablePhoneNumber", 159 | "CustomObservableTrackingNumber", 160 | "CustomObservableText", 161 | "CustomObservableMediaContent", 162 | "STIX_EXT_MITRE", 163 | "STIX_EXT_OCTI_SCO", 164 | "STIX_EXT_OCTI", 165 | "Capability", 166 | "Role", 167 | "Group", 168 | "User", 169 | "Settings", 170 | ] 171 | -------------------------------------------------------------------------------- /pycti/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/api/__init__.py -------------------------------------------------------------------------------- /pycti/api/opencti_api_connector.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict 3 | 4 | from pycti.connector.opencti_connector import OpenCTIConnector 5 | 6 | 7 | class OpenCTIApiConnector: 8 | """OpenCTIApiConnector""" 9 | 10 | def __init__(self, api): 11 | self.api = api 12 | 13 | def read(self, connector_id: str) -> Dict: 14 | """Reading the connector and its details 15 | 16 | :return: return all the connector details 17 | :rtype: dict 18 | """ 19 | self.api.app_logger.info("[INFO] Getting connector details ...") 20 | query = """ 21 | query GetConnector($id: String!) { 22 | connector(id: $id) { 23 | id 24 | name 25 | active 26 | auto 27 | only_contextual 28 | connector_type 29 | connector_scope 30 | connector_state 31 | connector_queue_details { 32 | messages_number 33 | messages_size 34 | } 35 | updated_at 36 | created_at 37 | config { 38 | listen 39 | listen_exchange 40 | push 41 | push_exchange 42 | push_routing 43 | } 44 | built_in 45 | } 46 | } 47 | """ 48 | result = self.api.query(query, {"id": connector_id}) 49 | return result["data"]["connector"] 50 | 51 | def list(self) -> Dict: 52 | """list available connectors 53 | 54 | :return: return dict with connectors 55 | :rtype: dict 56 | """ 57 | 58 | self.api.app_logger.info("Getting connectors ...") 59 | query = """ 60 | query GetConnectors { 61 | connectorsForWorker { 62 | id 63 | name 64 | connector_user { 65 | api_token 66 | } 67 | config { 68 | connection { 69 | host 70 | vhost 71 | use_ssl 72 | port 73 | user 74 | pass 75 | } 76 | listen 77 | listen_exchange 78 | listen_callback_uri 79 | push 80 | push_exchange 81 | push_routing 82 | } 83 | } 84 | } 85 | """ 86 | result = self.api.query(query) 87 | return result["data"]["connectorsForWorker"] 88 | 89 | def ping( 90 | self, connector_id: str, connector_state: Any, connector_info: Dict 91 | ) -> Dict: 92 | """pings a connector by id and state 93 | 94 | :param connector_id: the connectors id 95 | :type connector_id: str 96 | :param connector_state: state for the connector 97 | :type connector_state: 98 | :param connector_info: all details connector 99 | :type connector_info: Dict 100 | :return: the response pingConnector data dict 101 | :rtype: dict 102 | """ 103 | 104 | query = """ 105 | mutation PingConnector($id: ID!, $state: String, $connectorInfo: ConnectorInfoInput ) { 106 | pingConnector(id: $id, state: $state, connectorInfo: $connectorInfo) { 107 | id 108 | connector_state 109 | connector_info { 110 | run_and_terminate 111 | buffering 112 | queue_threshold 113 | queue_messages_size 114 | next_run_datetime 115 | last_run_datetime 116 | } 117 | } 118 | } 119 | """ 120 | result = self.api.query( 121 | query, 122 | { 123 | "id": connector_id, 124 | "state": json.dumps(connector_state), 125 | "connectorInfo": connector_info, 126 | }, 127 | ) 128 | return result["data"]["pingConnector"] 129 | 130 | def register(self, connector: OpenCTIConnector) -> Dict: 131 | """register a connector with OpenCTI 132 | 133 | :param connector: `OpenCTIConnector` connector object 134 | :type connector: OpenCTIConnector 135 | :return: the response registerConnector data dict 136 | :rtype: dict 137 | """ 138 | 139 | query = """ 140 | mutation RegisterConnector($input: RegisterConnectorInput) { 141 | registerConnector(input: $input) { 142 | id 143 | connector_state 144 | config { 145 | connection { 146 | host 147 | vhost 148 | use_ssl 149 | port 150 | user 151 | pass 152 | } 153 | listen 154 | listen_routing 155 | listen_exchange 156 | push 157 | push_routing 158 | push_exchange 159 | } 160 | connector_user_id 161 | } 162 | } 163 | """ 164 | result = self.api.query(query, connector.to_input()) 165 | return result["data"]["registerConnector"] 166 | 167 | def unregister(self, _id: str) -> Dict: 168 | """unregister a connector with OpenCTI 169 | 170 | :param _id: `OpenCTIConnector` connector id 171 | :type _id: string 172 | :return: the response registerConnector data dict 173 | :rtype: dict 174 | """ 175 | query = """ 176 | mutation ConnectorDeletionMutation($id: ID!) { 177 | deleteConnector(id: $id) 178 | } 179 | """ 180 | return self.api.query(query, {"id": _id}) 181 | -------------------------------------------------------------------------------- /pycti/api/opencti_api_playbook.py: -------------------------------------------------------------------------------- 1 | class OpenCTIApiPlaybook: 2 | """OpenCTIApiPlaybook""" 3 | 4 | def __init__(self, api): 5 | self.api = api 6 | 7 | def playbook_step_execution(self, playbook: dict, bundle: str): 8 | self.api.app_logger.info( 9 | "Executing playbook step", 10 | { 11 | "playbook_id": playbook["playbook_id"], 12 | "step_id": playbook["step_id"], 13 | "data_instance_id": playbook["data_instance_id"], 14 | }, 15 | ) 16 | query = """ 17 | mutation PlaybookStepExecution($execution_id: ID!, $event_id: ID!, $execution_start: DateTime!, $data_instance_id: ID!, $playbook_id: ID!, $previous_step_id: ID!, $step_id: ID!, $previous_bundle: String!, $bundle: String!) { 18 | playbookStepExecution(execution_id: $execution_id, event_id: $event_id, execution_start: $execution_start, data_instance_id: $data_instance_id, playbook_id: $playbook_id, previous_step_id: $previous_step_id, step_id: $step_id, previous_bundle: $previous_bundle, bundle: $bundle) 19 | } 20 | """ 21 | self.api.query( 22 | query, 23 | { 24 | "execution_id": playbook["execution_id"], 25 | "event_id": playbook["event_id"], 26 | "execution_start": playbook["execution_start"], 27 | "playbook_id": playbook["playbook_id"], 28 | "data_instance_id": playbook["data_instance_id"], 29 | "step_id": playbook["step_id"], 30 | "previous_step_id": playbook["previous_step_id"], 31 | "previous_bundle": playbook["previous_bundle"], 32 | "bundle": bundle, 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /pycti/connector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/connector/__init__.py -------------------------------------------------------------------------------- /pycti/connector/opencti_connector.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # Scope definition 4 | # EXTERNAL_IMPORT = None 5 | # INTERNAL_IMPORT_FILE = Files mime types to support (application/json, ...) 6 | # INTERNAL_ENRICHMENT = Entity types to support (Report, Hash, ...) 7 | # INTERNAL_EXPORT_FILE = Files mime types to generate (application/pdf, ...) 8 | 9 | 10 | class ConnectorType(Enum): 11 | EXTERNAL_IMPORT = "EXTERNAL_IMPORT" # From remote sources to OpenCTI stix2 12 | INTERNAL_IMPORT_FILE = ( 13 | "INTERNAL_IMPORT_FILE" # From OpenCTI file system to OpenCTI stix2 14 | ) 15 | INTERNAL_ENRICHMENT = "INTERNAL_ENRICHMENT" # From OpenCTI stix2 to OpenCTI stix2 16 | INTERNAL_ANALYSIS = "INTERNAL_ANALYSIS" # From OpenCTI file system or OpenCTI stix2 to OpenCTI file system 17 | INTERNAL_EXPORT_FILE = ( 18 | "INTERNAL_EXPORT_FILE" # From OpenCTI stix2 to OpenCTI file system 19 | ) 20 | STREAM = "STREAM" # Read the stream and do something 21 | 22 | 23 | class OpenCTIConnector: 24 | """Main class for OpenCTI connector 25 | 26 | :param connector_id: id for the connector (valid uuid4) 27 | :type connector_id: str 28 | :param connector_name: name for the connector 29 | :type connector_name: str 30 | :param connector_type: valid OpenCTI connector type (see `ConnectorType`) 31 | :type connector_type: str 32 | :param scope: connector scope 33 | :type scope: str 34 | :raises ValueError: if the connector type is not valid 35 | """ 36 | 37 | def __init__( 38 | self, 39 | connector_id: str, 40 | connector_name: str, 41 | connector_type: str, 42 | scope: str, 43 | auto: bool, 44 | only_contextual: bool, 45 | playbook_compatible: bool, 46 | listen_callback_uri=None, 47 | ): 48 | self.id = connector_id 49 | self.name = connector_name 50 | self.type = ConnectorType(connector_type) 51 | if self.type is None: 52 | raise ValueError("Invalid connector type: " + connector_type) 53 | if scope and len(scope) > 0: 54 | self.scope = scope.split(",") 55 | else: 56 | self.scope = [] 57 | self.auto = auto 58 | self.only_contextual = only_contextual 59 | self.playbook_compatible = playbook_compatible 60 | self.listen_callback_uri = listen_callback_uri 61 | 62 | def to_input(self) -> dict: 63 | """connector input to use in API query 64 | 65 | :return: dict with connector data 66 | :rtype: dict 67 | """ 68 | return { 69 | "input": { 70 | "id": self.id, 71 | "name": self.name, 72 | "type": self.type.name, 73 | "scope": self.scope, 74 | "auto": self.auto, 75 | "only_contextual": self.only_contextual, 76 | "playbook_compatible": self.playbook_compatible, 77 | "listen_callback_uri": self.listen_callback_uri, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pycti/connector/opencti_metric_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Union 2 | 3 | from prometheus_client import Counter, Enum, start_http_server 4 | 5 | 6 | class OpenCTIMetricHandler: 7 | def __init__(self, connector_logger, activated: bool = False, port: int = 9095): 8 | """ 9 | Init of OpenCTIMetricHandler class. 10 | 11 | Parameters 12 | ---------- 13 | activated : bool, default False 14 | If True use metrics in client and connectors. 15 | port : int, default 9095 16 | Port for prometheus server. 17 | """ 18 | self.activated = activated 19 | self.connector_logger = connector_logger 20 | if self.activated: 21 | self.connector_logger.info("Exposing metrics on port", {"port": port}) 22 | start_http_server(port) 23 | self._metrics = { 24 | "bundle_send": Counter( 25 | "bundle_send", 26 | "Number of bundle send", 27 | ), 28 | "record_send": Counter( 29 | "record_send", 30 | "Number of record (objects per bundle) send", 31 | ), 32 | "run_count": Counter( 33 | "run_count", 34 | "Number of run", 35 | ), 36 | "ping_api_count": Counter( 37 | "ping_api_count", 38 | "Number of ping to the api", 39 | ), 40 | "ping_api_error": Counter( 41 | "ping_api_error", 42 | "Number of error when pinging the api", 43 | ), 44 | "error_count": Counter( 45 | "error_count", 46 | "Number of error", 47 | ), 48 | "client_error_count": Counter( 49 | "client_error_count", 50 | "Number of client error", 51 | ), 52 | "state": Enum( 53 | "state", "State of connector", states=["idle", "running", "stopped"] 54 | ), 55 | } 56 | 57 | def _metric_exists( 58 | self, name: str, expected_type: Union[Type[Counter], Type[Enum]] 59 | ) -> bool: 60 | """ 61 | Check if a metric exists and has the correct type. 62 | 63 | If it does not, log an error and return False. 64 | 65 | Parameters 66 | ---------- 67 | name : str 68 | Name of the metric to check. 69 | expected_type : Counter or Enum 70 | Expected type of the metric. 71 | 72 | Returns 73 | ------- 74 | bool 75 | True if the metric exists and is of the correct type else False. 76 | """ 77 | if name not in self._metrics: 78 | self.connector_logger.error("Metric does not exist.", {"name": name}) 79 | return False 80 | if not isinstance(self._metrics[name], expected_type): 81 | self.connector_logger.error( 82 | "Metric not of expected type", 83 | {"name": name, "expected_type": expected_type}, 84 | ) 85 | return False 86 | return True 87 | 88 | def inc(self, name: str, n: int = 1): 89 | """ 90 | Increment the metric (counter) `name` by `n`. 91 | 92 | Parameters 93 | ---------- 94 | name : str 95 | Name of the metric to increment. 96 | n : int, default 1 97 | Increment the counter by `n`. 98 | """ 99 | if self.activated: 100 | if self._metric_exists(name, Counter): 101 | self._metrics[name].inc(n) 102 | 103 | def state(self, state: str, name: str = "state"): 104 | """ 105 | Set the state `state` for metric `name`. 106 | 107 | Parameters 108 | ---------- 109 | state : str 110 | State to set. 111 | name : str, default = "state" 112 | Name of the metric to set. 113 | """ 114 | if self.activated: 115 | if self._metric_exists(name, Enum): 116 | self._metrics[name].state(state) 117 | -------------------------------------------------------------------------------- /pycti/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/entities/__init__.py -------------------------------------------------------------------------------- /pycti/entities/indicator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/entities/indicator/__init__.py -------------------------------------------------------------------------------- /pycti/entities/indicator/opencti_indicator_properties.py: -------------------------------------------------------------------------------- 1 | INDICATOR_PROPERTIES = """ 2 | id 3 | standard_id 4 | entity_type 5 | parent_types 6 | spec_version 7 | created_at 8 | updated_at 9 | creators { 10 | id 11 | name 12 | } 13 | createdBy { 14 | ... on Identity { 15 | id 16 | standard_id 17 | entity_type 18 | parent_types 19 | spec_version 20 | identity_class 21 | name 22 | description 23 | roles 24 | contact_information 25 | x_opencti_aliases 26 | created 27 | modified 28 | objectLabel { 29 | id 30 | value 31 | color 32 | } 33 | } 34 | ... on Organization { 35 | x_opencti_organization_type 36 | x_opencti_reliability 37 | } 38 | ... on Individual { 39 | x_opencti_firstname 40 | x_opencti_lastname 41 | } 42 | } 43 | objectOrganization { 44 | id 45 | standard_id 46 | name 47 | } 48 | objectMarking { 49 | id 50 | standard_id 51 | entity_type 52 | definition_type 53 | definition 54 | created 55 | modified 56 | x_opencti_order 57 | x_opencti_color 58 | } 59 | objectLabel { 60 | id 61 | value 62 | color 63 | } 64 | externalReferences { 65 | edges { 66 | node { 67 | id 68 | standard_id 69 | entity_type 70 | source_name 71 | description 72 | url 73 | hash 74 | external_id 75 | created 76 | modified 77 | } 78 | } 79 | } 80 | revoked 81 | confidence 82 | created 83 | modified 84 | pattern_type 85 | pattern_version 86 | pattern 87 | name 88 | description 89 | indicator_types 90 | valid_from 91 | valid_until 92 | x_opencti_score 93 | x_opencti_detection 94 | x_opencti_main_observable_type 95 | x_opencti_observable_values { 96 | type 97 | value 98 | hashes { 99 | algorithm 100 | hash 101 | } 102 | } 103 | x_mitre_platforms 104 | observables { 105 | edges { 106 | node { 107 | id 108 | entity_type 109 | observable_value 110 | } 111 | } 112 | } 113 | killChainPhases { 114 | id 115 | standard_id 116 | entity_type 117 | kill_chain_name 118 | phase_name 119 | x_opencti_order 120 | created 121 | modified 122 | } 123 | """ 124 | INDICATOR_PROPERTIES_WITH_FILES = """ 125 | id 126 | standard_id 127 | entity_type 128 | parent_types 129 | spec_version 130 | created_at 131 | updated_at 132 | creators { 133 | id 134 | name 135 | } 136 | createdBy { 137 | ... on Identity { 138 | id 139 | standard_id 140 | entity_type 141 | parent_types 142 | spec_version 143 | identity_class 144 | name 145 | description 146 | roles 147 | contact_information 148 | x_opencti_aliases 149 | created 150 | modified 151 | objectLabel { 152 | id 153 | value 154 | color 155 | } 156 | } 157 | ... on Organization { 158 | x_opencti_organization_type 159 | x_opencti_reliability 160 | } 161 | ... on Individual { 162 | x_opencti_firstname 163 | x_opencti_lastname 164 | } 165 | } 166 | objectOrganization { 167 | id 168 | standard_id 169 | name 170 | } 171 | objectMarking { 172 | id 173 | standard_id 174 | entity_type 175 | definition_type 176 | definition 177 | created 178 | modified 179 | x_opencti_order 180 | x_opencti_color 181 | } 182 | objectLabel { 183 | id 184 | value 185 | color 186 | } 187 | externalReferences { 188 | edges { 189 | node { 190 | id 191 | standard_id 192 | entity_type 193 | source_name 194 | description 195 | url 196 | hash 197 | external_id 198 | created 199 | modified 200 | importFiles { 201 | edges { 202 | node { 203 | id 204 | name 205 | size 206 | metaData { 207 | mimetype 208 | version 209 | } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | revoked 217 | confidence 218 | created 219 | modified 220 | pattern_type 221 | pattern_version 222 | pattern 223 | name 224 | description 225 | indicator_types 226 | valid_from 227 | valid_until 228 | x_opencti_score 229 | x_opencti_detection 230 | x_opencti_main_observable_type 231 | x_opencti_observable_values { 232 | type 233 | value 234 | hashes { 235 | algorithm 236 | hash 237 | } 238 | } 239 | x_mitre_platforms 240 | observables { 241 | edges { 242 | node { 243 | id 244 | entity_type 245 | observable_value 246 | } 247 | } 248 | } 249 | killChainPhases { 250 | id 251 | standard_id 252 | entity_type 253 | kill_chain_name 254 | phase_name 255 | x_opencti_order 256 | created 257 | modified 258 | } 259 | importFiles { 260 | edges { 261 | node { 262 | id 263 | name 264 | size 265 | metaData { 266 | mimetype 267 | version 268 | } 269 | } 270 | } 271 | } 272 | """ 273 | -------------------------------------------------------------------------------- /pycti/entities/opencti_capability.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | 4 | class Capability: 5 | """Represents a role capability on the OpenCTI platform 6 | 7 | See the properties attribute to understand which properties are fetched by 8 | default from the graphql queries. 9 | """ 10 | 11 | def __init__(self, opencti): 12 | self.opencti = opencti 13 | self.properties = """ 14 | id 15 | standard_id 16 | entity_type 17 | parent_types 18 | name 19 | description 20 | attribute_order 21 | created_at 22 | updated_at 23 | """ 24 | 25 | def list(self, **kwargs) -> List[Dict]: 26 | """Lists all capabilities available on the platform 27 | 28 | :param customAttributes: Custom attributes to retrieve from the GraphQL 29 | query. 30 | :type customAttributes: str, optional 31 | :return: List of capabilities 32 | :rtype: List[Dict] 33 | """ 34 | custom_attributes = kwargs.get("customAttributes") 35 | self.opencti.admin_logger.info("Listing capabilities") 36 | query = ( 37 | """ 38 | query CapabilityList { 39 | capabilities { 40 | edges { 41 | node { 42 | """ 43 | + (self.properties if custom_attributes is None else custom_attributes) 44 | + """ 45 | } 46 | } 47 | } 48 | } 49 | """ 50 | ) 51 | result = self.opencti.query(query) 52 | return self.opencti.process_multiple(result["data"]["capabilities"]) 53 | -------------------------------------------------------------------------------- /pycti/entities/opencti_stix.py: -------------------------------------------------------------------------------- 1 | class Stix: 2 | def __init__(self, opencti): 3 | self.opencti = opencti 4 | 5 | """ 6 | Delete a Stix element 7 | 8 | :param id: the Stix element id 9 | :return void 10 | """ 11 | 12 | def delete(self, **kwargs): 13 | id = kwargs.get("id", None) 14 | if id is not None: 15 | self.opencti.app_logger.info("Deleting Stix element", {"id": id}) 16 | query = """ 17 | mutation StixEdit($id: ID!) { 18 | stixEdit(id: $id) { 19 | delete 20 | } 21 | } 22 | """ 23 | self.opencti.query(query, {"id": id}) 24 | else: 25 | self.opencti.app_logger.error("[opencti_stix] Missing parameters: id") 26 | return None 27 | 28 | """ 29 | Merge a Stix-Object object field 30 | 31 | :param id: the Stix-Object id 32 | :param key: the key of the field 33 | :param value: the value of the field 34 | :return The updated Stix-Object object 35 | """ 36 | 37 | def merge(self, **kwargs): 38 | id = kwargs.get("id") 39 | stix_objects_ids = kwargs.get("object_ids") 40 | if id is not None and stix_objects_ids is not None: 41 | self.opencti.app_logger.info( 42 | "Merging Stix object", {"id": id, "sources": ",".join(stix_objects_ids)} 43 | ) 44 | query = """ 45 | mutation StixEdit($id: ID!, $stixObjectsIds: [String]!) { 46 | stixEdit(id: $id) { 47 | merge(stixObjectsIds: $stixObjectsIds) { 48 | id 49 | standard_id 50 | entity_type 51 | } 52 | } 53 | } 54 | """ 55 | result = self.opencti.query( 56 | query, 57 | { 58 | "id": id, 59 | "stixObjectsIds": stix_objects_ids, 60 | }, 61 | ) 62 | return self.opencti.process_multiple_fields( 63 | result["data"]["stixEdit"]["merge"] 64 | ) 65 | else: 66 | self.opencti.app_logger.error( 67 | "[opencti_stix] Missing parameters: id and object_ids" 68 | ) 69 | return None 70 | -------------------------------------------------------------------------------- /pycti/entities/stix_cyber_observable/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/entities/stix_cyber_observable/__init__.py -------------------------------------------------------------------------------- /pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_deprecated.py: -------------------------------------------------------------------------------- 1 | import deprecation 2 | 3 | 4 | class StixCyberObservableDeprecatedMixin: 5 | """ 6 | deprecated [>=6.2 & <6.8]` 7 | Promote a Stix-Observable to an Indicator 8 | 9 | :param id: the Stix-Observable id 10 | :return the observable 11 | """ 12 | 13 | @deprecation.deprecated( 14 | deprecated_in="6.2", 15 | removed_in="6.5", 16 | details="Use promote_to_indicator_v2 instead.", 17 | ) 18 | def promote_to_indicator(self, **kwargs): 19 | id = kwargs.get("id", None) 20 | custom_attributes = kwargs.get("customAttributes", None) 21 | with_files = kwargs.get("withFiles", False) 22 | if id is not None: 23 | self.opencti.app_logger.info( 24 | "Promoting Stix-Observable", 25 | { 26 | "id": id, 27 | "withFiles": with_files, 28 | "customAttributes": custom_attributes, 29 | }, 30 | ) 31 | query = ( 32 | """ 33 | mutation StixCyberObservableEdit($id: ID!) { 34 | stixCyberObservableEdit(id: $id) { 35 | promote { 36 | """ 37 | + ( 38 | custom_attributes 39 | if custom_attributes is not None 40 | else (self.properties_with_files if with_files else self.properties) 41 | ) 42 | + """ 43 | } 44 | } 45 | } 46 | """ 47 | ) 48 | result = self.opencti.query(query, {"id": id}) 49 | return self.opencti.process_multiple_fields( 50 | result["data"]["stixCyberObservableEdit"]["promote"] 51 | ) 52 | else: 53 | self.opencti.app_logger.error( 54 | "[opencti_stix_cyber_observable_promote] Missing parameters: id" 55 | ) 56 | return None 57 | -------------------------------------------------------------------------------- /pycti/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/pycti/utils/__init__.py -------------------------------------------------------------------------------- /pycti/utils/opencti_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timezone 3 | 4 | from pythonjsonlogger import jsonlogger 5 | 6 | 7 | class CustomJsonFormatter(jsonlogger.JsonFormatter): 8 | def add_fields(self, log_record, record, message_dict): 9 | super(CustomJsonFormatter, self).add_fields(log_record, record, message_dict) 10 | if not log_record.get("timestamp"): 11 | # This doesn't use record.created, so it is slightly off 12 | now = datetime.now(tz=timezone.utc) 13 | log_record["timestamp"] = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ") 14 | if log_record.get("level"): 15 | log_record["level"] = log_record["level"].upper() 16 | else: 17 | log_record["level"] = record.levelname 18 | 19 | 20 | def logger(level, json_logging=True): 21 | # Exceptions 22 | logging.getLogger("urllib3").setLevel(logging.WARNING) 23 | logging.getLogger("pika").setLevel(logging.ERROR) 24 | # Exceptions 25 | if json_logging: 26 | log_handler = logging.StreamHandler() 27 | log_handler.setLevel(level) 28 | formatter = CustomJsonFormatter("%(timestamp)s %(level)s %(name)s %(message)s") 29 | log_handler.setFormatter(formatter) 30 | logging.basicConfig(handlers=[log_handler], level=level, force=True) 31 | else: 32 | logging.basicConfig(level=level) 33 | 34 | class AppLogger: 35 | def __init__(self, name): 36 | self.local_logger = logging.getLogger(name) 37 | 38 | @staticmethod 39 | def prepare_meta(meta=None): 40 | return None if meta is None else {"attributes": meta} 41 | 42 | @staticmethod 43 | def setup_logger_level(lib, log_level): 44 | logging.getLogger(lib).setLevel(log_level) 45 | 46 | def debug(self, message, meta=None): 47 | self.local_logger.debug(message, extra=AppLogger.prepare_meta(meta)) 48 | 49 | def info(self, message, meta=None): 50 | self.local_logger.info(message, extra=AppLogger.prepare_meta(meta)) 51 | 52 | def warning(self, message, meta=None): 53 | self.local_logger.warning(message, extra=AppLogger.prepare_meta(meta)) 54 | 55 | def error(self, message, meta=None): 56 | # noinspection PyTypeChecker 57 | self.local_logger.error( 58 | message, exc_info=1, extra=AppLogger.prepare_meta(meta) 59 | ) 60 | 61 | return AppLogger 62 | -------------------------------------------------------------------------------- /pycti/utils/opencti_stix2_identifier.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from stix2.canonicalization.Canonicalize import canonicalize 4 | 5 | 6 | def external_reference_generate_id(url=None, source_name=None, external_id=None): 7 | if url is not None: 8 | data = {"url": url} 9 | elif source_name is not None and external_id is not None: 10 | data = {"source_name": source_name, "external_id": external_id} 11 | else: 12 | return None 13 | data = canonicalize(data, utf8=False) 14 | id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) 15 | return "external-reference--" + id 16 | 17 | 18 | def kill_chain_phase_generate_id(phase_name, kill_chain_name): 19 | data = {"phase_name": phase_name, "kill_chain_name": kill_chain_name} 20 | data = canonicalize(data, utf8=False) 21 | id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) 22 | return "kill-chain-phase--" + id 23 | -------------------------------------------------------------------------------- /pycti/utils/opencti_stix2_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from stix2 import EqualityComparisonExpression, ObjectPath, ObservationExpression 4 | 5 | SUPPORTED_STIX_ENTITY_OBJECTS = [ 6 | "attack-pattern", 7 | "campaign", 8 | "case-incident", 9 | "x-opencti-case-incident", 10 | "case-rfi", 11 | "x-opencti-case-rfi", 12 | "case-rft", 13 | "x-opencti-case-rft", 14 | "channel", 15 | "course-of-action", 16 | "data-component", 17 | "x-mitre-data-component", 18 | "data-source", 19 | "x-mitre-data-source", 20 | "event", 21 | "external-reference", 22 | "feedback", 23 | "x-opencti-feedback", 24 | "grouping", 25 | "identity", 26 | "incident", 27 | "indicator", 28 | "infrastructure", 29 | "intrusion-set", 30 | "kill-chain-phase", 31 | "label", 32 | "language", 33 | "location", 34 | "malware", 35 | "malware-analysis", 36 | "marking-definition", 37 | "narrative", 38 | "note", 39 | "observed-data", 40 | "opinion", 41 | "report", 42 | "task", 43 | "x-opencti-task", 44 | "threat-actor", 45 | "tool", 46 | "vocabulary", 47 | "vulnerability", 48 | ] 49 | 50 | STIX_CYBER_OBSERVABLE_MAPPING = { 51 | "autonomous-system": "Autonomous-System", 52 | "directory": "Directory", 53 | "domain-name": "Domain-Name", 54 | "email-addr": "Email-Addr", 55 | "email-message": "Email-Message", 56 | "email-mime-part-type": "Email-Mime-Part-Type", 57 | "artifact": "Artifact", 58 | "file": "StixFile", 59 | "x509-certificate": "X509-Certificate", 60 | "ipv4-addr": "IPv4-Addr", 61 | "ipv6-addr": "IPv6-Addr", 62 | "mac-addr": "Mac-Addr", 63 | "mutex": "Mutex", 64 | "network-traffic": "Network-Traffic", 65 | "process": "Process", 66 | "software": "Software", 67 | "url": "Url", 68 | "user-account": "User-Account", 69 | "windows-registry-key": "Windows-Registry-Key", 70 | "windows-registry-value-type": "Windows-Registry-Value-Type", 71 | "hostname": "Hostname", 72 | "cryptographic-key": "Cryptographic-Key", 73 | "cryptocurrency-wallet": "Cryptocurrency-Wallet", 74 | "text": "Text", 75 | "user-agent": "User-Agent", 76 | "bank-account": "Bank-Account", 77 | "phone-number": "Phone-Number", 78 | "credential": "Credential", 79 | "tracking-number": "Tracking-Number", 80 | "payment-card": "Payment-Card", 81 | "media-content": "Media-Content", 82 | "simple-observable": "Simple-Observable", 83 | "persona": "Persona", 84 | } 85 | 86 | PATTERN_MAPPING = { 87 | "Autonomous-System": ["number"], 88 | "Directory": ["path"], 89 | "Domain-Name": ["value"], 90 | "Email-Addr": ["value"], 91 | "File_md5": ["hashes", "MD5"], 92 | "File_sha1": ["hashes", "SHA-1"], 93 | "File_sha256": ["hashes", "SHA-256"], 94 | "File_sha512": ["hashes", "SHA-512"], 95 | "Email-Message_Body": ["body"], 96 | "Email-Message_Subject": ["subject"], 97 | "Email-Mime-Part-Type": ["body"], 98 | "IPv4-Addr": ["value"], 99 | "IPv6-Addr": ["value"], 100 | "Mac-Addr": ["value"], 101 | "Mutex": ["name"], 102 | "Network-Traffic": ["dst_port"], 103 | "Process": ["pid"], 104 | "Software": ["name"], 105 | "Url": ["value"], 106 | "User-Account": ["acount_login"], 107 | "Windows-Registry-Key": ["key"], 108 | "Windows-Registry-Value-Type": ["name"], 109 | "Hostname": ["value"], 110 | "Bank-Account": ["iban"], 111 | "Phone-Number": ["value"], 112 | "Payment-Card": ["card_number"], 113 | "Tracking-Number": ["value"], 114 | "Credential": ["value"], 115 | "Media-Content": ["url"], 116 | } 117 | 118 | OBSERVABLES_VALUE_INT = [ 119 | "Autonomous-System.number", 120 | "Network-Traffic.dst_port", 121 | "Process.pid", 122 | ] 123 | 124 | 125 | class OpenCTIStix2Utils: 126 | @staticmethod 127 | def stix_observable_opencti_type(observable_type): 128 | if observable_type in STIX_CYBER_OBSERVABLE_MAPPING: 129 | return STIX_CYBER_OBSERVABLE_MAPPING[observable_type] 130 | else: 131 | return "Unknown" 132 | 133 | @staticmethod 134 | def create_stix_pattern(observable_type, observable_value): 135 | if observable_type in PATTERN_MAPPING: 136 | lhs = ObjectPath( 137 | ( 138 | observable_type.lower() 139 | if "_" not in observable_type 140 | else observable_type.split("_")[0].lower() 141 | ), 142 | PATTERN_MAPPING[observable_type], 143 | ) 144 | ece = ObservationExpression( 145 | EqualityComparisonExpression(lhs, observable_value) 146 | ) 147 | return str(ece) 148 | else: 149 | return None 150 | 151 | """Generate random stix id (uuid v1) 152 | This id will stored and resolved by openCTI 153 | We will stored only 5 stix of this type to prevent database flooding 154 | :param stix_type: the stix type 155 | """ 156 | 157 | @staticmethod 158 | def generate_random_stix_id(stix_type): 159 | raise ValueError( 160 | "This function should not be used anymore, please use the generate_id function for SDO or proper SCO constructor" 161 | ) 162 | 163 | @staticmethod 164 | def retrieveClassForMethod( 165 | openCTIApiClient, entity: Dict, type_path: str, method: str 166 | ) -> Any: 167 | if entity is not None and type_path in entity: 168 | attributeName = entity[type_path].lower().replace("-", "_") 169 | if hasattr(openCTIApiClient, attributeName): 170 | attribute = getattr(openCTIApiClient, attributeName) 171 | if hasattr(attribute, method): 172 | return attribute 173 | return None 174 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.isort] 6 | profile = "black" 7 | 8 | [tool.black] 9 | target-version = ["py37"] 10 | 11 | [tool.mypy] 12 | check_untyped_defs = true 13 | disallow_any_generics = true 14 | disallow_incomplete_defs = true 15 | disallow_subclassing_any = true 16 | disallow_untyped_calls = true 17 | disallow_untyped_decorators = true 18 | disallow_untyped_defs = true 19 | no_implicit_optional = true 20 | no_implicit_reexport = true 21 | show_error_context = true 22 | strict_equality = true 23 | warn_redundant_casts = true 24 | warn_return_any = true 25 | warn_unused_configs = true 26 | warn_unused_ignores = true 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "labels": [ 7 | "dependencies", 8 | "filigran team" 9 | ], 10 | "prConcurrentLimit": 2, 11 | "packageRules": [ 12 | { 13 | "matchUpdateTypes": [ 14 | "minor" 15 | ], 16 | "prPriority": 5 17 | } 18 | ], 19 | "timezone": "Europe/Paris", 20 | "schedule": [ 21 | "after 10pm every weekday", 22 | "every weekend", 23 | "before 5am every weekday" 24 | ], 25 | "updateNotScheduled": false 26 | } 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Filigran 2 | datefinder~=0.7.3 3 | pika~=1.3.0 4 | python-magic~=0.4.27; sys_platform == 'linux' or sys_platform == 'darwin' 5 | python-magic-bin~=0.4.14; sys_platform == 'win32' 6 | python_json_logger~=3.3.0 7 | PyYAML~=6.0 8 | pydantic~=2.11.3 9 | requests~=2.32.3 10 | setuptools~=78.1.0 11 | cachetools~=5.5.0 12 | prometheus-client~=0.21.1 13 | opentelemetry-api~=1.32.0 14 | opentelemetry-sdk~=1.32.0 15 | deprecation~=2.1.0 16 | fastapi>=0.115.8,<0.116.0 17 | uvicorn[standard]>=0.33.0,<0.35.0 18 | # OpenCTI 19 | filigran-sseclient>=1.0.2 20 | stix2~=3.0.1 -------------------------------------------------------------------------------- /scripts/clone-opencti.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z "$1" ]] || [[ -z "$2" ]] || [[ -z "$3" ]] 4 | then 5 | echo "[CLONE-DEPS] This scripts $0 requires 3 parameters: branch_name:$1, pr_target_branch: $2, workspace:$3 (optional: PR_number:$4)" 6 | exit 0 7 | fi 8 | 9 | PR_BRANCH_NAME=$1 10 | PR_TARGET_BRANCH=$2 11 | WORKSPACE=$3 12 | PR_NUMBER=$4 13 | 14 | OPENCTI_DIR="${WORKSPACE}/opencti" 15 | 16 | clone_for_pr_build() { 17 | cd ${WORKSPACE} 18 | export GH_TOKEN="${GITHUB_TOKEN}" 19 | 20 | gh auth login --hostname github.com --with-token ${GH_TOKEN} 21 | gh auth status 22 | gh repo set-default https://github.com/OpenCTI-Platform/client-python 23 | 24 | #Check current PR to see if label "multi-repository" is set 25 | IS_MULTI_REPO=$(gh pr view ${PR_NUMBER} --json labels | grep -c "multi-repository") 26 | if [[ ${IS_MULTI_REPO} -eq 1 ]] 27 | then 28 | 29 | OPENCTI_BRANCH=${PR_BRANCH_NAME} 30 | echo "[CLONE-DEPS] Multi repository PR, looking for opencti related branch" 31 | if [[ $(echo ${PR_BRANCH_NAME} | cut -d "/" -f 1) == "opencti" ]] 32 | then 33 | #remove opencti prefix when present 34 | OPENCTI_BRANCH=$(echo ${PR_BRANCH_NAME} | cut -d "/" -f2-) 35 | fi 36 | echo "[CLONE-DEPS] OPENCTI_BRANCH is ${OPENCTI_BRANCH}, target branch is ${PR_TARGET_BRANCH}" 37 | gh repo clone https://github.com/OpenCTI-Platform/opencti ${OPENCTI_DIR} -- --branch ${PR_TARGET_BRANCH} --depth=1 38 | cd ${OPENCTI_DIR} 39 | 40 | # search for the first opencti PR that matches OPENCTI_BRANCH 41 | gh repo set-default https://github.com/OpenCTI-Platform/opencti 42 | gh pr list --label "multi-repository" > multi-repo-prs.txt 43 | 44 | cat multi-repo-prs.txt 45 | 46 | OPENCTI_PR_NUMBER=$(cat multi-repo-prs.txt | grep "${OPENCTI_BRANCH}" | head -n 1 | sed 's/#//g' | awk '{print $1}') 47 | echo "OPENCTI_PR_NUMBER=${OPENCTI_PR_NUMBER}" 48 | 49 | if [[ "${OPENCTI_PR_NUMBER}" != "" ]] 50 | then 51 | echo "[CLONE-DEPS] Found a PR in opencti with number ${OPENCTI_PR_NUMBER}, using it." 52 | gh pr checkout ${OPENCTI_PR_NUMBER} 53 | else 54 | echo "[CLONE-DEPS] No PR found in opencti side, keeping opencti:${PR_TARGET_BRANCH}" 55 | # Repository already clone on PR target branch 56 | fi 57 | 58 | else 59 | echo "[CLONE-DEPS] NOT multi repo, cloning opencti:${PR_TARGET_BRANCH}" 60 | gh repo clone https://github.com/OpenCTI-Platform/opencti ${OPENCTI_DIR} -- --branch ${PR_TARGET_BRANCH} --depth=1 61 | fi 62 | } 63 | 64 | clone_for_push_build() { 65 | echo "[CLONE-DEPS] Build from a commit, checking if a dedicated branch is required." 66 | BRANCH_PREFIX=$(echo $PR_BRANCH_NAME | cut -d "/" -f 1 | grep -c "opencti") 67 | if [[ "${BRANCH_PREFIX}" -eq "1" ]] 68 | then 69 | echo "[CLONE-DEPS] Dedicated OpenCTI branch found, using it" 70 | OPENCTI_BRANCH=$(echo $PR_BRANCH_NAME | cut -d "/" -f2-) 71 | git clone -b $OPENCTI_BRANCH https://github.com/OpenCTI-Platform/opencti.git 72 | else 73 | echo "[CLONE-DEPS] No dedicated OpenCTI branch found, using master" 74 | git clone https://github.com/OpenCTI-Platform/opencti.git 75 | fi 76 | } 77 | 78 | echo "[CLONE-DEPS] START; with PR_BRANCH_NAME=${PR_BRANCH_NAME},PR_TARGET_BRANCH=${PR_TARGET_BRANCH}, PR_NUMBER=${PR_NUMBER}, OPENCTI_DIR=${OPENCTI_DIR}." 79 | if [[ -z ${PR_NUMBER} ]] || [[ ${PR_NUMBER} == "" ]] 80 | then 81 | # No PR number from Drone = "Push build". And it's only for repository branch (not fork) 82 | # Using github cli to get PR number anyway 83 | PR_NUMBER=$(gh pr view ${PR_BRANCH_NAME} --json number --jq '.number') 84 | PR_TARGET_BRANCH=$(gh pr view ${PR_BRANCH_NAME} --json baseRefName --jq '.baseRefName') 85 | 86 | if [[ -z ${PR_NUMBER} ]] || [[ ${PR_NUMBER} == "" ]] 87 | then 88 | echo "[CLONE-DEPS] PR is not created on github yet, using clone without PR number and default fallback to master" 89 | clone_for_push_build 90 | else 91 | echo "[CLONE-DEPS] Got data from github cli, continue with: PR_TARGET_BRANCH=${PR_TARGET_BRANCH}, PR_NUMBER=${PR_NUMBER}." 92 | clone_for_pr_build 93 | fi 94 | else 95 | # PR build is trigger from Pull Request coming both from branch and forks. 96 | # We need to have this clone accross repository that works for forks (community PR) 97 | echo "[CLONE-DEPS] Got PR number ${PR_NUMBER} from Drone = "PR build"; Pull Request coming both from branch and forks." 98 | clone_for_pr_build 99 | fi 100 | 101 | cd ${OPENCTI_DIR} 102 | echo "[CLONE-DEPS] END; Using opencti on branch:$(git branch --show-current)" 103 | 104 | cd ${WORKSPACE} -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pycti 3 | version = attr: pycti.__version__ 4 | author = Filigran 5 | author_email = contact@filigran.io 6 | maintainer = Filigran 7 | url = https://github.com/OpenCTI-Platform/client-python 8 | description = Python API client for OpenCTI. 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | license = Apache 12 | classifiers = 13 | Development Status :: 5 - Production/Stable 14 | Intended Audience :: Developers 15 | Intended Audience :: Information Technology 16 | Intended Audience :: Science/Research 17 | License :: OSI Approved :: Apache Software License 18 | Natural Language :: English 19 | Natural Language :: French 20 | Operating System :: OS Independent 21 | Programming Language :: Python :: 3 22 | Topic :: Security 23 | Topic :: Software Development :: Libraries :: Python Modules 24 | 25 | [options] 26 | python_requires = >=3.7 27 | packages = 28 | pycti 29 | pycti.api 30 | pycti.connector 31 | pycti.entities 32 | pycti.entities.indicator 33 | pycti.entities.stix_cyber_observable 34 | pycti.utils 35 | include_package_data = True 36 | install_requires = 37 | # Filigran 38 | datefinder~=0.7.3 39 | pika~=1.3.0 40 | pydantic~=2.11.3 41 | python-magic~=0.4.27; sys_platform == 'linux' or sys_platform == 'darwin' 42 | python-magic-bin~=0.4.14; sys_platform == 'win32' 43 | python_json_logger~=3.3.0 44 | PyYAML~=6.0 45 | requests~=2.32.3 46 | setuptools~=78.1.0 47 | cachetools~=5.5.0 48 | prometheus-client~=0.21.1 49 | opentelemetry-api~=1.32.0 50 | opentelemetry-sdk~=1.32.0 51 | deprecation~=2.1.0 52 | fastapi>=0.115.8,<0.116.0 53 | uvicorn[standard]>=0.33.0,<0.35.0 54 | # OpenCTI 55 | filigran-sseclient>=1.0.2 56 | stix2~=3.0.1 57 | 58 | [options.extras_require] 59 | dev = 60 | black~=25.1.0 61 | build~=1.2.1 62 | isort~=6.0.0 63 | types-pytz~=2025.2.0.20250326 64 | pre-commit~=4.2.0 65 | pytest-cases~=3.8.0 66 | pytest-cov~=6.1.1 67 | pytest_randomly~=3.16.0 68 | pytest~=8.3.4 69 | types-python-dateutil~=2.9.0 70 | wheel~=0.45.1 71 | doc = 72 | autoapi~=2.0.1 73 | sphinx-autodoc-typehints~=3.2.0 74 | sphinx-rtd-theme~=3.0.2 75 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 2 | build>=0.7 3 | isort>=5.10 4 | pre-commit~=4.2 5 | pytest~=8.1 6 | pytest-cases~=3.6 7 | pytest-cov~=6.1 8 | pytest_randomly~=3.8 9 | types-python-dateutil>=2.8 10 | types-pytz>=2021.3.5 11 | wheel~=0.38 12 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | {"type": "bundle", 2 | "objects": [{ 3 | "id": "software--290d2a0e-f613-5628-a540-e66962f749bd", 4 | "type": "software", 5 | "spec_version": "2.1", 6 | "cpe": "cpe:2.3:a:oracle:graalvm:17.0.14:*:*:*:*:*:*:*", 7 | "name": "graalvm 17.0.14", 8 | "version": "17.0.14", 9 | "vendor": "oracle", 10 | "x_version": "17.0.14", 11 | "x_vendor": "oracle", 12 | "x_product": "graalvm", 13 | "created_by_ref": "identity--be669b67-e9bb-5a5d-900b-c56f5b2a69f7", 14 | "object_marking_refs": [ 15 | "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da" 16 | ] 17 | }, 18 | { 19 | "type": "threat-actor", 20 | "id": "threat-actor--4b57918e-f774-58bb-bcbe-31c3c483daed", 21 | "name": "Turla", 22 | "secondary_motivations": ["dominance"] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /tests/01-unit/metric/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/01-unit/metric/__init__.py -------------------------------------------------------------------------------- /tests/01-unit/metric/test_opencti_metric_handler.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from prometheus_client import Counter, Enum 4 | 5 | from pycti import OpenCTIMetricHandler 6 | from pycti.utils.opencti_logger import logger 7 | 8 | 9 | class TestOpenCTIMetricHandler(TestCase): 10 | def test_metric_exists(self): 11 | test_logger = logger("INFO")("test") 12 | metric = OpenCTIMetricHandler(test_logger, activated=True) 13 | self.assertTrue(metric._metric_exists("error_count", Counter)) 14 | self.assertFalse(metric._metric_exists("error_count", Enum)) 15 | self.assertFalse(metric._metric_exists("best_metric_count", Counter)) 16 | -------------------------------------------------------------------------------- /tests/01-unit/stix/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/01-unit/stix/__init__.py -------------------------------------------------------------------------------- /tests/01-unit/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/01-unit/utils/__init__.py -------------------------------------------------------------------------------- /tests/01-unit/utils/test_constants.py: -------------------------------------------------------------------------------- 1 | from pycti.utils.constants import ( 2 | ContainerTypes, 3 | IdentityTypes, 4 | LocationTypes, 5 | StixCyberObservableTypes, 6 | ) 7 | 8 | 9 | def test_for_enum_typos(): 10 | # Test check for typos between enum names and values 11 | enums = [StixCyberObservableTypes, LocationTypes, IdentityTypes, ContainerTypes] 12 | for enum in enums: 13 | for data in enum: 14 | name = data.name.replace("_", "-") 15 | value = data.value.upper() 16 | assert name == value 17 | 18 | 19 | def test_for_enum_has_value_functionality(): 20 | # Test if the has_value function works as intended 21 | assert StixCyberObservableTypes.has_value("url") is True 22 | assert StixCyberObservableTypes.has_value("LRU") is False 23 | 24 | assert LocationTypes.has_value("CITY") is True 25 | assert LocationTypes.has_value("YTIC") is False 26 | 27 | assert IdentityTypes.has_value("SECTOR") is True 28 | assert IdentityTypes.has_value("RECTOS") is False 29 | 30 | assert ContainerTypes.has_value("Note") is True 31 | assert ContainerTypes.has_value("ETON") is False 32 | -------------------------------------------------------------------------------- /tests/01-unit/utils/test_opencti_stix2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | from pycti.utils.opencti_stix2 import OpenCTIStix2 6 | 7 | 8 | @pytest.fixture 9 | def opencti_stix2(api_client): 10 | return OpenCTIStix2(api_client) 11 | 12 | 13 | def test_unknown_type(opencti_stix2: OpenCTIStix2, caplog): 14 | opencti_stix2.unknown_type({"type": "foo"}) 15 | for record in caplog.records: 16 | assert record.levelname == "ERROR" 17 | assert "Unknown object type, doing nothing..." in caplog.text 18 | 19 | 20 | def test_convert_markdown(opencti_stix2: OpenCTIStix2): 21 | result = opencti_stix2.convert_markdown( 22 | " my is very to me" 23 | ) 24 | assert " my ` is very ` to me" == result 25 | 26 | 27 | def test_convert_markdown_typo(opencti_stix2: OpenCTIStix2): 28 | result = opencti_stix2.convert_markdown( 29 | " my to me" 30 | ) 31 | assert " my ` to me" == result 32 | 33 | 34 | def test_format_date_with_tz(opencti_stix2: OpenCTIStix2): 35 | # Test all 4 format_date cases with timestamp + timezone 36 | my_datetime = datetime.datetime( 37 | 2021, 3, 5, 13, 31, 19, 42621, tzinfo=datetime.timezone.utc 38 | ) 39 | my_datetime_str = my_datetime.isoformat(timespec="milliseconds").replace( 40 | "+00:00", "Z" 41 | ) 42 | assert my_datetime_str == opencti_stix2.format_date(my_datetime) 43 | my_date = my_datetime.date() 44 | my_date_str = "2021-03-05T00:00:00.000Z" 45 | assert my_date_str == opencti_stix2.format_date(my_date) 46 | assert my_datetime_str == opencti_stix2.format_date(my_datetime_str) 47 | assert ( 48 | str( 49 | datetime.datetime.now(tz=datetime.timezone.utc) 50 | .isoformat(timespec="seconds") 51 | .replace("+00:00", "") 52 | ) 53 | in opencti_stix2.format_date() 54 | ) 55 | with pytest.raises(ValueError): 56 | opencti_stix2.format_date("No time") 57 | 58 | # Test all 4 format_date cases with timestamp w/o timezone 59 | my_datetime = datetime.datetime(2021, 3, 5, 13, 31, 19, 42621) 60 | my_datetime_str = ( 61 | my_datetime.replace(tzinfo=datetime.timezone.utc) 62 | .isoformat(timespec="milliseconds") 63 | .replace("+00:00", "Z") 64 | ) 65 | assert my_datetime_str == opencti_stix2.format_date(my_datetime) 66 | my_date = my_datetime.date() 67 | my_date_str = "2021-03-05T00:00:00.000Z" 68 | assert my_date_str == opencti_stix2.format_date(my_date) 69 | assert my_datetime_str == opencti_stix2.format_date(my_datetime_str) 70 | assert ( 71 | str( 72 | datetime.datetime.now(tz=datetime.timezone.utc) 73 | .isoformat(timespec="seconds") 74 | .replace("+00:00", "") 75 | ) 76 | in opencti_stix2.format_date() 77 | ) 78 | with pytest.raises(ValueError): 79 | opencti_stix2.format_date("No time") 80 | 81 | 82 | def test_filter_objects(opencti_stix2: OpenCTIStix2): 83 | objects = [{"id": "123"}, {"id": "124"}, {"id": "125"}, {"id": "126"}] 84 | result = opencti_stix2.filter_objects(["123", "124", "126"], objects) 85 | assert len(result) == 1 86 | assert "126" not in result 87 | 88 | 89 | def test_pick_aliases(opencti_stix2: OpenCTIStix2) -> None: 90 | stix_object = {} 91 | assert opencti_stix2.pick_aliases(stix_object) is None 92 | stix_object["aliases"] = "alias" 93 | assert opencti_stix2.pick_aliases(stix_object) == "alias" 94 | stix_object["x_amitt_aliases"] = "amitt_alias" 95 | assert opencti_stix2.pick_aliases(stix_object) == "amitt_alias" 96 | stix_object["x_mitre_aliases"] = "mitre_alias" 97 | assert opencti_stix2.pick_aliases(stix_object) == "mitre_alias" 98 | stix_object["x_opencti_aliases"] = "opencti_alias" 99 | assert opencti_stix2.pick_aliases(stix_object) == "opencti_alias" 100 | 101 | 102 | def test_import_bundle_from_file(opencti_stix2: OpenCTIStix2, caplog) -> None: 103 | opencti_stix2.import_bundle_from_file("foo.txt") 104 | for record in caplog.records: 105 | assert record.levelname == "ERROR" 106 | assert "The bundle file does not exists" in caplog.text 107 | -------------------------------------------------------------------------------- /tests/01-unit/utils/test_opencti_stix2_splitter.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | from stix2 import Report 5 | 6 | from pycti.utils.opencti_stix2_splitter import OpenCTIStix2Splitter 7 | 8 | 9 | def test_split_bundle(): 10 | stix_splitter = OpenCTIStix2Splitter() 11 | with open("./tests/data/enterprise-attack.json") as file: 12 | content = file.read() 13 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 14 | assert expectations == 7016 15 | 16 | 17 | def test_split_test_bundle(): 18 | stix_splitter = OpenCTIStix2Splitter() 19 | with open("./tests/data/DATA-TEST-STIX2_v2.json") as file: 20 | content = file.read() 21 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 22 | assert expectations == 59 23 | base_bundles = json.loads(content)["objects"] 24 | for base in base_bundles: 25 | found = None 26 | for bundle in bundles: 27 | json_bundle = json.loads(bundle) 28 | object_json = json_bundle["objects"][0] 29 | if object_json["id"] == base["id"]: 30 | found = object_json 31 | break 32 | assert found is not None, "Every object of the bundle must be available" 33 | del found["nb_deps"] 34 | assert json.dumps(base) == json.dumps( 35 | found 36 | ), "Splitter must not have change the content" 37 | 38 | 39 | def test_split_mono_entity_bundle(): 40 | stix_splitter = OpenCTIStix2Splitter() 41 | with open("./tests/data/mono-bundle-entity.json") as file: 42 | content = file.read() 43 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 44 | assert expectations == 1 45 | json_bundle = json.loads(bundles[0])["objects"][0] 46 | assert json_bundle["created_by_ref"] == "fa42a846-8d90-4e51-bc29-71d5b4802168" 47 | # Split with cleanup_inconsistent_bundle 48 | stix_splitter = OpenCTIStix2Splitter() 49 | expectations, bundles = stix_splitter.split_bundle_with_expectations( 50 | bundle=content, cleanup_inconsistent_bundle=True 51 | ) 52 | assert expectations == 1 53 | json_bundle = json.loads(bundles[0])["objects"][0] 54 | assert json_bundle["created_by_ref"] is None 55 | 56 | 57 | def test_split_mono_relationship_bundle(): 58 | stix_splitter = OpenCTIStix2Splitter() 59 | with open("./tests/data/mono-bundle-relationship.json") as file: 60 | content = file.read() 61 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 62 | assert expectations == 1 63 | # Split with cleanup_inconsistent_bundle 64 | stix_splitter = OpenCTIStix2Splitter() 65 | expectations, bundles = stix_splitter.split_bundle_with_expectations( 66 | bundle=content, cleanup_inconsistent_bundle=True 67 | ) 68 | assert expectations == 0 69 | 70 | 71 | def test_split_capec_bundle(): 72 | stix_splitter = OpenCTIStix2Splitter() 73 | with open("./tests/data/mitre_att_capec.json") as file: 74 | content = file.read() 75 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 76 | assert expectations == 2610 77 | 78 | 79 | def test_split_internal_ids_bundle(): 80 | stix_splitter = OpenCTIStix2Splitter() 81 | with open("./tests/data/bundle_with_internal_ids.json") as file: 82 | content = file.read() 83 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 84 | assert expectations == 4 85 | # Split with cleanup_inconsistent_bundle 86 | stix_splitter = OpenCTIStix2Splitter() 87 | expectations, bundles = stix_splitter.split_bundle_with_expectations( 88 | bundle=content, cleanup_inconsistent_bundle=True 89 | ) 90 | assert expectations == 4 91 | for bundle in bundles: 92 | json_bundle = json.loads(bundle) 93 | object_json = json_bundle["objects"][0] 94 | if object_json["id"] == "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea04": 95 | assert ( 96 | object_json["created_by_ref"] == "ced3e53e-9663-4c96-9c60-07d2e778d931" 97 | ) 98 | 99 | 100 | def test_split_missing_refs_bundle(): 101 | stix_splitter = OpenCTIStix2Splitter() 102 | with open("./tests/data/missing_refs.json") as file: 103 | content = file.read() 104 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 105 | assert expectations == 4 106 | # Split with cleanup_inconsistent_bundle 107 | stix_splitter = OpenCTIStix2Splitter() 108 | expectations, bundles = stix_splitter.split_bundle_with_expectations( 109 | bundle=content, cleanup_inconsistent_bundle=True 110 | ) 111 | assert expectations == 3 112 | 113 | 114 | def test_split_cyclic_bundle(): 115 | stix_splitter = OpenCTIStix2Splitter() 116 | with open("./tests/data/cyclic-bundle.json") as file: 117 | content = file.read() 118 | expectations, bundles = stix_splitter.split_bundle_with_expectations(content) 119 | assert expectations == 6 120 | for bundle in bundles: 121 | json_bundle = json.loads(bundle) 122 | object_json = json_bundle["objects"][0] 123 | if object_json["id"] == "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7": 124 | assert ( 125 | len(object_json["external_references"]) == 1 126 | ) # References are duplicated 127 | assert len(object_json["object_refs"]) == 2 # Cleaned cyclic refs 128 | assert len(object_json["object_marking_refs"]) == 1 129 | assert ( 130 | object_json["object_marking_refs"][0] 131 | == "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27" 132 | ) 133 | 134 | 135 | def test_create_bundle(): 136 | stix_splitter = OpenCTIStix2Splitter() 137 | report = Report( 138 | report_types=["campaign"], 139 | name="Bad Cybercrime", 140 | published="2016-04-06T20:03:00.000Z", 141 | object_refs=["indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7"], 142 | ).serialize() 143 | observables = [report] 144 | 145 | bundle = stix_splitter.stix2_create_bundle( 146 | "bundle--" + str(uuid.uuid4()), 147 | 0, 148 | observables, 149 | use_json=False, 150 | event_version=None, 151 | ) 152 | 153 | for key in ["type", "id", "spec_version", "objects", "x_opencti_seq"]: 154 | assert key in bundle 155 | assert len(bundle.keys()) == 5 156 | 157 | bundle = stix_splitter.stix2_create_bundle( 158 | "bundle--" + str(uuid.uuid4()), 0, observables, use_json=False, event_version=1 159 | ) 160 | for key in [ 161 | "type", 162 | "id", 163 | "spec_version", 164 | "objects", 165 | "x_opencti_event_version", 166 | "x_opencti_seq", 167 | ]: 168 | assert key in bundle 169 | assert len(bundle.keys()) == 6 170 | -------------------------------------------------------------------------------- /tests/02-integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/02-integration/__init__.py -------------------------------------------------------------------------------- /tests/02-integration/connector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/02-integration/connector/__init__.py -------------------------------------------------------------------------------- /tests/02-integration/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/02-integration/entities/__init__.py -------------------------------------------------------------------------------- /tests/02-integration/entities/test_entity_crud.py: -------------------------------------------------------------------------------- 1 | from pytest_cases import fixture, parametrize_with_cases 2 | 3 | from tests.cases.entities import EntityTestCases 4 | from tests.utils import compare_values, is_filters_empty 5 | 6 | 7 | @fixture 8 | @parametrize_with_cases("entity", cases=EntityTestCases) 9 | def entity_class(entity): 10 | entity.setup() 11 | yield entity 12 | entity.teardown() 13 | 14 | 15 | def test_entity_create(entity_class): 16 | class_data = entity_class.data() 17 | test_indicator = entity_class.own_class().create(**class_data) 18 | try: 19 | assert test_indicator is not None, "Response is NoneType" 20 | assert "id" in test_indicator, "No ID on object" 21 | finally: 22 | if test_indicator and "id" in test_indicator: 23 | entity_class.base_class().delete(id=test_indicator["id"]) 24 | 25 | 26 | def test_read(entity_class): 27 | class_data = entity_class.data() 28 | test_indicator = entity_class.own_class().create(**class_data) 29 | try: 30 | assert test_indicator is not None, "Response is NoneType" 31 | assert "id" in test_indicator, "No ID on object" 32 | assert "standard_id" in test_indicator, "No standard_id (STIX ID) on object" 33 | test_indicator = entity_class.own_class().read(id=test_indicator["id"]) 34 | compare_values( 35 | class_data, 36 | test_indicator, 37 | entity_class.get_compare_exception_keys(), 38 | ) 39 | 40 | finally: 41 | if test_indicator and "id" in test_indicator: 42 | entity_class.base_class().delete(id=test_indicator["id"]) 43 | 44 | 45 | def test_update(entity_class): 46 | class_data = entity_class.data() 47 | test_indicator = entity_class.own_class().create(**class_data) 48 | try: 49 | assert test_indicator is not None, "Response is NoneType" 50 | assert "id" in test_indicator, "No ID on object" 51 | 52 | if len(entity_class.update_data()) > 0: 53 | function_present = getattr(entity_class.own_class(), "update_field", None) 54 | if function_present: 55 | for update_field, update_value in entity_class.update_data().items(): 56 | class_data[update_field] = update_value 57 | input = [{"key": update_field, "value": update_value}] 58 | result = entity_class.own_class().update_field( 59 | id=test_indicator["id"], input=input 60 | ) 61 | else: 62 | for update_field, update_value in entity_class.update_data().items(): 63 | class_data[update_field] = update_value 64 | class_data["update"] = True 65 | result = entity_class.own_class().create(**class_data) 66 | 67 | result = entity_class.own_class().read(id=result["id"]) 68 | assert ( 69 | result["id"] == test_indicator["id"] 70 | ), "Updated SDO does not match old ID" 71 | compare_values( 72 | class_data, result, entity_class.get_compare_exception_keys() 73 | ) 74 | else: 75 | result = test_indicator 76 | 77 | finally: 78 | if test_indicator and "id" in test_indicator: 79 | entity_class.base_class().delete(id=result["id"]) 80 | 81 | 82 | def test_delete(entity_class): 83 | class_data = entity_class.data() 84 | test_indicator = entity_class.own_class().create(**class_data) 85 | try: 86 | assert test_indicator is not None, "Response is NoneType" 87 | assert "id" in test_indicator, "No ID on object" 88 | result = entity_class.base_class().delete(id=test_indicator["id"]) 89 | assert result is None, f"Delete returned value '{result}'" 90 | result = entity_class.own_class().read(id=test_indicator["id"]) 91 | assert result is None, f"Read returned value '{result}' after delete" 92 | except AssertionError: 93 | if test_indicator and "id" in test_indicator: 94 | entity_class.base_class().delete(id=test_indicator["id"]) 95 | 96 | 97 | def test_filter(entity_class): 98 | if is_filters_empty(entity_class.get_filter()): 99 | return 100 | 101 | class_data = entity_class.data() 102 | test_indicator = entity_class.own_class().create(**class_data) 103 | try: 104 | assert test_indicator is not None, "Response is NoneType" 105 | assert "id" in test_indicator, "No ID on object" 106 | test_indicator = entity_class.own_class().read( 107 | filters=entity_class.get_filter() 108 | ) 109 | compare_values( 110 | class_data, 111 | test_indicator, 112 | entity_class.get_compare_exception_keys(), 113 | ) 114 | finally: 115 | if test_indicator and "id" in test_indicator: 116 | entity_class.base_class().delete(id=test_indicator["id"]) 117 | 118 | 119 | def test_search(entity_class): 120 | if not entity_class.get_search(): 121 | return 122 | 123 | class_data = entity_class.data() 124 | test_indicator = entity_class.own_class().create(**class_data) 125 | try: 126 | assert test_indicator is not None, "Response is NoneType" 127 | assert "id" in test_indicator, "No ID on object" 128 | test_indicator = entity_class.own_class().read(search=entity_class.get_search()) 129 | compare_values( 130 | class_data, 131 | test_indicator, 132 | entity_class.get_compare_exception_keys(), 133 | ) 134 | finally: 135 | if test_indicator and "id" in test_indicator: 136 | entity_class.base_class().delete(id=test_indicator["id"]) 137 | 138 | 139 | def test_relation(entity_class): 140 | if not entity_class.relation_test(): 141 | return 142 | class_data = entity_class.data() 143 | class_data2 = entity_class.relation_test() 144 | test_indicator = entity_class.own_class().create(**class_data) 145 | test_indicator2 = entity_class.own_class().create(**class_data2) 146 | try: 147 | assert test_indicator is not None, "Response is NoneType" 148 | assert "id" in test_indicator, "No ID on object" 149 | entity_class.own_class().add_stix_object_or_stix_relationship( 150 | id=test_indicator["id"], 151 | stixObjectOrStixRelationshipId=test_indicator2["id"], 152 | ) 153 | result = entity_class.own_class().read(id=test_indicator["id"]) 154 | assert result["objectsIds"][0] == test_indicator2["id"] 155 | entity_class.own_class().remove_stix_object_or_stix_relationship( 156 | id=test_indicator["id"], 157 | stixObjectOrStixRelationshipId=test_indicator2["id"], 158 | ) 159 | result = entity_class.own_class().read(id=test_indicator["id"]) 160 | assert len(result["objectsIds"]) == 0 161 | finally: 162 | if test_indicator and "id" in test_indicator: 163 | entity_class.base_class().delete(id=test_indicator["id"]) 164 | if test_indicator2 and "id" in test_indicator2: 165 | entity_class.base_class().delete(id=test_indicator2["id"]) 166 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_entity_filter.py: -------------------------------------------------------------------------------- 1 | from pytest_cases import fixture, parametrize_with_cases 2 | 3 | from tests.cases.entities import EntityTestCases 4 | from tests.utils import compare_values, is_filters_empty 5 | 6 | 7 | @fixture 8 | @parametrize_with_cases("entity", cases=EntityTestCases) 9 | def entity_class(entity): 10 | entity.setup() 11 | yield entity 12 | entity.teardown() 13 | 14 | 15 | def test_filter(entity_class): 16 | if is_filters_empty(entity_class.get_filter()): 17 | return 18 | 19 | class_data = entity_class.data() 20 | test_indicator = entity_class.own_class().create(**class_data) 21 | assert test_indicator is not None, "Response is NoneType" 22 | assert "id" in test_indicator, "No ID on object" 23 | test_indicator = entity_class.own_class().read(filters=entity_class.get_filter()) 24 | compare_values( 25 | class_data, 26 | test_indicator, 27 | entity_class.get_compare_exception_keys(), 28 | ) 29 | 30 | entity_class.base_class().delete(id=test_indicator["id"]) 31 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_group.py: -------------------------------------------------------------------------------- 1 | from tests.cases.entities import GroupTest, MarkingDefinitionTest, RoleTest, UserTest 2 | 3 | 4 | def test_group_roles(api_client): 5 | group_data = GroupTest(api_client).data() 6 | role_data = RoleTest(api_client).data() 7 | test_group = api_client.group.create(**group_data) 8 | test_role = api_client.role.create(**role_data) 9 | assert test_group is not None, "Create group response is NoneType" 10 | assert test_role is not None, "Create role response is NoneType" 11 | 12 | api_client.group.add_role(id=test_group["id"], role_id=test_role["id"]) 13 | result = api_client.group.read(id=test_group["id"]) 14 | assert result["rolesIds"][0] == test_role["id"] 15 | 16 | api_client.group.delete_role(id=test_group["id"], role_id=test_role["id"]) 17 | result = api_client.group.read(id=test_group["id"]) 18 | assert len(result["rolesIds"]) == 0 19 | 20 | api_client.group.delete(id=test_group["id"]) 21 | api_client.role.delete(id=test_role["id"]) 22 | 23 | 24 | def test_group_default_markings(api_client): 25 | group_data = GroupTest(api_client).data() 26 | marking_test = MarkingDefinitionTest(api_client) 27 | marking_data = marking_test.data() 28 | test_group = api_client.group.create(**group_data) 29 | test_marking = marking_test.own_class().create(**marking_data) 30 | assert test_group is not None, "Create group response is NoneType" 31 | assert test_marking is not None, "Create marking response is NoneType" 32 | 33 | api_client.group.edit_default_marking( 34 | id=test_group["id"], marking_ids=[test_marking["id"]] 35 | ) 36 | result = api_client.group.read(id=test_group["id"]) 37 | assert result["default_marking"][0]["values"][0]["id"] == test_marking["id"] 38 | 39 | api_client.group.edit_default_marking(id=test_group["id"], marking_ids=[]) 40 | result = api_client.group.read(id=test_group["id"]) 41 | assert len(result["default_marking"][0]["values"]) == 0 42 | 43 | api_client.group.delete(id=test_group["id"]) 44 | marking_test.base_class().delete(id=test_marking["id"]) 45 | 46 | 47 | def test_group_membership(api_client): 48 | group_test = GroupTest(api_client) 49 | user_test = UserTest(api_client) 50 | 51 | test_group = group_test.own_class().create(**group_test.data()) 52 | test_user = user_test.own_class().create(**user_test.data()) 53 | 54 | try: 55 | assert test_group is not None, "Create group response is NoneType" 56 | assert test_user is not None, "Create user response is NoneType" 57 | 58 | group_test.own_class().add_member(id=test_group["id"], user_id=test_user["id"]) 59 | result = group_test.own_class().read(id=test_group["id"]) 60 | assert result["membersIds"][0] == test_user["id"] 61 | 62 | group_test.own_class().delete_member( 63 | id=test_group["id"], user_id=test_user["id"] 64 | ) 65 | result = group_test.own_class().read(id=test_group["id"]) 66 | assert len(result["membersIds"]) == 0 67 | finally: 68 | group_test.base_class().delete(id=test_group["id"]) 69 | user_test.base_class().delete(id=test_user["id"]) 70 | 71 | 72 | def test_group_allowed_markings(api_client): 73 | group_data = GroupTest(api_client).data() 74 | marking_test = MarkingDefinitionTest(api_client) 75 | marking_data = marking_test.data() 76 | test_group = api_client.group.create(**group_data) 77 | test_marking = marking_test.own_class().create(**marking_data) 78 | assert test_group is not None, "Create group response is NoneType" 79 | assert test_marking is not None, "Create marking response is NoneType" 80 | 81 | api_client.group.add_allowed_marking( 82 | id=test_group["id"], marking_id=test_marking["id"] 83 | ) 84 | result = api_client.group.read(id=test_group["id"]) 85 | assert result["allowed_marking"][0]["id"] == test_marking["id"] 86 | 87 | api_client.group.delete_allowed_marking( 88 | id=test_group["id"], marking_id=test_marking["id"] 89 | ) 90 | result = api_client.group.read(id=test_group["id"]) 91 | assert len(result["allowed_marking"]) == 0 92 | 93 | api_client.group.delete(id=test_group["id"]) 94 | marking_test.base_class().delete(id=test_marking["id"]) 95 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_indicators.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | 5 | # Commented out since OpenCTI requires at least 100 indicators 6 | # for this test to work 7 | # 8 | # def test_get_100_indicators_with_pagination(api_client): 9 | # # Get 100 Indicators using the pagination 10 | # custom_attributes = """ 11 | # id 12 | # revoked 13 | # created 14 | # """ 15 | # 16 | # final_indicators = [] 17 | # data = api_client.indicator.list( 18 | # first=50, customAttributes=custom_attributes, withPagination=True 19 | # ) 20 | # final_indicators = final_indicators + data["entities"] 21 | # 22 | # assert len(final_indicators) == 50 23 | # 24 | # after = data["pagination"]["endCursor"] 25 | # data = api_client.indicator.list( 26 | # first=50, 27 | # after=after, 28 | # customAttributes=custom_attributes, 29 | # withPagination=True, 30 | # ) 31 | # final_indicators = final_indicators + data["entities"] 32 | # 33 | # assert len(final_indicators) == 100 34 | 35 | 36 | def test_indicator_stix_marshall(api_client): 37 | with open("tests/data/indicator_stix.json", "r") as content_file: 38 | content = content_file.read() 39 | 40 | json_data = json.loads(content) 41 | 42 | for indic in json_data["objects"]: 43 | imported_indicator = api_client.indicator.import_from_stix2( 44 | stixObject=indic, update=True 45 | ) 46 | assert imported_indicator is not None 47 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_malware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def test_malware_import_with_sample_refs(api_client): 5 | with open("tests/data/basicMalwareWithSample.json", "r") as content_file: 6 | content = content_file.read() 7 | 8 | imported_malware_bundle = api_client.stix2.import_bundle_from_json( 9 | json_data=content 10 | ) 11 | assert imported_malware_bundle is not None 12 | 13 | for importObject in imported_malware_bundle: 14 | if importObject["type"] == "malware": 15 | assert ( 16 | api_client.malware.read(id=importObject["id"]).get("samples") is not [] 17 | ) 18 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_observables.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def test_promote_observable_to_indicator_deprecated(api_client): 5 | # deprecated [>=6.2 & <6.8] 6 | obs1 = api_client.stix_cyber_observable.create( 7 | simple_observable_key="IPv4-Addr.value", simple_observable_value="55.55.55.55" 8 | ) 9 | observable = api_client.stix_cyber_observable.promote_to_indicator( 10 | id=obs1.get("id") 11 | ) 12 | assert observable is not None, "Returned observable is NoneType" 13 | assert observable.get("id") == obs1.get("id") 14 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_role.py: -------------------------------------------------------------------------------- 1 | from tests.cases.entities import RoleTest 2 | 3 | 4 | def test_role_capabilities(api_client): 5 | role_test = RoleTest(api_client) 6 | role_data = role_test.data() 7 | test_role = role_test.own_class().create(**role_data) 8 | assert test_role is not None, "Create role response is NoneType" 9 | 10 | capability_id = api_client.capability.list()[0]["id"] 11 | 12 | role_test.own_class().add_capability( 13 | id=test_role["id"], capability_id=capability_id 14 | ) 15 | result = role_test.own_class().read(id=test_role["id"]) 16 | assert capability_id in result["capabilitiesIds"] 17 | 18 | role_test.own_class().delete_capability( 19 | id=test_role["id"], capability_id=capability_id 20 | ) 21 | result = role_test.own_class().read(id=test_role["id"]) 22 | assert capability_id not in result["capabilitiesIds"] 23 | 24 | role_test.base_class().delete(id=test_role["id"]) 25 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_settings.py: -------------------------------------------------------------------------------- 1 | from tests.cases.entities import SettingsTest 2 | 3 | 4 | def test_settings_messages(api_client): 5 | settings_test = SettingsTest(api_client) 6 | settings_test.setup() 7 | try: 8 | result = settings_test.own_class().read(include_messages=True) 9 | id = result["id"] 10 | num_messages = len(result["platform_messages"]) 11 | 12 | # Add message 13 | test_message_data = { 14 | "message": "This is a test message", 15 | "activated": True, 16 | "dismissible": True, 17 | } 18 | result = settings_test.own_class().edit_message(id=id, input=test_message_data) 19 | assert len(result["platform_messages"]) == num_messages + 1 20 | 21 | test_message = result["platform_messages"][-1] 22 | assert test_message["message"] == test_message_data["message"] 23 | 24 | # Update message 25 | result = settings_test.own_class().edit_message( 26 | id=id, 27 | input={ 28 | "id": test_message["id"], 29 | "message": "This is an updated test message", 30 | "activated": True, 31 | "dismissible": False, 32 | }, 33 | ) 34 | assert len(result["platform_messages"]) == num_messages + 1 35 | 36 | updated_message = result["platform_messages"][-1] 37 | assert updated_message["id"] == test_message["id"] 38 | assert updated_message["message"] == "This is an updated test message" 39 | 40 | # Delete message 41 | result = settings_test.own_class().delete_message( 42 | id=id, input=test_message["id"] 43 | ) 44 | assert len(result["platform_messages"]) == num_messages 45 | assert test_message["id"] not in result["platform_messages_ids"] 46 | finally: 47 | settings_test.teardown() 48 | -------------------------------------------------------------------------------- /tests/02-integration/entities/test_user.py: -------------------------------------------------------------------------------- 1 | from tests.cases.entities import GroupTest, IdentityOrganizationTest, UserTest 2 | 3 | 4 | def test_user_membership(api_client): 5 | user_test = UserTest(api_client) 6 | group_test = GroupTest(api_client) 7 | 8 | test_user = user_test.own_class().create(**user_test.data()) 9 | test_group = group_test.own_class().create(**group_test.data()) 10 | 11 | try: 12 | assert test_user is not None, "User create response returned NoneType" 13 | assert test_group is not None, "Group create response returned NoneType" 14 | 15 | user_test.own_class().add_membership( 16 | id=test_user["id"], group_id=test_group["id"] 17 | ) 18 | result = user_test.own_class().read(id=test_user["id"]) 19 | assert test_group["id"] in result["groupsIds"] 20 | 21 | user_test.own_class().delete_membership( 22 | id=test_user["id"], group_id=test_group["id"] 23 | ) 24 | result = user_test.own_class().read(id=test_user["id"]) 25 | assert test_group["id"] not in result["groupsIds"] 26 | finally: 27 | user_test.base_class().delete(id=test_user["id"]) 28 | group_test.base_class().delete(id=test_group["id"]) 29 | 30 | 31 | def test_user_organization(api_client): 32 | user_test = UserTest(api_client) 33 | org_test = IdentityOrganizationTest(api_client) 34 | 35 | test_user = user_test.own_class().create(**user_test.data()) 36 | test_org = org_test.own_class().create(**org_test.data()) 37 | 38 | try: 39 | assert test_user is not None, "User create response returned NoneType" 40 | assert test_org is not None, "Organization create response returned NoneType" 41 | 42 | user_test.own_class().add_organization( 43 | id=test_user["id"], organization_id=test_org["id"] 44 | ) 45 | result = user_test.own_class().read(id=test_user["id"]) 46 | assert test_org["id"] in result["objectOrganizationIds"] 47 | 48 | user_test.own_class().delete_organization( 49 | id=test_user["id"], organization_id=test_org["id"] 50 | ) 51 | result = user_test.own_class().read(id=test_user["id"]) 52 | assert test_org["id"] not in result["objectOrganizationIds"] 53 | finally: 54 | user_test.base_class().delete(id=test_user["id"]) 55 | org_test.base_class().delete(id=test_org["id"]) 56 | 57 | 58 | def test_user_token_renew(api_client): 59 | user_test = UserTest(api_client) 60 | test_user = user_test.own_class().create(**user_test.data(), include_token=True) 61 | try: 62 | assert test_user is not None, "User create response returned NoneType" 63 | 64 | old_token = test_user["api_token"] 65 | result = user_test.own_class().token_renew( 66 | id=test_user["id"], include_token=True 67 | ) 68 | assert old_token != result["api_token"] 69 | finally: 70 | user_test.own_class().delete(id=test_user["id"]) 71 | -------------------------------------------------------------------------------- /tests/02-integration/utils/test_stix_crud.py: -------------------------------------------------------------------------------- 1 | from pytest_cases import fixture, parametrize_with_cases 2 | from stix2 import Bundle 3 | 4 | from tests.cases.entities import EntityTestCases 5 | 6 | 7 | @fixture 8 | @parametrize_with_cases("entity", cases=EntityTestCases) 9 | def entity_class(entity): 10 | entity.setup() 11 | yield entity 12 | entity.teardown() 13 | 14 | 15 | def test_entity_create(entity_class, api_stix, opencti_splitter): 16 | class_data = entity_class.data() 17 | stix_class = entity_class.stix_class() 18 | if stix_class is None: 19 | return 20 | 21 | stix_object = stix_class(**class_data) 22 | bundle = Bundle(objects=[stix_object]).serialize() 23 | split_bundle = opencti_splitter.split_bundle(bundle, True, None)[0] 24 | bundles_sent = api_stix.import_bundle_from_json(split_bundle, False, None, None) 25 | 26 | assert len(bundles_sent) == 1 27 | assert bundles_sent[0]["id"] == stix_object["id"] 28 | assert bundles_sent[0]["type"] == stix_object["type"] 29 | 30 | entity_class.base_class().delete(id=stix_object["id"]) 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/__init__.py -------------------------------------------------------------------------------- /tests/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCTI-Platform/client-python/fee6efbf821a1eb98af5fd09fc039598f52a79ae/tests/cases/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_cases import fixture 3 | 4 | from pycti import ( 5 | OpenCTIApiClient, 6 | OpenCTIApiConnector, 7 | OpenCTIApiWork, 8 | OpenCTIStix2, 9 | OpenCTIStix2Splitter, 10 | ) 11 | 12 | 13 | @fixture(scope="session") 14 | def api_client(pytestconfig): 15 | if pytestconfig.getoption("--drone"): 16 | return OpenCTIApiClient( 17 | "http://opencti:4000", 18 | "bfa014e0-e02e-4aa6-a42b-603b19dcf159", 19 | ssl_verify=False, 20 | ) 21 | else: 22 | return OpenCTIApiClient( 23 | "http://localhost:4000", 24 | "d434ce02-e58e-4cac-8b4c-42bf16748e84", 25 | ssl_verify=False, 26 | ) 27 | 28 | 29 | @fixture(scope="session") 30 | def api_connector(api_client): 31 | return OpenCTIApiConnector(api_client) 32 | 33 | 34 | @fixture(scope="session") 35 | def api_work(api_client): 36 | return OpenCTIApiWork(api_client) 37 | 38 | 39 | @fixture(scope="session") 40 | def api_stix(api_client): 41 | return OpenCTIStix2(api_client) 42 | 43 | 44 | @fixture(scope="session") 45 | def opencti_splitter(): 46 | return OpenCTIStix2Splitter() 47 | 48 | 49 | def pytest_addoption(parser): 50 | parser.addoption( 51 | "--connectors", action="store_true", default=False, help="run connector tests" 52 | ) 53 | parser.addoption( 54 | "--drone", 55 | action="store_true", 56 | default=False, 57 | help="run connector tests in drone environment", 58 | ) 59 | 60 | 61 | def pytest_configure(config): 62 | config.addinivalue_line("markers", "connectors: mark connector tests to run") 63 | 64 | 65 | def pytest_collection_modifyitems(config, items): 66 | if config.getoption("--connectors"): 67 | return 68 | skip_connectors = pytest.mark.skip(reason="need --connectors to run") 69 | for item in items: 70 | if "connectors" in item.keywords: 71 | item.add_marker(skip_connectors) 72 | -------------------------------------------------------------------------------- /tests/data/basicMalwareWithSample.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--59860f2d-2245-4901-b40e-46b51856bde4", 4 | "objects": [ 5 | { 6 | "id": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce0", 7 | "spec_version": "2.1", 8 | "revoked": false, 9 | "confidence": 100, 10 | "created": "2024-03-13T09:56:18.259Z", 11 | "modified": "2024-03-13T09:56:18.259Z", 12 | "name": "BasicMalware", 13 | "is_family": false, 14 | "x_opencti_id": "75f2a512-fcc6-4cbc-a2ef-52ca9c57df46", 15 | "x_opencti_type": "Malware", 16 | "type": "malware", 17 | "sample_refs": [ 18 | "file--9fb3f45e-ffce-5525-95a5-906a6695aa85" 19 | ] 20 | }, 21 | { 22 | "id": "file--9fb3f45e-ffce-5525-95a5-906a6695aa85", 23 | "spec_version": "2.1", 24 | "x_opencti_score": 50, 25 | "name": "basicFile", 26 | "x_opencti_id": "8c555399-ad2c-4862-8113-0889a3c14116", 27 | "x_opencti_type": "StixFile", 28 | "type": "file" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /tests/data/bundle_ids_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--8c939929-688f-4a72-badb-3dd1bd6af0fa", 4 | "objects": [ 5 | { 6 | "id": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", 7 | "spec_version": "2.1", 8 | "revoked": false, 9 | "confidence": 100, 10 | "created": "2024-03-13T09:56:18.259Z", 11 | "modified": "2024-03-13T09:56:18.259Z", 12 | "name": "BasicMalware", 13 | "is_family": false, 14 | "x_opencti_id": "75f2a512-fcc6-4cbc-a2ef-52ca9c57df46", 15 | "x_opencti_type": "Malware", 16 | "type": "malware" 17 | }, 18 | { 19 | "id": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", 20 | "type": "identity", 21 | "spec_version": "2.1", 22 | "name": "ANSSI", 23 | "identity_class": "organization", 24 | "labels": ["identity"], 25 | "created": "2020-02-23T23:40:53.575Z", 26 | "modified": "2020-02-27T08:45:39.351Z", 27 | "x_opencti_organization_type": "CSIRT" 28 | }, 29 | { 30 | "id": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27", 31 | "type": "marking-definition", 32 | "spec_version": "2.1", 33 | "definition_type": "TLP", 34 | "definition": { 35 | "TLP": "TLP:TEST" 36 | }, 37 | "created": "2020-02-25T09:02:29.040Z", 38 | "modified": "2020-02-25T09:02:29.040Z", 39 | "created_by_ref": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27" 40 | }, 41 | { 42 | "id": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", 43 | "type": "report", 44 | "spec_version": "2.1", 45 | "name": "A demo report for testing purposes", 46 | "labels": ["report"], 47 | "description": "Report for testing purposes (random data).", 48 | "published": "2020-03-01T14:02:48.111Z", 49 | "created": "2020-03-01T14:02:55.327Z", 50 | "modified": "2020-03-01T14:09:48.078Z", 51 | "report_types": ["threat-report"], 52 | "x_opencti_report_status": 2, 53 | "confidence": 3, 54 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", 55 | "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"], 56 | "object_refs": [ 57 | "observed-data--7d258c31-9a26-4543-aecb-2abc5ed366be", 58 | "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5" 59 | ] 60 | }, 61 | { 62 | "id": "relationship--ba52fced-422a-4bee-816a-85aa21c9eacc", 63 | "type": "relationship", 64 | "spec_version": "2.1", 65 | "relationship_type": "related-to", 66 | "source_ref": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", 67 | "target_ref": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", 68 | "created": "2020-03-01T14:07:14.316Z", 69 | "modified": "2020-03-01T14:07:14.316Z", 70 | "start_time": "1900-01-01T00:00:00.000Z", 71 | "stop_time": "1900-01-01T00:00:00.000Z", 72 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", 73 | "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"] 74 | }, 75 | { 76 | "type": "sighting", 77 | "spec_version": "2.1", 78 | "id": "sighting--ee20065d-2555-424f-ad9e-0f8428623c75", 79 | "created": "2016-08-06T20:08:31.000Z", 80 | "modified": "2016-09-06T20:08:31.000Z", 81 | "sighting_of_ref": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", 82 | "where_sighted_refs": ["identity--7b82b010-b1c0-4dae-981f-7756374a17da"], 83 | "first_seen": "2016-08-06T20:08:31.000Z", 84 | "last_seen": "2016-08-07T20:08:31.000Z", 85 | "count": 12, 86 | "x_opencti_negative": true 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /tests/data/bundle_with_internal_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--8c939929-688f-4a72-badb-3dd1bd6af0fa", 4 | "objects": [ 5 | { 6 | "id": "identity--67ffc076-1fba-5164-9df5-b7523092931e", 7 | "spec_version": "2.1", 8 | "type": "identity", 9 | "extensions": { 10 | "extension-definition--ea279b3e-5c71-4632-ac08-831c66a786ba": { 11 | "extension_type": "property-extension", 12 | "id": "ced3e53e-9663-4c96-9c60-07d2e778d931", 13 | "type": "Organization", 14 | "created_at": "2024-09-26T17:36:32.107Z", 15 | "updated_at": "2024-09-26T17:36:32.107Z", 16 | "is_inferred": false, 17 | "creator_ids": [ 18 | "88ec0c6a-13ce-5e39-b486-354fe4a7084f" 19 | ] 20 | } 21 | }, 22 | "created": "2024-09-26T17:36:32.107Z", 23 | "modified": "2024-09-26T17:36:32.107Z", 24 | "revoked": false, 25 | "confidence": 100, 26 | "lang": "en", 27 | "name": "JRI", 28 | "identity_class": "organization" 29 | }, 30 | { 31 | "id": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 32 | "type": "malware", 33 | "name": "malware 01", 34 | "created": "2019-09-30T16:38:26.000Z", 35 | "modified": "2020-01-14T14:01:48.288Z", 36 | "x_opencti_id": "82316ffd-a0ec-4519-a454-6566f8f5676c" 37 | }, 38 | { 39 | "id": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3d", 40 | "type": "malware", 41 | "name": "malware 02", 42 | "created": "2019-09-30T16:38:26.000Z", 43 | "modified": "2020-01-14T14:01:48.288Z" 44 | }, 45 | { 46 | "id": "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea04", 47 | "type": "relationship", 48 | "relationship_type": "part-of", 49 | "created_by_ref": "ced3e53e-9663-4c96-9c60-07d2e778d931", 50 | "source_ref": "82316ffd-a0ec-4519-a454-6566f8f5676c", 51 | "target_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3d" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /tests/data/cyclic-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--8c939929-688f-4a72-badb-3dd1bd6af0fa", 4 | "objects": [ 5 | { 6 | "id": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27", 7 | "type": "marking-definition", 8 | "spec_version": "2.1", 9 | "definition_type": "TLP", 10 | "definition": { 11 | "TLP": "TLP:TEST" 12 | }, 13 | "created": "2020-02-25T09:02:29.040Z", 14 | "modified": "2020-02-25T09:02:29.040Z", 15 | "created_by_ref": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27" 16 | }, 17 | { 18 | "id": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27", 19 | "type": "marking-definition", 20 | "spec_version": "2.1", 21 | "definition_type": "TLP", 22 | "definition": { 23 | "TLP": "TLP:TEST" 24 | }, 25 | "created": "2020-02-25T09:02:29.040Z", 26 | "modified": "2020-02-25T09:02:29.040Z", 27 | "created_by_ref": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27" 28 | }, 29 | { 30 | "id": "x-mitre-asset--0804f037-a3b9-4715-98e1-9f73d19d6945", 31 | "type": "x-mitre-asset", 32 | "name": "Virtual Private Network (VPN) Server" 33 | }, 34 | { 35 | "id": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 36 | "type": "malware", 37 | "spec_version": "2.1", 38 | "name": "Paradise Ransomware", 39 | "labels": ["malware"], 40 | "description": "MalwareHunterTeam discovered a new Paradise Ransomware variant that uses the extension _V.0.0.0.1{paradise@all-ransomware.info}.prt and drops a ransom note named PARADISE_README_paradise@all-ransomware.info.txt.", 41 | "created": "2019-09-30T16:38:26.000Z", 42 | "modified": "2020-01-14T14:01:48.288Z", 43 | "kill_chain_phases": [ 44 | { 45 | "kill_chain_name": "mitre-attack", 46 | "phase_name": "persistence", 47 | "x_opencti_order": 20 48 | } 49 | ] 50 | }, 51 | { 52 | "id": "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea04", 53 | "type": "relationship", 54 | "spec_version": "2.1", 55 | "relationship_type": "part-of", 56 | "description": "Sector Information Technologies Consulting is a subsector of Consulting", 57 | "source_ref": "x-mitre-asset--0804f037-a3b9-4715-98e1-9f73d19d6945", 58 | "target_ref": "identity--5556c4ab-3e5e-4d56-8410-60b29cecbeb6", 59 | "created": "2020-02-23T23:40:53.582Z", 60 | "modified": "2020-02-23T23:40:53.582Z", 61 | "start_time": "1900-05-01T00:00:00.000Z", 62 | "stop_time": "1900-05-01T00:00:00.000Z" 63 | }, 64 | { 65 | "id": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", 66 | "type": "report", 67 | "spec_version": "2.1", 68 | "name": "A demo report for testing purposes", 69 | "labels": ["report"], 70 | "description": "Report for testing purposes (random data).", 71 | "published": "2020-03-01T14:02:48.111Z", 72 | "created": "2020-03-01T14:02:55.327Z", 73 | "modified": "2020-03-01T14:09:48.078Z", 74 | "report_types": ["threat-report"], 75 | "x_opencti_report_status": 2, 76 | "confidence": 3, 77 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17df", 78 | "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"], 79 | "object_refs": [ 80 | "observed-data--7d258c31-9a26-4543-aecb-2abc5ed366be", 81 | "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", 82 | "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd8" 83 | ], 84 | "external_references": [ 85 | { 86 | "source_name": "mitre-attack", 87 | "external_id": "enterprise-attack", 88 | "url": "https://attack.mitre.org/matrices/enterprise" 89 | }, 90 | { 91 | "source_name": "mitre-attack", 92 | "external_id": "enterprise-attack", 93 | "url": "https://attack.mitre.org/matrices/enterprise" 94 | }, 95 | { 96 | "source_name": "mitre-attack", 97 | "external_id": "enterprise-attack", 98 | "url": "https://attack.mitre.org/matrices/enterprise" 99 | } 100 | ] 101 | }, 102 | { 103 | "id": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd8", 104 | "type": "report", 105 | "spec_version": "2.1", 106 | "name": "A cyclic report", 107 | "labels": ["report"], 108 | "description": "Report for testing purposes (random data).", 109 | "published": "2020-03-01T14:02:48.111Z", 110 | "created": "2020-03-01T14:02:55.327Z", 111 | "modified": "2020-03-01T14:09:48.078Z", 112 | "report_types": ["threat-report"], 113 | "x_opencti_report_status": 2, 114 | "confidence": 3, 115 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17df", 116 | "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"], 117 | "object_refs": [ 118 | "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", 119 | "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd9" 120 | ], 121 | "external_references": [ 122 | { 123 | "source_name": "mitre-attack", 124 | "external_id": "enterprise-attack", 125 | "url": "https://attack.mitre.org/matrices/enterprise" 126 | } 127 | ] 128 | }, 129 | { 130 | "id": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd9", 131 | "type": "report", 132 | "spec_version": "2.1", 133 | "name": "A cyclic report", 134 | "labels": ["report"], 135 | "description": "Report for testing purposes (random data).", 136 | "published": "2020-03-01T14:02:48.111Z", 137 | "created": "2020-03-01T14:02:55.327Z", 138 | "modified": "2020-03-01T14:09:48.078Z", 139 | "report_types": ["threat-report"], 140 | "x_opencti_report_status": 2, 141 | "confidence": 3, 142 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17df", 143 | "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"], 144 | "object_refs": [ 145 | "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7" 146 | ] 147 | }, 148 | { 149 | "id": "identity--7b82b010-b1c0-4dae-981f-7756374a17df", 150 | "type": "identity", 151 | "spec_version": "2.1", 152 | "name": "ANSSI", 153 | "identity_class": "organization", 154 | "labels": ["identity"], 155 | "created": "2020-02-23T23:40:53.575Z", 156 | "modified": "2020-02-27T08:45:39.351Z", 157 | "x_opencti_organization_type": "CSIRT", 158 | "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17df" 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /tests/data/external_import_config.yml: -------------------------------------------------------------------------------- 1 | connector: 2 | id: '65175acc-869b-465d-8a07-47ef00b92394' 3 | type: 'EXTERNAL_IMPORT' 4 | name: 'TestExternalImport' 5 | scope: 'identity,vulnerability' 6 | confidence_level: 80 # From 0 (Unknown) to 100 (Fully trusted) 7 | update_existing_data: True 8 | log_level: 'debug' 9 | 10 | test: 11 | interval: 1 12 | -------------------------------------------------------------------------------- /tests/data/indicator_stix.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--3e0921e7-0d87-49c4-8121-ec28db0f535f", 4 | "objects": [ 5 | { 6 | "id": "indicator--39915430-6d24-4f9d-a871-76a264169674", 7 | "type": "indicator", 8 | "spec_version": "2.1", 9 | "name": "103.83.192.6", 10 | "labels": [ 11 | "indicator" 12 | ], 13 | "pattern": "[ipv4-addr:value = '103.83.192.6']", 14 | "valid_from": "2020-04-17T09:50:27.000Z", 15 | "valid_until": "2021-04-17T09:50:27.000Z", 16 | "pattern_type": "stix", 17 | "created": "2020-04-30T06:29:41.232Z", 18 | "modified": "2020-04-30T06:29:41.232Z", 19 | "x_opencti_score": 50, 20 | "x_opencti_id": "40caf0b8-0632-4f62-9ba6-dc10c093a63e", 21 | "created_by_ref": "identity--26926631-96e9-40e9-9ac0-ab93384f0ce7", 22 | "object_marking_refs": [ 23 | "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" 24 | ] 25 | }, 26 | { 27 | "id": "indicator--41be0c61-43b3-4233-aea0-7077a8981c77", 28 | "type": "indicator", 29 | "spec_version": "2.1", 30 | "name": "192.185.46.253", 31 | "labels": [ 32 | "indicator" 33 | ], 34 | "pattern": "[ipv4-addr:value = '192.185.46.253']", 35 | "valid_from": "2020-04-17T09:50:27.000Z", 36 | "valid_until": "2021-04-17T09:50:27.000Z", 37 | "pattern_type": "stix", 38 | "created": "2020-04-30T06:29:41.303Z", 39 | "modified": "2020-04-30T06:29:41.303Z", 40 | "x_opencti_score": 50, 41 | "x_opencti_id": "2962d903-76ad-4092-b4e0-2021a30db0cb", 42 | "created_by_ref": "identity--26926631-96e9-40e9-9ac0-ab93384f0ce7", 43 | "object_marking_refs": [ 44 | "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" 45 | ] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/data/internal_enrichment_config.yml: -------------------------------------------------------------------------------- 1 | connector: 2 | id: '7f175acc-869b-465d-8a07-47ef11b92394' 3 | type: 'INTERNAL_ENRICHMENT' 4 | name: 'SetScore100Enrichment' 5 | scope: 'IPv4' 6 | auto: false # Enable/disable auto-import of file 7 | only_contextual: false # Only extract data related to an entity (a report, a threat actor, etc.) 8 | confidence_level: 15 # From 0 (Unknown) to 100 (Fully trusted) 9 | log_level: 'debug' 10 | -------------------------------------------------------------------------------- /tests/data/internal_import_config.yml: -------------------------------------------------------------------------------- 1 | connector: 2 | id: '7f175acc-811b-465d-8a07-47ef11b92394' 3 | type: 'INTERNAL_IMPORT_FILE' 4 | name: 'ParseFileTest' 5 | scope: 'application/pdf,text/plain' 6 | auto: true # Enable/disable auto-import of file 7 | only_contextual: true # Only extract data related to an entity (a report, a threat actor, etc.) 8 | confidence_level: 15 # From 0 (Unknown) to 100 (Fully trusted) 9 | log_level: 'debug' 10 | -------------------------------------------------------------------------------- /tests/data/internal_import_data.txt: -------------------------------------------------------------------------------- 1 | Test file 2 | -------------------------------------------------------------------------------- /tests/data/missing_refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "bundle", 3 | "id": "bundle--8c939929-688f-4a72-badb-3dd1bd6af0fa", 4 | "objects": [ 5 | { 6 | "id": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 7 | "type": "malware", 8 | "name": "malware 01", 9 | "created": "2019-09-30T16:38:26.000Z", 10 | "modified": "2020-01-14T14:01:48.288Z" 11 | }, 12 | { 13 | "id": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3d", 14 | "type": "malware", 15 | "name": "malware 02", 16 | "created": "2019-09-30T16:38:26.000Z", 17 | "modified": "2020-01-14T14:01:48.288Z" 18 | }, 19 | { 20 | "id": "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea04", 21 | "type": "relationship", 22 | "relationship_type": "part-of", 23 | "created_by_ref": "identity--not-available", 24 | "source_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 25 | "target_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3d" 26 | }, 27 | { 28 | "id": "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea05", 29 | "type": "relationship", 30 | "relationship_type": "part-of", 31 | "created_by_ref": "identity--not-available", 32 | "source_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 33 | "target_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3e" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/data/mono-bundle-entity.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bundle--1f126d0c-356c-43af-88f8-7a3b695dee83", 3 | "objects": [ 4 | { 5 | "id": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", 6 | "created": "2017-06-01T00:00:00.000Z", 7 | "created_by_ref": "fa42a846-8d90-4e51-bc29-71d5b4802168", 8 | "definition": { 9 | "statement": "Copyright 2015-2024, The MITRE Corporation. MITRE ATT&CK and ATT&CK are registered trademarks of The MITRE Corporation." 10 | }, 11 | "definition_type": "statement", 12 | "spec_version": "2.1", 13 | "type": "marking-definition", 14 | "x_mitre_attack_spec_version": "2.1.0", 15 | "x_mitre_domains": ["enterprise-attack"] 16 | } 17 | ], 18 | "spec_version": "2.1", 19 | "type": "bundle", 20 | "x_opencti_seq": 1 21 | } -------------------------------------------------------------------------------- /tests/data/mono-bundle-relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bundle--1f126d0c-356c-43af-88f8-7a3b695dee83", 3 | "objects": [ 4 | { 5 | "id": "relationship--10e8c71d-a1b4-4e35-bca8-2e4a3785ea04", 6 | "type": "relationship", 7 | "relationship_type": "part-of", 8 | "created_by_ref": "identity--not-available", 9 | "source_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3c", 10 | "target_ref": "malware--faa5b705-cf44-4e50-8472-29e5fec43c3d" 11 | } 12 | ], 13 | "spec_version": "2.1", 14 | "type": "bundle", 15 | "x_opencti_seq": 1 16 | } -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from typing import Dict, List 4 | 5 | from dateutil.parser import parse 6 | 7 | from pycti import OpenCTIApiClient, OpenCTIApiConnector, OpenCTIApiWork 8 | 9 | 10 | def is_filters_empty(filters): 11 | if not filters: 12 | return True 13 | if len(filters["filters"]) == 0 and len(filters["filterGroups"]) == 0: 14 | return True 15 | return False 16 | 17 | 18 | def get_incident_start_date(): 19 | return ( 20 | parse("2019-12-01") 21 | .replace(tzinfo=datetime.timezone.utc) 22 | .isoformat(sep="T", timespec="milliseconds") 23 | .replace("+00:00", "Z") 24 | ) 25 | 26 | 27 | def get_incident_end_date(): 28 | return ( 29 | parse("2021-12-01") 30 | .replace(tzinfo=datetime.timezone.utc) 31 | .isoformat(sep="T", timespec="milliseconds") 32 | .replace("+00:00", "Z") 33 | ) 34 | 35 | 36 | def read_marking(api_client: OpenCTIApiClient, tlp_id: int): 37 | return api_client.marking_definition.read(id=tlp_id) 38 | 39 | 40 | def get_connector_id(connector_name: str, api_connector: OpenCTIApiConnector) -> str: 41 | connector_list = api_connector.list() 42 | connector_id = "" 43 | for connector in connector_list: 44 | if connector["name"] == connector_name: 45 | connector_id = connector["id"] 46 | 47 | return connector_id 48 | 49 | 50 | def get_new_work_id(api_client: OpenCTIApiClient, connector_id: str) -> str: 51 | worker = OpenCTIApiWork(api_client) 52 | new_works = worker.get_connector_works(connector_id) 53 | cnt = 0 54 | while len(new_works) == 0: 55 | time.sleep(1) 56 | # wait 20 seconds for new work to be registered 57 | cnt += 1 58 | if cnt > 20: 59 | assert ( 60 | cnt != cnt 61 | ), "Connector hasn't registered new work yet. Elapsed time 20s" 62 | 63 | assert ( 64 | len(new_works) == 1 65 | ), f"Too many jobs were created. Expected 1, Actual: {len(new_works)}" 66 | return new_works[0]["id"] 67 | 68 | 69 | def compare_values(original_data: Dict, retrieved_data: Dict, exception_keys: List): 70 | for key, value in original_data.items(): 71 | # Attributes which aren't present in the final Stix objects 72 | if key in exception_keys: 73 | continue 74 | 75 | assert ( 76 | key in retrieved_data 77 | ), f"Key {key} is not in retrieved_data {retrieved_data}" 78 | 79 | compare_data = retrieved_data.get(key, None) 80 | if isinstance(value, str): 81 | assert ( 82 | value == compare_data 83 | ), f"Key '{key}': '{value}' does't match value '{retrieved_data[key]}' ({retrieved_data}" 84 | elif key == "objects" and isinstance(value, list): 85 | assert isinstance(compare_data, list), f"Key '{key}': is not a list" 86 | original_ids = set() 87 | for elem in value: 88 | if isinstance(elem, dict): 89 | original_ids.add(elem.get("id", None)) 90 | elif isinstance(elem, str): 91 | original_ids.add(elem) 92 | 93 | retrieved_ids = set() 94 | for elem in compare_data: 95 | if isinstance(elem, dict): 96 | retrieved_ids.add(elem.get("id", None)) 97 | elif isinstance(elem, str): 98 | original_ids.add(elem) 99 | 100 | assert ( 101 | original_ids == retrieved_ids 102 | ), f"Key '{key}': '{value}' does't match value '{compare_data}'" 103 | elif isinstance(value, dict): 104 | assert len(value) == len( 105 | compare_data 106 | ), f"Dict '{value}' does not have the same length as '{compare_data}'" 107 | assert ( 108 | value == compare_data 109 | ), f"Dict '{value}' does not have the same content as'{compare_data}'" 110 | --------------------------------------------------------------------------------