From bf9a779ba53e2d1e74cc08d839f71de4f8dfd6da Mon Sep 17 00:00:00 2001 From: KaanSK Date: Tue, 21 Jun 2022 19:19:07 -0400 Subject: [PATCH] feat: total refactoring done on code --- .github/workflows/build.yml | 24 ++++ .github/workflows/release.yml | 54 ++++++++ .goreleaser.Dockerfile | 3 + .goreleaser.yaml | 84 ++++++++++++ Dockerfile | 2 +- README.md | 53 +++++--- conf.yaml | 13 ++ conf/conf.go | 67 +++++++++ conf/conf_test.go | 15 ++ docker-compose.yml | 81 +++++++++++ go.mod | 18 ++- go.sum | 213 +++++++++++++++++++++++++---- images/help1.png | Bin 28734 -> 0 bytes log/log.go | 115 ++++++++++++++++ main.go | 24 ++++ pkg/conf/conf.go | 13 -- pkg/logwrapper/logwrapper.go | 25 ---- pkg/shodan/host.go | 242 --------------------------------- pkg/shodan/shodan.go | 113 --------------- pkg/thehive/alert.go | 79 ----------- producer/producer.go | 11 ++ producer/shodan_stream.go | 41 ++++++ producer/shodan_webhook.go | 76 +++++++++++ service/service.go | 124 +++++++++++++++++ service/service_stream_test.go | 145 ++++++++++++++++++++ shomon.go | 44 ------ thehive/models.go | 18 +++ thehive/thehive.go | 114 ++++++++++++++++ thehive/thehive_test.go | 61 +++++++++ 29 files changed, 1307 insertions(+), 565 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.Dockerfile create mode 100644 .goreleaser.yaml create mode 100644 conf.yaml create mode 100644 conf/conf.go create mode 100644 conf/conf_test.go create mode 100644 docker-compose.yml delete mode 100644 images/help1.png create mode 100644 log/log.go create mode 100644 main.go delete mode 100644 pkg/conf/conf.go delete mode 100644 pkg/logwrapper/logwrapper.go delete mode 100644 pkg/shodan/host.go delete mode 100644 pkg/shodan/shodan.go delete mode 100644 pkg/thehive/alert.go create mode 100644 producer/producer.go create mode 100644 producer/shodan_stream.go create mode 100644 producer/shodan_webhook.go create mode 100644 service/service.go create mode 100644 service/service_stream_test.go delete mode 100644 shomon.go create mode 100644 thehive/models.go create mode 100644 thehive/thehive.go create mode 100644 thehive/thehive_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0a33a54 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: build + +on: + pull_request: + push: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - run: go test -v ./... \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3cf9508 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +# +# Releaser workflow setup +# https://goreleaser.com/ci/actions/ +# +name: release + +# run only on tags +on: + push: + tags: + - 'v*' + +permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # this is important, otherwise it won't checkout the full tree (i.e. no previous tags) + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - uses: sigstore/cosign-installer@v2.4.0 # installs cosign + - uses: anchore/sbom-action/download-syft@v0.11.0 # installs syft + + - name: dockerhub login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: ghcr login + uses: docker/login-action@v2 # login to ghcr + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: goreleaser/goreleaser-action@v3 # run goreleaser + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.goreleaser.Dockerfile b/.goreleaser.Dockerfile new file mode 100644 index 0000000..b4ed9e5 --- /dev/null +++ b/.goreleaser.Dockerfile @@ -0,0 +1,3 @@ +FROM scratch +COPY shomon /usr/local/bin/shomon +ENTRYPOINT [ "/usr/local/bin/shomon" ] \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..103b84d --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,84 @@ +builds: +- env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + # ensures mod timestamp to be the commit timestamp + mod_timestamp: '{{ .CommitTimestamp }}' + binary: shomon + flags: + # trims path + - -trimpath + ldflags: + # use commit date instead of current date as main.date + # only needed if you actually use those things in your main package, otherwise can be ignored. + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} + +# proxies from the go mod proxy before building +# https://goreleaser.com/customization/gomod +#gomod: +# proxy: true + +# config the checksum filename +# https://goreleaser.com/customization/checksum +checksum: + name_template: 'checksums.txt' + +# create a source tarball +# https://goreleaser.com/customization/source/ +source: + enabled: true + +# creates SBOMs of all archives and the source tarball using syft +# https://goreleaser.com/customization/sbom +sboms: + - artifacts: archive + - id: source # Two different sbom configurations need two different IDs + artifacts: source + +# signs the checksum file +# all files (including the sboms) are included in the checksum, so we don't need to sign each one if we don't want to +# https://goreleaser.com/customization/sign +signs: +- cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + certificate: '${artifact}.pem' + args: + - sign-blob + - '--output-certificate=${certificate}' + - '--output-signature=${signature}' + - '${artifact}' + artifacts: checksum + output: true + + +dockers: +- image_templates: + - "docker.io/kaansk/shomon:{{ .Tag }}" + - "docker.io/kaansk/shomon:latest" + - "ghcr.io/kaansk/shomon:{{ .Tag }}" + - "ghcr.io/kaansk/shomon:latest" + dockerfile: .goreleaser.Dockerfile + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.name={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + + +docker_signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + artifacts: images + output: true + args: + - 'sign' + - '${artifact}' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f48b425..7c91f0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Taken from https://github.com/chemidy/smallest-secured-golang-docker-image -FROM golang@sha256:244a736db4a1d2611d257e7403c729663ce2eb08d4628868f9d9ef2735496659 as builder +FROM golang:alpine as builder # Install git + SSL ca certificates. # Git is required for fetching the dependencies. diff --git a/README.md b/README.md index a94ceed..2861585 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,53 @@ -# [![ShoMon](./images/logo.png)]() - -ShoMon is a shodan alert feeder for theHive written in GoLang. Takes advantage of Golang's goroutines with deferred recover to continuously monitor alerts from shodan and feed them into theHive as alerts. +

+ +

+

+ShoMon is a Shodan alert feeder for TheHive written in GoLang. With version 2.0, it is more powerful than ever! +

