diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..6f15b22 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,11 @@ +name: Linting +on: [push, pull_request] +jobs: + lint: + # Run per push for internal contributers. This isn't possible for forked pull requests, + # so we'll need to run on PR events for external contributers. + # String comparison below is case insensitive. + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + runs-on: ubuntu-latest + steps: + - uses: 'phantomcyber/dev-cicd-tools/github-actions/lint@main' diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..23d31c5 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,28 @@ +name: Semgrep +on: + pull_request_target: + branches: + - next + - main + push: + branches: + - next + - main +jobs: + semgrep: + runs-on: ubuntu-latest + steps: + - if: github.event_name == 'push' + run: | + echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + echo "REF=${{ github.REF }}" >> $GITHUB_ENV + - if: github.event_name == 'pull_request_target' + run: | + echo "REPOSITORY=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV + echo "REF=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + - uses: 'phantomcyber/dev-cicd-tools/github-actions/semgrep@main' + with: + SEMGREP_DEPLOYMENT_ID: ${{ secrets.SEMGREP_DEPLOYMENT_ID }} + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + REPOSITORY: ${{ github.repository }} + REF: ${{ github.ref }} diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml new file mode 100644 index 0000000..d5fb354 --- /dev/null +++ b/.github/workflows/start-release.yml @@ -0,0 +1,9 @@ +name: Start Release +on: workflow_dispatch +jobs: + start-release: + runs-on: ubuntu-latest + steps: + - uses: 'phantomcyber/dev-cicd-tools/github-actions/start-release@main' + with: + GITHUB_TOKEN: ${{ secrets.SOAR_APPS_TOKEN }} \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c350da0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/phantomcyber/dev-cicd-tools + rev: v1.6 + hooks: + - id: org-hook + - id: package-app-dependencies +- repo: https://github.com/Yelp/detect-secrets + rev: v1.1.0 + hooks: + - id: detect-secrets + args: ['--no-verify', '--exclude-files', '^taniumrest.json$'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b3cc507 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,2 @@ +# Contributing +For more information about contributing to Splunk SOAR Apps please take a look at our app [Contribution Guide](https://github.com/splunk-soar-connectors/.github/blob/main/.github/CONTRIBUTING.md)! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f003b93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Splunk Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..2352ba2 --- /dev/null +++ b/NOTICE @@ -0,0 +1,16 @@ +Splunk SOAR Tanium REST +Copyright (c) 2019-2021 Splunk Inc. + +Third-party Software Attributions: + +Library: beautifulsoup4 +Version: 4.9.1 +License: MIT +Copyright 2004-2017 Leonard Richardson +Copyright 2004-2019 Leonard Richardson +Copyright 2018 Isaac Muse + +Library: requests +Version: 2.25.0 +License: Apache 2.0 +Kenneth Reitz diff --git a/Tanium Rest.postman_collection.json b/Tanium Rest.postman_collection.json new file mode 100644 index 0000000..ac38f4c --- /dev/null +++ b/Tanium Rest.postman_collection.json @@ -0,0 +1,769 @@ +{ + "info": { + "_postman_id": "d836a525-245e-4a4a-9205-59d0e68799aa", + "name": "Tanium Rest", + "description": "# Postman Collection of Endpoints for Tanium Rest App on Splunk SOAR.\n\n- ### Prerequisite\n\nThe below mentioned are the required fields to use this collection. So, Set this all fields before run the request.\n\n| Variable Name | Description |\n| ------ | ------ |\n| username | Tanium Rest instance username (in environment variables ) |\n| password | Tanium Rest instance password (in environment variables ) |\n| base_url | Tanium Rest instance URL (in environment variables ) |\n\n\n\n\n\n\n> **_NOTE:_** The body parameters for the requests have been mentioned in the above table. Please set them as per your requirements. Refer the documentation of individual requests.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "List saved Questions", + "request": { + "method": "GET", + "header": [ + { + "key": "session", + "value": "{{session}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v2/saved_questions", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "saved_questions" + ] + } + }, + "response": [] + }, + { + "name": "List Questions", + "request": { + "method": "GET", + "header": [ + { + "key": "session", + "value": "{{session}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v2/questions", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "questions" + ] + } + }, + "response": [] + }, + { + "name": "List Process", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "var group_name = pm.variables.get(\"group_name\")", + "var sensor_name = pm.variables.get(\"sensor_name\")", + "var base_url = pm.environment.get(\"base_url\")", + "var session = pm.variables.get(\"session\")", + "var expire_seconds = pm.variables.get(\"expire_seconds\")", + "var id_url = \"/api/v2/questions\"", + "if(group_name){", + " ", + " var groupurl = \"/api/v2/groups/by-name/\"", + " const req = {", + " url: base_url + groupurl + group_name,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"group_id\", response.data.id)", + " });", + " ", + " setTimeout(function () { ", + " const req2 = {", + " url: base_url + id_url,", + " method: \"post\",", + " body: JSON.stringify(", + " {", + " \"expire_seconds\": expire_seconds,", + " \"context_group\": {\"id\": pm.variables.get(\"group_id\")},", + " \"selects\": [", + " {", + " \"sensor\": {", + " \"name\": sensor_name", + " }", + " }", + " ]", + " }", + " ),", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req2, function (err, response) {", + " var response2 = response.json()", + " pm.collectionVariables.set(\"id\", response2.data.id)", + " });", + " }, 2000);", + "}", + "else", + "{", + " const req3 = {", + " url: base_url + id_url,", + " method: \"post\",", + " body: JSON.stringify(", + " {", + " \"expire_seconds\": expire_seconds,", + " \"selects\": [", + " {", + " \"sensor\": {", + " \"name\": sensor_name", + " }", + " }", + " ]", + " }", + " ),", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req3, function (err, response) {", + " var response3 = response.json()", + " pm.collectionVariables.set(\"id\", response3.data.id)", + " });", + "}", + "setTimeout(function () { ", + "}, 5000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v2/result_data/question/{{id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "result_data", + "question", + "{{id}}" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| group_name(optional) | centos-computers |\n| sensor_name | CPU Details |\n| expire_seconds | 600 |" + }, + "response": [] + }, + { + "name": "parse quesion", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"text\": \"{{parse_query_text}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/v2/parse_question", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "parse_question" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| parse_query_text | Computer |" + }, + "response": [] + }, + { + "name": "get question results", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v2/result_data/question/{{qustion_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "result_data", + "question", + "{{qustion_id}}" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| qustion_id | 272799 |" + }, + "response": [] + }, + { + "name": "execute action/terminate process", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "var group_name = pm.variables.get(\"group_name\")", + "var base_url = pm.environment.get(\"base_url\")", + "var session = pm.variables.get(\"session\")", + "var expire_seconds = pm.variables.get(\"expire_seconds\")", + "var id_url = \"/api/v2/questions\"", + "var package_url = \"/api/v2/packages/by-name/\"", + "var action_group_url = \"/api/v2/action_groups/by-name/\"", + "var package_name = pm.collectionVariables.get(\"package_name\")", + "var action_group = pm.collectionVariables.get(\"action_group\")", + "var package_param_valid = pm.collectionVariables.get(\"package_param\")", + "var package_param_list = []", + "", + "if(package_param_valid)", + "{", + " var package_param = JSON.parse(pm.collectionVariables.get(\"package_param\"))", + " Object.entries(package_param).forEach(([key, value]) => {", + " var temp_obj = {}", + " temp_obj[\"key\"] = key", + " temp_obj[\"value\"] = value", + " package_param_list.push(temp_obj)", + " });", + "}", + "", + "const req2 = {", + " url: base_url + package_url + package_name,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req2, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"package_id\", response.data.id)", + " });", + " ", + "const req3 = {", + " url: base_url + action_group_url + action_group,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req3, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"action_group_id\", response.data.id)", + " });", + "", + "if(group_name){", + " ", + " var groupurl = \"/api/v2/groups/by-name/\"", + " const req = {", + " url: base_url + groupurl + group_name,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"group_id\", response.data.id)", + " });", + " ", + " setTimeout(function () { ", + " ", + " var body = {", + " 'target_group': {", + " 'source_id': pm.collectionVariables.get(\"group_id\"),", + " 'name': group_name", + " },", + " 'action_group': {'id': pm.collectionVariables.get(\"action_group_id\")},", + " 'package_spec': {", + " 'source_id': pm.collectionVariables.get(\"package_id\")", + " },", + " 'name': pm.collectionVariables.get(\"action_name\"),", + " \"expire_seconds\": expire_seconds,", + " }", + " if(package_param_valid)", + " {", + " body.package_spec.parameters = package_param_list", + " }", + " var body_str = JSON.stringify(body);", + " pm.collectionVariables.set('execute_action_body', body_str); ", + " ", + " }, 3000);", + "", + " ", + "", + "}", + "else{", + " setTimeout(function () { ", + " var body = {", + " 'action_group': {'id': pm.collectionVariables.get(\"action_group_id\")},", + " 'package_spec': {", + " 'source_id': pm.collectionVariables.get(\"package_id\")", + " },", + " 'name': pm.collectionVariables.get(\"action_name\"),", + " \"expire_seconds\": expire_seconds,", + " }", + " if(package_param_valid)", + " {", + " body.package_spec.parameters = package_param_list", + " }", + " var body_str = JSON.stringify(body);", + " pm.collectionVariables.set('execute_action_body', body_str); ", + " }, 3000);", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{{execute_action_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/v2/saved_actions", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "saved_actions" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| group_name(optional) | centos-computers |\n| package_name | Live Response - Linux |\n| action_group | Default |\n| action_name | Splunk Live Response Test |\n| package_param(optional) | {\"$1\":\"Standard_Collection\", \"$2\":\"SCP\"} |\n| expire_seconds | 600 |" + }, + "response": [] + }, + { + "name": "run query saved question", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "var base_url = pm.environment.get(\"base_url\")", + "var saved_question_name = pm.collectionVariables.get(\"saved_question_name\")", + "var session = pm.variables.get(\"session\")", + "var saved_question_url = \"/api/v2/saved_questions/by-name/\"", + "const req2 = {", + " url: base_url + saved_question_url + saved_question_name,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req2, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"saved_question_id\", response.data.id)", + " });", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v2/result_data/saved_question/{{saved_question_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "result_data", + "saved_question", + "{{saved_question_id}}" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| saved_qustion_name | Computer Name |" + }, + "response": [] + }, + { + "name": "run query not saved question", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "var base_url = pm.environment.get(\"base_url\")", + "var not_saved_question_name = pm.collectionVariables.get(\"not_saved_question_name\")", + "var group_name = pm.variables.get(\"group_name\")", + "var session = pm.variables.get(\"session\")", + "var expire_seconds = pm.variables.get(\"expire_seconds\")", + "var not_saved_question_url = \"/api/v2/questions\"", + "var parse_question_url = \"/api/v2/parse_question\"", + "", + "setTimeout(function () { ", + "const req2 = {", + " url: base_url + parse_question_url ,", + " method: \"post\",", + " body: JSON.stringify(", + " {", + " \"text\": not_saved_question_name", + " }", + " ),", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req2, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"not_saved_question_parse_data\", JSON.stringify(response.data[0]))", + " });", + "}, 1500);", + "if(group_name){", + " setTimeout(function () { ", + " var groupurl = \"/api/v2/groups/by-name/\"", + " const req = {", + " url: base_url + groupurl + group_name,", + " method: \"get\",", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"group_id\", response.data.id)", + " });", + " }, 4000);", + " setTimeout(function () { ", + " var json_body = JSON.parse(pm.collectionVariables.get(\"not_saved_question_parse_data\"))", + " json_body.expire_seconds = expire_seconds", + " json_body.context_group = {\"id\": -1}", + " json_body.context_group.id = pm.collectionVariables.get(\"group_id\")", + " const req4 = {", + " url: base_url + not_saved_question_url ,", + " method: \"post\",", + " body: JSON.stringify(json_body),", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req4, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"not_saved_question_id\", response.data.id)", + " });", + " }, 5500);", + " ", + "", + "}", + "else{", + " setTimeout(function () { ", + " var json_body = JSON.parse(pm.collectionVariables.get(\"not_saved_question_parse_data\"))", + " json_body.expire_seconds = expire_seconds", + " const req3 = {", + " url: base_url + not_saved_question_url ,", + " method: \"post\",", + " body: JSON.stringify(json_body),", + " header: {", + " 'Content-Type': 'application/json',", + " 'session': session,", + " }", + " ", + " }", + " pm.sendRequest(req3, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"not_saved_question_id\", response.data.id)", + " });", + " }, 3000);", + " ", + "}", + "", + "setTimeout(function () { },11000);", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "session", + "value": "{{session}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/v2/result_data/question/{{not_saved_question_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v2", + "result_data", + "question", + "{{not_saved_question_id}}" + ] + }, + "description": "The variables and their example values for the action are mentioned below.\n\n| **Variable Name** | **Example Value** |\n| ------ | ------ |\n| group_name(optional) | tanium-client-01 |\n| not_saved_question_name | Computer |\n| expire_seconds | 600 |" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "var base_url = pm.environment.get(\"base_url\")", + "var username = pm.environment.get(\"username\")", + "var password = pm.environment.get(\"password\")", + "var login_url = \"/api/v2/session/login\"", + "console.log", + "if(!(username && password)) throw new Error(\"Please set username and password in environment variables\")", + "if(!base_url) throw new Error(\"Please set base_url varible from environment variables\")", + "", + "const req = {", + " url: base_url + login_url,", + " method: \"post\",", + " body: JSON.stringify(", + " {", + " \"username\": username,", + " \"password\": password,", + " ", + " }", + " )", + " ", + " }", + " pm.sendRequest(req, function (err, response) {", + " var response = response.json()", + " pm.collectionVariables.set(\"session\", response.data.session)", + " });" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "session", + "value": "" + }, + { + "key": "sensor_name", + "value": "" + }, + { + "key": "expire_seconds", + "value": "" + }, + { + "key": "group_name", + "value": "" + }, + { + "key": "group_id", + "value": "" + }, + { + "key": "request_body", + "value": "" + }, + { + "key": "id", + "value": "" + }, + { + "key": "parse_query_text", + "value": "" + }, + { + "key": "qustion_id", + "value": "" + }, + { + "key": "package_name", + "value": "" + }, + { + "key": "action_name", + "value": "" + }, + { + "key": "action_group", + "value": "" + }, + { + "key": "action_group_id", + "value": "" + }, + { + "key": "package_id", + "value": "" + }, + { + "key": "package_param", + "value": "" + }, + { + "key": "execute_action_body", + "value": "" + }, + { + "key": "saved_question_id", + "value": "" + }, + { + "key": "saved_question_name", + "value": "" + }, + { + "key": "not_saved_question_id", + "value": "" + }, + { + "key": "not_saved_question_name", + "value": "" + }, + { + "key": "not_saved_question_parse_data", + "value": "" + } + ] +} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 270441f..d0522b6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,14 @@ # File: __init__.py +# # Copyright (c) 2019-2021 Splunk Inc. # -# Licensed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt) +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. diff --git a/readme.html b/readme.html index aaaae37..69fbfe7 100644 --- a/readme.html +++ b/readme.html @@ -1,7 +1,15 @@

