├── .github ├── renovate.json └── workflows │ ├── release.yml │ └── renovate-vault.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── nv_dashboard.json ├── nv_exporter.py ├── nv_exporter.yml ├── nv_exporter_secret.yaml ├── nv_grafana.png ├── package └── Dockerfile ├── prom-config.yml ├── prometheus.yml └── startup.sh /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>rancher/renovate-config#release" 4 | ], 5 | "baseBranches": [ 6 | "master" 7 | ], 8 | "prHourlyLimit": 2 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | 10 | publish: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | # write is needed for: 15 | # - OIDC for cosign's use in ecm-distro-tools/publish-image. 16 | # - Read vault secrets in rancher-eio/read-vault-secrets. 17 | id-token: write 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - name: Load Secrets from Vault 24 | uses: rancher-eio/read-vault-secrets@main 25 | with: 26 | secrets: | 27 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | RANCHER_DOCKER_USERNAME ; 28 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | RANCHER_DOCKER_PASSWORD ; 29 | secret/data/github/repo/${{ github.repository }}/dockerhub/neuvector/credentials username | DOCKER_USERNAME ; 30 | secret/data/github/repo/${{ github.repository }}/dockerhub/neuvector/credentials password | DOCKER_PASSWORD ; 31 | secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials registry | PRIME_REGISTRY ; 32 | secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials username | PRIME_REGISTRY_USERNAME ; 33 | secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials password | PRIME_REGISTRY_PASSWORD 34 | - name: Parse target tag 35 | run: | 36 | TARGET=${{ github.ref_name }} 37 | echo "TAG=${TARGET#v}" >> $GITHUB_ENV 38 | - name: Publish neuvector manifest 39 | uses: rancher/ecm-distro-tools/actions/publish-image@master 40 | with: 41 | push-to-public: true 42 | push-to-prime: false 43 | image: prometheus-exporter 44 | tag: ${{ env.TAG }} 45 | platforms: linux/amd64,linux/arm64 46 | 47 | public-registry: docker.io 48 | public-repo: neuvector 49 | public-username: ${{ env.DOCKER_USERNAME }} 50 | public-password: ${{ env.DOCKER_PASSWORD }} 51 | - name: Publish rancher manifest 52 | uses: rancher/ecm-distro-tools/actions/publish-image@master 53 | env: 54 | IMAGE_PREFIX: neuvector- 55 | with: 56 | image: neuvector-prometheus-exporter 57 | tag: ${{ env.TAG }} 58 | platforms: linux/amd64,linux/arm64 59 | 60 | public-registry: docker.io 61 | public-repo: rancher 62 | public-username: ${{ env.RANCHER_DOCKER_USERNAME }} 63 | public-password: ${{ env.RANCHER_DOCKER_PASSWORD }} 64 | 65 | prime-registry: ${{ env.PRIME_REGISTRY }} 66 | prime-repo: rancher 67 | prime-username: ${{ env.PRIME_REGISTRY_USERNAME }} 68 | prime-password: ${{ env.PRIME_REGISTRY_PASSWORD }} 69 | -------------------------------------------------------------------------------- /.github/workflows/renovate-vault.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: "Override default log level" 7 | required: false 8 | default: "info" 9 | type: string 10 | overrideSchedule: 11 | description: "Override all schedules" 12 | required: false 13 | default: "false" 14 | type: string 15 | # Run twice in the early morning (UTC) for initial and follow up steps (create pull request and merge) 16 | schedule: 17 | - cron: '30 4,6 * * *' 18 | 19 | permissions: 20 | contents: read 21 | id-token: write 22 | 23 | jobs: 24 | call-workflow: 25 | uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release 26 | with: 27 | logLevel: ${{ inputs.logLevel || 'info' }} 28 | overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }} 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vi temporary files 2 | .*.swp 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RUNNER := docker 2 | IMAGE_BUILDER := $(RUNNER) buildx 3 | MACHINE := neuvector 4 | BUILDX_ARGS ?= --sbom=true --attest type=provenance,mode=max 5 | DEFAULT_PLATFORMS := linux/amd64,linux/arm64,linux/x390s,linux/riscv64 6 | 7 | COMMIT = $(shell git rev-parse --short HEAD) 8 | ifeq ($(VERSION),) 9 | # Define VERSION, which is used for image tags or to bake it into the 10 | # compiled binary to enable the printing of the application version, 11 | # via the --version flag. 12 | CHANGES = $(shell git status --porcelain --untracked-files=no) 13 | ifneq ($(CHANGES),) 14 | DIRTY = -dirty 15 | endif 16 | 17 | 18 | COMMIT = $(shell git rev-parse --short HEAD) 19 | VERSION = $(COMMIT)$(DIRTY) 20 | 21 | # Override VERSION with the Git tag if the current HEAD has a tag pointing to 22 | # it AND the worktree isn't dirty. 23 | GIT_TAG = $(shell git tag -l --contains HEAD | head -n 1) 24 | ifneq ($(GIT_TAG),) 25 | ifeq ($(DIRTY),) 26 | VERSION = $(GIT_TAG) 27 | endif 28 | endif 29 | endif 30 | 31 | ifeq ($(TAG),) 32 | TAG = $(VERSION) 33 | ifneq ($(DIRTY),) 34 | TAG = dev 35 | endif 36 | endif 37 | 38 | TARGET_PLATFORMS ?= linux/amd64,linux/arm64 39 | STAGE_DIR=stage 40 | REPO ?= neuvector 41 | IMAGE = $(REPO)/prometheus-exporter:$(TAG) 42 | BUILD_ACTION = --load 43 | 44 | .PHONY: all build test copy_adpt 45 | 46 | buildx-machine: 47 | docker buildx ls 48 | @docker buildx ls | grep $(MACHINE) || \ 49 | docker buildx create --name=$(MACHINE) --platform=$(DEFAULT_PLATFORMS) 50 | 51 | test-image: 52 | # Instead of loading image, target all platforms, effectivelly testing 53 | # the build for the target architectures. 54 | $(MAKE) build-image BUILD_ACTION="--platform=$(TARGET_PLATFORMS)" 55 | 56 | build-image: buildx-machine ## build (and load) the container image targeting the current platform. 57 | $(IMAGE_BUILDER) build -f package/Dockerfile \ 58 | --builder $(MACHINE) $(IMAGE_ARGS) \ 59 | --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) -t "$(IMAGE)" $(BUILD_ACTION) . 60 | @echo "Built $(IMAGE)" 61 | 62 | 63 | push-image: buildx-machine 64 | $(IMAGE_BUILDER) build -f package/Dockerfile \ 65 | --builder $(MACHINE) $(IMAGE_ARGS) $(IID_FILE_FLAG) $(BUILDX_ARGS) \ 66 | --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) --platform=$(TARGET_PLATFORMS) -t "$(REPO)/$(IMAGE_PREFIX)prometheus-exporter:$(TAG)" --push . 67 | @echo "Pushed $(REPO)/$(IMAGE_PREFIX)prometheus-exporter:$(TAG)" 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus exporter and Grafana template 2 | 3 | ![](nv_grafana.png) 4 | 5 | ### NV_Exporter Setup: 6 | 7 | #### To run the exporter as Python program 8 | - Clone the repository 9 | - Make sure you installed Python 3 and python3-pip: 10 | ``` 11 | $ sudo apt-get install python3 12 | $ sudo apt-get install python3-pip 13 | ``` 14 | - Install the Prometheus Python client: 15 | ``` 16 | $ sudo pip3 install -U setuptools 17 | $ sudo pip3 install -U pip 18 | $ sudo pip3 install prometheus_client requests 19 | ``` 20 | 21 | #### To run the exporter and prometheus as a container 22 | It's easier to start NeuVector exporter as a container. The following section describe how to start the exporter in the Docker environment. A kubernetes sample yaml file, nv_exporter.yml, is also included. 23 | 24 | Modify both docker-compose.yml and nv_exporter.yml. Specify NeuVector controller's RESTful API endpoint `CTRL_API_SERVICE`, login username `CTRL_USERNAME`, password `CTRL_PASSWORD`, and the port that the export listens on through environment variables `EXPORTER_PORT`. Optionally, you can also specify `EXPORTER_METRICS` to a comma-separated list of metric groups to collect and export. **It's highly recommanded to create a read-only user account for the exporter.** 25 | 26 | Metric groups: 27 | - `summary` - overall NeuVector status 28 | - `conversation` - total bytes for every conversation between workloads 29 | - `enforcer` - enforcer CPU and memory usage 30 | - `host` - host memory usage 31 | - `admission` - number of allowed and denied Kubernetes admission requests 32 | - `image_vulnerability` - number of high and medium vulnerabilities for every scanned registry image 33 | - `container_vulnerability` - number of high and medium vulnerabilities for every service, reporting a single pod's status per service (excluding service mesh sidecars) 34 | - `log` - data for the latest threat, incident, and violation logs (latest 5 logs each) 35 | 36 | 37 | ##### Environment Variables 38 | 39 | Variable | Description | Default 40 | -------- | ----------- | ------- 41 | `CTRL_API_SERVICE` | NeuVector controller REST API service endpoint | `nil` 42 | `CTRL_USERNAME` | Username to login to controller REST API service | `admin` 43 | `CTRL_PASSWORD` | Password to login to controller REST API service | `admin` 44 | `EXPORTER_PORT` | The port that the export is listening on | `nil` 45 | `ENFORCER_STATS` | For the performance reason, by default the exporter does NOT pull CPU/memory usage from enforcers. Enable this if you want to see the metrix in the dashboard | `0` 46 | 47 | ##### In native docker environment 48 | 49 | Start NeuVector exporter container. 50 | ``` 51 | $ docker-compose up -d 52 | ``` 53 | - Open browser, go to: [exporter_host:exporter_port] (example: localbost:8068) 54 | - If you can load the metric page, the exporter is working fine. 55 | 56 | 57 | Add and modify the exporter target in your prometheus.yml file under `scrape_configs`: 58 | ``` 59 | scrape_configs: 60 | - job_name: prometheus 61 | scrape_interval: 10s 62 | static_configs: 63 | - targets: ["localhost:9090"] 64 | - job_name: nv-exporter 65 | scrape_interval: 30s 66 | static_configs: 67 | - targets: ["neuvector-svc-prometheus-exporter.neuvector:8068"] 68 | ``` 69 | 70 | Start Prometheus container. 71 | ``` 72 | $ docker run -itd -p 9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml --name prometheus prom/prometheus 73 | ``` 74 | - After deployed Prometheus, open browser and go to: [prometheus_host:9090] (example: localhost:9090) 75 | - On the top bar go to `Status -> Targets` to check exporter status. If the name is blue and `State` is UP, the exporter is running and Prometheus is successfully connected to the exporter. 76 | - On the top bar go to `Graph` and in the `Expression` box type `nv` to view all the metrics the exporter has. 77 | 78 | ##### In Kubernetes 79 | 80 | Start NeuVector exporter pod and service. 81 | ``` 82 | $ kubectl create -f nv_exporter.yml 83 | ``` 84 | 85 | Create configMap for Prometheus scrape_configs. 86 | ``` 87 | $ kubectl create cm prometheus-cm --from-file prom-config.yml 88 | ``` 89 | 90 | Start Prometheus pod and service. 91 | ``` 92 | $ kubectl create -f prometheus.yml 93 | ``` 94 | 95 | 96 | ### Grafana Setup: 97 | - Start Grafana container. "docker run" example, 98 | ``` 99 | $ sudo docker run -d -p 3000:3000 --name grafana grafana/grafana 100 | ``` 101 | - After deployed Grafana, open browser and go to: [grafana_host:3000] (example: localhost:3000) 102 | - Login and add Prometheus data source from Configurations -> Data Sources 103 | - find the `+` on the left bar, select `Import`. Upload NeuVector dashboard templet JSON file. 104 | 105 | 106 | ### Metrics 107 | | Metrics | Comment | 108 | | ------- | ---- | 109 | | nv_summary_services | Number of services | 110 | | nv_summary_policy | Number of network policies | 111 | | nv_summary_pods | Number of pods | 112 | | nv_summary_runningWorkloads | Number of running containers | 113 | | nv_summary_totalWorkloads | Total number of containers | 114 | | nv_summary_hosts | Number of hosts | 115 | | nv_summary_controllers | Number of controllers | 116 | | nv_summary_enforcers | Number of enforcers | 117 | | nv_summary_disconnectedEnforcers | Number of disconnected enforcers | 118 | | nv_summary_cvedbTime | Vulnerability database build time | 119 | | nv_summary_cvedbVersion | Vulnerability database version | 120 | | nv_host_memory | Memory usage of nodes (by node id) | 121 | | nv_controller_cpu | CPU usage of controllers (by controller id) | 122 | | nv_controller_memory | Memory usage of controllers (by controller id) | 123 | | nv_enforcer_cpu | CPU usage of enforcers (by enforcer id) | 124 | | nv_enforcer_memory | Memory usage of enforcers (by enforcer id) | 125 | | nv_conversation_bytes | Network bandwidth of applications | 126 | | nv_admission_allowed | Number of allowed admission control requests | 127 | | nv_admission_denied | Number of denied admission control requests | 128 | | nv_image_vulnerabilityHigh | Number of vulnerabilities of high severity (by image id) | 129 | | nv_image_vulnerabilityMedium | Number of vulnerabilities of medium severity (by image id) | 130 | | nv_container_vulnerabilityHigh | Number of vulnerabilities of high severity (by service name) | 131 | | nv_container_vulnerabilityMedium | Number of vulnerabilities of medium severity (by service name) | 132 | | nv_log_events | Lists of security events | 133 | | nv_fed_master | Shows the status of all the connected clusters to the federated master | 134 | | nv_fed_worker | Shows the status of the cluster to the federated master | 135 | 136 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | prometheus-exporter: 2 | image: neuvector/prometheus-exporter 3 | container_name: neuvector.prometheus-exporter 4 | environment: 5 | - CTRL_API_SERVICE=neuvector-svc-prometheus-exporter.neuvector:10443 6 | - CTRL_USERNAME=admin 7 | - CTRL_PASSWORD=admin 8 | - EXPORTER_PORT=8068 9 | ports: 10 | - 8068:8068 11 | -------------------------------------------------------------------------------- /nv_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 2, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "cacheTimeout": null, 23 | "colorBackground": false, 24 | "colorPostfix": false, 25 | "colorPrefix": false, 26 | "colorValue": true, 27 | "colors": [ 28 | "#1F60C4", 29 | "#3274D9", 30 | "#d44a3a" 31 | ], 32 | "description": "", 33 | "format": "short", 34 | "gauge": { 35 | "maxValue": 100, 36 | "minValue": 0, 37 | "show": false, 38 | "thresholdLabels": false, 39 | "thresholdMarkers": true 40 | }, 41 | "gridPos": { 42 | "h": 3, 43 | "w": 3, 44 | "x": 0, 45 | "y": 0 46 | }, 47 | "id": 26, 48 | "interval": null, 49 | "links": [], 50 | "mappingType": 1, 51 | "mappingTypes": [ 52 | { 53 | "name": "value to text", 54 | "value": 1 55 | }, 56 | { 57 | "name": "range to text", 58 | "value": 2 59 | } 60 | ], 61 | "maxDataPoints": 100, 62 | "nullPointMode": "connected", 63 | "nullText": null, 64 | "options": {}, 65 | "pluginVersion": "6.2.2", 66 | "postfix": "", 67 | "postfixFontSize": "50%", 68 | "prefix": "", 69 | "prefixFontSize": "50%", 70 | "rangeMaps": [ 71 | { 72 | "from": "null", 73 | "text": "N/A", 74 | "to": "null" 75 | } 76 | ], 77 | "sparkline": { 78 | "fillColor": "rgba(31, 118, 189, 0.18)", 79 | "full": false, 80 | "lineColor": "rgb(31, 120, 193)", 81 | "show": false 82 | }, 83 | "tableColumn": "", 84 | "targets": [ 85 | { 86 | "expr": "nv_summary_hosts", 87 | "format": "time_series", 88 | "intervalFactor": 1, 89 | "legendFormat": "{{target}}", 90 | "refId": "A" 91 | } 92 | ], 93 | "thresholds": "", 94 | "timeFrom": null, 95 | "timeShift": null, 96 | "title": "Hosts", 97 | "type": "singlestat", 98 | "valueFontSize": "80%", 99 | "valueMaps": [ 100 | { 101 | "op": "=", 102 | "text": "N/A", 103 | "value": "null" 104 | } 105 | ], 106 | "valueName": "current" 107 | }, 108 | { 109 | "cacheTimeout": null, 110 | "colorBackground": false, 111 | "colorValue": true, 112 | "colors": [ 113 | "#3274D9", 114 | "#3274D9", 115 | "#d44a3a" 116 | ], 117 | "format": "none", 118 | "gauge": { 119 | "maxValue": 100, 120 | "minValue": 0, 121 | "show": false, 122 | "thresholdLabels": false, 123 | "thresholdMarkers": true 124 | }, 125 | "gridPos": { 126 | "h": 3, 127 | "w": 3, 128 | "x": 3, 129 | "y": 0 130 | }, 131 | "id": 2, 132 | "interval": null, 133 | "links": [], 134 | "mappingType": 1, 135 | "mappingTypes": [ 136 | { 137 | "name": "value to text", 138 | "value": 1 139 | }, 140 | { 141 | "name": "range to text", 142 | "value": 2 143 | } 144 | ], 145 | "maxDataPoints": 100, 146 | "nullPointMode": "connected", 147 | "nullText": null, 148 | "options": {}, 149 | "pluginVersion": "6.2.2", 150 | "postfix": "", 151 | "postfixFontSize": "50%", 152 | "prefix": "", 153 | "prefixFontSize": "50%", 154 | "rangeMaps": [ 155 | { 156 | "from": "null", 157 | "text": "N/A", 158 | "to": "null" 159 | } 160 | ], 161 | "sparkline": { 162 | "fillColor": "rgba(31, 118, 189, 0.18)", 163 | "full": false, 164 | "lineColor": "rgb(31, 120, 193)", 165 | "show": false 166 | }, 167 | "tableColumn": "", 168 | "targets": [ 169 | { 170 | "expr": "nv_summary_controllers", 171 | "format": "time_series", 172 | "intervalFactor": 1, 173 | "legendFormat": "{{target}}", 174 | "refId": "A" 175 | } 176 | ], 177 | "thresholds": "", 178 | "timeFrom": null, 179 | "timeShift": null, 180 | "title": "Controllers", 181 | "type": "singlestat", 182 | "valueFontSize": "80%", 183 | "valueMaps": [ 184 | { 185 | "op": "=", 186 | "text": "N/A", 187 | "value": "null" 188 | } 189 | ], 190 | "valueName": "avg" 191 | }, 192 | { 193 | "cacheTimeout": null, 194 | "colorBackground": false, 195 | "colorValue": true, 196 | "colors": [ 197 | "#299c46", 198 | "#3274D9", 199 | "#d44a3a" 200 | ], 201 | "format": "none", 202 | "gauge": { 203 | "maxValue": 100, 204 | "minValue": 0, 205 | "show": false, 206 | "thresholdLabels": false, 207 | "thresholdMarkers": true 208 | }, 209 | "gridPos": { 210 | "h": 3, 211 | "w": 3, 212 | "x": 6, 213 | "y": 0 214 | }, 215 | "id": 25, 216 | "interval": null, 217 | "links": [], 218 | "mappingType": 1, 219 | "mappingTypes": [ 220 | { 221 | "name": "value to text", 222 | "value": 1 223 | }, 224 | { 225 | "name": "range to text", 226 | "value": 2 227 | } 228 | ], 229 | "maxDataPoints": 100, 230 | "nullPointMode": "connected", 231 | "nullText": null, 232 | "options": {}, 233 | "pluginVersion": "6.2.2", 234 | "postfix": "", 235 | "postfixFontSize": "50%", 236 | "prefix": "", 237 | "prefixFontSize": "50%", 238 | "rangeMaps": [ 239 | { 240 | "from": "null", 241 | "text": "N/A", 242 | "to": "null" 243 | } 244 | ], 245 | "sparkline": { 246 | "fillColor": "rgba(31, 118, 189, 0.18)", 247 | "full": false, 248 | "lineColor": "rgb(31, 120, 193)", 249 | "show": false 250 | }, 251 | "tableColumn": "", 252 | "targets": [ 253 | { 254 | "expr": "nv_summary_enforcers", 255 | "format": "time_series", 256 | "intervalFactor": 1, 257 | "legendFormat": "{{target}}", 258 | "refId": "A" 259 | } 260 | ], 261 | "thresholds": "", 262 | "timeFrom": null, 263 | "timeShift": null, 264 | "title": "Enforcers", 265 | "type": "singlestat", 266 | "valueFontSize": "80%", 267 | "valueMaps": [ 268 | { 269 | "op": "=", 270 | "text": "N/A", 271 | "value": "null" 272 | } 273 | ], 274 | "valueName": "avg" 275 | }, 276 | { 277 | "cacheTimeout": null, 278 | "colorBackground": false, 279 | "colorValue": true, 280 | "colors": [ 281 | "#299c46", 282 | "#3274D9", 283 | "#d44a3a" 284 | ], 285 | "decimals": 0, 286 | "format": "short", 287 | "gauge": { 288 | "maxValue": 100, 289 | "minValue": 0, 290 | "show": false, 291 | "thresholdLabels": false, 292 | "thresholdMarkers": true 293 | }, 294 | "gridPos": { 295 | "h": 3, 296 | "w": 3, 297 | "x": 9, 298 | "y": 0 299 | }, 300 | "id": 20, 301 | "interval": null, 302 | "links": [], 303 | "mappingType": 1, 304 | "mappingTypes": [ 305 | { 306 | "name": "value to text", 307 | "value": 1 308 | }, 309 | { 310 | "name": "range to text", 311 | "value": 2 312 | } 313 | ], 314 | "maxDataPoints": 100, 315 | "nullPointMode": "connected", 316 | "nullText": null, 317 | "options": {}, 318 | "pluginVersion": "6.2.2", 319 | "postfix": "", 320 | "postfixFontSize": "50%", 321 | "prefix": "", 322 | "prefixFontSize": "50%", 323 | "rangeMaps": [ 324 | { 325 | "from": "null", 326 | "text": "N/A", 327 | "to": "null" 328 | } 329 | ], 330 | "sparkline": { 331 | "fillColor": "rgba(31, 118, 189, 0.18)", 332 | "full": false, 333 | "lineColor": "rgb(31, 120, 193)", 334 | "show": false 335 | }, 336 | "tableColumn": "", 337 | "targets": [ 338 | { 339 | "expr": "nv_summary_pods", 340 | "format": "time_series", 341 | "intervalFactor": 1, 342 | "legendFormat": "{{target}}", 343 | "refId": "A" 344 | } 345 | ], 346 | "thresholds": "", 347 | "timeFrom": null, 348 | "timeShift": null, 349 | "title": "Pods", 350 | "type": "singlestat", 351 | "valueFontSize": "80%", 352 | "valueMaps": [ 353 | { 354 | "op": "=", 355 | "text": "N/A", 356 | "value": "null" 357 | } 358 | ], 359 | "valueName": "current" 360 | }, 361 | { 362 | "cacheTimeout": null, 363 | "colorBackground": false, 364 | "colorValue": true, 365 | "colors": [ 366 | "#299c46", 367 | "#3274D9", 368 | "#d44a3a" 369 | ], 370 | "decimals": 0, 371 | "format": "short", 372 | "gauge": { 373 | "maxValue": 100, 374 | "minValue": 0, 375 | "show": false, 376 | "thresholdLabels": false, 377 | "thresholdMarkers": true 378 | }, 379 | "gridPos": { 380 | "h": 3, 381 | "w": 3, 382 | "x": 12, 383 | "y": 0 384 | }, 385 | "id": 19, 386 | "interval": null, 387 | "links": [], 388 | "mappingType": 1, 389 | "mappingTypes": [ 390 | { 391 | "name": "value to text", 392 | "value": 1 393 | }, 394 | { 395 | "name": "range to text", 396 | "value": 2 397 | } 398 | ], 399 | "maxDataPoints": 100, 400 | "nullPointMode": "connected", 401 | "nullText": null, 402 | "options": {}, 403 | "pluginVersion": "6.2.2", 404 | "postfix": "", 405 | "postfixFontSize": "50%", 406 | "prefix": "", 407 | "prefixFontSize": "50%", 408 | "rangeMaps": [ 409 | { 410 | "from": "null", 411 | "text": "N/A", 412 | "to": "null" 413 | } 414 | ], 415 | "sparkline": { 416 | "fillColor": "rgba(31, 118, 189, 0.18)", 417 | "full": false, 418 | "lineColor": "rgb(31, 120, 193)", 419 | "show": false 420 | }, 421 | "tableColumn": "", 422 | "targets": [ 423 | { 424 | "expr": "nv_summary_disconnectedEnforcers", 425 | "format": "time_series", 426 | "intervalFactor": 1, 427 | "legendFormat": "{{target}}", 428 | "refId": "A" 429 | } 430 | ], 431 | "thresholds": "", 432 | "timeFrom": null, 433 | "timeShift": null, 434 | "title": "Disconnected Enforcers", 435 | "type": "singlestat", 436 | "valueFontSize": "80%", 437 | "valueMaps": [ 438 | { 439 | "op": "=", 440 | "text": "N/A", 441 | "value": "null" 442 | } 443 | ], 444 | "valueName": "current" 445 | }, 446 | { 447 | "aliasColors": {}, 448 | "bars": false, 449 | "dashLength": 10, 450 | "dashes": false, 451 | "decimals": null, 452 | "fill": 0, 453 | "gridPos": { 454 | "h": 6, 455 | "w": 9, 456 | "x": 15, 457 | "y": 0 458 | }, 459 | "id": 35, 460 | "legend": { 461 | "avg": false, 462 | "current": false, 463 | "hideEmpty": true, 464 | "hideZero": true, 465 | "max": false, 466 | "min": false, 467 | "show": false, 468 | "total": false, 469 | "values": false 470 | }, 471 | "lines": true, 472 | "linewidth": 1, 473 | "links": [], 474 | "nullPointMode": "null as zero", 475 | "options": {}, 476 | "percentage": false, 477 | "pointradius": 2, 478 | "points": false, 479 | "renderer": "flot", 480 | "seriesOverrides": [], 481 | "spaceLength": 10, 482 | "stack": false, 483 | "steppedLine": false, 484 | "targets": [ 485 | { 486 | "expr": "sum(rate(nv_conversation_bytes[1m]))*4", 487 | "format": "time_series", 488 | "intervalFactor": 1, 489 | "legendFormat": "bytes per minute", 490 | "refId": "A" 491 | }, 492 | { 493 | "expr": "api_conversation_bytes", 494 | "format": "time_series", 495 | "hide": true, 496 | "intervalFactor": 1, 497 | "refId": "B" 498 | } 499 | ], 500 | "thresholds": [], 501 | "timeFrom": null, 502 | "timeRegions": [], 503 | "timeShift": null, 504 | "title": "Traffics", 505 | "tooltip": { 506 | "shared": true, 507 | "sort": 2, 508 | "value_type": "individual" 509 | }, 510 | "type": "graph", 511 | "xaxis": { 512 | "buckets": null, 513 | "mode": "time", 514 | "name": null, 515 | "show": true, 516 | "values": [] 517 | }, 518 | "yaxes": [ 519 | { 520 | "format": "bytes", 521 | "label": "bytes per minute", 522 | "logBase": 1, 523 | "max": null, 524 | "min": null, 525 | "show": true 526 | }, 527 | { 528 | "format": "short", 529 | "label": null, 530 | "logBase": 1, 531 | "max": null, 532 | "min": null, 533 | "show": true 534 | } 535 | ], 536 | "yaxis": { 537 | "align": false, 538 | "alignLevel": null 539 | } 540 | }, 541 | { 542 | "cacheTimeout": null, 543 | "colorBackground": false, 544 | "colorValue": true, 545 | "colors": [ 546 | "#299c46", 547 | "#56A64B", 548 | "#d44a3a" 549 | ], 550 | "format": "none", 551 | "gauge": { 552 | "maxValue": 100, 553 | "minValue": 0, 554 | "show": false, 555 | "thresholdLabels": false, 556 | "thresholdMarkers": true 557 | }, 558 | "gridPos": { 559 | "h": 3, 560 | "w": 3, 561 | "x": 0, 562 | "y": 3 563 | }, 564 | "id": 22, 565 | "interval": null, 566 | "links": [], 567 | "mappingType": 1, 568 | "mappingTypes": [ 569 | { 570 | "name": "value to text", 571 | "value": 1 572 | }, 573 | { 574 | "name": "range to text", 575 | "value": 2 576 | } 577 | ], 578 | "maxDataPoints": 100, 579 | "nullPointMode": "connected", 580 | "nullText": null, 581 | "options": {}, 582 | "postfix": "", 583 | "postfixFontSize": "50%", 584 | "prefix": "", 585 | "prefixFontSize": "50%", 586 | "rangeMaps": [ 587 | { 588 | "from": "null", 589 | "text": "N/A", 590 | "to": "null" 591 | } 592 | ], 593 | "sparkline": { 594 | "fillColor": "rgba(31, 118, 189, 0.18)", 595 | "full": false, 596 | "lineColor": "rgb(31, 120, 193)", 597 | "show": false 598 | }, 599 | "tableColumn": "", 600 | "targets": [ 601 | { 602 | "expr": "nv_admission_allowed", 603 | "format": "time_series", 604 | "intervalFactor": 1, 605 | "legendFormat": "{{target}}:allowed", 606 | "refId": "A" 607 | } 608 | ], 609 | "thresholds": "", 610 | "timeFrom": null, 611 | "timeShift": null, 612 | "title": "Allowed Admissions", 613 | "type": "singlestat", 614 | "valueFontSize": "80%", 615 | "valueMaps": [ 616 | { 617 | "op": "=", 618 | "text": "N/A", 619 | "value": "null" 620 | } 621 | ], 622 | "valueName": "current" 623 | }, 624 | { 625 | "cacheTimeout": null, 626 | "colorBackground": false, 627 | "colorValue": true, 628 | "colors": [ 629 | "#299c46", 630 | "#E02F44", 631 | "#d44a3a" 632 | ], 633 | "format": "none", 634 | "gauge": { 635 | "maxValue": 100, 636 | "minValue": 0, 637 | "show": false, 638 | "thresholdLabels": false, 639 | "thresholdMarkers": true 640 | }, 641 | "gridPos": { 642 | "h": 3, 643 | "w": 3, 644 | "x": 3, 645 | "y": 3 646 | }, 647 | "id": 32, 648 | "interval": null, 649 | "links": [], 650 | "mappingType": 1, 651 | "mappingTypes": [ 652 | { 653 | "name": "value to text", 654 | "value": 1 655 | }, 656 | { 657 | "name": "range to text", 658 | "value": 2 659 | } 660 | ], 661 | "maxDataPoints": 100, 662 | "nullPointMode": "connected", 663 | "nullText": null, 664 | "options": {}, 665 | "postfix": "", 666 | "postfixFontSize": "50%", 667 | "prefix": "", 668 | "prefixFontSize": "50%", 669 | "rangeMaps": [ 670 | { 671 | "from": "null", 672 | "text": "N/A", 673 | "to": "null" 674 | } 675 | ], 676 | "sparkline": { 677 | "fillColor": "rgba(31, 118, 189, 0.18)", 678 | "full": false, 679 | "lineColor": "rgb(31, 120, 193)", 680 | "show": false 681 | }, 682 | "tableColumn": "", 683 | "targets": [ 684 | { 685 | "expr": "nv_admission_denied", 686 | "format": "time_series", 687 | "intervalFactor": 1, 688 | "legendFormat": "", 689 | "refId": "A" 690 | } 691 | ], 692 | "thresholds": "", 693 | "timeFrom": null, 694 | "timeShift": null, 695 | "title": "Denied Admissions", 696 | "type": "singlestat", 697 | "valueFontSize": "80%", 698 | "valueMaps": [ 699 | { 700 | "op": "=", 701 | "text": "N/A", 702 | "value": "null" 703 | } 704 | ], 705 | "valueName": "current" 706 | }, 707 | { 708 | "cacheTimeout": null, 709 | "colorBackground": false, 710 | "colorValue": true, 711 | "colors": [ 712 | "#299c46", 713 | "#F2CC0C", 714 | "#d44a3a" 715 | ], 716 | "decimals": 3, 717 | "format": "short", 718 | "gauge": { 719 | "maxValue": 100, 720 | "minValue": 0, 721 | "show": false, 722 | "thresholdLabels": false, 723 | "thresholdMarkers": true 724 | }, 725 | "gridPos": { 726 | "h": 3, 727 | "w": 3, 728 | "x": 6, 729 | "y": 3 730 | }, 731 | "id": 8, 732 | "interval": null, 733 | "links": [], 734 | "mappingType": 1, 735 | "mappingTypes": [ 736 | { 737 | "name": "value to text", 738 | "value": 1 739 | }, 740 | { 741 | "name": "range to text", 742 | "value": 2 743 | } 744 | ], 745 | "maxDataPoints": 100, 746 | "nullPointMode": "connected", 747 | "nullText": null, 748 | "options": {}, 749 | "pluginVersion": "6.2.2", 750 | "postfix": "", 751 | "postfixFontSize": "50%", 752 | "prefix": "", 753 | "prefixFontSize": "50%", 754 | "rangeMaps": [ 755 | { 756 | "from": "null", 757 | "text": "N/A", 758 | "to": "null" 759 | } 760 | ], 761 | "sparkline": { 762 | "fillColor": "rgba(31, 118, 189, 0.18)", 763 | "full": false, 764 | "lineColor": "rgb(31, 120, 193)", 765 | "show": false 766 | }, 767 | "tableColumn": "", 768 | "targets": [ 769 | { 770 | "expr": "nv_summary_cvedbVersion", 771 | "format": "time_series", 772 | "instant": false, 773 | "intervalFactor": 1, 774 | "legendFormat": "{{target}}", 775 | "refId": "A" 776 | } 777 | ], 778 | "thresholds": "", 779 | "timeFrom": null, 780 | "timeShift": null, 781 | "title": "CVEDB Version", 782 | "type": "singlestat", 783 | "valueFontSize": "80%", 784 | "valueMaps": [ 785 | { 786 | "op": "=", 787 | "text": "N/A", 788 | "value": "null" 789 | } 790 | ], 791 | "valueName": "current" 792 | }, 793 | { 794 | "cacheTimeout": null, 795 | "colorBackground": false, 796 | "colorValue": true, 797 | "colors": [ 798 | "#299c46", 799 | "#A352CC", 800 | "#d44a3a" 801 | ], 802 | "decimals": null, 803 | "format": "dateTimeAsIso", 804 | "gauge": { 805 | "maxValue": 100, 806 | "minValue": 0, 807 | "show": false, 808 | "thresholdLabels": false, 809 | "thresholdMarkers": true 810 | }, 811 | "gridPos": { 812 | "h": 3, 813 | "w": 6, 814 | "x": 9, 815 | "y": 3 816 | }, 817 | "id": 27, 818 | "interval": null, 819 | "links": [], 820 | "mappingType": 1, 821 | "mappingTypes": [ 822 | { 823 | "name": "value to text", 824 | "value": 1 825 | }, 826 | { 827 | "name": "range to text", 828 | "value": 2 829 | } 830 | ], 831 | "maxDataPoints": 100, 832 | "nullPointMode": "connected", 833 | "nullText": null, 834 | "options": {}, 835 | "pluginVersion": "6.2.2", 836 | "postfix": "", 837 | "postfixFontSize": "50%", 838 | "prefix": "", 839 | "prefixFontSize": "30%", 840 | "rangeMaps": [ 841 | { 842 | "from": "null", 843 | "text": "N/A", 844 | "to": "null" 845 | } 846 | ], 847 | "sparkline": { 848 | "fillColor": "rgba(31, 118, 189, 0.18)", 849 | "full": false, 850 | "lineColor": "rgb(31, 120, 193)", 851 | "show": false 852 | }, 853 | "tableColumn": "", 854 | "targets": [ 855 | { 856 | "expr": "nv_summary_cvedbTime", 857 | "format": "time_series", 858 | "instant": false, 859 | "intervalFactor": 1, 860 | "legendFormat": "{{target}}", 861 | "refId": "A" 862 | } 863 | ], 864 | "thresholds": "", 865 | "timeFrom": null, 866 | "timeShift": null, 867 | "title": "CVEDB Create Time", 868 | "type": "singlestat", 869 | "valueFontSize": "50%", 870 | "valueMaps": [ 871 | { 872 | "op": "=", 873 | "text": "N/A", 874 | "value": "null" 875 | } 876 | ], 877 | "valueName": "current" 878 | }, 879 | { 880 | "aliasColors": {}, 881 | "bars": false, 882 | "cacheTimeout": null, 883 | "dashLength": 10, 884 | "dashes": false, 885 | "fill": 0, 886 | "gridPos": { 887 | "h": 6, 888 | "w": 12, 889 | "x": 0, 890 | "y": 6 891 | }, 892 | "id": 10, 893 | "legend": { 894 | "avg": false, 895 | "current": false, 896 | "hideEmpty": true, 897 | "hideZero": true, 898 | "max": false, 899 | "min": false, 900 | "show": true, 901 | "total": false, 902 | "values": false 903 | }, 904 | "lines": true, 905 | "linewidth": 1, 906 | "links": [], 907 | "nullPointMode": "null", 908 | "options": {}, 909 | "percentage": false, 910 | "pluginVersion": "6.2.2", 911 | "pointradius": 2, 912 | "points": false, 913 | "renderer": "flot", 914 | "seriesOverrides": [], 915 | "spaceLength": 10, 916 | "stack": false, 917 | "steppedLine": false, 918 | "targets": [ 919 | { 920 | "expr": "nv_enforcer_cpu", 921 | "format": "time_series", 922 | "intervalFactor": 1, 923 | "legendFormat": "{{display}}", 924 | "refId": "A" 925 | } 926 | ], 927 | "thresholds": [], 928 | "timeFrom": null, 929 | "timeRegions": [], 930 | "timeShift": null, 931 | "title": "CPU Usage", 932 | "tooltip": { 933 | "shared": true, 934 | "sort": 2, 935 | "value_type": "individual" 936 | }, 937 | "type": "graph", 938 | "xaxis": { 939 | "buckets": null, 940 | "mode": "time", 941 | "name": null, 942 | "show": true, 943 | "values": [] 944 | }, 945 | "yaxes": [ 946 | { 947 | "format": "percentunit", 948 | "label": null, 949 | "logBase": 1, 950 | "max": null, 951 | "min": null, 952 | "show": true 953 | }, 954 | { 955 | "format": "short", 956 | "label": null, 957 | "logBase": 1, 958 | "max": null, 959 | "min": null, 960 | "show": true 961 | } 962 | ], 963 | "yaxis": { 964 | "align": false, 965 | "alignLevel": null 966 | } 967 | }, 968 | { 969 | "aliasColors": {}, 970 | "bars": false, 971 | "dashLength": 10, 972 | "dashes": false, 973 | "fill": 0, 974 | "gridPos": { 975 | "h": 6, 976 | "w": 12, 977 | "x": 12, 978 | "y": 6 979 | }, 980 | "id": 12, 981 | "legend": { 982 | "avg": false, 983 | "current": false, 984 | "hideEmpty": true, 985 | "hideZero": true, 986 | "max": false, 987 | "min": false, 988 | "show": true, 989 | "total": false, 990 | "values": false 991 | }, 992 | "lines": true, 993 | "linewidth": 1, 994 | "links": [], 995 | "nullPointMode": "null", 996 | "options": {}, 997 | "percentage": false, 998 | "pointradius": 2, 999 | "points": false, 1000 | "renderer": "flot", 1001 | "seriesOverrides": [], 1002 | "spaceLength": 10, 1003 | "stack": false, 1004 | "steppedLine": false, 1005 | "targets": [ 1006 | { 1007 | "expr": "nv_enforcer_memory", 1008 | "format": "time_series", 1009 | "intervalFactor": 1, 1010 | "legendFormat": "{{display}}", 1011 | "refId": "A" 1012 | } 1013 | ], 1014 | "thresholds": [], 1015 | "timeFrom": null, 1016 | "timeRegions": [], 1017 | "timeShift": null, 1018 | "title": "Memory Usage", 1019 | "tooltip": { 1020 | "shared": true, 1021 | "sort": 2, 1022 | "value_type": "individual" 1023 | }, 1024 | "type": "graph", 1025 | "xaxis": { 1026 | "buckets": null, 1027 | "mode": "time", 1028 | "name": null, 1029 | "show": true, 1030 | "values": [] 1031 | }, 1032 | "yaxes": [ 1033 | { 1034 | "format": "bytes", 1035 | "label": null, 1036 | "logBase": 1, 1037 | "max": null, 1038 | "min": null, 1039 | "show": true 1040 | }, 1041 | { 1042 | "format": "short", 1043 | "label": null, 1044 | "logBase": 1, 1045 | "max": null, 1046 | "min": null, 1047 | "show": true 1048 | } 1049 | ], 1050 | "yaxis": { 1051 | "align": false, 1052 | "alignLevel": null 1053 | } 1054 | }, 1055 | { 1056 | "cacheTimeout": null, 1057 | "columns": [ 1058 | { 1059 | "text": "Current", 1060 | "value": "current" 1061 | } 1062 | ], 1063 | "fontSize": "90%", 1064 | "gridPos": { 1065 | "h": 6, 1066 | "w": 12, 1067 | "x": 0, 1068 | "y": 12 1069 | }, 1070 | "id": 24, 1071 | "links": [], 1072 | "options": {}, 1073 | "pageSize": null, 1074 | "pluginVersion": "6.2.2", 1075 | "scroll": true, 1076 | "showHeader": true, 1077 | "sort": { 1078 | "col": 2, 1079 | "desc": true 1080 | }, 1081 | "styles": [ 1082 | { 1083 | "alias": "", 1084 | "colorMode": null, 1085 | "colors": [ 1086 | "rgba(245, 54, 54, 0.9)", 1087 | "rgba(237, 129, 40, 0.89)", 1088 | "rgba(50, 172, 45, 0.97)" 1089 | ], 1090 | "dateFormat": "HH:mm:ss", 1091 | "decimals": 2, 1092 | "mappingType": 1, 1093 | "pattern": "Time", 1094 | "thresholds": [], 1095 | "type": "hidden", 1096 | "unit": "short" 1097 | }, 1098 | { 1099 | "alias": "High", 1100 | "colorMode": "value", 1101 | "colors": [ 1102 | "#E02F44", 1103 | "rgba(237, 129, 40, 0.89)", 1104 | "rgba(50, 172, 45, 0.97)" 1105 | ], 1106 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1107 | "decimals": 0, 1108 | "mappingType": 1, 1109 | "pattern": "Value #A", 1110 | "thresholds": [], 1111 | "type": "number", 1112 | "unit": "short" 1113 | }, 1114 | { 1115 | "alias": "Medium", 1116 | "colorMode": "value", 1117 | "colors": [ 1118 | "#FF780A", 1119 | "rgba(237, 129, 40, 0.89)", 1120 | "rgba(50, 172, 45, 0.97)" 1121 | ], 1122 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1123 | "decimals": 0, 1124 | "mappingType": 1, 1125 | "pattern": "Value #B", 1126 | "thresholds": [], 1127 | "type": "number", 1128 | "unit": "short" 1129 | }, 1130 | { 1131 | "alias": "Service", 1132 | "colorMode": null, 1133 | "colors": [ 1134 | "rgba(245, 54, 54, 0.9)", 1135 | "rgba(237, 129, 40, 0.89)", 1136 | "rgba(50, 172, 45, 0.97)" 1137 | ], 1138 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1139 | "decimals": 2, 1140 | "mappingType": 1, 1141 | "pattern": "service", 1142 | "sanitize": true, 1143 | "thresholds": [], 1144 | "type": "string", 1145 | "unit": "short" 1146 | } 1147 | ], 1148 | "targets": [ 1149 | { 1150 | "expr": "sum(nv_container_vulnerabilityHigh) by (service)", 1151 | "format": "table", 1152 | "instant": true, 1153 | "interval": "", 1154 | "intervalFactor": 2, 1155 | "legendFormat": "", 1156 | "refId": "A" 1157 | }, 1158 | { 1159 | "expr": "sum(nv_container_vulnerabilityMedium) by (service)", 1160 | "format": "table", 1161 | "instant": true, 1162 | "interval": "", 1163 | "intervalFactor": 2, 1164 | "legendFormat": "", 1165 | "refId": "B" 1166 | } 1167 | ], 1168 | "timeFrom": null, 1169 | "timeShift": null, 1170 | "title": "Service Vulnerabilities", 1171 | "transform": "table", 1172 | "type": "table" 1173 | }, 1174 | { 1175 | "cacheTimeout": null, 1176 | "columns": [ 1177 | { 1178 | "text": "Current", 1179 | "value": "current" 1180 | } 1181 | ], 1182 | "fontSize": "90%", 1183 | "gridPos": { 1184 | "h": 6, 1185 | "w": 12, 1186 | "x": 12, 1187 | "y": 12 1188 | }, 1189 | "id": 33, 1190 | "links": [], 1191 | "options": {}, 1192 | "pageSize": null, 1193 | "pluginVersion": "6.2.2", 1194 | "scroll": true, 1195 | "showHeader": true, 1196 | "sort": { 1197 | "col": 2, 1198 | "desc": true 1199 | }, 1200 | "styles": [ 1201 | { 1202 | "alias": "", 1203 | "colorMode": null, 1204 | "colors": [ 1205 | "rgba(245, 54, 54, 0.9)", 1206 | "rgba(237, 129, 40, 0.89)", 1207 | "rgba(50, 172, 45, 0.97)" 1208 | ], 1209 | "dateFormat": "HH:mm:ss", 1210 | "decimals": 2, 1211 | "mappingType": 1, 1212 | "pattern": "Time", 1213 | "thresholds": [], 1214 | "type": "hidden", 1215 | "unit": "short" 1216 | }, 1217 | { 1218 | "alias": "High", 1219 | "colorMode": "value", 1220 | "colors": [ 1221 | "#E02F44", 1222 | "rgba(237, 129, 40, 0.89)", 1223 | "rgba(50, 172, 45, 0.97)" 1224 | ], 1225 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1226 | "decimals": 0, 1227 | "mappingType": 1, 1228 | "pattern": "Value #A", 1229 | "thresholds": [], 1230 | "type": "number", 1231 | "unit": "short" 1232 | }, 1233 | { 1234 | "alias": "Medium", 1235 | "colorMode": "value", 1236 | "colors": [ 1237 | "#FF780A", 1238 | "rgba(237, 129, 40, 0.89)", 1239 | "rgba(50, 172, 45, 0.97)" 1240 | ], 1241 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1242 | "decimals": 0, 1243 | "mappingType": 1, 1244 | "pattern": "Value #B", 1245 | "thresholds": [], 1246 | "type": "number", 1247 | "unit": "short" 1248 | } 1249 | ], 1250 | "targets": [ 1251 | { 1252 | "expr": "sum(nv_image_vulnerabilityHigh) by (name)", 1253 | "format": "table", 1254 | "instant": true, 1255 | "interval": "", 1256 | "intervalFactor": 2, 1257 | "legendFormat": "", 1258 | "refId": "A" 1259 | }, 1260 | { 1261 | "expr": "sum(nv_image_vulnerabilityMedium) by (name)", 1262 | "format": "table", 1263 | "instant": true, 1264 | "interval": "", 1265 | "intervalFactor": 2, 1266 | "legendFormat": "", 1267 | "refId": "B" 1268 | } 1269 | ], 1270 | "timeFrom": null, 1271 | "timeShift": null, 1272 | "title": "Image Vulnerabilities", 1273 | "transform": "table", 1274 | "type": "table" 1275 | }, 1276 | { 1277 | "cacheTimeout": null, 1278 | "columns": [ 1279 | { 1280 | "text": "Current", 1281 | "value": "current" 1282 | } 1283 | ], 1284 | "fontSize": "90%", 1285 | "gridPos": { 1286 | "h": 10, 1287 | "w": 24, 1288 | "x": 0, 1289 | "y": 18 1290 | }, 1291 | "id": 29, 1292 | "links": [], 1293 | "options": {}, 1294 | "pageSize": null, 1295 | "pluginVersion": "6.2.2", 1296 | "scroll": true, 1297 | "showHeader": true, 1298 | "sort": { 1299 | "col": 1, 1300 | "desc": true 1301 | }, 1302 | "styles": [ 1303 | { 1304 | "alias": "Event", 1305 | "colorMode": null, 1306 | "colors": [ 1307 | "rgba(245, 54, 54, 0.9)", 1308 | "rgba(237, 129, 40, 0.89)", 1309 | "rgba(50, 172, 45, 0.97)" 1310 | ], 1311 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 1312 | "decimals": 2, 1313 | "link": false, 1314 | "mappingType": 1, 1315 | "pattern": "Metric", 1316 | "preserveFormat": false, 1317 | "sanitize": true, 1318 | "thresholds": [], 1319 | "type": "string", 1320 | "unit": "short" 1321 | }, 1322 | { 1323 | "alias": "Time", 1324 | "colorMode": "value", 1325 | "colors": [ 1326 | "#E0B400", 1327 | "rgba(237, 129, 40, 0.89)", 1328 | "rgba(50, 172, 45, 0.97)" 1329 | ], 1330 | "decimals": 0, 1331 | "pattern": "Current", 1332 | "thresholds": [], 1333 | "type": "number", 1334 | "unit": "dateTimeAsIso" 1335 | } 1336 | ], 1337 | "targets": [ 1338 | { 1339 | "expr": "nv_log_events", 1340 | "format": "time_series", 1341 | "instant": false, 1342 | "intervalFactor": 2, 1343 | "legendFormat": "{{name}}
{{fromname}}
-> {{toname}}", 1344 | "refId": "A" 1345 | } 1346 | ], 1347 | "timeFrom": null, 1348 | "timeShift": null, 1349 | "title": "Security Events", 1350 | "transform": "timeseries_aggregations", 1351 | "type": "table" 1352 | } 1353 | ], 1354 | "refresh": "5s", 1355 | "schemaVersion": 18, 1356 | "style": "dark", 1357 | "tags": [], 1358 | "templating": { 1359 | "list": [] 1360 | }, 1361 | "time": { 1362 | "from": "now-1h", 1363 | "to": "now" 1364 | }, 1365 | "timepicker": { 1366 | "hidden": false, 1367 | "refresh_intervals": [ 1368 | "5s", 1369 | "10s", 1370 | "15s", 1371 | "30s", 1372 | "1m", 1373 | "5m", 1374 | "15m", 1375 | "30m", 1376 | "1h" 1377 | ], 1378 | "time_options": [ 1379 | "5m", 1380 | "15m", 1381 | "1h", 1382 | "6h", 1383 | "12h", 1384 | "24h", 1385 | "2d", 1386 | "7d", 1387 | "30d" 1388 | ] 1389 | }, 1390 | "timezone": "", 1391 | "title": "NV-dashboard", 1392 | "uid": "nv_dashboard0001", 1393 | "version": 1 1394 | } 1395 | 1396 | -------------------------------------------------------------------------------- /nv_exporter.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # pylint: disable=bare-except 3 | # pylint: disable=too-many-statements 4 | # pylint: disable=too-many-locals 5 | 6 | # This script uses the neuvector api to get information which can be used by 7 | # prometheus. It used the following library 8 | # https://prometheus.github.io/client_python/ 9 | 10 | # ---------------------------------------- 11 | # Imports 12 | # ---------------------------------------- 13 | import argparse 14 | import json 15 | import os 16 | import signal 17 | import sys 18 | import time 19 | import urllib3 20 | import requests 21 | from prometheus_client import start_http_server, Metric, REGISTRY 22 | 23 | # ---------------------------------------- 24 | # Constants 25 | # ---------------------------------------- 26 | 27 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 28 | 29 | SESSION = requests.Session() 30 | ENABLE_ENFORCER_STATS = False 31 | 32 | # ---------------------------------------- 33 | # Functions 34 | # ---------------------------------------- 35 | 36 | 37 | def _login(ctrl_url, ctrl_user, ctrl_pass): 38 | """ 39 | Login to the api and get a token 40 | """ 41 | print("Login to controller ...") 42 | body = {"password": {"username": ctrl_user, "password": ctrl_pass}} 43 | headers = {'Content-Type': 'application/json'} 44 | try: 45 | response = requests.post(ctrl_url + '/v1/auth', 46 | headers=headers, 47 | data=json.dumps(body), 48 | verify=False) 49 | except requests.exceptions.RequestException as login_error: 50 | print(login_error) 51 | return -1 52 | 53 | if response.status_code != 200: 54 | message = json.loads(response.text)["message"] 55 | print(message) 56 | return -1 57 | 58 | token = json.loads(response.text)["token"]["token"] 59 | 60 | # Update request session 61 | SESSION.headers.update({"Content-Type": "application/json"}) 62 | SESSION.headers.update({'X-Auth-Token': token}) 63 | return 0 64 | 65 | # ---------------------------------------- 66 | # Classes 67 | # ---------------------------------------- 68 | 69 | 70 | class NVApiCollector: 71 | """ 72 | main api object 73 | """ 74 | 75 | def __init__(self, endpoint, ctrl_user, ctrl_pass): 76 | """ 77 | Initialize the object 78 | """ 79 | self._endpoint = endpoint 80 | self._user = ctrl_user 81 | self._pass = ctrl_pass 82 | self._url = "https://" + endpoint 83 | 84 | def sigterm_handler(self, _signo, _stack_frame): 85 | """ 86 | Logout when terminated 87 | """ 88 | print("Logout ...") 89 | SESSION.delete(self._url + '/v1/auth') 90 | sys.exit(0) 91 | 92 | def get(self, path): 93 | """ 94 | Function to perform the get operations 95 | inside the class 96 | """ 97 | retry = 0 98 | while retry < 2: 99 | try: 100 | response = SESSION.get(self._url + path, verify=False) 101 | except requests.exceptions.RequestException as response_error: 102 | print(response_error) 103 | retry += 1 104 | else: 105 | if response.status_code == 401 or response.status_code == 408: 106 | _login(self._url, self._user, self._pass) 107 | retry += 1 108 | else: 109 | return response 110 | 111 | print("Failed to GET " + path) 112 | 113 | def collect(self): 114 | """ 115 | Collect the required information 116 | This method is called by the library, for more information 117 | see https://prometheus.io/docs/instrumenting/writing_clientlibs/#overall-structure 118 | """ 119 | eps = self._endpoint.split(':') 120 | ep = eps[0] 121 | 122 | # Get system summary 123 | response = self.get('/v1/system/summary') 124 | if response: 125 | sjson = json.loads(response.text) 126 | # Set summary metrics 127 | metric = Metric('nv_summary', 'A summary of ' + ep, 'summary') 128 | metric.add_sample('nv_summary_services', 129 | value=sjson["summary"]["services"], 130 | labels={'target': ep}) 131 | metric.add_sample('nv_summary_policy', 132 | value=sjson["summary"]["policy_rules"], 133 | labels={'target': ep}) 134 | metric.add_sample('nv_summary_runningWorkloads', 135 | value=sjson["summary"]["running_workloads"], 136 | labels={'target': ep}) 137 | metric.add_sample('nv_summary_totalWorkloads', 138 | value=sjson["summary"]["workloads"], 139 | labels={'target': ep}) 140 | metric.add_sample('nv_summary_hosts', 141 | value=sjson["summary"]["hosts"], 142 | labels={'target': ep}) 143 | metric.add_sample('nv_summary_controllers', 144 | value=sjson["summary"]["controllers"], 145 | labels={'target': ep}) 146 | metric.add_sample('nv_summary_enforcers', 147 | value=sjson["summary"]["enforcers"], 148 | labels={'target': ep}) 149 | metric.add_sample('nv_summary_pods', 150 | value=sjson["summary"]["running_pods"], 151 | labels={'target': ep}) 152 | metric.add_sample('nv_summary_disconnectedEnforcers', 153 | value=sjson["summary"]["disconnected_enforcers"], 154 | labels={'target': ep}) 155 | dt = sjson["summary"]["cvedb_create_time"] 156 | if not dt: 157 | metric.add_sample('nv_summary_cvedbVersion', 158 | value=1.0, 159 | labels={'target': ep}) 160 | else: 161 | metric.add_sample('nv_summary_cvedbVersion', 162 | value=sjson["summary"]["cvedb_version"], 163 | labels={'target': ep}) 164 | # Convert time, set CVEDB create time 165 | dt = sjson["summary"]["cvedb_create_time"] 166 | if not dt: 167 | metric.add_sample('nv_summary_cvedbTime', 168 | value=0, 169 | labels={'target': ep}) 170 | else: 171 | ts = time.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') 172 | metric.add_sample('nv_summary_cvedbTime', 173 | value=time.mktime(ts) * 1000, 174 | labels={'target': ep}) 175 | yield metric 176 | 177 | # Get conversation 178 | response = self.get('/v1/conversation') 179 | if response: 180 | # Set conversation metrics 181 | metric = Metric('nv_conversation', 'conversation of ' + ep, 182 | 'gauge') 183 | for c in json.loads(response.text)['conversations']: 184 | try: 185 | c['ports'] 186 | except KeyError: 187 | port_exists = False 188 | else: 189 | port_exists = True 190 | if port_exists is True: 191 | for k in c['ports']: 192 | if c['bytes'] != 0: 193 | metric.add_sample('nv_conversation_bytes', 194 | value=c['bytes'], 195 | labels={ 196 | 'port': k, 197 | 'from': c['from'], 198 | 'to': c['to'], 199 | 'target': ep 200 | }) 201 | yield metric 202 | 203 | # Get enforcer 204 | if ENABLE_ENFORCER_STATS: 205 | response = self.get('/v1/enforcer') 206 | if response: 207 | # Read each enforcer, set enforcer metrics 208 | metric = Metric('nv_enforcer', 'enforcers of ' + ep, 'gauge') 209 | for c in json.loads(response.text)['enforcers']: 210 | response2 = self.get('/v1/enforcer/' + c['id'] + '/stats') 211 | if response2: 212 | ejson = json.loads(response2.text) 213 | metric.add_sample('nv_enforcer_cpu', 214 | value=ejson['stats']['span_1']['cpu'], 215 | labels={ 216 | 'id': c['id'], 217 | 'host': c['host_name'], 218 | 'display': c['display_name'], 219 | 'target': ep 220 | }) 221 | metric.add_sample('nv_enforcer_memory', 222 | value=ejson['stats']['span_1']['memory'], 223 | labels={ 224 | 'id': c['id'], 225 | 'host': c['host_name'], 226 | 'display': c['display_name'], 227 | 'target': ep 228 | }) 229 | yield metric 230 | 231 | # Get controller 232 | response = self.get('/v1/controller') 233 | if response: 234 | # Read each controller, set controller metrics 235 | metric = Metric('nv_controller', 'controllers of ' + ep, 'gauge') 236 | for c in json.loads(response.text)['controllers']: 237 | response2 = self.get('/v1/controller/' + c['id'] + '/stats') 238 | if response2: 239 | ejson = json.loads(response2.text) 240 | metric.add_sample('nv_controller_cpu', 241 | value=ejson['stats']['span_1']['cpu'], 242 | labels={ 243 | 'id': c['id'], 244 | 'host': c['host_name'], 245 | 'display': c['display_name'], 246 | 'target': ep 247 | }) 248 | metric.add_sample('nv_controller_memory', 249 | value=ejson['stats']['span_1']['memory'], 250 | labels={ 251 | 'id': c['id'], 252 | 'host': c['host_name'], 253 | 'display': c['display_name'], 254 | 'target': ep 255 | }) 256 | yield metric 257 | 258 | # Get host 259 | response = self.get('/v1/host') 260 | if response: 261 | # Set host metrics 262 | metric = Metric('nv_host', 'host information of ' + ep, 'gauge') 263 | for c in json.loads(response.text)['hosts']: 264 | metric.add_sample('nv_host_memory', 265 | value=c['memory'], 266 | labels={ 267 | 'name': c['name'], 268 | 'id': c['id'], 269 | 'target': ep 270 | }) 271 | yield metric 272 | 273 | # Get debug admission stats 274 | response = self.get('/v1/debug/admission_stats') 275 | if response: 276 | if response.status_code != 200: 277 | print("Admission control stats request failed: %s" % response) 278 | else: 279 | djson = json.loads(response.text) 280 | # Set admission metrics 281 | metric = Metric('nv_admission', 'Debug admission stats of ' + ep, 282 | 'gauge') 283 | metric.add_sample('nv_admission_allowed', 284 | value=djson['stats']['k8s_allowed_requests'], 285 | labels={'target': ep}) 286 | metric.add_sample('nv_admission_denied', 287 | value=djson['stats']['k8s_denied_requests'], 288 | labels={'target': ep}) 289 | yield metric 290 | 291 | # Get image vulnerability 292 | response = self.get('/v1/scan/registry') 293 | if response: 294 | # Set vulnerability metrics 295 | metric = Metric('nv_image_vulnerability', 296 | 'image vulnerability of ' + ep, 'gauge') 297 | for c in json.loads(response.text)['summarys']: 298 | response2 = self.get('/v1/scan/registry/' + c['name'] + '/images') 299 | if response2: 300 | for img in json.loads(response2.text)['images']: 301 | metric.add_sample('nv_image_vulnerabilityHigh', 302 | value=img['high'], 303 | labels={ 304 | 'name': "%s:%s" % (img['repository'], img['tag']), 305 | 'imageid': img['image_id'], 306 | 'target': ep 307 | }) 308 | metric.add_sample('nv_image_vulnerabilityMedium', 309 | value=img['medium'], 310 | labels={ 311 | 'name': "%s:%s" % (img['repository'], img['tag']), 312 | 'imageid': img['image_id'], 313 | 'target': ep 314 | }) 315 | yield metric 316 | 317 | # Get platform vulnerability 318 | response = self.get('/v1/scan/platform/') 319 | if response: 320 | # Set vulnerability metrics 321 | metric = Metric('nv_platform_vulnerability', 322 | 'platform vulnerability of ' + ep, 'gauge') 323 | for platform in json.loads(response.text)['platforms']: 324 | if (platform['high'] != 0 or platform['medium'] != 0): 325 | metric.add_sample('nv_platform_vulnerabilityHigh', 326 | value=platform['high'], 327 | labels={ 328 | 'name': platform['platform'], 329 | 'target': ep 330 | }) 331 | metric.add_sample('nv_platform_vulnerabilityMedium', 332 | value=platform['medium'], 333 | labels={ 334 | 'name': platform['platform'], 335 | 'target': ep 336 | }) 337 | yield metric 338 | 339 | # Get container vulnerability 340 | response = self.get('/v1/workload?brief=true') 341 | if response: 342 | # Set vulnerability metrics 343 | cvlist = [] 344 | metric = Metric('nv_container_vulnerability', 345 | 'container vulnerability of ' + ep, 'gauge') 346 | for c in json.loads(response.text)['workloads']: 347 | if c['service'] not in cvlist and c['service_mesh_sidecar'] is False: 348 | scan = c['scan_summary'] 349 | if scan != None and (scan['high'] != 0 or scan['medium'] != 0): 350 | metric.add_sample('nv_container_vulnerabilityHigh', 351 | value=scan['high'], 352 | labels={ 353 | 'service': c['service'], 354 | 'target': ep 355 | }) 356 | metric.add_sample('nv_container_vulnerabilityMedium', 357 | value=scan['medium'], 358 | labels={ 359 | 'service': c['service'], 360 | 'target': ep 361 | }) 362 | cvlist.append(c['service']) 363 | yield metric 364 | 365 | # Set Log metrics 366 | metric = Metric('nv_log', 'log of ' + ep, 'gauge') 367 | # Get log threat 368 | response = self.get('/v1/log/threat') 369 | if response: 370 | # Set threat 371 | ttimelist = [] 372 | tnamelist = [] 373 | tcnamelist = [] 374 | tcnslist = [] 375 | tsnamelist = [] 376 | tsnslist = [] 377 | tidlist = [] 378 | for c in json.loads(response.text)['threats']: 379 | ttimelist.append(c['reported_timestamp']) 380 | tnamelist.append(c['name']) 381 | tcnamelist.append(c['client_workload_name']) 382 | tcnslist.append(c['client_workload_domain'] if 'client_workload_domain' in c else "") 383 | tsnamelist.append(c['server_workload_name']) 384 | tsnslist.append(c['server_workload_domain'] if 'server_workload_domain' in c else "") 385 | tidlist.append(c['id']) 386 | for x in range(0, min(5, len(tidlist))): 387 | metric.add_sample('nv_log_events', 388 | value=ttimelist[x] * 1000, 389 | labels={ 390 | 'log': "threat", 391 | 'fromname': tcnamelist[x], 392 | 'fromns': tcnslist[x], 393 | 'toname': tsnamelist[x], 394 | 'tons': tsnamelist[x], 395 | 'id': tidlist[x], 396 | 'name': tnamelist[x], 397 | 'target': ep 398 | }) 399 | 400 | # Get log incident 401 | response = self.get('/v1/log/incident') 402 | if response: 403 | # Set incident metrics 404 | itimelist = [] 405 | inamelist = [] 406 | iwnamelist = [] 407 | iclusterlist = [] 408 | iwnslist = [] 409 | iwidlist = [] 410 | iidlist = [] 411 | iproc_name_list = [] 412 | iproc_path_list = [] 413 | iproc_cmd_list = [] 414 | ifile_path_list = [] 415 | ifile_name_list = [] 416 | 417 | for c in json.loads(response.text)['incidents']: 418 | itimelist.append(c['reported_timestamp']) 419 | iidlist.append(c['id']) 420 | inamelist.append(c['name']) 421 | 422 | # Check proc_name 423 | if 'proc_name' in c: 424 | iproc_name_list.append(c['proc_name']) 425 | else: 426 | iproc_name_list.append("") 427 | 428 | # Check proc_path 429 | if 'proc_path' in c: 430 | iproc_path_list.append(c['proc_path']) 431 | else: 432 | iproc_path_list.append("") 433 | 434 | # Check proc_cmd 435 | if 'proc_cmd' in c: 436 | iproc_cmd_list.append(c['proc_cmd']) 437 | else: 438 | iproc_cmd_list.append("") 439 | 440 | # Check file_path 441 | if 'file_path' in c: 442 | ifile_path_list.append(c['file_path']) 443 | else: 444 | ifile_path_list.append("") 445 | 446 | # Check file_name 447 | if 'file_name' in c: 448 | ifile_name_list.append(c['file_name']) 449 | else: 450 | ifile_name_list.append("") 451 | 452 | if 'workload_name' in c: 453 | iwnamelist.append(c['workload_name']) 454 | iclusterlist.append(c['cluster_name']) 455 | iwnslist.append(c['workload_domain'] if 'workload_domain' in c else "") 456 | iwidlist.append(c['workload_id']) 457 | else: 458 | iwnamelist.append("") 459 | iclusterlist.append("") 460 | iwnslist.append("") 461 | iwidlist.append("") 462 | 463 | for x in range(0, min(5, len(iidlist))): 464 | metric.add_sample('nv_log_events', 465 | value=itimelist[x] * 1000, 466 | labels={ 467 | 'log': "incident", 468 | 'fromname': iwnamelist[x], 469 | 'fromns': iwnslist[x], 470 | 'fromid': iwidlist[x], 471 | 'toname': " ", 472 | 'tons': " ", 473 | 'cluster': iclusterlist[x], 474 | 'name': inamelist[x], 475 | 'id': iidlist[x], 476 | 'procname': iproc_name_list[x], 477 | 'procpath': iproc_path_list[x], 478 | 'proccmd': iproc_cmd_list[x], 479 | 'filepath': ifile_path_list[x], 480 | 'filename': ifile_name_list[x], 481 | 'target': ep 482 | }) 483 | 484 | # Get log violation 485 | response = self.get('/v1/log/violation') 486 | if response: 487 | # Set violation metrics 488 | vtimelist = [] 489 | vnamelist = [] 490 | vcnamelist = [] 491 | vcnslist = [] 492 | vsnamelist = [] 493 | vsnslist = [] 494 | vidlist = [] 495 | for c in json.loads(response.text)['violations']: 496 | vtimelist.append(c['reported_timestamp']) 497 | vcnamelist.append(c['client_name']) 498 | vcnslist.append(c['client_domain'] if 'client_domain' in c else "") 499 | vcidlist.append(c['client_id']) 500 | vnamelist.append("Network Violation") 501 | vsnamelist.append(c['server_name']) 502 | vsnslist.append(c['server_domain'] if 'server_domain' in c else "") 503 | vidlist.append(c['id']) 504 | for x in range(0, min(5, len(vidlist))): 505 | metric.add_sample('nv_log_events', 506 | value=vtimelist[x] * 1000, 507 | labels={ 508 | 'log': "violation", 509 | 'id': vidlist[x], 510 | 'fromname': vcnamelist[x], 511 | 'fromns': vcnslist[x], 512 | 'fromid': vcidlist[x], 513 | 'toname': vsnamelist[x], 514 | 'tons': vsnslist[x], 515 | 'name': vnamelist[x], 516 | 'target': ep 517 | }) 518 | yield metric 519 | 520 | # Get federated information 521 | # Create nv_fed metric 522 | metric = Metric('nv_fed', 'log of ' + ep, 'gauge') 523 | 524 | # Get the api endpoint 525 | response = self.get('/v1/fed/member') 526 | 527 | # Check the respone 528 | if response: 529 | 530 | # Perform json load 531 | sjson = json.loads(response.text) 532 | 533 | # Check if the cluster is a federated master 534 | if sjson['fed_role'] == "master": 535 | 536 | # Set name of the master cluster 537 | fed_master_name = sjson['master_cluster']['name'] 538 | 539 | # Loop through the list of nodes 540 | for fed_worker in sjson['joint_clusters']: 541 | 542 | # Set status variable 543 | if fed_worker['status'] != "synced": 544 | 545 | # Set value to 0 546 | fed_worker_value = 0 547 | 548 | else: 549 | fed_worker_value = 1 550 | 551 | # Write the fed master metrics 552 | metric.add_sample('nv_fed_master', 553 | value=fed_worker_value, 554 | labels={ 555 | 'master': fed_master_name, 556 | 'worker': fed_worker['name'], 557 | 'status': fed_worker['status'] 558 | }) 559 | yield metric 560 | 561 | # Add worker metrics 562 | else: 563 | 564 | # Write the worker metrics 565 | if sjson['fed_role'] != "joint": 566 | fed_joint_status = 0 567 | else: 568 | fed_joint_status = 1 569 | 570 | # Check if there is a master entry present 571 | if 'master_cluster' in sjson: 572 | fed_master_cluster = sjson['master_cluster']['name'] 573 | else: 574 | fed_master_cluster = "" 575 | 576 | # Write the metrics 577 | metric.add_sample('nv_fed_worker', 578 | value=fed_joint_status, 579 | labels={ 580 | 'status': sjson['fed_role'], 581 | 'master': fed_master_cluster 582 | }) 583 | yield metric 584 | 585 | 586 | ENV_CTRL_API_SVC = "CTRL_API_SERVICE" 587 | ENV_CTRL_USERNAME = "CTRL_USERNAME" 588 | ENV_CTRL_PASSWORD = "CTRL_PASSWORD" 589 | ENV_EXPORTER_PORT = "EXPORTER_PORT" 590 | ENV_ENFORCER_STATS = "ENFORCER_STATS" 591 | 592 | if __name__ == '__main__': 593 | PARSER = argparse.ArgumentParser(description='NeuVector command line.') 594 | PARSER.add_argument("-e", "--port", type=int, help="exporter port") 595 | PARSER.add_argument("-s", 596 | "--server", 597 | type=str, 598 | help="controller API service") 599 | PARSER.add_argument("-u", 600 | "--username", 601 | type=str, 602 | help="controller user name") 603 | PARSER.add_argument("-p", 604 | "--password", 605 | type=str, 606 | help="controller user password") 607 | ARGSS = PARSER.parse_args() 608 | 609 | if ARGSS.server: 610 | CTRL_SVC = ARGSS.server 611 | elif ENV_CTRL_API_SVC in os.environ: 612 | CTRL_SVC = os.environ.get(ENV_CTRL_API_SVC) 613 | else: 614 | sys.exit("Controller API service endpoint must be specified.") 615 | 616 | if ARGSS.port: 617 | PORT = ARGSS.port 618 | elif ENV_EXPORTER_PORT in os.environ: 619 | PORT = int(os.environ.get(ENV_EXPORTER_PORT)) 620 | else: 621 | sys.exit("Exporter port must be specified.") 622 | 623 | if ARGSS.username: 624 | CTRL_USER = ARGSS.username 625 | elif ENV_CTRL_USERNAME in os.environ: 626 | CTRL_USER = os.environ.get(ENV_CTRL_USERNAME) 627 | else: 628 | CTRL_USER = "admin" 629 | 630 | if ARGSS.password: 631 | CTRL_PASS = ARGSS.password 632 | elif ENV_CTRL_PASSWORD in os.environ: 633 | CTRL_PASS = os.environ.get(ENV_CTRL_PASSWORD) 634 | else: 635 | CTRL_PASS = "admin" 636 | 637 | if ENV_ENFORCER_STATS in os.environ: 638 | try: 639 | ENABLE_ENFORCER_STATS = bool(os.environ.get(ENV_ENFORCER_STATS)) 640 | except NameError: 641 | ENABLE_ENFORCER_STATS = False 642 | 643 | # Login and get token 644 | if _login("https://" + CTRL_SVC, CTRL_USER, CTRL_PASS) < 0: 645 | sys.exit(1) 646 | 647 | print("Start exporter server ...") 648 | start_http_server(PORT) 649 | 650 | print("Register collector ...") 651 | COLLECTOR = NVApiCollector(CTRL_SVC, CTRL_USER, CTRL_PASS) 652 | REGISTRY.register(COLLECTOR) 653 | signal.signal(signal.SIGTERM, COLLECTOR.sigterm_handler) 654 | 655 | while True: 656 | time.sleep(30) 657 | -------------------------------------------------------------------------------- /nv_exporter.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: neuvector-svc-prometheus-exporter 5 | namespace: neuvector 6 | spec: 7 | ports: 8 | - port: 8068 9 | name: prometheus-exporter 10 | protocol: TCP 11 | selector: 12 | app: neuvector-prometheus-exporter-pod 13 | 14 | --- 15 | 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: neuvector-prometheus-exporter-pod 20 | namespace: neuvector 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: neuvector-prometheus-exporter-pod 26 | template: 27 | metadata: 28 | labels: 29 | app: neuvector-prometheus-exporter-pod 30 | spec: 31 | containers: 32 | - name: neuvector-prometheus-exporter-pod 33 | image: neuvector/prometheus-exporter:1.0.2 34 | imagePullPolicy: Always 35 | env: 36 | - name: CTRL_API_SERVICE 37 | value: neuvector-svc-controller.neuvector:10443 38 | - name: CTRL_USERNAME 39 | value: admin 40 | - name: CTRL_PASSWORD 41 | value: admin 42 | - name: EXPORTER_PORT 43 | value: "8068" 44 | # - name: EXPORTER_METRICS 45 | # value: summary,conversation,enforcer,host,admission,image_vulnerability,container_vulnerability,log 46 | restartPolicy: Always 47 | -------------------------------------------------------------------------------- /nv_exporter_secret.yaml: -------------------------------------------------------------------------------- 1 | # NV Exporter Conf File - Using Secrets/ConfigMap 2 | # Use config map for not-secret configuration data 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: nv-exporter-cm 7 | data: 8 | CTRL_API_SERVICE: neuvector-svc-controller.neuvector:10443 9 | EXPORTER_PORT: '8068' 10 | 11 | --- 12 | # Use secrets for things which are actually secret like API keys, credentials, etc 13 | # echo -n 'admin' | base64 14 | apiVersion: v1 15 | kind: Secret 16 | metadata: 17 | name: nv-exporter-secret 18 | type: Opaque 19 | data: 20 | CTRL_USERNAME: YWRtaW4= 21 | CTRL_PASSWORD: YWRtaW4= 22 | 23 | --- 24 | apiVersion: v1 25 | kind: Service 26 | metadata: 27 | name: neuvector-svc-prometheus-exporter 28 | namespace: neuvector 29 | spec: 30 | ports: 31 | - port: 8068 32 | name: prometheus-exporter 33 | protocol: TCP 34 | selector: 35 | app: neuvector-prometheus-exporter-pod 36 | 37 | --- 38 | apiVersion: apps/v1 39 | kind: Deployment 40 | metadata: 41 | name: neuvector-prometheus-exporter-pod 42 | namespace: neuvector 43 | spec: 44 | selector: 45 | matchLabels: 46 | app: neuvector-prometheus-exporter-pod 47 | replicas: 1 48 | template: 49 | metadata: 50 | labels: 51 | app: neuvector-prometheus-exporter-pod 52 | spec: 53 | containers: 54 | - name: neuvector-prometheus-exporter-pod 55 | image: neuvector/prometheus-exporter 56 | imagePullPolicy: Always 57 | envFrom: 58 | - configMapRef: 59 | name: nv-exporter-cm 60 | - secretRef: 61 | name: nv-exporter-secret 62 | # env vars - only for test cases 63 | # env: 64 | # - name: CTRL_API_SERVICE 65 | # value: neuvector-svc-controller.neuvector:10443 66 | # - name: CTRL_USERNAME 67 | # value: admin 68 | # - name: CTRL_PASSWORD 69 | # value: admin 70 | # - name: EXPORTER_PORT 71 | # value: "8068" 72 | restartPolicy: Always 73 | -------------------------------------------------------------------------------- /nv_grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neuvector/prometheus-exporter/184ced61a7293587496d09a20ecb056501d79bc2/nv_grafana.png -------------------------------------------------------------------------------- /package/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.suse.com/bci/python:3.13 2 | 3 | ARG COMMIT 4 | ARG VERSION 5 | 6 | RUN python3 -m pip install -U pip setuptools 7 | RUN python3 -m venv .venv && source .venv/bin/activate && pip3 install --upgrade pip setuptools prometheus_client requests 8 | COPY startup.sh /usr/local/bin 9 | COPY nv_exporter.py /usr/local/bin 10 | 11 | LABEL "name"="prometheus-exporter" \ 12 | "vendor"="SUSE Security" \ 13 | "neuvector.image"="neuvector/prometheus-exporter" \ 14 | "neuvector.role"="prometheus-exporter" \ 15 | "neuvector.rev"="${COMMIT}" \ 16 | "io.artifacthub.package.logo-url"=https://avatars2.githubusercontent.com/u/19367275 \ 17 | "io.artifacthub.package.readme-url"="https://raw.githubusercontent.com/neuvector/prometheus-exporter/${VERSION}/README.md" \ 18 | "org.opencontainers.image.description"="SUSE Security Prometheus Exporter" \ 19 | "org.opencontainers.image.title"="SUSE Security Prometheus Exporter" \ 20 | "org.opencontainers.image.source"="https://github.com/neuvector/prometheus-exporter/" \ 21 | "org.opencontainers.image.version"="${VERSION}" \ 22 | "org.opensuse.reference"="neuvector/prometheus-exporter:${VERSION}" 23 | 24 | ENTRYPOINT ["startup.sh"] 25 | -------------------------------------------------------------------------------- /prom-config.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: prometheus 3 | scrape_interval: 10s 4 | static_configs: 5 | - targets: ["localhost:9090"] 6 | - job_name: nv-exporter 7 | scrape_interval: 30s 8 | static_configs: 9 | - targets: ["neuvector-svc-prometheus-exporter.neuvector:8068"] 10 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: prometheus-deployment 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: prometheus 11 | template: 12 | metadata: 13 | labels: 14 | app: prometheus 15 | spec: 16 | containers: 17 | - name: prometheus 18 | image: prom/prometheus 19 | volumeMounts: 20 | - name: config-volume 21 | mountPath: /etc/prometheus/prometheus.yml 22 | subPath: prom-config.yml 23 | ports: 24 | - containerPort: 9090 25 | volumes: 26 | - name: config-volume 27 | configMap: 28 | name: prometheus-cm 29 | --- 30 | kind: Service 31 | apiVersion: v1 32 | metadata: 33 | name: prometheus-service 34 | namespace: default 35 | spec: 36 | selector: 37 | app: prometheus 38 | type: NodePort 39 | ports: 40 | - name: promui 41 | protocol: TCP 42 | port: 9090 43 | -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -f /.venv/bin/activate ]; then 3 | source /.venv/bin/activate 4 | fi 5 | python3 -u /usr/local/bin/nv_exporter.py "$@" 6 | --------------------------------------------------------------------------------