+ + +# Functionalities +* Can be used as Webhook OR Stream listener + * Webhook listener opens a restful API endpoint for Shodan to send alerts. This means you need to make this endpoint available to public net + * Stream listener connects to Shodan and fetches/parses the alert stream +* Utilizes [shadowscatcher/shodan](https://github.com/shadowscatcher/shodan) (fantastic work) for Shodan interaction. +* Alert specifics can be adjusted via conf.yaml or environment variables +* Console logs are in JSON format and can be ingested by any other further log management tools +* CI/CD via Github Actions ensures that a proper Release with changelogs, artifacts, images on ghcr and dockerhub will be provided +* Provides a working [docker-compose file](docker-compose.yml) file for TheHive, dependencies +* Super fast and Super mini in size +* Complete code refactoring in v2.0 resulted in more modular, maintainable code # Usage - [![Help](./images/help1.png)]() +* Parameters should be provided via ```conf.yaml``` or environment variables. Please see [config file](conf.yaml) and [docker-compose file](docker-compose.yml) +* After conf or environment variables are set simply issue command: + + `./shomon` ## Notes -* Logs can be found in shodanmonitor.log under the same folder -* Alert reference is md5("ip:port") -* Default logging level is DEBUG. Can be changed via editing logwrapper +* Alert reference is first 6 chars of md5("ip:port") +* Only 1 mod can be active at a time. Webhook and Stream listener can not be activated together. # Setup & Compile Instructions ## Get latest compiled binary from releases -1. Check [Releases] section. +1. Check [Releases](https://github.com/KaanSK/shomon/releases/latest) section. ## Compile from source code 1. Make sure that you have a working Golang workspace. 2. `go build .` * `go build -ldflags="-s -w" .` could be used to customize compilation and produce smaller binary. -## Using Dockerfile -1. `docker build -t shomon .` -2. `docker run -it shomon -s {SHODANKEY} -t {THEHIVEKEY}` +## Using [Dockerfile](Dockerfile) +1. Edit [config file](conf.yaml) or provide environment variables to commands bellow +2. `docker build -t shomon .` +3. `docker run -it shomon` + +## Using [docker-compose file](docker-compose.yml) +1. Edit environment variables and configurations in [docker-compose file](docker-compose.yml) +2. `docker-compose run -d` # Credits * Logo Made via LogoMakr.com -* `go-shodan` : https://github.com/ns3777k/go-shodan -* logwrapper package : https://www.datadoghq.com/blog/go-logging/ -* Dockerfile : https://www.cloudreach.com/en/resources/blog/cts-build-golang-dockerfiles/ - -[Releases]: https://github.com/KaanSK/shomon/releases/latest +* [shadowscatcher/shodan](https://github.com/shadowscatcher/shodan) +* [Dockerfile Reference](https://www.cloudreach.com/en/resources/blog/cts-build-golang-dockerfiles/) +* Release management with [GoReleaser](https://goreleaser.com) diff --git a/conf.yaml b/conf.yaml new file mode 100644 index 0000000..daf0713 --- /dev/null +++ b/conf.yaml @@ -0,0 +1,13 @@ +HIVE_URL: "http://localhost:9000" +HIVE_KEY: +HIVE_CASE_TEMPLATE: +HIVE_TYPE: "SHODAN" +HIVE_TAGS: + - 'test:test2="test3"' + - 'test4:test5="test6"' +SHODAN_KEY: +INCLUDE_BANNER: true +LOG_LEVEL: DEBUG +WEBHOOK: true +WEBHOOK_ENDPOINT: "/" +WEBHOOK_PORT: 8080 \ No newline at end of file diff --git a/conf/conf.go b/conf/conf.go new file mode 100644 index 0000000..d4f2ae9 --- /dev/null +++ b/conf/conf.go @@ -0,0 +1,67 @@ +package conf + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" +) + +type ShomonConfig struct { + HiveUrl string `koanf:"HIVE_URL"` + HiveCaseTemplate string `koanf:"HIVE_CASE_TEMPLATE" ` + HiveKey string `koanf:"HIVE_KEY"` + HiveTags []string `koanf:"HIVE_TAGS"` + HiveType string `koanf:"HIVE_TYPE"` + ShodanKey string `koanf:"SHODAN_KEY"` + LogLevel string `koanf:"LOG_LEVEL"` + IncludeBanner bool `koanf:"INCLUDE_BANNER"` + Webhook bool `koanf:"WEBHOOK"` + WebhookEndpoint string `koanf:"WEBHOOK_ENDPOINT"` + WebhookPort int `koanf:"WEBHOOK_PORT"` +} + +func New() (conf ShomonConfig, err error) { + newConfig := &ShomonConfig{} + if err := newConfig.Setup(); err != nil { + return *newConfig, err + } + return *newConfig, nil +} + +func (d *ShomonConfig) Setup() error { + k := koanf.New(".") + + //Default Values + k.Load(confmap.Provider(map[string]interface{}{ + "HIVE_URL": "http://localhost:9000", + "HIVE_KEY": "NO_KEY", + "LOG_LEVEL": "INFO", + }, "."), nil) + + if err := k.Load(file.Provider("conf.yaml"), yaml.Parser()); err != nil { + if errors.Is(err, os.ErrNotExist) { + k.Load(env.Provider("SHOMON_", ".", func(s string) string { + return strings.TrimPrefix(s, "SHOMON_") + }), nil) + } else { + return err + } + } + + k.Unmarshal("", &d) + return nil +} + +func (d *ShomonConfig) Print() string { + if d != nil { + return fmt.Sprintf("%+v", d) + } + return "" +} diff --git a/conf/conf_test.go b/conf/conf_test.go new file mode 100644 index 0000000..1ee6809 --- /dev/null +++ b/conf/conf_test.go @@ -0,0 +1,15 @@ +package conf + +import ( + "testing" +) + +func TestGetConf(t *testing.T) { + conf, err := New() + if err != nil { + t.Errorf(err.Error()) + } + if conf.LogLevel == "" { + t.Errorf("Config could not be populated") + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3793e86 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,81 @@ +version: "3" +services: + shomon: + build: . + container_name: shomon + ports: + - "8080:8080" + environment: + - "SHOMON_HIVE_URL=http://thehive:9000" + - "SHOMON_HIVE_KEY=" + - "SHOMON_HIVE_TYPE=SHODAN" + - 'SHOMON_HIVE_TAGS=test:test2="test3"' + - "SHOMON_SHODAN_KEY=" + - "SHOMON_INCLUDE_BANNER=true" + - "SHOMON_LOG_LEVEL=DEBUG" + - "SHOMON_WEBHOOK=true" + - "SHOMON_WEBHOOK_ENDPOINT=/banner" + - "SHOMON_WEBHOOK_PORT=8080" + + thehive: + image: strangebee/thehive:latest + container_name: thehive + depends_on: + - cassandra + - elasticsearch + - minio + mem_limit: 1000m + ports: + - "9000:9000" + environment: + - JVM_OPTS="-Xms1024M -Xmx1024M" + command: + - --secret + - "mySecretForTheHive" + - "--cql-hostnames" + - "cassandra" + - "--cql-username" + - "cassandra" + - "--cql-password" + - "cassandra" + - "--index-backend" + - "elasticsearch" + - "--es-hostnames" + - "elasticsearch" + - "--s3-endpoint" + - "http://minio:9000" + - "--s3-access-key" + - "minioadmin" + - "--s3-secret-key" + - "minioadmin" + - "--s3-use-path-access-style" + - "--no-config-cortex" + + cassandra: + container_name: cassandra + image: bitnami/cassandra + ports: + - "9042:9042" + environment: + - CASSANDRA_CLUSTER_NAME=TheHive + + elasticsearch: + container_name: elastic + mem_limit: 1000m + image: docker.elastic.co/elasticsearch/elasticsearch:7.16.2 + ports: + - "9200:9200" + environment: + - discovery.type=single-node + - xpack.security.enabled=false + + minio: + container_name: minio + mem_limit: 1000m + image: quay.io/minio/minio + command: ["minio", "server", "/data", "--console-address", ":9001"] + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + ports: + - "9001:9001" diff --git a/go.mod b/go.mod index 84c2beb..605aae2 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,21 @@ module github.com/KaanSK/shomon -go 1.14 +go 1.18 require ( - github.com/jessevdk/go-flags v1.4.0 + github.com/jarcoal/httpmock v1.2.0 + github.com/knadh/koanf v1.4.2 + github.com/shadowscatcher/shodan v1.0.6 + go.uber.org/zap v1.21.0 +) - github.com/sirupsen/logrus v1.6.0 +require ( + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 001af06..136d8ad 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,190 @@ -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c h1:lIC98ZUNah83ky7d9EXktLFe4H7Nwus59dTOLXr8xAI= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200524003926-2c5affb30a03 h1:FjiybxAA4IL7VWbzhdskUDIXM2yIyE+/PRchayObTC4= -github.com/ianlancetaylor/demangle v0.0.0-20200524003926-2c5affb30a03/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/ns3777k/go-shodan v1.0.2 h1:WScCf0IU3x9uQvV2YhQINzrnNBZ7KGMyhCjvrza2YWE= -github.com/ns3777k/go-shodan v3.1.0+incompatible h1:x+R8ZgUO6TKCVz9V5nz6ihSYF64BJWAaEPUylD6Zjy4= -github.com/ns3777k/go-shodan/v4 v4.2.0 h1:18R6axS4f+l37ic14BfjnmMo1dLgNTiPi6dtPXd9qwc= -github.com/ns3777k/go-shodan/v4 v4.2.0/go.mod h1:7kSWq/PQ/JCH6U4k2YjXRmnJKfPaJZAhOSMgAXRB23U= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= +github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/knadh/koanf v1.4.2 h1:2itp+cdC6miId4pO4Jw7c/3eiYD26Z/Sz3ATJMwHxIs= +github.com/knadh/koanf v1.4.2/go.mod h1:4NCo0q4pmU398vF9vq2jStF9MWQZ8JEDcDMHlDCr4h0= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/shadowscatcher/shodan v1.0.6 h1:T8G8DSK9NZmpOK68elQpspGWTwa2l5pGCFhIyGubZuA= +github.com/shadowscatcher/shodan v1.0.6/go.mod h1:8tnu38ME7RJgOfkJt/20JXK7bMvPTYCkLagO3IhI2SI= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e h1:9vRrk9YW2BTzLP0VCB9ZDjU4cPqkg+IDWL7XgxA1yxQ= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/images/help1.png b/images/help1.png deleted file mode 100644 index 49f88183c5becff6bfe84b6d7e9fd316d036b6ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28734 zcmdSAbz57__CFlF#arB^rMO#hDDLiF+}*vD;tmZC#ogWAr8vdiHMk`|+WXk|ocnnJ zPyQhL+E)^0�TePluv{R(NvFTj1$O57XIL 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 diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..5a51a8b --- /dev/null +++ b/log/log.go @@ -0,0 +1,115 @@ +package log + +import ( + "io" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func (l *Logger) Debug(msg string, fields ...zap.Field) { + l.l.Debug(msg, fields...) +} + +func (l *Logger) Info(msg string, fields ...zap.Field) { + l.l.Info(msg, fields...) +} + +func (l *Logger) Warn(msg string, fields ...zap.Field) { + l.l.Warn(msg, fields...) +} + +func (l *Logger) Error(msg string, fields ...zap.Field) { + l.l.Error(msg, fields...) +} +func (l *Logger) DPanic(msg string, fields ...zap.Field) { + l.l.DPanic(msg, fields...) +} +func (l *Logger) Panic(msg string, fields ...zap.Field) { + l.l.Panic(msg, fields...) +} +func (l *Logger) Fatal(msg string, fields ...zap.Field) { + l.l.Fatal(msg, fields...) +} + +// function variables for all field types +// in github.com/uber-go/zap/field.go + +var ( + Skip = zap.Skip + Binary = zap.Binary + Bool = zap.Bool + Boolp = zap.Boolp + ByteString = zap.ByteString + Float64 = zap.Float64 + Float64p = zap.Float64p + Float32 = zap.Float32 + Float32p = zap.Float32p + Durationp = zap.Durationp + Any = zap.Any + Object = zap.Object + + Info = std.Info + Warn = std.Warn + Error = std.Error + DPanic = std.DPanic + Panic = std.Panic + Fatal = std.Fatal + Debug = std.Debug +) + +// not safe for concurrent use +func ResetDefault(l *Logger) { + std = l + Info = std.Info + Warn = std.Warn + Error = std.Error + DPanic = std.DPanic + Panic = std.Panic + Fatal = std.Fatal + Debug = std.Debug +} + +type Logger struct { + l *zap.Logger // zap ensure that zap.Logger is safe for concurrent use + level zapcore.Level +} + +var std, _ = New(os.Stdout, "INFO") + +func Default() *Logger { + return std +} + +func New(writer io.Writer, level string) (logger *Logger, err error) { + parsedAtomicLevel, err := zapcore.ParseLevel(level) + if err != nil { + return logger, err + } + if writer == nil { + return logger, err + } + cfg := zap.NewProductionConfig() + core := zapcore.NewCore( + zapcore.NewJSONEncoder(cfg.EncoderConfig), + zapcore.AddSync(writer), + zapcore.Level(parsedAtomicLevel), + ) + logger = &Logger{ + l: zap.New(core), + level: parsedAtomicLevel, + } + return logger, err +} + +func (l *Logger) Sync() error { + return l.l.Sync() +} + +func Sync() error { + if std != nil { + return std.Sync() + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e842673 --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "sync" + + "github.com/KaanSK/shomon/log" + "github.com/KaanSK/shomon/service" +) + +func main() { + var wg sync.WaitGroup + ctx := context.Background() + + srv, err := service.New(&wg, ctx) + if err != nil { + log.Error(err.Error()) + } + + wg.Add(1) + go srv.ListenStream() + wg.Wait() + +} diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go deleted file mode 100644 index 6ece940..0000000 --- a/pkg/conf/conf.go +++ /dev/null @@ -1,13 +0,0 @@ -package conf - -// Options : argument struct -type Options struct { - Endpoint string `short:"e" long:"endpoint" description:"Full endpoint for alert API" default:"http://127.0.0.1:9000/api/alert"` - ShodanKey string `short:"s" long:"shodanKey" description:"Shodan Api Key" required:"true"` - CaseTemplate string `short:"c" long:"caseTemplate" description:"Case template for alert creation. Can be empty" default:""` - TheHiveKey string `short:"t" long:"theHiveKey" description:"TheHive api key" required:"true"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` -} - -// Config : Used to retrieve conf key/values. Globally available -var Config Options diff --git a/pkg/logwrapper/logwrapper.go b/pkg/logwrapper/logwrapper.go deleted file mode 100644 index f23bc6b..0000000 --- a/pkg/logwrapper/logwrapper.go +++ /dev/null @@ -1,25 +0,0 @@ -package logwrapper - -import ( - "os" - - "github.com/sirupsen/logrus" -) - -// StandardLogger enforces specific log message formats -type StandardLogger struct { - *logrus.Logger -} - -// Logger : Globally shared logging instance -var Logger = NewLogger() - -// NewLogger initializes the standard logger -func NewLogger() *StandardLogger { - var baseLogger = logrus.New() - var standardLogger = &StandardLogger{baseLogger} - standardLogger.SetLevel(logrus.InfoLevel) - - standardLogger.SetOutput(os.Stdout) - return standardLogger -} diff --git a/pkg/shodan/host.go b/pkg/shodan/host.go deleted file mode 100644 index 40ee363..0000000 --- a/pkg/shodan/host.go +++ /dev/null @@ -1,242 +0,0 @@ -// This package is derived from https://github.com/ns3777k/go-shodan - -//The MIT License (MIT) -// -//Copyright (c) 2015-present Safonov Nikita -// -//Permission is hereby granted, free of charge, to any person obtaining a copy -//of this software and associated documentation files (the "Software"), to deal -//in the Software without restriction, including without limitation the rights -//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -//copies of the Software, and to permit persons to whom the Software is -//furnished to do so, subject to the following conditions: - -//The above copyright notice and this permission notice shall be included in all -//copies or substantial portions of the Software. - -//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -//SOFTWARE. - -package shodan - -import ( - "encoding/json" - "errors" - "io/ioutil" - "math/big" - "net" - "net/http" - "strconv" - "strings" -) - -// Facet is a property to get summary information on. -type Facet struct { - Count int `json:"count"` - Value string `json:"value"` -} - -// IntString is string with custom unmarshaling. -type IntString string - -// UnmarshalJSON handles either a string or a number -// and casts it to string. -func (v *IntString) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err == nil { - *v = IntString(s) - return nil - } - - var n int - if err := json.Unmarshal(b, &n); err != nil { - return err - } - - *v = IntString(strconv.Itoa(n)) - - return nil -} - -// String method just returns string out of IntString. -func (v *IntString) String() string { - return string(*v) -} - -// GetErrorFromResponse used to getting errors from streaming api endpoint -func GetErrorFromResponse(r *http.Response) error { - errorResponse := new(struct { - Error string `json:"error"` - }) - message, err := ioutil.ReadAll(r.Body) - if err == nil { - if err := json.Unmarshal(message, errorResponse); err == nil { - return errors.New(errorResponse.Error) - } - - return errors.New(strings.TrimSpace(string(message))) - } - - return err -} - -// HostServicesOptions is options for querying services. -type HostServicesOptions struct { - History bool `url:"history,omitempty"` - Minify bool `url:"minify,omitempty"` -} - -// HostLocation is the location of the host. -type HostLocation struct { - City string `json:"city"` - RegionCode string `json:"region_code"` - AreaCode int `json:"area_code"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Country string `json:"country_name"` - CountryCode string `json:"country_code"` - CountryCode3 string `json:"country_code3"` - Postal string `json:"postal_code"` - DMA int `json:"dma_code"` -} - -// HostDHParams is the Diffie-Hellman parameters if available. -type HostDHParams struct { - Prime string `json:"prime"` - PublicKey string `json:"public_key"` - Bits int `json:"bits"` - Generator *IntString `json:"generator"` - Fingerprint string `json:"fingerprint"` -} - -// HostTLSExtEntry contains id and name. -type HostTLSExtEntry struct { - ID int `json:"id"` - Name string `json:"name"` -} - -// HostCipher is a cipher description. -type HostCipher struct { - Version string `json:"version"` - Bits int `json:"bits"` - Name string `json:"name"` -} - -// HostCertificatePublicKey holds type and bits length of the key. -type HostCertificatePublicKey struct { - Type string `json:"type"` - Bits int `json:"bits"` -} - -// HostCertificateAttributes is an ordinary certificate attributes description. -type HostCertificateAttributes struct { - CountryName string `json:"C,omitempty"` - CommonName string `json:"CN,omitempty"` - Locality string `json:"L,omitempty"` - Organization string `json:"O,omitempty"` - StateOrProvinceName string `json:"ST,omitempty"` - OrganizationalUnit string `json:"OU,omitempty"` -} - -// HostCertificateExtension represent single cert extension. -type HostCertificateExtension struct { - Data string `json:"data"` - Name string `json:"name"` - IsCritical bool `json:"critical,omitempty"` -} - -// HostCertificate contains common certificate description. -type HostCertificate struct { - SignatureAlgorithm string `json:"sig_alg"` - IsExpired bool `json:"expired"` - Version int `json:"version"` - Serial *big.Int `json:"serial"` - Issued string `json:"issued"` - Expires string `json:"expires"` - Fingerprint map[string]string `json:"fingerprint"` - Issuer *HostCertificateAttributes `json:"issuer"` - Subject *HostCertificateAttributes `json:"subject"` - PublicKey *HostCertificatePublicKey `json:"pubkey"` - Extensions []*HostCertificateExtension `json:"extensions"` -} - -// HostSSL holds ssl host information. -type HostSSL struct { - Versions []string `json:"versions"` - Chain []string `json:"chain"` - DHParams *HostDHParams `json:"dhparams"` - TLSExt []*HostTLSExtEntry `json:"tlsext"` - Cipher *HostCipher `json:"cipher"` - Certificate *HostCertificate `json:"cert"` -} - -// HostData is all services that have been found on the given host IP. -type HostData struct { - Product string `json:"product"` - Hostnames []string `json:"hostnames"` - Version IntString `json:"version"` - Title string `json:"title"` - SSL *HostSSL `json:"ssl"` - IP net.IP `json:"ip_str"` - OS string `json:"os"` - Organization string `json:"org"` - ISP string `json:"isp"` - CPE []string `json:"cpe"` - Data string `json:"data"` - ASN string `json:"asn"` - Port int `json:"port"` - HTML string `json:"html"` - Banner string `json:"banner"` - Link string `json:"link"` - Transport string `json:"transport"` - Domains []string `json:"domains"` - Timestamp string `json:"timestamp"` - DeviceType string `json:"devicetype"` - Location *HostLocation `json:"location"` - ShodanData map[string]interface{} `json:"_shodan"` - Opts map[string]interface{} `json:"opts"` -} - -// Host is the all information about the host. -type Host struct { - OS string `json:"os"` - Ports []int `json:"ports"` - IP net.IP `json:"ip_str"` - ISP string `json:"isp"` - Hostnames []string `json:"hostnames"` - Organization string `json:"org"` - Vulnerabilities []string `json:"vulns"` - ASN string `json:"asn"` - LastUpdate string `json:"last_update"` - Data []*HostData `json:"data"` - HostLocation -} - -// HostQueryOptions is Shodan search query options. -type HostQueryOptions struct { - Query string `url:"query"` - Facets string `url:"facets,omitempty"` - Minify bool `url:"minify,omitempty"` - Page int `url:"page,omitempty"` -} - -// HostMatch is the search results with all matched hosts. -type HostMatch struct { - Total int `json:"total"` - Facets map[string][]*Facet `json:"facets"` - Matches []*HostData `json:"matches"` -} - -// HostQueryTokens is filters are being used by the query string and what -// parameters were provided to the filters. -type HostQueryTokens struct { - Filters []string `json:"filters"` - String string `json:"string"` - Errors []string `json:"errors"` - Attributes map[string]interface{} `json:"attributes"` -} diff --git a/pkg/shodan/shodan.go b/pkg/shodan/shodan.go deleted file mode 100644 index 8865d05..0000000 --- a/pkg/shodan/shodan.go +++ /dev/null @@ -1,113 +0,0 @@ -package shodan - -import ( - "bufio" - "bytes" - "crypto/md5" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - - conf "github.com/KaanSK/shomon/pkg/conf" - lw "github.com/KaanSK/shomon/pkg/logwrapper" - "github.com/KaanSK/shomon/pkg/thehive" -) - -var ( - errShomonServiceStop = errors.New("listener service stopped") -) - -func parseResponse(destination interface{}, body io.Reader) error { - var err error - - if w, ok := destination.(io.Writer); ok { - _, err = io.Copy(w, body) - } else { - decoder := json.NewDecoder(body) - err = decoder.Decode(destination) - } - - return err -} - -func handleAlertStream(ch chan *HostData) { - defer func() { - close(ch) - }() - resp, err := http.Get("https://stream.shodan.io/shodan/alert?key=" + conf.Config.ShodanKey) - if err != nil { - lw.Logger.Error(err) - } - if resp.StatusCode != http.StatusOK { - err = GetErrorFromResponse(resp) - resp.Body.Close() - lw.Logger.Error(err) - if err.Error() == "No alerts specified" || err.Error() == "Invalid API key" { - os.Exit(1) - } - } - - reader := bufio.NewReader(resp.Body) - for { - banner := new(HostData) - chunk, err := reader.ReadBytes('\n') - if err != nil { - resp.Body.Close() - break - } - - chunk = bytes.TrimRight(chunk, "\n\r") - if len(chunk) == 0 { - continue - } - - if err := parseResponse(banner, bytes.NewBuffer(chunk)); err != nil { - resp.Body.Close() - lw.Logger.Error(err) - break - } - - ch <- banner - } -} - -// ListenAlerts : Used to listen streaming monitoring API -func ListenAlerts() error { - ch := make(chan *HostData) - go handleAlertStream(ch) - - lw.Logger.Info("listening process initiated") - - for { - banner, ok := <-ch - if !ok { - break - } - - hiveAlert := new(thehive.HiveAlert) - foundService := fmt.Sprintf("%s:%d", banner.IP, banner.Port) - hiveAlert.Title = fmt.Sprintf("Alert: %s", foundService) - hiveAlert.Description = "Test description" - - if conf.Config.CaseTemplate != "" { - hiveAlert.CaseTemplate = conf.Config.CaseTemplate - } - - hiveAlert.Source = "Shodan" - hash := md5.Sum([]byte(foundService)) - hiveAlert.SourceRef = hex.EncodeToString(hash[:]) - - lw.Logger.Info("triggered alarm for: " + hiveAlert.SourceRef) - - err := thehive.CreateAlert(hiveAlert) - if err != nil { - return err - } - lw.Logger.Info("created alert for " + hiveAlert.SourceRef) - } - return errShomonServiceStop -} diff --git a/pkg/thehive/alert.go b/pkg/thehive/alert.go deleted file mode 100644 index e0376b1..0000000 --- a/pkg/thehive/alert.go +++ /dev/null @@ -1,79 +0,0 @@ -package thehive - -import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "time" - - conf "github.com/KaanSK/shomon/pkg/conf" -) - -// HiveAlert : Alert structure -type HiveAlert struct { - CaseTemplate string `json:"caseTemplate,omitempty"` - Artifacts []interface{} `json:"artifacts"` - CreatedAt int64 `json:"createdAt"` - CreatedBy string `json:"createdBy"` - Date int64 `json:"date"` - Description string `json:"description"` - Follow bool `json:"follow"` - ID string `json:"id,omitempty"` - LastSyncDate int64 `json:"lastSyncDate"` - Severity int `json:"severity"` - Source string `json:"source"` - SourceRef string `json:"sourceRef"` - Status string `json:"status"` - Title string `json:"title"` - Tlp int `json:"tlp"` - Type string `json:"type"` - User string `json:"user,omitempty"` -} - -func getResponseJSON(url string, target interface{}, input interface{}) (interface{}, error) { - var netClient = &http.Client{ - Timeout: time.Second * 10, - } - payload, err := json.Marshal(input) - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+conf.Config.TheHiveKey) - req.Header.Set("Content-Type", "application/json") - - resp, err := netClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 201 { - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - bodyString := string(bodyBytes) - return nil, errors.New(bodyString) - } - - err = json.NewDecoder(resp.Body).Decode(&target) - if err != nil { - return nil, err - } - return target, nil - -} - -// CreateAlert : Used to create alert on thehive -func CreateAlert(alertObject *HiveAlert) error { - endpoint := conf.Config.Endpoint - _, err := getResponseJSON(endpoint, HiveAlert{}, *alertObject) - if err != nil { - return err - } - return nil -} diff --git a/producer/producer.go b/producer/producer.go new file mode 100644 index 0000000..4fb9531 --- /dev/null +++ b/producer/producer.go @@ -0,0 +1,11 @@ +package producer + +import ( + "context" + + "github.com/shadowscatcher/shodan/models" +) + +type Producer interface { + ListenAlerts(ctx context.Context) (chan models.Service, error) +} diff --git a/producer/shodan_stream.go b/producer/shodan_stream.go new file mode 100644 index 0000000..705e3d4 --- /dev/null +++ b/producer/shodan_stream.go @@ -0,0 +1,41 @@ +package producer + +import ( + "context" + "errors" + "net/http" + + "github.com/shadowscatcher/shodan" + "github.com/shadowscatcher/shodan/models" +) + +type ShodanStream struct { + client *shodan.StreamClient +} + +func GetShodanStreamClient(ShodanKey string, httpClient *http.Client) (*ShodanStream, error) { + if ShodanKey == "" { + return nil, errors.New("empty Shodan API key") + } + + if httpClient == nil { + return nil, errors.New("HTTP client is nil") + } + + client, err := shodan.GetStreamClient(ShodanKey, httpClient) + if err != nil { + return nil, err + } + + return &ShodanStream{ + client: client, + }, nil +} + +func (ss *ShodanStream) ListenAlerts(ctx context.Context) (chan models.Service, error) { + alertChan, err := ss.client.Alerts(ctx) + if err != nil { + return nil, err + } + return alertChan, nil +} diff --git a/producer/shodan_webhook.go b/producer/shodan_webhook.go new file mode 100644 index 0000000..0bde2ef --- /dev/null +++ b/producer/shodan_webhook.go @@ -0,0 +1,76 @@ +package producer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/shadowscatcher/shodan/models" +) + +type ShodanWebhook struct { + ShodanKey string + Endpoint string + Port int +} + +func GetShodanWebhook(shodanKey string, endpoint string, port int) (*ShodanWebhook, error) { + if shodanKey == "" { + return nil, errors.New("empty API key") + } + + if endpoint == "" { + return nil, errors.New("endpoint is empty") + } + + return &ShodanWebhook{ + ShodanKey: shodanKey, + Endpoint: endpoint, + Port: port, + }, nil +} + +func (sw *ShodanWebhook) ListenAlerts(ctx context.Context) (chan models.Service, error) { + bannerChan := make(chan models.Service) + + bannerHandler := banner(bannerChan) + http.HandleFunc(sw.Endpoint, bannerHandler) + go func() { + defer close(bannerChan) + http.ListenAndServe(fmt.Sprintf(":%d", sw.Port), nil) + }() + + return bannerChan, nil + +} + +func banner(bannerChan chan models.Service) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + banner := models.Service{} + /* alertID := req.Header.Get("SHODAN-ALERT-ID") + alertName := req.Header.Get("SHODAN-ALERT-NAME") + alertTrigger := req.Header.Get("SHODAN-ALERT-TRIGGER") + alertVerify := req.Header.Get("SHODAN-SIGNATURE-SHA1") + if containsEmpty(alertID, alertName, alertTrigger, alertVerify) { + w.WriteHeader(http.StatusBadRequest) + return + } */ + if err := json.NewDecoder(req.Body).Decode(&banner); err != nil { + w.WriteHeader(http.StatusBadRequest) + } else { + bannerChan <- banner + w.WriteHeader(http.StatusOK) + } + } +} + +func containsEmpty(ss ...string) bool { + for _, s := range ss { + if s == "" { + return true + } + } + return false +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..a8452fe --- /dev/null +++ b/service/service.go @@ -0,0 +1,124 @@ +package service + +import ( + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "sync" + + "github.com/KaanSK/shomon/conf" + "github.com/KaanSK/shomon/log" + "github.com/KaanSK/shomon/producer" + "github.com/KaanSK/shomon/thehive" + "github.com/shadowscatcher/shodan/models" +) + +type Result struct { + Message string + Error error +} + +type Service struct { + Config conf.ShomonConfig + HiveClient *thehive.TheHiveClient + ShodanClient producer.Producer + Logger log.Logger + wg *sync.WaitGroup + ctx context.Context +} + +func New(wg *sync.WaitGroup, ctx context.Context) (srv Service, err error) { + shomonConf, err := conf.New() + if err != nil { + log.Fatal(err.Error()) + } + logger, err := log.New(os.Stdout, shomonConf.LogLevel) + if err != nil { + log.Fatal(err.Error()) + } + + hiveClient, err := thehive.GetHiveClient(shomonConf.HiveUrl, shomonConf.HiveKey, http.DefaultClient) + if err != nil { + log.Fatal(err.Error()) + } + var bannerClient producer.Producer + if shomonConf.Webhook { + bannerClient, err = producer.GetShodanWebhook(shomonConf.ShodanKey, shomonConf.WebhookEndpoint, shomonConf.WebhookPort) + log.Info("webhook mode is activated") + } else { + bannerClient, err = producer.GetShodanStreamClient(shomonConf.ShodanKey, http.DefaultClient) + log.Info("stream listening mode is activated") + } + + if err != nil { + log.Fatal(err.Error()) + } + + logger.Debug(fmt.Sprintf("Config= %s", shomonConf.Print())) + + srv.Config = shomonConf + srv.Logger = *logger + srv.wg = wg + srv.ctx = ctx + srv.HiveClient = hiveClient + srv.ShodanClient = bannerClient + + return srv, nil +} + +func (s *Service) ListenStream() error { + defer s.wg.Done() + + s.Logger.Info("starting service...") + + alertChan, err := s.ShodanClient.ListenAlerts(s.ctx) + if err != nil { + return err + } + + for banner := range alertChan { + s.wg.Add(1) + go func() { + id, err := s.ProcessAlert(banner) + if err != nil { + s.Logger.Error(err.Error()) + return + } + s.Logger.Info(fmt.Sprintf("Alert %s Created for %s", id, banner.IPstr)) + }() + } + + return nil +} + +func (s *Service) ProcessAlert(banner models.Service) (id string, err error) { + defer s.wg.Done() + s.Logger.Debug(PrintBanner(banner)) + + foundService := fmt.Sprintf("%s:%d", banner.IPstr, banner.Port) + foundServiceHash := md5.Sum([]byte(foundService)) + + alert := thehive.NewAlert() + alert.Type = s.Config.HiveType + alert.Source = "Shodan" + alert.SourceRef = hex.EncodeToString(foundServiceHash[:6]) + alert.Tags = s.Config.HiveTags + alert.Title = fmt.Sprintf("Shodan Alert: %s", foundService) + alert.AddObservable("ip", banner.IPstr) + alert.ExternalLink = fmt.Sprintf("https://www.shodan.io/host/%s", banner.IPstr) + alert.Description = fmt.Sprintf("[Alert Link](%s)", alert.ExternalLink) + if s.Config.IncludeBanner { + alert.Description = fmt.Sprintf("%s\n\n```\n\n%s\n\n```", alert.Description, PrintBanner(banner)) + } + id, err = s.HiveClient.CreateAlert(alert) + return id, err +} + +func PrintBanner(banner models.Service) string { + s, _ := json.MarshalIndent(banner, "", "\t") + return string(s) +} diff --git a/service/service_stream_test.go b/service/service_stream_test.go new file mode 100644 index 0000000..372b290 --- /dev/null +++ b/service/service_stream_test.go @@ -0,0 +1,145 @@ +package service + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "math/rand" + "net" + "net/http" + "os" + "sync" + "testing" + "time" + + "github.com/KaanSK/shomon/log" + "github.com/KaanSK/shomon/thehive" + "github.com/jarcoal/httpmock" + "github.com/shadowscatcher/shodan/models" +) + +type MockStream struct { +} + +func (ss *MockStream) ListenAlerts(ctx context.Context) (chan models.Service, error) { + bannerChan := make(chan models.Service) + bannerJson := `{ + "hash": 1015805840, + "timestamp": "2021-01-28T04:16:08.387364", + "hostnames": [ + "177-70-193-184-msltr-cw-1.visaonet.com.br" + ], + "org": "TESTORG", + "data": "SIP/2.0 404 Not Found\r\nFrom: ;tag=root\r\nTo: ;tag=b235f0-b146c1b8-13c4-50029-ec2e4-6c44dfcf-ec2e4\r\nCall-ID: 50000\r\nCSeq: 42 OPTIONS\r\nVia: SIP/2.0/UDP nm;received=224.238.62.40;rport=26810;branch=foo\r\nSupported: replaces,100rel,timer\r\nAccept: application/sdp\r\nAllow: INVITE,ACK,CANCEL,BYE,OPTIONS,REFER,INFO,NOTIFY,PRACK,MESSAGE\r\nContent-Length: 0\r\n\r\n", + "port": 5060, + "transport": "udp", + "info": "SIP end point; Status: 404 Not Found", + "isp": "L M Tiko Kamide - Sva", + "asn": "AS28359", + "location": { + "country_code3": null, + "city": "Jardim Alegre", + "region_code": "PR", + "postal_code": null, + "longitude": -51.7213, + "country_code": "BR", + "latitude": -24.2123, + "country_name": "Brazil", + "area_code": null, + "dma_code": null + }, + "ip": 2974204344, + "domains": [ + "visaonet.com.br" + ], + "ip_str": "177.70.193.184", + "_id": "45ad6383-1b1d-4c5d-8584-d586fbdefbc3", + "os": null, + "_shodan": { + "crawler": "bf213bc419cc8491376c12af31e32623c1b6f467", + "options": {}, + "id": "220ef463-756f-4446-a89f-685053da8865", + "module": "sip", + "ptr": true + }, + "opts": {} + }` + banner := models.Service{} + buf := make([]byte, 4) + err := json.Unmarshal([]byte(bannerJson), &banner) + if err != nil { + return nil, err + } + rand.Seed(time.Now().UTC().UnixNano()) + go func() { + for i := 0; i < 5; i++ { + time.Sleep(1 * time.Second) + ip := rand.Uint32() + binary.LittleEndian.PutUint32(buf, ip) + banner.IPstr = fmt.Sprintf("%s", net.IP(buf)) + bannerChan <- banner + } + close(bannerChan) + }() + + return bannerChan, nil +} + +func TestListenStreamAlerts(t *testing.T) { + ms := &MockStream{} + var wg sync.WaitGroup + ctx := context.Background() + logger, err := log.New(os.Stdout, "ERROR") + if err != nil { + log.Fatal(err.Error()) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + endpoint := "https://test.local" + + httpmock.RegisterResponder("POST", endpoint+"/api/v1/alert", + func(req *http.Request) (*http.Response, error) { + alert := thehive.Alert{} + if err := json.NewDecoder(req.Body).Decode(&alert); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + alert.Id = fmt.Sprintf("~%d", rand.Intn(1000000)) + resp, err := httpmock.NewJsonResponse(201, alert) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + hiveClient, err := thehive.GetHiveClient(endpoint, "TEST", http.DefaultClient) + if err != nil { + t.Errorf(err.Error()) + } + + srv := Service{ + ShodanClient: ms, + HiveClient: hiveClient, + wg: &wg, + ctx: ctx, + Logger: *logger, + } + + alertChan, err := srv.ShodanClient.ListenAlerts(ctx) + if err != nil { + t.Errorf(err.Error()) + } + + for banner := range alertChan { + srv.wg.Add(1) + go func() { + _, err := srv.ProcessAlert(banner) + if err != nil { + t.Errorf(err.Error()) + } + }() + } + wg.Wait() +} diff --git a/shomon.go b/shomon.go deleted file mode 100644 index d2f00ab..0000000 --- a/shomon.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "os" - - "github.com/KaanSK/shomon/pkg/conf" - lw "github.com/KaanSK/shomon/pkg/logwrapper" - "github.com/KaanSK/shomon/pkg/shodan" - "github.com/jessevdk/go-flags" - "github.com/sirupsen/logrus" -) - -func init() { - parser := flags.NewParser(&conf.Config, flags.Default) - _, err := parser.Parse() - if err != nil { - os.Exit(1) - } - if conf.Config.Verbose { - lw.Logger.Formatter = &logrus.JSONFormatter{} - lw.Logger.SetReportCaller(true) - lw.Logger.SetLevel(logrus.DebugLevel) - } -} - -func neverExit() { - defer func() { - if err := recover(); err != nil { - lw.Logger.Error(err) - go neverExit() - } - }() - err := shodan.ListenAlerts() - if err != nil { - lw.Logger.Error(err) - go neverExit() - } -} - -func main() { - lw.Logger.Info("main process started") - go neverExit() - select {} -} diff --git a/thehive/models.go b/thehive/models.go new file mode 100644 index 0000000..c3ff177 --- /dev/null +++ b/thehive/models.go @@ -0,0 +1,18 @@ +package thehive + +type Alert struct { + Id string `json:"_id,omitempty"` + Type string `json:"type"` + Source string `json:"source"` + SourceRef string `json:"sourceRef"` + Title string `json:"title"` + Description string `json:"description"` + ExternalLink string `json:"externalLink"` + Tags []string `json:"tags"` + Observables []Observable `json:"observables"` +} + +type Observable struct { + DataType string `json:"dataType"` + Data string `json:"data"` +} diff --git a/thehive/thehive.go b/thehive/thehive.go new file mode 100644 index 0000000..af65dc1 --- /dev/null +++ b/thehive/thehive.go @@ -0,0 +1,114 @@ +package thehive + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" +) + +type TheHiveClient struct { + apiKey string + url string + HTTP *http.Client + Logger log.Logger +} + +func GetHiveClient(url string, key string, client *http.Client) (*TheHiveClient, error) { + /* if key == "" { + return nil, errors.New("empty Hive API key") + } + + if client == nil { + return nil, errors.New("HTTP client is nil") + } */ + + return &TheHiveClient{ + apiKey: key, + url: url, + HTTP: client, + }, nil +} + +func NewAlert() Alert { + return Alert{} +} + +func (a *Alert) AddObservable(obsType string, obs string) { + obsInstance := Observable{ + Data: obs, + DataType: obsType, + } + a.Observables = append(a.Observables, obsInstance) +} + +func (s *TheHiveClient) CreateAlert(alert Alert) (id string, err error) { + if s == nil { + return id, errors.New("not initialized hive client") + } + payload, err := json.Marshal(alert) + if err != nil { + return id, err + } + req, err := http.NewRequest("POST", s.url+"/api/v1/alert", bytes.NewBuffer(payload)) + if err != nil { + return id, err + } + req.Header.Set("Authorization", "Bearer "+s.apiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := s.HTTP.Do(req) + if err != nil { + return id, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusCreated { + if err != nil { + return id, err + } + return id, errors.New(string(body)) + } + createdAlert := NewAlert() + err = json.Unmarshal(body, &createdAlert) + if err != nil { + return id, errors.New(err.Error()) + } + return createdAlert.Id, nil +} + +type DeleteAlertsInput struct { + Ids []string `json:"ids"` +} + +func (s *TheHiveClient) DeleteAlerts(ids []string) error { + if s == nil { + return errors.New("not initialized hive client") + } + payload, err := json.Marshal(&DeleteAlertsInput{Ids: ids}) + if err != nil { + return err + } + req, err := http.NewRequest("POST", s.url+"/api/v1/alert/delete/_bulk", bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+s.apiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := s.HTTP.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return errors.New(string(body)) + } + return nil +} diff --git a/thehive/thehive_test.go b/thehive/thehive_test.go new file mode 100644 index 0000000..4f595f9 --- /dev/null +++ b/thehive/thehive_test.go @@ -0,0 +1,61 @@ +package thehive + +import ( + "encoding/json" + "math/rand" + "net/http" + "strconv" + "testing" + + "github.com/jarcoal/httpmock" +) + +func TestGetHiveClient(t *testing.T) { + apiKey := "TeST" + endpoint := "https://test.local" + client, err := GetHiveClient(endpoint, apiKey, http.DefaultClient) + if err != nil { + t.Errorf(err.Error()) + } + if client.apiKey != apiKey || client.url != endpoint { + t.Errorf("Client could not be initalized with proper input") + } +} + +func TestCreateAlert(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + endpoint := "https://test.local" + + httpmock.RegisterResponder("POST", endpoint+"/api/v1/alert", + func(req *http.Request) (*http.Response, error) { + alert := Alert{} + if err := json.NewDecoder(req.Body).Decode(&alert); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + resp, err := httpmock.NewJsonResponse(201, alert) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + hiveClient, err := GetHiveClient(endpoint, "TEST", http.DefaultClient) + if err != nil { + t.Errorf(err.Error()) + } + alert := NewAlert() + alert.Description = "TestDescription" + alert.Type = "TEST_TYPE" + alert.Title = "TestTitle" + alert.Tags = []string{"test1", "test2"} + alert.Source = "Shodan" + alert.SourceRef = strconv.Itoa(100000 + rand.Intn(900000)) + alert.AddObservable("ip", "1.1.1.1") + _, err = hiveClient.CreateAlert(alert) + if err != nil { + t.Errorf(err.Error()) + } + +}