apullo 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: 576546b660b048b2b4949b8d6c721d59e0f62b4a6b9bcf89208e16af2ce303f5
4
+ data.tar.gz: 46ed8b546222f277e73d5659292b69da1778781dc64b4b6c51580eeadd5a3d1f
5
+ SHA512:
6
+ metadata.gz: b78b1a4ee1a227b5cf31e84d3b7c26db972df8b3382548282fd9187f0c5899919172a10048037bbb1642b61113004ef9718f7bfc921eec01573861081a6fde6b
7
+ data.tar.gz: 0bd921111b05851a64981c9c606cd414aef821458a4bea6b5475bf9f1b318351f2959174de3d720196ab63b94460adb4c9673cbe7d09d914e6d3ca06af0daed8
data/.gitignore ADDED
@@ -0,0 +1,52 @@
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_status
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
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in apullo.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,193 @@
1
+ # apullo
2
+
3
+ [![Build Status](https://travis-ci.com/ninoseki/apullo.svg?branch=master)](https://travis-ci.com/ninoseki/apullo)
4
+ [![Coverage Status](https://coveralls.io/repos/github/ninoseki/apullo/badge.svg?branch=master)](https://coveralls.io/github/ninoseki/apullo?branch=master)
5
+ [![CodeFactor](https://www.codefactor.io/repository/github/ninoseki/apullo/badge)](https://www.codefactor.io/repository/github/ninoseki/apullo)
6
+
7
+ ![eyecatch](./images/eyecatch.png)
8
+
9
+ A scanner for taking basic fingerprints.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ gem install apullo
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ $ apullo
21
+ Commands:
22
+ apullo check [Target] # Take fingerprints from a target(IP, domain or URL)
23
+ apullo help [COMMAND] # Describe available commands or one specific command
24
+
25
+ ```
26
+
27
+ It takes basic network fingerprints of a target.
28
+
29
+ - Hashes of an HTTP response body
30
+ - Hashes of an SSL certificate
31
+ - Hashes of a favicon image
32
+ - Hashes of an SSH host key
33
+ - DNS records
34
+ - WHOIS registrant data
35
+
36
+ ```bash
37
+ $ apullo check https://example.com
38
+ {
39
+ "http": {
40
+ "body": {
41
+ "md5": "84238dfc8092e5d9c0dac8ef93371a07",
42
+ "mmh3": -2087618365,
43
+ "sha1": "4a3ce8ee11e091dd7923f4d8c6e5b5e41ec7c047",
44
+ "sha256": "ea8fac7c65fb589b0d53560f5251f74f9e9b243478dcb6b3ea79b5e36449c8d9"
45
+ },
46
+ "cert": {
47
+ "md5": "3510c21c66bd62010fc547d3cd3f0ce6",
48
+ "serial": "21020869104500376438182461249190639870",
49
+ "sha1": "7bb698386970363d2919cc5772846984ffd4a889",
50
+ "sha256": "9250711c54de546f4370e0c3d3a3ec45bc96092a25a4a71a1afa396af7047eb8"
51
+ },
52
+ "favicon": {
53
+ }
54
+ },
55
+ "domain": {
56
+ "whois": {
57
+ "registrant_contacts": [
58
+ {
59
+ "id": null,
60
+ "type": 1,
61
+ "name": null,
62
+ "organization": "Internet Assigned Numbers Authority",
63
+ "address": null,
64
+ "city": null,
65
+ "zip": null,
66
+ "state": null,
67
+ "country": null,
68
+ "country_code": null,
69
+ "phone": null,
70
+ "fax": null,
71
+ "email": null,
72
+ "url": null,
73
+ "created_on": null,
74
+ "updated_on": null
75
+ }
76
+ ],
77
+ "admin_contacts": [
78
+
79
+ ],
80
+ "technical_contacts": [
81
+
82
+ ]
83
+ },
84
+ "dns": {
85
+ "ns": [
86
+ "a.iana-servers.net",
87
+ "b.iana-servers.net"
88
+ ],
89
+ "cname": [
90
+
91
+ ],
92
+ "soa": [
93
+ "noc.dns.icann.org"
94
+ ],
95
+ "mx": [
96
+
97
+ ],
98
+ "a": [
99
+ "93.184.216.34"
100
+ ],
101
+ "aaaa": [
102
+ "2606:2800:220:1:248:1893:25C8:1946"
103
+ ]
104
+ }
105
+ },
106
+ "ssh": {
107
+ },
108
+ "meta": {
109
+ "target": "https://example.com"
110
+ }
111
+ }
112
+
113
+ $ apullo check jppost-be.top
114
+ {
115
+ "http": {
116
+ "body": {
117
+ "md5": "74ad15c4ab3f67eee1d546e22248931f",
118
+ "mmh3": -330759974,
119
+ "sha1": "c0280893956852b0c07ae4da752ee5d776d248b8",
120
+ "sha256": "28fa3b0beaf188d48b32557fa4df8f0aa451bd10f8e8bb26e919009d2d41b8fb"
121
+ },
122
+ "cert": {
123
+ },
124
+ "favicon": {
125
+ "md5": "ad184c25a1a01d97696dcb59a1ffef74",
126
+ "mmh3": 111036816,
127
+ "sha1": "cb4842a54c3e96408765290cb810793302c17f0b",
128
+ "sha256": "6949c58f841fa21a89e2e2375ae5645e1db62385f89a0218766f2b0a9c490fb8",
129
+ "meta": {
130
+ "url": "https://www.post.japanpost.jp/img/common/touch-icon.png"
131
+ }
132
+ }
133
+ },
134
+ "domain": {
135
+ "whois": {
136
+ "registrant_contacts": [
137
+
138
+ ],
139
+ "admin_contacts": [
140
+
141
+ ],
142
+ "technical_contacts": [
143
+
144
+ ]
145
+ },
146
+ "dns": {
147
+ "ns": [
148
+ "ns1.bdydns.cn",
149
+ "ns2.bdydns.cn"
150
+ ],
151
+ "cname": [
152
+
153
+ ],
154
+ "soa": [
155
+ "sa.dudns.com"
156
+ ],
157
+ "mx": [
158
+
159
+ ],
160
+ "a": [
161
+ "193.148.69.12"
162
+ ],
163
+ "aaaa": [
164
+
165
+ ]
166
+ }
167
+ },
168
+ "ssh": {
169
+ "rsa": {
170
+ "md5": "960bb068dbfb9aa9f9d6899c15844fca",
171
+ "sha1": "d36555028decde1f931b47c90e469fc52e8f364a",
172
+ "sha256": "cf3c7ea7b9442f71423f2253a9c0e448fd0d619e1abc7e519499cd789fac6e74"
173
+ },
174
+ "ecdsa-sha2-nistp256": {
175
+ "md5": "551222e53a38c10817653a723e6caf0c",
176
+ "sha1": "cd6044db29b30d35f32e26e74d66258570cd6527",
177
+ "sha256": "0664dbea7580f9430da6d0ba13e7a4bba0f1efd449c895a6adcc147abc958ce6"
178
+ },
179
+ "ed25519": {
180
+ "md5": "6da2245d9a211731c2d229ea7cce829b",
181
+ "sha1": "d86afd8fca1a052249ef3a0ee26a24f6cc644485",
182
+ "sha256": "7f0d4b642ea2c236eca4018a2dadff3b8a03c37745f9a9f741d9d246a420f358"
183
+ }
184
+ },
185
+ "meta": {
186
+ "target": "jppost-be.top"
187
+ }
188
+ }
189
+ ```
190
+
191
+ ## License
192
+
193
+ 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/apullo.gemspec ADDED
@@ -0,0 +1,43 @@
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 "apullo/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "apullo"
9
+ spec.version = Apullo::VERSION
10
+ spec.authors = ["Manabu Niseki"]
11
+ spec.email = ["manabu.niseki@gmail.com"]
12
+
13
+ spec.summary = "A scanner for basic network fingerprints"
14
+ spec.description = 'A scanner for basic network fingerprints'
15
+ spec.homepage = "https://github.com/ninoseki/apullo"
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", "~> 13.0"
30
+ spec.add_development_dependency "rspec", "~> 3.9"
31
+ spec.add_development_dependency "vcr", "~> 5.0"
32
+ spec.add_development_dependency "webmock", "~> 3.7"
33
+
34
+ spec.add_dependency "addressable", "~> 2.7"
35
+ spec.add_dependency "ipaddr", "~> 1.2"
36
+ spec.add_dependency "mem", "~> 0.1"
37
+ spec.add_dependency "murmurhash3", "~> 0.1"
38
+ spec.add_dependency "oga", "~> 2.15"
39
+ spec.add_dependency "public_suffix", "~> 4.0"
40
+ spec.add_dependency "ssh_scan", "~> 0.0"
41
+ spec.add_dependency "whois", "~> 5.0"
42
+ spec.add_dependency "whois-parser", "~> 1.2"
43
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "apullo"
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
data/exe/apullo 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 "apullo"
7
+
8
+ Apullo::CLI.start
Binary file
data/lib/apullo.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mem"
4
+
5
+ module Apullo
6
+ class << self
7
+ include Mem
8
+
9
+ def fingerprints
10
+ []
11
+ end
12
+ memoize :fingerprints
13
+ end
14
+
15
+ class Error < StandardError; end
16
+ end
17
+
18
+ require "apullo/version"
19
+
20
+ require "apullo/target"
21
+ require "apullo/hash"
22
+ require "apullo/fingerprints/base"
23
+
24
+ require "apullo/fingerprints/http"
25
+ require "apullo/fingerprints/domain"
26
+ require "apullo/fingerprints/ssh"
27
+ require "apullo/fingerprints/favicon"
28
+
29
+ require "apullo/cli"
data/lib/apullo/cli.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+
6
+ module Apullo
7
+ class CLI < Thor
8
+ desc "check [Target]", "Take fingerprints from a target(IP, domain or URL)"
9
+ def check(target)
10
+ target = Target.new(target)
11
+
12
+ results = build_results(target)
13
+ meta = { target: target.id }
14
+ results = results.merge(meta: meta)
15
+
16
+ puts JSON.pretty_generate(results)
17
+ end
18
+
19
+ no_commands do
20
+ def build_results(target)
21
+ unless target.valid?
22
+ return {
23
+ error: "Invalid target is given. Target should be an IP, domain or URL."
24
+ }
25
+ end
26
+
27
+ Apullo.fingerprints.map do |klass|
28
+ fingerprint = klass.new(target)
29
+ [fingerprint.name, fingerprint.results]
30
+ end.to_h
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apullo
4
+ module Fingerprint
5
+ class Base
6
+ attr_reader :target
7
+
8
+ def initialize(target)
9
+ @target = target
10
+ end
11
+
12
+ def name
13
+ self.class.to_s.split("::").last.to_s.downcase
14
+ end
15
+
16
+ def results
17
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
18
+ end
19
+
20
+ class << self
21
+ def inherited(child)
22
+ Apullo.fingerprints << child
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "whois-parser"
5
+
6
+ module Apullo
7
+ module Fingerprint
8
+ class Domain < Base
9
+ RESOURCES = [
10
+ Resolv::DNS::Resource::IN::NS,
11
+ Resolv::DNS::Resource::IN::CNAME,
12
+ Resolv::DNS::Resource::IN::SOA,
13
+ Resolv::DNS::Resource::IN::MX,
14
+ Resolv::DNS::Resource::IN::A,
15
+ Resolv::DNS::Resource::IN::AAAA,
16
+ ].freeze
17
+
18
+ def results
19
+ {
20
+ dns: resources,
21
+ whois: contacts,
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def dns
28
+ @dns ||= Resolv::DNS.new
29
+ end
30
+
31
+ def resources
32
+ @resources ||= RESOURCES.map do |resource|
33
+ name = resource.to_s.split("::").last.to_s.downcase
34
+ values = resolve(resource)
35
+ [name, values]
36
+ end.to_h
37
+ end
38
+
39
+ def resolve(resource)
40
+ ress = dns.getresources(target.host, resource)
41
+ values = ress.map do |res|
42
+ if res.respond_to?(:address)
43
+ res.address.to_s
44
+ elsif res.respond_to?(:exchange)
45
+ res.exchange.to_s
46
+ elsif res.respond_to?(:name)
47
+ res.name.to_s
48
+ elsif res.respond_to?(:rname)
49
+ res.rname.to_s
50
+ end
51
+ end.compact
52
+ values.reject(&:empty?).empty? ? [] : values
53
+ end
54
+
55
+ def whois
56
+ @whois ||= Whois.whois(target.host)
57
+ rescue Timeout::Error, Whois::Error
58
+ nil
59
+ end
60
+
61
+ def registrant_contacts
62
+ whois&.parser&.registrant_contacts&.map(&:to_h)
63
+ rescue Whois::ParserError => _e
64
+ []
65
+ end
66
+
67
+ def admin_contacts
68
+ whois&.parser&.admin_contacts&.map(&:to_h)
69
+ rescue Whois::ParserError => _e
70
+ []
71
+ end
72
+
73
+ def technical_contacts
74
+ whois&.parser&.technical_contacts&.map(&:to_h)
75
+ rescue Whois::ParserError => _e
76
+ []
77
+ end
78
+
79
+ def contacts
80
+ return {} unless whois
81
+
82
+ {
83
+ registrant_contacts: registrant_contacts,
84
+ admin_contacts: admin_contacts,
85
+ technical_contacts: technical_contacts
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "base64"
6
+
7
+ module Apullo
8
+ module Fingerprint
9
+ class Favicon
10
+ attr_reader :uri
11
+
12
+ def initialize(url)
13
+ @uri = URI(url)
14
+ end
15
+
16
+ def results
17
+ data = b64_favicon_data
18
+ return {} unless data
19
+
20
+ hash = Hash.new(data.b)
21
+ {
22
+ md5: hash.md5,
23
+ mmh3: hash.mmh3,
24
+ sha1: hash.sha1,
25
+ sha256: hash.sha256,
26
+ meta: {
27
+ url: uri.to_s
28
+ }
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def b64_favicon_data
35
+ @b64_favicon_data ||= [].tap do |out|
36
+ data = get(uri.path)
37
+ break unless data
38
+
39
+ b64 = Base64.strict_encode64(data)
40
+ out << b64.chars.each_slice(76).map(&:join).join("\n") + "\n"
41
+ end.first
42
+ end
43
+
44
+ def get(path)
45
+ http = build_http
46
+ path = path.empty? ? "/" : path
47
+ request = Net::HTTP::Get.new(path)
48
+ response = http.request(request)
49
+ response.body
50
+ rescue Errno::ECONNREFUSED, Net::HTTPError, OpenSSL::OpenSSLError => _e
51
+ nil
52
+ end
53
+
54
+ def build_http
55
+ if uri.scheme == "http"
56
+ Net::HTTP.start(uri.host, uri.port)
57
+ else
58
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "oga"
5
+ require "openssl"
6
+
7
+ module Apullo
8
+ module Fingerprint
9
+ class HTTP < Base
10
+ def results
11
+ @results ||= [].tap do |out|
12
+ get(target.uri.path)
13
+
14
+ out << {
15
+ body: body,
16
+ cert: cert,
17
+ favicon: favicon
18
+ }
19
+ end.first
20
+ end
21
+
22
+ def cert
23
+ return {} unless @peer_cert
24
+
25
+ hash = Hash.new(@peer_cert.to_der)
26
+ {
27
+ md5: hash.md5,
28
+ serial: @peer_cert.serial,
29
+ sha1: hash.sha1,
30
+ sha256: hash.sha256,
31
+ }
32
+ end
33
+
34
+ def body
35
+ return {} unless @body
36
+
37
+ hash = Hash.new(@body)
38
+ {
39
+ md5: hash.md5,
40
+ mmh3: hash.mmh3,
41
+ sha1: hash.sha1,
42
+ sha256: hash.sha256,
43
+ }
44
+ end
45
+
46
+ def favicon
47
+ url = favicon_url
48
+ return {} unless url
49
+
50
+ favicon = Favicon.new(url)
51
+ favicon.results
52
+ end
53
+
54
+ private
55
+
56
+ def favicon_url
57
+ return nil unless @body
58
+
59
+ doc = Oga.parse_html(@body)
60
+ icon = doc.at_css("link[rel='shortcut icon']") || doc.at_css("link[rel='icon']")
61
+ return nil unless icon
62
+
63
+ href = icon.get("href")
64
+ return nil unless href
65
+
66
+ href.start_with?("http://", "https://") ? href : target.url + href
67
+ end
68
+
69
+ def get(path, limit: 3)
70
+ http = build_http
71
+ path = path.empty? ? "/" : path
72
+ request = Net::HTTP::Get.new(path)
73
+ response = http.request request
74
+
75
+ location = response["Location"]
76
+ if location && limit.positive?
77
+ get(location, limit: limit - 1)
78
+ else
79
+ @peer_cert = http.peer_cert
80
+ @body = response.body
81
+ @path = path
82
+ end
83
+ rescue Errno::ECONNREFUSED, Net::HTTPError, OpenSSL::OpenSSLError => _e
84
+ nil
85
+ end
86
+
87
+ def build_http
88
+ if target.scheme == "http"
89
+ Net::HTTP.start(target.uri.host, target.uri.port)
90
+ else
91
+ Net::HTTP.start(target.uri.host, target.uri.port, use_ssl: true)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ssh_scan"
4
+
5
+ module Apullo
6
+ module Fingerprint
7
+ class SSH < Base
8
+ DEFAULT_OPTIONS = { "timeout" => 3 }.freeze
9
+ DEFAULT_PORT = 22
10
+
11
+ def results
12
+ @results ||= pluck
13
+ end
14
+
15
+ private
16
+
17
+ def pluck
18
+ result = scan
19
+ keys = result.dig("keys") || []
20
+ keys.map do |cipher, data|
21
+ fingerprints = data.dig("fingerprints") || []
22
+ normalized = fingerprints.map do |hash, value|
23
+ [hash, value.delete(":")]
24
+ end.to_h
25
+ [cipher, normalized]
26
+ end.to_h
27
+ end
28
+
29
+ def scan
30
+ return {} unless target.ipv4
31
+
32
+ engine = SSHScan::ScanEngine.new
33
+ dest = "#{target.host}:#{DEFAULT_PORT}"
34
+ result = engine.scan_target(dest, DEFAULT_OPTIONS)
35
+ result.to_hash
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+ require "murmurhash3"
5
+
6
+ module Apullo
7
+ class Hash
8
+ attr_reader :data
9
+
10
+ def initialize(data)
11
+ @data = data
12
+ end
13
+
14
+ def sha1
15
+ Digest::SHA1.hexdigest data
16
+ end
17
+
18
+ def sha256
19
+ Digest::SHA256.hexdigest data
20
+ end
21
+
22
+ def md5
23
+ Digest::MD5.hexdigest data
24
+ end
25
+
26
+ def mmh3
27
+ hash = MurmurHash3::V32.str_hash(data)
28
+ if (hash & 0x80000000).zero?
29
+ hash
30
+ else
31
+ -((hash ^ 0xFFFFFFFF) + 1)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "addressable/uri"
4
+ require "ipaddr"
5
+ require "public_suffix"
6
+ require "resolv"
7
+
8
+ module Apullo
9
+ class Target
10
+ attr_reader :id
11
+
12
+ def initialize(id)
13
+ @id = id
14
+ end
15
+
16
+ def ipv4
17
+ @ipv4 ||= resolve
18
+ end
19
+
20
+ def host
21
+ @host ||= uri&.host
22
+ end
23
+
24
+ def scheme
25
+ @scheme ||= uri&.scheme
26
+ end
27
+
28
+ def url
29
+ @url ||= uri&.to_s
30
+ end
31
+
32
+ def uri
33
+ @uri ||= Addressable::URI.parse(_url)
34
+ rescue Addressable::URI::InvalidURIError => _e
35
+ nil
36
+ end
37
+
38
+ def valid?
39
+ uri && (ip? | domain?)
40
+ end
41
+
42
+ private
43
+
44
+ def _url
45
+ @_url ||= id.start_with?("http://", "https://") ? id : "http://#{id}"
46
+ end
47
+
48
+ def ip?
49
+ IPAddr.new host
50
+ true
51
+ rescue IPAddr::InvalidAddressError => _e
52
+ false
53
+ end
54
+
55
+ def domain?
56
+ return false if host.match? /[0-9]\z/
57
+
58
+ PublicSuffix.valid?(host, default_rule: nil)
59
+ end
60
+
61
+ def resolve
62
+ Resolv.getaddress uri&.host
63
+ rescue Resolv::ResolvError => _e
64
+ nil
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apullo
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,276 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apullo
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-11-04 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: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
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.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vcr
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.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.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.7'
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.7'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
111
+ - !ruby/object:Gem::Dependency
112
+ name: ipaddr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: mem
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: murmurhash3
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: oga
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.15'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.15'
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: '4.0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '4.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: ssh_scan
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '0.0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '0.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: whois
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '5.0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '5.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: whois-parser
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '1.2'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '1.2'
223
+ description: A scanner for basic network fingerprints
224
+ email:
225
+ - manabu.niseki@gmail.com
226
+ executables:
227
+ - apullo
228
+ extensions: []
229
+ extra_rdoc_files: []
230
+ files:
231
+ - ".gitignore"
232
+ - ".rspec"
233
+ - ".travis.yml"
234
+ - Gemfile
235
+ - LICENSE
236
+ - README.md
237
+ - Rakefile
238
+ - apullo.gemspec
239
+ - bin/console
240
+ - bin/setup
241
+ - exe/apullo
242
+ - images/eyecatch.png
243
+ - lib/apullo.rb
244
+ - lib/apullo/cli.rb
245
+ - lib/apullo/fingerprints/base.rb
246
+ - lib/apullo/fingerprints/domain.rb
247
+ - lib/apullo/fingerprints/favicon.rb
248
+ - lib/apullo/fingerprints/http.rb
249
+ - lib/apullo/fingerprints/ssh.rb
250
+ - lib/apullo/hash.rb
251
+ - lib/apullo/target.rb
252
+ - lib/apullo/version.rb
253
+ homepage: https://github.com/ninoseki/apullo
254
+ licenses:
255
+ - MIT
256
+ metadata: {}
257
+ post_install_message:
258
+ rdoc_options: []
259
+ require_paths:
260
+ - lib
261
+ required_ruby_version: !ruby/object:Gem::Requirement
262
+ requirements:
263
+ - - ">="
264
+ - !ruby/object:Gem::Version
265
+ version: '0'
266
+ required_rubygems_version: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ requirements: []
272
+ rubygems_version: 3.0.3
273
+ signing_key:
274
+ specification_version: 4
275
+ summary: A scanner for basic network fingerprints
276
+ test_files: []