miteru 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/gem.yml +36 -0
  3. data/.github/workflows/{test.yml → ruby.yml} +4 -13
  4. data/.gitignore +4 -1
  5. data/.rspec +1 -1
  6. data/README.md +7 -17
  7. data/docker-compose.yml +12 -0
  8. data/exe/miteru +3 -3
  9. data/lefthook.yml +9 -0
  10. data/lib/miteru/cli/application.rb +27 -0
  11. data/lib/miteru/cli/base.rb +16 -0
  12. data/lib/miteru/cli/database.rb +11 -0
  13. data/lib/miteru/commands/database.rb +23 -0
  14. data/lib/miteru/commands/main.rb +37 -0
  15. data/lib/miteru/commands/sidekiq.rb +35 -0
  16. data/lib/miteru/commands/web.rb +37 -0
  17. data/lib/miteru/concerns/database_connectable.rb +16 -0
  18. data/lib/miteru/concerns/error_unwrappable.rb +30 -0
  19. data/lib/miteru/config.rb +98 -0
  20. data/lib/miteru/crawler.rb +28 -44
  21. data/lib/miteru/database.rb +50 -38
  22. data/lib/miteru/downloader.rb +52 -41
  23. data/lib/miteru/errors.rb +37 -0
  24. data/lib/miteru/feeds/ayashige.rb +9 -20
  25. data/lib/miteru/feeds/base.rb +141 -0
  26. data/lib/miteru/feeds/phishing_database.rb +11 -10
  27. data/lib/miteru/feeds/urlscan.rb +47 -19
  28. data/lib/miteru/feeds/urlscan_pro.rb +20 -18
  29. data/lib/miteru/http.rb +51 -0
  30. data/lib/miteru/kit.rb +28 -20
  31. data/lib/miteru/mixin.rb +2 -29
  32. data/lib/miteru/notifiers/base.rb +10 -3
  33. data/lib/miteru/notifiers/slack.rb +85 -10
  34. data/lib/miteru/notifiers/urlscan.rb +29 -14
  35. data/lib/miteru/orchestrator.rb +51 -0
  36. data/lib/miteru/record.rb +8 -15
  37. data/lib/miteru/service.rb +28 -0
  38. data/lib/miteru/sidekiq/application.rb +13 -0
  39. data/lib/miteru/sidekiq/jobs.rb +21 -0
  40. data/lib/miteru/version.rb +1 -1
  41. data/lib/miteru/web/application.rb +42 -0
  42. data/lib/miteru/website.rb +48 -48
  43. data/lib/miteru.rb +130 -22
  44. data/miteru-sidekiq.service +13 -0
  45. data/miteru.db-shm +0 -0
  46. data/miteru.db-wal +0 -0
  47. data/miteru.gemspec +49 -38
  48. metadata +265 -97
  49. data/.overcommit.yml +0 -12
  50. data/.standard.yml +0 -4
  51. data/lib/miteru/attachement.rb +0 -74
  52. data/lib/miteru/cli.rb +0 -41
  53. data/lib/miteru/configuration.rb +0 -122
  54. data/lib/miteru/error.rb +0 -7
  55. data/lib/miteru/feeds/feed.rb +0 -53
  56. data/lib/miteru/feeds/phishstats.rb +0 -28
  57. data/lib/miteru/feeds.rb +0 -45
  58. data/lib/miteru/http_client.rb +0 -85