Playbook Backward Compatibility

@@ -13,8 +21,30 @@

Playbook Backward Compatibility

  • New action 'Get Question Results' has been added. Hence, it is requested to the end-user to please update their existing playbooks by inserting the corresponding action blocks for this action on the earlier versions of the app.
  • +

    Port Information

    +

    + The app uses HTTP/ HTTPS protocol for communicating with the Tanium server. Below are the default ports used by Splunk SOAR. + + + + + + + + + + + + + + + + +
            Service NameTransport ProtocolPort
            httptcp80
            httpstcp443
    +

    Asset Configuration

    + +

    API Token Generation

    +

    Permissions for Interacting with Tanium REST API

  • Actions may fail if the account you are using to connect to Tanium does not have sufficient permissions.
  • diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fbc283b --- /dev/null +++ b/readme.md @@ -0,0 +1,1023 @@ +[comment]: # "Auto-generated SOAR connector documentation" +# Tanium REST + +Publisher: Splunk +Connector Version: 2\.1\.4 +Product Vendor: Tanium +Product Name: Tanium REST +Product Version Supported (regex): "\.\*" +Minimum Product Version: 5\.0\.0 + +This app supports investigative and generic actions on Tanium + +[comment]: # " File: readme.md" +[comment]: # " Copyright (c) 2019-2021 Splunk Inc." +[comment]: # " Licensed under the Apache License, Version 2.0 (the 'License');" +[comment]: # " you may not use this file except in compliance with the License." +[comment]: # " You may obtain a copy of the License at" +[comment]: # "" +[comment]: # " http://www.apache.org/licenses/LICENSE-2.0" +[comment]: # "" +[comment]: # " Unless required by applicable law or agreed to in writing, software distributed under" +[comment]: # " the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND," +[comment]: # " either express or implied. See the License for the specific language governing permissions" +[comment]: # " and limitations under the License." +[comment]: # "" +## Playbook Backward Compatibility + +- The existing action parameters have been modified for the action given below. Hence, it is + requested to the end-user to please update their existing playbooks by re-inserting \| modifying + \| deleting the corresponding action blocks or by providing appropriate values to these action + parameters to ensure the correct functioning of the playbooks created on the earlier versions of + the app. + + + + - Run Query - 3 new action parameters 'wait_for_results_processing', + 'return_when_n\_results_available', 'wait_for_n\_results_available' are added which helps to + limit the data fetched from the Tanium server. + +- New action 'Get Question Results' has been added. Hence, it is requested to the end-user to + please update their existing playbooks by inserting the corresponding action blocks for this + action on the earlier versions of the app. + +## Port Information + +The app uses HTTP/ HTTPS protocol for communicating with the Tanium server. Below are the default +ports used by Splunk SOAR. + +|         Service Name | Transport Protocol | Port | +|----------------------|--------------------|------| +|         http | tcp | 80 | +|         https | tcp | 443 | + +## Asset Configuration + +- **Consider question results complete at (% out of 100)** + + + + - Consider Tanium question results complete at this value, a percentage out of 100. This + parameter impacts the **run query** and **list processes** actions only. Note that a similar + value can be defined in Tanium user preferences – you might want to reflect the same value + in your app asset configuration as you use in your Tanium user configuration. The time spent + returning your results is dependent on how much data you have on your Tanium instance and + you may want your action to end with a certain percentage threshold instead of waiting for + Tanium to return 100% of the results. + +- **API Token** + + + + - An API token can be used for authentication in place of the basic auth method of username + and password. If the asset is configured with **both** API token and username/password + credentials, the token will be used as the preferred method. However for security purposes, + once the token has expired or if it is invalid, the app will **NOT** revert to basic auth + credentials - the token must either be removed from or replaced in the asset config. + +## API Token Generation + +- There are different methods of creating an API token depending on which version of Tanium is + being used. Later versions allow token generation through the UI, while earlier versions require + the use of curl commands. + +- **IMPORTANT: The default expiration of a generated token is 7 days. To reduce maintenance, we + recommend setting the default expiration to 365 days. Note that you will have to repeat this + process to generate a new token before the current token expires. Failure to do so will cause + integration to break as your token will no longer be valid after such date.** + +- **The following information regarding API calls using curl commands and additional notes have + been taken from the "Tanium Server REST API Reference" documentation. More information can be + gathered by contacting Tanium Support.** + + + + ### UI + +- To generate an API token in the UI and to configure the system to use it, please follow the + steps mentioned in this + [documentation](https://docs.tanium.com/platform_user/platform_user/console_api_tokens.html) . + On Tanium 7.5.2.3503, new API tokens can be generated by selecting Administration \> Permissions + \> API Tokens \> New API Token. Depending on the version of Tanium, the UI may not contain the + token creation button on the page and will only display a list of the existing API tokens. If + this is the case, you will need to use the curl command method. + + + + ### Curl + +- To generate an API token using this method, a session string or token string will need to be + acquired first through the Login API endpoint. Then, the session or token string will be passed + in the header to get the API token. In the examples below, either an empty data set or optional + fields can be passed in the API token request; the latter allows one to specify the trusted ips + at the token level and also set a name and any notes about the token. This can be useful in + identifying the token after it is created, since the token string is not visible in the UI using + this method. + +- #### Login API Endpoint + + + ` /api/v2/login ` + + #### Example Request + + ` $ curl -s -X POST --data-binary @sample_login.json https://localhost/api/v2/session/login ` + + # where sample_login.json contains: + # { + # "username": "jane.doe", + # "domain": "dev", + # "password": "JanesPassword" + # } + + + #### Example Response + + { + "data": { + "session": "1-224-3cb8fe975e0b505045d55584014d99f6510c110d19d0708524c1219dbf717535" + } + } + + +- #### Token API Endpoint + + + ` /api/v2/api_tokens ` + + #### Example Request (session string): + + ` $ curl -s -X POST -H "session:{string}" --data "{ }" https://localhost/api/v2/api_tokens ` + + #### Header Parameters + + | Field | Type | Description | + |---------|--------|------------------------------------------------------------------------------------------------------------------| + | session | string | (Required) The Tanium session or token string. The session string is returned by the Log In and Validate routes. | + + #### Body Parameters + + | Field | Type | Description | + |--------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| + | object | application/json | (Required) An empty data set. You can also pass the data set on the header as ` --data "{ }" ` or ` --data null ` . | + + #### Example Request (with fields): + + ` $ curl -s -X POST -H "session:{string}" --data-binary @new_token.json https://localhost/api/v2/tokens ` + + # where new_token.json contains: + # { + # "expire_in_days": 365, + # "name": "my_token", + # "notes": "My module token.", + # "trusted_ip_addresses": "10.10.10.15,192.168.3.0/24" + # } + + +## Permissions for Interacting with Tanium REST API + +- **Actions may fail if the account you are using to connect to Tanium does not have sufficient + permissions.** + + + + + + +- Computer Groups + + + + - A component of Tanium permissions is the “Computer Groups” which an account can operate on. + Please ensure the account you used to configure the Tanium REST API app has access to any + machines you run queries or actions on. + + + +- Suggested Roles for Phantom Account in Tanium + + + + - The following Tanium Roles shown below can be configured within Tanium and applied to the + account used to connect to Phantom. Note that these roles represent guidance by the Splunk + Phantom team based on testing against Tanium 7.3.314. **The permissions required in your + environment may vary.** + + - On Tanium 7.3.314, roles can be configured by selecting Permissions \> Roles in the Tanium + UI. Roles can be applied to a user account by selecting Administration \> Users \> (View + User) \> Edit Roles in the Tanium UI. + + - Alternatively, you can **Import from XML** directly under Permissions \> Roles in the Tanium + UI. The XML files containing the roles described below are attached to this app's folder. + + + + ` Role #1 Name: Phantom All Questions ` + + - ` Permissions: Can Ask Question and Saved Question. Needed for run query and list processes actions. ` + - ` Ask Dynamic Question: Yes ` + - ` Show Interact: Yes ` + - ` Advanced Permissions: Read Sensor, Read Saved Question ` + + ` Role #2 Name: Phantom Actions ` + + - ` Permissions: Can execute actions only. Needed for execute action and terminate process. ` + - ` Show Interact: Yes ` + - ` Advanced Permissions: Read Action, Write Action, Read Package ` + +## Pagination + +- Pagination is not implemented in this release. So, the results for the actions mentioned below + will be the results that are fetched in a single API call. + + + + - List processes + - List questions + - Run query + +## How to use Run Query Action + +- The **Run Query** action uses **Tanium's Interact Question Bar** to ask questions to retrieve + information from endpoints. For example, you can ask a question that determines whether any + endpoints are missing critical security patches. + +- Parameter Information: + These parameters modify questions asked using one of the two modes of operation specified below. + - **wait_for_results_processing:** Some long-running sensors return intermediate results with + the contents "results currently unavailable", and then [later the sensor fills in the + results](https://docs.tanium.com/interact/interact/results.html#:~:text=Results%20Currently%20Unavailable) + . This option instructs the App to wait until the results are returned to Tanium and only + after that return the final results. The waiting is still time bounded by the + **timeout_seconds** setting. + - **return_when_n\_results_available:** When set, the Tanium REST App will return results to + the playbook as soon as \`N\` results are returned, even if the **Consider question results + complete at (% out of 100)** percentage has not been met. This is useful in scenarios where + the playbook expects to get at most \`N\` results, and wants to return as soon as this + occurs. + - **wait_for_n\_results_available:** When set, the Tanium REST App will wait (up to the + **timeout_seconds** timeout) until at least \`N\` results are returned. This is helpful in + situations where the Tanium server is under high utilization. Sometimes the App will + estimate that 100% of hosts have reported results, even when there are a few stragglers + left. If the playbook author knows that it should be getting \`N\` results, this will wait + past the **Consider question results complete at (% out of 100)** percentage. + +- Two modes of operation are supported for the run query action: + + + + + - Manual Questions + - Using Tanium question syntax, users can directly provide the question to be asked to the + Tanium server in the **query_text** parameter. For more information on Tanium's question + syntax, [click here.](https://docs.tanium.com/interact/interact/questions.html) + + - Make sure the **is_saved_question** box is unchecked since you are providing a question + from scratch. + + - Use the **group name** parameter to run your query on a particular computer group in + your Tanium instance. Users can create a computer group with specific IP + addresses/hostnames on the Tanium UI under Administration>Computer Groups. For a guide + on how to create/manage computer groups in Tanium, [click + here.](https://docs.tanium.com/platform_user/platform_user/console_computer_groups.html) + + + + - NOTE: If the **group_name** parameter is not provided, the query will be executed on + all registered IP addresses/hostnames in your Tanium instance. + + + + - Parameterized Query + + + + - Users can provide the parameter(s) of a Parameterized query in square + brackets(\[parameter-1, parameter-2, ..., parameter-n\]). + + + + - Example: Get Process Details\["parameter-1","parameter-2"\] from all machines + with Computer Name contains localhost + + - Users can ignore the parameter part in the query if they want the default value to + be considered. Below are the 2 ways a user can achieve this: + + + + - Query: Get Process Details from all machines with Computer Name contains + localhost + - Query: Get Process Details\["",""\] from all machines with Computer Name + contains localhost + + - If a user wants to add only one parameter out of two parameters, users can keep the + parameter empty. Below are the examples: + + + + - Example: Get Process Details\["parameter-1",""\] from all machines with Computer + Name contains localhost + - Example: Get Process Details\["","parameter-2"\] from all machines with Computer + Name contains localhost + + - For two or more sensors in a query, users can select one of the below: + + + + - Provide value for all the parameters of all the sensors in the query + + + + - Example: Get Child Processes\["parameter-1"\] and Process + Details\["parameter-2","parameter-3"\] from all machines + + - Do not provide value for any of the parameters of any of the sensors in the + query + + + + - Example: Get Child Processes and Process Details from all machines + + - Provide value for the parameters you want to provide. The parameters for which + you don't want to add value, please use double quotes("") + + + + - Example: Get Child Processes\[""\] and Process Details\["SHA1", ""\] from + all machines + - Example: Get Child Processes\["csrss.exe"\] and Process Details\["", ""\] + from all machines + + + + - Scenarios: + + + + 1. If the Child Processes sensor expects 1 parameter and Process Details expects 2 + parameters. But the user provides only 2 parameters instead of 3, then action + will fail with a proper error message. + - Example: Get Child Processes\["parameter-1"\] and Process + Details\["parameter-2"\] from all machines + 2. If the Child Processes sensor expects 1 parameter and Process Details expects 2 + parameters. But the user provides more than 3 parameters, then action will fail + with a proper error message. + - Example: Get Child Processes\["parameter-1", "parameter-2"\] and Process + Details\["parameter-3", "parameter-4"\] from all machines + 3. If the Child Processes sensor expects 1 parameter and Process Details expects 2 + parameters. But if the user does not provide any parameter in the Child + Processes sensor and 3 parameters in Process Details sensor, then the first + parameter from Process Details will be considered as the only parameter of the + Child Processes sensor and the action will fetch the results accordingly. + - Query provided: Get Child Processes and Process Details\["parameter-1", + "parameter-2", "parameter-3"\] from all machines + - Query that will be executed because of API limitations: Get Child + Processes\["parameter-1"\] and Process Details\["parameter-2", + "parameter-3"\] from all machines + 4. If the Child Processes sensor expects 1 parameter and Process Details expects 2 + parameters. But if the user provides 2 parameters in Child Processes sensor and + 1 parameter in Process Details sensor, then the second parameter from Child + Processes sensor will be considered as the first parameter of the Process + Details sensor and the only parameter of the Process Details sensor will be + considered as the second parameter of the same. The action will fetch the + results accordingly. + - Query provided: Get Child Processes\["parameter-1", "parameter-2"\] and + Process Details\["parameter-3"\] from all machines + - Query that will be executed because of API limitations: Get Child + Processes\["parameter-1"\] and Process Details\["parameter-2", + "parameter-3"\] from all machines + + - Example Run 1 - Get Computer Name: + + + + - ` query text : Get Computer Name from all machines ` + + - ` is saved question : False ` + + - ` group name : ` + + - ` timeout seconds : 600 ` + + + ` ` + + - Example Run 2 - Get Computer Name for Specified Computer Group: + + + + - ` query text : Get Computer Name from all machines ` + + - ` is saved question : False ` + + - ` group name : centos-computers ` + + - ` timeout seconds : 600 ` + + + ` ` + + - Example Run 3 - A Complex Query: + + + + - ` query text : Get Trace Executed Processes[1 month,1522723342293|1522726941293,0,0,10,0,rar.exe,"",-hp,"","",""] from all machines ` + + - ` is saved question : False ` + + - ` group name : ` + + - ` timeout seconds : 600 ` + + + ` ` + + - Example Run 4 - List Process Details for a Specified Device: + + + + - ` query text : Get Process Details["",""] from all machines with Computer Name contains localhost ` + + - ` is saved question : False ` + + - ` group name : centos-computers ` + + - ` timeout seconds : 600 ` + + + ` ` + + + + - Saved Questions + + + + - Users can create 'Saved Questions' on the Tanium UI under Content>Saved Questions and + provide the name of that saved question in the **query_text** parameter to fetch + appropriate results. For a guide on how to create/manage the Saved Questions on your + Tanium instance, [click + here.](https://docs.tanium.com/interact/interact/saving_questions.html) + + - The **is_saved_question** box must be checked for this to work correctly. + + + + + - Example Run: + + + + - ` query text : My Computers ` + + - ` is saved question : True ` + + - ` timeout seconds : 600 ` + + + ` ` + + + +## How to use Terminate Process Action + +- Please follow the steps below to execute this action successfully: + + + + - Create and save a package on the Tanium server with a meaningful package name and add a + command to terminate the required process in the package's command section. + - To terminate the process of particular computers, users can create a computer group with the + IP address/hostname of the target computers and can specify that group name in the + **group_name** parameter. + - If the **group_name** parameter is not provided, then the terminate process action will be + executed on all the registered IP addresses/hostnames. + + + +## How to use Execute Action + +- The 'Execute Action' action will cause a specified Tanium Package to be executed on the + specified group. + + + + - Create and save a package on the Tanium server with a meaningful package name and add a + command in the package's command section, or just use an existing package. + + - Any parameters required by the specified package must be supplied with a valid JSON via the + **package_parameters** parameter. For example, + ` {"$1":"Standard_Collection", "$2":"SCP"} ` + + - To execute this action on particular computers, users can create a computer group with the + IP address/hostname of the target computers and can specify that group name in the + **group_name** parameter. + + - If the **group_name** parameter is not provided, then the action will be executed on all the + registered IP addresses/hostnames. + + - Example Run: + + + + - ` action name : Splunk Live Response Test ` + + - ` action group : Default ` + + - ` package name : Live Response - Linux ` + + - ` package parameters : {"$1":"Standard_Collection", "$2":"SCP"} ` + + - ` group name : centos-computers ` + + + ` ` + + + + +### Configuration Variables +The below configuration variables are required for this Connector to operate. These variables are specified when configuring a Tanium REST asset in SOAR. + +VARIABLE | REQUIRED | TYPE | DESCRIPTION +-------- | -------- | ---- | ----------- +**base\_url** | required | string | Base URL \(e\.g\. https\://10\.1\.16\.94\) +**api\_token** | optional | password | API Token +**username** | optional | string | Username +**password** | optional | password | Password +**verify\_server\_cert** | optional | boolean | Verify Server Certificate +**results\_percentage** | optional | numeric | Consider question results complete at \(% out of 100\) + +### Supported Actions +[test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity using supplied configuration +[list processes](#action-list-processes) - List the running processes of the devices registered on the Tanium server +[parse question](#action-parse-question) - Parses the supplied text into a valid Tanium query string +[list questions](#action-list-questions) - Retrieves either a history of the most recent questions or a list of saved questions +[terminate process](#action-terminate-process) - Kill a running process of the devices registered on the Tanium server +[execute action](#action-execute-action) - Execute an action on the Tanium server +[run query](#action-run-query) - Run a search query on the devices registered on the Tanium server +[get question results](#action-get-question-results) - Return the results for an already asked question + +## action: 'test connectivity' +Validate the asset configuration for connectivity using supplied configuration + +Type: **test** +Read only: **True** + +#### Action Parameters +No parameters are required for this action + +#### Action Output +No Output + +## action: 'list processes' +List the running processes of the devices registered on the Tanium server + +Type: **investigate** +Read only: **True** + +This action requires specifying a sensor to be used to list processes\. A standard Tanium sensor, 'Process Details' is used by default but a different sensor can be specified instead\. Note that the 'Process Details' sensor may not be available on all Tanium deployments\. Note that at this time this action only supports limiting the query to specified computer groups, but a generic Run Query action can be constructed to query an in individual computer's processes\. As pagination is not implemented, the result\(s\) of the action will be the result\(s\) that are fetched in a single API call\. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**sensor** | required | Sensor which will list all the processes | string | +**group\_name** | optional | Computer group name of which the processes will be listed | string | +**timeout\_seconds** | required | The number of seconds before the question expires | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.group\_name | string | +action\_result\.parameter\.sensor | string | +action\_result\.parameter\.timeout\_seconds | numeric | +action\_result\.data\.\*\.data\.max\_available\_age | string | +action\_result\.data\.\*\.data\.now | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.age | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.archived\_question\_id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.cache\_id | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.hash | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.name | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.type | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.error\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.estimated\_total | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expiration | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.item\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_tested | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.no\_results\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.question\_id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.report\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.cid | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.data\.\*\.text | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.saved\_question\_id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.seconds\_since\_issued | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.select\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.tested | numeric | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary\.num\_results | numeric | +action\_result\.summary\.timeout\_seconds | numeric | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'parse question' +Parses the supplied text into a valid Tanium query string + +Type: **investigate** +Read only: **True** + +

    When asked a non\-saved question in the query\_text parameter, it will parse the given query and give a list of suggestions that are related to it\.

    For example, on the Tanium platform, if one were to just ask the question, 'all IP addresses,' Tanium will give the suggestions\:


    Tanium sorts this list, from most\-related to least\-related\.

    + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**query\_text** | required | Query text to parse | string | `taniumrest question text` + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.query\_text | string | `taniumrest question text` +action\_result\.data\.\*\.from\_canonical\_text | numeric | +action\_result\.data\.\*\.group | string | `taniumrest group definition` +action\_result\.data\.\*\.question\_text | string | `taniumrest question text` +action\_result\.data\.\*\.selects\.\*\.sensor\.hash | numeric | +action\_result\.data\.\*\.selects\.\*\.sensor\.name | string | +action\_result\.data\.\*\.sensor\_references\.\*\.name | string | +action\_result\.data\.\*\.sensor\_references\.\*\.real\_ms\_avg | numeric | +action\_result\.data\.\*\.sensor\_references\.\*\.start\_char | string | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary\.number\_of\_parsed\_questions | numeric | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'list questions' +Retrieves either a history of the most recent questions or a list of saved questions + +Type: **investigate** +Read only: **True** + +If the list\_saved\_questions parameter is true, this action will return a list of saved questions\. If the flag is not set, this action will return the history of recently asked questions\. As pagination is not implemented, the result\(s\) of the action will be the result\(s\) that are fetched in a single API call\. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**list\_saved\_questions** | optional | Retrieve Saved Questions | boolean | + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.list\_saved\_questions | boolean | +action\_result\.data\.\*\.action\_tracking\_flag | boolean | +action\_result\.data\.\*\.archive\_enabled\_flag | boolean | +action\_result\.data\.\*\.archive\_owner | string | +action\_result\.data\.\*\.archive\_owner\.id | numeric | +action\_result\.data\.\*\.archive\_owner\.name | string | +action\_result\.data\.\*\.content\_set\.id | numeric | +action\_result\.data\.\*\.content\_set\.name | string | +action\_result\.data\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.hidden\_flag | boolean | +action\_result\.data\.\*\.id | numeric | +action\_result\.data\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.issue\_seconds\_never\_flag | boolean | +action\_result\.data\.\*\.keep\_seconds | numeric | +action\_result\.data\.\*\.metadata\.\*\.admin\_flag | boolean | +action\_result\.data\.\*\.metadata\.\*\.name | string | +action\_result\.data\.\*\.metadata\.\*\.value | string | +action\_result\.data\.\*\.mod\_time | string | +action\_result\.data\.\*\.mod\_user\.display\_name | string | +action\_result\.data\.\*\.mod\_user\.domain | string | `domain` +action\_result\.data\.\*\.mod\_user\.id | numeric | +action\_result\.data\.\*\.mod\_user\.name | string | +action\_result\.data\.\*\.most\_recent\_question\_id | numeric | +action\_result\.data\.\*\.name | string | +action\_result\.data\.\*\.packages\.\*\.id | numeric | +action\_result\.data\.\*\.packages\.\*\.name | string | +action\_result\.data\.\*\.public\_flag | boolean | +action\_result\.data\.\*\.query\_text | string | `taniumrest question text` +action\_result\.data\.\*\.question\.id | numeric | +action\_result\.data\.\*\.row\_count\_flag | boolean | +action\_result\.data\.\*\.sort\_column | numeric | +action\_result\.data\.\*\.user\.deleted\_flag | boolean | +action\_result\.data\.\*\.user\.id | numeric | +action\_result\.data\.\*\.user\.name | string | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary\.num\_saved\_questions | numeric | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'terminate process' +Kill a running process of the devices registered on the Tanium server + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**action\_name** | required | Name of the action | string | +**action\_group** | required | Group of the action | string | +**package\_name** | required | Package name that will be executed | string | +**package\_parameters** | optional | Package parameters of the corresponding package | string | +**group\_name** | optional | Computer group name of which the process will be terminated | string | +**distribute\_seconds** | optional | The number of seconds over which to deploy the action | numeric | +**issue\_seconds** | optional | The number of seconds to reissue an action from the saved action | numeric | +**expire\_seconds** | required | The duration from the start time before the action expires | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.action\_group | string | +action\_result\.parameter\.action\_name | string | +action\_result\.parameter\.distribute\_seconds | numeric | +action\_result\.parameter\.expire\_seconds | numeric | +action\_result\.parameter\.group\_name | string | +action\_result\.parameter\.issue\_seconds | numeric | +action\_result\.parameter\.package\_name | string | +action\_result\.parameter\.package\_parameters | string | +action\_result\.data\.\*\.action\_group\_id | numeric | +action\_result\.data\.\*\.approved\_flag | boolean | +action\_result\.data\.\*\.approver\.id | numeric | +action\_result\.data\.\*\.approver\.name | string | +action\_result\.data\.\*\.comment | string | +action\_result\.data\.\*\.creation\_time | string | +action\_result\.data\.\*\.distribute\_seconds | numeric | +action\_result\.data\.\*\.end\_time | string | +action\_result\.data\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.id | numeric | +action\_result\.data\.\*\.issue\_count | numeric | +action\_result\.data\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.last\_action\.id | numeric | +action\_result\.data\.\*\.last\_action\.start\_time | string | +action\_result\.data\.\*\.last\_action\.target\_group\.id | numeric | +action\_result\.data\.\*\.last\_start\_time | string | +action\_result\.data\.\*\.name | string | +action\_result\.data\.\*\.next\_start\_time | string | +action\_result\.data\.\*\.package\_spec\.available\_time | string | +action\_result\.data\.\*\.package\_spec\.command | string | +action\_result\.data\.\*\.package\_spec\.command\_timeout | numeric | +action\_result\.data\.\*\.package\_spec\.content\_set\.id | numeric | +action\_result\.data\.\*\.package\_spec\.content\_set\.name | string | +action\_result\.data\.\*\.package\_spec\.creation\_time | string | +action\_result\.data\.\*\.package\_spec\.deleted\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.display\_name | string | +action\_result\.data\.\*\.package\_spec\.expire\_seconds | numeric | +action\_result\.data\.\*\.package\_spec\.hidden\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.id | numeric | +action\_result\.data\.\*\.package\_spec\.last\_modified\_by | string | +action\_result\.data\.\*\.package\_spec\.last\_update | string | +action\_result\.data\.\*\.package\_spec\.mod\_user\.display\_name | string | +action\_result\.data\.\*\.package\_spec\.mod\_user\.domain | string | `domain` +action\_result\.data\.\*\.package\_spec\.mod\_user\.id | numeric | +action\_result\.data\.\*\.package\_spec\.mod\_user\.name | string | +action\_result\.data\.\*\.package\_spec\.modification\_time | string | +action\_result\.data\.\*\.package\_spec\.name | string | +action\_result\.data\.\*\.package\_spec\.process\_group\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.skip\_lock\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.source\_hash | string | `sha256` +action\_result\.data\.\*\.package\_spec\.source\_hash\_changed\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.source\_id | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_expire\_seconds | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_group\.id | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_group\_id | numeric | +action\_result\.data\.\*\.policy\_flag | boolean | +action\_result\.data\.\*\.public\_flag | boolean | +action\_result\.data\.\*\.start\_now\_flag | boolean | +action\_result\.data\.\*\.start\_time | string | +action\_result\.data\.\*\.status | numeric | +action\_result\.data\.\*\.target\_group\.id | numeric | +action\_result\.data\.\*\.user\.id | numeric | +action\_result\.data\.\*\.user\.name | string | +action\_result\.data\.\*\.user\_start\_time | string | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary | string | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'execute action' +Execute an action on the Tanium server + +Type: **generic** +Read only: **False** + +
  • See top\-level app documentation for example parameters\.
  • If a parameterized package is used for executing an action all the parameters must be provided with correct and unique keys\. If any key is repeated then the value of that key will be overwritten\.
  • If the issue\_seconds parameter is provided, then the action will respawn after a time interval provided in the issue\_seconds parameter\.
  • + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**action\_name** | required | Creates a name for the action executed | string | +**action\_group** | required | Group of the action | string | +**package\_name** | required | Name of the Tanium package to be executed | string | +**package\_parameters** | optional | Parameter inputs of the corresponding package\. Provide JSON format \(i\.e\. \{"$1"\: "Standard\_Collection", "$2"\: "SCP"\}\) | string | +**group\_name** | optional | The Tanium Computer Group name on which the action will be executed\. If left blank, will execute on all registered IP addresses/hostnames in your Tanium instance | string | `taniumrest group definition` +**distribute\_seconds** | optional | The number of seconds over which to deploy the action | numeric | +**issue\_seconds** | optional | The number of seconds to reissue an action from the saved action | numeric | +**expire\_seconds** | required | The duration from the start time before the action expires | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.action\_group | string | +action\_result\.parameter\.action\_name | string | +action\_result\.parameter\.distribute\_seconds | numeric | +action\_result\.parameter\.expire\_seconds | numeric | +action\_result\.parameter\.group\_name | string | `taniumrest group definition` +action\_result\.parameter\.issue\_seconds | numeric | +action\_result\.parameter\.package\_name | string | +action\_result\.parameter\.package\_parameters | string | +action\_result\.data\.\*\.action\_group\_id | numeric | +action\_result\.data\.\*\.approved\_flag | boolean | +action\_result\.data\.\*\.approver\.id | numeric | +action\_result\.data\.\*\.approver\.name | string | +action\_result\.data\.\*\.comment | string | +action\_result\.data\.\*\.creation\_time | string | +action\_result\.data\.\*\.distribute\_seconds | numeric | +action\_result\.data\.\*\.end\_time | string | +action\_result\.data\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.id | numeric | `taniumrest question id` +action\_result\.data\.\*\.issue\_count | numeric | +action\_result\.data\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.last\_action\.id | numeric | +action\_result\.data\.\*\.last\_action\.start\_time | string | +action\_result\.data\.\*\.last\_action\.target\_group\.id | numeric | +action\_result\.data\.\*\.last\_start\_time | string | +action\_result\.data\.\*\.name | string | +action\_result\.data\.\*\.next\_start\_time | string | +action\_result\.data\.\*\.package\_spec\.available\_time | string | +action\_result\.data\.\*\.package\_spec\.command | string | +action\_result\.data\.\*\.package\_spec\.command\_timeout | numeric | +action\_result\.data\.\*\.package\_spec\.content\_set\.id | numeric | +action\_result\.data\.\*\.package\_spec\.content\_set\.name | string | +action\_result\.data\.\*\.package\_spec\.creation\_time | string | +action\_result\.data\.\*\.package\_spec\.deleted\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.display\_name | string | +action\_result\.data\.\*\.package\_spec\.expire\_seconds | numeric | +action\_result\.data\.\*\.package\_spec\.hidden\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.id | numeric | +action\_result\.data\.\*\.package\_spec\.last\_modified\_by | string | +action\_result\.data\.\*\.package\_spec\.last\_update | string | +action\_result\.data\.\*\.package\_spec\.mod\_user\.display\_name | string | +action\_result\.data\.\*\.package\_spec\.mod\_user\.domain | string | `domain` +action\_result\.data\.\*\.package\_spec\.mod\_user\.id | numeric | +action\_result\.data\.\*\.package\_spec\.mod\_user\.name | string | +action\_result\.data\.\*\.package\_spec\.modification\_time | string | +action\_result\.data\.\*\.package\_spec\.name | string | +action\_result\.data\.\*\.package\_spec\.parameters\.\*\.key | string | +action\_result\.data\.\*\.package\_spec\.parameters\.\*\.type | numeric | +action\_result\.data\.\*\.package\_spec\.parameters\.\*\.value | string | +action\_result\.data\.\*\.package\_spec\.process\_group\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.skip\_lock\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.source\_hash | string | `sha256` +action\_result\.data\.\*\.package\_spec\.source\_hash\_changed\_flag | boolean | +action\_result\.data\.\*\.package\_spec\.source\_id | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_expire\_seconds | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_group\.id | numeric | +action\_result\.data\.\*\.package\_spec\.verify\_group\_id | numeric | +action\_result\.data\.\*\.policy\_flag | boolean | +action\_result\.data\.\*\.public\_flag | boolean | +action\_result\.data\.\*\.start\_now\_flag | boolean | +action\_result\.data\.\*\.start\_time | string | +action\_result\.data\.\*\.status | numeric | +action\_result\.data\.\*\.target\_group\.id | numeric | +action\_result\.data\.\*\.user\.id | numeric | +action\_result\.data\.\*\.user\.name | string | +action\_result\.data\.\*\.user\_start\_time | string | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary | string | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'run query' +Run a search query on the devices registered on the Tanium server + +Type: **investigate** +Read only: **True** + +See top\-level app documentation for example parameters\. For manual questions only, the action waits for timeout\_seconds provided by the user in intervals of 5 seconds to fetch the results\. The action is a success as soon as the results are retrieved or else it will timeout and fail\. As pagination is not implemented, the result\(s\) of the action will be the result\(s\) that are fetched in a single API call\. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**query\_text** | required | Query to run \(in Tanium Question Syntax\) | string | `taniumrest question text` +**group\_name** | optional | The Tanium Computer Group name on which the query will be executed \(manual query only\) | string | +**is\_saved\_question** | optional | Check this box if the query text parameter refers to a 'Saved Question' on your Tanium | boolean | +**timeout\_seconds** | required | The number of seconds before the question expires \(manual query only\) | numeric | +**wait\_for\_results\_processing** | optional | Flag to wait for endpoint to return full results | boolean | +**return\_when\_n\_results\_available** | optional | Return results as soon as 'n' answers are available | numeric | +**wait\_for\_n\_results\_available** | optional | Wait until 'n' results are present, even if hit the percent complete threshold | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.group\_name | string | +action\_result\.parameter\.is\_saved\_question | boolean | +action\_result\.parameter\.query\_text | string | `taniumrest question text` +action\_result\.parameter\.timeout\_seconds | numeric | +action\_result\.parameter\.wait\_for\_results\_processing | boolean | +action\_result\.parameter\.return\_when\_n\_results\_available | numeric | +action\_result\.parameter\.wait\_for\_n\_results\_available | numeric | +action\_result\.data\.\*\.data\.max\_available\_age | string | +action\_result\.data\.\*\.data\.now | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.age | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.archived\_question\_id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.cache\_id | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.hash | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.name | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.type | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.error\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.estimated\_total | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expiration | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.item\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_tested | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.no\_results\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.question\_id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.report\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.cid | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.data\.\*\.text | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.saved\_question\_id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.seconds\_since\_issued | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.select\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.tested | numeric | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary\.number\_of\_rows | numeric | +action\_result\.summary\.timeout\_seconds | numeric | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | + +## action: 'get question results' +Return the results for an already asked question + +Type: **investigate** +Read only: **True** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**question\_id** | required | The ID of the question | numeric | `taniumrest question id` + +#### Action Output +DATA PATH | TYPE | CONTAINS +--------- | ---- | -------- +action\_result\.parameter\.question\_id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.max\_available\_age | string | +action\_result\.data\.\*\.data\.now | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.age | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.archived\_question\_id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.cache\_id | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.hash | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.name | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.columns\.\*\.type | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.error\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.estimated\_total | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expiration | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.expire\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.filtered\_row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.issue\_seconds | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.item\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.mr\_tested | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.no\_results\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.passed | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.question\_id | numeric | `taniumrest question id` +action\_result\.data\.\*\.data\.result\_sets\.\*\.report\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.row\_count\_machines | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.cid | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.data\.\*\.text | string | +action\_result\.data\.\*\.data\.result\_sets\.\*\.rows\.\*\.id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.saved\_question\_id | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.seconds\_since\_issued | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.select\_count | numeric | +action\_result\.data\.\*\.data\.result\_sets\.\*\.tested | numeric | +action\_result\.status | string | +action\_result\.message | string | +action\_result\.summary\.number\_of\_rows | numeric | +action\_result\.summary\.timeout\_seconds | numeric | +summary\.total\_objects | numeric | +summary\.total\_objects\_successful | numeric | \ No newline at end of file diff --git a/release_notes/1.0.2.md b/release_notes/1.0.2.md new file mode 100644 index 0000000..f28a6c7 --- /dev/null +++ b/release_notes/1.0.2.md @@ -0,0 +1,6 @@ +**Tanium REST Release Notes - Published by Splunk October 09, 2019** + + +**Version 1.0.2 - Released October 09, 2019** + +* Initial Release diff --git a/release_notes/2.0.0.md b/release_notes/2.0.0.md new file mode 100644 index 0000000..487ba44 --- /dev/null +++ b/release_notes/2.0.0.md @@ -0,0 +1,9 @@ +**Tanium REST Release Notes - Published by Splunk April 22, 2020** + + +**Version 2.0.0 - Released April 22, 2020** + +* Compatibility changes for Python 3 support +* Added support to auto refresh the expired token +* Fixed the output table views +* Handled exceptions for Unicode character issues diff --git a/release_notes/2.0.1.md b/release_notes/2.0.1.md new file mode 100644 index 0000000..eb9e1ad --- /dev/null +++ b/release_notes/2.0.1.md @@ -0,0 +1,7 @@ +**Tanium REST Release Notes - Published by Splunk January 22, 2021** + + +**Version 2.0.1 - Released January 22, 2021** + +* Fixed custom table views +* Handled exceptions with valid error messages diff --git a/release_notes/2.1.4.md b/release_notes/2.1.4.md new file mode 100644 index 0000000..7887aa0 --- /dev/null +++ b/release_notes/2.1.4.md @@ -0,0 +1,8 @@ +**Tanium REST Release Notes - Published by Splunk December 21, 2021** + + +**Version 2.1.4 - Released December 21, 2021** + +* Added configuration parameter for the token-based authentication [PAPP-21244] +* Updated the parsing logic for the 'run query' action [PAPP-19739] +* Added more descriptive message for permission errors [PAPP-8836] \ No newline at end of file diff --git a/release_notes/release_notes.html b/release_notes/release_notes.html new file mode 100644 index 0000000..3f58be0 --- /dev/null +++ b/release_notes/release_notes.html @@ -0,0 +1,24 @@ +Tanium REST Release Notes - Published by Splunk December 21, 2021 +

    +Version 2.1.4 - Released December 21, 2021 + +Version 2.0.1 - Released January 22, 2021 + +Version 2.0.0 - Released April 22, 2020 + +Version 1.0.2 - Released October 09, 2019 + diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md new file mode 100644 index 0000000..fbcb2fd --- /dev/null +++ b/release_notes/unreleased.md @@ -0,0 +1 @@ +**Unreleased** diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..523a0ce --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +beautifulsoup4==4.9.1 +requests==2.25.0 diff --git a/taniumrest.json b/taniumrest.json index ff70cf8..03d1767 100644 --- a/taniumrest.json +++ b/taniumrest.json @@ -10,15 +10,16 @@ "product_version_regex": ".*", "publisher": "Splunk", "python_version": "3", + "fips_compliant": true, "license": "Copyright (c) 2019-2021 Splunk Inc.", "latest_tested_versions": [ - "Build (Windows): 7.3.314.4269 | Console: 1.3.3.0150" + "Build (Windows): 7.5.2.3503 | Console: 3.0.64" ], - "app_version": "2.0.1", - "utctime_updated": "2021-01-18T06:20:26.000000Z", + "app_version": "2.1.4", + "utctime_updated": "2021-12-21T06:29:11.000000Z", "package_name": "phantom_taniumrest", "main_module": "taniumrest_connector.py", - "min_phantom_version": "4.9.39220", + "min_phantom_version": "5.0.0", "app_wizard_version": "1.0.0", "configuration": { "base_url": { @@ -27,29 +28,32 @@ "required": true, "order": 0 }, + "api_token": { + "description": "API Token", + "data_type": "password", + "order": 1 + }, "username": { "description": "Username", "data_type": "string", - "required": true, "order": 2 }, "password": { "description": "Password", "data_type": "password", - "required": true, "order": 3 }, "verify_server_cert": { "description": "Verify Server Certificate", "data_type": "boolean", "default": false, - "order": 1 + "order": 4 }, "results_percentage": { "description": "Consider question results complete at (% out of 100)", "data_type": "numeric", "default": 99, - "order": 4 + "order": 5 } }, "actions": [ @@ -302,7 +306,7 @@ ] }, { - "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.*.text", + "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.text", "data_type": "string", "example_values": [ "TaniumModuleServer.exe" @@ -2251,7 +2255,7 @@ ] }, { - "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.*.text", + "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.text", "data_type": "string", "example_values": [ "10.1.16.5" @@ -2562,7 +2566,7 @@ ] }, { - "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.*.text", + "data_path": "action_result.data.*.data.result_sets.*.rows.*.data.*.text", "data_type": "string", "example_values": [ "10.1.16.5" @@ -2655,5 +2659,37 @@ }, "versions": "EQ(*)" } - ] + ], + "pip_dependencies": { + "wheel": [ + { + "module": "beautifulsoup4", + "input_file": "wheels/beautifulsoup4-4.9.1-py3-none-any.whl" + }, + { + "module": "certifi", + "input_file": "wheels/certifi-2021.10.8-py2.py3-none-any.whl" + }, + { + "module": "chardet", + "input_file": "wheels/chardet-3.0.4-py2.py3-none-any.whl" + }, + { + "module": "idna", + "input_file": "wheels/idna-2.10-py2.py3-none-any.whl" + }, + { + "module": "requests", + "input_file": "wheels/requests-2.25.0-py2.py3-none-any.whl" + }, + { + "module": "soupsieve", + "input_file": "wheels/soupsieve-2.3-py3-none-any.whl" + }, + { + "module": "urllib3", + "input_file": "wheels/urllib3-1.26.7-py2.py3-none-any.whl" + } + ] + } } \ No newline at end of file diff --git a/taniumrest_connector.py b/taniumrest_connector.py index e920262..c2bde3d 100644 --- a/taniumrest_connector.py +++ b/taniumrest_connector.py @@ -1,22 +1,33 @@ # File: taniumrest_connector.py +# # Copyright (c) 2019-2021 Splunk Inc. # -# Licensed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt) - -# Phantom App imports -import phantom.app as phantom -from phantom.base_connector import BaseConnector -from phantom.action_result import ActionResult -from taniumrest_consts import * +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. +# +# import ast +import json import os import sys -import requests -import json from time import sleep -from bs4 import BeautifulSoup -from bs4 import UnicodeDammit + +import phantom.app as phantom +import requests +from bs4 import BeautifulSoup, UnicodeDammit +from phantom.action_result import ActionResult +from phantom.base_connector import BaseConnector + +from taniumrest_consts import * class RetVal(tuple): @@ -33,6 +44,7 @@ def __init__(self): self._state = None self._base_url = None + self._api_token = None self._username = None self._password = None self._verify = None @@ -50,7 +62,7 @@ def _handle_py_ver_compat_for_input_str(self, input_str): try: if input_str and self._python_version == 2: input_str = UnicodeDammit(input_str).unicode_markup.encode('utf-8') - except: + except Exception: self.debug_print("Error occurred while handling python 2to3 compatibility for the input string") return input_str @@ -61,27 +73,25 @@ def _get_error_message_from_exception(self, e): :return: error message """ + error_code = TANIUMREST_ERR_CODE_MSG + error_msg = TANIUMREST_ERR_MSG_UNAVAILABLE try: if e.args: if len(e.args) > 1: error_code = e.args[0] error_msg = e.args[1] elif len(e.args) == 1: - error_code = ERR_CODE_MSG + error_code = TANIUMREST_ERR_CODE_MSG error_msg = e.args[0] - else: - error_code = ERR_CODE_MSG - error_msg = ERR_MSG_UNAVAILABLE - except: - error_code = ERR_CODE_MSG - error_msg = ERR_MSG_UNAVAILABLE + except Exception: + self.debug_print("Error occurred while retrieving exception information") try: error_msg = self._handle_py_ver_compat_for_input_str(error_msg) except TypeError: - error_msg = TYPE_ERR_MSG - except: - error_msg = ERR_MSG_UNAVAILABLE + error_msg = TANIUMREST_TYPE_ERR_MSG + except Exception: + error_msg = TANIUMREST_ERR_MSG_UNAVAILABLE return error_code, error_msg @@ -97,16 +107,16 @@ def _validate_integer(self, action_result, parameter, key, allow_zero=False): if parameter is not None: try: if not float(parameter).is_integer(): - return action_result.set_status(phantom.APP_ERROR, INVALID_INT_ERR_MSG.format(key)), None + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_INVALID_INT_ERR_MSG.format(key)), None parameter = int(parameter) - except: - return action_result.set_status(phantom.APP_ERROR, INVALID_INT_ERR_MSG.format(key)), None + except Exception: + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_INVALID_INT_ERR_MSG.format(key)), None if parameter < 0: - return action_result.set_status(phantom.APP_ERROR, INVALID_NON_NEG_INT_ERR_MSG.format(key)), None + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_INVALID_NON_NEG_INT_ERR_MSG.format(key)), None if not allow_zero and parameter == 0: - return action_result.set_status(phantom.APP_ERROR, INVALID_NON_NEG_NON_ZERO_ERR_MSG.format(key)), None + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_INVALID_NON_NEG_NON_ZERO_ERR_MSG.format(key)), None return phantom.APP_SUCCESS, parameter @@ -115,8 +125,9 @@ def _process_empty_response(self, response, action_result): if response.status_code == 200: return RetVal(phantom.APP_SUCCESS, {}) - return RetVal(action_result.set_status(phantom.APP_ERROR, "Status Code: {}. Empty response and no information in the header".format( - response.status_code)), None) + return RetVal(action_result.set_status( + phantom.APP_ERROR, + "Status Code: {}. Empty response and no information in the header".format(response.status_code)), None) def _process_html_response(self, response, action_result): @@ -132,10 +143,11 @@ def _process_html_response(self, response, action_result): split_lines = error_text.split('\n') split_lines = [x.strip() for x in split_lines if x.strip()] error_text = '\n'.join(split_lines) - except: + except Exception: error_text = "Cannot parse error details" - message = "Status Code: {0}. Data from server:\n{1}\n".format(status_code, + message = "Status Code: {0}. Data from server:\n{1}\n".format( + status_code, self._handle_py_ver_compat_for_input_str(error_text)) message = message.replace('{', '{{').replace('}', '}}') @@ -152,7 +164,9 @@ def _process_json_response(self, r, action_result): resp_json = r.json() except Exception as e: error_code, error_msg = self._get_error_message_from_exception(e) - return RetVal(action_result.set_status(phantom.APP_ERROR, "Unable to parse JSON response. Error Code: {0} Error Message: {1}".format(error_code, error_msg)), None) + return RetVal(action_result.set_status( + phantom.APP_ERROR, + "Unable to parse JSON response. Error Code: {0} Error Message: {1}".format(error_code, error_msg)), None) # Please specify the status codes here if 200 <= r.status_code < 399: @@ -166,6 +180,11 @@ def _process_json_response(self, r, action_result): else: message = "Error from server. Status Code: {0} Data from server: {1}".format( r.status_code, self._handle_py_ver_compat_for_input_str(r.text.replace('{', '{{').replace('}', '}}'))) + if r.status_code == 404: + permission_error = "\nThis error usually means the account you are using to interface to Tanium " \ + "does not have sufficient permissions to perform this action. See Tanium's documentation " \ + "for more information on how to change your permissions." + message = "{}{}".format(message, permission_error) except Exception: message = "Error from server. Status Code: {0}. Please provide valid input".format(r.status_code) @@ -202,7 +221,8 @@ def _process_response(self, r, action_result): return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - def _make_rest_call(self, endpoint, action_result, verify=True, headers=None, params=None, data=None, json=None, method="get"): + def _make_rest_call(self, endpoint, action_result, verify=True, headers=None, + params=None, data=None, json=None, method="get"): """ Function that makes the REST call to the app. :param endpoint: REST endpoint that needs to appended to the service address @@ -236,12 +256,15 @@ def _make_rest_call(self, endpoint, action_result, verify=True, headers=None, pa return RetVal(action_result.set_status(phantom.APP_ERROR, error_message), resp_json) except Exception as e: error_code, error_msg = self._get_error_message_from_exception(e) - return RetVal(action_result.set_status(phantom.APP_ERROR, "Error occurred while making the REST call to the Tanium server. Error Code: {0}. Error Message: {1}" - .format(error_code, error_msg)), None) + return RetVal(action_result.set_status( + phantom.APP_ERROR, + "Error occurred while making the REST call to the Tanium server. Error Code: {0}. Error Message: {1}" + .format(error_code, error_msg)), None) return self._process_response(r, action_result) - def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=None, params=None, data=None, json=None, method="get"): + def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=None, + params=None, data=None, json=None, method="get"): """ Function that helps setting REST call to the app. :param endpoint: REST endpoint that needs to appended to the service address @@ -270,13 +293,15 @@ def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=N 'Content-Type': 'application/json' }) - ret_val, resp_json = self._make_rest_call(url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) + ret_val, resp_json = self._make_rest_call( + url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) # If token is expired, generate a new token msg = action_result.get_message() if msg and ("403" in msg or "401" in msg): - self.debug_print("Refreshing Tanium API and re-trying request to [{0}] because API token was expired or invalid with error code [{1}]".format(url, msg)) + self.debug_print("Refreshing Tanium API and re-trying request to [{0}] because API " + "token was expired or invalid with error code [{1}]".format(url, msg)) ret_val = self._get_token(action_result) if phantom.is_fail(ret_val): @@ -285,13 +310,16 @@ def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=N headers.update({'session': str(self._session_id), 'Content-Type': 'application/json'}) - ret_val, resp_json = self._make_rest_call(url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) + ret_val, resp_json = self._make_rest_call( + url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) elif msg and ("404" in msg and "result_data/question" in endpoint): - # Issue seen in Tanium 7.3.314.4103. Sometimes it returns a 404 for a sensor and says that the sensor doesn't exist even though it does - # A short sleep and resubmit fixes the issue - self.debug_print("Encountered Tanium `REST Object Not Found Exception: SensorNotFound: The requested sensor was not found` error") + # Issue seen in Tanium 7.3.314.4103. Sometimes it returns a 404 for a sensor and says that the sensor + # doesn't exist even though it does. A short sleep and resubmit fixes the issue + self.debug_print("Encountered Tanium `REST Object Not Found Exception: " + "SensorNotFound: The requested sensor was not found` error") sleep(5) - ret_val, resp_json = self._make_rest_call(url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) + ret_val, resp_json = self._make_rest_call( + url, action_result, verify=verify, headers=headers, params=params, data=data, json=json, method=method) if phantom.is_fail(ret_val): self.debug_print("REST API Call Failure! Failed call to Tanium API endpoint {0} with error code {1}".format(url, msg)) @@ -300,7 +328,7 @@ def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=N return phantom.APP_SUCCESS, resp_json def _get_token(self, action_result, from_action=False): - """ This function is used to get a token via REST Call. + """ If an API token is not already provided, this function is used to get a token via REST call. :param action_result: Object of action result :param from_action: Boolean object of from_action @@ -315,7 +343,9 @@ def _get_token(self, action_result, from_action=False): 'Content-Type': 'application/json' } - ret_val, resp_json = self._make_rest_call("{}{}".format(self._base_url, SESSION_URL), action_result, verify=self._verify, headers=headers, json=data, method='post') + ret_val, resp_json = self._make_rest_call( + "{}{}".format(self._base_url, TANIUMREST_SESSION_URL), action_result, + verify=self._verify, headers=headers, json=data, method='post') if phantom.is_fail(ret_val): self.debug_print("Failed to fetch a session token from Tanium API!") @@ -336,13 +366,15 @@ def _handle_test_connectivity(self, param): self.save_progress("Connecting to endpoint") - ret_val = self._get_token(action_result) + if not self._api_token: + ret_val = self._get_token(action_result) + if phantom.is_fail(ret_val): + self.save_progress("Test Connectivity Failed") + return action_result.get_status() - if phantom.is_fail(ret_val): - self.save_progress("Test Connectivity Failed") - return action_result.get_status() # make rest call - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_SAVED_QUESTIONS, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_SAVED_QUESTIONS, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): self.save_progress("Test Connectivity Failed") @@ -359,10 +391,12 @@ def _handle_list_questions(self, param): if param.get('list_saved_questions', False): summary_txt = "num_saved_questions" - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_SAVED_QUESTIONS, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_SAVED_QUESTIONS, verify=self._verify, params=None, headers=None) else: summary_txt = "num_questions" - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): return action_result.get_status() @@ -414,17 +448,18 @@ def _execute_action_support(self, param, action_result): # noqa: 901 group_name = self._handle_py_ver_compat_for_input_str(param.get('group_name')) # Integer validation for 'distribute_seconds' action parameter - ret_val, distribute_seconds = self._validate_integer(action_result, param.get('distribute_seconds'), DISTRIBUTE_SECONDS_KEY) + ret_val, distribute_seconds = self._validate_integer( + action_result, param.get('distribute_seconds'), TANIUMREST_DISTRIBUTE_SECONDS_KEY) if phantom.is_fail(ret_val): return action_result.get_status() # Integer validation for 'expire_seconds' action parameter - ret_val, expire_seconds = self._validate_integer(action_result, param['expire_seconds'], EXPIRE_SECONDS_KEY) + ret_val, expire_seconds = self._validate_integer(action_result, param['expire_seconds'], TANIUMREST_EXPIRE_SECONDS_KEY) if phantom.is_fail(ret_val): return action_result.get_status() # Integer validation for 'issue_seconds' action parameter - ret_val, issue_seconds = self._validate_integer(action_result, param.get('issue_seconds'), ISSUE_SECONDS_KEY) + ret_val, issue_seconds = self._validate_integer(action_result, param.get('issue_seconds'), TANIUMREST_ISSUE_SECONDS_KEY) if phantom.is_fail(ret_val): return action_result.get_status() @@ -438,8 +473,7 @@ def _execute_action_support(self, param, action_result): # noqa: 901 response_data = response.get("data") if not response_data: - return action_result.set_status(phantom.APP_ERROR, "No package exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the packages".format(package_name)) + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(package_name, "package")) resp_data = self._get_response_data(response_data, action_result, "package") @@ -459,23 +493,31 @@ def _execute_action_support(self, param, action_result): # noqa: 901 parameter_definition = json.loads(parameter_definition) except Exception as e: error_code, error_msg = self._get_error_message_from_exception(e) - action_result.set_status(phantom.APP_ERROR, "Error while fetching package details. Error Code: {0}. Error Message: {1}".format(error_code, error_msg)) + action_result.set_status( + phantom.APP_ERROR, + "Error while fetching package details. Error Code: {0}. Error Message: {1}".format(error_code, error_msg)) if parameter_definition and len(parameter_definition.get("parameters")) != 0: self.debug_print("Provided package is a parameterized package") if package_parameter is None: - return action_result.set_status(phantom.APP_ERROR, 'Please provide the required package parameter in the following format\ + return action_result.set_status( + phantom.APP_ERROR, + 'Please provide the required package parameter in the following format\ :- [{"": ""}, {"": ""}]') try: package_parameter = json.loads(package_parameter) except Exception as e: error_code, error_msg = self._get_error_message_from_exception(e) - return action_result.set_status(phantom.APP_ERROR, - "Error while parsing the 'package_parameter' field. Error Code: {0}. Error Message: {1}".format(error_code, error_msg)) + return action_result.set_status( + phantom.APP_ERROR, + "Error while parsing the 'package_parameter' field. Error Code: {0}. Error Message: {1}" + .format(error_code, error_msg)) if len(package_parameter) != len(parameter_definition.get("parameters")): - return action_result.set_status(phantom.APP_ERROR, "Please provide all the required package parameters in 'package_parameter' parameter") + return action_result.set_status( + phantom.APP_ERROR, + "Please provide all the required package parameters in 'package_parameter' parameter") param_list = list() invalid_keys = list() @@ -487,7 +529,9 @@ def _execute_action_support(self, param, action_result): # noqa: 901 invalid_keys.append(key) if invalid_keys: - return action_result.set_status(phantom.APP_ERROR, "The following key(s) are incorrect: {}. Please provide correct key(s)".format(', '.join(invalid_keys))) + return action_result.set_status( + phantom.APP_ERROR, + "The following key(s) are incorrect: {}. Please provide correct key(s)".format(', '.join(invalid_keys))) data = dict() package_param = dict() @@ -513,7 +557,8 @@ def _execute_action_support(self, param, action_result): # noqa: 901 data["target_group"] = group_as_obj else: endpoint = TANIUMREST_GET_GROUP.format(group_name=group_name) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, endpoint, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): return action_result.get_status() @@ -521,8 +566,7 @@ def _execute_action_support(self, param, action_result): # noqa: 901 response_data = response.get("data") if not response_data: - return action_result.set_status(phantom.APP_ERROR, "No group exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the groups".format(group_name)) + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(group_name, "group")) resp_data = self._get_response_data(response_data, action_result, "group") @@ -544,8 +588,7 @@ def _execute_action_support(self, param, action_result): # noqa: 901 response_data = response.get("data") if not response_data: - return action_result.set_status(phantom.APP_ERROR, "No action group exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the action groups".format(action_grp)) + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(action_grp, "action group")) resp_data = self._get_response_data(response_data, action_result, "action group") @@ -568,7 +611,8 @@ def _execute_action_support(self, param, action_result): # noqa: 901 data["issue_seconds"] = issue_seconds # make rest call - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_EXECUTE_ACTION, verify=self._verify, params=None, headers=None, json=data, method="post") + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_EXECUTE_ACTION, verify=self._verify, params=None, headers=None, json=data, method="post") if phantom.is_fail(ret_val): return action_result.get_status() @@ -628,34 +672,38 @@ def _determine_num_results_complete(self, data): return num_complete, num_incomplete def _question_result(self, timeout_seconds, results_percentage, endpoint, action_result, - wait_for_results_processing=None, return_when_n_results_available=None, wait_for_n_results_available=None): + wait_for_results_processing=None, return_when_n_results_available=None, + wait_for_n_results_available=None): - max_range = int(timeout_seconds / WAIT_SECONDS) + (1 if timeout_seconds % WAIT_SECONDS == 0 else 2) + max_range = int(timeout_seconds / TANIUMREST_WAIT_SECONDS) + (1 if timeout_seconds % TANIUMREST_WAIT_SECONDS == 0 else 2) for i in range(1, max_range): - if timeout_seconds > WAIT_SECONDS: + if timeout_seconds > TANIUMREST_WAIT_SECONDS: if i == max_range - 1: - sleep(timeout_seconds - (i - 1) * WAIT_SECONDS - 1) + sleep(timeout_seconds - (i - 1) * TANIUMREST_WAIT_SECONDS - 1) else: - sleep(WAIT_SECONDS) + sleep(TANIUMREST_WAIT_SECONDS) else: sleep(timeout_seconds - 1) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, endpoint, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): return None - # Checking to see if all the results have been returned by the question. Keeps questioning until all results have been returned. + # Checking to see if all the results have been returned by the question. + # Keeps questioning until all results have been returned. question_id = os.path.basename(endpoint) - self.debug_print("Checking if Tanium question ID {} has completed and returned all results . . .".format(question_id)) + self.debug_print( + "Checking if Tanium question ID {} has completed and returned all results . . .".format(question_id)) data = response.get("data", {}) mr_tested = data.get("result_sets", [])[0].get("mr_tested") estimated_total = data.get("result_sets", [])[0].get("estimated_total") if mr_tested and estimated_total: percentage_returned = float(mr_tested) / float(estimated_total) * 100 - self.debug_print("mr_tested: {} | est_total: {} | perc_returned: {} | results_perc: {}".format(mr_tested, estimated_total, - percentage_returned, results_percentage)) + self.debug_print("mr_tested: {} | est_total: {} | perc_returned: {} | results_perc: {}".format( + mr_tested, estimated_total, percentage_returned, results_percentage)) # incomplete is when a sensor returns the value 'current results unavailable' num_results_complete, num_results_incomplete = self._determine_num_results_complete(data) @@ -671,25 +719,36 @@ def _question_result(self, timeout_seconds, results_percentage, endpoint, action continue elif return_when_n_results_available and num_results >= return_when_n_results_available: self.debug_print("'wait_for_results_processing' is {}".format(wait_for_results_processing)) - self.debug_print("Returning results because 'num_results_complete' ({}) >= 'return_when_n_results_available' ({})".format( - num_results_complete, return_when_n_results_available)) + self.debug_print( + "Returning results because 'num_results_complete' ({}) >= 'return_when_n_results_available' ({})" + .format(num_results_complete, return_when_n_results_available)) return response elif wait_for_n_results_available and num_results_complete < wait_for_n_results_available: self.debug_print("Waiting for {} results to finish before completing".format(wait_for_n_results_available)) continue elif int(percentage_returned) < int(results_percentage): - self.debug_print("Tanium question ID {} is {}% done out of {}%. Fetching more results . . .".format(question_id, percentage_returned, results_percentage)) + self.debug_print("Tanium question ID {} is {}% done out of {}%. Fetching more results . . ." + .format(question_id, percentage_returned, results_percentage)) continue # else: return results if `columns` field present else: continue + # reformat response data to simplify data path if data.get("result_sets", [])[0].get("columns"): + rows = data.get("result_sets")[0].get("rows") + for j in range(len(rows)): + formatted = [] + for item in rows[j].get("data"): + formatted.append(item[0]) + response["data"]["result_sets"][0]["rows"][j]["data"] = formatted return response else: - action_result.set_status(phantom.APP_ERROR, "Error while fetching the results from the Tanium server in '{}' expire seconds. Please try increasing the timeout value" - .format(timeout_seconds)) + action_result.set_status( + phantom.APP_ERROR, + "Error while fetching the results from the Tanium server in '{}' expire seconds. \ + Please try increasing the timeout value".format(timeout_seconds)) return None def _handle_list_processes(self, param): @@ -703,7 +762,7 @@ def _handle_list_processes(self, param): group_name = self._handle_py_ver_compat_for_input_str(param.get('group_name')) timeout_seconds = param.get('timeout_seconds', 600) # Integer validation for 'timeout_seconds' action parameter - ret_val, timeout_seconds = self._validate_integer(action_result, timeout_seconds, TIMEOUT_SECONDS_KEY) + ret_val, timeout_seconds = self._validate_integer(action_result, timeout_seconds, TANIUMREST_TIMEOUT_SECONDS_KEY) if phantom.is_fail(ret_val): return action_result.get_status() @@ -712,7 +771,8 @@ def _handle_list_processes(self, param): if group_name: endpoint = TANIUMREST_GET_GROUP.format(group_name=group_name) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, endpoint, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): return action_result.get_status() @@ -720,8 +780,7 @@ def _handle_list_processes(self, param): response_data = response.get("data") if not response_data: - return action_result.set_status(phantom.APP_ERROR, "No group exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the groups".format(group_name)) + return action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(group_name, "group")) resp_data = self._get_response_data(response_data, action_result, "group") @@ -738,14 +797,16 @@ def _handle_list_processes(self, param): data["selects"] = select_list # Ask the 'List Processes' question to Tanium - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None, json=data, method="post") + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None, json=data, method="post") if phantom.is_fail(ret_val): return action_result.get_status() # Now that the question has been processed, fetch the results from the Tanium API question_id = response.get("data", {}).get("id") - self.debug_print("Successfully queried Tanium for 'list_processes' action, got question results id {0}".format(question_id)) + self.debug_print( + "Successfully queried Tanium for 'list_processes' action, got question results id {0}".format(question_id)) endpoint = TANIUMREST_GET_QUESTION_RESULTS.format(question_id=question_id) response = self._question_result(timeout_seconds, self._percentage, endpoint, action_result) @@ -787,7 +848,8 @@ def _handle_parse_question(self, param): query_text = param['query_text'] data = {"text": query_text} - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_PARSE_QUESTION, verify=self._verify, params=None, headers=None, json=data, method="post") + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_PARSE_QUESTION, verify=self._verify, params=None, headers=None, json=data, method="post") if phantom.is_fail(ret_val): return action_result.get_status() @@ -810,7 +872,7 @@ def _handle_run_query(self, param): group_name = self._handle_py_ver_compat_for_input_str(param.get('group_name')) timeout_seconds = param.get('timeout_seconds', 600) # Integer validation for 'timeout_seconds' action parameter - ret_val, timeout_seconds = self._validate_integer(action_result, timeout_seconds, TIMEOUT_SECONDS_KEY) + ret_val, timeout_seconds = self._validate_integer(action_result, timeout_seconds, TANIUMREST_TIMEOUT_SECONDS_KEY) if phantom.is_fail(ret_val): return action_result.get_status() @@ -821,22 +883,27 @@ def _handle_run_query(self, param): wait_for_results_processing = param.get('wait_for_results_processing', False) # Integer validation for 'return_when_n_results_available' action parameter - ret_val, return_when_n_results_available = self._validate_integer(action_result, param.get('return_when_n_results_available'), RETURN_WHEN_N_RESULTS_AVAILABLE_KEY, True) + ret_val, return_when_n_results_available = self._validate_integer( + action_result, param.get('return_when_n_results_available'), TANIUMREST_RETURN_WHEN_N_RESULTS_AVAILABLE_KEY, True) if phantom.is_fail(ret_val): return action_result.get_status() # Integer validation for 'wait_for_n_results_available' action parameter - ret_val, wait_for_n_results_available = self._validate_integer(action_result, param.get('wait_for_n_results_available'), WAIT_FOR_N_RESULTS_AVAILABLE_KEY, True) + ret_val, wait_for_n_results_available = self._validate_integer( + action_result, param.get('wait_for_n_results_available'), TANIUMREST_WAIT_FOR_N_RESULTS_AVAILABLE_KEY, True) if phantom.is_fail(ret_val): return action_result.get_status() if return_when_n_results_available and wait_for_n_results_available and return_when_n_results_available < wait_for_n_results_available: - return action_result.set_status(phantom.APP_ERROR, "Please provide 'return_when_n_results_available' greater than or equal to 'wait_for_n_results_available'") + return action_result.set_status( + phantom.APP_ERROR, + "Please provide 'return_when_n_results_available' greater than or equal to 'wait_for_n_results_available'") if is_saved_question: endpoint = TANIUMREST_GET_SAVED_QUESTION.format(saved_question=query_text) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, endpoint, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): return action_result.get_status() @@ -844,8 +911,8 @@ def _handle_run_query(self, param): response_data = response.get("data") if not response_data: - return action_result.set_status(phantom.APP_ERROR, "No saved question exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the saved questions".format(query_text)) + return action_result.set_status( + phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(query_text, "saved question")) resp_data = self._get_response_data(response_data, action_result, "saved question") @@ -856,15 +923,15 @@ def _handle_run_query(self, param): endpoint = TANIUMREST_GET_SAVED_QUESTION_RESULT.format(saved_question_id=saved_question_id) - response = self._question_result(timeout_seconds, self._percentage, endpoint, action_result, wait_for_results_processing, - return_when_n_results_available, wait_for_n_results_available) + response = self._question_result( + timeout_seconds, self._percentage, endpoint, action_result, wait_for_results_processing, + return_when_n_results_available, wait_for_n_results_available) if response is None: return action_result.get_status() action_result.add_data(response) else: - question_data = self._parse_manual_question(query_text, action_result, group_name=group_name or None) if not question_data: return action_result.get_status() @@ -896,14 +963,15 @@ def _handle_get_question_results(self, param): question_id = param.get('question_id') # Integer validation for 'question_id' action parameter - ret_val, question_id = self._validate_integer(action_result, question_id, QUESTION_ID_KEY, True) + ret_val, question_id = self._validate_integer(action_result, question_id, TANIUMREST_QUESTION_ID_KEY, True) if phantom.is_fail(ret_val): return action_result.get_status() summary = action_result.update_summary({}) self.save_progress("Getting results for question {}".format(question_id)) - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_QUESTION_RESULTS.format(question_id=question_id), verify=self._verify) + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_QUESTION_RESULTS.format(question_id=question_id), verify=self._verify) if phantom.is_fail(ret_val): return action_result.set_status(phantom.APP_ERROR, "Getting question results failed") @@ -919,86 +987,129 @@ def _handle_get_question_results(self, param): return action_result.set_status(phantom.APP_SUCCESS) + def _load_full_sensors_to_obj(self, action_result, obj, param_list): + """ + This method recursively replaces the sensor dictionary to valid key-value mapping + + :param action_result: Object of action result + :param obj: Object to sanitize + :param param_list: Parameter list contains values for the mapping + :return: obj: Valid sanitized object for ask question + """ + + if isinstance(obj, list): + return [self._load_full_sensors_to_obj(action_result, item, param_list) for item in obj] + if isinstance(obj, dict): + if 'sensor' in obj: + # Process the sensor dictionary and replace in the original object + success, obj['sensor'] = self._create_sensor_dict(action_result, obj["sensor"], param_list) + if not success: + raise Exception("Error occurred during creation of sensor dictionary") + else: + return {k: self._load_full_sensors_to_obj(action_result, v, param_list) for k, v in obj.items()} + return obj + def _parameterize_query(self, query, action_result): """ Creates a data structure to send a parameterized sensor query to Tanium """ - sensors = [select["sensor"] for select in query["selects"]] - self.save_progress("Sensors:\n{}".format(json.dumps(sensors))) + selects = query["selects"] + self.save_progress("Sensors:\n{}".format(json.dumps(selects))) # Set param index counter - param_idx = 0 - total_params = 0 + self._param_idx = 0 param_list = query["parameter_values"] sensor_data = [] - for sensor in sensors: - sensor_name = sensor["name"] - endpoint = TANIUMREST_GET_SENSOR_BY_NAME.format(sensor_name=sensor_name) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify) - if phantom.is_fail(ret_val): - action_result.set_status(phantom.APP_ERROR, "Failed to get sensor definition from Tanium") - return - - response_data = response.get("data") - if not response_data: - action_result.set_status(phantom.APP_ERROR, "No sensor exists with name {}. \ - Please verify that your account has sufficient permissions to access the sensors".format(sensor_name)) - return - - resp_data = self._get_response_data(response_data, action_result, "sensor") - if resp_data is None: - return + question_data = { + "selects": selects, + "question_text": query["question_text"] + } + if "group" in query: + question_data["group"] = query["group"] - self.save_progress("Parameter Definition:\n{}".format(resp_data.get("parameter_definition", ""))) + try: + question_data = self._load_full_sensors_to_obj(action_result, question_data, param_list) + except Exception as e: + error_code, error_msg = self._get_error_message_from_exception(e) + self.debug_print( + "Error occurred while converting the sensors. Error code: {}, Message: {}".format(error_code, error_msg)) + return - raw_parameter_definition = resp_data.get("parameter_definition", "") - parameter_definition = None - try: - if raw_parameter_definition: - parameter_definition = json.loads(raw_parameter_definition) - except Exception as e: - error_code, error_msg = self._get_error_message_from_exception(e) - return action_result.set_status(phantom.APP_ERROR, - "Error while parsing the 'parameter_definition'. Error Code: {0}. Error Message: {1}".format(error_code, error_msg)) - - if parameter_definition: - # Parameterized Sensor - parameter_keys = [parameter["key"] for parameter in parameter_definition["parameters"]] - self.save_progress("Parameter Keys:\n{}".format(json.dumps(parameter_keys))) - total_params += len(parameter_keys) - parameters = [] - - for key in parameter_keys: - if param_idx >= len(param_list): - action_result.set_status(phantom.APP_ERROR, "For parameters which you do not want to add value, please use double quotes(\"\").\ - For more details refer to the documentation") - return - - parameter = { - "key": "||%s||" % key, - "value": param_list[param_idx] } - parameters.append(parameter) - param_idx += 1 - - sensor_dict = { - "source_hash": sensor["hash"], - "parameters": parameters } - sensor_data.append({"sensor": sensor_dict}) - else: - # Regular Sensor, can use as-is - sensor_data.append({"sensor": sensor}) + self.save_progress("Sensor Data:\n{}".format(json.dumps(sensor_data))) - if total_params and total_params != len(param_list): + if self._param_idx and self._param_idx != len(param_list): action_result.set_status(phantom.APP_ERROR, "Please provide the exact number of parameters expected by the sensor") return - self.save_progress("Sensor Data:\n{}".format(json.dumps(sensor_data))) - question_data = { "selects": sensor_data, - "question_text": query["question_text"]} - if "group" in query: - # Add in filters - question_data["group"] = query["group"] - return question_data + def _create_sensor_dict(self, action_result, sensor, param_list): + """ + This method fetches parameter definition for the provided sensor and creates key-value mapping + + :param action_result: Object of action result + :param sensor: Sensor object to create key-value mapping + :param param_list: Parameter list contains values for the mapping + :return: obj: Valid sanitized object for provided sensor + """ + + sensor_name = sensor["name"] + endpoint = TANIUMREST_GET_SENSOR_BY_NAME.format(sensor_name=sensor_name) + ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify) + if phantom.is_fail(ret_val): + action_result.set_status(phantom.APP_ERROR, "Failed to get sensor definition from Tanium") + return phantom.APP_ERROR, {} + + response_data = response.get("data") + if not response_data: + action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(sensor_name, "sensor")) + return phantom.APP_ERROR, {} + + resp_data = self._get_response_data(response_data, action_result, "sensor") + if resp_data is None: + return phantom.APP_ERROR, {} + + self.save_progress("Parameter Definition:\n{}".format(resp_data.get("parameter_definition", ""))) + + raw_parameter_definition = resp_data.get("parameter_definition", "") + parameter_definition = None + try: + if raw_parameter_definition: + parameter_definition = json.loads(raw_parameter_definition) + + if not parameter_definition: + # Regular Sensor, can use as-is + return phantom.APP_SUCCESS, sensor + except Exception as e: + error_code, error_msg = self._get_error_message_from_exception(e) + error_msg = "Error while parsing the 'parameter_definition'. Error Code: {0}. Error Message: {1}".format( + error_code, error_msg) + return action_result.set_status(phantom.APP_ERROR, error_msg), {} + + # Parameterized Sensor + parameter_keys = [parameter["key"] for parameter in parameter_definition["parameters"]] + self.save_progress("Parameter Keys:\n{}".format(json.dumps(parameter_keys))) + parameters = [] + + # Map all the keys with the respective value from parma_list + for key in parameter_keys: + if self._param_idx >= len(param_list): + action_result.set_status(phantom.APP_ERROR, TANIUMREST_NOT_ENOUGH_PARAMS) + return + + parameter = { + "key": "||%s||" % key, + "value": param_list[self._param_idx] + } + parameters.append(parameter) + self._param_idx += 1 + + # Create a new sensor dictionary with valid format + sensor_dict = { + "source_hash": sensor["hash"], + "name": sensor_name, + "parameters": parameters + } + return phantom.APP_SUCCESS, sensor_dict + def _parse_manual_question(self, query_text, action_result, group_name=None): # Prepare data dict for posting to /questions data = dict() @@ -1006,7 +1117,8 @@ def _parse_manual_question(self, query_text, action_result, group_name=None): # If a group_name was supplied, validate the group name is valid if group_name: endpoint = TANIUMREST_GET_GROUP.format(group_name=group_name) - ret_val, response = self._make_rest_call_helper(action_result, endpoint, verify=self._verify, params=None, headers=None) + ret_val, response = self._make_rest_call_helper( + action_result, endpoint, verify=self._verify, params=None, headers=None) if phantom.is_fail(ret_val): action_result.set_status(phantom.APP_ERROR, "Failed to get group. Please provide a valid group name") @@ -1015,8 +1127,7 @@ def _parse_manual_question(self, query_text, action_result, group_name=None): response_data = response.get("data") if not response_data: - action_result.set_status(phantom.APP_ERROR, "No group exists with name {}. \ - Also, please verify that your account has sufficient permissions to access the groups".format(group_name)) + action_result.set_status(phantom.APP_ERROR, TANIUMREST_RESOURCE_NOT_EXIST.format(group_name, "group")) return resp_data = self._get_response_data(response_data, action_result, "group") @@ -1031,8 +1142,9 @@ def _parse_manual_question(self, query_text, action_result, group_name=None): # to ensure the query is in a valid Tanium syntax query_to_parse = {"text": query_text} - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_PARSE_QUESTION, verify=self._verify, params=None, headers=None, - json=query_to_parse, method="post") + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_PARSE_QUESTION, verify=self._verify, params=None, + headers=None, json=query_to_parse, method="post") self.save_progress("Parsed Question:\n{}".format(json.dumps(response))) if phantom.is_fail(ret_val): @@ -1064,12 +1176,14 @@ def _parse_manual_question(self, query_text, action_result, group_name=None): return data - def _ask_question(self, data, action_result, timeout_seconds=None, wait_for_results_processing=None, return_when_n_results_available=None, wait_for_n_results_available=None): + def _ask_question(self, data, action_result, timeout_seconds=None, wait_for_results_processing=None, + return_when_n_results_available=None, wait_for_n_results_available=None): # Post prepared data to questions endpoint and poll for results # config = self.get_config() if timeout_seconds: data['expire_seconds'] = timeout_seconds - ret_val, response = self._make_rest_call_helper(action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None, json=data, method="post") + ret_val, response = self._make_rest_call_helper( + action_result, TANIUMREST_GET_QUESTIONS, verify=self._verify, params=None, headers=None, json=data, method="post") if phantom.is_fail(ret_val): action_result.set_status(phantom.APP_ERROR, "Question post failed") @@ -1129,25 +1243,41 @@ def handle_action(self, param): def initialize(self): self._state = self.load_state() + if not isinstance(self._state, dict): + self.debug_print("Resetting the state file with the default format") + self._state = { + "app_version": self.get_app_json().get('app_version') + } # Fetching the Python major version try: self._python_version = int(sys.version_info[0]) - except: + except Exception: return self.set_status(phantom.APP_ERROR, "Error occurred while fetching the Phantom server's Python major version.") config = self.get_config() - self._username = self._handle_py_ver_compat_for_input_str(config['username']) - self._password = config['password'] + self._api_token = config.get('api_token') + if self._api_token: + self._session_id = self._api_token # API uses token in place of session id + else: + self._username = self._handle_py_ver_compat_for_input_str(config.get('username')) + self._password = config.get('password') + + if not self._api_token and not (self._username and self._password): + return self.set_status(phantom.APP_ERROR, "Please provide either an API token, or username and password credentials") + self._verify = config.get('verify_server_cert', False) self._percentage = config.get('results_percentage', 99) # Integer validation for 'results_percentage' configuration parameter - ret_val, self._percentage = self._validate_integer(self, self._percentage, RESULTS_PERCENTAGE_KEY, True) + ret_val, self._percentage = self._validate_integer(self, self._percentage, TANIUMREST_RESULTS_PERCENTAGE_KEY, True) if phantom.is_fail(ret_val): self.get_status() if self._percentage > 100: - return self.set_status(phantom.APP_ERROR, "Please provide a valid integer in range of 0-100 in {}".format(RESULTS_PERCENTAGE_KEY)) + return self.set_status( + phantom.APP_ERROR, + "Please provide a valid integer in range of 0-100 in {}".format(TANIUMREST_RESULTS_PERCENTAGE_KEY) + ) self._base_url = self._handle_py_ver_compat_for_input_str(config['base_url']) @@ -1163,7 +1293,8 @@ def initialize(self): elif self._base_url.startswith('\\'): self._base_url = self._base_url.strip('\\').strip('/') - self._session_id = self._state.get('session_id', '') + if not self._session_id: + self._session_id = self._state.get('session_id', '') return phantom.APP_SUCCESS @@ -1175,9 +1306,10 @@ def finalize(self): if __name__ == '__main__': - import pudb import argparse + import pudb + pudb.set_trace() argparser = argparse.ArgumentParser() @@ -1192,18 +1324,18 @@ def finalize(self): username = args.username password = args.password - if (username is not None and password is None): + if username is not None and password is None: # User specified a username but not a password, so ask import getpass password = getpass.getpass("Password: ") - if (username and password): + if username and password: try: - login_url = TaniumRestConnector._get_phantom_base_url() + '/login' + login_url = '{}/login'.format(TaniumRestConnector._get_phantom_base_url()) print("Accessing the Login page") - r = requests.get(login_url, verify=False) + r = requests.get(login_url, verify=False) # nosemgrep csrftoken = r.cookies['csrftoken'] data = dict() @@ -1212,15 +1344,15 @@ def finalize(self): data['csrfmiddlewaretoken'] = csrftoken headers = dict() - headers['Cookie'] = 'csrftoken=' + csrftoken + headers['Cookie'] = 'csrftoken={}'.format(csrftoken) headers['Referer'] = login_url print("Logging into Platform to get the session id") - r2 = requests.post(login_url, verify=False, data=data, headers=headers) + r2 = requests.post(login_url, verify=False, data=data, headers=headers) # nosemgrep session_id = r2.cookies['sessionid'] except Exception as e: - print("Unable to get session id from the platform. Error: " + str(e)) - exit(1) + print("Unable to get session id from the platform. Error: {}".format(str(e))) + exit(1) # nosemgrep with open(args.input_test_json) as f: in_json = f.read() @@ -1230,11 +1362,11 @@ def finalize(self): connector = TaniumRestConnector() connector.print_progress_message = True - if (session_id is not None): + if session_id is not None: in_json['user_session_token'] = session_id connector._set_csrf_info(csrftoken, headers['Referer']) ret_val = connector._handle_action(json.dumps(in_json), None) print(json.dumps(json.loads(ret_val), indent=4)) - exit(0) + exit(0) # nosemgrep diff --git a/taniumrest_consts.py b/taniumrest_consts.py index 6cecd2b..16eb311 100644 --- a/taniumrest_consts.py +++ b/taniumrest_consts.py @@ -1,9 +1,18 @@ # File: taniumrest_consts.py +# # Copyright (c) 2019-2021 Splunk Inc. # -# Licensed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt) - -SESSION_URL = "/api/v2/session/login" +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. +TANIUMREST_SESSION_URL = "/api/v2/session/login" TANIUMREST_GET_SAVED_QUESTIONS = "/api/v2/saved_questions" TANIUMREST_GET_QUESTIONS = "/api/v2/questions" TANIUMREST_GET_QUESTION_RESULTS = "/api/v2/result_data/question/{question_id}" @@ -15,23 +24,33 @@ TANIUMREST_GET_SAVED_QUESTION = "/api/v2/saved_questions/by-name/{saved_question}" TANIUMREST_GET_SENSOR_BY_NAME = "/api/v2/sensors/by-name/{sensor_name}" TANIUMREST_GET_SAVED_QUESTION_RESULT = "/api/v2/result_data/saved_question/{saved_question_id}" -WAIT_SECONDS = 5 -TANIUMREST_RESULTS_UNAVAILABLE = ["[current results unavailable]", "[current result unavailable]", "[results currently unavailable]"] +TANIUMREST_WAIT_SECONDS = 5 +TANIUMREST_RESULTS_UNAVAILABLE = [ + "[current results unavailable]", + "[current result unavailable]", + "[results currently unavailable]" +] # Constants relating to 'get_error_message_from_exception' -ERR_CODE_MSG = "Error code unavailable" -ERR_MSG_UNAVAILABLE = "Error message unavailable. Please check the asset configuration and|or action parameters" -TYPE_ERR_MSG = "Error occurred while connecting to the Tanium Server. Please check the asset configuration and|or action parameters" +TANIUMREST_ERR_CODE_MSG = "Error code unavailable" +TANIUMREST_ERR_MSG_UNAVAILABLE = "Error message unavailable. Please check the asset configuration and|or action parameters" +TANIUMREST_TYPE_ERR_MSG = "Error occurred while connecting to the Tanium Server. " \ + "Please check the asset configuration and|or action parameters" + +TANIUMREST_NOT_ENOUGH_PARAMS = 'For parameters which you do not want to add value, please use double quotes("").' \ + 'For more details refer to the documentation' +TANIUMREST_RESOURCE_NOT_EXIST = "No {1} exists with name {0}. " \ + "Also, please verify that your account has sufficient permissions to access the {1}s" # Constants relating to 'validate_integer' -INVALID_INT_ERR_MSG = "Please provide a valid integer value in the {}" -INVALID_NON_NEG_INT_ERR_MSG = "Please provide a valid non-negative integer value in the {}" -INVALID_NON_NEG_NON_ZERO_ERR_MSG = "PLease provide a valid non-zero non-negative integer value in the {}" -EXPIRE_SECONDS_KEY = "'expire_seconds' action parameter" -DISTRIBUTE_SECONDS_KEY = "'distribute_seconds' action parameter" -ISSUE_SECONDS_KEY = "'issue_seconds' action parameter" -TIMEOUT_SECONDS_KEY = "'timeout_seconds' action parameter" -RETURN_WHEN_N_RESULTS_AVAILABLE_KEY = "'return_when_n_results_available' action parameter" -WAIT_FOR_N_RESULTS_AVAILABLE_KEY = "'wait_for_n_results_available' action parameter" -RESULTS_PERCENTAGE_KEY = "'Consider question results complete at' configuration parameter" -QUESTION_ID_KEY = "'question_id' action parameter" +TANIUMREST_INVALID_INT_ERR_MSG = "Please provide a valid integer value in the {}" +TANIUMREST_INVALID_NON_NEG_INT_ERR_MSG = "Please provide a valid non-negative integer value in the {}" +TANIUMREST_INVALID_NON_NEG_NON_ZERO_ERR_MSG = "Please provide a valid non-zero non-negative integer value in the {}" +TANIUMREST_EXPIRE_SECONDS_KEY = "'expire_seconds' action parameter" +TANIUMREST_DISTRIBUTE_SECONDS_KEY = "'distribute_seconds' action parameter" +TANIUMREST_ISSUE_SECONDS_KEY = "'issue_seconds' action parameter" +TANIUMREST_TIMEOUT_SECONDS_KEY = "'timeout_seconds' action parameter" +TANIUMREST_RETURN_WHEN_N_RESULTS_AVAILABLE_KEY = "'return_when_n_results_available' action parameter" +TANIUMREST_WAIT_FOR_N_RESULTS_AVAILABLE_KEY = "'wait_for_n_results_available' action parameter" +TANIUMREST_RESULTS_PERCENTAGE_KEY = "'Consider question results complete at' configuration parameter" +TANIUMREST_QUESTION_ID_KEY = "'question_id' action parameter" diff --git a/taniumrest_run_query.html b/taniumrest_run_query.html index bb3c771..849a605 100644 --- a/taniumrest_run_query.html +++ b/taniumrest_run_query.html @@ -11,8 +11,16 @@