├── .github
└── workflows
│ ├── docker_build.yml
│ └── release_build.yml
├── .gitignore
├── .goreleaser.yaml
├── CODEOWNERS
├── Dockerfile
├── LICENSE
├── README.md
├── common
├── common.go
└── common_test.go
├── config-sample.yaml
├── config
└── config.go
├── dashboards
├── internals.json
└── scans.json
├── go.mod
├── go.sum
├── handlers
├── handlers.go
└── handlers_test.go
├── img
└── gobug.png
├── logger
└── logger.go
├── main.go
├── metrics
└── metrics.go
├── pprof
└── pprof.go
├── scan
├── icmp.go
├── scan.go
├── top_ports.go
├── utils.go
└── utils_test.go
└── storage
├── storage.go
└── storage_test.go
/.github/workflows/docker_build.yml:
--------------------------------------------------------------------------------
1 | name: Publish DockerHub image
2 |
3 | on:
4 | push:
5 | tags:
6 | # - 'v*'
7 | - '*'
8 |
9 | jobs:
10 | build-container:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 |
16 | - name: Login to DockerHub
17 | uses: docker/login-action@v3
18 | with:
19 | username: ${{ secrets.DOCKER_USERNAME }}
20 | password: ${{ secrets.DOCKER_PASSWORD }}
21 |
22 | - name: Extract metadata (tags, labels) for Docker
23 | id: meta
24 | uses: docker/metadata-action@v5
25 | with:
26 | images: devopsworks/scan-exporter
27 | tags: |
28 | type=raw,value=latest
29 | type=ref,event=tag
30 | type=match,pattern=\d+.\d+.\d+
31 | type=match,pattern=\d+.\d+
32 |
33 | - name: Build and push
34 | uses: docker/build-push-action@v5
35 | with:
36 | context: .
37 | push: true
38 | tags: ${{ steps.meta.outputs.tags }}
39 | labels: ${{ steps.meta.outputs.labels }}
40 |
41 |
--------------------------------------------------------------------------------
/.github/workflows/release_build.yml:
--------------------------------------------------------------------------------
1 | name: Release Go project
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*" # triggers only if push new tag version, like `0.8.4` or else
7 |
8 | jobs:
9 | build:
10 | name: GoReleaser build
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Check out code into the Go module directory
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | with:
22 | go-version: '1.24.2'
23 | id: go
24 |
25 | - name: Run GoReleaser
26 | uses: goreleaser/goreleaser-action@v6
27 | with:
28 | version: 'v2.4.8'
29 | args: release --clean
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | scan-exporter
2 | *.log
3 | todo
4 | config.yaml
5 | dashboard.json
6 | docker-compose.yaml
7 | /deploy/helm/Chart.lock
8 | *.prof
9 | prod-*
10 | *.out
11 | *.notes
12 | *.png
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example goreleaser.yaml file with some sane defaults.
2 | # Make sure to check the documentation at http://goreleaser.com
3 | before:
4 | hooks:
5 | # You may remove this if you don't use go modules.
6 | - go mod download
7 | # you may remove this if you don't need go generate
8 | - go generate ./...
9 | builds:
10 | - env:
11 | - CGO_ENABLED=0
12 | goos:
13 | - linux
14 | - windows
15 | - darwin
16 | ldflags:
17 | - -s -w -X main.Version={{.Version}} -X main.BuildDate={{.CommitDate}}
18 | ignore:
19 | - goos: darwin
20 | goarch: 386
21 | - goos: windows
22 |
23 | archives:
24 | - format: binary
25 | name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
26 |
27 | checksum:
28 | name_template: "checksums.txt"
29 |
30 | snapshot:
31 | name_template: "{{ .Tag }}-next"
32 |
33 | changelog:
34 | sort: asc
35 | filters:
36 | exclude:
37 | - "^docs:"
38 | - "^test:"
39 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @devops-works/admins @eze-kiel
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM devopsworks/golang-upx:1.24.2 AS builder
2 |
3 | RUN apt-get update && apt-get install -y libcap2-bin
4 |
5 | ENV GO111MODULE=on \
6 | CGO_ENABLED=0 \
7 | GOOS=linux \
8 | GOARCH=amd64
9 |
10 | ARG VERSION="n/a"
11 | ARG BUILD_DATE="n/a"
12 |
13 | WORKDIR /build
14 |
15 | COPY go.mod .
16 | COPY go.sum .
17 | RUN go mod download
18 |
19 | COPY . .
20 |
21 | # RUN go build -o scan-exporter . && \
22 | # strip scan-exporter && \
23 | # /usr/local/bin/upx -9 scan-exporter
24 |
25 | RUN go build \
26 | -ldflags "-X main.Version=${version} -X main.BuildDate=${builddate}" \
27 | -o scan-exporter . && \
28 | strip scan-exporter && \
29 | /usr/local/bin/upx -9 scan-exporter
30 |
31 | RUN setcap cap_net_raw+ep scan-exporter
32 |
33 | FROM gcr.io/distroless/base-debian12
34 |
35 | WORKDIR /app
36 |
37 | COPY --from=builder /build/scan-exporter .
38 |
39 | COPY --from=builder /build/config-sample.yaml config.yaml
40 |
41 | EXPOSE 2112
42 |
43 | ENTRYPOINT [ "/app/scan-exporter" ]
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 devops.works
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scan Exporter
2 |
3 | Massive TCP/ICMP port scanning tool with exporter for Prometheus and JSON-formatted logs.
4 |
5 |
6 |
7 | ## Table of Contents
8 |
9 | - [Getting started](#getting-started)
10 | - [From the sources](#from-the-sources)
11 | - [From the releases](#from-the-releases)
12 | - [From `binenv`](#from-binenv)
13 | - [Usage](#usage)
14 | - [CLI](#cli)
15 | - [Kubernetes](#kubernetes)
16 | - [Configuration](#configuration)
17 | - [Configuration file](#configuration-file)
18 | - [`target_config`](#target_config)
19 | - [`tcp_config`](#tcp_config)
20 | - [`icmp_config`](#icmp_config)
21 | - [Helm](#helm)
22 | - [Metrics](#metrics)
23 | - [Logs](#logs)
24 | - [Performances](#performances)
25 | - [License](#license)
26 | - [Swag zone](#swag-zone)
27 |
28 | ## Getting started
29 |
30 | There is multiple ways to get `scan-exporter`:
31 |
32 | ### From the sources
33 |
34 | ```
35 | $ git clone https://github.com/devops-works/scan-exporter.git
36 | $ cd scan-exporter
37 | $ go build .
38 | ```
39 |
40 | ### From the releases
41 |
42 | Download the latest build from [the official releases](https://github.com/devops-works/scan-exporter/releases)
43 |
44 | ### From `binenv`
45 |
46 | If you have `binenv` installed:
47 |
48 | ```
49 | $ binenv install scan-exporter
50 | ```
51 |
52 | ## Usage
53 |
54 | ### CLI
55 |
56 | ```
57 | USAGE: [sudo] ./scan-exporter [OPTIONS]
58 |
59 | OPTIONS:
60 |
61 | -config
62 | Path to config file.
63 | Default: config.yaml (in the current directory).
64 |
65 | -pprof.addr
66 | pprof server address. pprof will expose it's metrics on this address.
67 |
68 | -metric.addr
69 | metric server address. prometheus metrics will be exposed on this address.
70 | Default: *:2112
71 |
72 | -log.lvl {trace,debug,info,warn,error,fatal}
73 | Log level.
74 | Default: info
75 | ```
76 |
77 | :bulb: ICMP can fail if you don't start `scan-exporter` with `root` permissions. However, it will not prevent ports scans from being realised.
78 |
79 | ### Kubernetes
80 |
81 | Use the charts located [here](https://github.com/devops-works/helm-charts/tree/master/scan-exporter).
82 |
83 | To deploy with charts that are, for example, under `helm-charts/scan-exporter/` in the current Kubernetes context:
84 |
85 | ```
86 | $ helm install scanexporter helm-charts/scan-exporter/
87 | ```
88 |
89 | ## Configuration
90 |
91 | ### Configuration file
92 |
93 | ```yaml
94 | # Hold the timeout, in seconds, that will be used all over the program (i.e for scans).
95 | timeout: int
96 |
97 | # Hold the number of files that can be simultaneously opened on the host.
98 | # It will be the number of scan goroutines. We recommand 1024 or 2048, but it depends
99 | # on the `ulimit` of the host.
100 | limit: int
101 |
102 | # The log level that will be used all over the program. Supported values:
103 | # trace, debug, info, warn, error, fatal
104 | [log_level: | default = "info"]
105 |
106 | # This field is used to rate limit the queries for all the targets. If it is not
107 | # set, no rate limiting will occur. It will also be overwritten by the
108 | # target-specific value.
109 | [queries_per_sec: ]
110 |
111 | # Hold the global TCP period value. It will be the default if none has been set
112 | # inside the target-specific configuration.
113 | [tcp_period: ]
114 |
115 | # Hold the global ICMP period value. It will be the default if none has been set
116 | # inside the target-specific configuration.
117 | [icmp_period: ]
118 |
119 | # Configure targets.
120 | targets:
121 | - []
122 | ```
123 |
124 | #### `target_config`
125 |
126 | ```yaml
127 | # Name of the target.
128 | # It can be whatever you want.
129 | name:
130 |
131 | # IP address of the target.
132 | # Only IPv4 addresses are supported.
133 | ip:
134 |
135 | # Apply a rate limit for a specific target. This value will overwrite the one set
136 | # globally if it exists.
137 | [queries_per_sec: ]
138 |
139 | # TCP scan parameters.
140 | [tcp: ]
141 |
142 | # ICMP scan parameters
143 | [icmp: ]
144 | ```
145 |
146 | #### `tcp_config`
147 |
148 | ```yaml
149 | # TCP scan frequency. Supported values:
150 | # {1..9999}{s,m,h,d}
151 | # For example, all those values are working and reprensent a frenquecy of
152 | # one hour: 3600s, 60m, 1h.
153 | period:
154 |
155 | # Range of ports to scan. Supported values:
156 | # all, reserved, top1000, 22, 100-1000, 11,12-14,15...
157 | range:
158 |
159 | # Ports that are expected to be open. Supported values are the same than
160 | # for range.
161 | expected:
162 | ```
163 |
164 | #### `icmp_config`
165 |
166 | ```yaml
167 | # Ping frequency. Supported values are the same than for TCP's period. To
168 | # disable ICMP requests for a specific target, use 0.
169 | period:
170 | ```
171 |
172 | Here is a working example:
173 |
174 | ```yaml
175 | timeout: 2
176 | limit: 1024
177 | log_level: "warn"
178 | queries_per_sec: 1000
179 | tcp_period: 6h
180 | icmp_period: 30s
181 |
182 | targets:
183 | - name: "app1"
184 | ip: "198.51.100.42"
185 | queries_per_sec: 500
186 | tcp:
187 | period: "12h"
188 | range: "reserved"
189 | expected: "22,80,443"
190 | icmp:
191 | period: "1m"
192 |
193 | - name: "app2"
194 | ip: "198.51.100.69"
195 | tcp:
196 | period: "1d"
197 | range: "all"
198 | expected: ""
199 |
200 | - name: "app3"
201 | ip: "198.51.100.85"
202 | tcp:
203 | period: "3h"
204 | range: "top1000"
205 | expected: "80,443"
206 | ```
207 |
208 | - `app1` will be scanned using TCP every 12 hours on all the reserved ports (1-1023), and we expect that ports 22, 80, 443 will be open, and all the others closed. It will also receive an ICMP ping every minute. It will send 500 queries per second.
209 | - `app2` will be scanned using TCP every day on all its ports (1-65535), and none of its ports should be open. It will send 1000 queries per second.
210 | - `app3` will be scanned using TCP every 3 hours on the top 1000 ports (as determined by running the `nmap -sT --top-ports 1000 -v -oG -` command), and we expect that ports 80, 443 will be open, and all the others closed.
211 |
212 | In addition, only logs with "warn" level will be displayed.
213 |
214 | ### Helm
215 |
216 | The structure of the configuration file is the same, except that is should be placed inside `values.yaml`.
217 |
218 | [See an example](https://github.com/devops-works/helm-charts/blob/master/scan-exporter/values.yaml) of `values.yaml` configuration.
219 |
220 | ## Metrics
221 |
222 | The metrics exposed by `scan-exporter` itself are the following:
223 |
224 | * `scanexporter_uptime_sec`: Uptime, in seconds. The minimal resolution is 5 seconds.
225 |
226 | * `scanexporter_targets_number_total`: Number of targets detected in configuration file.
227 |
228 | * `scanexporter_pending_scans`: Number of scans that are in the waiting line.
229 |
230 | * `scanexporter_icmp_not_responding_total`: Number of targets that doesn't respond to ICMP ping requests.
231 |
232 | * `scanexporter_open_ports_total`: Number of ports that are open for each target.
233 |
234 | * `scanexporter_unexpected_open_ports_total`: Number of ports that are open, and shouldn't be, for each target.
235 |
236 | * `scanexporter_unexpected_closed_ports_total`: Number of ports that are closed, and shouldn't be, for each target.
237 |
238 | * `scanexporter_diff_ports_total`: Number of ports that are in a different state from previous scan, for each target.
239 |
240 | * `scanexporter_rtt_total`: Respond time for each target.
241 |
242 | You can also fetch metrics from Go, promhttp etc.
243 |
244 | ## Logs
245 |
246 | `scan-exporter` produce a lot of logs about scans results and ICMP requests formatted in JSON, in order for them to be exploitable by log aggregation systems such as Loki.
247 |
248 | ## Performances
249 |
250 | In our production cluster, `scan-exporter` is able to scan all TCP ports (from 1 to 65535) of a target in less than 3 minutes.
251 |
252 | To work without problems, it requires approximately 100MiB of memory, but 50MiB should be sufficient, depending on the number of targets in your pool.
253 |
254 | ## License
255 |
256 | [MIT](https://choosealicense.com/licenses/mit/)
257 |
258 | Gopher from [Maria Letta](https://github.com/MariaLetta/free-gophers-pack)
259 |
260 | ## Swag zone
261 |
262 | [](https://forthebadge.com)
263 | [](https://forthebadge.com)
264 | [](https://forthebadge.com)
265 | [](https://forthebadge.com)
266 |
--------------------------------------------------------------------------------
/common/common.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "sort"
5 | )
6 |
7 | // StringInSlice checks if a string appears in a slice.
8 | func StringInSlice(s string, sl []string) bool {
9 | for _, v := range sl {
10 | if v == s {
11 | return true
12 | }
13 | }
14 | return false
15 | }
16 |
17 | // CompareStringSlices checks if two slices are equal.
18 | // It returns the number of different items.
19 | func CompareStringSlices(sl1, sl2 []string) int {
20 | sort.Strings(sl1)
21 | sort.Strings(sl2)
22 |
23 | newports := []string{}
24 | missingports := []string{}
25 |
26 | for _, v := range sl2 {
27 | if !StringInSlice(v, sl1) {
28 | newports = append(newports, v)
29 | }
30 | }
31 |
32 | for _, v := range sl1 {
33 | if !StringInSlice(v, sl2) {
34 | missingports = append(missingports, v)
35 | }
36 | }
37 |
38 | return len(newports) + len(missingports)
39 | }
40 |
--------------------------------------------------------------------------------
/common/common_test.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_StringInSlice(t *testing.T) {
8 | tests := []struct {
9 | name string
10 | s string
11 | sl []string
12 | want bool
13 | }{
14 | {name: "exists", s: "a", sl: []string{"a", "b", "c"}, want: true},
15 | {name: "does not exist", s: "z", sl: []string{"a", "b", "c"}, want: false},
16 | {name: "empty string", s: "", sl: []string{"a", "b", "c"}, want: false},
17 | {name: "empty slice", s: "a", sl: []string{}, want: false},
18 | }
19 | for _, tt := range tests {
20 | t.Run(tt.name, func(t *testing.T) {
21 | if got := StringInSlice(tt.s, tt.sl); got != tt.want {
22 | t.Errorf("stringInSlice() = %v, want %v", got, tt.want)
23 | }
24 | })
25 | }
26 | }
27 |
28 | func TestCompareStringSlices(t *testing.T) {
29 |
30 | tests := []struct {
31 | name string
32 | sl1 []string
33 | sl2 []string
34 | want int
35 | }{
36 | {name: "same lists", sl1: []string{"1", "2", "3"}, sl2: []string{"1", "2", "3"}, want: 0},
37 | {name: "different length1", sl1: []string{"1", "2", "3"}, sl2: []string{"1", "2", "3", "4"}, want: 1},
38 | {name: "different length2", sl1: []string{"1", "2", "3", "4"}, sl2: []string{"1", "2", "3"}, want: 1},
39 | {name: "same content, not same order", sl1: []string{"1", "2", "3"}, sl2: []string{"3", "2", "1"}, want: 0},
40 | {name: "different lists1", sl1: []string{"1", "2", "3"}, sl2: []string{"4", "5", "6"}, want: 6},
41 | {name: "different lists2", sl1: []string{"0", "1", "2", "3"}, sl2: []string{"4", "5", "6"}, want: 7},
42 | }
43 | for _, tt := range tests {
44 | t.Run(tt.name, func(t *testing.T) {
45 | if got := CompareStringSlices(tt.sl1, tt.sl2); got != tt.want {
46 | t.Errorf("CompareStringSlices() = %v, want %v", got, tt.want)
47 | }
48 | })
49 | }
50 | }
51 |
52 | func BenchmarkCompareStringSlices(b *testing.B) {
53 | for i := 0; i < b.N; i++ {
54 | CompareStringSlices([]string{"0", "1", "2", "3"}, []string{"4", "5", "6"})
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/config-sample.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | timeout: 2
3 | limit: 1024
4 | log_level: "info"
5 | queries_per_sec: 2000
6 | tcp_period: 6h
7 | icmp_period: 30s
8 |
9 | targets:
10 | - name: "app1"
11 | ip: "127.0.0.1"
12 | queries_per_sec: 1000
13 | tcp:
14 | period: "12h"
15 | range: "reserved"
16 | expected: "22,80,443"
17 | icmp:
18 | period: "1m"
19 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 |
7 | "gopkg.in/yaml.v3"
8 | )
9 |
10 | // Target holds an IP and a range of ports to scan
11 | type Target struct {
12 | IP string `yaml:"ip"`
13 | Name string `yaml:"name"`
14 | Range string `yaml:"range"`
15 | QueriesPerSecond int `yaml:"queries_per_sec"`
16 | TCP protocol `yaml:"tcp"`
17 | ICMP protocol `yaml:"icmp"`
18 | }
19 |
20 | type protocol struct {
21 | Period string `yaml:"period"`
22 | Range string `yaml:"range"`
23 | Expected string `yaml:"expected"`
24 | }
25 |
26 | // Conf holds configuration
27 | type Conf struct {
28 | Timeout int `yaml:"timeout"`
29 | Limit int `yaml:"limit"`
30 | LogLevel string `yaml:"log_level"`
31 | QueriesPerSecond int `yaml:"queries_per_sec"`
32 | TcpPeriod string `yaml:"tcp_period"`
33 | IcmpPeriod string `yaml:"icmp_period"`
34 | Targets []Target `yaml:"targets"`
35 | }
36 |
37 | // New reads config from file and returns a config struct
38 | func New(f string) (*Conf, error) {
39 | conf, err := os.Open(f)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | y, err := ioutil.ReadAll(conf)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | c := Conf{}
50 |
51 | if err = yaml.Unmarshal(y, &c); err != nil {
52 | return nil, err
53 | }
54 |
55 | return &c, nil
56 | }
57 |
--------------------------------------------------------------------------------
/dashboards/internals.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 | "iteration": 1619450818553,
19 | "links": [],
20 | "panels": [
21 | {
22 | "collapsed": false,
23 | "datasource": "Prometheus",
24 | "gridPos": {
25 | "h": 1,
26 | "w": 24,
27 | "x": 0,
28 | "y": 0
29 | },
30 | "id": 4,
31 | "panels": [],
32 | "title": "Memory",
33 | "type": "row"
34 | },
35 | {
36 | "aliasColors": {},
37 | "bars": false,
38 | "dashLength": 10,
39 | "dashes": false,
40 | "datasource": "${datasource}",
41 | "fieldConfig": {
42 | "defaults": {},
43 | "overrides": []
44 | },
45 | "fill": 1,
46 | "fillGradient": 0,
47 | "gridPos": {
48 | "h": 12,
49 | "w": 12,
50 | "x": 0,
51 | "y": 1
52 | },
53 | "hiddenSeries": false,
54 | "id": 10,
55 | "legend": {
56 | "avg": false,
57 | "current": false,
58 | "max": false,
59 | "min": false,
60 | "show": true,
61 | "total": false,
62 | "values": false
63 | },
64 | "lines": true,
65 | "linewidth": 1,
66 | "nullPointMode": "null",
67 | "options": {
68 | "alertThreshold": true
69 | },
70 | "percentage": false,
71 | "pluginVersion": "7.5.3",
72 | "pointradius": 2,
73 | "points": false,
74 | "renderer": "flot",
75 | "seriesOverrides": [],
76 | "spaceLength": 10,
77 | "stack": false,
78 | "steppedLine": false,
79 | "targets": [
80 | {
81 | "exemplar": true,
82 | "expr": "go_memstats_heap_alloc_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
83 | "interval": "",
84 | "legendFormat": "Heap allocations",
85 | "refId": "A"
86 | },
87 | {
88 | "exemplar": true,
89 | "expr": "go_memstats_heap_inuse_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
90 | "interval": "",
91 | "legendFormat": "Heap inuse",
92 | "refId": "B"
93 | },
94 | {
95 | "exemplar": true,
96 | "expr": "go_memstats_heap_idle_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
97 | "interval": "",
98 | "legendFormat": "Heap idle",
99 | "refId": "D"
100 | },
101 | {
102 | "expr": "go_memstats_stack_inuse_bytes{job=\"apps/scanexporter-scan-exporter-chart\"}",
103 | "interval": "",
104 | "legendFormat": "Stack inuse",
105 | "refId": "C"
106 | }
107 | ],
108 | "thresholds": [],
109 | "timeFrom": null,
110 | "timeRegions": [],
111 | "timeShift": null,
112 | "title": "Heap / Stack",
113 | "tooltip": {
114 | "shared": true,
115 | "sort": 0,
116 | "value_type": "individual"
117 | },
118 | "type": "graph",
119 | "xaxis": {
120 | "buckets": null,
121 | "mode": "time",
122 | "name": null,
123 | "show": true,
124 | "values": []
125 | },
126 | "yaxes": [
127 | {
128 | "format": "bytes",
129 | "label": null,
130 | "logBase": 1,
131 | "max": null,
132 | "min": null,
133 | "show": true
134 | },
135 | {
136 | "format": "short",
137 | "label": null,
138 | "logBase": 1,
139 | "max": null,
140 | "min": null,
141 | "show": true
142 | }
143 | ],
144 | "yaxis": {
145 | "align": false,
146 | "alignLevel": null
147 | }
148 | },
149 | {
150 | "datasource": "${datasource}",
151 | "fieldConfig": {
152 | "defaults": {
153 | "mappings": [],
154 | "max": 160000000,
155 | "thresholds": {
156 | "mode": "absolute",
157 | "steps": [
158 | {
159 | "color": "green",
160 | "value": null
161 | },
162 | {
163 | "color": "semi-dark-orange",
164 | "value": 100000000
165 | },
166 | {
167 | "color": "semi-dark-red",
168 | "value": 150000000
169 | }
170 | ]
171 | },
172 | "unit": "bytes"
173 | },
174 | "overrides": []
175 | },
176 | "gridPos": {
177 | "h": 12,
178 | "w": 4,
179 | "x": 12,
180 | "y": 1
181 | },
182 | "id": 12,
183 | "options": {
184 | "orientation": "auto",
185 | "reduceOptions": {
186 | "calcs": [
187 | "last"
188 | ],
189 | "fields": "",
190 | "values": false
191 | },
192 | "showThresholdLabels": false,
193 | "showThresholdMarkers": true,
194 | "text": {}
195 | },
196 | "pluginVersion": "7.5.3",
197 | "targets": [
198 | {
199 | "exemplar": true,
200 | "expr": "go_memstats_heap_alloc_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
201 | "interval": "",
202 | "legendFormat": "Heap allocated",
203 | "refId": "A"
204 | }
205 | ],
206 | "timeFrom": null,
207 | "timeShift": null,
208 | "title": "Heap allocated",
209 | "type": "gauge"
210 | },
211 | {
212 | "datasource": "${datasource}",
213 | "fieldConfig": {
214 | "defaults": {
215 | "mappings": [],
216 | "max": 160000000,
217 | "thresholds": {
218 | "mode": "absolute",
219 | "steps": [
220 | {
221 | "color": "green",
222 | "value": null
223 | },
224 | {
225 | "color": "semi-dark-orange",
226 | "value": 100000000
227 | },
228 | {
229 | "color": "red",
230 | "value": 150000000
231 | }
232 | ]
233 | },
234 | "unit": "bytes"
235 | },
236 | "overrides": []
237 | },
238 | "gridPos": {
239 | "h": 12,
240 | "w": 4,
241 | "x": 16,
242 | "y": 1
243 | },
244 | "id": 14,
245 | "options": {
246 | "orientation": "auto",
247 | "reduceOptions": {
248 | "calcs": [
249 | "last"
250 | ],
251 | "fields": "",
252 | "values": false
253 | },
254 | "showThresholdLabels": false,
255 | "showThresholdMarkers": true,
256 | "text": {}
257 | },
258 | "pluginVersion": "7.5.3",
259 | "targets": [
260 | {
261 | "exemplar": true,
262 | "expr": "go_memstats_heap_inuse_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
263 | "interval": "",
264 | "legendFormat": "",
265 | "refId": "A"
266 | }
267 | ],
268 | "timeFrom": null,
269 | "timeShift": null,
270 | "title": "Heap in use",
271 | "type": "gauge"
272 | },
273 | {
274 | "datasource": "${datasource}",
275 | "fieldConfig": {
276 | "defaults": {
277 | "mappings": [],
278 | "thresholds": {
279 | "mode": "absolute",
280 | "steps": [
281 | {
282 | "color": "green",
283 | "value": null
284 | },
285 | {
286 | "color": "#EAB839",
287 | "value": 100000000
288 | },
289 | {
290 | "color": "red",
291 | "value": 150000000
292 | }
293 | ]
294 | },
295 | "unit": "bytes"
296 | },
297 | "overrides": []
298 | },
299 | "gridPos": {
300 | "h": 12,
301 | "w": 4,
302 | "x": 20,
303 | "y": 1
304 | },
305 | "id": 16,
306 | "options": {
307 | "colorMode": "value",
308 | "graphMode": "area",
309 | "justifyMode": "auto",
310 | "orientation": "auto",
311 | "reduceOptions": {
312 | "calcs": [
313 | "last"
314 | ],
315 | "fields": "",
316 | "values": false
317 | },
318 | "text": {},
319 | "textMode": "auto"
320 | },
321 | "pluginVersion": "7.5.3",
322 | "targets": [
323 | {
324 | "exemplar": true,
325 | "expr": "go_memstats_stack_inuse_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
326 | "interval": "",
327 | "legendFormat": "",
328 | "refId": "A"
329 | }
330 | ],
331 | "timeFrom": null,
332 | "timeShift": null,
333 | "title": "Stack in use",
334 | "type": "stat"
335 | },
336 | {
337 | "datasource": "${datasource}",
338 | "fieldConfig": {
339 | "defaults": {
340 | "mappings": [],
341 | "max": 160000000,
342 | "thresholds": {
343 | "mode": "absolute",
344 | "steps": [
345 | {
346 | "color": "green",
347 | "value": null
348 | },
349 | {
350 | "color": "semi-dark-orange",
351 | "value": 100000000
352 | },
353 | {
354 | "color": "red",
355 | "value": 150000000
356 | }
357 | ]
358 | },
359 | "unit": "bytes"
360 | },
361 | "overrides": []
362 | },
363 | "gridPos": {
364 | "h": 9,
365 | "w": 6,
366 | "x": 0,
367 | "y": 13
368 | },
369 | "id": 30,
370 | "options": {
371 | "reduceOptions": {
372 | "calcs": [
373 | "max"
374 | ],
375 | "fields": "",
376 | "values": false
377 | },
378 | "showThresholdLabels": false,
379 | "showThresholdMarkers": true,
380 | "text": {}
381 | },
382 | "pluginVersion": "7.5.3",
383 | "targets": [
384 | {
385 | "exemplar": true,
386 | "expr": "go_memstats_heap_alloc_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
387 | "interval": "",
388 | "legendFormat": "",
389 | "refId": "A"
390 | }
391 | ],
392 | "timeFrom": null,
393 | "timeShift": null,
394 | "title": "Max allocated heap",
395 | "type": "gauge"
396 | },
397 | {
398 | "datasource": "${datasource}",
399 | "fieldConfig": {
400 | "defaults": {
401 | "mappings": [],
402 | "max": 160000000,
403 | "thresholds": {
404 | "mode": "absolute",
405 | "steps": [
406 | {
407 | "color": "green",
408 | "value": null
409 | },
410 | {
411 | "color": "semi-dark-orange",
412 | "value": 100000000
413 | },
414 | {
415 | "color": "red",
416 | "value": 150000000
417 | }
418 | ]
419 | },
420 | "unit": "bytes"
421 | },
422 | "overrides": []
423 | },
424 | "gridPos": {
425 | "h": 9,
426 | "w": 6,
427 | "x": 6,
428 | "y": 13
429 | },
430 | "id": 31,
431 | "options": {
432 | "reduceOptions": {
433 | "calcs": [
434 | "max"
435 | ],
436 | "fields": "",
437 | "values": false
438 | },
439 | "showThresholdLabels": false,
440 | "showThresholdMarkers": true,
441 | "text": {}
442 | },
443 | "pluginVersion": "7.5.3",
444 | "targets": [
445 | {
446 | "exemplar": true,
447 | "expr": "go_memstats_heap_inuse_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
448 | "interval": "",
449 | "legendFormat": "",
450 | "refId": "A"
451 | }
452 | ],
453 | "timeFrom": null,
454 | "timeShift": null,
455 | "title": "Max inuse heap",
456 | "type": "gauge"
457 | },
458 | {
459 | "datasource": "${datasource}",
460 | "fieldConfig": {
461 | "defaults": {
462 | "mappings": [],
463 | "max": 160000000,
464 | "thresholds": {
465 | "mode": "absolute",
466 | "steps": [
467 | {
468 | "color": "green",
469 | "value": null
470 | },
471 | {
472 | "color": "semi-dark-orange",
473 | "value": 100000000
474 | },
475 | {
476 | "color": "red",
477 | "value": 150000000
478 | }
479 | ]
480 | },
481 | "unit": "bytes"
482 | },
483 | "overrides": []
484 | },
485 | "gridPos": {
486 | "h": 9,
487 | "w": 6,
488 | "x": 12,
489 | "y": 13
490 | },
491 | "id": 33,
492 | "options": {
493 | "reduceOptions": {
494 | "calcs": [
495 | "mean"
496 | ],
497 | "fields": "",
498 | "values": false
499 | },
500 | "showThresholdLabels": false,
501 | "showThresholdMarkers": true,
502 | "text": {}
503 | },
504 | "pluginVersion": "7.5.3",
505 | "targets": [
506 | {
507 | "exemplar": true,
508 | "expr": "go_memstats_heap_alloc_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
509 | "interval": "",
510 | "legendFormat": "",
511 | "refId": "A"
512 | }
513 | ],
514 | "timeFrom": null,
515 | "timeShift": null,
516 | "title": "Mean allocated heap",
517 | "type": "gauge"
518 | },
519 | {
520 | "datasource": "${datasource}",
521 | "fieldConfig": {
522 | "defaults": {
523 | "mappings": [],
524 | "max": 160000000,
525 | "thresholds": {
526 | "mode": "absolute",
527 | "steps": [
528 | {
529 | "color": "green",
530 | "value": null
531 | },
532 | {
533 | "color": "semi-dark-orange",
534 | "value": 100000000
535 | },
536 | {
537 | "color": "red",
538 | "value": 150000000
539 | }
540 | ]
541 | },
542 | "unit": "bytes"
543 | },
544 | "overrides": []
545 | },
546 | "gridPos": {
547 | "h": 9,
548 | "w": 6,
549 | "x": 18,
550 | "y": 13
551 | },
552 | "id": 32,
553 | "options": {
554 | "reduceOptions": {
555 | "calcs": [
556 | "mean"
557 | ],
558 | "fields": "",
559 | "values": false
560 | },
561 | "showThresholdLabels": false,
562 | "showThresholdMarkers": true,
563 | "text": {}
564 | },
565 | "pluginVersion": "7.5.3",
566 | "targets": [
567 | {
568 | "exemplar": true,
569 | "expr": "go_memstats_heap_inuse_bytes{job=\"metro/scanexporter-scan-exporter-chart\"}",
570 | "interval": "",
571 | "legendFormat": "",
572 | "refId": "A"
573 | }
574 | ],
575 | "timeFrom": null,
576 | "timeShift": null,
577 | "title": "Mean inuse heap",
578 | "type": "gauge"
579 | },
580 | {
581 | "collapsed": false,
582 | "datasource": "Prometheus",
583 | "gridPos": {
584 | "h": 1,
585 | "w": 24,
586 | "x": 0,
587 | "y": 22
588 | },
589 | "id": 6,
590 | "panels": [],
591 | "title": "Go",
592 | "type": "row"
593 | },
594 | {
595 | "datasource": "${datasource}",
596 | "fieldConfig": {
597 | "defaults": {
598 | "mappings": [],
599 | "thresholds": {
600 | "mode": "absolute",
601 | "steps": [
602 | {
603 | "color": "green",
604 | "value": null
605 | }
606 | ]
607 | }
608 | },
609 | "overrides": []
610 | },
611 | "gridPos": {
612 | "h": 10,
613 | "w": 4,
614 | "x": 6,
615 | "y": 23
616 | },
617 | "id": 18,
618 | "options": {
619 | "colorMode": "value",
620 | "graphMode": "none",
621 | "justifyMode": "auto",
622 | "orientation": "auto",
623 | "reduceOptions": {
624 | "calcs": [
625 | "last"
626 | ],
627 | "fields": "",
628 | "values": false
629 | },
630 | "text": {},
631 | "textMode": "auto"
632 | },
633 | "pluginVersion": "7.5.3",
634 | "targets": [
635 | {
636 | "exemplar": true,
637 | "expr": "go_goroutines{job=\"metro/scanexporter-scan-exporter-chart\"}",
638 | "interval": "",
639 | "legendFormat": "",
640 | "refId": "A"
641 | }
642 | ],
643 | "timeFrom": null,
644 | "timeShift": null,
645 | "title": "Goroutines",
646 | "type": "stat"
647 | },
648 | {
649 | "datasource": "Prometheus",
650 | "fieldConfig": {
651 | "defaults": {
652 | "mappings": [],
653 | "thresholds": {
654 | "mode": "absolute",
655 | "steps": [
656 | {
657 | "color": "green",
658 | "value": null
659 | }
660 | ]
661 | },
662 | "unit": "s"
663 | },
664 | "overrides": []
665 | },
666 | "gridPos": {
667 | "h": 10,
668 | "w": 4,
669 | "x": 10,
670 | "y": 23
671 | },
672 | "id": 28,
673 | "options": {
674 | "colorMode": "value",
675 | "graphMode": "area",
676 | "justifyMode": "auto",
677 | "orientation": "auto",
678 | "reduceOptions": {
679 | "calcs": [
680 | "last"
681 | ],
682 | "fields": "",
683 | "values": false
684 | },
685 | "text": {},
686 | "textMode": "auto"
687 | },
688 | "pluginVersion": "7.5.3",
689 | "targets": [
690 | {
691 | "expr": "scanexporter_uptime_sec",
692 | "interval": "",
693 | "legendFormat": "",
694 | "refId": "A"
695 | }
696 | ],
697 | "timeFrom": null,
698 | "timeShift": null,
699 | "title": "Uptime",
700 | "type": "stat"
701 | },
702 | {
703 | "datasource": "${datasource}",
704 | "fieldConfig": {
705 | "defaults": {
706 | "mappings": [],
707 | "thresholds": {
708 | "mode": "absolute",
709 | "steps": [
710 | {
711 | "color": "green",
712 | "value": null
713 | },
714 | {
715 | "color": "red",
716 | "value": 80
717 | }
718 | ]
719 | }
720 | },
721 | "overrides": []
722 | },
723 | "gridPos": {
724 | "h": 10,
725 | "w": 4,
726 | "x": 14,
727 | "y": 23
728 | },
729 | "id": 20,
730 | "options": {
731 | "colorMode": "value",
732 | "graphMode": "none",
733 | "justifyMode": "auto",
734 | "orientation": "auto",
735 | "reduceOptions": {
736 | "calcs": [
737 | "last"
738 | ],
739 | "fields": "",
740 | "values": false
741 | },
742 | "text": {},
743 | "textMode": "auto"
744 | },
745 | "pluginVersion": "7.5.3",
746 | "targets": [
747 | {
748 | "exemplar": true,
749 | "expr": "go_threads{job=\"metro/scanexporter-scan-exporter-chart\"}",
750 | "interval": "",
751 | "legendFormat": "",
752 | "refId": "A"
753 | }
754 | ],
755 | "timeFrom": null,
756 | "timeShift": null,
757 | "title": "Threads",
758 | "type": "stat"
759 | },
760 | {
761 | "collapsed": false,
762 | "datasource": "Prometheus",
763 | "gridPos": {
764 | "h": 1,
765 | "w": 24,
766 | "x": 0,
767 | "y": 33
768 | },
769 | "id": 8,
770 | "panels": [],
771 | "title": "Promhttp",
772 | "type": "row"
773 | },
774 | {
775 | "datasource": "${datasource}",
776 | "fieldConfig": {
777 | "defaults": {
778 | "mappings": [],
779 | "thresholds": {
780 | "mode": "absolute",
781 | "steps": [
782 | {
783 | "color": "green",
784 | "value": null
785 | }
786 | ]
787 | }
788 | },
789 | "overrides": []
790 | },
791 | "gridPos": {
792 | "h": 9,
793 | "w": 8,
794 | "x": 0,
795 | "y": 34
796 | },
797 | "id": 22,
798 | "options": {
799 | "colorMode": "value",
800 | "graphMode": "area",
801 | "justifyMode": "auto",
802 | "orientation": "auto",
803 | "reduceOptions": {
804 | "calcs": [
805 | "mean"
806 | ],
807 | "fields": "",
808 | "values": false
809 | },
810 | "text": {},
811 | "textMode": "auto"
812 | },
813 | "pluginVersion": "7.5.3",
814 | "targets": [
815 | {
816 | "exemplar": true,
817 | "expr": "promhttp_metric_handler_requests_total{job=\"metro/scanexporter-scan-exporter-chart\", code=\"200\"}",
818 | "interval": "",
819 | "legendFormat": "HTTP 200",
820 | "refId": "A"
821 | }
822 | ],
823 | "timeFrom": null,
824 | "timeShift": null,
825 | "title": "HTTP 200",
826 | "type": "stat"
827 | },
828 | {
829 | "datasource": "${datasource}",
830 | "fieldConfig": {
831 | "defaults": {
832 | "mappings": [],
833 | "thresholds": {
834 | "mode": "absolute",
835 | "steps": [
836 | {
837 | "color": "green",
838 | "value": null
839 | },
840 | {
841 | "color": "red",
842 | "value": 1
843 | }
844 | ]
845 | }
846 | },
847 | "overrides": []
848 | },
849 | "gridPos": {
850 | "h": 9,
851 | "w": 8,
852 | "x": 8,
853 | "y": 34
854 | },
855 | "id": 24,
856 | "options": {
857 | "colorMode": "value",
858 | "graphMode": "area",
859 | "justifyMode": "auto",
860 | "orientation": "auto",
861 | "reduceOptions": {
862 | "calcs": [
863 | "mean"
864 | ],
865 | "fields": "",
866 | "values": false
867 | },
868 | "text": {},
869 | "textMode": "auto"
870 | },
871 | "pluginVersion": "7.5.3",
872 | "targets": [
873 | {
874 | "exemplar": true,
875 | "expr": "promhttp_metric_handler_requests_total{job=\"metro/scanexporter-scan-exporter-chart\", code=\"500\"}",
876 | "interval": "",
877 | "legendFormat": "HTTP 500",
878 | "refId": "A"
879 | }
880 | ],
881 | "timeFrom": null,
882 | "timeShift": null,
883 | "title": "HTTP 500",
884 | "type": "stat"
885 | },
886 | {
887 | "datasource": "${datasource}",
888 | "fieldConfig": {
889 | "defaults": {
890 | "mappings": [],
891 | "thresholds": {
892 | "mode": "absolute",
893 | "steps": [
894 | {
895 | "color": "green",
896 | "value": null
897 | },
898 | {
899 | "color": "red",
900 | "value": 1
901 | }
902 | ]
903 | }
904 | },
905 | "overrides": []
906 | },
907 | "gridPos": {
908 | "h": 9,
909 | "w": 8,
910 | "x": 16,
911 | "y": 34
912 | },
913 | "id": 26,
914 | "options": {
915 | "colorMode": "value",
916 | "graphMode": "area",
917 | "justifyMode": "auto",
918 | "orientation": "auto",
919 | "reduceOptions": {
920 | "calcs": [
921 | "mean"
922 | ],
923 | "fields": "",
924 | "values": false
925 | },
926 | "text": {},
927 | "textMode": "auto"
928 | },
929 | "pluginVersion": "7.5.3",
930 | "targets": [
931 | {
932 | "exemplar": true,
933 | "expr": "promhttp_metric_handler_requests_total{job=\"metro/scanexporter-scan-exporter-chart\", code=\"503\"}",
934 | "interval": "",
935 | "legendFormat": "HTTP 503",
936 | "refId": "A"
937 | }
938 | ],
939 | "timeFrom": null,
940 | "timeShift": null,
941 | "title": "HTTP 503",
942 | "type": "stat"
943 | }
944 | ],
945 | "refresh": "5s",
946 | "schemaVersion": 27,
947 | "style": "dark",
948 | "tags": [],
949 | "templating": {
950 | "list": [
951 | {
952 | "current": {
953 | "selected": false,
954 | "text": "Prometheus",
955 | "value": "Prometheus"
956 | },
957 | "description": null,
958 | "error": null,
959 | "hide": 0,
960 | "includeAll": false,
961 | "label": "datasource",
962 | "multi": false,
963 | "name": "datasource",
964 | "options": [],
965 | "query": "prometheus",
966 | "queryValue": "",
967 | "refresh": 1,
968 | "regex": "",
969 | "skipUrlSync": false,
970 | "type": "datasource"
971 | }
972 | ]
973 | },
974 | "time": {
975 | "from": "now-3h",
976 | "to": "now"
977 | },
978 | "timepicker": {},
979 | "timezone": "",
980 | "title": "Scan-Exporter / Internals",
981 | "uid": "FqShpwYMk",
982 | "version": 1
983 | }
--------------------------------------------------------------------------------
/dashboards/scans.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 | "iteration": 1621845596523,
19 | "links": [],
20 | "panels": [
21 | {
22 | "datasource": "Prometheus",
23 | "gridPos": {
24 | "h": 1,
25 | "w": 24,
26 | "x": 0,
27 | "y": 0
28 | },
29 | "id": 10,
30 | "title": "Summary",
31 | "type": "row"
32 | },
33 | {
34 | "datasource": "Prometheus",
35 | "fieldConfig": {
36 | "defaults": {
37 | "mappings": [],
38 | "thresholds": {
39 | "mode": "absolute",
40 | "steps": [
41 | {
42 | "color": "super-light-purple",
43 | "value": null
44 | }
45 | ]
46 | }
47 | },
48 | "overrides": []
49 | },
50 | "gridPos": {
51 | "h": 9,
52 | "w": 6,
53 | "x": 0,
54 | "y": 1
55 | },
56 | "id": 6,
57 | "options": {
58 | "colorMode": "value",
59 | "graphMode": "none",
60 | "justifyMode": "auto",
61 | "orientation": "auto",
62 | "reduceOptions": {
63 | "calcs": [
64 | "last"
65 | ],
66 | "fields": "",
67 | "values": false
68 | },
69 | "text": {},
70 | "textMode": "auto"
71 | },
72 | "pluginVersion": "7.5.5",
73 | "targets": [
74 | {
75 | "expr": "scanexporter_targets_number_total",
76 | "interval": "",
77 | "legendFormat": "",
78 | "refId": "A"
79 | }
80 | ],
81 | "timeFrom": null,
82 | "timeShift": null,
83 | "title": "Number of targets",
84 | "type": "stat"
85 | },
86 | {
87 | "aliasColors": {},
88 | "bars": false,
89 | "dashLength": 10,
90 | "dashes": false,
91 | "datasource": "Prometheus",
92 | "description": "Number of ports that are open for each target.",
93 | "fieldConfig": {
94 | "defaults": {},
95 | "overrides": []
96 | },
97 | "fill": 1,
98 | "fillGradient": 0,
99 | "gridPos": {
100 | "h": 9,
101 | "w": 9,
102 | "x": 6,
103 | "y": 1
104 | },
105 | "hiddenSeries": false,
106 | "id": 18,
107 | "legend": {
108 | "avg": false,
109 | "current": false,
110 | "max": false,
111 | "min": false,
112 | "show": true,
113 | "total": false,
114 | "values": false
115 | },
116 | "lines": true,
117 | "linewidth": 1,
118 | "nullPointMode": "null",
119 | "options": {
120 | "alertThreshold": true
121 | },
122 | "percentage": false,
123 | "pluginVersion": "7.5.5",
124 | "pointradius": 2,
125 | "points": false,
126 | "renderer": "flot",
127 | "seriesOverrides": [],
128 | "spaceLength": 10,
129 | "stack": false,
130 | "steppedLine": false,
131 | "targets": [
132 | {
133 | "expr": "scanexporter_open_ports_total >= 1",
134 | "interval": "",
135 | "legendFormat": "{{name}} ({{ip}})",
136 | "refId": "A"
137 | }
138 | ],
139 | "thresholds": [],
140 | "timeFrom": null,
141 | "timeRegions": [],
142 | "timeShift": null,
143 | "title": "Open ports",
144 | "tooltip": {
145 | "shared": true,
146 | "sort": 0,
147 | "value_type": "individual"
148 | },
149 | "type": "graph",
150 | "xaxis": {
151 | "buckets": null,
152 | "mode": "time",
153 | "name": null,
154 | "show": true,
155 | "values": []
156 | },
157 | "yaxes": [
158 | {
159 | "decimals": 0,
160 | "format": "short",
161 | "label": null,
162 | "logBase": 1,
163 | "max": null,
164 | "min": "0",
165 | "show": true
166 | },
167 | {
168 | "format": "short",
169 | "label": null,
170 | "logBase": 1,
171 | "max": null,
172 | "min": null,
173 | "show": true
174 | }
175 | ],
176 | "yaxis": {
177 | "align": false,
178 | "alignLevel": null
179 | }
180 | },
181 | {
182 | "aliasColors": {},
183 | "bars": false,
184 | "dashLength": 10,
185 | "dashes": false,
186 | "datasource": "Prometheus",
187 | "description": "Number of ports that are open, and shouldn't be, for each target.",
188 | "fieldConfig": {
189 | "defaults": {},
190 | "overrides": []
191 | },
192 | "fill": 1,
193 | "fillGradient": 0,
194 | "gridPos": {
195 | "h": 9,
196 | "w": 9,
197 | "x": 15,
198 | "y": 1
199 | },
200 | "hiddenSeries": false,
201 | "id": 19,
202 | "legend": {
203 | "avg": false,
204 | "current": false,
205 | "max": false,
206 | "min": false,
207 | "show": true,
208 | "total": false,
209 | "values": false
210 | },
211 | "lines": true,
212 | "linewidth": 1,
213 | "nullPointMode": "null",
214 | "options": {
215 | "alertThreshold": true
216 | },
217 | "percentage": false,
218 | "pluginVersion": "7.5.5",
219 | "pointradius": 2,
220 | "points": false,
221 | "renderer": "flot",
222 | "seriesOverrides": [],
223 | "spaceLength": 10,
224 | "stack": false,
225 | "steppedLine": false,
226 | "targets": [
227 | {
228 | "expr": "scanexporter_unexpected_open_ports_total >= 1",
229 | "interval": "",
230 | "legendFormat": "{{name}} ({{ip}})",
231 | "refId": "A"
232 | }
233 | ],
234 | "thresholds": [],
235 | "timeFrom": null,
236 | "timeRegions": [],
237 | "timeShift": null,
238 | "title": "Unexpected open ports",
239 | "tooltip": {
240 | "shared": true,
241 | "sort": 0,
242 | "value_type": "individual"
243 | },
244 | "type": "graph",
245 | "xaxis": {
246 | "buckets": null,
247 | "mode": "time",
248 | "name": null,
249 | "show": true,
250 | "values": []
251 | },
252 | "yaxes": [
253 | {
254 | "decimals": 0,
255 | "format": "short",
256 | "label": null,
257 | "logBase": 1,
258 | "max": null,
259 | "min": "0",
260 | "show": true
261 | },
262 | {
263 | "format": "short",
264 | "label": null,
265 | "logBase": 1,
266 | "max": null,
267 | "min": null,
268 | "show": true
269 | }
270 | ],
271 | "yaxis": {
272 | "align": false,
273 | "alignLevel": null
274 | }
275 | },
276 | {
277 | "datasource": "Prometheus",
278 | "fieldConfig": {
279 | "defaults": {
280 | "mappings": [],
281 | "thresholds": {
282 | "mode": "absolute",
283 | "steps": [
284 | {
285 | "color": "green",
286 | "value": null
287 | }
288 | ]
289 | }
290 | },
291 | "overrides": []
292 | },
293 | "gridPos": {
294 | "h": 8,
295 | "w": 3,
296 | "x": 0,
297 | "y": 10
298 | },
299 | "id": 2,
300 | "options": {
301 | "orientation": "auto",
302 | "reduceOptions": {
303 | "calcs": [
304 | "last"
305 | ],
306 | "fields": "",
307 | "values": false
308 | },
309 | "showThresholdLabels": false,
310 | "showThresholdMarkers": true,
311 | "text": {}
312 | },
313 | "pluginVersion": "7.5.5",
314 | "targets": [
315 | {
316 | "expr": "scanexporter_pending_scans",
317 | "interval": "",
318 | "legendFormat": "",
319 | "refId": "A"
320 | }
321 | ],
322 | "timeFrom": null,
323 | "timeShift": null,
324 | "title": "Pending scans",
325 | "type": "gauge"
326 | },
327 | {
328 | "datasource": "Prometheus",
329 | "fieldConfig": {
330 | "defaults": {
331 | "decimals": 0,
332 | "mappings": [],
333 | "thresholds": {
334 | "mode": "absolute",
335 | "steps": [
336 | {
337 | "color": "green",
338 | "value": null
339 | },
340 | {
341 | "color": "red",
342 | "value": 1
343 | }
344 | ]
345 | }
346 | },
347 | "overrides": []
348 | },
349 | "gridPos": {
350 | "h": 8,
351 | "w": 3,
352 | "x": 3,
353 | "y": 10
354 | },
355 | "id": 4,
356 | "options": {
357 | "colorMode": "value",
358 | "graphMode": "area",
359 | "justifyMode": "auto",
360 | "orientation": "auto",
361 | "reduceOptions": {
362 | "calcs": [
363 | "last"
364 | ],
365 | "fields": "",
366 | "values": false
367 | },
368 | "text": {},
369 | "textMode": "auto"
370 | },
371 | "pluginVersion": "7.5.5",
372 | "targets": [
373 | {
374 | "expr": "scanexporter_icmp_not_responding_total",
375 | "interval": "",
376 | "legendFormat": "",
377 | "refId": "A"
378 | }
379 | ],
380 | "timeFrom": null,
381 | "timeShift": null,
382 | "title": "ICMP not responding",
383 | "type": "stat"
384 | },
385 | {
386 | "aliasColors": {},
387 | "bars": false,
388 | "dashLength": 10,
389 | "dashes": false,
390 | "datasource": "Prometheus",
391 | "description": "Number of ports that are closed, and shouldn't be, for each target.",
392 | "fieldConfig": {
393 | "defaults": {},
394 | "overrides": []
395 | },
396 | "fill": 1,
397 | "fillGradient": 0,
398 | "gridPos": {
399 | "h": 8,
400 | "w": 9,
401 | "x": 6,
402 | "y": 10
403 | },
404 | "hiddenSeries": false,
405 | "id": 17,
406 | "legend": {
407 | "avg": false,
408 | "current": false,
409 | "max": false,
410 | "min": false,
411 | "show": true,
412 | "total": false,
413 | "values": false
414 | },
415 | "lines": true,
416 | "linewidth": 1,
417 | "nullPointMode": "null",
418 | "options": {
419 | "alertThreshold": true
420 | },
421 | "percentage": false,
422 | "pluginVersion": "7.5.5",
423 | "pointradius": 2,
424 | "points": false,
425 | "renderer": "flot",
426 | "seriesOverrides": [],
427 | "spaceLength": 10,
428 | "stack": false,
429 | "steppedLine": false,
430 | "targets": [
431 | {
432 | "expr": "scanexporter_unexpected_closed_ports_total >= 1",
433 | "interval": "",
434 | "legendFormat": "{{name}} ({{ip}})",
435 | "refId": "A"
436 | }
437 | ],
438 | "thresholds": [],
439 | "timeFrom": null,
440 | "timeRegions": [],
441 | "timeShift": null,
442 | "title": "Unexpected closed ports",
443 | "tooltip": {
444 | "shared": true,
445 | "sort": 0,
446 | "value_type": "individual"
447 | },
448 | "type": "graph",
449 | "xaxis": {
450 | "buckets": null,
451 | "mode": "time",
452 | "name": null,
453 | "show": true,
454 | "values": []
455 | },
456 | "yaxes": [
457 | {
458 | "decimals": 0,
459 | "format": "short",
460 | "label": null,
461 | "logBase": 1,
462 | "max": null,
463 | "min": "0",
464 | "show": true
465 | },
466 | {
467 | "format": "short",
468 | "label": null,
469 | "logBase": 1,
470 | "max": null,
471 | "min": null,
472 | "show": true
473 | }
474 | ],
475 | "yaxis": {
476 | "align": false,
477 | "alignLevel": null
478 | }
479 | },
480 | {
481 | "aliasColors": {},
482 | "bars": false,
483 | "dashLength": 10,
484 | "dashes": false,
485 | "datasource": "Prometheus",
486 | "description": "Number of ports that are in a different state from previous scan, for each target.",
487 | "fieldConfig": {
488 | "defaults": {},
489 | "overrides": []
490 | },
491 | "fill": 1,
492 | "fillGradient": 0,
493 | "gridPos": {
494 | "h": 8,
495 | "w": 9,
496 | "x": 15,
497 | "y": 10
498 | },
499 | "hiddenSeries": false,
500 | "id": 20,
501 | "legend": {
502 | "avg": false,
503 | "current": false,
504 | "max": false,
505 | "min": false,
506 | "show": true,
507 | "total": false,
508 | "values": false
509 | },
510 | "lines": true,
511 | "linewidth": 1,
512 | "nullPointMode": "null",
513 | "options": {
514 | "alertThreshold": true
515 | },
516 | "percentage": false,
517 | "pluginVersion": "7.5.5",
518 | "pointradius": 2,
519 | "points": false,
520 | "renderer": "flot",
521 | "seriesOverrides": [],
522 | "spaceLength": 10,
523 | "stack": false,
524 | "steppedLine": false,
525 | "targets": [
526 | {
527 | "expr": "scanexporter_diff_ports_total >= 1",
528 | "interval": "",
529 | "legendFormat": "{{name}} ({{ip}})",
530 | "refId": "A"
531 | }
532 | ],
533 | "thresholds": [],
534 | "timeFrom": null,
535 | "timeRegions": [],
536 | "timeShift": null,
537 | "title": "Evolution of ports states",
538 | "tooltip": {
539 | "shared": true,
540 | "sort": 0,
541 | "value_type": "individual"
542 | },
543 | "type": "graph",
544 | "xaxis": {
545 | "buckets": null,
546 | "mode": "time",
547 | "name": null,
548 | "show": true,
549 | "values": []
550 | },
551 | "yaxes": [
552 | {
553 | "decimals": 0,
554 | "format": "short",
555 | "label": null,
556 | "logBase": 1,
557 | "max": null,
558 | "min": "0",
559 | "show": true
560 | },
561 | {
562 | "format": "short",
563 | "label": null,
564 | "logBase": 1,
565 | "max": null,
566 | "min": null,
567 | "show": true
568 | }
569 | ],
570 | "yaxis": {
571 | "align": false,
572 | "alignLevel": null
573 | }
574 | },
575 | {
576 | "datasource": "loki",
577 | "fieldConfig": {
578 | "defaults": {},
579 | "overrides": []
580 | },
581 | "gridPos": {
582 | "h": 8,
583 | "w": 12,
584 | "x": 0,
585 | "y": 18
586 | },
587 | "id": 26,
588 | "options": {
589 | "dedupStrategy": "none",
590 | "showLabels": false,
591 | "showTime": false,
592 | "sortOrder": "Descending",
593 | "wrapLogMessage": true
594 | },
595 | "pluginVersion": "7.5.3",
596 | "targets": [
597 | {
598 | "expr": "{app=\"scanexporter-scan-exporter-chart\"} | json | level=\"warn\" | line_format \" {{.time}} - [{{.level}}] {{.message}}\"",
599 | "refId": "A"
600 | }
601 | ],
602 | "title": "Warnings from logs",
603 | "type": "logs"
604 | },
605 | {
606 | "datasource": "loki",
607 | "fieldConfig": {
608 | "defaults": {},
609 | "overrides": []
610 | },
611 | "gridPos": {
612 | "h": 8,
613 | "w": 12,
614 | "x": 12,
615 | "y": 18
616 | },
617 | "id": 28,
618 | "options": {
619 | "dedupStrategy": "none",
620 | "showLabels": false,
621 | "showTime": true,
622 | "sortOrder": "Descending",
623 | "wrapLogMessage": true
624 | },
625 | "pluginVersion": "7.5.3",
626 | "targets": [
627 | {
628 | "expr": "{app=\"scanexporter-scan-exporter-chart\"} | json | level=\"error\" | line_format \" {{.time}} - [{{.level}}] {{.message}}\"",
629 | "refId": "A"
630 | }
631 | ],
632 | "title": "Errors from logs",
633 | "type": "logs"
634 | },
635 | {
636 | "aliasColors": {},
637 | "bars": false,
638 | "dashLength": 10,
639 | "dashes": false,
640 | "datasource": "${datasource}",
641 | "fieldConfig": {
642 | "defaults": {},
643 | "overrides": []
644 | },
645 | "fill": 0,
646 | "fillGradient": 0,
647 | "gridPos": {
648 | "h": 8,
649 | "w": 12,
650 | "x": 6,
651 | "y": 26
652 | },
653 | "hiddenSeries": false,
654 | "id": 30,
655 | "legend": {
656 | "avg": false,
657 | "current": false,
658 | "max": false,
659 | "min": false,
660 | "show": true,
661 | "total": false,
662 | "values": false
663 | },
664 | "lines": true,
665 | "linewidth": 1,
666 | "nullPointMode": "null",
667 | "options": {
668 | "alertThreshold": true
669 | },
670 | "percentage": false,
671 | "pluginVersion": "7.5.5",
672 | "pointradius": 2,
673 | "points": false,
674 | "renderer": "flot",
675 | "seriesOverrides": [],
676 | "spaceLength": 10,
677 | "stack": false,
678 | "steppedLine": false,
679 | "targets": [
680 | {
681 | "exemplar": true,
682 | "expr": "scanexporter_rtt_total",
683 | "interval": "",
684 | "legendFormat": "{{name}} ({{ip}})",
685 | "refId": "A"
686 | }
687 | ],
688 | "thresholds": [],
689 | "timeRegions": [],
690 | "title": "RTT",
691 | "tooltip": {
692 | "shared": true,
693 | "sort": 0,
694 | "value_type": "individual"
695 | },
696 | "type": "graph",
697 | "xaxis": {
698 | "buckets": null,
699 | "mode": "time",
700 | "name": null,
701 | "show": true,
702 | "values": []
703 | },
704 | "yaxes": [
705 | {
706 | "$$hashKey": "object:105",
707 | "format": "ns",
708 | "label": null,
709 | "logBase": 1,
710 | "max": null,
711 | "min": null,
712 | "show": true
713 | },
714 | {
715 | "$$hashKey": "object:106",
716 | "format": "short",
717 | "label": null,
718 | "logBase": 1,
719 | "max": null,
720 | "min": null,
721 | "show": true
722 | }
723 | ],
724 | "yaxis": {
725 | "align": false,
726 | "alignLevel": null
727 | }
728 | },
729 | {
730 | "collapsed": false,
731 | "datasource": "Prometheus",
732 | "gridPos": {
733 | "h": 1,
734 | "w": 24,
735 | "x": 0,
736 | "y": 34
737 | },
738 | "id": 12,
739 | "panels": [],
740 | "title": "Target",
741 | "type": "row"
742 | },
743 | {
744 | "datasource": "Prometheus",
745 | "description": "Number of ports that are open for each target.",
746 | "fieldConfig": {
747 | "defaults": {
748 | "mappings": [],
749 | "thresholds": {
750 | "mode": "absolute",
751 | "steps": [
752 | {
753 | "color": "green",
754 | "value": null
755 | }
756 | ]
757 | }
758 | },
759 | "overrides": []
760 | },
761 | "gridPos": {
762 | "h": 11,
763 | "w": 4,
764 | "x": 0,
765 | "y": 35
766 | },
767 | "id": 14,
768 | "options": {
769 | "colorMode": "value",
770 | "graphMode": "none",
771 | "justifyMode": "center",
772 | "orientation": "auto",
773 | "reduceOptions": {
774 | "calcs": [
775 | "last"
776 | ],
777 | "fields": "",
778 | "values": false
779 | },
780 | "text": {},
781 | "textMode": "value_and_name"
782 | },
783 | "pluginVersion": "7.5.5",
784 | "targets": [
785 | {
786 | "expr": "scanexporter_open_ports_total{name=\"$target\"}",
787 | "interval": "",
788 | "legendFormat": "{{name}} ({{ip}})",
789 | "refId": "A"
790 | }
791 | ],
792 | "timeFrom": null,
793 | "timeShift": null,
794 | "title": "Open ports",
795 | "type": "stat"
796 | },
797 | {
798 | "aliasColors": {},
799 | "bars": false,
800 | "dashLength": 10,
801 | "dashes": false,
802 | "datasource": "Prometheus",
803 | "description": "Number of ports that are open for each target.",
804 | "fieldConfig": {
805 | "defaults": {},
806 | "overrides": []
807 | },
808 | "fill": 1,
809 | "fillGradient": 0,
810 | "gridPos": {
811 | "h": 11,
812 | "w": 8,
813 | "x": 4,
814 | "y": 35
815 | },
816 | "hiddenSeries": false,
817 | "id": 8,
818 | "legend": {
819 | "avg": false,
820 | "current": false,
821 | "max": false,
822 | "min": false,
823 | "show": true,
824 | "total": false,
825 | "values": false
826 | },
827 | "lines": true,
828 | "linewidth": 1,
829 | "nullPointMode": "null",
830 | "options": {
831 | "alertThreshold": true
832 | },
833 | "percentage": false,
834 | "pluginVersion": "7.5.5",
835 | "pointradius": 2,
836 | "points": false,
837 | "renderer": "flot",
838 | "seriesOverrides": [],
839 | "spaceLength": 10,
840 | "stack": false,
841 | "steppedLine": false,
842 | "targets": [
843 | {
844 | "exemplar": true,
845 | "expr": "scanexporter_open_ports_total{name=\"$target\"}",
846 | "interval": "",
847 | "legendFormat": "{{name}} ({{ip}})",
848 | "refId": "A"
849 | }
850 | ],
851 | "thresholds": [],
852 | "timeFrom": null,
853 | "timeRegions": [],
854 | "timeShift": null,
855 | "title": "Open ports",
856 | "tooltip": {
857 | "shared": true,
858 | "sort": 0,
859 | "value_type": "individual"
860 | },
861 | "type": "graph",
862 | "xaxis": {
863 | "buckets": null,
864 | "mode": "time",
865 | "name": null,
866 | "show": true,
867 | "values": []
868 | },
869 | "yaxes": [
870 | {
871 | "$$hashKey": "object:174",
872 | "decimals": 0,
873 | "format": "short",
874 | "label": null,
875 | "logBase": 1,
876 | "max": null,
877 | "min": "0",
878 | "show": true
879 | },
880 | {
881 | "$$hashKey": "object:175",
882 | "format": "short",
883 | "label": null,
884 | "logBase": 1,
885 | "max": null,
886 | "min": null,
887 | "show": true
888 | }
889 | ],
890 | "yaxis": {
891 | "align": false,
892 | "alignLevel": null
893 | }
894 | },
895 | {
896 | "datasource": "Prometheus",
897 | "description": "Number of ports that are open, and shouldn't be, for each target.",
898 | "fieldConfig": {
899 | "defaults": {
900 | "mappings": [],
901 | "thresholds": {
902 | "mode": "absolute",
903 | "steps": [
904 | {
905 | "color": "green",
906 | "value": null
907 | },
908 | {
909 | "color": "semi-dark-red",
910 | "value": 1
911 | }
912 | ]
913 | }
914 | },
915 | "overrides": []
916 | },
917 | "gridPos": {
918 | "h": 11,
919 | "w": 4,
920 | "x": 12,
921 | "y": 35
922 | },
923 | "id": 22,
924 | "options": {
925 | "colorMode": "value",
926 | "graphMode": "none",
927 | "justifyMode": "center",
928 | "orientation": "auto",
929 | "reduceOptions": {
930 | "calcs": [
931 | "last"
932 | ],
933 | "fields": "",
934 | "values": false
935 | },
936 | "text": {},
937 | "textMode": "value_and_name"
938 | },
939 | "pluginVersion": "7.5.5",
940 | "targets": [
941 | {
942 | "expr": "scanexporter_unexpected_open_ports_total{name=\"$target\"}",
943 | "interval": "",
944 | "legendFormat": "{{name}} ({{ip}})",
945 | "refId": "A"
946 | }
947 | ],
948 | "timeFrom": null,
949 | "timeShift": null,
950 | "title": "Unexpected open ports",
951 | "type": "stat"
952 | },
953 | {
954 | "aliasColors": {},
955 | "bars": false,
956 | "dashLength": 10,
957 | "dashes": false,
958 | "datasource": "Prometheus",
959 | "description": "Number of ports that are open, and shouldn't be, for each target.",
960 | "fieldConfig": {
961 | "defaults": {},
962 | "overrides": []
963 | },
964 | "fill": 1,
965 | "fillGradient": 0,
966 | "gridPos": {
967 | "h": 11,
968 | "w": 8,
969 | "x": 16,
970 | "y": 35
971 | },
972 | "hiddenSeries": false,
973 | "id": 21,
974 | "legend": {
975 | "avg": false,
976 | "current": false,
977 | "max": false,
978 | "min": false,
979 | "show": true,
980 | "total": false,
981 | "values": false
982 | },
983 | "lines": true,
984 | "linewidth": 1,
985 | "nullPointMode": "null",
986 | "options": {
987 | "alertThreshold": true
988 | },
989 | "percentage": false,
990 | "pluginVersion": "7.5.5",
991 | "pointradius": 2,
992 | "points": false,
993 | "renderer": "flot",
994 | "seriesOverrides": [],
995 | "spaceLength": 10,
996 | "stack": false,
997 | "steppedLine": false,
998 | "targets": [
999 | {
1000 | "expr": "scanexporter_unexpected_open_ports_total{name=\"$target\"}",
1001 | "interval": "",
1002 | "legendFormat": "{{name}} ({{ip}})",
1003 | "refId": "A"
1004 | }
1005 | ],
1006 | "thresholds": [],
1007 | "timeFrom": null,
1008 | "timeRegions": [],
1009 | "timeShift": null,
1010 | "title": "Unexpected open ports",
1011 | "tooltip": {
1012 | "shared": true,
1013 | "sort": 0,
1014 | "value_type": "individual"
1015 | },
1016 | "type": "graph",
1017 | "xaxis": {
1018 | "buckets": null,
1019 | "mode": "time",
1020 | "name": null,
1021 | "show": true,
1022 | "values": []
1023 | },
1024 | "yaxes": [
1025 | {
1026 | "decimals": 0,
1027 | "format": "short",
1028 | "label": null,
1029 | "logBase": 1,
1030 | "max": null,
1031 | "min": "0",
1032 | "show": true
1033 | },
1034 | {
1035 | "format": "short",
1036 | "label": null,
1037 | "logBase": 1,
1038 | "max": null,
1039 | "min": null,
1040 | "show": true
1041 | }
1042 | ],
1043 | "yaxis": {
1044 | "align": false,
1045 | "alignLevel": null
1046 | }
1047 | },
1048 | {
1049 | "datasource": "Prometheus",
1050 | "description": "Number of ports that are closed, and shouldn't be, for each target.",
1051 | "fieldConfig": {
1052 | "defaults": {
1053 | "mappings": [],
1054 | "thresholds": {
1055 | "mode": "absolute",
1056 | "steps": [
1057 | {
1058 | "color": "green",
1059 | "value": null
1060 | },
1061 | {
1062 | "color": "semi-dark-red",
1063 | "value": 1
1064 | }
1065 | ]
1066 | }
1067 | },
1068 | "overrides": []
1069 | },
1070 | "gridPos": {
1071 | "h": 11,
1072 | "w": 4,
1073 | "x": 0,
1074 | "y": 46
1075 | },
1076 | "id": 15,
1077 | "options": {
1078 | "colorMode": "value",
1079 | "graphMode": "none",
1080 | "justifyMode": "center",
1081 | "orientation": "auto",
1082 | "reduceOptions": {
1083 | "calcs": [
1084 | "last"
1085 | ],
1086 | "fields": "",
1087 | "values": false
1088 | },
1089 | "text": {},
1090 | "textMode": "value_and_name"
1091 | },
1092 | "pluginVersion": "7.5.5",
1093 | "targets": [
1094 | {
1095 | "expr": "scanexporter_unexpected_closed_ports_total{name=\"$target\"}",
1096 | "interval": "",
1097 | "legendFormat": "{{name}} ({{ip}})",
1098 | "refId": "A"
1099 | }
1100 | ],
1101 | "timeFrom": null,
1102 | "timeShift": null,
1103 | "title": "Unexpected closed ports",
1104 | "type": "stat"
1105 | },
1106 | {
1107 | "aliasColors": {},
1108 | "bars": false,
1109 | "dashLength": 10,
1110 | "dashes": false,
1111 | "datasource": "Prometheus",
1112 | "description": "Number of ports that are closed, and shouldn't be, for each target.",
1113 | "fieldConfig": {
1114 | "defaults": {},
1115 | "overrides": []
1116 | },
1117 | "fill": 1,
1118 | "fillGradient": 0,
1119 | "gridPos": {
1120 | "h": 11,
1121 | "w": 8,
1122 | "x": 4,
1123 | "y": 46
1124 | },
1125 | "hiddenSeries": false,
1126 | "id": 13,
1127 | "legend": {
1128 | "avg": false,
1129 | "current": false,
1130 | "max": false,
1131 | "min": false,
1132 | "show": true,
1133 | "total": false,
1134 | "values": false
1135 | },
1136 | "lines": true,
1137 | "linewidth": 1,
1138 | "nullPointMode": "null",
1139 | "options": {
1140 | "alertThreshold": true
1141 | },
1142 | "percentage": false,
1143 | "pluginVersion": "7.5.5",
1144 | "pointradius": 2,
1145 | "points": false,
1146 | "renderer": "flot",
1147 | "seriesOverrides": [],
1148 | "spaceLength": 10,
1149 | "stack": false,
1150 | "steppedLine": false,
1151 | "targets": [
1152 | {
1153 | "expr": "scanexporter_unexpected_closed_ports_total{name=\"$target\"}",
1154 | "interval": "",
1155 | "legendFormat": "{{name}} ({{ip}})",
1156 | "refId": "A"
1157 | }
1158 | ],
1159 | "thresholds": [
1160 | {
1161 | "colorMode": "critical",
1162 | "fill": false,
1163 | "line": false,
1164 | "op": "gt",
1165 | "value": 0,
1166 | "yaxis": "left"
1167 | }
1168 | ],
1169 | "timeFrom": null,
1170 | "timeRegions": [],
1171 | "timeShift": null,
1172 | "title": "Unexpected closed ports",
1173 | "tooltip": {
1174 | "shared": true,
1175 | "sort": 0,
1176 | "value_type": "individual"
1177 | },
1178 | "type": "graph",
1179 | "xaxis": {
1180 | "buckets": null,
1181 | "mode": "time",
1182 | "name": null,
1183 | "show": true,
1184 | "values": []
1185 | },
1186 | "yaxes": [
1187 | {
1188 | "decimals": 0,
1189 | "format": "short",
1190 | "label": null,
1191 | "logBase": 1,
1192 | "max": null,
1193 | "min": "0",
1194 | "show": true
1195 | },
1196 | {
1197 | "format": "short",
1198 | "label": null,
1199 | "logBase": 1,
1200 | "max": null,
1201 | "min": null,
1202 | "show": true
1203 | }
1204 | ],
1205 | "yaxis": {
1206 | "align": false,
1207 | "alignLevel": null
1208 | }
1209 | },
1210 | {
1211 | "datasource": "Prometheus",
1212 | "description": "Number of ports that are in a different state from previous scan, for each target.",
1213 | "fieldConfig": {
1214 | "defaults": {
1215 | "mappings": [],
1216 | "thresholds": {
1217 | "mode": "absolute",
1218 | "steps": [
1219 | {
1220 | "color": "green",
1221 | "value": null
1222 | },
1223 | {
1224 | "color": "semi-dark-orange",
1225 | "value": 1
1226 | }
1227 | ]
1228 | }
1229 | },
1230 | "overrides": []
1231 | },
1232 | "gridPos": {
1233 | "h": 11,
1234 | "w": 4,
1235 | "x": 12,
1236 | "y": 46
1237 | },
1238 | "id": 23,
1239 | "options": {
1240 | "colorMode": "value",
1241 | "graphMode": "none",
1242 | "justifyMode": "center",
1243 | "orientation": "auto",
1244 | "reduceOptions": {
1245 | "calcs": [
1246 | "last"
1247 | ],
1248 | "fields": "",
1249 | "values": false
1250 | },
1251 | "text": {},
1252 | "textMode": "value_and_name"
1253 | },
1254 | "pluginVersion": "7.5.5",
1255 | "targets": [
1256 | {
1257 | "expr": "scanexporter_diff_ports_total{name=\"$target\"}",
1258 | "interval": "",
1259 | "legendFormat": "{{name}} ({{ip}})",
1260 | "refId": "A"
1261 | }
1262 | ],
1263 | "timeFrom": null,
1264 | "timeShift": null,
1265 | "title": "Evolution of ports states",
1266 | "type": "stat"
1267 | },
1268 | {
1269 | "aliasColors": {},
1270 | "bars": false,
1271 | "dashLength": 10,
1272 | "dashes": false,
1273 | "datasource": "Prometheus",
1274 | "description": "Number of ports that are in a different state from previous scan, for each target.",
1275 | "fieldConfig": {
1276 | "defaults": {},
1277 | "overrides": []
1278 | },
1279 | "fill": 1,
1280 | "fillGradient": 0,
1281 | "gridPos": {
1282 | "h": 11,
1283 | "w": 8,
1284 | "x": 16,
1285 | "y": 46
1286 | },
1287 | "hiddenSeries": false,
1288 | "id": 24,
1289 | "legend": {
1290 | "avg": false,
1291 | "current": false,
1292 | "max": false,
1293 | "min": false,
1294 | "show": true,
1295 | "total": false,
1296 | "values": false
1297 | },
1298 | "lines": true,
1299 | "linewidth": 1,
1300 | "nullPointMode": "null",
1301 | "options": {
1302 | "alertThreshold": true
1303 | },
1304 | "percentage": false,
1305 | "pluginVersion": "7.5.5",
1306 | "pointradius": 2,
1307 | "points": false,
1308 | "renderer": "flot",
1309 | "seriesOverrides": [],
1310 | "spaceLength": 10,
1311 | "stack": false,
1312 | "steppedLine": false,
1313 | "targets": [
1314 | {
1315 | "expr": "scanexporter_diff_ports_total{name=\"$target\"}",
1316 | "interval": "",
1317 | "legendFormat": "{{name}} ({{ip}})",
1318 | "refId": "A"
1319 | }
1320 | ],
1321 | "thresholds": [],
1322 | "timeFrom": null,
1323 | "timeRegions": [],
1324 | "timeShift": null,
1325 | "title": "Evolution of ports states",
1326 | "tooltip": {
1327 | "shared": true,
1328 | "sort": 0,
1329 | "value_type": "individual"
1330 | },
1331 | "type": "graph",
1332 | "xaxis": {
1333 | "buckets": null,
1334 | "mode": "time",
1335 | "name": null,
1336 | "show": true,
1337 | "values": []
1338 | },
1339 | "yaxes": [
1340 | {
1341 | "decimals": 0,
1342 | "format": "short",
1343 | "label": null,
1344 | "logBase": 1,
1345 | "max": null,
1346 | "min": "0",
1347 | "show": true
1348 | },
1349 | {
1350 | "format": "short",
1351 | "label": null,
1352 | "logBase": 1,
1353 | "max": null,
1354 | "min": null,
1355 | "show": true
1356 | }
1357 | ],
1358 | "yaxis": {
1359 | "align": false,
1360 | "alignLevel": null
1361 | }
1362 | },
1363 | {
1364 | "aliasColors": {},
1365 | "bars": false,
1366 | "dashLength": 10,
1367 | "dashes": false,
1368 | "datasource": null,
1369 | "fieldConfig": {
1370 | "defaults": {},
1371 | "overrides": []
1372 | },
1373 | "fill": 1,
1374 | "fillGradient": 0,
1375 | "gridPos": {
1376 | "h": 11,
1377 | "w": 12,
1378 | "x": 6,
1379 | "y": 57
1380 | },
1381 | "hiddenSeries": false,
1382 | "id": 32,
1383 | "legend": {
1384 | "avg": false,
1385 | "current": false,
1386 | "max": false,
1387 | "min": false,
1388 | "show": true,
1389 | "total": false,
1390 | "values": false
1391 | },
1392 | "lines": true,
1393 | "linewidth": 1,
1394 | "nullPointMode": "null",
1395 | "options": {
1396 | "alertThreshold": true
1397 | },
1398 | "percentage": false,
1399 | "pluginVersion": "7.5.5",
1400 | "pointradius": 2,
1401 | "points": false,
1402 | "renderer": "flot",
1403 | "seriesOverrides": [],
1404 | "spaceLength": 10,
1405 | "stack": false,
1406 | "steppedLine": false,
1407 | "targets": [
1408 | {
1409 | "exemplar": true,
1410 | "expr": "scanexporter_rtt_total{name=\"$target\"}",
1411 | "interval": "",
1412 | "legendFormat": "{{name}} ({{ip}})",
1413 | "refId": "A"
1414 | }
1415 | ],
1416 | "thresholds": [],
1417 | "timeFrom": null,
1418 | "timeRegions": [],
1419 | "timeShift": null,
1420 | "title": "RTT",
1421 | "tooltip": {
1422 | "shared": true,
1423 | "sort": 0,
1424 | "value_type": "individual"
1425 | },
1426 | "type": "graph",
1427 | "xaxis": {
1428 | "buckets": null,
1429 | "mode": "time",
1430 | "name": null,
1431 | "show": true,
1432 | "values": []
1433 | },
1434 | "yaxes": [
1435 | {
1436 | "$$hashKey": "object:231",
1437 | "format": "ns",
1438 | "label": null,
1439 | "logBase": 1,
1440 | "max": null,
1441 | "min": null,
1442 | "show": true
1443 | },
1444 | {
1445 | "$$hashKey": "object:232",
1446 | "format": "short",
1447 | "label": null,
1448 | "logBase": 1,
1449 | "max": null,
1450 | "min": null,
1451 | "show": true
1452 | }
1453 | ],
1454 | "yaxis": {
1455 | "align": false,
1456 | "alignLevel": null
1457 | }
1458 | }
1459 | ],
1460 | "refresh": "10s",
1461 | "schemaVersion": 27,
1462 | "style": "dark",
1463 | "tags": [],
1464 | "templating": {
1465 | "list": [
1466 | {
1467 | "current": {
1468 | "selected": false,
1469 | "text": "Prometheus",
1470 | "value": "Prometheus"
1471 | },
1472 | "description": null,
1473 | "error": null,
1474 | "hide": 0,
1475 | "includeAll": false,
1476 | "label": "Datasource",
1477 | "multi": false,
1478 | "name": "datasource",
1479 | "options": [],
1480 | "query": "prometheus",
1481 | "queryValue": "",
1482 | "refresh": 1,
1483 | "regex": "",
1484 | "skipUrlSync": false,
1485 | "type": "datasource"
1486 | },
1487 | {
1488 | "allValue": null,
1489 | "datasource": "Prometheus",
1490 | "definition": "label_values(scanexporter_open_ports_total, name)",
1491 | "description": null,
1492 | "error": null,
1493 | "hide": 0,
1494 | "includeAll": false,
1495 | "label": "Target",
1496 | "multi": false,
1497 | "name": "target",
1498 | "options": [],
1499 | "query": {
1500 | "query": "label_values(scanexporter_open_ports_total, name)",
1501 | "refId": "StandardVariableQuery"
1502 | },
1503 | "refresh": 0,
1504 | "regex": "",
1505 | "skipUrlSync": false,
1506 | "sort": 1,
1507 | "tagValuesQuery": "",
1508 | "tags": [],
1509 | "tagsQuery": "",
1510 | "type": "query",
1511 | "useTags": false
1512 | }
1513 | ]
1514 | },
1515 | "time": {
1516 | "from": "now-1h",
1517 | "to": "now"
1518 | },
1519 | "timepicker": {},
1520 | "timezone": "",
1521 | "title": "Scan-Exporter / Scans",
1522 | "uid": "qMrJJwYGz",
1523 | "version": 1
1524 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/devops-works/scan-exporter
2 |
3 | go 1.24.2
4 |
5 | require (
6 | github.com/go-ping/ping v1.2.0
7 | github.com/gorilla/mux v1.8.1
8 | github.com/prometheus/client_golang v1.21.1
9 | github.com/rs/zerolog v1.34.0
10 | golang.org/x/sync v0.12.0
11 | gopkg.in/yaml.v3 v3.0.1
12 | )
13 |
14 | require (
15 | github.com/beorn7/perks v1.0.1 // indirect
16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
17 | github.com/google/uuid v1.6.0 // indirect
18 | github.com/klauspost/compress v1.18.0 // indirect
19 | github.com/kr/text v0.2.0 // indirect
20 | github.com/mattn/go-colorable v0.1.14 // indirect
21 | github.com/mattn/go-isatty v0.0.20 // indirect
22 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
23 | github.com/prometheus/client_model v0.6.1 // indirect
24 | github.com/prometheus/common v0.63.0 // indirect
25 | github.com/prometheus/procfs v0.16.0 // indirect
26 | golang.org/x/net v0.38.0 // indirect
27 | golang.org/x/sys v0.31.0 // indirect
28 | google.golang.org/protobuf v1.36.6 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
10 | github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
11 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
12 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
13 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
14 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
17 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
18 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
19 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
20 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
26 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
27 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
28 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
29 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
30 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
31 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
32 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
33 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
36 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
39 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
40 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
41 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
42 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
43 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
44 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
45 | github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
46 | github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
47 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
48 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
49 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
50 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
51 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
52 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
53 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
54 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
55 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
56 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
57 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
58 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
59 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
60 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
61 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
62 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
64 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
66 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
67 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
68 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
69 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
70 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
71 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
74 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
76 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
77 |
--------------------------------------------------------------------------------
/handlers/handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/gorilla/mux"
10 | "github.com/prometheus/client_golang/prometheus/promhttp"
11 | )
12 |
13 | // HandleFunc fills the router.
14 | func HandleFunc() *mux.Router {
15 | r := mux.NewRouter()
16 | r.Handle("/metrics", promhttp.Handler())
17 | r.Handle("/health", http.HandlerFunc(healthCheckPage))
18 | r.NotFoundHandler = http.HandlerFunc(notFoundPage)
19 |
20 | return r
21 | }
22 |
23 | // notFoundPage set the response header to 404 status and prints an error message.
24 | func notFoundPage(w http.ResponseWriter, r *http.Request) {
25 | w.WriteHeader(http.StatusNotFound)
26 | fmt.Fprint(w, "404 page not found
")
27 | }
28 |
29 | // healthCheckPage handles the /health page.
30 | func healthCheckPage(w http.ResponseWriter, r *http.Request) {
31 | w.WriteHeader(http.StatusOK)
32 | w.Header().Set("Content-Type", "application/json")
33 | fmt.Fprintf(w, `
34 | {
35 | "alive": "true",
36 | "motd": "%s"
37 | }`, motd())
38 | }
39 |
40 | func motd() string {
41 | messages := []string{
42 | "Who the f*ck is Jeff, and why does he have nuclear weapons ?",
43 | "Working as a dancing monkey doesn't make you an anarchist.",
44 | "Seek cake now.",
45 | "How can you ensure yourself that a hairdresser isn't a robot ?",
46 | "Accept a monkey.",
47 | "Reject humanity, return to monke.",
48 | "Did you drink enough water today ?",
49 | "Pigeons are our closest relatives.",
50 | "Don't attempt to dryhump what appears to be undryhumpable.",
51 | "Being an adult can be very similar to being a contract killer.",
52 | }
53 | rand.Seed(time.Now().UnixNano())
54 | n := rand.Intn(len(messages))
55 | return messages[n]
56 | }
57 |
--------------------------------------------------------------------------------
/handlers/handlers_test.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func Test_notFoundPage(t *testing.T) {
11 | req, err := http.NewRequest("GET", "/doesnotexist", nil)
12 | if err != nil {
13 | t.Fatal(err)
14 | }
15 |
16 | rr := httptest.NewRecorder()
17 | handler := http.HandlerFunc(notFoundPage)
18 |
19 | handler.ServeHTTP(rr, req)
20 |
21 | if status := rr.Code; status != http.StatusNotFound {
22 | t.Errorf("handler returned wrong status code: got %v want %v",
23 | status, http.StatusOK)
24 | }
25 |
26 | expected := `404 page not found
`
27 | if rr.Body.String() != expected {
28 | t.Errorf("handler returned unexpected body: got %v want %v",
29 | rr.Body.String(), expected)
30 | }
31 | }
32 |
33 | func Test_healthCheckPage(t *testing.T) {
34 | req, err := http.NewRequest("GET", "/health", nil)
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 |
39 | rr := httptest.NewRecorder()
40 | handler := http.HandlerFunc(healthCheckPage)
41 |
42 | handler.ServeHTTP(rr, req)
43 |
44 | if status := rr.Code; status != http.StatusOK {
45 | t.Errorf("handler returned wrong status code: got %v want %v",
46 | status, http.StatusOK)
47 | }
48 |
49 | healthStatus := `"alive": "true"`
50 | if !strings.Contains(rr.Body.String(), healthStatus) {
51 | t.Errorf("handler returned unexpected body: got %v want %v",
52 | rr.Body.String(), healthStatus)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/img/gobug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devops-works/scan-exporter/9668200a97f50fad4d43a1a7de0acc636b5feb8f/img/gobug.png
--------------------------------------------------------------------------------
/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/rs/zerolog"
7 | "github.com/rs/zerolog/log"
8 | )
9 |
10 | // New creates a new zerolog logger
11 | func New(level string) zerolog.Logger {
12 | lvl, err := zerolog.ParseLevel(level)
13 | if err != nil {
14 | log.Error().Msgf("cannot parse level %s, using 'info'", level)
15 | lvl = zerolog.InfoLevel
16 | }
17 | zerolog.SetGlobalLevel(lvl)
18 | logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
19 | return logger
20 | }
21 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/devops-works/scan-exporter/config"
10 | "github.com/devops-works/scan-exporter/logger"
11 | "github.com/devops-works/scan-exporter/metrics"
12 | "github.com/devops-works/scan-exporter/pprof"
13 | "github.com/devops-works/scan-exporter/scan"
14 | "github.com/rs/zerolog/log"
15 | )
16 |
17 | var (
18 | // Version holds the build version
19 | Version string
20 | // BuildDate holds the build date
21 | BuildDate string
22 | )
23 |
24 | func main() {
25 | if err := run(os.Args, os.Stdout); err != nil {
26 | log.Fatal().Err(err).Msgf("error running %s", os.Args[0])
27 | os.Exit(1)
28 | }
29 | }
30 |
31 | func run(args []string, stdout io.Writer) error {
32 | var confFile, pprofAddr, metricAddr, loglvl string
33 | flag.StringVar(&confFile, "config", "config.yaml", "path to config file")
34 | flag.StringVar(&pprofAddr, "pprof.addr", "", "pprof addr")
35 | flag.StringVar(&metricAddr, "metric.addr", ":2112", "metric server addr")
36 | flag.StringVar(&loglvl, "log.lvl", "debug", "log level. Can be {trace,debug,info,warn,error,fatal}")
37 | flag.Parse()
38 |
39 | fmt.Printf("scan-exporter version %s (built %s)\n", Version, BuildDate)
40 |
41 | // Start pprof server is asked.
42 | if pprofAddr != "" {
43 | pprofServer, err := pprof.New(pprofAddr)
44 | if err != nil {
45 | log.Fatal().Err(err).Msg("unable to create pprof server")
46 | }
47 | log.Info().Msgf("pprof started on 'http://%s'", pprofServer.Addr)
48 |
49 | go pprofServer.Run()
50 | }
51 |
52 | // Parse configuration file
53 | c, err := config.New(confFile)
54 | if err != nil {
55 | log.Fatal().Msgf("error reading %s: %s", confFile, err)
56 | }
57 |
58 | // Set global loglevel
59 | // Overwrite the flag loglevel by the one given in configuration
60 | if c.LogLevel != "" {
61 | log.Info().Msgf("log level from configuration file found: %s", c.LogLevel)
62 | loglvl = c.LogLevel
63 | }
64 |
65 | // Create scanner
66 | scanner := scan.Scanner{
67 | Logger: logger.New(loglvl),
68 | }
69 |
70 | // Create metrics server
71 | scanner.MetricsServ = *metrics.Init(metricAddr)
72 |
73 | // Start metrics server
74 | go func() {
75 | if err := scanner.MetricsServ.Start(); err != nil {
76 | scanner.Logger.Fatal().Err(err).Msg("metrics server failed critically")
77 | }
78 | }()
79 |
80 | if err := scanner.Start(c); err != nil {
81 | return err
82 | }
83 | return nil
84 | }
85 |
--------------------------------------------------------------------------------
/metrics/metrics.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/devops-works/scan-exporter/common"
8 | "github.com/devops-works/scan-exporter/handlers"
9 | "github.com/prometheus/client_golang/prometheus"
10 | "github.com/rs/zerolog/log"
11 | )
12 |
13 | // Server is the metrics server. It contains all the Prometheus metrics
14 | type Server struct {
15 | Addr string
16 | NotRespondingList map[string]bool
17 | NumOfTargets, PendingScans, NumOfDownTargets, Uptime prometheus.Gauge
18 | UnexpectedPorts, OpenPorts, ClosedPorts, DiffPorts, Rtt *prometheus.GaugeVec
19 | }
20 |
21 | // NewMetrics is the type that will transit between scan and metrics. It carries
22 | // informations that will be used for calculation, such as expected ports.
23 | type NewMetrics struct {
24 | Name string
25 | IP string
26 | Diff int
27 | Open []string
28 | Closed []string
29 | Expected []string
30 | }
31 |
32 | // PingInfo holds the ping update of a specific target
33 | type PingInfo struct {
34 | Name string
35 | IP string
36 | IsResponding bool
37 | RTT time.Duration
38 | }
39 |
40 | // Init initialize the metrics
41 | func Init(addr string) *Server {
42 | s := Server{
43 | NumOfTargets: prometheus.NewGauge(prometheus.GaugeOpts{
44 | Name: "scanexporter_targets_number_total",
45 | Help: "Number of targets detected in config file.",
46 | }),
47 |
48 | PendingScans: prometheus.NewGauge(prometheus.GaugeOpts{
49 | Name: "scanexporter_pending_scans",
50 | Help: "Number of scans in the waiting line.",
51 | }),
52 |
53 | Uptime: prometheus.NewGauge(prometheus.GaugeOpts{
54 | Name: "scanexporter_uptime_sec",
55 | Help: "Scan exporter uptime, in seconds.",
56 | }),
57 |
58 | NumOfDownTargets: prometheus.NewGauge(prometheus.GaugeOpts{
59 | Name: "scanexporter_icmp_not_responding_total",
60 | Help: "Number of targets that doesn't respond to pings.",
61 | }),
62 | UnexpectedPorts: prometheus.NewGaugeVec(prometheus.GaugeOpts{
63 | Name: "scanexporter_unexpected_open_ports_total",
64 | Help: "Number of ports that are open, and shouldn't be.",
65 | }, []string{"name", "ip"}),
66 | OpenPorts: prometheus.NewGaugeVec(prometheus.GaugeOpts{
67 | Name: "scanexporter_open_ports_total",
68 | Help: "Number of ports that are open.",
69 | }, []string{"name", "ip"}),
70 |
71 | ClosedPorts: prometheus.NewGaugeVec(prometheus.GaugeOpts{
72 | Name: "scanexporter_unexpected_closed_ports_total",
73 | Help: "Number of ports that are closed and shouldn't be.",
74 | }, []string{"name", "ip"}),
75 |
76 | DiffPorts: prometheus.NewGaugeVec(prometheus.GaugeOpts{
77 | Name: "scanexporter_diff_ports_total",
78 | Help: "Number of ports that are different from previous scan.",
79 | }, []string{"name", "ip"}),
80 |
81 | Rtt: prometheus.NewGaugeVec(prometheus.GaugeOpts{
82 | Name: "scanexporter_rtt_total",
83 | Help: "Response time of the target.",
84 | }, []string{"name", "ip"}),
85 | }
86 |
87 | prometheus.MustRegister(
88 | s.NumOfTargets,
89 | s.PendingScans,
90 | s.Uptime,
91 | s.NumOfDownTargets,
92 | s.UnexpectedPorts,
93 | s.OpenPorts,
94 | s.ClosedPorts,
95 | s.DiffPorts,
96 | s.Rtt,
97 | )
98 |
99 | s.Addr = addr
100 |
101 | // Initialize the map
102 | s.NotRespondingList = make(map[string]bool)
103 |
104 | // Start uptime counter
105 | go s.uptimeCounter()
106 |
107 | return &s
108 | }
109 |
110 | // Start starts the prometheus server
111 | func (s *Server) Start() error {
112 | srv := &http.Server{
113 | Addr: s.Addr,
114 | Handler: handlers.HandleFunc(),
115 | ReadTimeout: 5 * time.Second,
116 | WriteTimeout: 10 * time.Second,
117 | }
118 | return srv.ListenAndServe()
119 | }
120 |
121 | // Updater updates metrics
122 | func (s *Server) Updater(metChan chan NewMetrics, pingChan chan PingInfo, pending chan int) {
123 | var unexpectedPorts, closedPorts []string
124 | for {
125 | select {
126 | case nm := <-metChan:
127 | // New metrics set has been receievd
128 |
129 | s.DiffPorts.WithLabelValues(nm.Name, nm.IP).Set(float64(nm.Diff))
130 | log.Info().Str("name", nm.Name).Str("ip", nm.IP).Msgf("%s (%s) open ports: %s", nm.Name, nm.IP, nm.Open)
131 |
132 | s.OpenPorts.WithLabelValues(nm.Name, nm.IP).Set(float64(len(nm.Open)))
133 |
134 | // If the port is open but not expected
135 | for _, port := range nm.Open {
136 | if !common.StringInSlice(port, nm.Expected) {
137 | unexpectedPorts = append(unexpectedPorts, port)
138 | }
139 | }
140 | s.UnexpectedPorts.WithLabelValues(nm.Name, nm.IP).Set(float64(len(unexpectedPorts)))
141 | if len(unexpectedPorts) > 0 {
142 | log.Warn().Str("name", nm.Name).Str("ip", nm.IP).Msgf("%s (%s) unexpected open ports: %s", nm.Name, nm.IP, unexpectedPorts)
143 | } else {
144 | log.Info().Str("name", nm.Name).Str("ip", nm.IP).Msgf("%s (%s) unexpected open ports: %s", nm.Name, nm.IP, unexpectedPorts)
145 | }
146 |
147 | unexpectedPorts = nil
148 |
149 | // If the port is expected but not open
150 | for _, port := range nm.Expected {
151 | if !common.StringInSlice(port, nm.Open) {
152 | closedPorts = append(closedPorts, port)
153 | }
154 | }
155 | s.ClosedPorts.WithLabelValues(nm.Name, nm.IP).Set(float64(len(closedPorts)))
156 | if len(closedPorts) > 0 {
157 | log.Warn().Str("name", nm.Name).Str("ip", nm.IP).Msgf("%s (%s) unexpected closed ports: %s", nm.Name, nm.IP, closedPorts)
158 | } else {
159 | log.Info().Str("name", nm.Name).Str("ip", nm.IP).Msgf("%s (%s) unexpected closed ports: %s", nm.Name, nm.IP, closedPorts)
160 | }
161 |
162 | closedPorts = nil
163 | case pm := <-pingChan:
164 | log.Debug().Str("name", pm.Name).Str("ip", pm.IP).Msg("received new ping result")
165 |
166 | // New ping metric has been received
167 | if pm.IsResponding {
168 | log.Debug().Str("name", pm.Name).Str("ip", pm.IP).Str("rtt", pm.RTT.String()).Msgf("%s (%s) responds to ICMP requests", pm.Name, pm.IP)
169 | } else {
170 | log.Warn().Str("name", pm.Name).Str("ip", pm.IP).Str("rtt", "nil").Msgf("%s (%s) does not respond to ICMP requests", pm.Name, pm.IP)
171 | }
172 |
173 | // Update target's RTT metric
174 | s.Rtt.WithLabelValues(pm.Name, pm.IP).Set(float64(pm.RTT))
175 |
176 | // Check if the IP is already in the map.
177 | _, ok := s.NotRespondingList[pm.IP]
178 | if !ok {
179 | // If not, add it as responding.
180 | s.NotRespondingList[pm.IP] = false
181 | }
182 |
183 | // Check if the target didn't respond in the previous scan.
184 | alreadyNotResponding := s.NotRespondingList[pm.IP]
185 |
186 | if pm.IsResponding && alreadyNotResponding {
187 | // Wasn't responding, but now is ok
188 | s.NumOfDownTargets.Dec()
189 | s.NotRespondingList[pm.IP] = false
190 |
191 | } else if !pm.IsResponding && !alreadyNotResponding {
192 | // First time it doesn't respond.
193 | // Increment the number of down targets.
194 | s.NumOfDownTargets.Inc()
195 | s.NotRespondingList[pm.IP] = true
196 | }
197 | // Else, everything is good, do nothing or everything is as bad as it was, so do nothing too.
198 | case pending := <-pending:
199 | // New pending metric has been received
200 |
201 | s.PendingScans.Set(float64(pending))
202 | log.Trace().Int("pending", pending).Msgf("%d pending scans", pending)
203 | }
204 | }
205 | }
206 |
207 | // uptime metric
208 | func (s *Server) uptimeCounter() {
209 | for {
210 | s.Uptime.Add(5)
211 | time.Sleep(5 * time.Second)
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/pprof/pprof.go:
--------------------------------------------------------------------------------
1 | package pprof
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/rs/zerolog/log"
8 |
9 | // Import and mount pprof
10 | _ "net/http/pprof"
11 | )
12 |
13 | // Server holds pprof server required informations
14 | type Server struct {
15 | http.Server
16 | }
17 |
18 | // New returns a pprof server instance
19 | func New(addr string) (*Server, error) {
20 | s := Server{}
21 | s.ReadTimeout = time.Second
22 | s.Addr = addr
23 | return &s, nil
24 | }
25 |
26 | // Run an independent pprof server
27 | func (p *Server) Run() {
28 | if err := p.ListenAndServe(); err != nil {
29 | log.Fatal().Err(err).Msg("error running pprof server")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/scan/icmp.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "math/rand"
5 | "time"
6 |
7 | "github.com/devops-works/scan-exporter/metrics"
8 | "github.com/go-ping/ping"
9 | "github.com/rs/zerolog"
10 | )
11 |
12 | // ping realises an ICMP echo request to a specified target.
13 | // Each error is followed by a continue, which will not stop the goroutine.
14 | func (t *target) ping(logger zerolog.Logger, timeout time.Duration, pchan chan metrics.PingInfo) {
15 | p, err := getDuration(t.icmpPeriod)
16 | if err != nil {
17 | logger.Fatal().Err(err).Msgf("cannot parse duration %s", t.icmpPeriod)
18 | }
19 |
20 | // Randomize period to avoid listening override.
21 | // The random time added will be between 1 and 1.5s
22 | rand.Seed(time.Now().UnixNano())
23 | n := rand.Intn(500) + 1000
24 | randPeriod := p + (time.Duration(n) * time.Millisecond)
25 |
26 | ticker := time.NewTicker(randPeriod)
27 |
28 | for {
29 | select {
30 | case <-ticker.C:
31 | pinfo := metrics.PingInfo{
32 | Name: t.name,
33 | IP: t.ip,
34 | IsResponding: false,
35 | RTT: 0,
36 | }
37 |
38 | pinger, err := ping.NewPinger(t.ip)
39 | if err != nil {
40 | logger.Error().Err(err).Msgf("error creating pinger for %s (%s)", t.name, t.ip)
41 | continue
42 | }
43 |
44 | pinger.Timeout = timeout
45 | pinger.SetPrivileged(true)
46 | pinger.Count = 3
47 |
48 | pinger.OnFinish = func(stats *ping.Statistics) {
49 | logger.Debug().Str("name", t.name).Str("ip", t.ip).Msgf("ping ended")
50 | pinfo.RTT = stats.AvgRtt
51 | if stats.AvgRtt != 0 {
52 | pinfo.IsResponding = true
53 | } else {
54 | pinfo.IsResponding = false
55 | }
56 | pchan <- pinfo
57 | }
58 |
59 | pinger.OnRecv = func(p *ping.Packet) {
60 | logger.Debug().Str("name", t.name).Str("ip", t.ip).Msgf("received one ICMP reply")
61 | }
62 |
63 | logger.Debug().Str("name", t.name).Str("ip", t.ip).Msgf("running a new ping")
64 | err = pinger.Run()
65 | if err != nil {
66 | logger.Error().Err(err).Msgf("error running pinger for %s (%s)", t.name, t.ip)
67 | continue
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/scan/scan.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "os"
8 | "strconv"
9 | "strings"
10 | "sync"
11 | "time"
12 |
13 | "github.com/devops-works/scan-exporter/common"
14 | "github.com/devops-works/scan-exporter/config"
15 | "github.com/devops-works/scan-exporter/metrics"
16 | "github.com/devops-works/scan-exporter/storage"
17 | "github.com/rs/zerolog"
18 | "github.com/rs/zerolog/log"
19 | "golang.org/x/sync/semaphore"
20 | )
21 |
22 | type target struct {
23 | ip string
24 | name string
25 | ports string
26 | expected []string
27 | doTCP bool
28 | doPing bool
29 | tcpPeriod string
30 | icmpPeriod string
31 | qps int
32 | }
33 |
34 | // Scanner holds the targets list, global settings such as timeout and lock size,
35 | // the logger and the metrics server.
36 | type Scanner struct {
37 | Targets []target
38 | Timeout time.Duration
39 | Lock *semaphore.Weighted
40 | Logger zerolog.Logger
41 | MetricsServ metrics.Server
42 | }
43 |
44 | // Start configure targets and launches scans.
45 | func (s *Scanner) Start(c *config.Conf) error {
46 |
47 | s.Logger.Info().Msgf("%d target(s) found in configuration file", len(c.Targets))
48 | s.MetricsServ.NumOfTargets.Set(float64(len(c.Targets)))
49 |
50 | // Check if shared values are set
51 | if c.Timeout == 0 {
52 | s.Logger.Fatal().Msgf("no timeout provided in configuration file")
53 | }
54 | if c.Limit == 0 {
55 | s.Logger.Fatal().Msgf("no limit provided in configuration file")
56 | }
57 | s.Lock = semaphore.NewWeighted(int64(c.Limit))
58 | s.Timeout = time.Second * time.Duration(c.Timeout)
59 |
60 | // If an ICMP period has been provided, it means that we want to ping the
61 | // target. But before, we need to check if we have enough privileges.
62 | if os.Geteuid() != 0 {
63 | s.Logger.Warn().Msgf("scan-exporter not launched as superuser, ICMP requests can fail")
64 | }
65 |
66 | // ping channel to send ICMP update to metrics
67 | pchan := make(chan metrics.PingInfo, len(c.Targets)*2)
68 |
69 | // Configure local target objects
70 | for _, t := range c.Targets {
71 | target := target{
72 | ip: t.IP,
73 | name: t.Name,
74 | tcpPeriod: t.TCP.Period,
75 | icmpPeriod: t.ICMP.Period,
76 | ports: t.TCP.Range,
77 | qps: t.QueriesPerSecond,
78 | }
79 |
80 | // Set to global values if specific values are not set
81 | if target.qps == 0 {
82 | target.qps = c.QueriesPerSecond
83 | }
84 | if target.tcpPeriod == "" {
85 | target.tcpPeriod = c.TcpPeriod
86 | }
87 | if target.icmpPeriod == "" {
88 | target.icmpPeriod = c.IcmpPeriod
89 | }
90 |
91 | // Truth table for icmpPeriod value
92 | //
93 | // | global | target | doPing | period |
94 | // | ------ | ------ | ------ | :----: |
95 | // | "" | "" | false | - |
96 | // | "" | "0" | false | - |
97 | // | "" | "y" | true | y |
98 | // | "0" | "" | false | - |
99 | // | "0" | "0" | false | - |
100 | // | "0" | "y" | true | y |
101 | // | "x" | "" | true | x |
102 | // | "x" | "0" | false | - |
103 | // | "x" | "y" | true | y |
104 | switch c.IcmpPeriod {
105 | case "", "0":
106 | if target.icmpPeriod != "" && target.icmpPeriod != "0" {
107 | target.doPing = true
108 | }
109 | default:
110 | if target.icmpPeriod != "0" {
111 | target.doPing = true
112 | if target.icmpPeriod == "" {
113 | target.icmpPeriod = c.IcmpPeriod
114 | }
115 | }
116 | }
117 | // Inform that ping is disabled
118 | if !target.doPing {
119 | s.Logger.Warn().Msgf("ping explicitly disabled for %s (%s) in configuration",
120 | target.name,
121 | target.ip)
122 | }
123 |
124 | // Read target's expected port range
125 | exp, err := readPortsRange(t.TCP.Expected)
126 | if err != nil {
127 | return err
128 | }
129 |
130 | // Append them to the target
131 | for _, port := range exp {
132 | target.expected = append(target.expected, strconv.Itoa(port))
133 | }
134 |
135 | // Inform that we can't parse the IP, and skip this target
136 | if ok := net.ParseIP(target.ip); ok == nil {
137 | s.Logger.Error().Msgf("cannot parse IP %s", target.ip)
138 | continue
139 | }
140 |
141 | // If TCP period or ports range has been provided, it means that we want
142 | // to do TCP scan on the target
143 | if target.tcpPeriod != "" || target.ports != "" || len(target.expected) != 0 {
144 | target.doTCP = true
145 | }
146 |
147 | // Launch target's ping goroutine. It embeds its own ticker
148 | if target.doPing {
149 | go target.ping(s.Logger, time.Duration(c.Timeout)*time.Second, pchan)
150 | }
151 |
152 | if target.doTCP {
153 | s.Targets = append(s.Targets, target)
154 | }
155 | }
156 |
157 | trigger := make(chan string, len(s.Targets)*2)
158 |
159 | // scanIsOver is used by s.run() to notify the receiver that all the ports
160 | // have been scanned
161 | scanIsOver := make(chan target, len(s.Targets))
162 |
163 | // singleResult is used by s.scanPort() to send an open port to the receiver.
164 | // The format is ip:port
165 | singleResult := make(chan string, c.Limit)
166 |
167 | s.Logger.Debug().Msgf("%d targets will be scanned using TCP", len(s.Targets))
168 |
169 | // Start scheduler for each target
170 | for _, t := range s.Targets {
171 | t := t
172 | s.Logger.Debug().Msgf("start scheduler for %s", t.name)
173 | go t.scheduler(s.Logger, trigger)
174 | }
175 |
176 | // Create channel for communication with metrics server
177 | mchan := make(chan metrics.NewMetrics, len(s.Targets)*2)
178 |
179 | // Channel that will hold the number of scans in the waiting line (len of
180 | // the trigger chan)
181 | pendingchan := make(chan int, len(s.Targets))
182 |
183 | // Goroutine that will send to metrics the number of pendings scan
184 | go func() {
185 | for {
186 | time.Sleep(500 * time.Millisecond)
187 | pendingchan <- len(trigger)
188 | }
189 | }()
190 |
191 | // Start the metrics updater
192 | go s.MetricsServ.Updater(mchan, pchan, pendingchan)
193 |
194 | // Start the receiver
195 | go receiver(scanIsOver, singleResult, pchan, mchan)
196 |
197 | // Wait for triggers, build the scanner and run it
198 | for {
199 | select {
200 | case triggeredIP := <-trigger:
201 | s.Logger.Debug().Msgf("starting new scan for %s", triggeredIP)
202 | if err := s.run(triggeredIP, scanIsOver, singleResult); err != nil {
203 | s.Logger.Error().Err(err).Msg("error running scan")
204 | }
205 | }
206 | }
207 | }
208 |
209 | func (s *Scanner) run(ip string, scanIsOver chan target, singleResult chan string) error {
210 | for _, t := range s.Targets {
211 | // Find which target to scan
212 | if t.ip == ip {
213 | wg := sync.WaitGroup{}
214 |
215 | ports, err := readPortsRange(t.ports)
216 | if err != nil {
217 | return err
218 | }
219 |
220 | // Configure sleeping time for rate limiting
221 | var sleepingTime time.Duration
222 | if t.qps > 1000000 || t.qps <= 0 {
223 | // We want to wait less than a microsecond between each port scanning
224 | // so, we do not wait at all.
225 | // From time.Sleep documentation:
226 | // A negative or zero duration causes Sleep to return immediately
227 | sleepingTime = -1
228 | t.qps = 0
229 | } else {
230 | sleepingTime = time.Second / time.Duration(t.qps)
231 | }
232 |
233 | for _, p := range ports {
234 | wg.Add(1)
235 | s.Lock.Acquire(context.TODO(), 1)
236 | go func(port int) {
237 | defer s.Lock.Release(1)
238 | defer wg.Done()
239 | s.scanPort(ip, port, singleResult)
240 | }(p)
241 | time.Sleep(sleepingTime)
242 | }
243 | wg.Wait()
244 |
245 | // Inform the receiver that the scan for the target is over
246 | scanIsOver <- t
247 | return nil
248 | }
249 | }
250 | return fmt.Errorf("IP to scan not found: %s", ip)
251 | }
252 |
253 | // scanPort scans a single port and sends the result through singleResult.
254 | // There is 2 formats: when a port is open, it sends `ip:port:OK`, and when it is
255 | // closed, it sends `ip:port:NOP`
256 | func (s *Scanner) scanPort(ip string, port int, singleResult chan string) {
257 | p := strconv.Itoa(port)
258 | target := ip + ":" + p
259 | conn, err := net.DialTimeout("tcp", target, s.Timeout)
260 | if err != nil {
261 | // If the error contains the message "too many open files", wait a little
262 | // and retry
263 | if strings.Contains(err.Error(), "too many open files") {
264 | time.Sleep(s.Timeout)
265 | s.scanPort(ip, port, singleResult)
266 | }
267 | // The result follows the format ip:port:NOP
268 | singleResult <- ip + ":" + p + ":NOP"
269 | return
270 | }
271 | conn.Close()
272 |
273 | // The result follows the format ip:port:OK
274 | singleResult <- ip + ":" + p + ":OK"
275 | }
276 |
277 | // scheduler create tickers for each protocol given and when they tick,
278 | // it sends the protocol name in the trigger's channel in order to alert
279 | // feeder that a scan must be started.
280 | func (t *target) scheduler(logger zerolog.Logger, trigger chan string) {
281 | var ticker *time.Ticker
282 | tcpFreq, err := getDuration(t.tcpPeriod)
283 | if err != nil {
284 | logger.Error().Msgf("error getting TCP frequency for %s scheduler: %s", t.name, err)
285 | }
286 | ticker = time.NewTicker(tcpFreq)
287 |
288 | // starts its own ticker
289 | go func(trigger chan string, ticker *time.Ticker, ip string) {
290 | // Start scan at launch
291 | trigger <- t.ip
292 | for {
293 | select {
294 | case <-ticker.C:
295 | trigger <- t.ip
296 | }
297 | }
298 | }(trigger, ticker, t.ip)
299 | }
300 |
301 | func receiver(scanIsOver chan target, singleResult chan string, pchan chan metrics.PingInfo, mchan chan metrics.NewMetrics) {
302 | // openPorts holds the ports that are open for each target
303 | openPorts := make(map[string][]string)
304 | // closedPorts holds the ports that are closed
305 | closedPorts := make(map[string][]string)
306 |
307 | // Create the store for the values
308 | store := storage.Create()
309 |
310 | for {
311 | select {
312 | case t := <-scanIsOver:
313 | // Compare stored results with current results and get the delta
314 | delta := common.CompareStringSlices(store.Get(t.ip), openPorts[t.ip])
315 |
316 | // Update metrics
317 | updatedMetrics := metrics.NewMetrics{
318 | Name: t.name,
319 | IP: t.ip,
320 | Diff: delta,
321 | Open: openPorts[t.ip],
322 | Closed: closedPorts[t.ip],
323 | Expected: t.expected,
324 | }
325 |
326 | // Send new metrics
327 | mchan <- updatedMetrics
328 |
329 | // Update the store
330 | store.Update(t.ip, openPorts[t.ip])
331 |
332 | // Clear slices
333 | openPorts[t.ip] = nil
334 | closedPorts[t.ip] = nil
335 | case res := <-singleResult:
336 | split := strings.Split(res, ":")
337 | // Useless allocations, but it's easier to read
338 | ip := string(split[0])
339 | port := string(split[1])
340 | status := string(split[2])
341 |
342 | if status == "OK" {
343 | openPorts[ip] = append(openPorts[ip], port)
344 | } else if status == "NOP" {
345 | closedPorts[ip] = append(closedPorts[ip], port)
346 | } else {
347 | log.Fatal().Msgf("port status not recognised: %s (%s)", status, ip)
348 | }
349 | }
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/scan/top_ports.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | var top1000Ports = []int{
4 | 1, 3, 4, 6, 7, 9, 13, 17, 19, 20, 21, 22, 23, 24, 25, 26, 30, 32, 33, 37, 42, 43, 49, 53, 70, 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 99, 100, 106, 109, 110, 111, 113, 119, 125, 135, 139, 143, 144, 146, 161, 163, 179, 199, 211, 212, 222, 254, 255, 256, 259, 264, 280, 301, 306, 311, 340, 366, 389, 406, 407, 416, 417, 425, 427, 443, 444, 445, 458, 464, 465,
5 | 481, 497, 500, 512, 513, 514, 515, 524, 541, 543, 544, 545, 548, 554, 555, 563, 587, 593, 616, 617, 625, 631, 636, 646, 648, 666, 667, 668, 683, 687, 691, 700, 705, 711, 714, 720, 722, 726, 749, 765, 777, 783, 787, 800, 801, 808, 843, 873, 880, 888, 898, 900, 901, 902, 903, 911, 912, 981, 987, 990, 992, 993, 995, 999, 1000, 1001, 1002, 1007, 1009, 1010, 1011, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029,
6 | 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1102, 1104, 1105, 1106, 1107, 1108, 1110, 1111, 1112,
7 | 1113, 1114, 1117, 1119, 1121, 1122, 1123, 1124, 1126, 1130, 1131, 1132, 1137, 1138, 1141, 1145, 1147, 1148, 1149, 1151, 1152, 1154, 1163, 1164, 1165, 1166, 1169, 1174, 1175, 1183, 1185, 1186, 1187, 1192, 1198, 1199, 1201, 1213, 1216, 1217, 1218, 1233, 1234, 1236, 1244, 1247, 1248, 1259, 1271, 1272, 1277, 1287, 1296, 1300, 1301, 1309, 1310, 1311, 1322, 1328, 1334, 1352, 1417, 1433, 1434, 1443, 1455, 1461, 1494, 1500, 1501, 1503, 1521, 1524, 1533, 1556, 1580, 1583, 1594, 1600,
8 | 1641, 1658, 1666, 1687, 1688, 1700, 1717, 1718, 1719, 1720, 1721, 1723, 1755, 1761, 1782, 1783, 1801, 1805, 1812, 1839, 1840, 1862, 1863, 1864, 1875, 1900, 1914, 1935, 1947, 1971, 1972, 1974, 1984, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2020, 2021, 2022, 2030, 2033, 2034, 2035, 2038, 2040, 2041, 2042, 2043, 2045, 2046, 2047, 2048, 2049, 2065, 2068, 2099, 2100, 2103, 2105, 2106, 2107, 2111, 2119, 2121, 2126, 2135, 2144, 2160, 2161,
9 | 2170, 2179, 2190, 2191, 2196, 2200, 2222, 2251, 2260, 2288, 2301, 2323, 2366, 2381, 2382, 2383, 2393, 2394, 2399, 2401, 2492, 2500, 2522, 2525, 2557, 2601, 2602, 2604, 2605, 2607, 2608, 2638, 2701, 2702, 2710, 2717, 2718, 2725, 2800, 2809, 2811, 2869, 2875, 2909, 2910, 2920, 2967, 2968, 2998, 3000, 3001, 3003, 3005, 3006, 3007, 3011, 3013, 3017, 3030, 3031, 3052, 3071, 3077, 3128, 3168, 3211, 3221, 3260, 3261, 3268, 3269, 3283, 3300, 3301, 3306, 3322, 3323, 3324, 3325, 3333,
10 | 3351, 3367, 3369, 3370, 3371, 3372, 3389, 3390, 3404, 3476, 3493, 3517, 3527, 3546, 3551, 3580, 3659, 3689, 3690, 3703, 3737, 3766, 3784, 3800, 3801, 3809, 3814, 3826, 3827, 3828, 3851, 3869, 3871, 3878, 3880, 3889, 3905, 3914, 3918, 3920, 3945, 3971, 3986, 3995, 3998, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4045, 4111, 4125, 4126, 4129, 4224, 4242, 4279, 4321, 4343, 4443, 4444, 4445, 4446, 4449, 4550, 4567, 4662, 4848, 4899, 4900, 4998, 5000, 5001, 5002, 5003, 5004, 5009,
11 | 5030, 5033, 5050, 5051, 5054, 5060, 5061, 5080, 5087, 5100, 5101, 5102, 5120, 5190, 5200, 5214, 5221, 5222, 5225, 5226, 5269, 5280, 5298, 5357, 5405, 5414, 5431, 5432, 5440, 5500, 5510, 5544, 5550, 5555, 5560, 5566, 5631, 5633, 5666, 5678, 5679, 5718, 5730, 5800, 5801, 5802, 5810, 5811, 5815, 5822, 5825, 5850, 5859, 5862, 5877, 5900, 5901, 5902, 5903, 5904, 5906, 5907, 5910, 5911, 5915, 5922, 5925, 5950, 5952, 5959, 5960, 5961, 5962, 5963, 5987, 5988, 5989, 5998, 5999, 6000,
12 | 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6009, 6025, 6059, 6100, 6101, 6106, 6112, 6123, 6129, 6156, 6346, 6389, 6502, 6510, 6543, 6547, 6565, 6566, 6567, 6580, 6646, 6666, 6667, 6668, 6669, 6689, 6692, 6699, 6779, 6788, 6789, 6792, 6839, 6881, 6901, 6969, 7000, 7001, 7002, 7004, 7007, 7019, 7025, 7070, 7100, 7103, 7106, 7200, 7201, 7402, 7435, 7443, 7496, 7512, 7625, 7627, 7676, 7741, 7777, 7778, 7800, 7911, 7920, 7921, 7937, 7938, 7999, 8000, 8001, 8002, 8007, 8008, 8009,
13 | 8010, 8011, 8021, 8022, 8031, 8042, 8045, 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8093, 8099, 8100, 8180, 8181, 8192, 8193, 8194, 8200, 8222, 8254, 8290, 8291, 8292, 8300, 8333, 8383, 8400, 8402, 8443, 8500, 8600, 8649, 8651, 8652, 8654, 8701, 8800, 8873, 8888, 8899, 8994, 9000, 9001, 9002, 9003, 9009, 9010, 9011, 9040, 9050, 9071, 9080, 9081, 9090, 9091, 9099, 9100, 9101, 9102, 9103, 9110, 9111, 9200, 9207, 9220, 9290, 9415, 9418, 9485, 9500, 9502,
14 | 9503, 9535, 9575, 9593, 9594, 9595, 9618, 9666, 9876, 9877, 9878, 9898, 9900, 9917, 9929, 9943, 9944, 9968, 9998, 9999, 10000, 10001, 10002, 10003, 10004, 10009, 10010, 10012, 10024, 10025, 10082, 10180, 10215, 10243, 10566, 10616, 10617, 10621, 10626, 10628, 10629, 10778, 11110, 11111, 11967, 12000, 12174, 12265, 12345, 13456, 13722, 13782, 13783, 14000, 14238, 14441, 14442, 15000, 15002, 15003, 15004, 15660, 15742, 16000, 16001, 16012, 16016, 16018, 16080, 16113, 16992, 16993, 17877, 17988, 18040, 18101, 18988, 19101, 19283, 19315,
15 | 19350, 19780, 19801, 19842, 20000, 20005, 20031, 20221, 20222, 20828, 21571, 22939, 23502, 24444, 24800, 25734, 25735, 26214, 27000, 27352, 27353, 27355, 27356, 27715, 28201, 30000, 30718, 30951, 31038, 31337, 32768, 32769, 32770, 32771, 32772, 32773, 32774, 32775, 32776, 32777, 32778, 32779, 32780, 32781, 32782, 32783, 32784, 32785, 33354, 33899, 34571, 34572, 34573, 35500, 38292, 40193, 40911, 41511, 42510, 44176, 44442, 44443, 44501, 45100, 48080, 49152, 49153, 49154, 49155, 49156, 49157, 49158, 49159, 49160, 49161, 49163, 49165, 49167, 49175, 49176,
16 | 49400, 49999, 50000, 50001, 50002, 50003, 50006, 50300, 50389, 50500, 50636, 50800, 51103, 51493, 52673, 52822, 52848, 52869, 54045, 54328, 55055, 55056, 55555, 55600, 56737, 56738, 57294, 57797, 58080, 60020, 60443, 61532, 61900, 62078, 63331, 64623, 64680, 65000, 65129, 65389,
17 | }
18 |
--------------------------------------------------------------------------------
/scan/utils.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "time"
8 | )
9 |
10 | // getDuration transforms a protocol's period into a time.Duration value.
11 | func getDuration(period string) (time.Duration, error) {
12 | // only hours, minutes and seconds are handled by ParseDuration
13 | if strings.ContainsAny(period, "hms") {
14 | t, err := time.ParseDuration(period)
15 | if err != nil {
16 | return 0, err
17 | }
18 | return t, nil
19 | }
20 |
21 | sep := strings.Split(period, "d")
22 | days, err := strconv.Atoi(sep[0])
23 | if err != nil {
24 | return 0, err
25 | }
26 |
27 | t := time.Duration(days) * time.Hour * 24
28 | return t, nil
29 | }
30 |
31 | // readPortsRange transforms a range of ports given in conf to an array of
32 | // effective ports
33 | func readPortsRange(ranges string) ([]int, error) {
34 | ports := []int{}
35 |
36 | // Remove spaces
37 | ranges = strings.Replace(ranges, " ", "", -1)
38 |
39 | parts := strings.Split(ranges, ",")
40 |
41 | for _, spec := range parts {
42 | if spec == "" {
43 | continue
44 | }
45 | switch spec {
46 | case "all":
47 | for port := 1; port <= 65535; port++ {
48 | ports = append(ports, port)
49 | }
50 | case "reserved":
51 | for port := 1; port < 1024; port++ {
52 | ports = append(ports, port)
53 | }
54 | case "top1000":
55 | ports = append(ports, top1000Ports...)
56 | default:
57 | var decomposedRange []string
58 |
59 | if !strings.Contains(spec, "-") {
60 | decomposedRange = []string{spec, spec}
61 | } else {
62 | decomposedRange = strings.Split(spec, "-")
63 | }
64 |
65 | min, err := strconv.Atoi(decomposedRange[0])
66 | if err != nil {
67 | return nil, err
68 | }
69 | max, err := strconv.Atoi(decomposedRange[len(decomposedRange)-1])
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | if min > max {
75 | return nil, fmt.Errorf("lower port %d is higher than high port %d", min, max)
76 | }
77 | if max > 65535 {
78 | return nil, fmt.Errorf("port %d is higher than max port", max)
79 | }
80 | for i := min; i <= max; i++ {
81 | ports = append(ports, i)
82 | }
83 | }
84 | }
85 |
86 | return ports, nil
87 | }
88 |
--------------------------------------------------------------------------------
/scan/utils_test.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func Test_getDuration(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | period string
13 | want time.Duration
14 | wantErr bool
15 | }{
16 | {name: "seconds", period: "42s", want: 42 * time.Second, wantErr: false},
17 | {name: "minutes", period: "666m", want: 666 * time.Minute, wantErr: false},
18 | {name: "hours", period: "1337h", want: 1337 * time.Hour, wantErr: false},
19 | {name: "days", period: "69d", want: 69 * 24 * time.Hour, wantErr: false},
20 | {name: "error", period: "abc", wantErr: true},
21 | }
22 | for _, tt := range tests {
23 | t.Run(tt.name, func(t *testing.T) {
24 | got, err := getDuration(tt.period)
25 | if (err != nil) != tt.wantErr {
26 | t.Errorf("getDuration() error = %v, wantErr %v", err, tt.wantErr)
27 | return
28 | }
29 | if got != tt.want {
30 | t.Errorf("getDuration() = %v, want %v", got, tt.want)
31 | }
32 | })
33 | }
34 | }
35 |
36 | func Test_readPortsRange(t *testing.T) {
37 | tests := []struct {
38 | name string
39 | ranges string
40 | want []int
41 | wantErr bool
42 | }{
43 | {name: "single port", ranges: "1", want: []int{1}, wantErr: false},
44 | {name: "comma", ranges: "1,22", want: []int{1, 22}, wantErr: false},
45 | {name: "hyphen", ranges: "22-25", want: []int{22, 23, 24, 25}, wantErr: false},
46 | {name: "comma and hyphen", ranges: "22,30-32", want: []int{22, 30, 31, 32}, wantErr: false},
47 | {name: "comma, hyphen, comma", ranges: "22,30-32,50", want: []int{22, 30, 31, 32, 50}, wantErr: false},
48 | {name: "unknown", ranges: "foobar", wantErr: true},
49 | }
50 | for _, tt := range tests {
51 | t.Run(tt.name, func(t *testing.T) {
52 | got, err := readPortsRange(tt.ranges)
53 | if (err != nil) != tt.wantErr {
54 | t.Errorf("readPortsRange() error = %v, wantErr %v", err, tt.wantErr)
55 | return
56 | }
57 | if !reflect.DeepEqual(got, tt.want) {
58 | t.Errorf("readPortsRange() = %v, want %v", got, tt.want)
59 | }
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/storage/storage.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | // Store is a key/value
4 | type Store map[string][]string
5 |
6 | // Add a value to the store
7 | func (s Store) Add(k, v string) {
8 | s[k] = append(s[k], v)
9 | }
10 |
11 | // Create initalize the store
12 | func Create() Store {
13 | s := make(Store)
14 | return s
15 | }
16 |
17 | // Delete a key from the store
18 | func (s Store) Delete(k string) {
19 | delete(s, k)
20 | }
21 |
22 | // Get the values associated to a key from the store
23 | func (s Store) Get(k string) []string {
24 | return s[k]
25 | }
26 |
27 | // Update a value in the store
28 | func (s Store) Update(k string, v []string) {
29 | s[k] = v
30 | }
31 |
--------------------------------------------------------------------------------
/storage/storage_test.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestStore_Get(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | s Store
12 | k string
13 | expected []string
14 | }{
15 | {
16 | name: "test",
17 | s: map[string][]string{"foo": {"bar"}, "toor": {"root"}},
18 | k: "toor",
19 | expected: []string{"root"},
20 | },
21 | }
22 | for _, tt := range tests {
23 | t.Run(tt.name, func(t *testing.T) {
24 | output := tt.s.Get(tt.k)
25 | if !equal(output, tt.expected) {
26 | t.Errorf("got %q want %q given", output, tt.expected)
27 | }
28 | })
29 | }
30 | }
31 |
32 | func equal(a, b []string) bool {
33 | if len(a) != len(b) {
34 | return false
35 | }
36 | for i, v := range a {
37 | if v != b[i] {
38 | return false
39 | }
40 | }
41 | return true
42 | }
43 | func TestStore_Add(t *testing.T) {
44 | tests := []struct {
45 | name string
46 | s Store
47 | k string
48 | v string
49 | expected []string
50 | }{
51 | {
52 | name: "add value to non-existing key",
53 | s: map[string][]string{"toor": {"root"}},
54 | k: "foo",
55 | v: "bar",
56 | expected: []string{"bar"},
57 | },
58 | {
59 | name: "add value to existing key",
60 | s: map[string][]string{"toor": {"root"}},
61 | k: "toor",
62 | v: "bar",
63 | expected: []string{"root", "bar"},
64 | },
65 | }
66 | for _, tt := range tests {
67 | t.Run(tt.name, func(t *testing.T) {
68 | tt.s.Add(tt.k, tt.v)
69 | if !equal(tt.s[tt.k], tt.expected) {
70 | t.Errorf("got %q want %q given", tt.s[tt.k], tt.expected)
71 | }
72 | })
73 | }
74 | }
75 |
76 | func TestStore_Delete(t *testing.T) {
77 | tests := []struct {
78 | name string
79 | s Store
80 | k string
81 | v string
82 | expected Store
83 | }{
84 | {
85 | name: "remove key",
86 | s: map[string][]string{"toor": {"root"}},
87 | k: "toor",
88 | expected: map[string][]string{},
89 | },
90 | }
91 | for _, tt := range tests {
92 | t.Run(tt.name, func(t *testing.T) {
93 | tt.s.Delete(tt.k)
94 | if len(tt.s) != len(tt.expected) {
95 | t.Errorf("got %q want %q given", tt.s, tt.expected)
96 | }
97 | })
98 | }
99 | }
100 |
101 | func TestStore_Update(t *testing.T) {
102 | tests := []struct {
103 | name string
104 | s Store
105 | k string
106 | v []string
107 | expected []string
108 | }{
109 | {
110 | name: "update key",
111 | s: map[string][]string{"toor": {"root"}},
112 | k: "toor",
113 | v: []string{"bar"},
114 | expected: []string{"bar"},
115 | },
116 | }
117 | for _, tt := range tests {
118 | t.Run(tt.name, func(t *testing.T) {
119 | tt.s.Update(tt.k, tt.v)
120 | if !equal(tt.s[tt.k], tt.expected) {
121 | t.Errorf("got %q want %q given", tt.s[tt.k], tt.expected)
122 | }
123 | })
124 | }
125 | }
126 |
127 | func TestCreate(t *testing.T) {
128 | tests := []struct {
129 | name string
130 | want Store
131 | }{
132 | {
133 | name: "create basic store",
134 | want: map[string][]string{},
135 | },
136 | }
137 | for _, tt := range tests {
138 | t.Run(tt.name, func(t *testing.T) {
139 | if got := Create(); !reflect.DeepEqual(got, tt.want) {
140 | t.Errorf("Create() = %v, want %v", got, tt.want)
141 | }
142 | })
143 | }
144 | }
145 |
--------------------------------------------------------------------------------