├── .flake8 ├── .gitignore ├── Jenkinsfile ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── pytest.ini ├── requirements.txt ├── setup.py ├── test-requirements.txt ├── tests ├── functional │ ├── __init__.py │ ├── project.properties.sample │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── payloads.py │ │ ├── test_ctia_api.py │ │ ├── test_ctr_05_api.py │ │ └── test_ctr_05_int_api.py └── unit │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── assertions.py │ ├── routing │ │ ├── __init__.py │ │ ├── test_resolution.py │ │ └── test_router.py │ ├── test_casebook.py │ ├── test_commands.py │ ├── test_enrich.py │ ├── test_entity.py │ ├── test_event.py │ ├── test_feed.py │ ├── test_feedback.py │ ├── test_inspect.py │ ├── test_intel_entity.py │ ├── test_profile.py │ ├── test_response.py │ └── test_user_mgmt.py │ ├── request │ ├── __init__.py │ ├── test_authorized.py │ ├── test_logged.py │ ├── test_proxied.py │ ├── test_relative.py │ ├── test_response.py │ ├── test_standard.py │ └── test_timed.py │ └── test_client.py ├── threatresponse ├── __init__.py ├── api │ ├── __init__.py │ ├── asset.py │ ├── base.py │ ├── bundle.py │ ├── casebook.py │ ├── commands.py │ ├── enrich.py │ ├── entity.py │ ├── event.py │ ├── feed.py │ ├── incident.py │ ├── indicator.py │ ├── inspect.py │ ├── int.py │ ├── intel.py │ ├── judgement.py │ ├── module_type_patch.py │ ├── profile.py │ ├── response.py │ ├── routing │ │ ├── __init__.py │ │ ├── resolution.py │ │ └── router.py │ ├── sighting.py │ ├── sse.py │ ├── user_mgmt.py │ └── vulnerability.py ├── client.py ├── exceptions.py ├── request │ ├── __init__.py │ ├── authorized.py │ ├── base.py │ ├── logged.py │ ├── proxied.py │ ├── relative.py │ ├── response.py │ ├── standard.py │ └── timed.py ├── urls.py └── version.py └── travis_key.enc /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # consistent with options used while app compiling 3 | ignore=F403,E128,E126,E111,E121,E127,E731,E201,E202,F405,E722,D,E1,E23,F811,F401 4 | max-complexity=28 5 | exclude = venv/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # PyCharm 127 | .idea/ 128 | 129 | # Properties file 130 | project.properties 131 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('softserve-jenkins-library@main') _ 2 | 3 | startPipeline('threatresponse', [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]) 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Cisco Systems, Inc. and/or its affiliates 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | prune tests/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/CiscoSecurity/Threat-Response "Gitter Chat") 2 | [![Travis CI Build Status](https://travis-ci.com/CiscoSecurity/tr-05-api-module.svg?branch=develop)](https://travis-ci.com/CiscoSecurity/tr-05-api-module) 3 | [![PyPi Version](https://img.shields.io/pypi/v/threatresponse.svg)](https://pypi.python.org/pypi/threatresponse) 4 | [![Python Versions](https://img.shields.io/pypi/pyversions/threatresponse.svg)](https://pypi.python.org/pypi/threatresponse) 5 | 6 | # Threat Response API Module 7 | 8 | Python API Module for Threat Response APIs. 9 | 10 | ## Installation 11 | 12 | * Local 13 | 14 | ```bash 15 | pip install --upgrade . 16 | pip show threatresponse 17 | ``` 18 | 19 | * GitHub 20 | 21 | ```bash 22 | pip install --upgrade git+https://github.com/CiscoSecurity/tr-05-api-module.git[@branch_name_or_release_version] 23 | pip show threatresponse 24 | ``` 25 | 26 | * PyPi 27 | 28 | ```bash 29 | pip install --upgrade threatresponse[==release_version] 30 | pip show threatresponse 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```python 36 | from threatresponse import ThreatResponse 37 | 38 | client = ThreatResponse( 39 | client_id='', # required 40 | client_password='', # required 41 | region='', # optional 42 | logger=, # optional 43 | proxy='', # optional 44 | environment='' # optional 45 | ) 46 | ``` 47 | 48 | - `client_id` and `client_password` credentials must be taken from an existing 49 | API client for accessing the Cisco Threat Response APIs. 50 | The official documentation on how to create such a client can be found 51 | [here](https://visibility.amp.cisco.com/#/help/integration). 52 | Make sure to properly set some scopes which will grant the client 53 | different (ideally minimum) privileges. 54 | - `region` must be one of: `''` or `'us'` (default), `'eu'`, `'apjc'`. 55 | Other regions are not supported yet. 56 | - `logger` must be an (already configured) instance of the built-in 57 | `logging.Logger` class (or one of its descendants). 58 | - `timeout` must be a number (`int` or `float`) meaning the default amount of 59 | time (in seconds) to wait for the server to send data before giving up and 60 | raising an exception. Can be overwritten by explicitly specifying `timeout` on 61 | each call to any endpoint. 62 | - `proxy` must be a URL in the format: `http[s]://[username[:password]@]host[:port]`. 63 | - `environment` must be a dict in the format: 64 | { 65 | 'visibility': 'https://www.example.com', 66 | 'private_intel': 'https://www.example.come', 67 | 'global_intel': 'https://www.example.com', 68 | } 69 | By default will be used: 70 | { 71 | 'visibility': 'https://visibility{region}.amp.cisco.com', 72 | 'private_intel': 'https://private.intel{region}.amp.cisco.com', 73 | 'global_intel': 'https://intel{region}.amp.cisco.com', 74 | } 75 | 76 | ### Concrete Usage 77 | 78 | - Inspect 79 | 80 | Inspect allows to find an observable in a concrete string. 81 | ```python 82 | response = client.inspect.inspect({'content': 'example.com'}) 83 | ``` 84 | 85 | - Observe 86 | 87 | Observe returns summary for an observable. 88 | ```python 89 | response = client.enrich.observe.observables( 90 | [{'type': 'sha256', 'value': '8A32950CD96C5EF88F9DCBB66A08F59A7E8D8E5FECCDE9E115FBAA46D9AF88F9'}] 91 | ) 92 | ``` 93 | 94 | - Deliberate 95 | 96 | Deliberate returns judgments based on added modules. 97 | ```python 98 | response = client.enrich.deliberate.observables( 99 | [{'type': 'sha256', 'value': '8A32950CD96C5EF88F9DCBB66A08F59A7E8D8E5FECCDE9E115FBAA46D9AF88F9'}] 100 | ) 101 | ``` 102 | 103 | ### Commands 104 | 105 | For your convenience, we have made some predefined commands that you can use. 106 | 107 | - Verdicts 108 | 109 | Verdicts returns verdicts from all modules if the modules are configured. Accepts multiple observables. 110 | ```python 111 | response = client.commands.verdict( 112 | 'string with observables ("8A32950CD96C5EF88F9DCBB66A08F59A7E8D8E5FECCDE9E115FBAA46D9AF88F9, cisco.com")' 113 | ) 114 | ``` 115 | 116 | - Targets 117 | 118 | Targets returns all available targets if the modules are configured. Accepts multiple observables. 119 | ```python 120 | response = client.commands.targets( 121 | 'string with observables ("8A32950CD96C5EF88F9DCBB66A08F59A7E8D8E5FECCDE9E115FBAA46D9AF88F9, cisco.com")' 122 | ) 123 | ``` 124 | 125 | ### Available Endpoints 126 | 127 | Switch between `.private_intel` and `.global_intel` if necessary. 128 | 129 | # Actor 130 | actor = client.private_intel.actor 131 | Available methods: 132 | - actor.post() 133 | - actor.get() 134 | - actor.put() 135 | - actor.delete() 136 | - actor.external_id() 137 | - actor.search.get() 138 | - actor.search.delete() 139 | - actor.search.count() 140 | - actor.metric.histogram() 141 | - actor.metric.topn() 142 | - actor.metric.cardinality() 143 | 144 | # Asset 145 | asset = client.private_intel.asset 146 | Available methods: 147 | - asset.post() 148 | - asset.get() 149 | - asset.put() 150 | - asset.delete() 151 | - asset.external_id() 152 | - asset.search.get() 153 | - asset.search.delete() 154 | - asset.search.count() 155 | - asset.metric.histogram() 156 | - asset.metric.topn() 157 | - asset.metric.cardinality() 158 | 159 | # Asset mapping 160 | asset_mapping = client.private_intel.asset_mapping 161 | Available methods: 162 | - asset_mapping.post() 163 | - asset_mapping.get() 164 | - asset_mapping.put() 165 | - asset_mapping.delete() 166 | - asset_mapping.expire() 167 | - asset_mapping.external_id() 168 | - asset_mapping.search.get() 169 | - asset_mapping.search.delete() 170 | - asset_mapping.search.count() 171 | - asset_mapping.metric.histogram() 172 | - asset_mapping.metric.topn() 173 | - asset_mapping.metric.cardinality() 174 | 175 | # Asset properties 176 | asset_properties = client.private_intel.asset_properties 177 | Available methods: 178 | - asset_properties.post() 179 | - asset_properties.get() 180 | - asset_properties.put() 181 | - asset_properties.delete() 182 | - asset_properties.expire() 183 | - asset_properties.external_id() 184 | - asset_properties.search.get() 185 | - asset_properties.search.delete() 186 | - asset_properties.search.count() 187 | - asset_properties.metric.histogram() 188 | - asset_properties.metric.topn() 189 | - asset_properties.metric.cardinality() 190 | 191 | # Attack Pattern 192 | attack_pattern = client.private_intel.attack_pattern 193 | Available methods: 194 | - attack_pattern.post() 195 | - attack_pattern.get() 196 | - attack_pattern.put() 197 | - attack_pattern.delete() 198 | - attack_pattern.external_id() 199 | - attack_pattern.search.get() 200 | - attack_pattern.search.delete() 201 | - attack_pattern.search.count() 202 | - attack_pattern.metric.histogram() 203 | - attack_pattern.metric.topn() 204 | - attack_pattern.metric.cardinality() 205 | 206 | # Bulk 207 | bulk = client.private_intel.bulk 208 | Available methods: 209 | - bulk.post() 210 | - bulk.get() 211 | 212 | # Bundle 213 | bundle = client.private_intel.bundle 214 | Available methods: 215 | - bundle.export.post() 216 | - bundle.export.get() 217 | - bundle.import_.post() 218 | 219 | # Campaign 220 | campaign = client.private_intel.campaign 221 | Available methods: 222 | - campaign.post() 223 | - campaign.get() 224 | - campaign.put() 225 | - campaign.delete() 226 | - campaign.external_id() 227 | - campaign.search.get() 228 | - campaign.search.delete() 229 | - campaign.search.count() 230 | - campaign.metric.histogram() 231 | - campaign.metric.topn() 232 | - campaign.metric.cardinality() 233 | 234 | # Casebook 235 | casebook = client.private_intel.casebook 236 | Available methods: 237 | - casebook.post() 238 | - casebook.get() 239 | - casebook.put() 240 | - casebook.delete() 241 | - casebook.external_id() 242 | - casebook.observables() 243 | - casebook.texts() 244 | - casebook.bundle() 245 | - casebook.patch() 246 | - casebook.search.get() 247 | - casebook.search.delete() 248 | - casebook.search.count() 249 | - casebook.metric.histogram() 250 | - casebook.metric.topn() 251 | - casebook.metric.cardinality() 252 | 253 | # COA 254 | coa = client.private_intel.coa 255 | Available methods: 256 | - coa.post() 257 | - coa.get() 258 | - coa.put() 259 | - coa.delete() 260 | - coa.external_id() 261 | - coa.search.get() 262 | - coa.search.delete() 263 | - coa.search.count() 264 | - coa.metric.histogram() 265 | - coa.metric.topn() 266 | - coa.metric.cardinality() 267 | 268 | # DataTable 269 | data_table = client.private_intel.data_table 270 | Available methods: 271 | - data_table.post() 272 | - data_table.get() 273 | - data_table.delete() 274 | - data_table.external_id() 275 | 276 | # Enrich 277 | enrich = client.enrich 278 | Available methods: 279 | - enrich.health() 280 | - enrich.health(_id) 281 | - enrich.deliberate.observables() 282 | - enrich.deliberate.sighting() 283 | - enrich.deliberate.sighting_ref() 284 | - enrich.observe.observables() 285 | - enrich.observe.sighting() 286 | - enrich.observe.sighting_ref() 287 | - enrich.refer.observables() 288 | - enrich.refer.sighting() 289 | - enrich.refer.sighting_ref() 290 | 291 | # Event 292 | event = client.private_intel.event 293 | Available methods: 294 | - event.history() 295 | - event.get() 296 | - event.delete() 297 | - event.search.get() 298 | - event.search.delete() 299 | - event.search.count() 300 | 301 | # Feed 302 | feed = client.private_intel.feed 303 | Available methods: 304 | - feed.view.txt() 305 | - feed.view() 306 | - feed.post() 307 | - feed.put() 308 | - feed.get() 309 | - feed.delete() 310 | - feed.external_id() 311 | - feed.search.get() 312 | - feed.search.delete() 313 | - feed.search.count() 314 | 315 | # Feedback 316 | feedback = client.private_intel.feedback 317 | Available methods: 318 | - feedback.post() 319 | - feedback.get() 320 | - feedback.delete() 321 | - feedback.external_id() 322 | - feedback.get(_id) 323 | 324 | # GraphQL 325 | graph = client.private_intel.graphql 326 | Available methods: 327 | - graphql.post() 328 | 329 | # Identity Assertion 330 | identity_assertion = client.private_intel.identity_assertion 331 | Available methods: 332 | - identity_assertion.post() 333 | - identity_assertion.get() 334 | - identity_assertion.put() 335 | - identity_assertion.delete() 336 | - identity_assertion.external_id() 337 | - identity_assertion.search.get() 338 | - identity_assertion.search.delete() 339 | - identity_assertion.search.count() 340 | - identity_assertion.metric.histogram() 341 | - identity_assertion.metric.topn() 342 | - identity_assertion.metric.cardinality() 343 | 344 | # Incident 345 | incident = client.private_intel.incident 346 | Available methods: 347 | - incident.post() 348 | - incident.get() 349 | - incident.put() 350 | - incident.delete() 351 | - incident.external_id() 352 | - incident.link() 353 | - incident.status() 354 | - incident.sightings.incidents() 355 | - incident.patch() 356 | - incident.search.get() 357 | - incident.search.delete() 358 | - incident.search.count() 359 | - incident.metric.histogram() 360 | - incident.metric.topn() 361 | - incident.metric.cardinality() 362 | 363 | # Indicator 364 | indicator = client.private_intel.indicator 365 | Available methods: 366 | - indicator.post() 367 | - indicator.get() 368 | - indicator.put() 369 | - indicator.delete() 370 | - indicator.external_id() 371 | - indicator.judgements.indicators() 372 | - indicator.sightings.indicators() 373 | - indicator.search.get() 374 | - indicator.search.delete() 375 | - indicator.search.count() 376 | - indicator.metric.histogram() 377 | - indicator.metric.topn() 378 | - indicator.metric.cardinality() 379 | 380 | # Inspect 381 | inspect = client.inspect 382 | Available methods: 383 | - inspect.inspect() 384 | 385 | # Int 386 | int = client.int 387 | Available methods: 388 | - int.integration.get(_id) 389 | - int.integration.patch(_id) 390 | - int.integration.delete(_id) 391 | - int.integration.get() 392 | - int.integration.post() 393 | - int.module_instance.get(_id) 394 | - int.module_instance.patch(_id) 395 | - int.module_instance.delete(_id) 396 | - int.module_instance.get() 397 | - int.module_instance.post() 398 | - int.module_type.get(_id) 399 | - int.module_type.patch(_id) 400 | - int.module_type.delete(_id) 401 | - int.module_type.get() 402 | - int.module_type.post() 403 | - int.module_type_patch.get() 404 | - int.module_type_patch.post() 405 | - int.module_type_patch.get(_id) 406 | - int.module_type_patch.put(_id) 407 | - int.module_type_patch.delete(_id) 408 | - int.module_type_patch.action_preview(_id) 409 | 410 | # Investigation 411 | investigation = client.private_intel.investigation 412 | Available methods: 413 | - investigation.post() 414 | - investigation.get() 415 | - investigation.put() 416 | - investigation.delete() 417 | - investigation.external_id() 418 | - investigation.search.get() 419 | - investigation.search.delete() 420 | - investigation.search.count() 421 | - investigation.metric.histogram() 422 | - investigation.metric.topn() 423 | - investigation.metric.cardinality() 424 | 425 | # Judgment 426 | judgment = client.private_intel.judgment 427 | Available methods: 428 | - judgment.post() 429 | - judgment.get() 430 | - judgment.put() 431 | - judgment.delete() 432 | - judgment.expire() 433 | - judgment.external_id() 434 | - judgment.judgments() 435 | - judgment.search.get() 436 | - judgment.search.delete() 437 | - judgment.search.count() 438 | - judgment.metric.histogram() 439 | - judgment.metric.topn() 440 | - judgment.metric.cardinality() 441 | 442 | # Malware 443 | malware = client.private_intel.malware 444 | Available methods: 445 | - malware.post() 446 | - malware.get() 447 | - malware.put() 448 | - malware.delete() 449 | - malware.external_id() 450 | - malware.search.get() 451 | - malware.search.delete() 452 | - malware.search.count() 453 | - malware.metric.histogram() 454 | - malware.metric.topn() 455 | - malware.metric.cardinality() 456 | 457 | # Metrics 458 | metrics = client.private_intel.metrics 459 | Available methods: 460 | - metrics.get() 461 | 462 | # Profile 463 | profile = client.profile 464 | Available methods: 465 | - profile.whoami() 466 | - profile.org.get() 467 | - profile.org.post() 468 | 469 | # Properties 470 | properties = client.private_intel.properties 471 | Available methods: 472 | - properties.get() 473 | 474 | # Relationship 475 | relationship = client.private_intel.relationship 476 | Available methods: 477 | - relationship.post() 478 | - relationship.get() 479 | - relationship.put() 480 | - relationship.delete() 481 | - relationship.external_id() 482 | - relationship.search.get() 483 | - relationship.search.delete() 484 | - relationship.search.count() 485 | - relationship.metric.histogram() 486 | - relationship.metric.topn() 487 | - relationship.metric.cardinality() 488 | 489 | # Response 490 | response = client.response 491 | Available methods: 492 | - response.respond.observables() 493 | - response.respond.sighting() 494 | - response.respond.trigger() 495 | 496 | # Sighting 497 | sighting = client.private_intel.sighting 498 | Available methods: 499 | - sighting.post() 500 | - sighting.get() 501 | - sighting.put() 502 | - sighting.delete() 503 | - sighting.external_id() 504 | - sighting.sightings() 505 | - sighting.search.get() 506 | - sighting.search.delete() 507 | - sighting.search.count() 508 | - sighting.metric.histogram() 509 | - sighting.metric.topn() 510 | - sighting.metric.cardinality() 511 | 512 | # SSE Device 513 | sse_device = client.sse_device 514 | Available methods: 515 | - sse_device.get_all() 516 | - sse_device.get_by_id() 517 | - sse_device.post() 518 | - sse_device.patch() 519 | - sse_device.token() 520 | - sse_device.re_token() 521 | - sse_device.api_proxy() 522 | - sse_device.delete() 523 | 524 | # SSE Tenant 525 | sse_tenant = client.sse_tenant 526 | Available methods: 527 | - sse_tenant.get_token() 528 | 529 | # Target record 530 | target_record = client.private_intel.target_record 531 | Available methods: 532 | - target_record.post() 533 | - target_record.get() 534 | - target_record.put() 535 | - target_record.delete() 536 | - target_record.external_id() 537 | - target_record.search.get() 538 | - target_record.search.delete() 539 | - target_record.search.count() 540 | - target_record.metric.histogram() 541 | - target_record.metric.topn() 542 | - target_record.metric.cardinality() 543 | 544 | # Status 545 | status = client.private_intel.status 546 | Available methods: 547 | - status.get() 548 | 549 | # Tool 550 | tool = client.private_intel.tool 551 | Available methods: 552 | - tool.post() 553 | - tool.get() 554 | - tool.put() 555 | - tool.delete() 556 | - tool.external_id() 557 | - tool.search.get() 558 | - tool.search.delete() 559 | - tool.search.count() 560 | - tool.metric.histogram() 561 | - tool.metric.topn() 562 | - tool.metric.cardinality() 563 | 564 | # User Management 565 | user_mgmt = client.user_mgmt 566 | Available methods: 567 | - user_mgmt.users.get() 568 | - user_mgmt.users.post() 569 | - user_mgmt.batch.users() 570 | - user_mgmt.search.users() 571 | 572 | # Verdict 573 | verdict = client.private_intel.verdict 574 | Available methods: 575 | - verdict.get() 576 | 577 | # Version 578 | version = client.private_intel.version 579 | Available methods: 580 | - version.get() 581 | 582 | # Vulnerability 583 | vulnerability = client.private_intel.vulnerability 584 | Available methods: 585 | - vulnerability.cpe_match_strings() 586 | - vulnerability.post() 587 | - vulnerability.get() 588 | - vulnerability.put() 589 | - vulnerability.delete() 590 | - vulnerability.external_id() 591 | - vulnerability.search.get() 592 | - vulnerability.search.delete() 593 | - vulnerability.search.count() 594 | - vulnerability.metric.histogram() 595 | - vulnerability.metric.topn() 596 | - vulnerability.metric.cardinality() 597 | 598 | # Weakness 599 | weakness = client.private_intel.weakness 600 | Available methods: 601 | - weakness.post() 602 | - weakness.get() 603 | - weakness.put() 604 | - weakness.delete() 605 | - weakness.external_id() 606 | - weakness.search.get() 607 | - weakness.search.delete() 608 | - weakness.search.count() 609 | - weakness.metric.histogram() 610 | - weakness.metric.topn() 611 | - weakness.metric.cardinality() 612 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | pep8: mark a PEP8 compliance test. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.19 2 | six~=1.12 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import setuptools 3 | 4 | 5 | def read_version(): 6 | with open('threatresponse/version.py', 'r') as fin: 7 | return re.search( 8 | r"^__version__ = '(?P.+)'$", 9 | fin.read().strip(), 10 | ).group('version') 11 | 12 | 13 | def read_readme(): 14 | with open('README.md', 'r') as fin: 15 | return fin.read().strip() 16 | 17 | 18 | def read_requirements(): 19 | with open('requirements.txt', 'r') as fin: 20 | requirements = [] 21 | for line in fin: 22 | # Discard any comments (i.e. everything after the very first '#'). 23 | line = line.split('#', 1)[0].strip() 24 | if line: 25 | requirements.append(line) 26 | return requirements 27 | 28 | 29 | NAME = 'threatresponse' 30 | 31 | VERSION = read_version() 32 | 33 | DESCRIPTION = 'Threat Response API Module' 34 | 35 | LONG_DESCRIPTION = read_readme() 36 | 37 | LONG_DESCRIPTION_CONTENT_TYPE = 'text/markdown' 38 | 39 | URL = 'https://github.com/CiscoSecurity/tr-05-api-module' 40 | 41 | AUTHOR = 'Cisco Security' 42 | 43 | LICENSE = 'MIT' 44 | 45 | PACKAGES = setuptools.find_packages(exclude=['tests', 'tests.*']) 46 | 47 | PYTHON_REQUIRES = '>=2.6' 48 | 49 | INSTALL_REQUIRES = read_requirements() 50 | 51 | KEYWORDS = [ 52 | 'cisco', 'security', 53 | 'threat', 'response', 54 | 'api', 'module', 55 | 'python', 56 | ] 57 | 58 | CLASSIFIERS = [ 59 | 'Intended Audience :: Developers', 60 | 'Operating System :: OS Independent', 61 | 'Programming Language :: Python', 62 | 'Programming Language :: Python :: 2', 63 | 'Programming Language :: Python :: 2.6', 64 | 'Programming Language :: Python :: 2.7', 65 | 'Programming Language :: Python :: 3', 66 | 'Programming Language :: Python :: 3.5', 67 | 'Programming Language :: Python :: 3.6', 68 | 'Programming Language :: Python :: 3.7', 69 | 'Programming Language :: Python :: 3.8', 70 | 'Topic :: Software Development :: Libraries :: Python Modules', 71 | ] 72 | 73 | 74 | setuptools.setup( 75 | name=NAME, 76 | version=VERSION, 77 | description=DESCRIPTION, 78 | long_description=LONG_DESCRIPTION, 79 | long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE, 80 | url=URL, 81 | author=AUTHOR, 82 | license=LICENSE, 83 | packages=PACKAGES, 84 | python_requires=PYTHON_REQUIRES, 85 | install_requires=INSTALL_REQUIRES, 86 | keywords=KEYWORDS, 87 | classifiers=CLASSIFIERS, 88 | ) 89 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | flake8 3 | mock 4 | pytest 5 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/project.properties.sample: -------------------------------------------------------------------------------- 1 | [general] 2 | # Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) 3 | verbosity=INFO 4 | 5 | # Path to the log file 6 | log_path= 7 | 8 | # use w mode - to overwrite file, a mode - to append data 9 | log_mode= 10 | 11 | 12 | [servers] 13 | # CTR server data 14 | # Hostname of CTR server 15 | ctr_hostname=https://visibility.amp.cisco.com 16 | 17 | # Client ID for token authentication 18 | ctr_client_id= 19 | 20 | # Client Password for token authentication 21 | ctr_client_password= 22 | -------------------------------------------------------------------------------- /tests/functional/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/functional/tests/__init__.py -------------------------------------------------------------------------------- /tests/functional/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Configurations for py.test runner""" 3 | import pytest 4 | from ctrlibrary.core.datafactory import gen_string 5 | from ctrlibrary.threatresponse import token 6 | from ctrlibrary.core import settings 7 | from ctrlibrary.threatresponse.profile import update_org 8 | from requests import HTTPError 9 | from ctrlibrary.core.utils import delayed_return 10 | 11 | from threatresponse import ThreatResponse 12 | 13 | 14 | def pytest_collection_modifyitems(): 15 | if not settings.configured: 16 | settings.configure() 17 | return settings 18 | 19 | 20 | @pytest.fixture(scope='module') 21 | def module_token(): 22 | return token.request_token( 23 | settings.server.ctr_client_id, settings.server.ctr_client_password) 24 | 25 | 26 | @pytest.fixture(scope='module') 27 | def module_headers(module_token): 28 | return {'Authorization': 'Bearer {}'.format(module_token)} 29 | 30 | 31 | @pytest.fixture(scope='module') 32 | def module_tool_client(): 33 | return ThreatResponse( 34 | client_id=settings.server.ctr_client_id, 35 | client_password=settings.server.ctr_client_password 36 | ) 37 | 38 | 39 | @pytest.fixture(scope='module') 40 | def module_tool_client_token(module_token): 41 | return ThreatResponse( 42 | token=module_token 43 | ) 44 | 45 | 46 | @pytest.fixture(scope='function') 47 | def update_org_name(module_headers): 48 | 49 | default_org_name = 'cisco' 50 | 51 | updated_org_name = update_org(payload={"name": "{0}".format(gen_string())}, 52 | **{'headers': module_headers})['name'] 53 | 54 | yield default_org_name, updated_org_name 55 | 56 | update_org(payload={"name": default_org_name}, 57 | **{'headers': module_headers}) 58 | 59 | 60 | @pytest.fixture(scope='function') 61 | def get_entity(module_tool_client): 62 | def _get_entity(entity_name): 63 | return getattr(module_tool_client.private_intel, entity_name) 64 | return _get_entity 65 | 66 | 67 | @pytest.fixture(scope='function') 68 | def get_post_response(): 69 | def _get_response(entity_object, payload): 70 | post_tool_response = entity_object.post( 71 | payload=payload, params={'wait_for': 'true'}) 72 | return post_tool_response 73 | return _get_response 74 | 75 | 76 | @pytest.fixture(scope='function') 77 | def get_entity_response(get_entity, get_post_response): 78 | global entity 79 | global entity_id 80 | entity = None 81 | entity_id = None 82 | 83 | def _get_entity_response(entity_name, payload, refs=None): 84 | if refs: 85 | payload.update(refs) 86 | global entity 87 | global entity_id 88 | entity = get_entity(entity_name) 89 | response = get_post_response(entity, payload) 90 | entity_id = response['id'].rpartition('/')[-1] 91 | return response 92 | yield _get_entity_response 93 | delayed_return(entity.delete(entity_id)) 94 | with pytest.raises(HTTPError): 95 | entity.get(entity_id) 96 | -------------------------------------------------------------------------------- /tests/functional/tests/payloads.py: -------------------------------------------------------------------------------- 1 | from ctrlibrary.core.datafactory import gen_ip 2 | 3 | SERVER_VERSION = '1.1.3' 4 | observable = {'type': 'ip', 'value': gen_ip()} 5 | 6 | ACTOR_PAYLOAD = { 7 | 'actor_type': 'Hacker', 8 | 'description': 'For Test', 9 | 'confidence': 'High', 10 | 'schema_version': SERVER_VERSION, 11 | 'source': 'Test source', 12 | 'type': 'actor', 13 | 'short_description': 'test', 14 | 'title': 'for test', 15 | 'external_ids': ['3'] 16 | } 17 | 18 | PUT_ACTOR_PAYLOAD = { 19 | 'source': 'new source point', 20 | 'actor_type': 'Hacker', 21 | 'description': 'for Test', 22 | 'confidence': 'High', 23 | 'schema_version': SERVER_VERSION, 24 | 'type': 'actor', 25 | 'short_description': 'test', 26 | 'title': 'for test' 27 | } 28 | 29 | ASSET_PAYLOAD = { 30 | 'asset_type': 'data', 31 | 'description': 'For Test', 32 | 'valid_time': { 33 | "start_time": "2021-07-27T07:55:38.193Z", 34 | "end_time": "2021-07-27T07:55:38.193Z"}, 35 | 'schema_version': SERVER_VERSION, 36 | 'source': 'test source', 37 | 'type': 'asset', 38 | 'short_description': 'test', 39 | 'title': 'for test', 40 | 'external_ids': ['3'] 41 | } 42 | 43 | PUT_ASSET_PAYLOAD = { 44 | 'source': 'new source point', 45 | 'asset_type': 'device', 46 | 'description': 'for Test', 47 | 'valid_time': { 48 | "start_time": "2021-07-27T07:55:38.193Z", 49 | "end_time": "2021-07-27T07:55:38.193Z"}, 50 | 'schema_version': SERVER_VERSION, 51 | 'type': 'asset', 52 | 'short_description': 'test', 53 | 'title': 'for test', 54 | 'external_ids': ['3'] 55 | } 56 | 57 | ATTACK_PATTERN_PAYLOAD = { 58 | 'description': ( 59 | 'A boot kit is a malware variant that modifies the boot sectors of' 60 | ' a hard drive' 61 | ), 62 | 'schema_version': SERVER_VERSION, 63 | 'type': 'attack-pattern', 64 | 'short_description': 'desc for test', 65 | 'source': 'new source point', 66 | 'title': 'for test', 67 | 'external_ids': ['3'] 68 | } 69 | 70 | PUT_ATTACK_PATTERN_PAYLOAD = { 71 | 'short_description': 'Updated descr', 72 | 'description': ( 73 | 'A standalone malware that replicates itself in order to' 74 | ' spread to other computers' 75 | ), 76 | 'title': 'for test' 77 | } 78 | 79 | CAMPAIGN_PAYLOAD = { 80 | 'campaign_type': 'Critical', 81 | 'confidence': 'Medium', 82 | 'type': 'campaign', 83 | 'schema_version': SERVER_VERSION, 84 | 'description': 'For test', 85 | 'short_description': 'Short test description', 86 | 'title': 'Test' 87 | } 88 | 89 | PUT_CAMPAIGN_PAYLOAD = { 90 | 'title': 'New demo campaign', 91 | 'campaign_type': 'Critical', 92 | 'description': 'For Test', 93 | 'short_description': 'Short test description' 94 | } 95 | 96 | COA_PAYLOAD = { 97 | 'description': 'COA entity we use for bulk testing', 98 | 'coa_type': 'Diplomatic Actions', 99 | 'type': 'coa', 100 | 'schema_version': SERVER_VERSION, 101 | 'short_description': 'Short test description', 102 | 'title': 'Test', 103 | 'external_ids': ['3'] 104 | } 105 | 106 | INCIDENT_PAYLOAD = { 107 | 'confidence': 'Low', 108 | 'incident_time': { 109 | 'opened': "2014-01-11T00:40:48.212Z" 110 | }, 111 | 'status': 'New', 112 | 'type': 'incident', 113 | 'schema_version': SERVER_VERSION, 114 | 'external_ids': ['3'] 115 | } 116 | 117 | PUT_INCIDENT_PAYLOAD = { 118 | 'confidence': 'Medium', 119 | 'incident_time': { 120 | 'opened': "2016-02-11T00:40:48.212Z" 121 | }, 122 | 'status': 'Open', 123 | } 124 | 125 | INDICATOR_PAYLOAD = { 126 | 'producer': 'producer', 127 | 'schema_version': SERVER_VERSION, 128 | 'type': 'indicator', 129 | 'revision': 0, 130 | 'external_ids': ['3'] 131 | } 132 | 133 | CASEBOOK_PAYLOAD = { 134 | 'type': 'casebook', 135 | 'title': 'Case September 24, 2019 2:34 PM', 136 | 'short_description': 'New Casebook', 137 | 'description': 'New Casebook for malicious tickets', 138 | 'observables': [observable], 139 | 'timestamp': '2019-09-24T11:34:18.000Z', 140 | 'external_ids': ['3'] 141 | } 142 | 143 | CASEBOOK_PATCH_PAYLOAD = { 144 | 'type': 'casebook', 145 | 'title': 'Case November, 2021 0:00 PM', 146 | 'short_description': 'Patched Casebook', 147 | 'description': 'Patched entity', 148 | 'observables': [], 149 | 'timestamp': '2019-09-24T11:34:18.000Z' 150 | } 151 | 152 | DATA_TABLE_PAYLOAD = { 153 | 'schema_version': SERVER_VERSION, 154 | 'type': 'data-table', 155 | 'columns': [{'name': 'column', 'type': 'string'}], 156 | 'rows': [[{}]] 157 | } 158 | 159 | JUDGEMENT_PAYLOAD = { 160 | 'confidence': 'High', 161 | 'disposition': 2, 162 | 'disposition_name': 'Malicious', 163 | 'observable': observable, 164 | 'priority': 99, 165 | 'schema_version': SERVER_VERSION, 166 | 'severity': 'Medium', 167 | 'source': 'source', 168 | 'type': 'judgement', 169 | 'external_ids': ['3'] 170 | } 171 | 172 | PUT_JUDGEMENT_PAYLOAD = { 173 | 'confidence': 'High', 174 | 'priority': 43, 175 | 'severity': 'High', 176 | 'observable': observable, 177 | 'source': 'source', 178 | } 179 | 180 | IDENTITY_ASSERTION_PAYLOAD = { 181 | 'identity': { 182 | 'observables': [ 183 | { 184 | 'type': 'ip', 185 | 'value': '10.0.0.1' 186 | }, 187 | ] 188 | }, 189 | 'schema_version': SERVER_VERSION, 190 | 'type': 'identity-assertion', 191 | 'source': 'ATQC data', 192 | 'external_ids': ['3'], 193 | 'assertions': [ 194 | { 195 | 'name': 'severity', 196 | 'value': 'Medium' 197 | } 198 | ], 199 | } 200 | 201 | PUT_IDENTITY_ASSERTION_PAYLOAD = { 202 | 'identity': { 203 | 'observables': [ 204 | { 205 | 'type': 'ip', 206 | 'value': '10.0.0.1' 207 | }, 208 | ] 209 | }, 210 | 'assertions': [ 211 | { 212 | 'name': 'severity', 213 | 'value': 'Low' 214 | } 215 | ], 216 | } 217 | 218 | SIGHTING_PAYLOAD = { 219 | 'count': 1, 220 | 'observed_time': { 221 | 'start_time': '2019-09-25T00:40:48.212Z', 222 | 'end_time': '2019-09-25T00:40:48.212Z' 223 | }, 224 | 'confidence': 'High', 225 | 'type': 'sighting', 226 | 'schema_version': SERVER_VERSION, 227 | 'external_ids': ['3'], 228 | 'observables': [observable] 229 | } 230 | 231 | PUT_SIGHTING_PAYLOAD = { 232 | 'confidence': 'Low', 233 | 'observed_time': { 234 | 'start_time': '2019-09-25T00:40:48.212Z', 235 | 'end_time': '2019-09-25T00:40:48.212Z' 236 | }, 237 | } 238 | 239 | INVESTIGATION_PAYLOAD = { 240 | 'title': 'Demo investigation', 241 | 'description': 'Request investigation for yesterday malware', 242 | 'type': 'investigation', 243 | 'source': 'a source', 244 | 'schema_version': SERVER_VERSION, 245 | 'external_ids': ['3'] 246 | } 247 | 248 | MALWARE_PAYLOAD = { 249 | 'type': 'malware', 250 | 'schema_version': SERVER_VERSION, 251 | 'labels': ['malware'], 252 | 'description': 'Test description', 253 | 'title': 'Title for test', 254 | 'short_description': 'Short test description', 255 | 'external_ids': ['3'] 256 | 257 | } 258 | 259 | PUT_MALWARE_PAYLOAD = {'labels': ['malware'], 260 | 'description': 'Test description', 261 | 'title': 'Changed title for test', 262 | 'short_description': 'Short test description' 263 | } 264 | 265 | TARGET_RECORD_PAYLOAD = { 266 | "targets": [ 267 | { 268 | "type": "string", 269 | "observables": [observable], 270 | "observed_time": { 271 | "start_time": "2021-08-05T14:17:54.726Z", 272 | "end_time": "2021-08-05T14:17:54.726Z" 273 | } 274 | } 275 | ], 276 | 'source': 'For test', 277 | 'type': 'target-record', 278 | 'schema_version': SERVER_VERSION, 279 | 'external_ids': ['3'] 280 | } 281 | 282 | PUT_TARGET_RECORD_PAYLOAD = { 283 | 'source': 'Updated source', 284 | 'targets': [ 285 | { 286 | "type": "string", 287 | "observables": [ 288 | { 289 | "value": "asdf.com", 290 | "type": "domain" 291 | } 292 | ], 293 | "observed_time": { 294 | "start_time": "2021-08-05T14:17:54.726Z", 295 | "end_time": "2021-08-05T14:17:54.726Z" 296 | } 297 | } 298 | ] 299 | } 300 | 301 | TOOL_PAYLOAD = { 302 | 'labels': ['tool'], 303 | 'type': 'tool', 304 | 'schema_version': SERVER_VERSION, 305 | 'description': 'Test description', 306 | 'title': 'Title for test', 307 | 'short_description': 'Short test description', 308 | 'external_ids': ['3'] 309 | } 310 | 311 | PUT_TOOL_PAYLOAD = {'labels': ['tool'], 312 | 'description': 'Test description', 313 | 'title': 'Changed title for test', 314 | 'short_description': 'Short test description' 315 | } 316 | 317 | VULNERABILITY_PAYLOAD = { 318 | 'description': 'Browser vulnerability', 319 | 'type': 'vulnerability', 320 | 'schema_version': SERVER_VERSION, 321 | 'external_ids': ['3'] 322 | } 323 | 324 | WEAKNESS_PAYLOAD = { 325 | 'description': ( 326 | 'The software receives input from an upstream component, but it' 327 | ' does not neutralize or incorrectly neutralizes code syntax' 328 | ' before using the input in a dynamic evaluation call' 329 | ' (e.g. \"eval\").'), 330 | 'schema_version': SERVER_VERSION, 331 | 'likelihood': 'Medium', 332 | 'type': 'weakness', 333 | 'external_ids': ['3'] 334 | } 335 | 336 | RELATIONSHIP_PAYLOAD = { 337 | 'description': 'Test relation', 338 | 'schema_version': SERVER_VERSION, 339 | 'type': 'relationship', 340 | 'relationship_type': 'indicates', 341 | 'external_ids': ['3'] 342 | } 343 | 344 | ASSET_MAPPING_PAYLOAD = { 345 | 'asset_type': 'data', 346 | 'confidence': 'High', 347 | 'stability': 'Physical', 348 | 'specificity': 'Medium', 349 | 'valid_time': { 350 | "start_time": "2021-07-27T07:55:38.193Z", 351 | "end_time": "2021-07-27T07:55:38.193Z"}, 352 | 'schema_version': SERVER_VERSION, 353 | 'observable': { 354 | 'value': '1.1.1.1', 355 | 'type': 'ip' 356 | }, 357 | 'source': 'test source', 358 | 'type': 'asset-mapping', 359 | 'external_ids': ['3'] 360 | } 361 | 362 | ASSET_PROPERTIES_PAYLOAD = { 363 | 'valid_time': { 364 | "start_time": "2021-07-27T07:55:38.193Z", 365 | "end_time": "2021-07-27T07:55:38.193Z"}, 366 | 'schema_version': SERVER_VERSION, 367 | 'source': 'test source', 368 | 'type': 'asset-properties', 369 | 'external_ids': ['3'] 370 | } 371 | 372 | BUNDLE_PAYLOAD = { 373 | "operation": "add", 374 | "bundle": { 375 | "description": "string", 376 | "valid_time": { 377 | "start_time": "2021-08-26T11:48:51.490Z", 378 | "end_time": "2021-08-26T11:48:51.490Z" 379 | }, 380 | "schema_version": "1.1.3", 381 | "type": "bundle", 382 | "source": "Source For bundle", 383 | "short_description": "Bundle description", 384 | "title": "Title for test" 385 | } 386 | } 387 | 388 | FEED_PAYLOAD = { 389 | "schema_version": SERVER_VERSION, 390 | "revision": 0, 391 | "type": "feed", 392 | "output": "observables", 393 | "feed_type": "indicator", 394 | "external_ids": ['3'] 395 | 396 | } 397 | 398 | FEEDBACK_PAYLOAD = { 399 | 'schema_version': SERVER_VERSION, 400 | 'type': 'feedback', 401 | 'feedback': 1, 402 | 'reason': 'improvement' 403 | } 404 | -------------------------------------------------------------------------------- /tests/functional/tests/test_ctr_05_api.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | from requests import ReadTimeout, HTTPError 4 | 5 | from ctrlibrary.core.datafactory import ( 6 | gen_sha256, 7 | gen_string, 8 | gen_random_ctr_token 9 | ) 10 | from ctrlibrary.core.utils import get_observables 11 | from ctrlibrary.threatresponse.inspect import inspect 12 | from ctrlibrary.threatresponse.enrich import ( 13 | enrich_deliberate_observables, 14 | enrich_observe_observables, 15 | enrich_refer_observables 16 | ) 17 | from ctrlibrary.threatresponse.profile import ( 18 | get_profile, 19 | get_org, 20 | update_org 21 | ) 22 | from ctrlibrary.threatresponse.response import response_respond_observables 23 | from ctrlibrary.threatresponse.user_mgmt import ( 24 | get_user_info, 25 | get_users_info, 26 | search_users 27 | ) 28 | from threatresponse import ThreatResponse 29 | from threatresponse.exceptions import CredentialsError 30 | 31 | IP = '95.95.0.1' 32 | SHA256_HASH = ( 33 | '10745318f9dd601ab76f029cbc41c7e13c9754f87eb2c85948734b2b0b148140') 34 | DOMAIN = 'anotheratqcdata.com' 35 | 36 | 37 | def test_ctr_positive_smoke_inspect(module_headers): 38 | """Perform testing for inspect end point of threat response application 39 | server 40 | 41 | ID: aaf14b29-9f5a-41aa-805e-73398ed2b112 42 | 43 | Steps: 44 | 45 | 1. Send request with domain name to inspect end point 46 | 47 | Expectedresults: POST action successfully get to the end point and return 48 | correct data 49 | 50 | Importance: High 51 | """ 52 | response = inspect( 53 | payload={'content': DOMAIN}, 54 | **{'headers': module_headers} 55 | ) 56 | assert response[0]['value'] == DOMAIN 57 | assert response[0]['type'] == 'domain' 58 | 59 | 60 | def test_ctr_positive_timeout_support(module_headers): 61 | """Perform testing for inspect end point of threat response application 62 | server 63 | 64 | ID: CCTRI-eeb97060-f393-46d3-b281-602f7624e91e 65 | 66 | Steps: 67 | 68 | 1. Send request with domain name and timeout to inspect end point 69 | 70 | Expectedresults: It is possible to use timeout as part of POST request to 71 | have delay and return correct data 72 | 73 | Importance: High 74 | """ 75 | response = inspect( 76 | payload={'content': DOMAIN}, 77 | **{ 78 | 'headers': module_headers, 79 | 'timeout': 5 80 | } 81 | ) 82 | assert response[0]['value'] == DOMAIN 83 | assert response[0]['type'] == 'domain' 84 | 85 | 86 | def test_python_module_negative_endpoint_timeout(module_tool_client): 87 | """Perform testing of timeout argument for any threat response endpoint 88 | 89 | ID: CCTRI-374-8b4de3f0-2e24-444a-8631-3ddb0745be46 90 | 91 | Steps: 92 | 93 | 1. Send request to inspect end point of threat response server using 94 | long enough timeout to finish successfully 95 | 2. Send same request, but with short timeout, so it fail with exception 96 | 97 | Expectedresults: Timeout argument affects request result in expected way 98 | 99 | Importance: High 100 | """ 101 | request_content = gen_sha256(gen_string()) 102 | module_tool_client.inspect.inspect({'content': request_content}, timeout=5) 103 | with pytest.raises(ReadTimeout): 104 | module_tool_client.inspect.inspect( 105 | {'content': request_content}, timeout=0.01) 106 | 107 | 108 | def test_python_module_positive_inspect(module_headers, module_tool_client): 109 | """Perform testing for inspect end point of custom threat response python 110 | module 111 | 112 | ID: 3ce73f46-7db9-4ae7-a69d-fd791c943d28 113 | 114 | Steps: 115 | 116 | 1. Send request sha256 hash to inspect end point of threat response 117 | server using direct POST call 118 | 2. Send same request using custom python module 119 | 3. Compare results 120 | 121 | Expectedresults: Inspect requests which are sent directly and sent using 122 | custom tool return same responses 123 | 124 | Importance: Critical 125 | """ 126 | direct_response = inspect( 127 | payload={'content': SHA256_HASH}, 128 | **{'headers': module_headers} 129 | ) 130 | tool_response = module_tool_client.inspect.inspect( 131 | {'content': SHA256_HASH}) 132 | assert direct_response[0]['value'] == tool_response[0]['value'] 133 | assert direct_response[0]['type'] == tool_response[0]['type'] 134 | assert tool_response[0]['value'] == SHA256_HASH 135 | assert tool_response[0]['type'] == 'sha256' 136 | 137 | 138 | def test_python_module_positive_enrich_observe_observables( 139 | module_headers, module_tool_client): 140 | """Perform testing for enrich observe observables end point of custom 141 | threat response python module 142 | 143 | ID: d1dd6d3b-f762-4280-a573-7cc815da5a85 144 | 145 | Steps: 146 | 147 | 1. Send request sha256 hash to enrich observe observables end point of 148 | threat response server using direct POST call 149 | 2. Send same request using custom python module 150 | 3. Compare results 151 | 152 | Expectedresults: POST action successfully get to the end point and return 153 | correct data 154 | 155 | Importance: Critical 156 | """ 157 | response = enrich_observe_observables( 158 | payload=[{'type': 'sha256', 'value': SHA256_HASH}], 159 | **{'headers': module_headers} 160 | ) 161 | tool_response = module_tool_client.enrich.observe.observables( 162 | [{'type': 'sha256', 'value': SHA256_HASH}]) 163 | direct_observables = get_observables(response, 'Private Intelligence') 164 | tool_observables = get_observables(tool_response, 'Private Intelligence') 165 | assert tool_observables['data']['verdicts']['count'] > 0, ( 166 | 'No observable verdicts returned from server. Check hash value') 167 | assert tool_observables['data']['judgements']['count'] > 0, ( 168 | 'No observable judgements returned from server. Check hash value') 169 | assert tool_observables[ 170 | 'data']['verdicts']['docs'][0]['disposition_name'] == 'Malicious' 171 | assert direct_observables['data']['judgements'][ 172 | 'count'] == tool_observables['data']['judgements']['count'] 173 | 174 | 175 | def test_python_module_positive_enrich_deliberate_observables( 176 | module_headers, module_tool_client): 177 | """Perform testing for enrich deliberate observables end point of custom 178 | threat response python module 179 | 180 | ID: 2deb7d0f-a44f-49d6-81f1-5a6e16e7d652 181 | 182 | Steps: 183 | 184 | 1. Send request sha256 hash to enrich deliberate observables end 185 | point of threat response server using direct POST call 186 | 2. Send same request using custom python module 187 | 3. Compare results 188 | 189 | Expectedresults: POST action successfully get to the end point and return 190 | correct data 191 | 192 | Importance: Critical 193 | """ 194 | response = enrich_deliberate_observables( 195 | payload=[{'type': 'sha256', 'value': SHA256_HASH}], 196 | **{'headers': module_headers} 197 | ) 198 | tool_response = module_tool_client.enrich.deliberate.observables( 199 | [{'type': 'sha256', 'value': SHA256_HASH}]) 200 | direct_observables = get_observables(response, 'Private Intelligence') 201 | tool_observables = get_observables(tool_response, 'Private Intelligence') 202 | assert tool_observables['data']['verdicts']['count'] > 0, ( 203 | 'No observables returned from server. Check hash value') 204 | assert 'judgements' not in tool_observables['data'].keys() 205 | assert tool_observables[ 206 | 'data']['verdicts']['docs'][0]['type'] == 'verdict' 207 | assert direct_observables['data']['verdicts'][ 208 | 'count'] == tool_observables['data']['verdicts']['count'] 209 | 210 | 211 | def test_python_module_positive_enrich_refer_observables( 212 | module_headers, module_tool_client): 213 | """Perform testing for enrich refer observables end point of custom 214 | threat response python module 215 | 216 | ID: 7b8d86b5-a360-4f91-acd7-f2d9e4104b03 217 | 218 | Steps: 219 | 220 | 1. Send request sha256 hash to enrich refer observables end point of 221 | threat response server using direct POST call 222 | 2. Send same request using custom python module 223 | 3. Compare results 224 | 225 | Expectedresults: POST action successfully get to the end point and return 226 | correct data 227 | 228 | Importance: Critical 229 | """ 230 | response = enrich_refer_observables( 231 | payload=[{'type': 'domain', 'value': DOMAIN}], 232 | **{'headers': module_headers} 233 | )['data'][0] 234 | tool_response = module_tool_client.enrich.refer.observables( 235 | [{'type': 'domain', 'value': DOMAIN}])['data'][0] 236 | assert tool_response['module'] 237 | assert tool_response['title'] == 'Search for this domain' 238 | assert response == tool_response 239 | 240 | 241 | def test_python_module_positive_response_respond_observables_by_hash( 242 | module_headers, module_tool_client): 243 | """Perform testing for response respond observables end point of custom 244 | threat response python module by hash type 245 | 246 | ID: CCTRI-137-b8f74c6e-b670-4159-8d74-eb4756b24084 247 | 248 | Steps: 249 | 250 | 1. Send request sha256 hash to response respond observables end 251 | point of threat response server using direct POST call 252 | 2. Send same request using custom python module 253 | 3. Compare results 254 | 255 | Expectedresults: POST action successfully get to the end point and return 256 | correct data 257 | 258 | Importance: Critical 259 | """ 260 | expected_list = [ 261 | 'Add SHA256 to custom detections 500 PDFs', 262 | 'Add SHA256 to custom detections File Blacklist', 263 | 'Add SHA256 to custom detections testing' 264 | ] 265 | response = response_respond_observables( 266 | payload=[{'type': 'sha256', 'value': SHA256_HASH}], 267 | **{'headers': module_headers} 268 | )['data'] 269 | tool_response = module_tool_client.response.respond.observables( 270 | [{'type': 'sha256', 'value': SHA256_HASH}])['data'] 271 | assert len(tool_response) > 0 272 | assert set(expected_list) == set([d['title'] for d in tool_response]) 273 | assert response == tool_response 274 | 275 | 276 | def test_python_module_positive_response_respond_observables_by_domain( 277 | module_headers, module_tool_client): 278 | """Perform testing for response respond observables end point of custom 279 | threat response python module by domain type 280 | 281 | ID: CCTRI-137-38e4089c-7ca5-4c0a-820d-e6124e939428 282 | 283 | Steps: 284 | 285 | 1. Send domain name in request to response respond observables end 286 | point of threat response server using direct POST call 287 | 2. Send same request using custom python module 288 | 3. Compare results 289 | 290 | Expectedresults: POST action successfully get to the end point and return 291 | correct data 292 | 293 | Importance: Critical 294 | """ 295 | response = response_respond_observables( 296 | payload=[{'type': 'domain', 'value': DOMAIN}], 297 | **{'headers': module_headers} 298 | )['data'] 299 | tool_response = module_tool_client.response.respond.observables( 300 | [{'type': 'domain', 'value': DOMAIN}])['data'] 301 | assert len(tool_response) > 0 302 | assert tool_response[0]['module'] == 'Umbrella' 303 | assert tool_response[0]['title'] == 'Block this domain' 304 | assert response == tool_response 305 | 306 | 307 | def test_python_module_positive_commands_verdict(module_tool_client): 308 | """Perform testing for verdict command from custom threat response python 309 | module for one observable 310 | 311 | ID: CCTRI-385-9f8cc790-a316-4e82-b592-43229f85e381 312 | 313 | Steps: 314 | 315 | 1. Get observable verdict using default deliberate observable request 316 | 2. Get observable verdict using our new tool command 317 | 318 | Expectedresults: Verdict command for provided observable returns expected 319 | values and disposition name is the same in comparison to direct server 320 | request 321 | 322 | Importance: Critical 323 | """ 324 | tool_response = module_tool_client.enrich.deliberate.observables( 325 | [{'type': 'sha256', 'value': SHA256_HASH}]) 326 | tool_observables = get_observables(tool_response, 'Private Intelligence') 327 | assert tool_observables['data']['verdicts']['count'] > 0, ( 328 | 'No observable verdicts returned from server. Check hash value') 329 | assert tool_observables[ 330 | 'data']['verdicts']['docs'][0]['disposition_name'] == 'Malicious' 331 | 332 | tool_command_response = module_tool_client.commands.verdict(SHA256_HASH) 333 | tool_command_private_intel = get_observables( 334 | tool_command_response['response'], 'Private Intelligence') 335 | assert tool_command_private_intel['module'] == 'Private Intelligence' 336 | assert tool_command_private_intel['module_type_id'] 337 | assert tool_command_private_intel['module_instance_id'] 338 | 339 | tool_command_verdict = ( 340 | tool_command_private_intel['data']['verdicts']['docs'][0]) 341 | 342 | assert tool_command_verdict['observable']['value'] == SHA256_HASH 343 | assert tool_command_verdict['observable']['type'] == 'sha256' 344 | assert tool_command_verdict['valid_time'] is not None 345 | assert tool_command_verdict['disposition_name'] == 'Malicious' 346 | 347 | 348 | def test_python_module_positive_commands_verdict_multiple(module_tool_client): 349 | """Perform testing for verdict command from custom threat response python 350 | module for multiple observable 351 | 352 | ID: CCTRI-385-9d42b99e-13ac-4f4e-b142-5e6781db4b00 353 | 354 | Steps: 355 | 356 | 1. Get verdict using our new tool command for both hash and ip 357 | observables 358 | 359 | Expectedresults: Verdict command for provided observables returns expected 360 | values 361 | 362 | Importance: Critical 363 | """ 364 | tool_command_response = module_tool_client.commands.verdict(( 365 | SHA256_HASH, IP)) 366 | tool_command_hash_observable = [ 367 | d 368 | for d in tool_command_response['verdicts'] 369 | if d['observable_value'] == SHA256_HASH and ( 370 | d['module'] == 'Private Intelligence') 371 | ][0] 372 | tool_command_ip_observable = [ 373 | d 374 | for d in tool_command_response['verdicts'] 375 | if d['observable_value'] == IP and ( 376 | d['module'] == 'Private Intelligence') 377 | ][0] 378 | assert tool_command_hash_observable['observable_type'] == 'sha256' 379 | assert tool_command_hash_observable['disposition_name'] == 'Malicious' 380 | assert tool_command_ip_observable['observable_value'] == IP 381 | assert tool_command_ip_observable['observable_type'] == 'ip' 382 | assert tool_command_ip_observable['module_type_id'] 383 | assert tool_command_ip_observable['module_instance_id'] 384 | assert tool_command_ip_observable['disposition_name'] == 'Malicious' 385 | 386 | 387 | def test_python_module_positive_commands_target(module_tool_client): 388 | """Perform testing for target command from custom threat response python 389 | module for the observable 390 | 391 | ID: CCTRI-422-c701fb34-8d35-4407-b103-0f319171e30d 392 | 393 | Steps: 394 | 395 | 1. Get observable targets using our new tool command 396 | 397 | Expectedresults: Target command for provided observable returns expected 398 | values 399 | 400 | Importance: Critical 401 | """ 402 | expected_target = [ 403 | {'value': 'new_demo_endpoint', 'type': 'hostname'}, 404 | {'value': '44:cc:7a:aa:1d:bb', 'type': 'mac_address'}, 405 | {'value': '192.168.4.4', 'type': 'ip'} 406 | ] 407 | tool_command_response = module_tool_client.commands.targets( 408 | SHA256_HASH)['response'] 409 | tool_command_private_intel = get_observables( 410 | tool_command_response, 'Private Intelligence') 411 | 412 | assert tool_command_private_intel['module'] == 'Private Intelligence' 413 | assert tool_command_private_intel['module_type_id'] 414 | assert tool_command_private_intel['module_instance_id'] 415 | tool_command_targets = ( 416 | tool_command_private_intel['data']['sightings']['docs'][0]['targets']) 417 | # We expect 1 target for observable 418 | assert len(tool_command_targets) == 1 419 | assert tool_command_targets[0]['type'] == 'endpoint' 420 | assert tool_command_targets[0]['observables'] == expected_target 421 | 422 | 423 | def test_python_module_positive_profile_whoami(module_headers): 424 | """Perform testing for enrich profile endpoint to check user information 425 | 426 | ID: CCTRI-1720-3487d4de-e647-4dc5-9b79-70a73381949d 427 | 428 | Steps: 429 | 430 | 1. Send GET request to enrich profile endpoint 431 | 432 | Expectedresults: The response body contains all needed data 433 | 434 | Importance: Critical 435 | """ 436 | response = get_profile(**{'headers': module_headers}) 437 | 438 | user = response['user'] 439 | org = response['org'] 440 | 441 | assert user['role'] == 'admin' 442 | assert user['scopes'] 443 | assert user['updated-at'] 444 | assert user['user-email'] 445 | assert user['org-id'] 446 | assert user['user-id'] 447 | assert user['enabled?'] is True 448 | assert user['created-at'] 449 | assert user['user-nick'] == 'ATQC account' 450 | assert user['idp-mappings'][0]['idp'] 451 | assert user['idp-mappings'][0]['user-identity-id'] 452 | assert user['idp-mappings'][0]['organization-id'] 453 | assert user['idp-mappings'][0]['enabled?'] is True 454 | assert user['last-logged-at'] 455 | 456 | assert org['updated-at'] 457 | assert org['name'] == 'cisco' 458 | assert org['allow-all-role-to-login'] is False 459 | assert org['enabled?'] is True 460 | assert org['activation-status'] == 'activated' 461 | assert org['scim-status'] == 'activated' 462 | assert org['id'] 463 | assert org['created-at'] 464 | assert org['allow-all-role-to-login-editable?'] is True 465 | 466 | 467 | def test_python_module_positive_profile_org(module_headers): 468 | """Perform testing for enrich profile endpoint to check user information 469 | 470 | ID: CCTRI-1720-b12cee8e-1200-11eb-adc1-0242ac120002 471 | 472 | Steps: 473 | 474 | 1. Send GET request to enrich profile endpoint 475 | 476 | Expectedresults: The response body contains all needed data 477 | 478 | Importance: Critical 479 | """ 480 | response = get_org(**{'headers': module_headers}) 481 | 482 | assert response['updated-at'] 483 | assert response['name'] == 'cisco' 484 | assert response['allow-all-role-to-login'] is False 485 | assert response['enabled?'] is True 486 | assert response['activation-status'] == 'activated' 487 | assert response['scim-status'] == 'activated' 488 | assert response['id'] 489 | assert response['created-at'] 490 | assert response['allow-all-role-to-login-editable?'] is True 491 | 492 | 493 | def test_python_module_positive_profile_change_org(update_org_name, 494 | module_headers): 495 | """Perform testing for enrich profile endpoint to check possibility of 496 | updating org name 497 | 498 | ID: CCTRI-1720-b12cf140-1200-11eb-adc1-0242ac120002 499 | 500 | Steps: 501 | 502 | 1. Send POST request with random string of org name 503 | 2. Check that default name isn't equal with updated one 504 | 505 | Expectedresults: The default name isn't equal with updated one 506 | 507 | Importance: Critical 508 | """ 509 | default_org_name, updated_org_name = update_org_name 510 | 511 | assert default_org_name != updated_org_name 512 | 513 | 514 | def test_python_module_negative_profile_change_org(module_headers): 515 | """Perform testing for enrich profile endpoint to check inability to change 516 | org name with wrong payload 517 | 518 | ID: CCTRI-1720-b12cf258-1200-11eb-adc1-0242ac120002 519 | 520 | Steps: 521 | 522 | 1. Send POST request with wrong payload 523 | 2. Check that response body contains the error 524 | 525 | 526 | Expectedresults: The response body contains the error 527 | 528 | Importance: Critical 529 | """ 530 | response = update_org(payload={"invalid_key": "invalid_value"}, 531 | **{'headers': module_headers}) 532 | assert response['errors'] == {'invalid_key': 'disallowed-key'} 533 | 534 | 535 | def test_python_module_positive_user_mgmt_user(module_headers): 536 | """Perform testing for enrich user management endpoint to check getting 537 | information about the user using user id 538 | 539 | ID: CCTRI-1698-d6fd0f29-34cd-4ad9-bc4c-5136ed4544b8 540 | 541 | Steps: 542 | 543 | 1. Send GET request to profile endpoint for getting current user id 544 | 2. Send GET request to user_mgmt endpoint with user id for getting user 545 | info from user_mgmt endpoint 546 | 3. Check that we able to get user info from user_mgmt endpoint by id 547 | and this info is match with user info from profile endpoint 548 | 549 | Expectedresults: The user info can be obtained from user_mgmt endpoint by 550 | id and it's match with user info that was received from profile endpoint 551 | 552 | Importance: Critical 553 | """ 554 | whoami_user = get_profile(**{'headers': module_headers})['user'] 555 | user_mgmt_user = get_user_info(whoami_user['user-id'], 556 | **{'headers': module_headers}) 557 | 558 | assert user_mgmt_user['role'] == whoami_user['role'] 559 | assert user_mgmt_user['scopes'] == whoami_user['scopes'] 560 | assert user_mgmt_user['user-email'] == whoami_user['user-email'] 561 | assert user_mgmt_user['user-name'] == whoami_user['user-name'] 562 | assert user_mgmt_user['org-id'] == whoami_user['org-id'] 563 | assert user_mgmt_user['user-id'] == whoami_user['user-id'] 564 | assert user_mgmt_user['enabled?'] == whoami_user['enabled?'] 565 | assert user_mgmt_user['created-at'] == whoami_user['created-at'] 566 | assert user_mgmt_user['user-nick'] == whoami_user['user-nick'] 567 | 568 | 569 | def test_python_module_positive_user_mgmt_users(module_headers): 570 | """Perform testing for enrich user management endpoint to check getting 571 | information about the batch of users using users ids 572 | 573 | ID: CCTRI-1698-7be98c52-1451-11eb-adc1-0242ac120002 574 | 575 | Steps: 576 | 577 | 1. Send GET request to profile endpoint for getting current user id 578 | 2. Send GET request to user_mgmt endpoint with users ids list for 579 | getting users info from user_mgmt endpoint 580 | 3. Check that we able to query a list with users id's on user_mgmt 581 | endpoint and this info is match with users info from profile endpoint 582 | 583 | Expectedresults: The users info can be obtained from user_mgmt endpoint by 584 | querying the list of ids and it's match with user info that was received 585 | from profile endpoint 586 | 587 | Importance: Critical 588 | """ 589 | whoami_user = get_profile(**{'headers': module_headers})['user'] 590 | 591 | user_mgmt_users = get_users_info( 592 | [whoami_user['user-id'], whoami_user['user-id']], 593 | **{'headers': module_headers}) 594 | 595 | assert user_mgmt_users[0]['role'] == whoami_user['role'] 596 | assert user_mgmt_users[0]['scopes'] == whoami_user['scopes'] 597 | assert user_mgmt_users[0]['user-email'] == whoami_user['user-email'] 598 | assert user_mgmt_users[0]['user-name'] == whoami_user['user-name'] 599 | assert user_mgmt_users[0]['org-id'] == whoami_user['org-id'] 600 | assert user_mgmt_users[0]['user-id'] == whoami_user['user-id'] 601 | assert user_mgmt_users[0]['enabled?'] == whoami_user['enabled?'] 602 | assert user_mgmt_users[0]['created-at'] == whoami_user['created-at'] 603 | assert user_mgmt_users[0]['user-nick'] == whoami_user['user-nick'] 604 | 605 | 606 | def test_python_module_positive_user_mgmt_search_users(module_headers): 607 | """Perform testing for enrich user management endpoint to check ability to 608 | search users by their roles 609 | 610 | ID: CCTRI-1698-ff55fd2a-1454-11eb-adc1-0242ac120002 611 | 612 | Steps: 613 | 614 | 1. Send POST request to user_mgmt endpoint for getting users with admin 615 | role 616 | 2. Check that response contains status code 200 617 | 618 | Expectedresults: The search method of user management endpoint is able to 619 | search users with admin role 620 | 621 | Importance: Critical 622 | """ 623 | admins = search_users(**{'headers': module_headers}) 624 | assert admins.status_code == 200 625 | 626 | 627 | def test_python_module_positive_token(module_tool_client_token): 628 | """Perform testing of availability perform request to the Threat response 629 | using token 630 | 631 | ID: CCTRI-1579-8f1c20ea-fe40-11ea-adc1-0242ac120002 632 | 633 | Steps: 634 | 635 | 1. Inspect observable using token 636 | 2. Sleep and wait until token will expired 637 | 638 | Expectedresults: Inspect for provided observable returns expected 639 | values, wait until token will expired and check that exception 640 | raises 641 | 642 | Importance: Critical 643 | """ 644 | assert module_tool_client_token.inspect.inspect( 645 | {'content': '1.1.1.1'}) == [{'type': 'ip', 'value': '1.1.1.1'}] 646 | 647 | # wait till token will expired 648 | time.sleep(601) 649 | 650 | with pytest.raises(HTTPError): 651 | assert module_tool_client_token.inspect.inspect( 652 | {'content': '1.1.1.1'}) != [{'type': 'ip', 'value': '1.1.1.1'}] 653 | 654 | 655 | @pytest.mark.parametrize( 656 | 'token, error', 657 | ((gen_random_ctr_token(token_length=0), CredentialsError), 658 | (gen_random_ctr_token(), HTTPError)) 659 | ) 660 | def test_python_module_negative_token(token, error): 661 | """Perform testing of availability perform request to the Threat response 662 | using invalid token 663 | 664 | ID: CCTRI-1579-4ca2a94f-db81-44c9-bf5b-53146cfd127a 665 | 666 | Steps: 667 | 668 | 1. Inspect observable using empty token 669 | 2. Inspect observable using invalid token 670 | 671 | Expectedresults: Inspect for provided observable doesn't returns expected 672 | values, because token is invalid 673 | 674 | Importance: Critical 675 | """ 676 | with pytest.raises(error): 677 | assert ThreatResponse(token=token).inspect.inspect( 678 | {'content': '1.1.1.1'}) != [{'type': 'ip', 'value': '1.1.1.1'}] 679 | -------------------------------------------------------------------------------- /tests/functional/tests/test_ctr_05_int_api.py: -------------------------------------------------------------------------------- 1 | from ctrlibrary.core.utils import get_observables 2 | from ctrlibrary.threatresponse.int import ( 3 | int_get_integration, 4 | int_get_integration_by_id, 5 | int_post_integration, 6 | int_patch_integration, 7 | int_delete_integration, 8 | int_get_module_instance, 9 | int_get_module_instance_by_id, 10 | int_post_module_instance, 11 | int_patch_module_instance, 12 | int_delete_module_instance, 13 | int_get_module_type, 14 | int_get_module_type_by_id, 15 | int_post_module_type, 16 | int_patch_module_type, 17 | int_delete_module_type, 18 | ) 19 | 20 | 21 | CUSTOM_RELAY_MODULE_TYPE = 'a14ae422-01b6-5013-9876-695ff1b0ebe0' 22 | PRIVATE_INTEL_MODULE_TYPE = '2c8b4134-c521-5be5-aaf8-af06e5e27cbb' 23 | SHA256_HASH = ( 24 | '10745318f9dd601ab76f029cbc41c7e13c9754f87eb2c85948734b2b0b148140') 25 | 26 | 27 | def test_ctr_positive_end_to_end_integration(module_headers): 28 | """Perform testing for int integration end point 29 | 30 | ID: ed5700a9-d020-4c9e-bb3a-adc5e91f7508 31 | 32 | Steps: 33 | 34 | 1. Send POST request to int integration end point to create new entity 35 | 2. Send GET request to fetch all integration entities and check that 36 | one we created is present here 37 | 3. Send GET request to get specific entity and validate that all fields 38 | contain expected data 39 | 4. Send PATCH request to update entity with specific data 40 | 5. Send DELETE request to delete entity 41 | 42 | Expectedresults: End to end scenario for int integration end point works 43 | properly 44 | 45 | Importance: High 46 | """ 47 | # Validate that integration we plan to use for test automation purpose is 48 | # not present in system 49 | integration_list = int_get_integration(**{'headers': module_headers}) 50 | assert len(integration_list) > 0, ( 51 | 'There are no integrations in selected sandbox' 52 | ) 53 | integration = next( 54 | ( 55 | item for item in integration_list 56 | if item['title'] == 'Test Automation Integration Demo' 57 | ), 58 | False 59 | ) 60 | assert not integration, ( 61 | 'Test integration was not removed from the system from previous runs' 62 | ) 63 | # Create new integration 64 | payload = { 65 | 'description': 'Test Automation Integration Demo', 66 | 'tips': 'Some useful tips', 67 | 'module_type_id': CUSTOM_RELAY_MODULE_TYPE, 68 | 'org_id': 'system', 69 | 'short_description': 'Demo Integration', 70 | 'title': 'Test Automation Integration Demo', 71 | 'user_id': 'system', 72 | 'client_id': 'system', 73 | 'flags': ['default'], 74 | 'enabled': True, 75 | } 76 | response = int_post_integration( 77 | payload=payload, **{'headers': module_headers}) 78 | assert response['title'] == 'Test Automation Integration Demo' 79 | # Check that new integration was created 80 | integration_list = int_get_integration(**{'headers': module_headers}) 81 | integration = next( 82 | ( 83 | item for item in integration_list 84 | if item['title'] == 'Test Automation Integration Demo' 85 | ), 86 | False 87 | ) 88 | assert integration, 'Test integration cannot be found' 89 | # Check that integration contains valid data 90 | integration = int_get_integration_by_id( 91 | response['id'], **{'headers': module_headers}) 92 | for key in [ 93 | 'description', 94 | 'title', 95 | 'module_type_id', 96 | 'flags', 97 | 'enabled', 98 | 'tips', 99 | 'short_description' 100 | ]: 101 | assert integration[key] == payload[key] 102 | assert integration['user_id'] 103 | assert integration['org_id'] 104 | assert 'client-' in integration['client_id'] 105 | assert integration['visibility'] == 'org' 106 | assert integration['created_at'] 107 | # Update integration with new data 108 | patch_payload = { 109 | 'tips': 'New tips', 'flags': ['beta']} 110 | response = int_patch_integration( 111 | payload=patch_payload, 112 | entity_id=integration['id'], 113 | **{'headers': module_headers} 114 | ) 115 | assert response['tips'] == 'New tips' 116 | assert response['flags'] == ['beta'] 117 | # Check that entity was updated properly 118 | integration = int_get_integration_by_id( 119 | integration['id'], **{'headers': module_headers}) 120 | assert integration['tips'] == 'New tips' 121 | assert integration['flags'] == ['beta'] 122 | # Delete integration 123 | response = int_delete_integration( 124 | entity_id=integration['id'], **{'headers': module_headers}) 125 | assert response.status_code == 204 126 | response = int_get_integration_by_id( 127 | integration['id'], **{'headers': module_headers}) 128 | assert response['error'] == 'integration_not_found' 129 | 130 | 131 | def test_ctr_positive_end_to_end_module_type(module_headers): 132 | """Perform testing for int module type end point 133 | 134 | ID: 4b423a70-aae4-42fe-9d31-b106b5ab629d 135 | 136 | Steps: 137 | 138 | 1. Send POST request to int module type end point to create new entity 139 | 2. Send GET request to fetch all module type entities and check that 140 | one we created is present here 141 | 3. Send GET request to get specific entity and validate that all fields 142 | contain expected data 143 | 4. Send PATCH request to update entity with specific data 144 | 5. Send DELETE request to delete entity 145 | 146 | Expectedresults: End to end scenario for int module type end point works 147 | properly 148 | 149 | Importance: High 150 | """ 151 | # Validate that module type we plan to use for test automation purpose is 152 | # not present in system 153 | module_type_list = int_get_module_type(**{'headers': module_headers}) 154 | assert len(module_type_list) > 0, ( 155 | 'There are no module types in selected sandbox' 156 | ) 157 | module_type = next( 158 | ( 159 | item for item in module_type_list 160 | if item['default_name'] == 'Test Automation Module Type Demo' 161 | ), 162 | False 163 | ) 164 | assert not module_type, ( 165 | 'Test module type was not removed from the system from previous runs' 166 | ) 167 | # Create new module type 168 | payload = { 169 | 'description': 'Test Automation Module Type Demo', 170 | 'capabilities': [ 171 | {'id': 'health', 'description': 'Healthcheck'}, 172 | {'id': 'deliberate', 'description': 'Deliberation'}, 173 | {'id': 'observe', 'description': 'Enrichments'}, 174 | {'id': 'refer', 'description': 'Reference links'}, 175 | {'id': 'respond', 'description': 'Response actions'}, 176 | {'id': 'tiles', 'description': 'Dashboard tiles'} 177 | ], 178 | 'tips': 'Some useful tips', 179 | 'title': 'Test Automation Module Type Demo', 180 | 'default_name': 'Test Automation Module Type Demo', 181 | 'flags': ['default'], 182 | 'short_description': 'Demo Module Type', 183 | 'enabled': True, 184 | } 185 | response = int_post_module_type( 186 | payload=payload, **{'headers': module_headers}) 187 | assert response['default_name'] == 'Test Automation Module Type Demo' 188 | # Check that new module type was created 189 | module_type_list = int_get_module_type(**{'headers': module_headers}) 190 | module_type = next( 191 | ( 192 | item for item in module_type_list 193 | if item['default_name'] == 'Test Automation Module Type Demo' 194 | ), 195 | False 196 | ) 197 | assert module_type, 'Test module type cannot be found' 198 | # Check that module type contains valid data 199 | module_type = int_get_module_type_by_id( 200 | response['id'], **{'headers': module_headers}) 201 | for key in [ 202 | 'description', 203 | 'capabilities', 204 | 'tips', 205 | 'title', 206 | 'default_name', 207 | 'flags', 208 | 'enabled', 209 | 'short_description' 210 | ]: 211 | assert module_type[key] == payload[key] 212 | assert module_type['user_id'] 213 | assert module_type['org_id'] 214 | assert module_type['client_id'] 215 | assert module_type['record'] == 'relay-module.module/RelayModule' 216 | assert module_type['visibility'] == 'org' 217 | assert module_type['created_at'] 218 | # Update module type with new data 219 | patch_payload = { 220 | 'description': 'New demo', 'flags': ['beta']} 221 | response = int_patch_module_type( 222 | payload=patch_payload, 223 | entity_id=module_type['id'], 224 | **{'headers': module_headers} 225 | ) 226 | assert response['description'] == 'New demo' 227 | assert response['flags'] == ['beta'] 228 | # Check that entity was updated properly 229 | module_type = int_get_module_type_by_id( 230 | module_type['id'], **{'headers': module_headers}) 231 | assert module_type['description'] == 'New demo' 232 | assert module_type['flags'] == ['beta'] 233 | # Delete module type 234 | response = int_delete_module_type( 235 | entity_id=module_type['id'], **{'headers': module_headers}) 236 | assert response.status_code == 204 237 | response = int_get_module_type_by_id( 238 | module_type['id'], **{'headers': module_headers}) 239 | assert response['error'] == 'module_type_not_found' 240 | 241 | 242 | def test_ctr_positive_end_to_end_module_instance(module_headers): 243 | """Perform testing for int module instance end point 244 | 245 | ID: a814f45b-5ed5-49e5-85a0-b1c310333751 246 | 247 | Steps: 248 | 249 | 1. Send POST request to int module instance end point to create new 250 | entity 251 | 2. Send GET request to fetch all module instance entities and check 252 | that one we created is present here 253 | 3. Send GET request to get specific entity and validate that all fields 254 | contain expected data 255 | 4. Send PATCH request to update entity with specific data 256 | 5. Send DELETE request to delete entity 257 | 258 | Expectedresults: End to end scenario for int module instance end point 259 | works properly 260 | 261 | Importance: High 262 | """ 263 | # Validate that module instance we plan to use for test automation purpose 264 | # is not present in system 265 | module_instance_list = int_get_module_instance( 266 | **{'headers': module_headers}) 267 | assert len(module_instance_list) > 0, ( 268 | 'There are no module types in selected sandbox' 269 | ) 270 | module_instance = next( 271 | ( 272 | item for item in module_instance_list 273 | if item['name'] == 'Test Automation Module' 274 | ), 275 | False 276 | ) 277 | assert not module_instance, ( 278 | 'Test module instance was not removed from the system from previous ' 279 | 'runs' 280 | ) 281 | # Create new module instance 282 | payload = { 283 | 'name': 'Test Automation Module', 284 | 'module_type_id': CUSTOM_RELAY_MODULE_TYPE, 285 | 'enabled': True, 286 | 'visibility': 'org', 287 | 'settings': { 288 | 'url': 'https://unayyja4q2.execute-api.eu-central-1.amazonaws.com/' 289 | 'dev', 290 | 'supported-apis': [ 291 | 'health', 292 | 'observe/observables', 293 | 'deliberate/observables', 294 | 'refer/observables', 295 | 'respond/observables', 296 | 'respond/trigger' 297 | ] 298 | } 299 | } 300 | response = int_post_module_instance( 301 | payload=payload, **{'headers': module_headers}) 302 | assert response['name'] == 'Test Automation Module' 303 | # Check that new module instance was created 304 | module_instance_list = int_get_module_instance( 305 | **{'headers': module_headers}) 306 | module_instance = next( 307 | ( 308 | item for item in module_instance_list 309 | if item['name'] == 'Test Automation Module' 310 | ), 311 | False 312 | ) 313 | assert module_instance, 'Test module instance cannot be found' 314 | # Check that module instance contains valid data 315 | module_instance = int_get_module_instance_by_id( 316 | response['id'], **{'headers': module_headers}) 317 | for key in [ 318 | 'name', 319 | 'module_type_id', 320 | 'visibility', 321 | 'settings', 322 | ]: 323 | assert module_instance[key] == payload[key] 324 | assert module_instance['user_id'] 325 | assert module_instance['org_id'] 326 | assert module_instance['client_id'] 327 | assert module_instance['client_id'] 328 | assert module_instance['created_at'] 329 | # Update module instance with new data 330 | patch_payload = {'name': 'Updated Test Automation Module'} 331 | response = int_patch_module_instance( 332 | payload=patch_payload, 333 | entity_id=module_instance['id'], 334 | **{'headers': module_headers} 335 | ) 336 | assert response['name'] == 'Updated Test Automation Module' 337 | # Check that entity was updated properly 338 | module_instance = int_get_module_instance_by_id( 339 | module_instance['id'], **{'headers': module_headers}) 340 | assert module_instance['name'] == 'Updated Test Automation Module' 341 | # Delete module instance 342 | response = int_delete_module_instance( 343 | entity_id=module_instance['id'], **{'headers': module_headers}) 344 | assert response.status_code == 204 345 | response = int_get_module_instance_by_id( 346 | module_instance['id'], **{'headers': module_headers}) 347 | assert response['error'] == 'module_instance_not_found' 348 | 349 | 350 | def test_ctr_positive_module_instance_investigate( 351 | module_headers, module_tool_client): 352 | """Validate that module we create through int module instance end point 353 | can return data for observable investigation process 354 | 355 | ID: 15b9b1c0-b771-42a7-b6a6-861682a09d17 356 | 357 | Steps: 358 | 359 | 1. Send POST request to int module instance end point to create new 360 | entity 361 | 2. Send enrich observe observable request to server 362 | 3. Check response for data from just created custom module 363 | 364 | Expectedresults: Data returned from server contains valid and expected 365 | values 366 | 367 | Importance: High 368 | """ 369 | # Validate that module instance we plan to use for test automation purpose 370 | # is not present in system 371 | module_instance_list = int_get_module_instance( 372 | **{'headers': module_headers}) 373 | module_instance = next( 374 | ( 375 | item for item in module_instance_list 376 | if item['name'] == 'Test Investigation Module' 377 | ), 378 | False 379 | ) 380 | assert not module_instance, ( 381 | 'Test module instance was not removed from the system from previous ' 382 | 'runs' 383 | ) 384 | # Create new module instance 385 | payload = { 386 | 'name': 'Investigation Module', 387 | 'module_type_id': PRIVATE_INTEL_MODULE_TYPE, 388 | 'enabled': True, 389 | 'visibility': 'org', 390 | } 391 | module_instance = int_post_module_instance( 392 | payload=payload, **{'headers': module_headers}) 393 | assert module_instance['name'] == 'Investigation Module' 394 | 395 | try: 396 | # Check that created module can return data for investigation process 397 | response = module_tool_client.enrich.observe.observables( 398 | [{'type': 'sha256', 'value': SHA256_HASH}]) 399 | observables = get_observables(response, module_instance['name']) 400 | assert observables['data']['verdicts']['count'] > 0, ( 401 | 'No observable verdicts returned from server. Check hash value') 402 | assert observables['data']['judgements']['count'] > 0, ( 403 | 'No observable judgements returned from server. Check hash value') 404 | assert observables['data']['verdicts']['docs'][0][ 405 | 'disposition_name'] == 'Malicious' 406 | finally: 407 | # Clean system from created module 408 | int_delete_module_instance( 409 | entity_id=module_instance['id'], **{'headers': module_headers}) 410 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/unit/api/__init__.py -------------------------------------------------------------------------------- /tests/unit/api/assertions.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | from pytest import raises 3 | 4 | 5 | payload = {'ham': 'eggs'} 6 | 7 | 8 | def invoke(api, invocation, response_type='json', command=False): 9 | request, response = MagicMock(), MagicMock() 10 | if command: 11 | response.json.side_effect = [{'ham': 'eggs'}, {'ham': 'eggs'}] 12 | 13 | for method in ['get', 'post', 'patch', 'put', 'delete', 'perform']: 14 | method = getattr(request, method) 15 | method.return_value = response 16 | 17 | invocation(api(request)) 18 | 19 | if command: 20 | assert response.json.call_count == 2 21 | else: 22 | if response_type == 'raw': 23 | response.json.assert_not_called() 24 | if response_type == 'json': 25 | response.json.assert_called_once() 26 | 27 | return request 28 | 29 | 30 | def invoke_with_failure(api, invocation): 31 | class TestError(Exception): 32 | pass 33 | 34 | request, response = MagicMock(), MagicMock() 35 | response.raise_for_status.side_effect = TestError('Oops!') 36 | 37 | for method in ['get', 'post', 'patch', 'put', 'delete', 'perform']: 38 | method = getattr(request, method) 39 | method.return_value = response 40 | 41 | with raises(TestError): 42 | invocation(api(request)) 43 | 44 | response.raise_for_status.assert_called_once_with() 45 | return request 46 | -------------------------------------------------------------------------------- /tests/unit/api/routing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/unit/api/routing/__init__.py -------------------------------------------------------------------------------- /tests/unit/api/routing/test_resolution.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | 3 | from threatresponse.api.routing import Resolution 4 | 5 | 6 | def test_getattr(): 7 | owner = object() 8 | router = object() 9 | 10 | def check(resolution, route): 11 | assert ( 12 | isinstance(resolution, Resolution) and ( 13 | resolution._owner is owner) and ( 14 | resolution._router is router) and ( 15 | resolution._route == route) 16 | ) 17 | 18 | resolution = Resolution(owner, router) 19 | 20 | check(resolution, []) 21 | check(resolution.x, ['x']) 22 | check(resolution.x.y, ['x', 'y']) 23 | check(resolution.x.y.z, ['x', 'y', 'z']) 24 | 25 | 26 | def test_call(): 27 | owner = object() 28 | router = MagicMock() 29 | 30 | method = MagicMock() 31 | router.resolve.return_value = method 32 | 33 | resolution = Resolution(owner, router, ['alpha', 'beta', 'gamma']) 34 | 35 | resolution('a', 'b', 'c', x=1, y=2, z=3) 36 | 37 | router.resolve.assert_called_once_with('alpha.beta.gamma') 38 | 39 | method.assert_called_once_with(owner, 'a', 'b', 'c', x=1, y=2, z=3) 40 | -------------------------------------------------------------------------------- /tests/unit/api/routing/test_router.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import six 3 | 4 | from threatresponse.api.routing import Router 5 | from threatresponse.exceptions import RouteError 6 | 7 | 8 | def test_new(): 9 | router, register = Router.new() 10 | 11 | assert isinstance(router, Router) 12 | assert callable(register) 13 | 14 | # Check that `register` is an instance method bound to `router`. 15 | self_attr = 'im_self' if six.PY2 else '__self__' 16 | assert getattr(register, self_attr) is router 17 | 18 | 19 | def test_register_resolve(): 20 | router, register = Router.new() 21 | 22 | def a(): 23 | pass 24 | 25 | def ab(): 26 | pass 27 | 28 | def abc(): 29 | pass 30 | 31 | # Check that `register` returns a function and can be used as a decorator. 32 | register('a')(a) 33 | register('a.b')(ab) 34 | register('a.b.c')(abc) 35 | 36 | with pytest.raises(RouteError): 37 | register('a.b')(ab) 38 | 39 | # Check that `router` gets populated behind the scenes. 40 | assert router._routes == {'a': a, 'a.b': ab, 'a.b.c': abc} 41 | 42 | assert router.resolve('a') is a 43 | assert router.resolve('a.b') is ab 44 | assert router.resolve('a.b.c') is abc 45 | 46 | with pytest.raises(RouteError): 47 | router.resolve('a.b.c.d') 48 | 49 | 50 | def test_merged(): 51 | def a_x(): 52 | pass 53 | 54 | def b_x(): 55 | pass 56 | 57 | def b_y(): 58 | pass 59 | 60 | def c_y(): 61 | pass 62 | 63 | x = Router({'a': a_x, 'b': b_x}) 64 | y = Router({'b': b_y, 'c': c_y}) 65 | 66 | # Check that `merged` expects instances of `Router` or at least `None`. 67 | 68 | with pytest.raises(TypeError): 69 | Router.merged(x, {}) 70 | 71 | with pytest.raises(TypeError): 72 | Router.merged({}, y) 73 | 74 | # Check that `merged` always returns a new instance. 75 | 76 | z = Router.merged(x, None) 77 | assert z is not x and z._routes == x._routes 78 | 79 | z = Router.merged(None, y) 80 | assert z is not y and z._routes == y._routes 81 | 82 | # Check that `merged` preserves the resolution order. 83 | 84 | z = Router.merged(x, y) 85 | assert z._routes == {'a': a_x, 'b': b_x, 'c': c_y} 86 | 87 | z = Router.merged(y, x) 88 | assert z._routes == {'a': a_x, 'b': b_y, 'c': c_y} 89 | -------------------------------------------------------------------------------- /tests/unit/api/test_casebook.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import IntelAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_casebook_observable_with_id_succeeds(): 7 | request = invoke(IntelAPI, 8 | lambda api: api.casebook.observables('12', payload)) 9 | request.perform.assert_called_once_with( 10 | 'POST', 11 | '/ctia/casebook/12/observables', 12 | json=payload 13 | ) 14 | 15 | 16 | def test_casebook_text_with_id_succeeds(): 17 | request = invoke(IntelAPI, 18 | lambda api: api.casebook.texts('12', payload)) 19 | request.perform.assert_called_once_with( 20 | 'POST', 21 | '/ctia/casebook/12/texts', 22 | json=payload 23 | ) 24 | 25 | 26 | def test_casebook_bulk_with_id_succeeds(): 27 | request = invoke(IntelAPI, 28 | lambda api: api.casebook.bundle('12', payload)) 29 | request.perform.assert_called_once_with( 30 | 'POST', 31 | '/ctia/casebook/12/bundle', 32 | json=payload 33 | ) 34 | -------------------------------------------------------------------------------- /tests/unit/api/test_commands.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import CommandsAPI 2 | from threatresponse.api.commands import build_array_for_targets, \ 3 | build_array_for_verdicts 4 | 5 | from .assertions import * 6 | 7 | 8 | def test_command_verdict_succeeds(): 9 | request = invoke(CommandsAPI, lambda api: api.verdict(payload), 10 | command=True) 11 | 12 | assert request.perform.mock_calls[0].args == ( 13 | 'POST', '/iroh/iroh-inspect/inspect') 14 | assert request.perform.mock_calls[0].kwargs == ( 15 | {'json': {'content': "{'ham': 'eggs'}"}}) 16 | assert request.perform.mock_calls[1].args == ( 17 | 'POST', '/iroh/iroh-enrich/deliberate/observables') 18 | assert request.perform.mock_calls[1].kwargs == ({'json': {'ham': 'eggs'}}) 19 | 20 | 21 | def test_command_targets_succeeds(): 22 | request = invoke(CommandsAPI, lambda api: api.targets(payload), 23 | command=True) 24 | 25 | assert request.perform.mock_calls[0].args == ( 26 | 'POST', '/iroh/iroh-inspect/inspect') 27 | assert request.perform.mock_calls[0].kwargs == ( 28 | {'json': {'content': "{'ham': 'eggs'}"}}) 29 | assert request.perform.mock_calls[1].args == ( 30 | 'POST', '/iroh/iroh-enrich/observe/observables') 31 | assert request.perform.mock_calls[1].kwargs == ({'json': {'ham': 'eggs'}}) 32 | 33 | 34 | def test_build_array_for_a_verdict(): 35 | json = { 36 | 'data': [ 37 | { 38 | 'data': { 39 | 'verdicts': { 40 | 'count': 1, 41 | 'docs': [ 42 | { 43 | 'valid_time': { 44 | 'start_time': '2020-02-06T13:19:39.499Z', 45 | 'end_time': '2020-03-07T13:19:39.499Z' 46 | }, 47 | 'observable': { 48 | 'type': 'domain', 49 | 'value': 'value' 50 | }, 51 | 'type': 'verdict', 52 | 'disposition': 5 53 | } 54 | ] 55 | } 56 | }, 57 | 'module_instance_id': 'first_instance_id', 58 | 'module_type_id': 'first_type_id', 59 | 'module': 'first_module' 60 | }, 61 | { 62 | 'data': { 63 | 'verdicts': { 64 | 'count': 1, 65 | 'docs': [ 66 | { 67 | 'valid_time': { 68 | 'start_time': '2020-02-06T13:19:39.499Z' 69 | }, 70 | 'observable': { 71 | 'type': 'domain', 72 | 'value': 'value' 73 | }, 74 | 'type': 'verdict', 75 | 'disposition': 3 76 | } 77 | ] 78 | } 79 | }, 80 | 'module_instance_id': 'second_instance_id', 81 | 'module_type_id': 'second_type_id', 82 | 'module': 'second_module' 83 | }, 84 | { 85 | 'data': { 86 | 'verdicts': { 87 | 'count': 1, 88 | 'docs': [ 89 | { 90 | 'valid_time': { 91 | 'start_time': '2020-02-06T13:19:39.875Z', 92 | 'end_time': '2020-03-07T13:19:39.875Z' 93 | }, 94 | 'observable': { 95 | 'type': 'domain', 96 | 'value': 'value' 97 | }, 98 | 'type': 'verdict', 99 | 'disposition': 1 100 | } 101 | ] 102 | } 103 | }, 104 | 'module_instance_id': 'third_instance_id', 105 | 'module_type_id': 'third_type_id', 106 | 'module': 'third_module' 107 | } 108 | ] 109 | } 110 | array_for_a_verdict = build_array_for_verdicts(json) 111 | assert array_for_a_verdict == [ 112 | {'disposition_name': 'Unknown', 'observable_value': 'value', 113 | 'expiration': '2020-03-07T13:19:39.499Z', 114 | 'module': 'first_module', 'module_instance_id': 'first_instance_id', 115 | 'module_type_id': 'first_type_id', 'observable_type': 'domain'}, 116 | {'disposition_name': 'Suspicious', 'observable_value': 'value', 117 | 'expiration': '', # N/A 118 | 'module': 'second_module', 'module_instance_id': 'second_instance_id', 119 | 'module_type_id': 'second_type_id', 'observable_type': 'domain'}, 120 | {'disposition_name': 'Clean', 'observable_value': 'value', 121 | 'expiration': '2020-03-07T13:19:39.875Z', 122 | 'module': 'third_module', 'module_instance_id': 'third_instance_id', 123 | 'module_type_id': 'third_type_id', 'observable_type': 'domain'}] 124 | 125 | 126 | def test_build_array_for_targets(): 127 | json = { 128 | 'data': [ 129 | { 130 | 'data': { 131 | 'sightings': { 132 | 'count': 1, 133 | 'docs': [ 134 | { 135 | 'targets': [ 136 | { 137 | 'observables': [ 138 | { 139 | 'type': 'email', 140 | 'value': 'example.com' 141 | } 142 | ], 143 | 'type': 'email', 144 | } 145 | ], 146 | } 147 | ] 148 | } 149 | }, 150 | 'module_instance_id': 'module_instance_id', 151 | 'module_type_id': 'module_type_id', 152 | 'module': 'module' 153 | } 154 | ] 155 | } 156 | array_for_a_targets = build_array_for_targets(json) 157 | assert array_for_a_targets == [{'targets': [ 158 | {'observables': [{'type': 'email', 'value': 'example.com'}], 159 | 'type': 'email'}], 'module': 'module', 160 | 'module_instance_id': 'module_instance_id', 161 | 'module_type_id': 'module_type_id'}] 162 | -------------------------------------------------------------------------------- /tests/unit/api/test_enrich.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import EnrichAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_health_succeeds(): 7 | request = invoke(EnrichAPI, lambda api: api.health()) 8 | request.perform.assert_called_once_with( 9 | 'POST', 10 | '/iroh/iroh-enrich/health' 11 | ) 12 | 13 | 14 | def test_health_fails(): 15 | request = invoke_with_failure(EnrichAPI, lambda api: api.health()) 16 | request.perform.assert_called_once_with( 17 | 'POST', 18 | '/iroh/iroh-enrich/health' 19 | ) 20 | 21 | 22 | def test_health_with_id_succeeds(): 23 | request = invoke( 24 | EnrichAPI, 25 | lambda api: api.health.module_instance_id('id') 26 | ) 27 | request.perform.assert_called_once_with( 28 | 'POST', 29 | '/iroh/iroh-enrich/health/id' 30 | ) 31 | 32 | 33 | def test_health_with_id_fails(): 34 | request = invoke_with_failure( 35 | EnrichAPI, 36 | lambda api: api.health.module_instance_id('id') 37 | ) 38 | request.perform.assert_called_once_with( 39 | 'POST', 40 | '/iroh/iroh-enrich/health/id' 41 | ) 42 | 43 | 44 | def test_deliberate_observables_succeeds(): 45 | request = invoke( 46 | EnrichAPI, 47 | lambda api: api.deliberate.observables(payload) 48 | ) 49 | request.perform.assert_called_once_with( 50 | 'POST', 51 | '/iroh/iroh-enrich/deliberate/observables', 52 | json=payload 53 | ) 54 | 55 | 56 | def test_deliberate_observables_fails(): 57 | request = invoke_with_failure( 58 | EnrichAPI, 59 | lambda api: api.deliberate.observables(payload) 60 | ) 61 | request.perform.assert_called_once_with( 62 | 'POST', 63 | '/iroh/iroh-enrich/deliberate/observables', 64 | json=payload 65 | ) 66 | 67 | 68 | def test_deliberate_sighting_succeeds(): 69 | request = invoke( 70 | EnrichAPI, 71 | lambda api: api.deliberate.sighting(payload) 72 | ) 73 | request.perform.assert_called_once_with( 74 | 'POST', 75 | '/iroh/iroh-enrich/deliberate/sighting', 76 | json=payload 77 | ) 78 | 79 | 80 | def test_deliberate_sighting_fails(): 81 | request = invoke_with_failure( 82 | EnrichAPI, 83 | lambda api: api.deliberate.sighting(payload) 84 | ) 85 | request.perform.assert_called_once_with( 86 | 'POST', 87 | '/iroh/iroh-enrich/deliberate/sighting', 88 | json=payload 89 | ) 90 | 91 | 92 | def test_deliberate_sighting_ref_succeeds(): 93 | request = invoke( 94 | EnrichAPI, 95 | lambda api: api.deliberate.sighting_ref(payload) 96 | ) 97 | request.perform.assert_called_once_with( 98 | 'POST', 99 | '/iroh/iroh-enrich/deliberate/sighting_ref', 100 | json=payload 101 | ) 102 | 103 | 104 | def test_deliberate_sighting_ref_fails(): 105 | request = invoke_with_failure( 106 | EnrichAPI, 107 | lambda api: api.deliberate.sighting_ref(payload) 108 | ) 109 | request.perform.assert_called_once_with( 110 | 'POST', 111 | '/iroh/iroh-enrich/deliberate/sighting_ref', 112 | json=payload 113 | ) 114 | 115 | 116 | def test_observe_observables_succeeds(): 117 | request = invoke(EnrichAPI, lambda api: api.observe.observables(payload)) 118 | request.perform.assert_called_once_with( 119 | 'POST', 120 | '/iroh/iroh-enrich/observe/observables', 121 | json=payload 122 | ) 123 | 124 | 125 | def test_observe_observables_fails(): 126 | request = invoke_with_failure( 127 | EnrichAPI, 128 | lambda api: api.observe.observables(payload) 129 | ) 130 | request.perform.assert_called_once_with( 131 | 'POST', 132 | '/iroh/iroh-enrich/observe/observables', 133 | json=payload 134 | ) 135 | 136 | 137 | def test_observe_sighting_succeeds(): 138 | request = invoke(EnrichAPI, lambda api: api.observe.sighting(payload)) 139 | request.perform.assert_called_once_with( 140 | 'POST', 141 | '/iroh/iroh-enrich/observe/sighting', 142 | json=payload 143 | ) 144 | 145 | 146 | def test_observe_sighting_fails(): 147 | request = invoke_with_failure( 148 | EnrichAPI, 149 | lambda api: api.observe.sighting(payload) 150 | ) 151 | request.perform.assert_called_once_with( 152 | 'POST', 153 | '/iroh/iroh-enrich/observe/sighting', 154 | json=payload 155 | ) 156 | 157 | 158 | def test_observe_sighting_ref_succeeds(): 159 | request = invoke(EnrichAPI, lambda api: api.observe.sighting_ref(payload)) 160 | request.perform.assert_called_once_with( 161 | 'POST', 162 | '/iroh/iroh-enrich/observe/sighting_ref', 163 | json=payload 164 | ) 165 | 166 | 167 | def test_observe_sighting_ref_fails(): 168 | request = invoke_with_failure( 169 | EnrichAPI, 170 | lambda api: api.observe.sighting_ref(payload) 171 | ) 172 | request.perform.assert_called_once_with( 173 | 'POST', 174 | '/iroh/iroh-enrich/observe/sighting_ref', 175 | json=payload 176 | ) 177 | 178 | 179 | def test_refer_observables_succeeds(): 180 | request = invoke(EnrichAPI, lambda api: api.refer.observables(payload)) 181 | request.perform.assert_called_once_with( 182 | 'POST', 183 | '/iroh/iroh-enrich/refer/observables', 184 | json=payload 185 | ) 186 | 187 | 188 | def test_refer_observables_fails(): 189 | request = invoke_with_failure( 190 | EnrichAPI, 191 | lambda api: api.refer.observables(payload) 192 | ) 193 | request.perform.assert_called_once_with( 194 | 'POST', 195 | '/iroh/iroh-enrich/refer/observables', 196 | json=payload 197 | ) 198 | 199 | 200 | def test_refer_sighting_succeeds(): 201 | request = invoke(EnrichAPI, lambda api: api.refer.sighting(payload)) 202 | request.perform.assert_called_once_with( 203 | 'POST', 204 | '/iroh/iroh-enrich/refer/sighting', 205 | json=payload 206 | ) 207 | 208 | 209 | def test_refer_sighting_fails(): 210 | request = invoke_with_failure( 211 | EnrichAPI, 212 | lambda api: api.refer.sighting(payload) 213 | ) 214 | request.perform.assert_called_once_with( 215 | 'POST', 216 | '/iroh/iroh-enrich/refer/sighting', 217 | json=payload 218 | ) 219 | 220 | 221 | def test_refer_sighting_ref_succeeds(): 222 | request = invoke(EnrichAPI, lambda api: api.refer.sighting_ref(payload)) 223 | request.perform.assert_called_once_with( 224 | 'POST', 225 | '/iroh/iroh-enrich/refer/sighting_ref', 226 | json=payload 227 | ) 228 | 229 | 230 | def test_refer_sighting_ref_fails(): 231 | request = invoke_with_failure( 232 | EnrichAPI, 233 | lambda api: api.refer.sighting_ref(payload) 234 | ) 235 | request.perform.assert_called_once_with( 236 | 'POST', 237 | '/iroh/iroh-enrich/refer/sighting_ref', 238 | json=payload 239 | ) 240 | 241 | 242 | def test_settings_succeeds(): 243 | request = invoke(EnrichAPI, lambda api: api.settings.get()) 244 | request.perform.assert_called_once_with( 245 | 'GET', 246 | '/iroh/iroh-enrich/settings' 247 | ) 248 | 249 | 250 | def test_settings_fails(): 251 | request = invoke_with_failure( 252 | EnrichAPI, 253 | lambda api: api.settings.get() 254 | ) 255 | request.perform.assert_called_once_with( 256 | 'GET', 257 | '/iroh/iroh-enrich/settings' 258 | ) 259 | -------------------------------------------------------------------------------- /tests/unit/api/test_entity.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from threatresponse.api.entity import EntityAPI 4 | 5 | from .assertions import * 6 | 7 | 8 | def entity_api(url): 9 | return partial(EntityAPI, url=url) 10 | 11 | 12 | def test_get_succeeds(): 13 | request = invoke(entity_api('/x'), lambda api: api.get()) 14 | request.perform.assert_called_once_with( 15 | 'GET', 16 | '/x' 17 | ) 18 | 19 | request = invoke(entity_api('/x'), 20 | lambda api: api.get(response_type='raw'), 21 | 'raw') 22 | request.perform.assert_called_once_with( 23 | 'GET', 24 | '/x' 25 | ) 26 | 27 | 28 | def test_get_with_id_succeeds(): 29 | request = invoke(entity_api('/x'), lambda api: api.get('42')) 30 | request.perform.assert_called_once_with( 31 | 'GET', 32 | '/x/42' 33 | ) 34 | 35 | request = invoke(entity_api('/x'), 36 | lambda api: api.get('42', response_type='raw'), 37 | 'raw') 38 | request.perform.assert_called_once_with( 39 | 'GET', 40 | '/x/42' 41 | ) 42 | 43 | 44 | def test_get_with_id_and_fields_succeeds(): 45 | params = {'fields': ['schema_version', 'revision']} 46 | 47 | request = invoke(entity_api('/x'), 48 | lambda api: api.get('42', params=params)) 49 | request.perform.assert_called_once_with( 50 | 'GET', 51 | '/x/42', 52 | params=params 53 | ) 54 | 55 | request = invoke(entity_api('/x'), 56 | lambda api: api.get('42', 57 | params=params, 58 | response_type='raw'), 59 | 'raw') 60 | request.perform.assert_called_once_with( 61 | 'GET', 62 | '/x/42', 63 | params=params 64 | ) 65 | 66 | 67 | def test_post_succeeds(): 68 | request = invoke(entity_api('/x'), lambda api: api.post(payload)) 69 | request.perform.assert_called_once_with( 70 | 'POST', 71 | '/x', 72 | json=payload 73 | ) 74 | 75 | request = invoke(entity_api('/x'), 76 | lambda api: api.post(payload, response_type='raw'), 77 | 'raw') 78 | request.perform.assert_called_once_with( 79 | 'POST', 80 | '/x', 81 | json=payload 82 | ) 83 | 84 | 85 | def test_delete_succeeds(): 86 | request = invoke(entity_api('/x'), lambda api: api.delete('42'), 'raw') 87 | request.perform.assert_called_once_with( 88 | 'DELETE', 89 | '/x/42' 90 | ) 91 | 92 | 93 | def test_put_succeeds(): 94 | request = invoke(entity_api('/x'), lambda api: api.put('12', payload)) 95 | request.perform.assert_called_once_with( 96 | 'PUT', 97 | '/x/12', 98 | json=payload 99 | ) 100 | 101 | request = invoke(entity_api('/x'), 102 | lambda api: api.put('12', payload, response_type='raw'), 103 | 'raw') 104 | request.perform.assert_called_once_with( 105 | 'PUT', 106 | '/x/12', 107 | json=payload 108 | ) 109 | -------------------------------------------------------------------------------- /tests/unit/api/test_event.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import IntelAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_event_with_id_succeeds(): 7 | request = invoke(IntelAPI, lambda api: api.event.history('12')) 8 | request.perform.assert_called_once_with( 9 | 'GET', 10 | '/ctia/event/history/12' 11 | ) 12 | -------------------------------------------------------------------------------- /tests/unit/api/test_feed.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import IntelAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_feed_view_with_id_succeeds(): 7 | request = invoke(IntelAPI, 8 | lambda api: api.feed.view(12, 'test')) 9 | request.perform.assert_called_once_with( 10 | 'GET', 11 | '/ctia/feed/12/view', 12 | params={'s': 'test'} 13 | ) 14 | 15 | 16 | def test_feed_view_txt_with_id_succeeds(): 17 | request = invoke(IntelAPI, 18 | lambda api: api.feed.view.txt('12', 'test'), 19 | response_type='text') 20 | request.perform.assert_called_once_with( 21 | 'GET', 22 | '/ctia/feed/12/view.txt', 23 | params={'s': 'test'} 24 | ) 25 | -------------------------------------------------------------------------------- /tests/unit/api/test_feedback.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api import IntelAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_feedback_without_id_succeeds(): 7 | request = invoke(IntelAPI, lambda api: api.feedback.get()) 8 | request.perform.assert_called_once_with( 9 | 'GET', 10 | '/ctia/feedback' 11 | ) 12 | -------------------------------------------------------------------------------- /tests/unit/api/test_inspect.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api.inspect import InspectAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_inspect_succeeds(): 7 | request = invoke(InspectAPI, lambda api: api.inspect(payload)) 8 | request.perform.assert_called_once_with( 9 | 'POST', 10 | '/iroh/iroh-inspect/inspect', 11 | json=payload 12 | ) 13 | 14 | 15 | def test_inspect_fails(): 16 | request = invoke_with_failure(InspectAPI, lambda api: api.inspect(payload)) 17 | request.perform.assert_called_once_with( 18 | 'POST', 19 | '/iroh/iroh-inspect/inspect', 20 | json=payload 21 | ) 22 | -------------------------------------------------------------------------------- /tests/unit/api/test_intel_entity.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from threatresponse.api.entity import IntelEntityAPI 4 | 5 | from .assertions import * 6 | 7 | 8 | def intel_entity_api(url): 9 | return partial(IntelEntityAPI, url=url) 10 | 11 | 12 | def test_get_by_external_id_succeeds(): 13 | request = invoke(intel_entity_api('/x'), lambda api: api.external_id('42')) 14 | request.perform.assert_called_once_with( 15 | 'GET', 16 | '/x/external_id/42' 17 | ) 18 | 19 | request = invoke( 20 | intel_entity_api('/x'), 21 | lambda api: api.external_id('42', response_type='raw'), 22 | 'raw' 23 | ) 24 | request.perform.assert_called_once_with( 25 | 'GET', 26 | '/x/external_id/42' 27 | ) 28 | 29 | 30 | def test_search_by_id_succeeds(): 31 | params = {'id': 12} 32 | 33 | request = invoke( 34 | intel_entity_api('/x'), 35 | lambda api: api.search.get(params=params) 36 | ) 37 | request.perform.assert_called_once_with( 38 | 'GET', 39 | '/x/search', 40 | params=params 41 | ) 42 | 43 | request = invoke( 44 | intel_entity_api('/x'), 45 | lambda api: api.search.get(params=params, response_type='raw'), 46 | 'raw' 47 | ) 48 | request.perform.assert_called_once_with( 49 | 'GET', 50 | '/x/search', 51 | params=params 52 | ) 53 | 54 | 55 | def test_search_with_query_succeeds(): 56 | params = {'query': '*'} 57 | 58 | request = invoke( 59 | intel_entity_api('/x'), 60 | lambda api: api.search.get(params=params) 61 | ) 62 | request.perform.assert_called_once_with( 63 | 'GET', 64 | '/x/search', 65 | params=params 66 | ) 67 | 68 | request = invoke( 69 | intel_entity_api('/x'), 70 | lambda api: api.search.get(params=params, response_type='raw'), 71 | 'raw' 72 | ) 73 | request.perform.assert_called_once_with( 74 | 'GET', 75 | '/x/search', 76 | params=params 77 | ) 78 | -------------------------------------------------------------------------------- /tests/unit/api/test_profile.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api.profile import ProfileAPI 2 | from .assertions import * 3 | 4 | 5 | def test_whoami_succeeds(): 6 | request = invoke(ProfileAPI, lambda api: api.whoami()) 7 | request.perform.assert_called_once_with( 8 | 'GET', 9 | '/iroh/profile/whoami', 10 | ) 11 | 12 | 13 | def test_whoami_fails(): 14 | request = invoke_with_failure(ProfileAPI, lambda api: api.whoami()) 15 | request.perform.assert_called_once_with( 16 | 'GET', 17 | '/iroh/profile/whoami', 18 | ) 19 | 20 | 21 | def test_org_get_succeeds(): 22 | request = invoke(ProfileAPI, lambda api: api.org.get()) 23 | request.perform.assert_called_once_with( 24 | 'GET', 25 | '/iroh/profile/org', 26 | ) 27 | 28 | 29 | def test_org_get_fails(): 30 | request = invoke_with_failure(ProfileAPI, lambda api: api.org.get()) 31 | request.perform.assert_called_once_with( 32 | 'GET', 33 | '/iroh/profile/org', 34 | ) 35 | 36 | 37 | def test_org_post_succeeds(): 38 | request = invoke(ProfileAPI, lambda api: api.org.post(payload)) 39 | request.perform.assert_called_once_with( 40 | 'POST', 41 | '/iroh/profile/org', 42 | json=payload, 43 | ) 44 | 45 | 46 | def test_org_post_fails(): 47 | request = invoke_with_failure( 48 | ProfileAPI, 49 | lambda api: api.org.post(payload) 50 | ) 51 | request.perform.assert_called_once_with( 52 | 'POST', 53 | '/iroh/profile/org', 54 | json=payload 55 | ) 56 | -------------------------------------------------------------------------------- /tests/unit/api/test_response.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api.response import ResponseAPI 2 | 3 | from .assertions import * 4 | 5 | 6 | def test_respond_observables_succeeds(): 7 | request = invoke(ResponseAPI, lambda api: api.respond.observables(payload)) 8 | request.perform.assert_called_once_with( 9 | 'POST', 10 | '/iroh/iroh-response/respond/observables', 11 | json=payload 12 | ) 13 | 14 | 15 | def test_respond_sighting_succeeds(): 16 | request = invoke(ResponseAPI, lambda api: api.respond.sighting(payload)) 17 | request.perform.assert_called_once_with( 18 | 'POST', 19 | '/iroh/iroh-response/respond/sighting', 20 | json=payload 21 | ) 22 | 23 | 24 | def test_respond_trigger_succeeds(): 25 | params = {'x': 1, 'y': 2, 'z': 3} 26 | 27 | request = invoke( 28 | ResponseAPI, 29 | lambda api: api.respond.trigger( 30 | 'Monty Python!', 31 | 'x|y&z', 32 | 'spam', 33 | 'eggs', 34 | params=params 35 | ) 36 | ) 37 | 38 | params.update({ 39 | 'observable_type': 'spam', 40 | 'observable_value': 'eggs' 41 | }) 42 | 43 | request.perform.assert_called_once_with( 44 | 'POST', 45 | '/iroh/iroh-response/respond/trigger/Monty%20Python%21/x%7Cy%26z', 46 | params=params 47 | ) 48 | 49 | 50 | def test_respond_trigger_fails(): 51 | params = {'x': 1, 'y': 2, 'z': 3} 52 | 53 | request = invoke_with_failure( 54 | ResponseAPI, 55 | lambda api: api.respond.trigger( 56 | 'Monty Python!', 57 | 'x|y&z', 58 | 'spam', 59 | 'eggs', 60 | params=params 61 | ) 62 | ) 63 | 64 | params.update({ 65 | 'observable_type': 'spam', 66 | 'observable_value': 'eggs' 67 | }) 68 | 69 | request.perform.assert_called_once_with( 70 | 'POST', 71 | '/iroh/iroh-response/respond/trigger/Monty%20Python%21/x%7Cy%26z', 72 | params=params, 73 | ) 74 | 75 | 76 | def test_respond_observables_fails(): 77 | request = invoke_with_failure( 78 | ResponseAPI, 79 | lambda api: api.respond.observables(payload) 80 | ) 81 | 82 | request.perform.assert_called_once_with( 83 | 'POST', 84 | '/iroh/iroh-response/respond/observables', 85 | json=payload 86 | ) 87 | 88 | 89 | def test_respond_sighting_fails(): 90 | request = invoke_with_failure( 91 | ResponseAPI, 92 | lambda api: api.respond.sighting(payload) 93 | ) 94 | 95 | request.perform.assert_called_once_with( 96 | 'POST', 97 | '/iroh/iroh-response/respond/sighting', 98 | json=payload 99 | ) 100 | -------------------------------------------------------------------------------- /tests/unit/api/test_user_mgmt.py: -------------------------------------------------------------------------------- 1 | from threatresponse.api.user_mgmt import UserMgmtAPI 2 | from .assertions import * 3 | 4 | test_user_id = "00000000-0000-0000-0000-000000000000" 5 | 6 | 7 | def test_users_get_succeeds(): 8 | request = invoke( 9 | UserMgmtAPI, 10 | lambda api: api.users.get(user_id=test_user_id) 11 | ) 12 | request.perform.assert_called_once_with( 13 | 'GET', 14 | '/iroh/user-mgmt/users/{}'.format(test_user_id), 15 | ) 16 | 17 | 18 | def test_users_get_fails(): 19 | request = invoke_with_failure( 20 | UserMgmtAPI, 21 | lambda api: api.users.get(user_id=test_user_id) 22 | ) 23 | request.perform.assert_called_once_with( 24 | 'GET', 25 | '/iroh/user-mgmt/users/{}'.format(test_user_id), 26 | ) 27 | 28 | 29 | def test_users_post_succeeds(): 30 | request = invoke( 31 | UserMgmtAPI, 32 | lambda api: api.users.post(user_id=test_user_id, payload=payload) 33 | ) 34 | request.perform.assert_called_once_with( 35 | 'POST', 36 | '/iroh/user-mgmt/users/{}'.format(test_user_id), 37 | json=payload, 38 | ) 39 | 40 | 41 | def test_users_post_fails(): 42 | request = invoke_with_failure( 43 | UserMgmtAPI, 44 | lambda api: api.users.post(user_id=test_user_id, payload=payload) 45 | ) 46 | request.perform.assert_called_once_with( 47 | 'POST', 48 | '/iroh/user-mgmt/users/{}'.format(test_user_id), 49 | json=payload, 50 | ) 51 | 52 | 53 | def test_batch_users_succeeds(): 54 | request = invoke( 55 | UserMgmtAPI, 56 | lambda api: api.batch.users(user_ids=[test_user_id]) 57 | ) 58 | request.perform.assert_called_once_with( 59 | 'GET', 60 | '/iroh/user-mgmt/batch/users', 61 | params={'id': ['00000000-0000-0000-0000-000000000000']} 62 | ) 63 | 64 | 65 | def test_batch_users_fails(): 66 | request = invoke_with_failure( 67 | UserMgmtAPI, 68 | lambda api: api.batch.users(user_ids=[test_user_id]) 69 | ) 70 | request.perform.assert_called_once_with( 71 | 'GET', 72 | '/iroh/user-mgmt/batch/users', 73 | params={'id': ['00000000-0000-0000-0000-000000000000']} 74 | ) 75 | 76 | 77 | def test_search_users_succeeds(): 78 | params = { 79 | "sort_by": "foo", 80 | "sort_order": "desc", 81 | "offset": "1", 82 | "search_after": ["bar"], 83 | "limit": "10", 84 | } 85 | request = invoke( 86 | UserMgmtAPI, 87 | lambda api: api.search.users(payload=payload, **params) 88 | ) 89 | request.perform.assert_called_once_with( 90 | 'POST', 91 | '/iroh/user-mgmt/search/users', 92 | json=payload, 93 | params=params 94 | ) 95 | 96 | 97 | def test_search_users_fails(): 98 | request = invoke( 99 | UserMgmtAPI, 100 | lambda api: api.search.users(payload=payload) 101 | ) 102 | request.perform.assert_called_once_with( 103 | 'POST', 104 | '/iroh/user-mgmt/search/users', 105 | json=payload, 106 | params={ 107 | "sort_by": None, 108 | "sort_order": None, 109 | "offset": None, 110 | "search_after": None, 111 | "limit": None, 112 | } 113 | ) 114 | -------------------------------------------------------------------------------- /tests/unit/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoSecurity/tr-05-api-module/ce0f8d583b2fce3aadcc5a5c174a5b2b23e14d72/tests/unit/request/__init__.py -------------------------------------------------------------------------------- /tests/unit/request/test_authorized.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | from six.moves.http_client import UNAUTHORIZED 3 | 4 | from threatresponse.request.authorized import ( 5 | ClientAuthorizedRequest, 6 | TokenAuthorizedRequest 7 | ) 8 | 9 | 10 | def test_that_client_authorized_request_provides_header_with_token(): 11 | request = MagicMock() 12 | request.post.return_value = token('Cake') 13 | 14 | authorized = ClientAuthorizedRequest(request, 'x', 'y') 15 | authorized.post('/some', headers={'Just': 'Test'}) 16 | 17 | request.perform.assert_called_once_with( 18 | 'POST', 19 | '/some', 20 | headers={ 21 | 'Just': 'Test', 22 | 'Authorization': 'Bearer Cake' 23 | } 24 | ) 25 | 26 | 27 | def test_that_token_authorized_request_provides_header_with_token(): 28 | request = MagicMock() 29 | request.post.side_effect = ({'data': 'test'}, {}) 30 | 31 | authorized = TokenAuthorizedRequest(request, 'test_token') 32 | authorized.post('/some') 33 | 34 | request.perform.assert_called_with( 35 | 'POST', 36 | '/some', 37 | headers={ 38 | 'Authorization': 'Bearer test_token' 39 | } 40 | ) 41 | 42 | 43 | def test_that_token_authorized_request_check_token(): 44 | request = MagicMock() 45 | request.post.return_value = {'data': 'test'} 46 | 47 | TokenAuthorizedRequest(request, 'test_token') 48 | 49 | request.perform.assert_called_once_with( 50 | 'GET', 51 | 'https://visibility.amp.cisco.com/iroh/iroh-enrich/settings', 52 | headers={ 53 | 'Accept': 'application/json', 54 | 'Authorization': 'Bearer test_token' 55 | } 56 | ) 57 | 58 | 59 | def test_that_authorized_request_retrieves_token_on_init(): 60 | request = MagicMock() 61 | 62 | ClientAuthorizedRequest(request, 'x', 'y') 63 | 64 | request.post.assert_called_once_with( 65 | 'https://visibility.amp.cisco.com/iroh/oauth2/token', 66 | auth=('x', 'y'), 67 | data={'grant_type': 'client_credentials'}, 68 | headers={ 69 | 'Content-Type': 'application/x-www-form-urlencoded', 70 | 'Accept': 'application/json' 71 | } 72 | ) 73 | 74 | 75 | def test_that_authorized_request_retrieves_token_on_expiration_and_retries(): 76 | response = MagicMock() 77 | response.status_code = UNAUTHORIZED 78 | 79 | request = MagicMock() 80 | request.post.return_value = token('Cake') 81 | request.perform.return_value = response 82 | 83 | authorized = ClientAuthorizedRequest(request, 'x', 'y') 84 | authorized.post('/some') 85 | 86 | assert request.post.call_count == 2 87 | assert ( 88 | request.post.call_args_list[0] == request.post.call_args_list[1] 89 | ) 90 | 91 | assert request.perform.call_count == 2 92 | assert ( 93 | request.perform.call_args_list[0] == request.perform.call_args_list[1] 94 | ) 95 | 96 | 97 | def token(bearer): 98 | mocked = MagicMock() 99 | mocked.json.return_value = {'access_token': bearer} 100 | 101 | return mocked 102 | -------------------------------------------------------------------------------- /tests/unit/request/test_logged.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mock import MagicMock 3 | 4 | from threatresponse.request.logged import LoggedRequest 5 | 6 | 7 | def test_that_logged_request_logs_success(): 8 | inner_request = MagicMock() 9 | inner_request.perform.return_value = response(200) 10 | 11 | logger = MagicMock() 12 | 13 | request = LoggedRequest(inner_request, logger) 14 | request.get('/foo/bar/123') 15 | 16 | inner_request.perform.assert_called_once_with('GET', '/foo/bar/123') 17 | 18 | logger.info.assert_called_once_with('GET /foo/bar/123 200 OK') 19 | 20 | 21 | def test_that_logged_request_logs_error_when_response_fails(): 22 | inner_request = MagicMock() 23 | inner_request.perform.return_value = response(404) 24 | 25 | logger = MagicMock() 26 | 27 | request = LoggedRequest(inner_request, logger) 28 | request.get('/foo/bar/123') 29 | 30 | inner_request.perform.assert_called_once_with('GET', '/foo/bar/123') 31 | 32 | logger.error.assert_called_once_with('GET /foo/bar/123 404 Not Found') 33 | 34 | 35 | def test_that_logged_request_logs_error_when_exception_occurs(): 36 | class TestError(Exception): 37 | pass 38 | 39 | inner_request = MagicMock() 40 | inner_request.perform.side_effect = TestError('Something went wrong.') 41 | 42 | logger = MagicMock() 43 | 44 | request = LoggedRequest(inner_request, logger) 45 | 46 | with pytest.raises(TestError): 47 | request.get('/foo/bar/123') 48 | 49 | inner_request.perform.assert_called_once_with('GET', '/foo/bar/123') 50 | 51 | logger.exception.assert_called_once_with('GET /foo/bar/123') 52 | 53 | 54 | def response(status_code): 55 | mocked = MagicMock() 56 | mocked.status_code = status_code 57 | mocked.ok = 100 <= status_code < 400 58 | 59 | return mocked 60 | -------------------------------------------------------------------------------- /tests/unit/request/test_proxied.py: -------------------------------------------------------------------------------- 1 | from threatresponse.request.proxied import ProxiedRequest 2 | 3 | 4 | def test_that_proxied_request_properly_configures_session_proxies(): 5 | proxy = 'proxy://111.222.333.444:5555' 6 | request = ProxiedRequest(proxy=proxy) 7 | 8 | assert request._proxy == proxy 9 | assert request._session.proxies == {'http': proxy, 'https': proxy} 10 | -------------------------------------------------------------------------------- /tests/unit/request/test_relative.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | 3 | from threatresponse.request.relative import RelativeRequest 4 | 5 | 6 | def test_that_relative_request_invokes_inner_request(): 7 | inner_request = MagicMock() 8 | 9 | request = RelativeRequest(inner_request, 'http://one.com') 10 | request.post('/two') 11 | 12 | inner_request.perform.assert_called_once() 13 | 14 | 15 | def test_that_relative_request_builds_correct_parameters(): 16 | inner_request = MagicMock() 17 | 18 | request = RelativeRequest(inner_request, 'http://one.com') 19 | request.post('/two', json={'some': 'data'}) 20 | 21 | inner_request.perform.assert_called_once_with( 22 | 'POST', 'http://one.com/two', json={'some': 'data'} 23 | ) 24 | 25 | 26 | def test_that_relative_request_returns_correct_response(): 27 | inner_request = MagicMock() 28 | inner_request.perform.return_value = 'duck' 29 | 30 | request = RelativeRequest(inner_request, 'http://one.com') 31 | response = request.post('/two') 32 | 33 | assert response == 'duck' 34 | -------------------------------------------------------------------------------- /tests/unit/request/test_response.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import six 3 | from mock import MagicMock 4 | from requests import HTTPError 5 | 6 | from threatresponse.request.response import Response 7 | 8 | 9 | def test_that_getattr_and_setattr_are_delegated(): 10 | inner_response = MagicMock() 11 | 12 | response = Response(inner_response) 13 | response.foo = 'bar' 14 | response.spam('eggs') 15 | 16 | assert inner_response.foo == 'bar' 17 | inner_response.spam.assert_called_once_with('eggs') 18 | 19 | 20 | def test_that_raise_for_status_extends_error_message(): 21 | inner_response = MagicMock() 22 | inner_response.text = '{"foo": "bar", "spam": ["eggs"]}' 23 | 24 | error = HTTPError('Something went wrong.') 25 | error.response = inner_response 26 | 27 | inner_response.raise_for_status.side_effect = error 28 | 29 | response = Response(inner_response) 30 | with pytest.raises(HTTPError): 31 | response.raise_for_status() 32 | 33 | inner_response.raise_for_status.assert_called_once_with() 34 | assert error.args == ( 35 | 'Something went wrong.\n' 36 | '{\n' 37 | ' "foo": "bar",' + (' ' if six.PY2 else '') + '\n' 38 | ' "spam": [\n' 39 | ' "eggs"\n' 40 | ' ]\n' 41 | '}', 42 | ) 43 | -------------------------------------------------------------------------------- /tests/unit/request/test_standard.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | 3 | from threatresponse.request.response import Response 4 | from threatresponse.request.standard import StandardRequest 5 | 6 | 7 | @patch('requests.Session.request') 8 | def test_that_standard_request_wraps_session_response(inner_session_request): 9 | request = StandardRequest() 10 | response = request.post( 11 | '/foo/bar', 12 | json={'spam': 'eggs'}, 13 | headers={'Threat': 'Response'}, 14 | ) 15 | 16 | inner_session_request.assert_called_once_with( 17 | 'POST', 18 | '/foo/bar', 19 | json={'spam': 'eggs'}, 20 | headers={'Threat': 'Response'}, 21 | ) 22 | assert isinstance(response, Response) 23 | -------------------------------------------------------------------------------- /tests/unit/request/test_timed.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | 3 | from threatresponse.request.timed import TimedRequest 4 | 5 | 6 | def test_that_timed_request_sets_default_timeout_if_not_specified(): 7 | inner_request = MagicMock() 8 | default_timeout = 3.14 9 | 10 | request = TimedRequest(inner_request, default_timeout) 11 | request.get('/foo/bar/123') 12 | 13 | inner_request.perform.assert_called_once_with( 14 | 'GET', '/foo/bar/123', timeout=default_timeout 15 | ) 16 | 17 | 18 | def test_that_timed_request_overwrites_default_timeout_if_specified(): 19 | inner_request = MagicMock() 20 | default_timeout = 3.14 21 | specified_timeout = 2.71 22 | 23 | request = TimedRequest(inner_request, default_timeout) 24 | request.get('/foo/bar/123', timeout=specified_timeout) 25 | 26 | inner_request.perform.assert_called_once_with( 27 | 'GET', '/foo/bar/123', timeout=specified_timeout 28 | ) 29 | -------------------------------------------------------------------------------- /tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mock import patch, MagicMock 3 | from requests import HTTPError 4 | 5 | from threatresponse.api import ( 6 | InspectAPI, 7 | EnrichAPI, 8 | ResponseAPI, 9 | IntelAPI, 10 | ) 11 | from threatresponse.client import ThreatResponse 12 | from threatresponse.exceptions import RegionError 13 | 14 | 15 | @patch('requests.Session.request') 16 | def test_types_of_inner_apis(_): 17 | client = ThreatResponse('CLIENT_ID', 'CLIENT_PASSWORD') 18 | 19 | assert isinstance(client.inspect, InspectAPI) 20 | assert isinstance(client.enrich, EnrichAPI) 21 | assert isinstance(client.response, ResponseAPI) 22 | assert isinstance(client.private_intel, IntelAPI) 23 | assert isinstance(client.global_intel, IntelAPI) 24 | 25 | 26 | @patch('requests.Session.request') 27 | def test_different_regions(_): 28 | def TR(region): 29 | return ThreatResponse('CLIENT_ID', 'CLIENT_PASSWORD', region=region) 30 | 31 | for region in [None, '', 'us', 'eu', 'apjc']: 32 | TR(region) 33 | 34 | for region in ['foo', 'bar']: 35 | with pytest.raises(RegionError): 36 | TR(region) 37 | 38 | 39 | @patch('requests.Session.request') 40 | def test_that_client_with_valid_credentials_succeeds(inner_session_request): 41 | inner_session_request.return_value = auth_response(200) 42 | 43 | logger = MagicMock() 44 | 45 | ThreatResponse( 46 | client_id='CLIENT_ID', 47 | client_password='CLIENT_PASSWORD', 48 | logger=logger, 49 | ) 50 | 51 | # Verify that only a single request has been made to an external API. 52 | # Don't check the actual arguments since we're not interested in any 53 | # auth-specific details here. 54 | inner_session_request.assert_called_once() 55 | 56 | logger.info.assert_called_once_with( 57 | 'POST https://visibility.amp.cisco.com/iroh/oauth2/token 200 OK' 58 | ) 59 | 60 | 61 | @patch('requests.Session.request') 62 | def test_that_client_with_invalid_credentials_fails(inner_session_request): 63 | inner_session_request.return_value = auth_response(401) 64 | 65 | logger = MagicMock() 66 | 67 | with pytest.raises(HTTPError): 68 | ThreatResponse( 69 | client_id='CLIENT_ID', 70 | client_password='CLIENT_PASSWORD', 71 | logger=logger, 72 | ) 73 | 74 | # Verify that only a single request has been made to an external API. 75 | # Don't check the actual arguments since we're not interested in any 76 | # auth-specific details here. 77 | inner_session_request.assert_called_once() 78 | 79 | logger.error.assert_called_once_with( 80 | 'POST https://visibility.amp.cisco.com/iroh/oauth2/token 401 ' 81 | 'Unauthorized' 82 | ) 83 | 84 | 85 | def auth_response(status_code): 86 | mocked = MagicMock() 87 | mocked.status_code = status_code 88 | mocked.ok = 100 <= status_code < 400 89 | 90 | if mocked.ok: 91 | mocked.json.return_value = {'access_token': 'ACCESS_TOKEN'} 92 | 93 | else: 94 | error = HTTPError('Some error message here.', response=mocked) 95 | 96 | mocked.text = '{"error": "ERROR"}' 97 | mocked.raise_for_status.side_effect = error 98 | 99 | return mocked 100 | -------------------------------------------------------------------------------- /threatresponse/__init__.py: -------------------------------------------------------------------------------- 1 | # Make the main class importable from the root package directly. 2 | from .client import ThreatResponse 3 | 4 | # Load the current version meta-attribute into the package. 5 | from .version import __version__ 6 | -------------------------------------------------------------------------------- /threatresponse/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Make the classes below importable from the `.api` subpackage directly. 2 | from .enrich import EnrichAPI 3 | from .inspect import InspectAPI 4 | from .response import ResponseAPI 5 | from .intel import IntelAPI 6 | from .commands import CommandsAPI 7 | -------------------------------------------------------------------------------- /threatresponse/api/asset.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class AssetAPI(IntelEntityAPI): 7 | 8 | __router, route = Router.new() 9 | 10 | @route('expire') 11 | def _perform(self, id_, payload, **kwargs): 12 | return self._post( 13 | urls.join(self._url, id_, 'expire'), 14 | json=payload, 15 | **kwargs 16 | ) 17 | -------------------------------------------------------------------------------- /threatresponse/api/base.py: -------------------------------------------------------------------------------- 1 | from .routing import Resolution, Router 2 | from ..exceptions import ResponseTypeError 3 | 4 | 5 | class API(object): 6 | """ Base `API`. """ 7 | 8 | def __init__(self, request): 9 | self._request = request 10 | self._resolution = None 11 | 12 | def _get(self, *args, **kwargs): 13 | return self.__perform('GET', *args, **kwargs) 14 | 15 | def _post(self, *args, **kwargs): 16 | return self.__perform('POST', *args, **kwargs) 17 | 18 | def _put(self, *args, **kwargs): 19 | return self.__perform('PUT', *args, **kwargs) 20 | 21 | def _patch(self, *args, **kwargs): 22 | return self.__perform('PATCH', *args, **kwargs) 23 | 24 | def _delete(self, *args, **kwargs): 25 | return self.__perform('DELETE', *args, **kwargs) 26 | 27 | def __perform(self, method, *args, **kwargs): 28 | response_types = { 29 | 'raw': lambda response: response, 30 | 'json': lambda response: response.json(), 31 | 'text': lambda response: response.text, 32 | } 33 | response_type = kwargs.pop('response_type', 'json') 34 | 35 | if response_type not in response_types: 36 | raise ResponseTypeError( 37 | 'Unsupported response type {type}, must be one of:' 38 | ' {types}.'.format( 39 | type=repr(response_type), 40 | types=', '.join(map(repr, response_types.keys())), 41 | ) 42 | ) 43 | 44 | response = self._request.perform(method, *args, **kwargs) 45 | response.raise_for_status() 46 | 47 | processed = response_types[response_type] 48 | 49 | return processed(response) 50 | 51 | def __getattr__(self, item): 52 | if self._resolution is None: 53 | self._resolution = self._build_resolution() 54 | 55 | return self._resolution.__getattr__(item) 56 | 57 | def _build_resolution(self): 58 | """ Traverses the MRO and merges values of 59 | `__router` attributes to build a single `Resolution`. """ 60 | 61 | router = None 62 | 63 | for cls in type(self).mro(): 64 | attribute = '_{class_name}__{router}'.format( 65 | class_name=cls.__name__, 66 | router='router' 67 | ) 68 | 69 | if hasattr(cls, attribute): 70 | router = Router.merged(router, getattr(cls, attribute)) 71 | 72 | if router is None: 73 | raise Exception( 74 | 'Could not build a resolution for {type}.'.format( 75 | type=type(self) 76 | ) 77 | ) 78 | 79 | return Resolution(self, router) 80 | -------------------------------------------------------------------------------- /threatresponse/api/bundle.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | 4 | 5 | class BundleAPI(API): 6 | """https://private.intel.amp.cisco.com/index.html#/Bundle""" 7 | 8 | __router, route = Router.new() 9 | 10 | @route('export.get') 11 | def _perform(self, **kwargs): 12 | return self._get( 13 | '/ctia/bundle/export', 14 | **kwargs 15 | ) 16 | 17 | @route('export.post') 18 | def _perform(self, payload, **kwargs): 19 | return self._post( 20 | '/ctia/bundle/export', 21 | json=payload, 22 | **kwargs 23 | ) 24 | 25 | @route('import_.post') 26 | def _perform(self, payload, **kwargs): 27 | return self._post( 28 | '/ctia/bundle/import', 29 | json=payload, 30 | **kwargs 31 | ) 32 | -------------------------------------------------------------------------------- /threatresponse/api/casebook.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class CasebookAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Casebook""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(CasebookAPI, self).__init__(request, '/ctia/casebook') 13 | 14 | @route('observables') 15 | def _perform(self, id_, payload, **kwargs): 16 | return self._post( 17 | urls.join(self._url, id_, 'observables'), 18 | json=payload, 19 | **kwargs 20 | ) 21 | 22 | @route('texts') 23 | def _perform(self, id_, payload, **kwargs): 24 | return self._post( 25 | urls.join(self._url, id_, 'texts'), 26 | json=payload, 27 | **kwargs 28 | ) 29 | 30 | @route('bundle') 31 | def _perform(self, id_, payload, **kwargs): 32 | return self._post( 33 | urls.join(self._url, id_, 'bundle'), 34 | json=payload, 35 | **kwargs 36 | ) 37 | -------------------------------------------------------------------------------- /threatresponse/api/commands.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from .base import API 4 | from .routing import Router 5 | 6 | 7 | class CommandsAPI(API): 8 | __router, route = Router.new() 9 | 10 | @route('verdict') 11 | def _perform(self, payload, **kwargs): 12 | """ 13 | Command allow to simple query CTR 14 | for a verdict for a bunch of observables 15 | """ 16 | 17 | response = self._post( 18 | '/iroh/iroh-inspect/inspect', 19 | json={'content': str(payload)}, 20 | **kwargs 21 | ) 22 | 23 | response = self._post( 24 | '/iroh/iroh-enrich/deliberate/observables', 25 | json=response, 26 | **kwargs 27 | ) 28 | verdicts = build_array_for_verdicts(response) 29 | return {"response": response, "verdicts": verdicts} 30 | 31 | @route('targets') 32 | def _perform(self, payload, **kwargs): 33 | """ 34 | Command allow to simple query CTR for a targets 35 | for a bunch of observables 36 | """ 37 | 38 | response = self._post( 39 | '/iroh/iroh-inspect/inspect', 40 | json={'content': str(payload)}, 41 | **kwargs 42 | ) 43 | 44 | response = self._post( 45 | '/iroh/iroh-enrich/observe/observables', 46 | json=response, 47 | **kwargs 48 | ) 49 | 50 | result = build_array_for_targets(response) 51 | 52 | return {"response": response, "targets": result} 53 | 54 | 55 | def build_array_for_verdicts(verdict_dict): 56 | verdicts = [] 57 | 58 | # According to the official documentation, `disposition_name` is 59 | # optional, so simply infer it from required `disposition`. 60 | disposition_map = { 61 | 1: 'Clean', 62 | 2: 'Malicious', 63 | 3: 'Suspicious', 64 | 4: 'Common', 65 | 5: 'Unknown', 66 | } 67 | 68 | for module in verdict_dict.get('data', []): 69 | module_name = module['module'] 70 | module_type_id = module['module_type_id'] 71 | module_instance_id = module['module_instance_id'] 72 | 73 | for doc in module.get('data', {}) \ 74 | .get('verdicts', {}) \ 75 | .get('docs', []): 76 | verdicts.append({ 77 | 'observable_value': doc['observable']['value'], 78 | 'observable_type': doc['observable']['type'], 79 | 'expiration': doc['valid_time'].get('end_time', ''), 80 | 'module': module_name, 81 | 'module_type_id': module_type_id, 82 | 'module_instance_id': module_instance_id, 83 | 'disposition_name': disposition_map[doc['disposition']], 84 | }) 85 | 86 | return verdicts 87 | 88 | 89 | def build_array_for_targets(targets_dict): 90 | result = [] 91 | 92 | for module in targets_dict.get('data', []): 93 | module_name = module['module'] 94 | module_type_id = module['module_type_id'] 95 | module_instance_id = module['module_instance_id'] 96 | targets = [] 97 | 98 | for doc in module.get('data', {}) \ 99 | .get('sightings', {}) \ 100 | .get('docs', []): 101 | 102 | for target in doc.get('targets', []): 103 | element = deepcopy(target) 104 | element.pop('observed_time', None) 105 | if element not in targets: 106 | targets.append(element) 107 | 108 | result.append({ 109 | 'module': module_name, 110 | 'module_type_id': module_type_id, 111 | 'module_instance_id': module_instance_id, 112 | 'targets': targets 113 | }) 114 | return result 115 | -------------------------------------------------------------------------------- /threatresponse/api/enrich.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class EnrichAPI(API): 7 | __router, route = Router.new() 8 | 9 | @route('health') 10 | def _perform(self, **kwargs): 11 | """ 12 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Health/post_iroh_iroh_enrich_health 13 | """ 14 | 15 | return self._post( 16 | '/iroh/iroh-enrich/health', 17 | **kwargs 18 | ) 19 | 20 | @route('health.module_instance_id') 21 | def _perform(self, module_instance_id, **kwargs): 22 | """ 23 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Health/post_iroh_iroh_enrich_health__module_instance_id_ 24 | """ 25 | 26 | return self._post( 27 | urls.join('/iroh/iroh-enrich/health', module_instance_id), 28 | **kwargs 29 | ) 30 | 31 | @route('deliberate.observables') 32 | def _perform(self, payload, **kwargs): 33 | """ 34 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Deliberate/post_iroh_iroh_enrich_deliberate_observables 35 | """ 36 | 37 | return self._post( 38 | '/iroh/iroh-enrich/deliberate/observables', 39 | json=payload, 40 | **kwargs 41 | ) 42 | 43 | @route('deliberate.sighting') 44 | def _perform(self, payload, **kwargs): 45 | """ 46 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Deliberate/post_iroh_iroh_enrich_deliberate_sighting 47 | """ 48 | 49 | return self._post( 50 | '/iroh/iroh-enrich/deliberate/sighting', 51 | json=payload, 52 | **kwargs 53 | ) 54 | 55 | @route('deliberate.sighting_ref') 56 | def _perform(self, payload, **kwargs): 57 | """ 58 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Deliberate/post_iroh_iroh_enrich_deliberate_sighting_ref 59 | """ 60 | 61 | return self._post( 62 | '/iroh/iroh-enrich/deliberate/sighting_ref', 63 | json=payload, 64 | **kwargs 65 | ) 66 | 67 | @route('observe.observables') 68 | def _perform(self, payload, **kwargs): 69 | """ 70 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Observe/post_iroh_iroh_enrich_observe_observables 71 | """ 72 | 73 | return self._post( 74 | '/iroh/iroh-enrich/observe/observables', 75 | json=payload, 76 | **kwargs 77 | ) 78 | 79 | @route('observe.sighting') 80 | def _perform(self, payload, **kwargs): 81 | """ 82 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Observe/post_iroh_iroh_enrich_observe_sighting 83 | """ 84 | 85 | return self._post( 86 | '/iroh/iroh-enrich/observe/sighting', 87 | json=payload, 88 | **kwargs 89 | ) 90 | 91 | @route('observe.sighting_ref') 92 | def _perform(self, payload, **kwargs): 93 | """ 94 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Observe/post_iroh_iroh_enrich_observe_sighting_ref 95 | """ 96 | 97 | return self._post( 98 | '/iroh/iroh-enrich/observe/sighting_ref', 99 | json=payload, 100 | **kwargs 101 | ) 102 | 103 | @route('refer.observables') 104 | def _perform(self, payload, **kwargs): 105 | """ 106 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Refer/post_iroh_iroh_enrich_refer_observables 107 | """ 108 | 109 | return self._post( 110 | '/iroh/iroh-enrich/refer/observables', 111 | json=payload, 112 | **kwargs 113 | ) 114 | 115 | @route('refer.sighting') 116 | def _perform(self, payload, **kwargs): 117 | """ 118 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Refer/post_iroh_iroh_enrich_refer_sighting 119 | """ 120 | 121 | return self._post( 122 | '/iroh/iroh-enrich/refer/sighting', 123 | json=payload, 124 | **kwargs 125 | ) 126 | 127 | @route('refer.sighting_ref') 128 | def _perform(self, payload, **kwargs): 129 | """ 130 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Refer/post_iroh_iroh_enrich_refer_sighting_ref 131 | """ 132 | 133 | return self._post( 134 | '/iroh/iroh-enrich/refer/sighting_ref', 135 | json=payload, 136 | **kwargs 137 | ) 138 | 139 | @route('settings.get') 140 | def _perform(self, **kwargs): 141 | """ 142 | https://visibility.amp.cisco.com/iroh/iroh-enrich/index.html#/Settings/get_iroh_iroh_enrich_settings 143 | """ 144 | 145 | return self._get( 146 | '/iroh/iroh-enrich/settings', 147 | **kwargs 148 | ) 149 | -------------------------------------------------------------------------------- /threatresponse/api/entity.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .. import urls 3 | from ..exceptions import ResponseTypeError 4 | 5 | 6 | class EntityAPI(API): 7 | 8 | def __init__(self, request, url): 9 | super(EntityAPI, self).__init__(request) 10 | 11 | self._url = url 12 | 13 | def get(self, id_=None, **kwargs): 14 | if id_: 15 | url = urls.join(self._url, id_) 16 | else: 17 | url = self._url 18 | 19 | return self._get(url, **kwargs) 20 | 21 | def post(self, payload, **kwargs): 22 | return self._post(self._url, json=payload, **kwargs) 23 | 24 | def put(self, id_, payload, **kwargs): 25 | return self._put( 26 | urls.join(self._url, id_), 27 | json=payload, 28 | **kwargs 29 | ) 30 | 31 | def patch(self, id_, payload, **kwargs): 32 | return self._patch( 33 | urls.join(self._url, id_), 34 | json=payload, 35 | **kwargs 36 | ) 37 | 38 | def delete(self, id_, **kwargs): 39 | if 'response_type' in kwargs: 40 | raise ResponseTypeError("'response_type' cannot be " 41 | "specified for this method.") 42 | 43 | return self._delete( 44 | urls.join(self._url, id_), 45 | response_type='raw', 46 | **kwargs 47 | ) 48 | 49 | 50 | class Search(EntityAPI): 51 | NAME = 'search' 52 | 53 | def get(self, **kwargs): 54 | return self._get( 55 | urls.join(self._url, self.NAME), 56 | **kwargs 57 | ) 58 | 59 | def delete(self, **kwargs): 60 | return self._delete( 61 | urls.join(self._url, self.NAME), 62 | **kwargs 63 | ) 64 | 65 | def count(self, **kwargs): 66 | return self._get( 67 | urls.join(self._url, self.NAME, 'count'), 68 | **kwargs 69 | ) 70 | 71 | 72 | class Metric(EntityAPI): 73 | NAME = 'metric' 74 | 75 | def histogram(self, **kwargs): 76 | return self._get( 77 | urls.join(self._url, self.NAME, 'histogram'), 78 | **kwargs 79 | ) 80 | 81 | def topn(self, **kwargs): 82 | return self._get( 83 | urls.join(self._url, self.NAME, 'topn'), 84 | **kwargs 85 | ) 86 | 87 | def cardinality(self, **kwargs): 88 | return self._get( 89 | urls.join(self._url, self.NAME, 'cardinality'), 90 | **kwargs 91 | ) 92 | 93 | 94 | class IntelEntityAPI(EntityAPI): 95 | 96 | def __init__(self, request, url): 97 | super(IntelEntityAPI, self).__init__(request, url) 98 | self.search = Search(request, url) 99 | self.metric = Metric(request, url) 100 | 101 | def external_id(self, id_, **kwargs): 102 | return self._get( 103 | urls.join(self._url, 'external_id', id_), 104 | **kwargs 105 | ) 106 | -------------------------------------------------------------------------------- /threatresponse/api/event.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class EventAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Event""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(EventAPI, self).__init__(request, '/ctia/event') 13 | 14 | @route('history') 15 | def _perform(self, id_, **kwargs): 16 | return self._get( 17 | urls.join(self._url, 'history', id_), 18 | **kwargs 19 | ) 20 | -------------------------------------------------------------------------------- /threatresponse/api/feed.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class FeedAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Feed""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(FeedAPI, self).__init__(request, '/ctia/feed') 13 | 14 | @route('view') 15 | def _perform(self, id_, share_token, **kwargs): 16 | return self._get( 17 | urls.join(self._url, id_, 'view'), 18 | params={'s': share_token}, 19 | **kwargs 20 | ) 21 | 22 | @route('view.txt') 23 | def _perform(self, id_, share_token, **kwargs): 24 | return self._get( 25 | urls.join(self._url, id_, 'view.txt'), 26 | params={'s': share_token}, 27 | response_type='text', 28 | **kwargs 29 | ) 30 | -------------------------------------------------------------------------------- /threatresponse/api/incident.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class IncidentAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Incident""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(IncidentAPI, self).__init__(request, '/ctia/incident') 13 | 14 | @route('status') 15 | def _perform(self, id_, payload, **kwargs): 16 | return self._post( 17 | urls.join(self._url, id_, 'status'), 18 | json=payload, 19 | **kwargs 20 | ) 21 | 22 | @route('link') 23 | def _perform(self, id_, payload, **kwargs): 24 | return self._post( 25 | urls.join(self._url, id_, 'link'), 26 | json=payload, 27 | **kwargs 28 | ) 29 | 30 | @route('sightings.incidents') 31 | def _perform(self, observable_type, observable_value, **kwargs): 32 | return self._get( 33 | urls.join( 34 | '/ctia', 35 | observable_type, 36 | observable_value, 37 | 'sightings', 38 | 'incidents' 39 | ), 40 | **kwargs 41 | ) 42 | -------------------------------------------------------------------------------- /threatresponse/api/indicator.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class IndicatorAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Indicator""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(IndicatorAPI, self).__init__(request, '/ctia/indicator') 13 | 14 | @route('judgements.indicators') 15 | def _perform(self, observable_type, observable_value, **kwargs): 16 | return self._get( 17 | urls.join( 18 | '/ctia', 19 | observable_value, 20 | observable_type, 21 | 'judgements', 22 | 'indicators' 23 | ), 24 | **kwargs 25 | ) 26 | 27 | @route('sightings.indicators') 28 | def _perform(self, observable_type, observable_value, **kwargs): 29 | return self._get( 30 | urls.join( 31 | '/ctia', 32 | observable_type, 33 | observable_value, 34 | 'sightings', 35 | 'indicators' 36 | ), 37 | **kwargs 38 | ) 39 | -------------------------------------------------------------------------------- /threatresponse/api/inspect.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | 4 | 5 | class InspectAPI(API): 6 | __router, route = Router.new() 7 | 8 | @route('inspect') 9 | def _perform(self, payload, **kwargs): 10 | """ 11 | https://visibility.amp.cisco.com/iroh/iroh-inspect/index.html#/INSPECT/post_iroh_iroh_inspect_inspect 12 | """ 13 | 14 | return self._post( 15 | '/iroh/iroh-inspect/inspect', 16 | json=payload, 17 | **kwargs 18 | ) 19 | -------------------------------------------------------------------------------- /threatresponse/api/int.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | from .entity import EntityAPI 4 | from .module_type_patch import ModuleTypePatchAPI 5 | 6 | 7 | class IntAPI(API): 8 | __router, route = Router.new() 9 | 10 | def __init__(self, request): 11 | super(IntAPI, self).__init__(request) 12 | 13 | self._integration = EntityAPI( 14 | request, '/iroh/iroh-int/integration') 15 | self._integration.__doc__ = "https://visibility.amp.cisco.com/iroh/" \ 16 | "iroh-int/index.html#/Integration" 17 | 18 | self._module_instance = EntityAPI( 19 | request, '/iroh/iroh-int/module-instance') 20 | self._integration.__doc__ = "https://visibility.amp.cisco.com/iroh/" \ 21 | "iroh-int/index.html#/ModuleInstance" 22 | 23 | self._module_type = EntityAPI( 24 | request, '/iroh/iroh-int/module-type') 25 | self._integration.__doc__ = "https://visibility.amp.cisco.com/iroh/" \ 26 | "iroh-int/index.html#/ModuleType" 27 | 28 | self._module_type_patch = ModuleTypePatchAPI(request) 29 | 30 | @property 31 | def integration(self): 32 | return self._integration 33 | 34 | @property 35 | def module_instance(self): 36 | return self._module_instance 37 | 38 | @property 39 | def module_type(self): 40 | return self._module_type 41 | 42 | @property 43 | def module_type_patch(self): 44 | return self._module_type_patch 45 | -------------------------------------------------------------------------------- /threatresponse/api/intel.py: -------------------------------------------------------------------------------- 1 | from .asset import AssetAPI 2 | from .base import API 3 | from .bundle import BundleAPI 4 | from .casebook import CasebookAPI 5 | from .entity import IntelEntityAPI 6 | from .event import EventAPI 7 | from .incident import IncidentAPI 8 | from .indicator import IndicatorAPI 9 | from .judgement import JudgementAPI 10 | from .routing import Router 11 | from .sighting import SightingAPI 12 | from .feed import FeedAPI 13 | from .vulnerability import VulnerabilityAPI 14 | from .. import urls 15 | 16 | 17 | class IntelAPI(API): 18 | 19 | __router, route = Router.new() 20 | 21 | def __init__(self, request): 22 | super(IntelAPI, self).__init__(request) 23 | 24 | self._actor = IntelEntityAPI(request, '/ctia/actor') 25 | self._actor.__doc__ = \ 26 | ("https://private.intel.amp.cisco.com/index.html#/Actor" 27 | "https://intel.amp.cisco.com/index.html#/Actor") 28 | 29 | self._campaign = IntelEntityAPI(request, '/ctia/campaign') 30 | self._campaign.__doc__ = \ 31 | ("https://private.intel.amp.cisco.com/index.html#/Campaign" 32 | "https://intel.amp.cisco.com/index.html#/Campaign") 33 | 34 | self._coa = IntelEntityAPI(request, '/ctia/coa') 35 | self._coa.__doc__ = \ 36 | ("https://private.intel.amp.cisco.com/index.html#/COA" 37 | "https://intel.amp.cisco.com/index.html#/COA") 38 | 39 | self._data_table = IntelEntityAPI(request, '/ctia/data-table') 40 | self._data_table.__doc__ = \ 41 | ("https://private.intel.amp.cisco.com/index.html#/DataTable" 42 | "https://intel.amp.cisco.com/index.html#/DataTable") 43 | 44 | self._attack_pattern = IntelEntityAPI(request, '/ctia/attack-pattern') 45 | self._attack_pattern.__doc__ = \ 46 | ("https://private.intel.amp.cisco.com/index.html#/Attack_Pattern" 47 | "https://intel.amp.cisco.com/index.html#/Attack_Pattern") 48 | 49 | self._feedback = IntelEntityAPI(request, '/ctia/feedback') 50 | self._feedback.__doc__ = \ 51 | ("https://private.intel.amp.cisco.com/index.html#/Feedback" 52 | "https://intel.amp.cisco.com/index.html#/Feedback") 53 | 54 | self._graphql = IntelEntityAPI(request, '/ctia/graphql') 55 | self._graphql.__doc__ = \ 56 | ("https://private.intel.amp.cisco.com/index.html#/GraphQL" 57 | "https://intel.amp.cisco.com/index.html#/GraphQL") 58 | 59 | self._bulk = IntelEntityAPI(request, '/ctia/bulk') 60 | self._bulk.__doc__ = \ 61 | ("https://private.intel.amp.cisco.com/index.html#/Bulk" 62 | "https://intel.amp.cisco.com/index.html#/Bulk") 63 | 64 | self._malware = IntelEntityAPI(request, '/ctia/malware') 65 | self._malware.__doc__ = \ 66 | ("https://private.intel.amp.cisco.com/index.html#/Malware" 67 | "https://intel.amp.cisco.com/index.html#/Malware") 68 | 69 | self._relationship = IntelEntityAPI(request, '/ctia/relationship') 70 | self._relationship.__doc__ = \ 71 | ("https://private.intel.amp.cisco.com/index.html#/Relationship" 72 | "https://intel.amp.cisco.com/index.html#/Relationship") 73 | 74 | self._tool = IntelEntityAPI(request, '/ctia/tool') 75 | self._tool.__doc__ = \ 76 | ("https://private.intel.amp.cisco.com/index.html#/Tool" 77 | "https://intel.amp.cisco.com/index.html#/Tool") 78 | 79 | self._investigation = IntelEntityAPI(request, '/ctia/investigation') 80 | self._investigation.__doc__ = \ 81 | ("https://private.intel.amp.cisco.com/index.html#/Investigation" 82 | "https://intel.amp.cisco.com/index.html#/Investigation") 83 | 84 | self._weakness = IntelEntityAPI(request, '/ctia/weakness') 85 | self._weakness.__doc__ = \ 86 | ("https://private.intel.amp.cisco.com/index.html#/Weakness" 87 | "https://intel.amp.cisco.com/index.html#/Weakness") 88 | 89 | self._identity_assertion = \ 90 | IntelEntityAPI(request, '/ctia/identity-assertion') 91 | self._identity_assertion.__doc__ = \ 92 | ("https://private.intel.amp.cisco.com/index.html#" 93 | "/IdentityAssertion" 94 | "https://intel.amp.cisco.com/index.html#/IdentityAssertion") 95 | 96 | self._bundle = BundleAPI(request) 97 | self._event = EventAPI(request) 98 | self._incident = IncidentAPI(request) 99 | self._indicator = IndicatorAPI(request) 100 | self._judgement = JudgementAPI(request) 101 | self._casebook = CasebookAPI(request) 102 | self._sighting = SightingAPI(request) 103 | self._feed = FeedAPI(request) 104 | self._vulnerability = VulnerabilityAPI(request) 105 | 106 | @property 107 | def actor(self): 108 | return self._actor 109 | 110 | @property 111 | def bundle(self): 112 | return self._bundle 113 | 114 | @property 115 | def campaign(self): 116 | return self._campaign 117 | 118 | @property 119 | def coa(self): 120 | return self._coa 121 | 122 | @property 123 | def data_table(self): 124 | return self._data_table 125 | 126 | @property 127 | def attack_pattern(self): 128 | return self._attack_pattern 129 | 130 | @property 131 | def feedback(self): 132 | return self._feedback 133 | 134 | @property 135 | def feed(self): 136 | return self._feed 137 | 138 | @property 139 | def graphql(self): 140 | return self._graphql 141 | 142 | @property 143 | def event(self): 144 | return self._event 145 | 146 | @property 147 | def identity_assertion(self): 148 | return self._identity_assertion 149 | 150 | @property 151 | def incident(self): 152 | return self._incident 153 | 154 | @property 155 | def indicator(self): 156 | return self._indicator 157 | 158 | @property 159 | def judgement(self): 160 | return self._judgement 161 | 162 | @property 163 | def casebook(self): 164 | return self._casebook 165 | 166 | @property 167 | def sighting(self): 168 | return self._sighting 169 | 170 | @property 171 | def bulk(self): 172 | return self._bulk 173 | 174 | @property 175 | def malware(self): 176 | return self._malware 177 | 178 | @property 179 | def relationship(self): 180 | return self._relationship 181 | 182 | @property 183 | def tool(self): 184 | return self._tool 185 | 186 | @property 187 | def investigation(self): 188 | return self._investigation 189 | 190 | @property 191 | def vulnerability(self): 192 | return self._vulnerability 193 | 194 | @property 195 | def weakness(self): 196 | return self._weakness 197 | 198 | @route('properties.get') 199 | def _perform(self, **kwargs): 200 | """ 201 | https://private.intel.amp.cisco.com/index.html#/Properties 202 | https://intel.amp.cisco.com/index.html#/Properties 203 | """ 204 | 205 | return self._get('/ctia/properties', **kwargs) 206 | 207 | @route('metrics.get') 208 | def _perform(self, **kwargs): 209 | """ 210 | https://private.intel.amp.cisco.com/index.html#/Metrics 211 | https://intel.amp.cisco.com/index.html#/Metrics 212 | """ 213 | 214 | return self._get('/ctia/metrics', **kwargs) 215 | 216 | @route('verdict.get') 217 | def _perform(self, 218 | observable_type, 219 | observable_value, 220 | **kwargs): 221 | """ 222 | https://private.intel.amp.cisco.com/index.html#/Verdict 223 | https://intel.amp.cisco.com/index.html#/Verdict 224 | """ 225 | 226 | return self._get( 227 | urls.join('/ctia', observable_type, observable_value, 'verdict'), 228 | **kwargs 229 | ) 230 | 231 | @route('status.get') 232 | def _perform(self, **kwargs): 233 | """ 234 | https://private.intel.amp.cisco.com/index.html#/Status 235 | https://intel.amp.cisco.com/index.html#/Status 236 | """ 237 | 238 | return self._get('/ctia/status', **kwargs) 239 | 240 | @route('version.get') 241 | def _perform(self, **kwargs): 242 | """ 243 | https://private.intel.amp.cisco.com/index.html#/Version 244 | https://intel.amp.cisco.com/index.html#/Version 245 | """ 246 | 247 | return self._get('/ctia/version', **kwargs) 248 | 249 | 250 | class PrivateIntel(IntelAPI): 251 | """https://private.intel.amp.cisco.com/index.html""" 252 | 253 | def __init__(self, request): 254 | super(PrivateIntel, self).__init__(request) 255 | 256 | self._asset = IntelEntityAPI(request, '/ctia/asset') 257 | self._asset.__doc__ = \ 258 | "https://private.intel.amp.cisco.com/index.html#/Asset" 259 | 260 | self._asset_mapping = AssetAPI(request, '/ctia/asset-mapping') 261 | self._asset_mapping.__doc__ = \ 262 | "https://private.intel.amp.cisco.com/index.html#/Asset%20Mapping" 263 | 264 | self._asset_properties = AssetAPI(request, '/ctia/asset-properties') 265 | self._asset_properties.__doc__ = \ 266 | "https://private.intel.amp.cisco.com/index.html#/" \ 267 | "Asset%20Properties" 268 | 269 | self._target_record = IntelEntityAPI(request, '/ctia/target-record') 270 | self._target_record.__doc__ = \ 271 | "https://private.intel.amp.cisco.com/index.html#/Target%20Record" 272 | 273 | @property 274 | def asset(self): 275 | return self._asset 276 | 277 | @property 278 | def asset_mapping(self): 279 | return self._asset_mapping 280 | 281 | @property 282 | def asset_properties(self): 283 | return self._asset_properties 284 | 285 | @property 286 | def target_record(self): 287 | return self._target_record 288 | 289 | 290 | class GlobalIntel(IntelAPI): 291 | """https://intel.amp.cisco.com/index.html""" 292 | 293 | def __init__(self, request): 294 | super(GlobalIntel, self).__init__(request) 295 | -------------------------------------------------------------------------------- /threatresponse/api/judgement.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class JudgementAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Judgement""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(JudgementAPI, self).__init__(request, '/ctia/judgement') 13 | 14 | @route('judgements') 15 | def _perform(self, observable_type, observable_value, **kwargs): 16 | return self._get( 17 | urls.join( 18 | '/ctia', 19 | observable_type, 20 | observable_value, 21 | 'judgements' 22 | ), 23 | **kwargs 24 | ) 25 | 26 | @route('expire') 27 | def _perform(self, id_, payload, **kwargs): 28 | return self._post( 29 | urls.join(self._url, id_, 'expire'), 30 | json=payload, 31 | **kwargs 32 | ) 33 | -------------------------------------------------------------------------------- /threatresponse/api/module_type_patch.py: -------------------------------------------------------------------------------- 1 | from .entity import EntityAPI 2 | from .. import urls 3 | 4 | 5 | class ModuleTypePatchAPI(EntityAPI): 6 | """ 7 | https://visibility.amp.cisco.com/iroh/iroh-int/index.html#/ModuleTypePatch 8 | """ 9 | URL = '/iroh/iroh-int/module-type-patch' 10 | 11 | def __init__(self, request): 12 | super(ModuleTypePatchAPI, self).__init__(request, self.URL) 13 | 14 | def action_preview(self, id_, **kwargs): 15 | return self._get( 16 | urls.join(self._url, id_, 'action/preview'), 17 | **kwargs 18 | ) 19 | -------------------------------------------------------------------------------- /threatresponse/api/profile.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | 4 | 5 | class ProfileAPI(API): 6 | __router, route = Router.new() 7 | 8 | @route('whoami') 9 | def _perform(self, **kwargs): 10 | """ 11 | https://visibility.amp.cisco.com/iroh/profile/index.html#/Profile/get_iroh_profile_whoami 12 | """ 13 | 14 | return self._get( 15 | '/iroh/profile/whoami', 16 | **kwargs 17 | ) 18 | 19 | @route('org.get') 20 | def _perform(self, **kwargs): 21 | """ 22 | https://visibility.amp.cisco.com/iroh/profile/index.html#/Profile/get_iroh_profile_org 23 | """ 24 | 25 | return self._get( 26 | '/iroh/profile/org', 27 | **kwargs 28 | ) 29 | 30 | @route('org.post') 31 | def _perform(self, payload, **kwargs): 32 | """ 33 | https://visibility.amp.cisco.com/iroh/profile/index.html#/Profile/post_iroh_profile_org 34 | """ 35 | 36 | return self._post( 37 | '/iroh/profile/org', 38 | json=payload, 39 | **kwargs 40 | ) 41 | -------------------------------------------------------------------------------- /threatresponse/api/response.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class ResponseAPI(API): 7 | __router, route = Router.new() 8 | 9 | @route('respond.observables') 10 | def _perform(self, payload, **kwargs): 11 | """ 12 | https://visibility.amp.cisco.com/iroh/iroh-response/index.html#/Response/post_iroh_iroh_response_respond_observables 13 | """ 14 | 15 | return self._post( 16 | '/iroh/iroh-response/respond/observables', 17 | json=payload, 18 | **kwargs 19 | ) 20 | 21 | @route('respond.sighting') 22 | def _perform(self, payload, **kwargs): 23 | """ 24 | https://visibility.amp.cisco.com/iroh/iroh-response/index.html#/Response/post_iroh_iroh_response_respond_sighting 25 | """ 26 | 27 | return self._post( 28 | '/iroh/iroh-response/respond/sighting', 29 | json=payload, 30 | **kwargs 31 | ) 32 | 33 | @route('respond.trigger') 34 | def _perform(self, 35 | module_name, 36 | action_id, 37 | observable_type=None, 38 | observable_value=None, 39 | **kwargs): 40 | """ 41 | https://visibility.amp.cisco.com/iroh/iroh-response/index.html#/Response/post_iroh_iroh_response_respond_trigger__module_name___action_id_ 42 | """ 43 | 44 | url = urls.join( 45 | '/iroh/iroh-response/respond/trigger', 46 | module_name, 47 | action_id 48 | ) 49 | 50 | # Extend optional module-specific query params with the required ones. 51 | query = kwargs.pop('params', {}) 52 | if observable_type and observable_value: 53 | query.update({ 54 | 'observable_type': observable_type, 55 | 'observable_value': observable_value, 56 | }) 57 | 58 | return self._post(url, params=query, **kwargs) 59 | -------------------------------------------------------------------------------- /threatresponse/api/routing/__init__.py: -------------------------------------------------------------------------------- 1 | # Make the classes below importable from the `.routing` subpackage directly. 2 | from .resolution import Resolution 3 | from .router import Router 4 | -------------------------------------------------------------------------------- /threatresponse/api/routing/resolution.py: -------------------------------------------------------------------------------- 1 | class Resolution(object): 2 | """ Represents a resolution of attribute chains. 3 | `resolution.x.y.z` would contain `resolution._route = ['x', 'y', 'z']`. 4 | """ 5 | 6 | def __init__(self, owner, router, route=None): 7 | self._owner = owner 8 | self._router = router 9 | self._route = route or [] 10 | 11 | def __call__(self, *args, **kwargs): 12 | """ Invokes a method by the built route. """ 13 | 14 | route = '.'.join(self._route) 15 | method = self._router.resolve(route) 16 | 17 | return method(self._owner, *args, **kwargs) 18 | 19 | def __getattr__(self, item): 20 | return Resolution(self._owner, self._router, self._route + [item]) 21 | -------------------------------------------------------------------------------- /threatresponse/api/routing/router.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from ...exceptions import RouteError 4 | 5 | 6 | class Router(object): 7 | """ Represents a mapping from a route to a method. """ 8 | 9 | def __init__(self, routes=None): 10 | self._routes = routes or {} 11 | 12 | def register(self, route): 13 | """ Returns a function to register a method by a specified `route`. """ 14 | 15 | def register(method): 16 | if route in self._routes: 17 | raise RouteError( 18 | 'Route {route} has already been registered.'.format( 19 | route=repr(route) 20 | ) 21 | ) 22 | 23 | self._routes[route] = method 24 | 25 | # We do not return anything 26 | # to set the decorated method to `None`. 27 | return None 28 | 29 | return register 30 | 31 | def resolve(self, route): 32 | """ Returns a method by the specified route. """ 33 | 34 | if not route: 35 | raise RouteError('Route cannot be empty.') 36 | if route not in self._routes: 37 | raise RouteError('Route {route} is not registered.'.format( 38 | route=repr(route)) 39 | ) 40 | 41 | return self._routes[route] 42 | 43 | @staticmethod 44 | def merged(x, y): 45 | """ Merges two instances of `Router` into a single one. """ 46 | 47 | message = 'Expected {obj} to be of type {router}, but got {type}.' 48 | 49 | if x is not None and not isinstance(x, Router): 50 | raise TypeError(message.format( 51 | obj=repr('x'), 52 | router=Router, 53 | type=type(x)) 54 | ) 55 | if y is not None and not isinstance(y, Router): 56 | raise TypeError(message.format( 57 | obj=repr('y'), 58 | router=Router, 59 | type=type(y)) 60 | ) 61 | 62 | x = x._routes if x is not None else {} 63 | y = y._routes if y is not None else {} 64 | 65 | result = {} 66 | 67 | for key, value in chain(x.items(), y.items()): 68 | if key not in result: 69 | result[key] = value 70 | 71 | return Router(result) 72 | 73 | @staticmethod 74 | def new(): 75 | """ Returns a tuple of `(router, router.register)`. 76 | 77 | Usage example: 78 | class SomeAPI(API): 79 | __router, route = Router.new() 80 | 81 | @route('a.b.c') 82 | def _perform(self, ...): 83 | ... 84 | """ 85 | 86 | router = Router() 87 | 88 | return router, router.register 89 | -------------------------------------------------------------------------------- /threatresponse/api/sighting.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class SightingAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Sighting""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(SightingAPI, self).__init__(request, '/ctia/sighting') 13 | 14 | @route('sightings') 15 | def _perform(self, observable_type, observable_value, **kwargs): 16 | return self._get( 17 | urls.join('/ctia', observable_type, observable_value, 'sightings'), 18 | **kwargs 19 | ) 20 | -------------------------------------------------------------------------------- /threatresponse/api/sse.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class SSEDeviceAPI(API): 7 | __router, route = Router.new() 8 | 9 | @route('get_all') 10 | def _perform(self, **kwargs): 11 | """ 12 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/get_iroh_iroh_sse_device 13 | """ 14 | 15 | return self._get( 16 | '/iroh/iroh-sse/device', 17 | **kwargs 18 | ) 19 | 20 | @route('get_by_id') 21 | def _perform(self, device_id, **kwargs): 22 | """ 23 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/get_iroh_iroh_sse_device__device_id_ 24 | """ 25 | 26 | return self._get( 27 | urls.join('/iroh/iroh-sse/device', device_id), 28 | **kwargs 29 | ) 30 | 31 | @route('post') 32 | def _perform(self, 33 | payload, 34 | **kwargs): 35 | """ 36 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/post_iroh_iroh_sse_device 37 | """ 38 | 39 | return self._post( 40 | '/iroh/iroh-sse/device', 41 | json=payload, 42 | **kwargs 43 | ) 44 | 45 | @route('patch') 46 | def _perform(self, 47 | device_id, 48 | payload, 49 | **kwargs): 50 | """ 51 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/patch_iroh_iroh_sse_device__device_id_ 52 | """ 53 | 54 | return self._patch( 55 | urls.join('/iroh/iroh-sse/device', device_id), 56 | json=payload, 57 | **kwargs 58 | ) 59 | 60 | @route('token') 61 | def _perform(self, 62 | device_id, 63 | payload, 64 | **kwargs): 65 | """ 66 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/post_iroh_iroh_sse_device__device_id__token 67 | """ 68 | 69 | return self._post( 70 | urls.join('/iroh/iroh-sse/device', device_id, '/token'), 71 | json=payload, 72 | **kwargs 73 | ) 74 | 75 | @route('re_token') 76 | def _perform(self, 77 | device_id, 78 | payload, 79 | **kwargs): 80 | """ 81 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/post_iroh_iroh_sse_device__device_id__token 82 | """ 83 | 84 | return self._post( 85 | urls.join('/iroh/iroh-sse/device', device_id, '/token'), 86 | json=payload, 87 | **kwargs 88 | ) 89 | 90 | @route('api_proxy') 91 | def _perform(self, 92 | device_id, 93 | payload, 94 | **kwargs): 95 | """ 96 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/post_iroh_iroh_sse_device__device_id__api_proxy 97 | """ 98 | 99 | return self._post( 100 | urls.join('/iroh/iroh-sse/device', device_id, '/api-proxy'), 101 | json=payload, 102 | **kwargs 103 | ) 104 | 105 | @route('delete') 106 | def _perform(self, device_id, **kwargs): 107 | """ 108 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/delete_iroh_iroh_sse_device__device_id_ 109 | """ 110 | 111 | return self._delete( 112 | urls.join('/iroh/iroh-sse/device', device_id), 113 | **kwargs 114 | ) 115 | 116 | 117 | class SSETenantAPI(API): 118 | __router, route = Router.new() 119 | 120 | @route('get_token') 121 | def _perform(self, **kwargs): 122 | """ 123 | https://visibility.amp.cisco.com/iroh/iroh-sse/index.html#/SSE/get_iroh_iroh_sse_tenant_token 124 | """ 125 | 126 | return self._get( 127 | '/iroh/iroh-sse/tenant/token', 128 | **kwargs 129 | ) 130 | -------------------------------------------------------------------------------- /threatresponse/api/user_mgmt.py: -------------------------------------------------------------------------------- 1 | from .base import API 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class UserMgmtAPI(API): 7 | __router, route = Router.new() 8 | 9 | @route('users.get') 10 | def _perform(self, user_id, **kwargs): 11 | """ 12 | https://visibility.amp.cisco.com/iroh/user-mgmt/index.html#/User/get_iroh_user_mgmt_users__user_id_ 13 | """ 14 | 15 | return self._get( 16 | urls.join('/iroh/user-mgmt/users', user_id), 17 | **kwargs 18 | ) 19 | 20 | @route('users.post') 21 | def _perform(self, 22 | user_id, 23 | payload, 24 | **kwargs): 25 | """ 26 | https://visibility.amp.cisco.com/iroh/user-mgmt/index.html#/User/post_iroh_user_mgmt_users__user_id_ 27 | """ 28 | 29 | return self._post( 30 | urls.join('/iroh/user-mgmt/users', user_id), 31 | json=payload, 32 | **kwargs 33 | ) 34 | 35 | @route('batch.users') 36 | def _perform(self, user_ids, **kwargs): 37 | """ 38 | https://visibility.amp.cisco.com/iroh/user-mgmt/index.html#/User/get_iroh_user_mgmt_batch_users 39 | """ 40 | 41 | return self._get( 42 | '/iroh/user-mgmt/batch/users', 43 | params={'id': user_ids}, 44 | **kwargs 45 | ) 46 | 47 | @route('search.users') 48 | def _perform(self, 49 | payload, 50 | sort_by=None, 51 | sort_order=None, 52 | offset=None, 53 | search_after=None, 54 | limit=None, 55 | **kwargs): 56 | """ 57 | https://visibility.amp.cisco.com/iroh/user-mgmt/index.html#/User/post_iroh_user_mgmt_search_users 58 | """ 59 | 60 | query = { 61 | 'sort_by': sort_by, 62 | 'sort_order': sort_order, 63 | 'offset': offset, 64 | 'search_after': search_after, 65 | 'limit': limit 66 | } 67 | 68 | return self._post( 69 | '/iroh/user-mgmt/search/users', 70 | json=payload, 71 | params=query, 72 | **kwargs 73 | ) 74 | -------------------------------------------------------------------------------- /threatresponse/api/vulnerability.py: -------------------------------------------------------------------------------- 1 | from .entity import IntelEntityAPI 2 | from .routing import Router 3 | from .. import urls 4 | 5 | 6 | class VulnerabilityAPI(IntelEntityAPI): 7 | """https://private.intel.amp.cisco.com/index.html#/Vulnerability""" 8 | 9 | __router, route = Router.new() 10 | 11 | def __init__(self, request): 12 | super(VulnerabilityAPI, self).__init__(request, '/ctia/vulnerability') 13 | 14 | @route('cpe_match_strings') 15 | def _perform(self, **kwargs): 16 | return self._get( 17 | urls.join(self._url, 'cpe_match_strings'), 18 | **kwargs 19 | ) 20 | -------------------------------------------------------------------------------- /threatresponse/client.py: -------------------------------------------------------------------------------- 1 | from .api.enrich import EnrichAPI 2 | from .api.int import IntAPI 3 | from .api.inspect import InspectAPI 4 | from .api.intel import PrivateIntel, GlobalIntel 5 | from .api.profile import ProfileAPI 6 | from .api.response import ResponseAPI 7 | from .api.commands import CommandsAPI 8 | from .api.sse import SSEDeviceAPI, SSETenantAPI 9 | from .api.user_mgmt import UserMgmtAPI 10 | from .exceptions import CredentialsError 11 | from .request.authorized import ClientAuthorizedRequest, TokenAuthorizedRequest 12 | from .request.logged import LoggedRequest 13 | from .request.proxied import ProxiedRequest 14 | from .request.relative import RelativeRequest 15 | from .request.standard import StandardRequest 16 | from .request.timed import TimedRequest 17 | from .urls import url_for 18 | 19 | 20 | class ThreatResponse(object): 21 | 22 | def __init__(self, client_id=None, client_password=None, 23 | token=None, **options): 24 | 25 | proxy = options.get('proxy') 26 | timeout = options.get('timeout') 27 | logger = options.get('logger') 28 | region = options.get('region') 29 | environment = options.get('environment') 30 | 31 | request = ProxiedRequest(proxy) if proxy else StandardRequest() 32 | request = TimedRequest(request, timeout) if timeout else request 33 | request = LoggedRequest(request, logger) if logger else request 34 | if token: 35 | request = TokenAuthorizedRequest(request, 36 | token, 37 | region=region, 38 | environment=environment) 39 | elif client_id and client_password: 40 | request = ClientAuthorizedRequest(request, 41 | client_id, 42 | client_password, 43 | region=region, 44 | environment=environment) 45 | else: 46 | raise CredentialsError( 47 | 'Credentials must be supplied either ' 48 | 'as a pair of client_id and client_password or ' 49 | 'as a single token.' 50 | ) 51 | 52 | def request_for(family): 53 | return RelativeRequest( 54 | request, 55 | url_for(region, family, environment) 56 | ) 57 | 58 | self._inspect = InspectAPI(request_for('visibility')) 59 | self._enrich = EnrichAPI(request_for('visibility')) 60 | self._int = IntAPI(request_for('visibility')) 61 | self._response = ResponseAPI(request_for('visibility')) 62 | self._private_intel = PrivateIntel(request_for('private_intel')) 63 | self._profile = ProfileAPI(request_for('visibility')) 64 | self._global_intel = GlobalIntel(request_for('global_intel')) 65 | self._commands = CommandsAPI(request_for('visibility')) 66 | self._user_mgmt = UserMgmtAPI(request_for('visibility')) 67 | self._sse_device = SSEDeviceAPI(request_for('visibility')) 68 | self._sse_tenant = SSETenantAPI(request_for('visibility')) 69 | 70 | @property 71 | def inspect(self): 72 | return self._inspect 73 | 74 | @property 75 | def enrich(self): 76 | return self._enrich 77 | 78 | @property 79 | def int(self): 80 | return self._int 81 | 82 | @property 83 | def response(self): 84 | return self._response 85 | 86 | @property 87 | def private_intel(self): 88 | return self._private_intel 89 | 90 | @property 91 | def profile(self): 92 | return self._profile 93 | 94 | @property 95 | def global_intel(self): 96 | return self._global_intel 97 | 98 | @property 99 | def commands(self): 100 | return self._commands 101 | 102 | @property 103 | def user_mgmt(self): 104 | return self._user_mgmt 105 | 106 | @property 107 | def sse_device(self): 108 | return self._sse_device 109 | 110 | @property 111 | def sse_tenant(self): 112 | return self._sse_tenant 113 | -------------------------------------------------------------------------------- /threatresponse/exceptions.py: -------------------------------------------------------------------------------- 1 | class RegionError(ValueError): 2 | pass 3 | 4 | 5 | class RouteError(ValueError): 6 | pass 7 | 8 | 9 | class ResponseTypeError(ValueError): 10 | pass 11 | 12 | 13 | class CredentialsError(ValueError): 14 | pass 15 | -------------------------------------------------------------------------------- /threatresponse/request/__init__.py: -------------------------------------------------------------------------------- 1 | # Make the classes below importable from the `.request` subpackage directly. 2 | from .authorized import ClientAuthorizedRequest, TokenAuthorizedRequest 3 | from .logged import LoggedRequest 4 | from .proxied import ProxiedRequest 5 | from .relative import RelativeRequest 6 | from .response import Response 7 | from .standard import StandardRequest 8 | from .timed import TimedRequest 9 | -------------------------------------------------------------------------------- /threatresponse/request/authorized.py: -------------------------------------------------------------------------------- 1 | from six.moves.http_client import UNAUTHORIZED 2 | from six.moves.urllib.parse import urljoin 3 | 4 | from .base import Request 5 | from ..urls import url_for 6 | 7 | 8 | class ClientAuthorizedRequest(Request): 9 | """ 10 | Provides authorization header for inner request. 11 | """ 12 | 13 | def __init__(self, request, client_id, 14 | client_password, region=None, environment=None): 15 | self._request = request 16 | self._client_id = client_id 17 | self._client_password = client_password 18 | 19 | self._token_url = urljoin( 20 | url_for(region, 'visibility', environment), 21 | '/iroh/oauth2/token' 22 | ) 23 | 24 | self._token = self._request_token() 25 | 26 | def perform(self, method, url, **kwargs): 27 | headers = kwargs.pop('headers', {}) 28 | 29 | response = self._perform(method, url, headers, **kwargs) 30 | 31 | if response.status_code == UNAUTHORIZED: 32 | # The token has already expired (most probably), 33 | # so regenerate it again and try one more time 34 | self._token = self._request_token() 35 | response = self._perform(method, url, headers, **kwargs) 36 | 37 | return response 38 | 39 | def _request_token(self): 40 | data = {'grant_type': 'client_credentials'} 41 | headers = {'Content-Type': 'application/x-www-form-urlencoded', 42 | 'Accept': 'application/json'} 43 | auth = (self._client_id, self._client_password) # HTTP Basic Auth 44 | 45 | response = self._request.post(self._token_url, 46 | data=data, 47 | headers=headers, 48 | auth=auth) 49 | 50 | response.raise_for_status() 51 | 52 | return response.json()['access_token'] # OK 53 | 54 | @property 55 | def _headers(self): 56 | return {'Authorization': 'Bearer {token}'.format(token=self._token)} 57 | 58 | def _perform(self, method, url, headers, **kwargs): 59 | headers.update(self._headers) 60 | kwargs['headers'] = headers 61 | return self._request.perform(method, url, **kwargs) 62 | 63 | 64 | class TokenAuthorizedRequest(Request): 65 | """ 66 | Provides authorization header for inner request. 67 | """ 68 | 69 | def __init__(self, request, token, region=None, environment=None): 70 | self._request = request 71 | self._token = token 72 | self._check_url = urljoin( 73 | url_for(region, 'visibility', environment), 74 | '/iroh/iroh-enrich/settings', 75 | ) 76 | self._check_token() 77 | 78 | def perform(self, method, url, **kwargs): 79 | headers = kwargs.pop('headers', {}) 80 | response = self._perform(method, url, headers, **kwargs) 81 | return response 82 | 83 | def _check_token(self): 84 | headers = {'Accept': 'application/json'} 85 | headers.update(self._headers) 86 | 87 | response = self._perform('GET', self._check_url, headers) 88 | response.raise_for_status() 89 | 90 | @property 91 | def _headers(self): 92 | return {'Authorization': 'Bearer {token}'.format(token=self._token)} 93 | 94 | def _perform(self, method, url, headers, **kwargs): 95 | headers.update(self._headers) 96 | kwargs['headers'] = headers 97 | return self._request.perform(method, url, **kwargs) 98 | -------------------------------------------------------------------------------- /threatresponse/request/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import six 4 | 5 | 6 | class Request(six.with_metaclass(abc.ABCMeta, object)): 7 | """ 8 | Interface for performing HTTP requests. 9 | """ 10 | 11 | @abc.abstractmethod 12 | def perform(self, method, url, **kwargs): 13 | pass 14 | 15 | def get(self, url, **kwargs): 16 | return self.perform('GET', url, **kwargs) 17 | 18 | def post(self, url, **kwargs): 19 | return self.perform('POST', url, **kwargs) 20 | 21 | def put(self, url, **kwargs): 22 | return self.perform('PUT', url, **kwargs) 23 | 24 | def patch(self, url, **kwargs): 25 | return self.perform('PATCH', url, **kwargs) 26 | 27 | def delete(self, url, **kwargs): 28 | return self.perform('DELETE', url, **kwargs) 29 | -------------------------------------------------------------------------------- /threatresponse/request/logged.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .base import Request 4 | 5 | 6 | class LoggedRequest(Request): 7 | """ 8 | Logs every response of inner request. 9 | """ 10 | 11 | MESSAGE_FORMAT = '{method} {url} {status_code} {reason_phrase}' 12 | 13 | def __init__(self, request, logger): 14 | self._request = request 15 | self._logger = logger 16 | 17 | def perform(self, method, url, **kwargs): 18 | try: 19 | response = self._request.perform(method, url, **kwargs) 20 | except Exception: 21 | self._log_error(method, url) 22 | raise 23 | 24 | if response.ok: # 100 <= code < 400. 25 | self._log_success(method, url, response) 26 | else: # 400 <= code < 600. 27 | self._log_error(method, url, response) 28 | 29 | return response 30 | 31 | @classmethod 32 | def _format(cls, method, url, response=None): 33 | return cls.MESSAGE_FORMAT.format( 34 | method=method.upper(), 35 | url=url, 36 | status_code=( 37 | '' if response is None else 38 | str(response.status_code) 39 | ), 40 | reason_phrase=( 41 | '' if response is None else 42 | six.moves.http_client.responses[response.status_code] 43 | ), 44 | ).rstrip() 45 | 46 | def _log_success(self, method, url, response): 47 | message = self._format(method, url, response) 48 | 49 | self._logger.info(message) 50 | 51 | def _log_error(self, method, url, response=None): 52 | message = self._format(method, url, response) 53 | 54 | if response is None: 55 | # The same as .error(), but also includes the current traceback. 56 | self._logger.exception(message) 57 | else: 58 | self._logger.error(message) 59 | -------------------------------------------------------------------------------- /threatresponse/request/proxied.py: -------------------------------------------------------------------------------- 1 | from .standard import StandardRequest 2 | 3 | 4 | class ProxiedRequest(StandardRequest): 5 | """ 6 | Supports HTTP request proxying via a specified proxy server. 7 | """ 8 | 9 | def __init__(self, proxy): 10 | super(ProxiedRequest, self).__init__() 11 | 12 | self._proxy = proxy 13 | 14 | self._configure_session_proxies() 15 | 16 | def _configure_session_proxies(self): 17 | self._session.proxies = { 18 | 'http': self._proxy, 19 | 'https': self._proxy, 20 | } 21 | -------------------------------------------------------------------------------- /threatresponse/request/relative.py: -------------------------------------------------------------------------------- 1 | from six.moves.urllib.parse import urljoin 2 | 3 | from .base import Request 4 | 5 | 6 | class RelativeRequest(Request): 7 | """ 8 | Performs requests relative to provided `prefix`. 9 | """ 10 | 11 | def __init__(self, request, prefix): 12 | self._request = request 13 | self._prefix = prefix 14 | 15 | def perform(self, method, url, **kwargs): 16 | url = urljoin(self._prefix, url) 17 | 18 | return self._request.perform(method, url, **kwargs) 19 | -------------------------------------------------------------------------------- /threatresponse/request/response.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | 6 | class Response(object): 7 | """ 8 | Wraps an instance of the `requests.Response` class. 9 | Redirects all calls to get/set any attribute to inner response. 10 | May also customize some instance methods. 11 | """ 12 | 13 | def __init__(self, response): 14 | self._response = response 15 | 16 | def __getattr__(self, key): 17 | return getattr(self._response, key) 18 | 19 | def __setattr__(self, key, value): 20 | # This is an antidote against infinite recursion: 21 | # in order to use self._response for redirecting calls, 22 | # make sure to set the '_response' attribute directly first. 23 | if key == '_response': 24 | super(Response, self).__setattr__(key, value) 25 | else: 26 | setattr(self._response, key, value) 27 | 28 | def raise_for_status(self): 29 | extended = self._extended 30 | 31 | try: 32 | self._response.raise_for_status() 33 | except requests.HTTPError as error: 34 | raise extended(error) 35 | 36 | @staticmethod 37 | def _extended(error): 38 | # Try to extend the default error message with the response payload 39 | # in order to give the user more insight about what went wrong. 40 | 41 | try: 42 | payload = json.loads(error.response.text) 43 | except json.JSONDecodeError: 44 | return error 45 | 46 | message = error.args[0] # 1-element tuple. 47 | message += '\n' + json.dumps(payload, indent=4, sort_keys=True) 48 | 49 | error.args = (message,) 50 | 51 | return error 52 | -------------------------------------------------------------------------------- /threatresponse/request/standard.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .base import Request 4 | from .response import Response 5 | 6 | 7 | class StandardRequest(Request): 8 | """ 9 | Performs plain HTTP requests using the `requests` library. 10 | """ 11 | 12 | def __init__(self): 13 | self._session = requests.Session() 14 | 15 | def perform(self, method, url, **kwargs): 16 | return Response(self._session.request(method, url, **kwargs)) 17 | -------------------------------------------------------------------------------- /threatresponse/request/timed.py: -------------------------------------------------------------------------------- 1 | from .base import Request 2 | 3 | 4 | class TimedRequest(Request): 5 | """ 6 | Sets the default request timeout (unless explicitly specified). 7 | """ 8 | 9 | def __init__(self, request, timeout): 10 | self._request = request 11 | self._timeout = timeout 12 | 13 | def perform(self, method, url, **kwargs): 14 | kwargs.setdefault('timeout', self._timeout) 15 | 16 | return self._request.perform(method, url, **kwargs) 17 | -------------------------------------------------------------------------------- /threatresponse/urls.py: -------------------------------------------------------------------------------- 1 | from six.moves.urllib.parse import quote 2 | 3 | from .exceptions import RegionError 4 | 5 | _url_patterns_by_api_family = { 6 | 'visibility': 'https://visibility{region}.amp.cisco.com', 7 | 'private_intel': 'https://private.intel{region}.amp.cisco.com', 8 | 'global_intel': 'https://intel{region}.amp.cisco.com' 9 | } 10 | 11 | 12 | def _url_for_region(url_pattern, region): 13 | # Fall back to the default region. 14 | if region == 'us': 15 | region = '' 16 | 17 | return url_pattern.format(region='.' + region if region != '' else '') 18 | 19 | 20 | def _urls_by_region(urls): 21 | return dict( 22 | ( 23 | region, 24 | dict( 25 | (api_family, _url_for_region(url_pattern, region)) 26 | for api_family, url_pattern in urls.items() 27 | ) 28 | ) 29 | for region in ( 30 | '', 31 | 'us', 32 | 'eu', 33 | 'apjc', 34 | ) 35 | ) 36 | 37 | 38 | def url_for(region, family, environment=None): 39 | # Fall back to the default region. 40 | if region is None: 41 | region = '' 42 | # Fall back to the default environment. 43 | if environment is None: 44 | environment = _url_patterns_by_api_family 45 | if region not in _urls_by_region(environment): 46 | # Use `repr` to make each region enclosed in quotes. 47 | raise RegionError( 48 | 'Invalid region {}, must be one of: {}.'.format( 49 | repr(region), 50 | ', '.join(map(repr, _urls_by_region(environment).keys())), 51 | ) 52 | ) 53 | 54 | return _urls_by_region(environment)[region][family] 55 | 56 | 57 | def join(base, *parts): 58 | return base.rstrip('/') + '/' + '/'.join( 59 | quote(str(part).strip('/'), safe='') 60 | for part in parts 61 | ) 62 | -------------------------------------------------------------------------------- /threatresponse/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.15.0' 2 | -------------------------------------------------------------------------------- /travis_key.enc: -------------------------------------------------------------------------------- 1 | U2FsdGVkX1+OMxg2yqLrd1OxNQsKmDhUViW1sIAlEvVPQUv9IA0401EG+oZsgAR9 2 | lSnAYJjHjKZZ0u+BP5eZ1mqmFxTBo591fM+B6GALlFtlZ1SNjfpiNsUir9OwVzdf 3 | xwLKYhzc0oIf0qEOL/FNPiW4IIZkKakkW8x9F4v/ez6jYzlfwvuAMOzO7/SW7rSu 4 | rEpK5ihoSlzv6A3fR282lgrC2PE+uIvTqSaEZuM/lQviNDU1FPgd3+lcD369RlkE 5 | zjTpcgd182hbp4Sub6kFyDvIi0JBZfibYHLOE9Ht2d9KeC9T5Muzf18YaQ+bMpwQ 6 | ujF2FjwOXN5tIjZpOx9333zp3/qLF9SMq3kfxXt4ON+u/5NjV9dfyX6qMw8elYOq 7 | u+Vng+AodjHCMI8RZUODeOkFrMDTkrm/MXc2DaDq1uGY1kHT+0HWq4+NU3mjixuW 8 | IoXHd8R6JSpRAaXMVuyLtzvf+aSfsk7AtDCtEb04xTpbWawd3z02nnjwjtHtVrLs 9 | V5qoyWSwfm3pJrpQIMi/db6UV5sLEcMIt7F4rZnexqfo0VWLCTVFimpQNQU08932 10 | DWwMFCmQSqpGSegnPTRYQnFRgPbGidz0rKmA0rR2OlP3tks23Luv0zm4O6ikqTD7 11 | KYDhxZhnYu3uuaqxmm1A3npl8iiP1fs4lpt7gPygJir8sbOLGw3JsyeLvnDYwmLS 12 | tz1BX6OdssTWQnHd6pNrW92bLaSd3E7MiQBfzZz3NVvxI//bBj+LBRel27M2tiSR 13 | Cka1+00wKuZTktrs/HbFKyTrUFU9IBwaHvsJVrnmAHPkWjwQSDSlA9phHmx/p6j/ 14 | zF8YRwTUhqsZUt4nCfLz4sYK10a4tmC1zQuLCZzx5Qq14agdfyRYsdOdf2xxsFE5 15 | /+qcpOCZdkuqE2ADUVUW8Fy+AfvcnmLuiNGFPQ7nqzaaxoSmuWXgu0b2ofilUW0f 16 | 582PV0t7Q7HH/wV0yI4jFUWik8wH5W8Ds4CUD+eJQYoiXiTxWgtIg9/miiHTIYoo 17 | yEVxHHIsHSiYPAhFf0QiacspVlInE6Q1kiqtkyjQSOrN4Cc+bIP4wFvOTJjeWDEW 18 | FDBypnIRWQetfeu/HdpilycxHEYSkznR+chb53gqWbtUtawqukzOd7f0d6vgv7ZI 19 | N7/GqHfr+sdd8pt1V7YWS63QZlH7BbXgl/yb72hjUPc+KLdrRlBfxJn3oyUjYJTa 20 | /MiD1j5EiOhkX/KlrfUTkxkwCRlrcw+5oGpxqBRVtB5SWQfesWu4nzpdvat1LKlM 21 | 7/Cm00Eo9jn5JCqNmSVaNq8lAdMQlTaWt2KFxHTo1W9geuzpJhzZQqyLC5PKRWEf 22 | xjq8K/PaEoTkOg4ymJR6kAoouP04YfriwoyAYRpfFCXjx0d1CI3JkbgWXWCeo7W1 23 | MAWlIssAtT3HVhvmVE6xp048jTITctsFWcMFhPPwMpvqmij4WsEPKpXEPrm9Ffyl 24 | STaLMAXOcYBikKDJ0x1xgm3Sg9LUNKRTmVeTMv3BKjO+lb2+tldOETB8IIjo48JG 25 | EjC1UCJz6rZ2XN342TaTQtz8sCf/ISXVkLyc0yIsChNA9aPK8xCDn/VN3ZoD5kIv 26 | OFfBs35fuaFxw4kg485bOXU73oWFals0wDrYO/4OHgZi8GwLarhtO9q7bsTNd8U1 27 | Da5u3leZSD/pNodr8rmJ0UL0uQ3ZE0O1tTraaqsXTsqkCcqlg6ttqAdRZUxehSP3 28 | uQE7jceWrwA1QuKAMXwIlcfXu8dwikYxhdydJB3PpQ++U3bT1adXjpZIgHGOIO7K 29 | XxI80QYxcrFyKxegjVj4lyoD6ngCpn/3swmnJGLM7y1Oyd5baRIDDnKM5axopRzv 30 | AUAFCOipHieeZ3g61BzpmV9eYi9txeG20hfHXc7KRW2m5DqBmDltKE/8njfTuxIF 31 | eSd/QxA8tNlFC410DVzl97k6fNy00i9MgH6lTeXzVD0CRIru1gvIjbUznR81btU8 32 | 8huSgFC4uKnjPToLjKlnFJR5iNc+t2S58FBbzUTUdc81sUoxQ+7Kz+LNcOPufUI9 33 | nGrRHYs8yipQHVYrbFwuhIUESImPJ3LxX07eMP9EgKoAAr0QHxdA1EL3f6EFJQNW 34 | mGJcdG+xaAFkGxAcrrvaP8xkvaEmh66vrX7SNxnX7RVfcVP41gnEZOq/nFfHj0BH 35 | QVtIQ2Ex16ZRmJ1bBZQR2qWN44AF4KgYGiy6bR1TN7VRxdO8Y/12bMWbNw48m40y 36 | 4uFbqLLCZY4+xqri6qFaQTW0srYVo5+OIDKItjUFZqrflmDidFLTARAc1XTRHnbK 37 | K2jrYkBmZce27NKmJh8Whe8Lsanjr1V5Xaa1jsqrLXC6vaqJPk5Fzn8W97YFE5Cd 38 | QTY+8pOj8s/9doQckC9x95BW+aqCne1O0TBH+vJ1KR5gpsJX79Nba/B2sVNbKdeu 39 | PSEhBA5IwBkqJnjbZeMAOiv7iHXxIvT+ya1tEZn6bXo9gKM8r4D1TrTRWSxIkj2n 40 | Kbb/7QzbrbWBy6dtVkQHwcmHCU2M9I+7LFmGdJsfLbC4tZnOIGmbde+WhvDsFSz0 41 | BW4NWvBGjWPSFtNDjQyJMY8d3e3qHF5XWHRcrw+5WDx7GAcO4IJcHzE9QQyiyBrI 42 | DPPyou3kZbv0GxvYKmIfJA1P+f8sPi20UJBx6ZWY4H58OG6nRz7SmDDx6yfZhfI3 43 | +YgEiRXqQStwJJPg+zL3EBz/OhPdPT0AIKEQQ0WU05JegVjT6WeoNZyU4BdnFCaT 44 | 1yzysDKZ1l/hed1ssnWa56Pw7uFINNDsaIK7ne26Z+ogJra3XF4hnX3EY7wXZ2Zz 45 | gZsMP9A6TcDJDvs/oXqAbdtyG2mi7LGu75ScssTJNNnDy/xZymBm8mrXCZ5XAskv 46 | +ki89t5jAVOagBlArg5xg+ZjAQrlDcvpuaXV2rHWwsxyUwnNtH7n1r3R+3qGzg6Z 47 | z5+2IeCztMOY88wnWBSwA0SSTW7qViKoMIDTw5sX4rcJkQSIKdeiRlvwa/pXEGJE 48 | oB+yWh1NNVlnc+Y9vgp31ok8fdu/2k/faSyxvXNpu775JMDdnoMosUgWtM9PesSQ 49 | bmtf1Ig0ny3ivTdQE1F8VdRusad7BbVAMkFgsu26z/TrUuYY3DibMgvTfjpmXbER 50 | CLBHZ4f+iVr9XSeHw5jBC+CB6uE/cujOE6Fc5vzGU6IRtfbLqjKBr1Moox59ulPC 51 | uJgDc9wTqJTlYailk42ErIZzacDTpXFOu3q/K2BbzsJFK/yZt9Fvn8p6A4G3tLaA 52 | E80RxIPP30tP2eR3dKQVDwEiqp3DJzhe35XwWnJgYCgFfWwf44VJbuWTsv74sMSj 53 | l5Sa5SUMf1udfya0F7JBigmgigmd5abfXFld04GdtW4TtG5LS978QRK7s/6m714Q 54 | aAMblHczpNnHXNbMp1HqqSRQE/mbTfcNWDJ/Eb93yPr56gBZoWpcG4K/de1LcW6V 55 | O6IdTIim6mwYwhygYScdyx7AbdKnrE+Ht/AEGdrKA7D3rvj8Z8T20+/iLEso2Rw6 56 | RdrwsOyw0z7ejMdT4yyLW5bfSKLs7evJqATodcwwEjhQWmvNhDWwSHbDusHleTl0 57 | bnG2vYciZKwl0qwbANFN9JqWiiHuLkEE14cNzRdZeGbsG5BrCFs70h836wbxb7Pq 58 | 6j+TRAYonUiU7i56M3ZvY0p7/b50HnyiCEMkdr1RDTLdRvt0ZcAGNQ7cCCFC7KOp 59 | yjK9OLyErJicbHqUbZ4dcs0YuJ9NJ27lYmmhIc8jZU4okFGTpjZB+d/6CcSqoAsZ 60 | LDTuPcPpuWVTkxEegZ1yzput+B27nBCfcJgWbPn1RnKwPha/95JdNe+ypD6y5Gd/ 61 | DrKYYkBcgNAL2se8YAQZFqMh5OAgvEIjTEYEDmN7whTh07rdyxDPqBC4ydYDDJe/ 62 | dhkHiLMxltdVAP9OE8SsCb9n0KIpZMLHHOny4qxfH2s606aMBxFEpDBy8z5wBMJi 63 | Qo3vz25zwQSaVYbGjehQlYkUBif931p88QZbtRb2GtIvfpA2WIuSSCgo6Q7Z4Cvb 64 | 4+Yhv8X6GR+B/H4GbothF38iGpA/M00MjZI5R+aKC9DKrjlzyqKwzuQCKayZ/v7r 65 | aBT0XiiungjHhwB+2dM6Me1WRHyA8a10x4mLGz1+UjYqw4OKxjzuGidQcUfm8jkq 66 | 211Gb9s850HQLVI72eH0WR0AgTZCzo1WEh5fVZnbLe4CZdV5rvtCzIBwwiDYD5my 67 | AQ7X3HIiqhgXToZg5a2S9evMRFhkdOot+0ddelnREzHHlc30oRmtXbf3laNjgSVd 68 | AuZXBHzzk4jGXoKwycYnQgXGOP+uRR1CeLKV4F4bQLuaUrffNKuVsDl2/nfkhml9 --------------------------------------------------------------------------------