data/.standard.yml DELETED
@@ -1,4 +0,0 @@
1
- ignore:
2
- - "**/*":
3
- - Layout/SpaceInsideHashLiteralBraces
4
- - Style/RescueStandardError
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "uri"
4
-
5
- module Miteru
6
- class Attachement
7
- attr_reader :url
8
-
9
- def initialize(url)
10
- @url = url
11
- end
12
-
13
- def to_a
14
- [
15
- {
16
- text: defanged_url,
17
- fallback: "VT & urlscan.io links",
18
- actions: actions
19
- }
20
- ]
21
- end
22
-
23
- private
24
-
25
- def actions
26
- [vt_link, urlscan_link].compact
27
- end
28
-
29
- def vt_link
30
- return nil unless _vt_link
31
-
32
- {
33
- type: "button",
34
- text: "Lookup on VirusTotal",
35
- url: _vt_link
36
- }
37
- end
38
-
39
- def urlscan_link
40
- return nil unless _urlscan_link
41
-
42
- {
43
- type: "button",
44
- text: "Lookup on urlscan.io",
45
- url: _urlscan_link
46
- }
47
- end
48
-
49
- def defanged_url
50
- @defanged_url ||= url.to_s.gsub(/\./, "[.]")
51
- end
52
-
53
- def domain
54
- @domain ||=
55
- [].tap do |out|
56
- out << URI(url).hostname
57
- rescue URI::Error => _e
58
- out << nil
59
- end.first
60
- end
61
-
62
- def _urlscan_link
63
- return nil unless domain
64
-
65
- "https://urlscan.io/domain/#{domain}"
66
- end
67
-
68
- def _vt_link
69
- return nil unless domain
70
-
71
- "https://www.virustotal.com/#/domain/#{domain}"
72
- end
73
- end
74
- end
data/lib/miteru/cli.rb DELETED
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "thor"
4
-
5
- module Miteru
6
- class CLI < Thor
7
- method_option :auto_download, type: :boolean, default: false, desc: "Enable or disable auto-download of phishing kits"
8
- method_option :ayashige, type: :boolean, default: false, desc: "Enable or disable Ayashige(ninoseki/ayashige) feed"
9
- method_option :directory_traveling, type: :boolean, default: false, desc: "Enable or disable directory traveling"
10
- method_option :download_to, type: :string, default: "/tmp", desc: "Directory to download phishing kits"
11
- method_option :post_to_slack, type: :boolean, default: false, desc: "Enable or disable Slack notification"
12
- method_option :size, type: :numeric, default: 100, desc: "Number of urlscan.io's results to fetch. (Max: 10,000)"
13
- method_option :threads, type: :numeric, desc: "Number of threads to use"
14
- method_option :verbose, type: :boolean, default: true
15
- desc "execute", "Execute the crawler"
16
- def execute
17
- Miteru.configure do |config|
18
- config.auto_download = options["auto_download"]
19
- config.ayashige = options["ayashige"]
20
- config.directory_traveling = options["directory_traveling"]
21
- config.download_to = options["download_to"]
22
- config.post_to_slack = options["post_to_slack"]
23
- config.size = options["size"]
24
- config.verbose = options["verbose"]
25
-
26
- threads = options["threads"]
27
- config.threads = threads if threads
28
- end
29
-
30
- Crawler.execute
31
- end
32
-
33
- default_command :execute
34
-
35
- class << self
36
- def exit_on_failure?
37
- true
38
- end
39
- end
40
- end
41
- end
@@ -1,122 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "parallel"
4
-
5
- module Miteru
6
- class Configuration
7
- # @return [Boolean]
8
- attr_accessor :auto_download
9
-
10
- # @return [Boolean]
11
- attr_accessor :ayashige
12
-
13
- # @return [Boolean]
14
- attr_accessor :directory_traveling
15
-
16
- # @return [String]
17
- attr_accessor :download_to
18
-
19
- # @return [Boolean]
20
- attr_accessor :post_to_slack
21
-
22
- # @return [Integer]
23
- attr_accessor :size
24
-
25
- # @return [Integer]
26
- attr_accessor :threads
27
-
28
- # @return [Boolean]
29
- attr_accessor :verbose
30
-
31
- # @return [String]
32
- attr_accessor :database
33
-
34
- # @return [String, nil]
35
- attr_accessor :slack_webhook_url
36
-
37
- # @return [String]
38
- attr_accessor :slack_channel
39
-
40
- # @return [String]
41
- attr_accessor :urlscan_api_key
42
-
43
- # @return [String]
44
- attr_accessor :urlscan_submit_visibility
45
-
46
- # @return [Array<String>]
47
- attr_reader :valid_extensions
48
-
49
- # @return [Array<String>]
50
- attr_reader :valid_mime_types
51
-
52
- # @return [Integer]
53
- attr_reader :file_maxsize
54
-
55
- def initialize
56
- @auto_download = false
57
- @ayashige = false
58
- @directory_traveling = false
59
- @download_to = "/tmp"
60
- @post_to_slack = false
61
- @size = 100
62
- @threads = Parallel.processor_count
63
- @verbose = false
64
- @database = ENV.fetch("MITERU_DATABASE", "miteru.db")
65
- @file_maxsize = ENV.fetch("FILE_MAXSIZE", 1024 * 1024 * 100).to_i
66
-
67
- @slack_webhook_url = ENV.fetch("SLACK_WEBHOOK_URL", nil)
68
- @slack_channel = ENV.fetch("SLACK_CHANNEL", "#general")
69
-
70
- @urlscan_api_key = ENV.fetch("URLSCAN_API_KEY", nil)
71
- @urlscan_submit_visibility = ENV.fetch("URLSCAN_SUBMIT_VISIBILITY", "public")
72
-
73
- @valid_extensions = [".zip", ".rar", ".7z", ".tar", ".gz"].freeze
74
- @valid_mime_types = ["application/zip", "application/vnd.rar", "application/x-7z-compressed", "application/x-tar", "application/gzip"]
75
- end
76
-
77
- def auto_download?
78
- @auto_download
79
- end
80
-
81
- def ayashige?
82
- @ayashige
83
- end
84
-
85
- def directory_traveling?
86
- @directory_traveling
87
- end
88
-
89
- def post_to_slack?
90
- @post_to_slack
91
- end
92
-
93
- def verbose?
94
- @verbose
95
- end
96
-
97
- def slack_webhook_url?
98
- !@slack_webhook_url.nil?
99
- end
100
-
101
- def urlscan_api_key?
102
- !@urlscan_api_key.nil?
103
- end
104
- end
105
-
106
- class << self
107
- # @return [Miteru::Configuration] Miteru's current configuration
108
- def configuration
109
- @configuration ||= Configuration.new
110
- end
111
-
112
- # Set Miteru's configuration
113
- # @param config [Miteru::Configuration]
114
- attr_writer :configuration
115
-
116
- # Modify Miteru's current configuration
117
- # @yieldparam [Miteru::Configuration] config current Miteru config
118
- def configure
119
- yield configuration
120
- end
121
- end
122
- end
data/lib/miteru/error.rb DELETED
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Miteru
4
- class HTTPResponseError < StandardError; end
5
-
6
- class DownloadError < StandardError; end
7
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Miteru
4
- class Feeds
5
- class Feed
6
- include Mixins::URL
7
-
8
- def source
9
- @source ||= self.class.to_s.split("::").last
10
- end
11
-
12
- #
13
- # Return URLs
14
- #
15
- # @return [Array<String>] URLs
16
- #
17
- def urls
18
- raise NotImplementedError, "You must implement #{self.class}##{__method__}"
19
- end
20
-
21
- #
22
- # Return entries
23
- #
24
- # @return [Array<Miteru::Entry>]
25
- #
26
- def entries
27
- breakdowend_urls.map do |url|
28
- Entry.new(url, source)
29
- end
30
- end
31
-
32
- #
33
- # Return breakdowned URLs
34
- #
35
- # @return [Array<String>] Breakdowned URLs
36
- #
37
- def breakdowend_urls
38
- urls.select { |url| url.start_with?("http://", "https://") }.map do |url|
39
- breakdown(url, Miteru.configuration.directory_traveling?)
40
- end.flatten.uniq
41
- end
42
-
43
- private
44
-
45
- def get(url)
46
- res = HTTPClient.get(url)
47
- raise HTTPResponseError if res.code != 200
48
-
49
- res.body.to_s
50
- end
51
- end
52
- end
53
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "uri"
5
-
6
- module Miteru
7
- class Feeds
8
- class PhishStats < Feed
9
- URL = "https://phishstats.info:2096/api/phishing?_sort=-id&size=100"
10
-
11
- def urls
12
- json = JSON.parse(get(URL))
13
- json.map do |entry|
14
- entry["url"]
15
- end
16
- rescue HTTPResponseError, HTTP::Error, JSON::ParserError => e
17
- Miteru.logger.error "Failed to load PhishStats feed (#{e})"
18
- []
19
- end
20
-
21
- private
22
-
23
- def url_for(path)
24
- URI(URL + path)
25
- end
26
- end
27
- end
28
- end
data/lib/miteru/feeds.rb DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "./feeds/feed"
4
- require_relative "./feeds/phishing_database"
5
- require_relative "./feeds/phishstats"
6
- require_relative "./feeds/ayashige"
7
- require_relative "./feeds/urlscan"
8
- require_relative "./feeds/urlscan_pro"
9
-
10
- module Miteru
11
- class Entry
12
- # @return [String]
13
- attr_reader :url
14
- # @return [String]
15
- attr_reader :source
16
-
17
- def initialize(url, source)
18
- @url = url
19
- @source = source
20
- end
21
- end
22
-
23
- class Feeds
24
- IGNORE_EXTENSIONS = %w[.htm .html .php .asp .aspx .exe .txt].freeze
25
-
26
- def initialize
27
- @feeds = [
28
- PhishingDatabase.new,
29
- PhishStats.new,
30
- UrlScan.new(Miteru.configuration.size),
31
- UrlScanPro.new,
32
- Miteru.configuration.ayashige? ? Ayashige.new : nil
33
- ].compact
34
- end
35
-
36
- #
37
- # Returns a list of suspicious entries
38
- #
39
- # @return [Array<Entry>]
40
- #
41
- def suspicious_entries
42
- @suspicious_entries ||= @feeds.map(&:entries).flatten.uniq(&:url)
43
- end
44
- end
45
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "down/http"
4
- require "http"
5
- require "uri"
6
-
7
- module Miteru
8
- class HTTPClient
9
- DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
10
- URLSCAN_UA = "miteru/#{Miteru::VERSION}"
11
-
12
- attr_reader :ssl_context
13
-
14
- def initialize
15
- ctx = OpenSSL::SSL::SSLContext.new
16
- ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
17
- @ssl_context = ctx
18
- end
19
-
20
- def download(url, destination)
21
- down = Down::Http.new(**default_options) { |client| client.headers(**default_headers) }
22
- down.download(url, destination: destination)
23
- destination
24
- end
25
-
26
- def head(url, options = {})
27
- options = options.merge default_options
28
-
29
- HTTP.follow
30
- .timeout(3)
31
- .headers(urlscan_url?(url) ? urlscan_headers : default_headers)
32
- .head(url, options)
33
- end
34
-
35
- def get(url, options = {})
36
- options = options.merge default_options
37
-
38
- HTTP.follow
39
- .timeout(write: 2, connect: 5, read: 10)
40
- .headers(urlscan_url?(url) ? urlscan_headers : default_headers)
41
- .get(url, options)
42
- end
43
-
44
- def post(url, options = {})
45
- HTTP.post url, options
46
- end
47
-
48
- class << self
49
- def download(url, base_dir = "/tmp")
50
- new.download(url, base_dir)
51
- end
52
-
53
- def get(url, options = {})
54
- new.get url, options
55
- end
56
-
57
- def post(url, options = {})
58
- new.post url, options
59
- end
60
-
61
- def head(url, options = {})
62
- new.head url, options
63
- end
64
- end
65
-
66
- private
67
-
68
- def default_headers
69
- { user_agent: DEFAULT_UA }
70
- end
71
-
72
- def default_options
73
- { ssl_context: ssl_context }
74
- end
75
-
76
- def urlscan_headers
77
- { user_agent: URLSCAN_UA }
78
- end
79
-
80
- def urlscan_url?(url)
81
- uri = URI(url)
82
- uri.hostname == "urlscan.io"
83
- end
84
- end
85
- end