├── .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 | [![forthebadge](https://forthebadge.com/images/badges/made-with-go.svg)](https://forthebadge.com) 263 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 264 | [![forthebadge](https://forthebadge.com/images/badges/open-source.svg)](https://forthebadge.com) 265 | [![forthebadge](https://forthebadge.com/images/badges/powered-by-black-magic.svg)](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 | --------------------------------------------------------------------------------