mihari 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a60ee7c7a40195a7b6b49484ce1df99926278721bd77603b6c9d0f3a952c4ac2
4
+ data.tar.gz: 5c51449f1c2c3a7e14f745490c2bee6f2d18c3f5e5eaafc25a2ba55b15e83e12
5
+ SHA512:
6
+ metadata.gz: dba9abb2e8c856bd2fc814daf4d7d4b5d63912d707c6e4e1225dc8ecb3d7f07067d6e8764509013a99621902fb24d7a74c8dd8a5dcbde00ec2469075799458db
7
+ data.tar.gz: 53f255209c397224a7250c5adc6323c1e99a833426f9304649c779751f0299a898c28c9b911289bf941ebed8b8ab0b61c9aef298153acf3fe269a9e9044b58e5
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ Gemfile.lock
46
+ .ruby-version
47
+ .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
51
+
52
+ # rspec
53
+ .rspec_status
54
+
55
+ # solargraph
56
+ .solargraph.yml
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.1
7
+ before_install: gem install bundler -v 2.0.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mihari.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Manabu Niseki
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.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # mihari
2
+
3
+ [![Build Status](https://travis-ci.org/ninoseki/mihari.svg?branch=master)](https://travis-ci.org/ninoseki/mihari)
4
+ [![Coverage Status](https://coveralls.io/repos/github/ninoseki/mihari/badge.svg?branch=master)](https://coveralls.io/github/ninoseki/mihari?branch=master)
5
+
6
+ mihari(`見張り`) is a framework for continuous malicious hosts (C2 / landing page / phishing, etc.) monitoring backended with [TheHive](https://github.com/TheHive-Project/TheHive).
7
+
8
+ ## How it works
9
+
10
+ - mihari checks whether a TheHive instance contains given artifacts or not.
11
+ - If it doesn't contain the artifacts:
12
+ - mihari creates an alert with the artifacts on the TheHive instance.
13
+ - mihari sends a notification to Slack. (Optional)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ gem install mihari
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ All configuration is done via ENV variables.
24
+
25
+ | Key | Desc. | Required or optional |
26
+ |----------------------|--------------------|--------------------------------|
27
+ | THEHIVE_API_ENDPOINT | TheHive URL | Required |
28
+ | THEHIVE_API_KEY | TheHive API key | Required |
29
+ | SLACK_WEBHOOK_URL | Slack Webhook URL | Optional |
30
+ | SLACK_CHANNEL | Slack channel name | Optional (default: `#general`) |
31
+ | CENSYS_ID | CENSYS API ID | Optional |
32
+ | CENSYS_SECRET | CENSYS secret | Optional |
33
+ | SHODAN_API_KEY | Shodan API key | Optional |
34
+
35
+ ## Basic usage
36
+
37
+ ### Censys
38
+
39
+ ```bash
40
+ mihari censys "YOUR_QUERY"
41
+ ```
42
+
43
+ ### Shodan
44
+
45
+ ```bash
46
+ mihari shodan "YOUR QUERY"
47
+ ```
48
+
49
+ ### Import from JSON
50
+
51
+ ```bash
52
+ echo '{ "title": "test", "description": "test", "artifacts": ["1.1.1.1", "github.com", "2.2.2.2"] }' | mihari import_from_json
53
+ ```
54
+
55
+ The input is a JSON data should have `title`, `description` and `artifacts` key.
56
+
57
+ ```json
58
+ {
59
+ "title": "test",
60
+ "description": "test",
61
+ "artifacts": ["1.1.1.1", "github.com"]
62
+ }
63
+ ```
64
+
65
+ | Key | Desc. |
66
+ |-------------|----------------------------------------------------------------------------|
67
+ | title | A title of an alert |
68
+ | description | A description of an alert |
69
+ | artifacts | An array of artifacts (supported data types: ip, domain, url, email, hash) |
70
+
71
+ ## How to create a custom analyzer
72
+
73
+ Create a class which extends `Mihari::Analyzers::Base` and implements the following methods.
74
+
75
+ | Name | Desc. | @return |
76
+ |----------------|----------------------------------------------------------------------------|---------------|
77
+ | `#title` | A title of an alert | String |
78
+ | `#description` | A description of an alert | String |
79
+ | `#artifacts` | An array of artifacts (supported data types: ip, domain, url, email, hash) | Array<String> |
80
+
81
+ ```ruby
82
+ require "mihari"
83
+
84
+ module Mihari
85
+ module Analyzers
86
+ class Example < Base
87
+ def title
88
+ "example"
89
+ end
90
+
91
+ def description
92
+ "example"
93
+ end
94
+
95
+ def artifacts
96
+ ["9.9.9.9", "example.com"]
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ example = Mihari::Analyzers::Example.new
103
+ example.run
104
+ ```
105
+
106
+ See `/examples` for more.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mihari"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
4
+
5
+ require "json"
6
+ require "mihari"
7
+ require "open-uri"
8
+
9
+ module Mihari
10
+ module Analyzers
11
+ class HostedDomains < Base
12
+ attr_reader :ip
13
+
14
+ IPINFO_API_ENDPOINT = "https://ipinfo.io"
15
+
16
+ def initialize(ip, token: nil)
17
+ @ip = ip
18
+ @token = token
19
+ end
20
+
21
+ def title
22
+ "IPinfo hosted domains"
23
+ end
24
+
25
+ def description
26
+ "IP info hosted domains: #{ip}"
27
+ end
28
+
29
+ def token
30
+ ENV["IPINFO_TOKEN"] || @token
31
+ end
32
+
33
+ def artifacts
34
+ uri = URI("#{IPINFO_API_ENDPOINT}/domains/#{ip}?token=#{token}")
35
+ res = uri.read
36
+ json = JSON.parse(res)
37
+ json.dig("domains") || []
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ ip = "TARGET_IP"
44
+ analyzer = Mihari::Analyzers::HostedDomains.new(ip)
45
+ analyzer.run
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
4
+
5
+ require "mihari"
6
+
7
+ require "virustotal_api"
8
+
9
+ module Mihari
10
+ module Analyzers
11
+ class VTPassiveDNS < Base
12
+ attr_reader :ip
13
+
14
+ def initialize(ip, api_key: nil)
15
+ @ip = ip
16
+ @api_key = api_key
17
+ end
18
+
19
+ def title
20
+ "VT passive DNS"
21
+ end
22
+
23
+ def description
24
+ "VT passive DNS: #{ip}"
25
+ end
26
+
27
+ def api_key
28
+ ENV["VT_API_KEY"] || @api_key
29
+ end
30
+
31
+ def artifacts
32
+ ip_report = VirustotalAPI::IPReport.find(ip, api_key)
33
+ return [] unless ip_report.exists?
34
+
35
+ report = ip_report.report
36
+ report.dig("resolutions")&.map do |resolution|
37
+ resolution.dig("hostname")
38
+ end&.compact
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ip = "TARGET_IP"
45
+ analyzer = Mihari::Analyzers::VTPassiveDNS.new(ip)
46
+ analyzer.run
data/exe/mihari ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require "mihari"
7
+
8
+ Mihari::CLI.start
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Analyzers
5
+ class Base
6
+ attr_reader :the_hive
7
+
8
+ def initialize
9
+ @the_hive = TheHive.new
10
+ end
11
+
12
+ # @return [Array<String>, Array<Mihari::Artifact>]
13
+ def artifacts
14
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
15
+ end
16
+
17
+ # @return [String]
18
+ def title
19
+ self.class.to_s.split("::").last
20
+ end
21
+
22
+ # @return [String]
23
+ def description
24
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
25
+ end
26
+
27
+ def run(reject_exists_ones: true)
28
+ unique_artifacts = normalized_artifacts.reject do |artifact|
29
+ reject_exists_ones & the_hive.valid? && the_hive.exists?(data: artifact.data, data_type: artifact.data_type)
30
+ end
31
+
32
+ Mihari.notifiers.each do |notifier_class|
33
+ notifier = notifier_class.new
34
+ next unless notifier.valid?
35
+
36
+ notifier.notify(title: title, description: description, artifacts: unique_artifacts)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # @return [Array<Mihari::Artifact>]
43
+ def normalized_artifacts
44
+ artifacts.map do |artifact|
45
+ artifact.is_a?(Artifact) ? artifact : Artifact.new(artifact)
46
+ end.select(&:valid?)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Analyzers
5
+ class Basic < Base
6
+ attr_reader :title
7
+ attr_reader :description
8
+ attr_reader :artifacts
9
+
10
+ def initialize(title:, description:, artifacts:)
11
+ super()
12
+
13
+ @title = title
14
+ @description = description
15
+ @artifacts = artifacts
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "censu"
4
+
5
+ module Mihari
6
+ module Analyzers
7
+ class Censys < Base
8
+ attr_reader :api
9
+ attr_reader :title
10
+ attr_reader :description
11
+ attr_reader :query
12
+
13
+ CENSYS_ID_KEY = "CENSYS_ID"
14
+ CENSYS_SECRET_KEY = "CENSYS_SECRET"
15
+
16
+ def initialize(query)
17
+ super()
18
+
19
+ raise ArgumentError, "#{CENSYS_ID_KEY} and #{CENSYS_SECRET_KEY} are required" unless valid?
20
+
21
+ @api = ::Censys::API.new
22
+ @query = query
23
+ @title = "Censys lookup"
24
+ @description = "Query: #{query}"
25
+ end
26
+
27
+ def artifacts
28
+ ipv4s = []
29
+ res = api.ipv4.search(query: query)
30
+ res.each_page do |page|
31
+ page.each { |result| ipv4s << result.ip }
32
+ end
33
+
34
+ ipv4s
35
+ end
36
+
37
+ # @return [true, false]
38
+ def censys_id?
39
+ ENV.key? CENSYS_ID_KEY
40
+ end
41
+
42
+ # @return [true, false]
43
+ def censys_secret?
44
+ ENV.key? CENSYS_SECRET_KEY
45
+ end
46
+
47
+ # @return [true, false]
48
+ def valid?
49
+ censys_id? && censys_secret?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open-uri"
4
+ require "json"
5
+
6
+ module Mihari
7
+ module Analyzers
8
+ class Shodan < Base
9
+ attr_reader :api_key
10
+ attr_reader :title
11
+ attr_reader :description
12
+ attr_reader :query
13
+
14
+ def initialize(query)
15
+ super()
16
+
17
+ api_key = ENV.fetch("SHODAN_API_KEY", nil)
18
+ raise ArgumentError, "SHODAN_API_KEY is required" unless api_key
19
+
20
+ @api_key = api_key
21
+ @query = query
22
+ @title = "Shodan lookup"
23
+ @description = "Query: #{query}"
24
+ end
25
+
26
+ def artifacts
27
+ result = search
28
+ return [] unless result
29
+
30
+ matches = result.dig("matches") || []
31
+ matches.map do |match|
32
+ match.dig "ip_str"
33
+ end.compact
34
+ end
35
+
36
+ private
37
+
38
+ def search
39
+ uri = URI("https://api.shodan.io/shodan/host/search?key=#{api_key}&query=#{query}")
40
+ begin
41
+ JSON.parse uri.read
42
+ rescue OpenURI::HTTPError
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hachi"
4
+
5
+ module Mihari
6
+ class Artifact
7
+ attr_reader :data
8
+
9
+ #
10
+ # @param [String] data
11
+ # @param [String, nil] message
12
+ #
13
+ def initialize(data, message: nil)
14
+ @data = data
15
+ @message = message
16
+ end
17
+
18
+ # @return [String, nil]
19
+ def data_type
20
+ TypeChecker.type data
21
+ end
22
+
23
+ # @return [String]
24
+ def message
25
+ @mesasge || data
26
+ end
27
+
28
+ # @return [true, false]
29
+ def valid?
30
+ !data_type.nil?
31
+ end
32
+
33
+ # @return [Hash]
34
+ def to_h
35
+ { data: data, data_type: data_type, message: message }
36
+ end
37
+ end
38
+ end
data/lib/mihari/cli.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+
6
+ module Mihari
7
+ class CLI < Thor
8
+ desc "censys [QUERY]", "Censys lookup by a given query"
9
+ def censys(query)
10
+ with_error_handling do
11
+ censys = Analyzers::Censys.new(query)
12
+ censys.run
13
+ end
14
+ end
15
+
16
+ desc "shodan [QUERY]", "Shodan lookup by a given query"
17
+ def shodan(query)
18
+ with_error_handling do
19
+ shodan = Analyzers::Shodan.new(query)
20
+ shodan.run
21
+ end
22
+ end
23
+
24
+ desc "import_from_json", "Give a JSON input via STDIN"
25
+ def import_from_json(input = nil)
26
+ json = input || STDIN.gets.chomp
27
+ raise ArgumentError, "Input not found: please give an input in a JSON format" unless json
28
+
29
+ json = parse_as_json(json)
30
+ raise ArgumentError, "Invalid input format: an input JSON data should have title, description and artifacts key" unless valid_json?(json)
31
+
32
+ title = json.dig("title")
33
+ description = json.dig("description")
34
+ artifacts = json.dig("artifacts")
35
+
36
+ with_error_handling do
37
+ basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts)
38
+ basic.run
39
+ end
40
+ end
41
+
42
+ no_commands do
43
+ def with_error_handling
44
+ yield
45
+ rescue ArgumentError, Hachi::Error, Censys::ResponseError => e
46
+ puts "Warning: #{e}"
47
+ rescue StandardError => e
48
+ puts "Warning: #{e}"
49
+ puts e.backtrace.join('\n')
50
+ end
51
+
52
+ def parse_as_json(input)
53
+ JSON.parse input
54
+ rescue JSON::ParserError => _
55
+ nil
56
+ end
57
+
58
+ # @return [true, false]
59
+ def valid_json?(json)
60
+ %w(title description artifacts).all? { |key| json.key? key }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Notifiers
5
+ class Base
6
+ def self.inherited(child)
7
+ Mihari.notifiers << child
8
+ end
9
+
10
+ # @return [true, false]
11
+ def valid?
12
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
13
+ end
14
+
15
+ def notify(title:, description:, artifacts:)
16
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slack/incoming/webhooks"
4
+ require "digest/sha2"
5
+
6
+ module Mihari
7
+ module Notifiers
8
+ class Attachment
9
+ attr_reader :data, :data_type
10
+
11
+ def initialize(data:, data_type:)
12
+ @data = data
13
+ @data_type = data_type
14
+ end
15
+
16
+ # @return [String]
17
+ def link
18
+ case data_type
19
+ when "hash"
20
+ "https://www.virustotal.com/#/file/#{data}"
21
+ when "ip"
22
+ "https://www.virustotal.com/#/ip-address/#{data}"
23
+ when "domain"
24
+ "https://www.virustotal.com/#/domain/#{data}"
25
+ when "url"
26
+ "https://www.virustotal.com/#/url/#{sha256}"
27
+ when "mail"
28
+ "https://www.virustotal.com/#/search/#{data}"
29
+ else
30
+ ""
31
+ end
32
+ end
33
+
34
+ # @return [Hash]
35
+ def to_h
36
+ {
37
+ fallback: "VT link",
38
+ title: data,
39
+ title_link: link,
40
+ footer: "virustotal.com",
41
+ footer_icon: "http://www.google.com/s2/favicons?domain=virustotal.com"
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ # @return [String]
48
+ def sha256
49
+ Digest::SHA256.hexdigest data
50
+ end
51
+ end
52
+
53
+ class Slack < Base
54
+ SLACK_WEBHOOK_URL_KEY = "SLACK_WEBHOOK_URL"
55
+ SLACK_CHANNEL_KEY = "SLACK_CHANNEL"
56
+
57
+ def slack_channel
58
+ ENV.fetch SLACK_CHANNEL_KEY, "#general"
59
+ end
60
+
61
+ def slack_webhook_url
62
+ ENV.fetch SLACK_WEBHOOK_URL_KEY
63
+ end
64
+
65
+ def slack_webhook_url?
66
+ ENV.key? SLACK_WEBHOOK_URL_KEY
67
+ end
68
+
69
+ def valid?
70
+ slack_webhook_url?
71
+ end
72
+
73
+ def to_attachments(artifacts)
74
+ artifacts.map do |artifact|
75
+ Attachment.new(data: artifact.data, data_type: artifact.data_type).to_h
76
+ end
77
+ end
78
+
79
+ def notify(title:, description:, artifacts:)
80
+ return if artifacts.empty?
81
+
82
+ attachments = to_attachments(artifacts)
83
+
84
+ slack = ::Slack::Incoming::Webhooks.new(slack_webhook_url, channel: slack_channel)
85
+ slack.post("#{title} (#{description})", attachments: attachments)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Notifiers
5
+ class TheHive < Base
6
+ attr_reader :api
7
+
8
+ def initialize
9
+ @api = Mihari::TheHive.new
10
+ end
11
+
12
+ # @return [true, false]
13
+ def valid?
14
+ api.valid?
15
+ end
16
+
17
+ def notify(title:, description:, artifacts:)
18
+ return if artifacts.empty?
19
+
20
+ res = api.create_alert(title: title, description: description, artifacts: artifacts.map(&:to_h))
21
+ id = res.dig("id")
22
+ puts "A new alret is created. (id: #{id})"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ class TheHive
5
+ # @return [true, false]
6
+ def api_endpont?
7
+ ENV.key? "THEHIVE_API_ENDPOINT"
8
+ end
9
+
10
+ # @return [true, false]
11
+ def api_key?
12
+ ENV.key? "THEHIVE_API_KEY"
13
+ end
14
+
15
+ # @return [true, false]
16
+ def valid?
17
+ api_endpont? && api_key?
18
+ end
19
+
20
+ # @return [Hachi::API]
21
+ def api
22
+ @api ||= Hachi::API.new
23
+ end
24
+
25
+ # @return [Hash]
26
+ def search(data:, data_type:, range: "all")
27
+ api.artifact.search(data: data, data_type: data_type, range: range)
28
+ end
29
+
30
+ # @return [true, false]
31
+ def exists?(data:, data_type:)
32
+ res = search(data: data, data_type: data_type, range: "0-1")
33
+ !res.empty?
34
+ end
35
+
36
+ # @return [Hash]
37
+ def create_alert(title:, description:, artifacts:)
38
+ api.alert.create(title: title, description: description, artifacts: artifacts, type: "external", source: "mihari")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "addressable/uri"
4
+ require "email_address"
5
+ require "ipaddr"
6
+ require "public_suffix"
7
+
8
+ module Mihari
9
+ class TypeChecker
10
+ attr_reader :data
11
+
12
+ def initialize(data)
13
+ @data = data
14
+ end
15
+
16
+ # @return [true, false]
17
+ def hash?
18
+ md5? || sha1? || sha256? || sha512?
19
+ end
20
+
21
+ # @return [true, false]
22
+ def ip?
23
+ IPAddr.new data
24
+ true
25
+ rescue IPAddr::InvalidAddressError => _
26
+ false
27
+ end
28
+
29
+ # @return [true, false]
30
+ def domain?
31
+ uri = Addressable::URI.parse("http://#{data}")
32
+ uri.host == data && PublicSuffix.valid?(uri.host)
33
+ rescue Addressable::URI::InvalidURIError => _
34
+ false
35
+ end
36
+
37
+ # @return [true, false]
38
+ def url?
39
+ uri = Addressable::URI.parse(data)
40
+ uri.scheme && uri.host && uri.path && PublicSuffix.valid?(uri.host)
41
+ rescue Addressable::URI::InvalidURIError => _
42
+ false
43
+ end
44
+
45
+ # @return [true, false]
46
+ def mail?
47
+ EmailAddress.valid? data
48
+ end
49
+
50
+ # @return [String, nil]
51
+ def type
52
+ return "hash" if hash?
53
+ return "ip" if ip?
54
+ return "domain" if domain?
55
+ return "url" if url?
56
+ return "mail" if mail?
57
+ end
58
+
59
+ # @return [String, nil]
60
+ def self.type(data)
61
+ new(data).type
62
+ end
63
+
64
+ private
65
+
66
+ # @return [true, false]
67
+ def md5?
68
+ data.match? /^[A-Fa-f0-9]{32}$/
69
+ end
70
+
71
+ # @return [true, false]
72
+ def sha1?
73
+ data.match? /^[A-Fa-f0-9]{40}$/
74
+ end
75
+
76
+ # @return [true, false]
77
+ def sha256?
78
+ data.match? /^[A-Fa-f0-9]{64}$/
79
+ end
80
+
81
+ # @return [true, false]
82
+ def sha512?
83
+ data.match? /^[A-Fa-f0-9]{128}$/
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mihari.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mem"
4
+
5
+ module Mihari
6
+ class << self
7
+ include Mem
8
+
9
+ def notifiers
10
+ []
11
+ end
12
+ memoize :notifiers
13
+ end
14
+ end
15
+
16
+ require "mihari/version"
17
+
18
+ require "mihari/errors"
19
+
20
+ require "mihari/type_checker"
21
+ require "mihari/artifact"
22
+
23
+ require "mihari/the_hive"
24
+
25
+ require "mihari/analyzers/base"
26
+ require "mihari/analyzers/basic"
27
+ require "mihari/analyzers/censys"
28
+ require "mihari/analyzers/shodan"
29
+
30
+ require "mihari/notifiers/base"
31
+ require "mihari/notifiers/slack"
32
+ require "mihari/notifiers/the_hive"
33
+
34
+ require "mihari/cli"
data/mihari.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "mihari/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "mihari"
9
+ spec.version = Mihari::VERSION
10
+ spec.authors = ["Manabu Niseki"]
11
+ spec.email = ["manabu.niseki@gmail.com"]
12
+
13
+ spec.summary = "A framework for continuous malicious hosts monitoring."
14
+ spec.description = "A framework for continuous malicious hosts monitoring."
15
+ spec.homepage = "https://github.com/ninoseki/mihari"
16
+ spec.license = "MIT"
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_development_dependency "bundler", "~> 2.0"
28
+ spec.add_development_dependency "coveralls", "~> 0.8"
29
+ spec.add_development_dependency "rake", "~> 12.3"
30
+ spec.add_development_dependency "rspec", "~> 3.8"
31
+ spec.add_development_dependency "vcr", "~> 4.0"
32
+ spec.add_development_dependency "webmock", "~> 3.5"
33
+
34
+ spec.add_dependency "addressable", "~> 2.6"
35
+ spec.add_dependency "censu", "~> 0.2"
36
+ spec.add_dependency "email_address", "~> 0.1"
37
+ spec.add_dependency "hachi", "~> 0.1"
38
+ spec.add_dependency "mem", "~> 0.1"
39
+ spec.add_dependency "public_suffix", "~> 3.0"
40
+ spec.add_dependency "slack-incoming-webhooks", "~> 0.2"
41
+ spec.add_dependency "thor", "~> 0.19"
42
+ end
metadata ADDED
@@ -0,0 +1,267 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mihari
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Manabu Niseki
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vcr
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: addressable
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.6'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.6'
111
+ - !ruby/object:Gem::Dependency
112
+ name: censu
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: email_address
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.1'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.1'
139
+ - !ruby/object:Gem::Dependency
140
+ name: hachi
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.1'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.1'
153
+ - !ruby/object:Gem::Dependency
154
+ name: mem
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.1'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: public_suffix
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '3.0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '3.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: slack-incoming-webhooks
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '0.2'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '0.2'
195
+ - !ruby/object:Gem::Dependency
196
+ name: thor
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '0.19'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '0.19'
209
+ description: A framework for continuous malicious hosts monitoring.
210
+ email:
211
+ - manabu.niseki@gmail.com
212
+ executables:
213
+ - mihari
214
+ extensions: []
215
+ extra_rdoc_files: []
216
+ files:
217
+ - ".gitignore"
218
+ - ".rspec"
219
+ - ".travis.yml"
220
+ - Gemfile
221
+ - LICENSE
222
+ - README.md
223
+ - Rakefile
224
+ - bin/console
225
+ - bin/setup
226
+ - examples/ipinfo_hosted_domains.rb
227
+ - examples/vt_passive_dns.rb
228
+ - exe/mihari
229
+ - lib/mihari.rb
230
+ - lib/mihari/analyzers/base.rb
231
+ - lib/mihari/analyzers/basic.rb
232
+ - lib/mihari/analyzers/censys.rb
233
+ - lib/mihari/analyzers/shodan.rb
234
+ - lib/mihari/artifact.rb
235
+ - lib/mihari/cli.rb
236
+ - lib/mihari/errors.rb
237
+ - lib/mihari/notifiers/base.rb
238
+ - lib/mihari/notifiers/slack.rb
239
+ - lib/mihari/notifiers/the_hive.rb
240
+ - lib/mihari/the_hive.rb
241
+ - lib/mihari/type_checker.rb
242
+ - lib/mihari/version.rb
243
+ - mihari.gemspec
244
+ homepage: https://github.com/ninoseki/mihari
245
+ licenses:
246
+ - MIT
247
+ metadata: {}
248
+ post_install_message:
249
+ rdoc_options: []
250
+ require_paths:
251
+ - lib
252
+ required_ruby_version: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - ">="
255
+ - !ruby/object:Gem::Version
256
+ version: '0'
257
+ required_rubygems_version: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - ">="
260
+ - !ruby/object:Gem::Version
261
+ version: '0'
262
+ requirements: []
263
+ rubygems_version: 3.0.2
264
+ signing_key:
265
+ specification_version: 4
266
+ summary: A framework for continuous malicious hosts monitoring.
267
+ test_files: []