├── .github └── workflows │ ├── acs-demo.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── src ├── acs │ └── client.go ├── appinspect │ ├── client.go │ └── client_test.go └── cmd │ ├── get.go │ ├── install.go │ ├── login.go │ ├── main.go │ └── vet.go └── testapp ├── README └── default └── app.conf /.github/workflows/acs-demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: ACS CLI Demo 4 | 5 | on: workflow_dispatch 6 | 7 | env: 8 | STACK_NAME: ${{ secrets.VICTORIA_STACK_NAME }} 9 | 10 | jobs: 11 | install-app-classic: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Generate Package 18 | run: make generate-app-package 19 | 20 | - name: Install Homebrew 21 | run: | 22 | echo "Installing homebrew" 23 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 24 | test -d ~/.linuxbrew && eval $(~/.linuxbrew/bin/brew shellenv) 25 | test -d /home/linuxbrew/.linuxbrew && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) 26 | echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile 27 | echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH 28 | brew --version 29 | 30 | - name: Install ACS CLI 31 | run: | 32 | echo "Installing ACS CLI from Homebrew" 33 | brew tap splunk/tap 34 | brew install acs 35 | 36 | - name: Configure stack in ACS CLI 37 | run: | 38 | acs config add-stack $STACK_NAME --use 39 | 40 | - name: Authenticate ACS CLI with ACS 41 | env: 42 | STACK_TOKEN: ${{ secrets.VICTORIA_STACK_TOKEN }} 43 | run: acs login 44 | 45 | - name: Install The App On Stack using CLI 46 | env: 47 | SPLUNK_USERNAME: ${{ secrets.SPLUNK_COM_USERNAME }} 48 | SPLUNK_PASSWORD: ${{ secrets.SPLUNK_COM_PASSWORD }} 49 | run: acs apps install private --app-package ./app-package.tar.gz --acs-legal-ack Y 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: InstallApp 4 | 5 | on: [push] 6 | 7 | jobs: 8 | 9 | install: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.18 19 | 20 | - name: Build cloudCtl 21 | run: make build-cloudctl 22 | 23 | - name: Generate Package 24 | run: make generate-app-package 25 | 26 | - name: Vet The Package - AppInspect 27 | env: 28 | SPLUNK_COM_USERNAME: ${{ secrets.SPLUNK_COM_USERNAME }} 29 | SPLUNK_COM_PASSWORD: ${{ secrets.SPLUNK_COM_PASSWORD }} 30 | run: make inspect-app-victoria # this can be changed to inspect-app to vet an app intended to be installed on a classic stack 31 | 32 | - uses: actions/upload-artifact@v2 33 | if: ${{ always() }} 34 | with: 35 | name: report.json 36 | path: report.json 37 | 38 | - name: Install The App On Stack - ACS 39 | env: 40 | SPLUNK_COM_USERNAME: ${{ secrets.SPLUNK_COM_USERNAME }} 41 | SPLUNK_COM_PASSWORD: ${{ secrets.SPLUNK_COM_PASSWORD }} 42 | STACK_TOKEN: ${{ secrets.VICTORIA_STACK_TOKEN }} 43 | STACK_NAME: ${{ secrets.VICTORIA_STACK_NAME }} 44 | ACS_URL: ${{ secrets.ACS_URL }} 45 | run: make install-app-victoria # this can be changed to install-app to install an app on a classic stack 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Release Package 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | required: true 10 | description: 'Release version' 11 | 12 | jobs: 13 | tag-commit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v2 18 | 19 | - name: Bump version and push tag 20 | run: | 21 | git tag ${{ github.event.inputs.tag }} 22 | git push origin ${{ github.event.inputs.tag }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | app-package.tar.gz 18 | report.json 19 | cloudCtl 20 | 21 | # GitHub Action Runner 22 | actions-runner/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | (c) 2021 Splunk Inc. All rights reserved. 205 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-cloudctl: 2 | go build -o cloudCtl ./src/cmd 3 | 4 | generate-app-package: 5 | COPYFILE_DISABLE=1 tar zcf app-package.tar.gz testapp 6 | 7 | inspect-app: 8 | ./cloudCtl vet app-package.tar.gz --json-report-file=report.json 9 | 10 | inspect-app-victoria: 11 | ./cloudCtl vet app-package.tar.gz --json-report-file=report.json --victoria 12 | 13 | install-app: 14 | ./cloudCtl install ${STACK_NAME} app-package.tar.gz 15 | 16 | install-app-victoria: 17 | ./cloudCtl install ${STACK_NAME} app-package.tar.gz --victoria 18 | 19 | uninstall-app: 20 | ./cloudCtl uninstall ${STACK_NAME} testapp 21 | 22 | uninstall-app-victoria: 23 | ./cloudCtl uninstall ${STACK_NAME} testapp --victoria 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acs-privateapps-demo 2 | 3 | This repository demonstrates how one can use [Splunkcloud's self-service apis](https://www.splunk.com/en_us/blog/platform/splunk-cloud-self-service-announcing-the-new-admin-config-service-api.html) and [ACS CLI](https://docs.splunk.com/Documentation/SplunkCloud/latest/Config/ACSCLI) to build a pipeline that can continuously deploy Splunk apps to Splunk Enterprise Cloud stacks. 4 | 5 | ## Workflows 6 | 7 | ### [InstallApp](./.github/workflows/main.yml) 8 | The workflow primarily consists of 4 steps: 9 | 1. Build cloudctl (`make build-cloudctl`), the CLI that will be used for the remaining steps -- this step assumes that [go](https://golang.org) is installed. 10 | 1. Package the app artifacts into a tar gz archive (`make generate-app-package`) -- this step assumes there is a top-level directory called `testapp` which contains the app. 11 | 1. Upload the app-package to the app inspect service and wait for the inspection report (`make inspect-app`) -- this step assumes the existence of the environment variables defined below. 12 | 1. If the inspection is successful, install/update the app on the stack using the self-serive apis (`make install-app`) -- this step also assumes the existence of the environment variables defined below. 13 | 14 | ### [ACS CLI Demo](./.github/workflows/acs-demo.yml) 15 | The workflow consists of the following steps: 16 | 1. Package the app artifacts into a tar gz archive (`make generate-app-package`) -- this step assumes there is a top-level directory called `testapp` which contains the app. 17 | 2. Install [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux). 18 | 3. Install [ACS CLI](https://docs.splunk.com/Documentation/SplunkCloud/latest/Config/ACSCLI#Install_or_upgrade_the_ACS_CLI) from Homebrew. 19 | 4. Configure ACS CLI (setup stack and required credentials) 20 | 5. Inspect and install app using ACS CLI `acs apps install private` command. 21 | 22 | 23 | ## Note 24 | * Few steps (app-vetting and app-installation) have Victoria and Classic variations in the Makefile. 25 | * For Stacks in Victoria Experience: Make sure your Victoria stack in at least on Butterfinger (8.2.2112) to use this github demo. 26 | 27 | ## Setting up the environment 28 | The environment needs to be configured with a few variables. If leveraging this from a Github repository using Github Actions workflows, the variables will need to be set up as [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets). If running this locally, these values simply need to be set as environment variables: 29 | * `SPLUNK_COM_USERNAME` / `SPLUNK_COM_PASSWORD` - the [splunk.com](https://login.splunk.com/) credentials to use for authentication to perform app inspection. 30 | * `STACK_NAME` - the name of the Splunk Cloud stack where you want to install/update the app package on. 31 | * `STACK_TOKEN` - the [JWT Token](https://docs.splunk.com/Documentation/Splunk/latest/Security/Setupauthenticationwithtokens) created on the stack. 32 | 33 | 34 | ## Publishing a new version 35 | This repository has been used as dependencies for other projects. 36 | 37 | To manage the dependencies easily by specifying different versions for this module, we will use the module version numbering as recommended by Go - https://go.dev/doc/modules/version-numbers. The versioning will be done using GitHub tags. 38 | 39 | To create a new tag, run the following command: 40 | ```shell 41 | $ git tag vx.x.x 42 | $ git push origin vx.x.x 43 | ``` 44 | 45 | Alternatively, you can use the GitHub action `Release Package` to release generate new tags. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splunk/acs-privateapps-demo 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.2.7 7 | github.com/alecthomas/kong v0.2.12 8 | github.com/go-resty/resty/v2 v2.4.0 9 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect 10 | github.com/jarcoal/httpmock v1.1.0 // indirect 11 | github.com/segmentio/go-prompt v1.2.0 12 | github.com/stretchr/testify v1.7.0 13 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey v1.8.8 h1:Y4yypp763E8cbqb5RBqZhGgkCFLRFnbRBHrxnpMMsgQ= 2 | github.com/AlecAivazis/survey/v2 v2.2.7 h1:5NbxkF4RSKmpywYdcRgUmos1o+roJY8duCLZXbVjoig= 3 | github.com/AlecAivazis/survey/v2 v2.2.7/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk= 4 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 5 | github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= 6 | github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-resty/resty v1.12.0 h1:L1P5qymrXL5H/doXe2pKUr1wxovAI5ilm2LdVLbwThc= 12 | github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= 13 | github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= 14 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 15 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk= 16 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= 17 | github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= 18 | github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 19 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 20 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 21 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 23 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 24 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 25 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 26 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 27 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 28 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 29 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/segmentio/go-prompt v1.2.0 h1:o63/ApvCzF0mC8A07+e5zN/ZE9VTsWsinVSQcNFewUY= 33 | github.com/segmentio/go-prompt v1.2.0/go.mod h1:B3ehdD1xPoWDKgrQgUaGk+m8H1xb1J5TyYDfKpKNeEE= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 36 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 37 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 38 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 41 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 42 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 43 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= 45 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 52 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 54 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 55 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 56 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 57 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 58 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 59 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | -------------------------------------------------------------------------------- /src/acs/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package acs 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | 21 | "github.com/go-resty/resty/v2" 22 | ) 23 | 24 | type Client interface { 25 | InstallApp(stack, token, packageFileName string, packageReader io.Reader) error 26 | DescribeApp(stack string, appName string) (*App, error) 27 | ListApps(stack string) ([]App, error) 28 | UninstallApp(stack string, appName string) error 29 | } 30 | 31 | // victoriaClient is a client used to interface with ACS for Victoria stacks 32 | type victoriaClient struct { 33 | client 34 | } 35 | 36 | // classicClient is a client used to interface with ACS for Classic (non-Victoria) stacks 37 | type classicClient struct { 38 | client 39 | } 40 | 41 | type client struct { 42 | resty *resty.Client 43 | } 44 | 45 | type acsError struct { 46 | Code string `json:"code"` 47 | Description string `json:"description"` 48 | } 49 | 50 | func (e *acsError) Error() string { 51 | return fmt.Sprintf("%s: %s", e.Code, e.Description) 52 | } 53 | 54 | func newClient(acsURL, token string) client { 55 | return client{ 56 | resty: resty.New().SetHostURL(acsURL).SetError(&acsError{}).SetAuthScheme("Bearer").SetAuthToken(token), 57 | } 58 | } 59 | 60 | // NewVictoriaWithURL creates a new VictoriaClient 61 | func NewVictoriaWithURL(acsURL, token string) Client { 62 | return &victoriaClient{ 63 | client: newClient(acsURL, token), 64 | } 65 | } 66 | 67 | // NewClassicWithURL creates a new ClassicClient 68 | func NewClassicWithURL(acsURL, token string) Client { 69 | return &classicClient{ 70 | client: newClient(acsURL, token), 71 | } 72 | } 73 | 74 | // InstallApp installs an app on a classic stack 75 | func (c *classicClient) InstallApp(stack, token, packageFileName string, packageReader io.Reader) error { 76 | resp, err := c.resty.R().SetFormData(map[string]string{"token": token}). 77 | SetFileReader("package", packageFileName, packageReader). 78 | SetHeader("ACS-Legal-Ack", "Y"). 79 | Post("/" + stack + "/adminconfig/v2/apps") 80 | if err != nil { 81 | return fmt.Errorf("error while installing app: %s", err) 82 | } 83 | if resp.IsError() { 84 | return fmt.Errorf("error while submit: %s: %s", resp.Status(), resp.String()) 85 | } 86 | return nil 87 | } 88 | 89 | // InstallApp installs an app on a victoria stack 90 | func (c *victoriaClient) InstallApp(stack, token, packageFileName string, packageReader io.Reader) error { 91 | resp, err := c.resty.R().SetHeader("Content-Type", "application/x-www-form-urlencoded"). 92 | SetHeader("X-Splunk-Authorization", token). 93 | SetHeader("ACS-Legal-Ack", "Y"). 94 | SetBody(packageReader). 95 | Post("/" + stack + "/adminconfig/v2/apps/victoria") 96 | if err != nil { 97 | return fmt.Errorf("error while installing app: %s", err) 98 | } 99 | if resp.IsError() { 100 | return fmt.Errorf("error while submit: %s: %s", resp.Status(), resp.String()) 101 | } 102 | return nil 103 | } 104 | 105 | // App ... 106 | type App struct { 107 | Label *string `json:"label,omitempty"` 108 | Package *string `json:"package,omitempty"` 109 | Status string `json:"status"` 110 | Version *string `json:"version,omitempty"` 111 | } 112 | 113 | // ListApps on a classic stack 114 | func (c *classicClient) ListApps(stack string) ([]App, error) { 115 | return listApps(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps", stack)) 116 | } 117 | 118 | // ListApps on a victoria stack 119 | func (c *victoriaClient) ListApps(stack string) ([]App, error) { 120 | return listApps(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps/victoria", stack)) 121 | } 122 | 123 | func listApps(c client, url string) ([]App, error) { 124 | type listAppsResponse struct { 125 | Apps []App 126 | } 127 | resp, err := c.resty.R().SetResult(&listAppsResponse{}).Get(url) 128 | if err != nil { 129 | return nil, fmt.Errorf("error while listing apps: %s", err) 130 | } 131 | if resp.IsError() { 132 | return nil, fmt.Errorf("error while listing apps: %s: %s", resp.Status(), resp.String()) 133 | } 134 | apps, ok := resp.Result().(*listAppsResponse) 135 | if !ok { 136 | return nil, fmt.Errorf("error while parsing response") 137 | } 138 | return apps.Apps, nil 139 | } 140 | 141 | // DescribeApp on a classic stack 142 | func (c *classicClient) DescribeApp(stack string, appName string) (*App, error) { 143 | return describeApp(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps/%s", stack, appName)) 144 | } 145 | 146 | // DescribeApp on a classic stack 147 | func (c *victoriaClient) DescribeApp(stack string, appName string) (*App, error) { 148 | return describeApp(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps/victoria/%s", stack, appName)) 149 | } 150 | 151 | func describeApp(c client, url string) (*App, error) { 152 | resp, err := c.resty.R().SetResult(&App{}).Get(url) 153 | if err != nil { 154 | return nil, fmt.Errorf("error while describing app: %s", err) 155 | } 156 | if resp.IsError() { 157 | return nil, fmt.Errorf("error while describing apps: %s: %s", resp.Status(), resp.String()) 158 | } 159 | app, ok := resp.Result().(*App) 160 | if !ok { 161 | return nil, fmt.Errorf("error while parsing response") 162 | } 163 | return app, nil 164 | } 165 | 166 | // UninstallApp on a classic stack 167 | func (c *classicClient) UninstallApp(stack string, appName string) error { 168 | return uninstallApp(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps/%s", stack, appName)) 169 | } 170 | 171 | // UninstallApp on a victoria stack 172 | func (c *victoriaClient) UninstallApp(stack string, appName string) error { 173 | return uninstallApp(c.client, fmt.Sprintf("/%s/adminconfig/v2/apps/victoria/%s", stack, appName)) 174 | } 175 | 176 | func uninstallApp(c client, url string) error { 177 | resp, err := c.resty.R().Delete(url) 178 | if err != nil { 179 | return fmt.Errorf("error while uninstalling app: %s", err) 180 | } 181 | if resp.IsError() { 182 | return fmt.Errorf("error while uninstalling apps: %s: %s", resp.Status(), resp.String()) 183 | } 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /src/appinspect/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package appinspect 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "net/url" 21 | "strings" 22 | 23 | "github.com/go-resty/resty/v2" 24 | ) 25 | 26 | const ( 27 | appInspectBaseURL = "https://appinspect.splunk.com/v1/app" 28 | ) 29 | 30 | type ClientInterface interface { 31 | Login(username string, password string) error 32 | SetToken(token string) 33 | Submit(filename string, file io.Reader, isVictoria bool) (*SubmitResult, error) 34 | Status(statusBy interface{}) (*StatusResult, error) 35 | ReportJSON(reportBy interface{}) (*ReportJSONResult, error) 36 | ReportHTML(reportBy interface{}) ([]byte, error) 37 | } 38 | 39 | // Client to interface with the appinspect service 40 | type Client struct { 41 | *resty.Client 42 | token string 43 | } 44 | 45 | // Error ... 46 | type Error struct { 47 | Code string `json:"code"` 48 | Description string `json:"description"` 49 | } 50 | 51 | func (e *Error) Error() string { 52 | return fmt.Sprintf("%s: %s", e.Code, e.Description) 53 | } 54 | 55 | // New client to interface with the appinspect service 56 | func New() *Client { 57 | client := &Client{ 58 | Client: resty.New().SetHostURL(appInspectBaseURL).SetError(&Error{}).SetAuthScheme("Bearer"), 59 | } 60 | client.Client = client.Client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error { 61 | if client.token != "" { 62 | req.SetAuthToken(client.token) 63 | } 64 | return nil 65 | }) 66 | return client 67 | } 68 | 69 | // NewWithToken ... 70 | func NewWithToken(token string) *Client { 71 | c := New() 72 | c.token = token 73 | return c 74 | } 75 | 76 | // This is helpful for mocking appinspect client 77 | func (c *Client) SetToken(token string) { 78 | c.token = token 79 | } 80 | 81 | // AuthenticateResult ... 82 | type AuthenticateResult struct { 83 | StatusCode int `json:"status_code"` 84 | Status string `json:"status"` 85 | Msg string `json:"msg"` 86 | Data struct { 87 | Token string `json:"token"` 88 | User struct { 89 | Name string `json:"name"` 90 | Email string `json:"email"` 91 | Username string `json:"username"` 92 | Groups []string `json:"groups"` 93 | } `json:"user"` 94 | } `json:"data"` 95 | } 96 | 97 | // Authenticate ... 98 | func Authenticate(username, password string) (*AuthenticateResult, error) { 99 | type erro struct { 100 | StatusCode int `json:"status_code"` 101 | Status string `json:"status"` 102 | Msg string `json:"msg"` 103 | Errors string `json:"errors"` 104 | } 105 | resp, err := resty.New().R().SetBasicAuth(username, password).SetResult(&AuthenticateResult{}).SetError(&erro{}). 106 | Get("https://api.splunk.com/2.0/rest/login/splunk") 107 | if err != nil { 108 | return nil, fmt.Errorf("error while login: %s", err) 109 | } 110 | if resp.IsError() { 111 | if e, ok := resp.Error().(*erro); ok { 112 | return nil, fmt.Errorf("error while login: %s-%s", e.Status, e.Msg) 113 | } 114 | return nil, fmt.Errorf("error while login: %s", resp.Status()) 115 | } 116 | var r *AuthenticateResult 117 | var ok bool 118 | if r, ok = resp.Result().(*AuthenticateResult); !ok { 119 | return nil, fmt.Errorf("error while login: failed to parse response") 120 | } 121 | return r, nil 122 | } 123 | 124 | // Login to appinspect service 125 | func (c *Client) Login(username, password string) error { 126 | r, err := Authenticate(username, password) 127 | if err != nil { 128 | return err 129 | } 130 | c.token = r.Data.Token 131 | return nil 132 | } 133 | 134 | // SubmitResult ... 135 | type SubmitResult struct { 136 | RequestID string `json:"request_id"` 137 | Message string `json:"message"` 138 | Links []struct { 139 | Rel string `json:"rel"` 140 | Href string `json:"href"` 141 | } `json:"links"` 142 | } 143 | 144 | // Submit an app-package for inspection 145 | func (c *Client) Submit(filename string, file io.Reader, isVictoria bool) (*SubmitResult, error) { 146 | 147 | /* 148 | This app inspect client uses tag "private_app" which leverages APAV and skips manual checks. 149 | Note: Victoria stacks must be on butterfinger (8.2.2112) onwards to use this client. For Victoria stacks 150 | pre-butterfinger, app inspect included_tags is "cloud,self-service". No restriction for classic expreince, 151 | i.e included_tags is always "private_app" regardless of stack version. 152 | */ 153 | var includedTags []string 154 | if isVictoria { 155 | includedTags = []string{"private_victoria"} 156 | } else { 157 | includedTags = []string{"private_classic"} 158 | } 159 | 160 | var formdata = url.Values{ 161 | "included_tags": includedTags, 162 | } 163 | 164 | resp, err := c.R().SetAuthToken(c.token).SetFormDataFromValues(formdata). 165 | SetFileReader("app_package", filename, file).SetResult(&SubmitResult{}).Post("/validate") 166 | if err != nil { 167 | return nil, fmt.Errorf("error while submit: %s", err) 168 | } 169 | if resp.IsError() { 170 | if e, ok := resp.Error().(*Error); ok { 171 | return nil, fmt.Errorf("error while submit: %s", e) 172 | } 173 | return nil, fmt.Errorf("error while submit: %s", resp.Status()) 174 | } 175 | var r *SubmitResult 176 | var ok bool 177 | if r, ok = resp.Result().(*SubmitResult); !ok { 178 | return nil, fmt.Errorf("error while submit: failed to parse response") 179 | } 180 | return r, nil 181 | } 182 | 183 | type ShaId struct { 184 | Sha string 185 | IncludeTags []string 186 | } 187 | 188 | type addIdResult struct { 189 | ShaId 190 | RequestID string 191 | } 192 | 193 | // StatusResult ... 194 | type StatusResult struct { 195 | RequestID string `json:"request_id"` 196 | Sha string `json:"sha"` 197 | StatusCode int `json:"status_code"` 198 | Status string `json:"status"` 199 | Info struct { 200 | Error int `json:"error"` 201 | Failure int `json:"failure"` 202 | Skipped int `json:"skipped"` 203 | ManualCheck int `json:"manual_check"` 204 | NotApplicable int `json:"not_applicable"` 205 | Warning int `json:"warning"` 206 | Success int `json:"success"` 207 | } `json:"info"` 208 | Links []struct { 209 | Rel string `json:"rel"` 210 | Href string `json:"href"` 211 | } `json:"links"` 212 | } 213 | 214 | // Status of an app-package inspection 215 | func (c *Client) Status(statusBy interface{}) (*StatusResult, error) { 216 | request := c.R().SetAuthToken(c.token).SetResult(&StatusResult{}) 217 | request.Method = resty.MethodGet 218 | request.URL = "/validate/status/" 219 | 220 | idResult, err := addIdToRequest(request, statusBy) 221 | if err != nil { 222 | return nil, err 223 | } 224 | resp, err := request.Send() 225 | if err != nil { 226 | return nil, fmt.Errorf("error while getting status: %s", err) 227 | } 228 | if resp.IsError() { 229 | r := &StatusResult{ 230 | RequestID: idResult.RequestID, 231 | Sha: idResult.Sha, 232 | StatusCode: resp.StatusCode(), 233 | } 234 | if e, ok := resp.Error().(*Error); ok { 235 | return r, fmt.Errorf("error while getting status: %s", e) 236 | } 237 | return r, fmt.Errorf("error while getting status: %s", resp.Status()) 238 | } 239 | var r *StatusResult 240 | var ok bool 241 | if r, ok = resp.Result().(*StatusResult); !ok { 242 | return nil, fmt.Errorf("error while getting status: failed to parse response") 243 | } 244 | r.StatusCode = resp.StatusCode() 245 | return r, nil 246 | } 247 | 248 | // ReportJSONResult ... 249 | type ReportJSONResult struct { 250 | RequestID string `json:"request_id"` 251 | Cloc string `json:"cloc"` 252 | Reports []struct { 253 | AppAuthor string `json:"app_author"` 254 | AppDescription string `json:"app_description"` 255 | AppHash string `json:"app_hash"` 256 | AppName string `json:"app_name"` 257 | AppVersion string `json:"app_version"` 258 | Metrics struct { 259 | StartTime string `json:"start_time"` 260 | EndTime string `json:"end_time"` 261 | ExecutionTime float64 `json:"execution_time"` 262 | } `json:"metrics"` 263 | RunParameters struct { 264 | APIRequestID string `json:"api_request_id"` 265 | Identity string `json:"identity"` 266 | SplunkbaseID string `json:"splunkbase_id"` 267 | Version string `json:"version"` 268 | SplunkVersion string `json:"splunk_version"` 269 | StackID string `json:"stack_id"` 270 | APITimestamp string `json:"api_timestamp"` 271 | PackageLocation string `json:"package_location"` 272 | AppinspectVersion string `json:"appinspect_version"` 273 | IncludedTags []string `json:"included_tags"` 274 | ExcludedTags []string `json:"excluded_tags"` 275 | } `json:"run_parameters"` 276 | Groups []struct { 277 | Checks []struct { 278 | Description string `json:"description"` 279 | Messages []struct { 280 | Code string `json:"code"` 281 | Filename string `json:"filename"` 282 | Line int `json:"line"` 283 | Message string `json:"message"` 284 | Result string `json:"result"` 285 | MessageFilename string `json:"message_filename"` 286 | MessageLine interface{} `json:"message_line"` 287 | } `json:"messages"` 288 | Name string `json:"name"` 289 | Tags []string `json:"tags"` 290 | Result string `json:"result"` 291 | } `json:"checks"` 292 | Description string `json:"description"` 293 | Name string `json:"name"` 294 | } `json:"groups"` 295 | Summary struct { 296 | Error int `json:"error"` 297 | Failure int `json:"failure"` 298 | Skipped int `json:"skipped"` 299 | ManualCheck int `json:"manual_check"` 300 | NotApplicable int `json:"not_applicable"` 301 | Warning int `json:"warning"` 302 | Success int `json:"success"` 303 | } `json:"summary"` 304 | } `json:"reports"` 305 | Summary struct { 306 | Error int `json:"error"` 307 | Failure int `json:"failure"` 308 | Skipped int `json:"skipped"` 309 | ManualCheck int `json:"manual_check"` 310 | NotApplicable int `json:"not_applicable"` 311 | Warning int `json:"warning"` 312 | Success int `json:"success"` 313 | } `json:"summary"` 314 | Metrics struct { 315 | StartTime string `json:"start_time"` 316 | EndTime string `json:"end_time"` 317 | ExecutionTime float64 `json:"execution_time"` 318 | } `json:"metrics"` 319 | RunParameters struct { 320 | APIRequestID string `json:"api_request_id"` 321 | Identity string `json:"identity"` 322 | SplunkbaseID string `json:"splunkbase_id"` 323 | Version string `json:"version"` 324 | SplunkVersion string `json:"splunk_version"` 325 | StackID string `json:"stack_id"` 326 | APITimestamp string `json:"api_timestamp"` 327 | PackageLocation string `json:"package_location"` 328 | AppinspectVersion string `json:"appinspect_version"` 329 | IncludedTags []string `json:"included_tags"` 330 | ExcludedTags []string `json:"excluded_tags"` 331 | } `json:"run_parameters"` 332 | Links []struct { 333 | Rel string `json:"rel"` 334 | Href string `json:"href"` 335 | } `json:"links"` 336 | } 337 | 338 | // ReportJSON of an app-package inspection 339 | func (c *Client) ReportJSON(reportBy interface{}) (*ReportJSONResult, error) { 340 | resp, err := c.report(reportBy, "application/json") 341 | if err != nil { 342 | return nil, err 343 | } 344 | var r *ReportJSONResult 345 | var ok bool 346 | if r, ok = resp.Result().(*ReportJSONResult); !ok { 347 | return nil, fmt.Errorf("error while getting json report: failed to parse response") 348 | } 349 | return r, nil 350 | } 351 | 352 | // ReportHTML of an app-package inspection 353 | func (c *Client) ReportHTML(reportBy interface{}) ([]byte, error) { 354 | resp, err := c.report(reportBy, "text/html") 355 | if err != nil { 356 | return nil, err 357 | } 358 | return resp.Body(), nil 359 | } 360 | 361 | func (c *Client) report(reportBy interface{}, contentType string) (*resty.Response, error) { 362 | request := c.R().SetAuthToken(c.token).SetResult(&ReportJSONResult{}) 363 | request.Method = resty.MethodGet 364 | request.URL = "/report/" 365 | request.SetHeader("Content-Type", contentType) 366 | 367 | if _, err := addIdToRequest(request, reportBy); err != nil { 368 | return nil, err 369 | } 370 | 371 | resp, err := request.Send() 372 | if err != nil { 373 | return nil, fmt.Errorf("error while getting json report: %s", err) 374 | } 375 | if resp.IsError() { 376 | if e, ok := resp.Error().(*Error); ok { 377 | return nil, fmt.Errorf("error while getting json report: %s", e) 378 | } 379 | return nil, fmt.Errorf("error while getting json report: %s", resp.Status()) 380 | } 381 | return resp, nil 382 | } 383 | 384 | func addIdToRequest(r *resty.Request, id interface{}) (*addIdResult, error) { 385 | result := new(addIdResult) 386 | switch value := id.(type) { 387 | case string: 388 | result.RequestID = value 389 | r.URL += value 390 | case ShaId: 391 | result.Sha = value.Sha 392 | result.IncludeTags = value.IncludeTags 393 | r.URL += value.Sha 394 | if value.IncludeTags != nil { 395 | r.SetQueryParam("included_tags", strings.Join(value.IncludeTags, ",")) 396 | } 397 | default: 398 | return nil, fmt.Errorf("can't add id to request: unknown type %q", id) 399 | } 400 | return result, nil 401 | } 402 | -------------------------------------------------------------------------------- /src/appinspect/client_test.go: -------------------------------------------------------------------------------- 1 | package appinspect 2 | 3 | import ( 4 | "github.com/jarcoal/httpmock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | const TESTING_URL = "http://foo-bar" 10 | 11 | func getClient() *Client { 12 | client := New() 13 | client.Client.SetHostURL(TESTING_URL) 14 | httpmock.ActivateNonDefault(client.GetClient()) 15 | return client 16 | } 17 | 18 | func TestClientImplementsInterface(t *testing.T) { 19 | var _ ClientInterface = (*Client)(nil) 20 | } 21 | 22 | func TestStatusById(t *testing.T) { 23 | assert := assert.New(t) 24 | requestId := "Foo" 25 | client := getClient() 26 | 27 | responder, _ := httpmock.NewJsonResponder(200, StatusResult{ 28 | RequestID: requestId, 29 | }) 30 | httpmock.RegisterResponder("GET", TESTING_URL+"/validate/status/"+requestId, responder) 31 | status, _ := client.Status(requestId) 32 | assert.Equal(status.RequestID, requestId) 33 | 34 | responder, _ = httpmock.NewJsonResponder(401, nil) 35 | httpmock.RegisterResponder("GET", TESTING_URL+"/validate/status/"+requestId, responder) 36 | status, err := client.Status(requestId) 37 | assert.Error(err) 38 | assert.Equal(status.RequestID, requestId) 39 | assert.Equal(status.StatusCode, 401) 40 | } 41 | 42 | func TestStatusByHash(t *testing.T) { 43 | assert := assert.New(t) 44 | requestId := "Foo" 45 | client := getClient() 46 | 47 | responder, _ := httpmock.NewJsonResponder(200, StatusResult{ 48 | RequestID: requestId, 49 | }) 50 | httpmock.RegisterResponder("GET", TESTING_URL+"/validate/status/Baz?included_tags=test-tag", responder) 51 | status, _ := client.Status(ShaId{"Baz", []string{"test-tag"}}) 52 | assert.Equal(status.RequestID, requestId) 53 | 54 | responder, _ = httpmock.NewJsonResponder(402, nil) 55 | httpmock.RegisterResponder("GET", TESTING_URL+"/validate/status/"+requestId, responder) 56 | status, err := client.Status(requestId) 57 | assert.Error(err) 58 | assert.Equal(status.RequestID, requestId) 59 | assert.Equal(status.StatusCode, 402) 60 | } 61 | 62 | func TestReportJSON(t *testing.T) { 63 | assert := assert.New(t) 64 | requestId := "Bar" 65 | client := getClient() 66 | 67 | responder, _ := httpmock.NewJsonResponder(200, ReportJSONResult{ 68 | RequestID: requestId, 69 | }) 70 | httpmock.RegisterResponder("GET", TESTING_URL+"/report/"+requestId, responder) 71 | report, _ := client.ReportJSON(requestId) 72 | assert.Equal(report.RequestID, requestId) 73 | 74 | responder, _ = httpmock.NewJsonResponder(401, nil) 75 | httpmock.RegisterResponder("GET", TESTING_URL+"/report/"+requestId, responder) 76 | report, err := client.ReportJSON(requestId) 77 | assert.Error(err) 78 | assert.Nil(report) 79 | } 80 | 81 | func TestReportHTML(t *testing.T) { 82 | assert := assert.New(t) 83 | requestId := "Baz" 84 | client := getClient() 85 | 86 | response := []byte("Hello test") 87 | httpmock.RegisterResponder("GET", TESTING_URL+"/report/"+requestId, httpmock.NewBytesResponder( 88 | 200, response)) 89 | report, _ := client.ReportHTML(requestId) 90 | assert.Equal(report, response) 91 | 92 | httpmock.RegisterResponder("GET", TESTING_URL+"/report/"+requestId, httpmock.NewBytesResponder( 93 | 402, nil)) 94 | report, err := client.ReportHTML(requestId) 95 | assert.Error(err) 96 | assert.Nil(report) 97 | } 98 | 99 | func TestAddIdToRequest(t *testing.T) { 100 | assert := assert.New(t) 101 | request := getClient().Client.R() 102 | 103 | request.URL = "/foo/" 104 | result, err := addIdToRequest(request, "bar") 105 | assert.Nil(err) 106 | assert.Empty(result.Sha) 107 | assert.Equal("bar", result.RequestID) 108 | assert.Equal("/foo/bar", request.URL) 109 | 110 | request.URL = "/foo/" 111 | result, err = addIdToRequest(request, ShaId{"baz", []string{"test-tag"}}) 112 | assert.Nil(err) 113 | assert.Empty(result.RequestID) 114 | assert.Equal("baz", result.Sha) 115 | assert.Equal("/foo/baz", request.URL) 116 | 117 | _, err = addIdToRequest(request, 1) 118 | assert.Error(err) 119 | } 120 | -------------------------------------------------------------------------------- /src/cmd/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/AlecAivazis/survey/v2" 22 | "github.com/splunk/acs-privateapps-demo/src/acs" 23 | ) 24 | 25 | type get struct { 26 | StackName string `kong:"arg,help='the splunk cloud stack'"` 27 | AppName string `kong:"arg,optional,help='the app'"` 28 | StackToken string `kong:"env='STACK_TOKEN',help='the stack sc_admin jwt token'"` 29 | AcsURL string `kong:"env='ACS_URL',help='the acs url',default='https://admin.splunk.com'"` 30 | Victoria bool `kong:"help='whether the stack is a Victora stack'"` 31 | } 32 | 33 | func (g *get) Run(c *context) error { 34 | 35 | if g.StackToken == "" { 36 | survey.AskOne(&survey.Password{ 37 | Message: "stack token:", 38 | }, &g.StackToken) 39 | fmt.Println("") 40 | } 41 | 42 | var cli acs.Client 43 | if g.Victoria { 44 | cli = acs.NewVictoriaWithURL(g.AcsURL, g.StackToken) 45 | } else { 46 | cli = acs.NewClassicWithURL(g.AcsURL, g.StackToken) 47 | } 48 | 49 | var object interface{} 50 | var err error 51 | if g.AppName == "" { 52 | object, err = cli.ListApps(g.StackName) 53 | if err != nil { 54 | return err 55 | } 56 | } else { 57 | object, err = cli.DescribeApp(g.StackName, g.AppName) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | if data, e := json.MarshalIndent(object, "", " "); e == nil { 64 | fmt.Printf("%s\n", string(data)) 65 | } else { 66 | fmt.Printf("%v\n", data) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /src/cmd/install.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "github.com/AlecAivazis/survey/v2" 21 | "github.com/splunk/acs-privateapps-demo/src/acs" 22 | "github.com/splunk/acs-privateapps-demo/src/appinspect" 23 | "io/ioutil" 24 | "path/filepath" 25 | ) 26 | 27 | type install struct { 28 | StackName string `kong:"arg,help='the splunk cloud stack'"` 29 | SplunkComUsername string `kong:"env='SPLUNK_COM_USERNAME',help='the splunkbase username'"` 30 | SplunkComPassword string `kong:"env='SPLUNK_COM_PASSWORD',help='the splunkbase password'"` 31 | PackageFilePath string `kong:"arg,help='the path to the app-package (tar.gz) file',type='path'"` 32 | StackToken string `kong:"env='STACK_TOKEN',help='the stack sc_admin jwt token'"` 33 | AcsURL string `kong:"env='ACS_URL',help='the acs url',default='https://admin.splunk.com'"` 34 | Victoria bool `kong:"help='whether the stack is a Victora stack'"` 35 | } 36 | 37 | func (i *install) Run(c *context) error { 38 | 39 | pf, err := ioutil.ReadFile(i.PackageFilePath) 40 | if err != nil { 41 | return err 42 | } 43 | if i.SplunkComUsername == "" { 44 | survey.AskOne(&survey.Input{ 45 | Message: "splunkbase username:", 46 | }, &i.SplunkComUsername) 47 | } 48 | if i.SplunkComPassword == "" { 49 | survey.AskOne(&survey.Password{ 50 | Message: "splunkbase password:", 51 | }, &i.SplunkComPassword) 52 | fmt.Println("") 53 | } 54 | 55 | if i.StackToken == "" { 56 | survey.AskOne(&survey.Password{ 57 | Message: "stack token:", 58 | }, &i.StackToken) 59 | fmt.Println("") 60 | } 61 | ar, err := appinspect.Authenticate(i.SplunkComUsername, i.SplunkComPassword) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var cli acs.Client 67 | if i.Victoria { 68 | cli = acs.NewVictoriaWithURL(i.AcsURL, i.StackToken) 69 | } else { 70 | cli = acs.NewClassicWithURL(i.AcsURL, i.StackToken) 71 | } 72 | return cli.InstallApp(i.StackName, ar.Data.Token, filepath.Base(i.PackageFilePath), bytes.NewReader(pf)) 73 | } 74 | 75 | type uninstall struct { 76 | StackName string `kong:"arg,help='the splunk cloud stack'"` 77 | AppName string `kong:"arg,optional,help='the app'"` 78 | StackToken string `kong:"env='STACK_TOKEN',help='the stack sc_admin jwt token'"` 79 | AcsURL string `kong:"env='ACS_URL',help='the acs url',default='https://admin.splunk.com'"` 80 | Victoria bool `kong:"help='whether the stack is a Victora stack'"` 81 | } 82 | 83 | func (u *uninstall) Run(c *context) error { 84 | 85 | if u.StackToken == "" { 86 | survey.AskOne(&survey.Password{ 87 | Message: "stack token:", 88 | }, &u.StackToken) 89 | fmt.Println("") 90 | } 91 | 92 | var cli acs.Client 93 | if u.Victoria { 94 | cli = acs.NewVictoriaWithURL(u.AcsURL, u.StackToken) 95 | } else { 96 | cli = acs.NewClassicWithURL(u.AcsURL, u.StackToken) 97 | } 98 | return cli.UninstallApp(u.StackName, u.AppName) 99 | } 100 | -------------------------------------------------------------------------------- /src/cmd/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "github.com/AlecAivazis/survey/v2" 20 | "github.com/splunk/acs-privateapps-demo/src/appinspect" 21 | ) 22 | 23 | type login struct { 24 | SplunkComUsername string `kong:"env='SPLUNK_COM_USERNAME',help='the splunkbase username'"` 25 | SplunkComPassword string `kong:"env='SPLUNK_COM_PASSWORD',help='the splunkbase password'"` 26 | } 27 | 28 | func (v *login) Run(c *context) error { 29 | 30 | if v.SplunkComUsername == "" { 31 | survey.AskOne(&survey.Input{ 32 | Message: "splunkbase username:", 33 | }, &v.SplunkComUsername) 34 | } 35 | if v.SplunkComPassword == "" { 36 | survey.AskOne(&survey.Password{ 37 | Message: "splunkbase password:", 38 | }, &v.SplunkComPassword) 39 | fmt.Println("") 40 | } 41 | res, err := appinspect.Authenticate(v.SplunkComUsername, v.SplunkComPassword) 42 | if err != nil { 43 | return err 44 | } 45 | fmt.Printf("Token: %s\n", res.Data.Token) 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /src/cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/alecthomas/kong" 19 | ) 20 | 21 | type context struct { 22 | Debug bool 23 | } 24 | 25 | var cli struct { 26 | Debug bool `kong:"help='enable debug mode'"` 27 | Login login `kong:"cmd,help='login to splunkbase and generate token'"` 28 | Vet vet `kong:"cmd,help='vet the app package against the app-inspect service'"` 29 | Install install `kong:"cmd,help=install the app package on the splunk stack"` 30 | Uninstall uninstall `kong:"cmd,help=uninstall the app package from the splunk stack"` 31 | Get get `kong:"cmd,help=get an app/apps installed on the splunk stack"` 32 | } 33 | 34 | func main() { 35 | ctx := kong.Parse(&cli) 36 | // Call the Run() method of the selected parsed command. 37 | err := ctx.Run(&context{Debug: cli.Debug}) 38 | ctx.FatalIfErrorf(err) 39 | } 40 | -------------------------------------------------------------------------------- /src/cmd/vet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "github.com/AlecAivazis/survey/v2" 22 | "github.com/splunk/acs-privateapps-demo/src/appinspect" 23 | "io/ioutil" 24 | "path/filepath" 25 | "time" 26 | ) 27 | 28 | type vet struct { 29 | PackageFilePath string `kong:"arg,help='the path to the app-package (tar.gz) file',type='path'"` 30 | SplunkComUsername string `kong:"env='SPLUNK_COM_USERNAME',help='the splunkbase username'"` 31 | SplunkComPassword string `kong:"env='SPLUNK_COM_PASSWORD',help='the splunkbase password'"` 32 | JSONReportFile string `kong:"help='the file to write the inspection report in json format',type='path'"` 33 | Victoria bool `kong:"help='whether the stack is a Victora stack'"` 34 | } 35 | 36 | func (v *vet) Run(c *context) error { 37 | 38 | pf, err := ioutil.ReadFile(v.PackageFilePath) 39 | if err != nil { 40 | return err 41 | } 42 | if v.SplunkComUsername == "" { 43 | survey.AskOne(&survey.Input{ 44 | Message: "splunkbase username:", 45 | }, &v.SplunkComUsername) 46 | } 47 | if v.SplunkComPassword == "" { 48 | survey.AskOne(&survey.Password{ 49 | Message: "splunkbase password:", 50 | }, &v.SplunkComPassword) 51 | fmt.Println("") 52 | } 53 | cli := appinspect.New() 54 | err = cli.Login(v.SplunkComUsername, v.SplunkComPassword) 55 | if err != nil { 56 | return err 57 | } 58 | submitRes, err := cli.Submit(filepath.Base(v.PackageFilePath), bytes.NewReader(pf), v.Victoria) 59 | if err != nil { 60 | return err 61 | } 62 | fmt.Printf("submitted app for inspection (requestId='%s')\n", submitRes.RequestID) 63 | 64 | status, err := cli.Status(submitRes.RequestID) 65 | if err != nil { 66 | return err 67 | } 68 | if status.Status == "PROCESSING" || status.Status == "PREPARING" || status.Status == "PENDING" { 69 | fmt.Printf("waiting for inspection to finish...\n") 70 | for { 71 | time.Sleep(2 * time.Second) 72 | status, err = cli.Status(submitRes.RequestID) 73 | if err != nil { 74 | return err 75 | } 76 | if status.Status != "PROCESSING" { 77 | break 78 | } 79 | } 80 | } 81 | if status.Status == "SUCCESS" { 82 | data, _ := json.MarshalIndent(status.Info, "", " ") 83 | fmt.Printf("vetting completed, summary: \n%s\n", string(data)) 84 | if status.Info.Failure > 0 || status.Info.Error > 0 { 85 | err = fmt.Errorf("vetting failed (failures=%d, errors=%d)", status.Info.Failure, status.Info.Error) 86 | } 87 | } else { 88 | err = fmt.Errorf("vetting failed to complete (status='%s')", status.Status) 89 | } 90 | 91 | if v.JSONReportFile != "" { 92 | report, e := cli.ReportJSON(submitRes.RequestID) 93 | if e != nil { 94 | fmt.Printf("failed to pull report: %s\n", e) 95 | } 96 | data, _ := json.MarshalIndent(report, "", " ") 97 | e = ioutil.WriteFile(v.JSONReportFile, data, 0644) 98 | if e != nil { 99 | fmt.Printf("failed to write report: %s\n", e) 100 | } 101 | } 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /testapp/README: -------------------------------------------------------------------------------- 1 | This is where you put any scripts you want to add to this app. 2 | -------------------------------------------------------------------------------- /testapp/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = Private App 1 11 | 12 | [package] 13 | id = testapp 14 | 15 | [launcher] 16 | author = Admin Config Service 17 | description = 18 | version = 1.0.0 --------------------------------------------------------------------------------