├── .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 | 
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 |
--------------------------------------------------------------------------------