ruby-masscan 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: 221aba609e42676f60a8ff8ca50017f52b107ce4bbfd09b2d17fac243e876db8
4
+ data.tar.gz: 92ffeee1fd37297f5a1731fb2af18d5409c99ceda7704beab4f762e9270e85e3
5
+ SHA512:
6
+ metadata.gz: f3cb61b85ec433a38a77e5f3993902fb48c340ee8eb6a2476ad98afc8369f33a55ade52fb75f55bcde9f36251da1cea84e5215f717b33821af733744d22087e6
7
+ data.tar.gz: 4224460de4162eeb091db2d5d194e66eec3c81bea585c64101adadbfc9c959dfdecae482c7c607717d828f2922ef82b99c2d52f99a0b146153cafe3b8e724add
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
data/.editorconfig ADDED
@@ -0,0 +1,11 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+ tab_width = 8
7
+ trim_trailing_whitespace = true
8
+
9
+ [{Gemfile,Rakefile,*.rb,*.gemspec,*.yml}]
10
+ indent_style = space
11
+ indent_size = 2
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on: [ push, pull_request ]
4
+
5
+ jobs:
6
+ tests:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby:
12
+ - 2.4
13
+ - 2.5
14
+ - 2.6
15
+ - 2.7
16
+ - 3.0
17
+ - jruby
18
+ - truffleruby
19
+ name: Ruby ${{ matrix.ruby }}
20
+ steps:
21
+ - uses: actions/checkout@v2
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ - name: Install dependencies
27
+ run: bundle install --jobs 4 --retry 3
28
+ - name: Run tests
29
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /Gemfile.lock
2
+ /coverage
3
+ /doc
4
+ /pkg
5
+ /.yardoc
6
+ .DS_Store
7
+ *.db
8
+ *.log
9
+ *.swp
10
+ *~
11
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title 'Ruby Masscan Documentation' --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,6 @@
1
+ ### 0.1.0 / 2021-08-31
2
+
3
+ * Initial release:
4
+ * Provides a Ruby interface for running the `masscan` command.
5
+ * Supports parsing masscan Binary, List, and JSON output files.
6
+
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'rake'
7
+ gem 'rubygems-tasks', '~> 0.2'
8
+ gem 'rspec', '~> 3.0'
9
+
10
+ gem 'json'
11
+ gem 'simplecov', '~> 0.7'
12
+ gem 'kramdown'
13
+ gem 'yard', '~> 0.9'
14
+ gem 'yard-spellcheck', require: false
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 Hal Brodigan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # ruby-masscan
2
+
3
+ [![CI](https://github.com/postmodern/ruby-masscan/actions/workflows/ruby.yml/badge.svg)](https://github.com/postmodern/ruby-masscan/actions/workflows/ruby.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/ruby-masscan.svg)](https://badge.fury.io/rb/ruby-masscan)
5
+
6
+ * [Source](https://github.com/postmodern/ruby-masscan/)
7
+ * [Issues](https://github.com/postmodern/ruby-masscan/issues)
8
+ * [Documentation](http://rubydoc.info/gems/ruby-masscan/frames)
9
+
10
+ ## Description
11
+
12
+ A Ruby interface to [masscan], an Internet-scale port scanner.
13
+ Allows automating masscan and parsing masscan Binary, List, and JSON output
14
+ file formats.
15
+
16
+ ## Features
17
+
18
+ * Provides a Ruby interface for running the `masscan` command.
19
+ * Supports parsing masscan Binary, List, and JSON output files.
20
+
21
+ ## Examples
22
+
23
+ Run `sudo masscan` from Ruby:
24
+
25
+ ```ruby
26
+ require 'masscan/program'
27
+
28
+ Masscan::Program.sudo_scan do |masscan|
29
+ masscan.output_format = :list
30
+ masscan.output_file = 'masscan.txt'
31
+
32
+ masscan.ips = '192.168.1.1/24'
33
+ masscan.ports = [20,21,22,23,25,80,110,443,512,522,8080,1080]
34
+ end
35
+ ```
36
+
37
+ Parse a `masscan` output file and guess the format:
38
+
39
+ ```ruby
40
+ require 'masscan/output_file'
41
+
42
+ output_file = Masscan::OutputFile.new('masscan.txt')
43
+ output.format
44
+ # => :list
45
+ ```
46
+
47
+ Parse `masscan` Binary output files:
48
+
49
+ ```ruby
50
+ output_file = Masscan::OutputFile.new('masscan.scan', format: :binary)
51
+ output_file.each do |record|
52
+ p record
53
+ end
54
+ ```
55
+
56
+ ```
57
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=80, reason=[:syn, :ack], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:33 -0700, mac=nil>
58
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=443, reason=[:syn, :ack], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:33 -0700, mac=nil>
59
+ #<struct Masscan::Status status=:open, protocol=:icmp, port=0, reason=[], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:33 -0700, mac=nil>
60
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:35 -0700, app_protocol=:ssl3, payload="TLS/1.1 cipher:0xc013, www.example.org, www.example.org, example.com, example.edu, example.net, example.org, www.example.com, www.example.edu, www.example.net">
61
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:35 -0700, app_protocol=:x509_cert, payload="MIIG1TCCBb2gAwIBAgIQD74IsIVNBXOKsMzhya/uyTANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBEaWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMDExMjQwMDAwMDBaFw0yMTEyMjUyMzU5NTlaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLTG9zIEFuZ2VsZXMxPDA6BgNVBAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczEYMBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvzuzMoKCP8Okx2zvgucA5YinrFPEK5RQP1TX7PEYUAoBO6i5hIAsIKFmFxtW2sghERilU5rdnxQcF3fEx3sY4OtY6VSBPLPhLrbKozHLrQ8ZN/rYTb+hgNUeT7NA1mP78IEkxAj4qG5tli4Jq41aCbUlCt7equGXokImhC+UY5IpQEZS0tKD4vu2ksZ04Qetp0k8jWdAvMA27W3EwgHHNeVGWbJPC0Dn7RqPw13r7hFyS5TpleywjdY1nB7ad6kcZXZbEcaFZ7ZuerA6RkPGE+PsnZRb1oFJkYoXimsuvkVFhWeHQXCGC1cuDWSrM3cpQvOzKH2vS7d15+zGls4IwIDAQABo4IDaTCCA2UwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iyxZV2ufQwHQYDVR0OBBYEFCYa+OSxsHKEztqBBtInmPvtOj0XMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUuY29tggtleGFtcGxlLmVkdYILZXhhbXBsZS5uZXSCC2V4YW1wbGUub3Jngg93d3cuZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgYsGA1UdHwSBgzCBgDA+oDygOoY4aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS5jcmwwPqA8oDqGOGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv">
62
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:35 -0700, app_protocol=:http_server, payload="ECS (sec/97A6)">
63
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:35 -0700, app_protocol=:html_title, payload="404 - Not Found">
64
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-26 16:07:35 -0700, app_protocol=:http, payload="HTTP/1.0 404 Not Found\r\nContent-Type: text/html\r\nDate: Thu, 26 Aug 2021 23:07:35 GMT\r\nServer: ECS (sec/97A6)\r\nContent-Length: 345\r\nConnection: close\r\n\r">
65
+ ```
66
+
67
+ Parse `masscan` simple list output files:
68
+
69
+ ```ruby
70
+ output_file = Masscan::OutputFile.new('masscan.txt', format: :list)
71
+ output_file.each do |record|
72
+ p record
73
+ end
74
+ ```
75
+
76
+ ```
77
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=443, reason=nil, ttl=nil, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:50 -0700, mac=nil>
78
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=80, reason=nil, ttl=nil, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:50 -0700, mac=nil>
79
+ #<struct Masscan::Status status=:open, protocol=:icmp, port=0, reason=nil, ttl=nil, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:50 -0700, mac=nil>
80
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:52 -0700, app_protocol=:ssl3, payload="TLS/1.1 cipher:0xc013, www.example.org, www.example.org, example.com, example.edu, example.net, example.org, www.example.com, www.example.edu, www.example.net">
81
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:52 -0700, app_protocol=:x509, payload="MIIG1TCCBb2gAwIBAgIQD74IsIVNBXOKsMzhya/uyTANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBEaWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMDExMjQwMDAwMDBaFw0yMTEyMjUyMzU5NTlaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLTG9zIEFuZ2VsZXMxPDA6BgNVBAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczEYMBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvzuzMoKCP8Okx2zvgucA5YinrFPEK5RQP1TX7PEYUAoBO6i5hIAsIKFmFxtW2sghERilU5rdnxQcF3fEx3sY4OtY6VSBPLPhLrbKozHLrQ8ZN/rYTb+hgNUeT7NA1mP78IEkxAj4qG5tli4Jq41aCbUlCt7equGXokImhC+UY5IpQEZS0tKD4vu2ksZ04Qetp0k8jWdAvMA27W3EwgHHNeVGWbJPC0Dn7RqPw13r7hFyS5TpleywjdY1nB7ad6kcZXZbEcaFZ7ZuerA6RkPGE+PsnZRb1oFJkYoXimsuvkVFhWeHQXCGC1cuDWSrM3cpQvOzKH2vS7d15+zGls4IwIDAQABo4IDaTCCA2UwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iyxZV2ufQwHQYDVR0OBBYEFCYa+OSxsHKEztqBBtInmPvtOj0XMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUuY29tggtleGFtcGxlLmVkdYILZXhhbXBsZS5uZXSCC2V4YW1wbGUub3Jngg93d3cuZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgYsGA1UdHwSBgzCBgDA+oDygOoY4aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS5jcmwwPqA8oDqGOGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv">
82
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:52 -0700, app_protocol=:http_server, payload="ECS (sec/974D)">
83
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:52 -0700, app_protocol=:html_title, payload="404 - Not Found">
84
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:47:52 -0700, app_protocol=:http, payload="HTTP/1.0 404 Not Found\\x0d\\x0aContent-Type: text/html\\x0d\\x0aDate: Thu, 26 Aug 2021 06:47:52 GMT\\x0d\\x0aServer: ECS (sec/974D)\\x0d\\x0aContent-Length: 345\\x0d\\x0aConnection: close\\x0d\\x0a\\x0d">
85
+ ```
86
+
87
+ Parse `masscan` JSON output files:
88
+
89
+ ```ruby
90
+ output_file = Masscan::OutputFile.new('masscan.json', format: :json)
91
+ output_file.each do |record|
92
+ p record
93
+ end
94
+ ```
95
+
96
+ ```
97
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=80, reason=[:syn, :ack], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:21 -0700, mac=nil>
98
+ #<struct Masscan::Status status=:open, protocol=:tcp, port=443, reason=[:syn, :ack], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:21 -0700, mac=nil>
99
+ #<struct Masscan::Status status=:open, protocol=:icmp, port=0, reason=["none"], ttl=54, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:22 -0700, mac=nil>
100
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:24 -0700, app_protocol=:http_server, payload="ECS (sec/974D)">
101
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:24 -0700, app_protocol=:html_title, payload="404 - Not Found">
102
+ #<struct Masscan::Banner protocol=:tcp, port=80, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:24 -0700, app_protocol=:http, payload="HTTP/1.0 404 Not Found\r\nContent-Type: text/html\r\nDate: Thu, 26 Aug 2021 06:50:24 GMT\r\nServer: ECS (sec/974D)\r\nContent-Length: 345\r\nConnection: close\r\n\r">
103
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:33 -0700, app_protocol=:ssl3, payload="TLS/1.1 cipher:0xc013, www.example.org, www.example.org, example.com, example.edu, example.net, example.org, www.example.com, www.example.edu, www.example.net">
104
+ #<struct Masscan::Banner protocol=:tcp, port=443, ip=#<IPAddr: IPv4:93.184.216.34/255.255.255.255>, timestamp=2021-08-25 23:50:33 -0700, app_protocol=:x509, payload="MIIG1TCCBb2gAwIBAgIQD74IsIVNBXOKsMzhya/uyTANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBEaWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMDExMjQwMDAwMDBaFw0yMTEyMjUyMzU5NTlaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLTG9zIEFuZ2VsZXMxPDA6BgNVBAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczEYMBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvzuzMoKCP8Okx2zvgucA5YinrFPEK5RQP1TX7PEYUAoBO6i5hIAsIKFmFxtW2sghERilU5rdnxQcF3fEx3sY4OtY6VSBPLPhLrbKozHLrQ8ZN/rYTb+hgNUeT7NA1mP78IEkxAj4qG5tli4Jq41aCbUlCt7equGXokImhC+UY5IpQEZS0tKD4vu2ksZ04Qetp0k8jWdAvMA27W3EwgHHNeVGWbJPC0Dn7RqPw13r7hFyS5TpleywjdY1nB7ad6kcZXZbEcaFZ7ZuerA6RkPGE+PsnZRb1oFJkYoXimsuvkVFhWeHQXCGC1cuDWSrM3cpQvOzKH2vS7d15+zGls4IwIDAQABo4IDaTCCA2UwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iyxZV2ufQwHQYDVR0OBBYEFCYa+OSxsHKEztqBBtInmPvtOj0XMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUuY29tggtleGFtcGxlLmVkdYILZXhhbXBsZS5uZXSCC2V4YW1wbGUub3Jngg93d3cuZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgYsGA1UdHwSBgzCBgDA+oDygOoY4aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS5jcmwwPqA8oDqGOGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv">
105
+ ```
106
+
107
+ ## Requirements
108
+
109
+ * [ruby] >= 2.0.0
110
+ * [masscan] >= 1.0.0
111
+ * [rprogram] ~> 0.3
112
+
113
+ ## Install
114
+
115
+ ```shell
116
+ $ gem install ruby-masscan
117
+ ```
118
+
119
+ ### gemspec
120
+
121
+ ```ruby
122
+ gemspec.add_dependency 'ruby-masscan', '~> 0.1'
123
+ ```
124
+
125
+ ### Gemfile
126
+
127
+ ```ruby
128
+ gem 'ruby-masscan', '~> 0.1'
129
+ ```
130
+
131
+ ## License
132
+
133
+ Copyright (c) 2021 Hal Brodigan
134
+
135
+ See {file:LICENSE.txt} for license information.
136
+
137
+ [masscan]: https://github.com/robertdavidgraham/masscan#readme
138
+ [ruby]: https://www.ruby-lang.org/
139
+ [rprogram]: https://github.com/postmodern/rprogram#readme
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError => e
8
+ warn e.message
9
+ warn "Run `gem install bundler` to install Bundler"
10
+ exit -1
11
+ end
12
+
13
+ require 'rubygems/tasks'
14
+ Gem::Tasks.new
15
+
16
+ require 'rspec/core/rake_task'
17
+ RSpec::Core::RakeTask.new
18
+ task :test => :spec
19
+ task :default => :spec
20
+
21
+ require 'yard'
22
+ YARD::Rake::YardocTask.new
23
+ task :doc => :yard
data/gemspec.yml ADDED
@@ -0,0 +1,28 @@
1
+ name: ruby-masscan
2
+ summary: A Ruby interface to masscan.
3
+ description:
4
+ A Ruby interface to masscan, an Internet-scale port scanner.
5
+ Allows automating masscan and parsing masscan Binary, List, JSON, and output
6
+ file formats.
7
+
8
+ license: MIT
9
+ authors: Postmodern
10
+ email: postmodern.mod3@gmail.com
11
+ homepage: https://github.com/postmodern/ruby-masscan#readme
12
+ has_yard: true
13
+
14
+ metadata:
15
+ documentation_uri: https://rubydoc.info/gems/ruby-masscan
16
+ source_code_uri: https://github.com/postmodern/ruby-masscan
17
+ bug_tracker_uri: https://github.com/postmodern/ruby-masscan/issues
18
+ changelog_uri: https://github.com/postmodern/ruby-masscan/blob/master/ChangeLog.md
19
+
20
+ required_ruby_version: ">= 2.0.0"
21
+
22
+ requirements: masscan >= 1.0.0
23
+
24
+ dependencies:
25
+ rprogram: ~> 0.3
26
+
27
+ development_dependencies:
28
+ bundler: ~> 2.0
@@ -0,0 +1,11 @@
1
+ module Masscan
2
+ #
3
+ # Represents a banner record.
4
+ #
5
+ class Banner < Struct.new(:protocol,:port,:ip,:timestamp,:app_protocol,:payload)
6
+
7
+ alias service app_protocol
8
+ alias banner payload
9
+
10
+ end
11
+ end
@@ -0,0 +1,100 @@
1
+ require 'masscan/parsers/list'
2
+ require 'masscan/parsers/json'
3
+ require 'masscan/parsers/binary'
4
+
5
+ module Masscan
6
+ #
7
+ # Represents an output file.
8
+ #
9
+ class OutputFile
10
+
11
+ PARSERS = {
12
+ binary: Parsers::Binary,
13
+ list: Parsers::List,
14
+ json: Parsers::JSON,
15
+ ndjson: Parsers::JSON,
16
+ # xml: Parsers::XML,
17
+ }
18
+
19
+ # The path to the output file.
20
+ #
21
+ # @return [String]
22
+ attr_reader :path
23
+
24
+ # The format of the output file.
25
+ #
26
+ # @return [Symbol]
27
+ attr_reader :format
28
+
29
+ # The parser for the output file format.
30
+ #
31
+ # @return [Parsers::Binary, Parsers::JSON, Parsers::List]
32
+ attr_reader :parser
33
+
34
+ #
35
+ # Initializes the output file.
36
+ #
37
+ # @param [String] path
38
+ # The path to the output file.
39
+ #
40
+ # @param [:binary, :list, :json, :ndjson] format
41
+ # The format of the output file. Defaults to {infer_format}.
42
+ #
43
+ # @raise [ArgumentError]
44
+ # The output format was not given and it cannot be inferred.
45
+ #
46
+ def initialize(path, format: self.class.infer_format(path))
47
+ @path = path
48
+ @format = format
49
+
50
+ @parser = PARSERS.fetch(format) do
51
+ raise(ArgumentError,"unknown format: #{format.inspect}")
52
+ end
53
+ end
54
+
55
+ #
56
+ # Infers the format from the output file's extension name.
57
+ #
58
+ # @param [String] path
59
+ # The path to the output file.
60
+ #
61
+ # @return [:binary, :list, :json, :ndjson]
62
+ # The output format inferred from the file's extension name.
63
+ #
64
+ # @raise [ArgumentError]
65
+ # The output format could not be inferred from the file's name.
66
+ #
67
+ def self.infer_format(path)
68
+ case File.extname(path)
69
+ when '.bin', '.dat' then :binary
70
+ when '.txt', '.list' then :list
71
+ when '.json' then :json
72
+ when '.ndjson' then :ndjson
73
+ when '.xml' then :xml
74
+ else
75
+ raise(ArgumentError,"could not infer format of #{path}")
76
+ end
77
+ end
78
+
79
+ #
80
+ # Parses the contents of the output file.
81
+ #
82
+ # @yield [record]
83
+ # If a block is given, it will be passed each parsed record.
84
+ #
85
+ # @yield [Status, Banner] record
86
+ # A parsed record, either a {Status} or a {Banner}.
87
+ #
88
+ # @return [Enumerator]
89
+ # If no block is given, an Enumerator will be returned.
90
+ #
91
+ def each(&block)
92
+ return enum_for(__method__) unless block
93
+
94
+ @parser.open(@path) do |file|
95
+ @parser.parse(file,&block)
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,591 @@
1
+ require 'masscan/status'
2
+ require 'masscan/banner'
3
+
4
+ require 'socket'
5
+
6
+ module Masscan
7
+ module Parsers
8
+ #
9
+ # Parses the `masscan -oB` output format.
10
+ #
11
+ # @note Ported from https://github.com/robertdavidgraham/masscan/blob/1.3.2/src/in-binary.c
12
+ #
13
+ # @api semipublic
14
+ #
15
+ module Binary
16
+ #
17
+ # Opens a binary file for parsing.
18
+ #
19
+ # @param [String] path
20
+ # The path to the file.
21
+ #
22
+ # @yield [file]
23
+ # If a block is given, it will be passed the opened file.
24
+ # Once the block returns, the file will be closed.
25
+ #
26
+ # @yieldparam [File]
27
+ # The opened file.
28
+ #
29
+ # @return [File]
30
+ # If no block was given, the opened file will be returned.
31
+ #
32
+ def self.open(path,&block)
33
+ File.open(path,'rb',&block)
34
+ end
35
+
36
+ class CorruptedFile < RuntimeError
37
+ end
38
+
39
+ # Maximum buffer length for a single record.
40
+ BUF_MAX = 1024 * 1024
41
+
42
+ #
43
+ # Parses masscan binary data.
44
+ #
45
+ # @param [IO] io
46
+ # The IO object to read from.
47
+ #
48
+ # @yield [record]
49
+ # If a block is given, it will be passed each parsed record.
50
+ #
51
+ # @yieldparam [Status, Banner] record
52
+ # A parsed record, either a {Status} or a {Banner} object.
53
+ #
54
+ # @return [Enumerator]
55
+ # If no block is given, it will return an Enumerator.
56
+ #
57
+ def self.parse(io)
58
+ return enum_for(__method__,io) unless block_given?
59
+
60
+ pseudo = read_pseudo_record(io)
61
+
62
+ # look for the start time
63
+ if (match = pseudo.match(/s:(\d+)/))
64
+ start_time = decode_timestamp(match[1].to_i)
65
+ end
66
+
67
+ total_records = 0
68
+
69
+ # read all records
70
+ loop do
71
+ # read the TYPE field
72
+ unless (type = read_multibyte_uint(io))
73
+ return
74
+ end
75
+
76
+ # read the LENGTH field
77
+ unless (length = read_multibyte_uint(io))
78
+ return
79
+ end
80
+
81
+ if length > BUF_MAX
82
+ raise(CorruptedFile,"file corrupted")
83
+ end
84
+
85
+ # read the remainder of the record
86
+ buffer = io.read(length)
87
+
88
+ if buffer.length < length
89
+ return
90
+ end
91
+
92
+ # parse the specific record type
93
+ record = case type
94
+ when 1 # STATUS: open
95
+ parse_status(buffer,:open)
96
+ when 2 # STATUS: closed
97
+ parse_status(buffer,:closed)
98
+ when 3 # BANNER
99
+ parse_banner3(buffer)
100
+ when 4
101
+ io.getbyte
102
+ parse_banner4(buffer)
103
+ when 5
104
+ parse_banner4(buffer)
105
+ when 6 # STATUS: open
106
+ parse_status2(buffer,:open)
107
+ when 7 # STATUS: closed
108
+ parse_status2(buffer,:closed)
109
+ when 9
110
+ parse_banner9(buffer)
111
+ when 10 # Open6
112
+ parse_status6(buffer,:open)
113
+ when 11 # Closed6
114
+ parse_status6(buffer,:closed)
115
+ when 13 # Banner6
116
+ parse_banner6(buffer)
117
+ when 109 # 'm'.ord # FILEHEADER
118
+ next
119
+ else
120
+ raise(CorruptedFile,"unknown type: #{type.inspect}")
121
+ end
122
+
123
+ if record
124
+ start_time ||= record.timestamp
125
+
126
+ yield record
127
+
128
+ total_records += 1
129
+ end
130
+ end
131
+
132
+ return total_records
133
+ end
134
+
135
+ # The "pseudo record" length
136
+ PSEUDO_RECORD_SIZE = 99 # 'a'.ord + 2
137
+
138
+ # Masscan binary format version compatibility.
139
+ MASSCAN_VERSION_FAMILY = "1.1"
140
+
141
+ # The `masscan` binary format magic string.
142
+ MASSCAN_MAGIC = "masscan/#{MASSCAN_VERSION_FAMILY}"
143
+
144
+ #
145
+ # Reads the "pseudo record" at the beginning of the file.
146
+ #
147
+ # @param [IO] io
148
+ # The IO object to read from.
149
+ #
150
+ # @return [String]
151
+ # The read buffer.
152
+ #
153
+ def self.read_pseudo_record(io)
154
+ buffer = io.read(PSEUDO_RECORD_SIZE)
155
+
156
+ if buffer.length < PSEUDO_RECORD_SIZE
157
+ raise(CorruptedFile,"invalid masscan binary format")
158
+ end
159
+
160
+ unless buffer.start_with?(MASSCAN_MAGIC)
161
+ raise(CorruptedFile,"unknown file format (expected #{MASSCAN_MAGIC})")
162
+ end
163
+
164
+ return buffer
165
+ end
166
+
167
+ #
168
+ # Reads a multi-byte unsigned integer.
169
+ #
170
+ # @param [IO] io
171
+ # The IO object to read from.
172
+ #
173
+ # @return [Integer, nil]
174
+ # The unsigned integer, or `nil` if End-of-Stream was reached.
175
+ #
176
+ def self.read_multibyte_uint(io)
177
+ unless (b = io.getbyte)
178
+ return
179
+ end
180
+
181
+ type = b & 0x7f
182
+
183
+ while (b & 0x80) != 0
184
+ unless (b = io.getbyte)
185
+ return
186
+ end
187
+
188
+ type = (type << 7) | (b & 0x7f)
189
+ end
190
+
191
+ return type
192
+ end
193
+
194
+ #
195
+ # Decodes a timestamp from an integer.
196
+ #
197
+ # @param [Integer] timestamp
198
+ # The raw UNIX timestamp integer.
199
+ #
200
+ # @return [Time]
201
+ # The decoded time value.
202
+ #
203
+ def self.decode_timestamp(timestamp)
204
+ Time.at(timestamp)
205
+ end
206
+
207
+ #
208
+ # Decodes an IPv4 address from an integer.
209
+ #
210
+ # @param [Integer] ip
211
+ # The IP in raw integer form.
212
+ #
213
+ # @return [IPAddr]
214
+ # The decoded IPv4 address.
215
+ #
216
+ def self.decode_ipv4(ip)
217
+ IPAddr.new(ip,Socket::AF_INET)
218
+ end
219
+
220
+ #
221
+ # Decodes an IPv6 address from two 64bit integers.
222
+ #
223
+ # @param [Integer] ipv6_hi
224
+ # The top-half of the 128bit IPv6 address.
225
+ #
226
+ # @param [Integer] ipv6_lo
227
+ # The top-half of the 128bit IPv6 address.
228
+ #
229
+ # @return [IPAddr]
230
+ # The decoded IPv6 address.
231
+ #
232
+ def self.decode_ipv6(ipv6_hi,ipv6_lo)
233
+ IPAddr.new((ipv6_hi << 64) | ipv6_lo,Socket::AF_INET6)
234
+ end
235
+
236
+ # Mapping of IP protocol numbers to keywords.
237
+ IP_PROTOCOLS = {
238
+ Socket::IPPROTO_ICMP => :icmp,
239
+ Socket::IPPROTO_ICMPV6 => :icmp,
240
+
241
+ Socket::IPPROTO_TCP => :tcp,
242
+ Socket::IPPROTO_UDP => :udp,
243
+
244
+ 132 => :sctp # Socket::IPPROTO_SCTP might not always be defined
245
+ }
246
+
247
+ #
248
+ # Looks up an IP protocol number.
249
+ #
250
+ # @param [Integer] proto
251
+ # The IP protocol number.
252
+ #
253
+ # @return [:icmp, :tcp, :udp, :sctp, nil]
254
+ # The IP protocol keyword.
255
+ #
256
+ # @see IP_PROTOCOLS
257
+ #
258
+ def self.lookup_ip_protocol(proto)
259
+ IP_PROTOCOLS[proto]
260
+ end
261
+
262
+ #
263
+ # Decodes a reason bitflag.
264
+ #
265
+ # @param [Integer] reason
266
+ # The reason bitflag.
267
+ #
268
+ # @return [Array<:fin, :syn, :rst, :psh, :ack, :urg, :ece, :cwr>]
269
+ # The reason flags.
270
+ #
271
+ def self.decode_reason(reason)
272
+ flags = []
273
+ flags << :fin if (reason & 0x01) != 0
274
+ flags << :syn if (reason & 0x02) != 0
275
+ flags << :rst if (reason & 0x04) != 0
276
+ flags << :psh if (reason & 0x08) != 0
277
+ flags << :ack if (reason & 0x10) != 0
278
+ flags << :urg if (reason & 0x20) != 0
279
+ flags << :ece if (reason & 0x40) != 0
280
+ flags << :cwr if (reason & 0x80) != 0
281
+ flags
282
+ end
283
+
284
+ # List of application protocol keywords.
285
+ APP_PROTOCOLS = [
286
+ nil,
287
+ :heur,
288
+ :ssh1,
289
+ :ssh2,
290
+ :http,
291
+ :ftp,
292
+ :dns_versionbind,
293
+ :snmp, # simple network management protocol, udp/161
294
+ :nbtstat, # netbios, udp/137
295
+ :ssl3,
296
+ :smb, # SMB tcp/139 and tcp/445
297
+ :smtp,
298
+ :pop3,
299
+ :imap4,
300
+ :udp_zeroaccess,
301
+ :x509_cert,
302
+ :html_title,
303
+ :html_full,
304
+ :ntp, # network time protocol, udp/123
305
+ :vuln,
306
+ :heartbleed,
307
+ :ticketbleed,
308
+ :vnc_rfb,
309
+ :safe,
310
+ :memcached,
311
+ :scripting,
312
+ :versioning,
313
+ :coap, # constrained app proto, udp/5683, RFC7252
314
+ :telnet,
315
+ :rdp, # Microsoft Remote Desktop Protocol tcp/3389
316
+ :http_server, # HTTP "Server:" field
317
+ ]
318
+
319
+ #
320
+ # Looks up an application protocol number.
321
+ #
322
+ # @param [Integer] proto
323
+ # The application protocol number.
324
+ #
325
+ # @return [Symbol, nil]
326
+ # The application protocol keyword.
327
+ #
328
+ # @see APP_PROTOCOLS
329
+ #
330
+ def self.lookup_app_protocol(proto)
331
+ APP_PROTOCOLS[proto]
332
+ end
333
+
334
+ #
335
+ # Parses a status record.
336
+ #
337
+ # @param [String] buffer
338
+ # The buffer to parse.
339
+ #
340
+ # @param [:open, :closed] status
341
+ # Indicates whether the port status is open or closed.
342
+ #
343
+ # @return [Status]
344
+ # The parsed status record.
345
+ #
346
+ def self.parse_status(buffer,status)
347
+ if buffer.length < 12
348
+ return
349
+ end
350
+
351
+ timestamp, ip, port, reason, ttl = buffer.unpack("L>L>S>CC")
352
+
353
+ timestamp = decode_timestamp(timestamp)
354
+ ip = decode_ipv4(ip)
355
+ reason = decode_reason(reason)
356
+
357
+ # if ARP, there will be a MAC address after the record
358
+ mac = if ip == 0 && buffer.length >= 12+6
359
+ buffer[12+6,6]
360
+ end
361
+
362
+ protocol = case port
363
+ when 53, 123, 137, 161 then :udp
364
+ when 36422, 36412, 2905 then :sctp
365
+ else :tcp
366
+ end
367
+
368
+ return Status.new(
369
+ status,
370
+ protocol,
371
+ port,
372
+ ip,
373
+ timestamp,
374
+ mac
375
+ )
376
+ end
377
+
378
+ #
379
+ # Parses a banner record.
380
+ #
381
+ # @param [String] buffer
382
+ # The buffer to parse.
383
+ #
384
+ # @return [Buffer]
385
+ # The parsed buffer record.
386
+ #
387
+ def self.parse_banner3(buffer)
388
+ timestamp, ip, port, app_proto, payload = buffer.unpack('L>L>S>S>A*')
389
+
390
+ timestamp = decode_timestamp(timestamp)
391
+ ip = decode_ipv4(ip)
392
+ app_proto = lookup_app_protocol(app_proto)
393
+
394
+ # defaults
395
+ ip_proto = :tcp
396
+ ttl = 0
397
+
398
+ return Banner.new(
399
+ ip_proto,
400
+ port,
401
+ ip,
402
+ timestamp,
403
+ app_proto,
404
+ payload
405
+ )
406
+ end
407
+
408
+ #
409
+ # Parses a banner record.
410
+ #
411
+ # @param [String] buffer
412
+ # The buffer to parse.
413
+ #
414
+ # @return [Buffer]
415
+ # The parsed buffer record.
416
+ #
417
+ def self.parse_banner4(buffer)
418
+ if buffer.length < 13
419
+ return
420
+ end
421
+
422
+ timestamp, ip, ip_prot, port, app_proto, payload = buffer.unpack('L>L>CS>S>A*')
423
+
424
+ timestamp = decode_timestamp(timestamp)
425
+ ip = decode_ipv4(ip)
426
+ ip_proto = lookup_ip_protocol(ip_proto)
427
+ app_proto = lookup_app_protocol(app_proto)
428
+
429
+ # defaults
430
+ ttl = 0
431
+
432
+ return Banner.new(
433
+ ip_proto,
434
+ port,
435
+ ip,
436
+ timestamp,
437
+ app_proto,
438
+ payload
439
+ )
440
+ end
441
+
442
+ #
443
+ # Parses a status record.
444
+ #
445
+ # @param [String] buffer
446
+ # The buffer to parse.
447
+ #
448
+ # @param [:open, :closed] status
449
+ # Indicates whether the port status is open or closed.
450
+ #
451
+ # @return [Status]
452
+ # The parsed status record.
453
+ #
454
+ def self.parse_status2(buffer,status)
455
+ if buffer.length < 13
456
+ return
457
+ end
458
+
459
+ timestamp, ip, ip_proto, port, reason, ttl = buffer.unpack('L>L>CS>CC')
460
+ timestamp = decode_timestamp(timestamp)
461
+ ip = decode_ipv4(ip)
462
+ ip_proto = lookup_ip_protocol(ip_proto)
463
+ reason = decode_reason(reason)
464
+
465
+ mac = if ip == 0 && buffer.length >= 13+6
466
+ buffer[13,6]
467
+ end
468
+
469
+ return Status.new(
470
+ status,
471
+ ip_proto,
472
+ port,
473
+ reason,
474
+ ttl,
475
+ ip,
476
+ timestamp,
477
+ mac
478
+ )
479
+ end
480
+
481
+ #
482
+ # Parses a banner record.
483
+ #
484
+ # @param [String] buffer
485
+ # The buffer to parse.
486
+ #
487
+ # @return [Buffer]
488
+ # The parsed buffer record.
489
+ #
490
+ def self.parse_banner9(buffer)
491
+ if buffer.length < 14
492
+ return
493
+ end
494
+
495
+ timestamp, ip, ip_proto, port, app_proto, ttl, payload = buffer.unpack('L>L>CS>S>CA*')
496
+ timestamp = decode_timestamp(timestamp)
497
+ ip = decode_ipv4(ip)
498
+ ip_proto = lookup_ip_protocol(ip_proto)
499
+ app_proto = lookup_app_protocol(app_proto)
500
+
501
+ return Banner.new(
502
+ ip_proto,
503
+ port,
504
+ ip,
505
+ timestamp,
506
+ app_proto,
507
+ payload
508
+ )
509
+ end
510
+
511
+ #
512
+ # Parses a status record.
513
+ #
514
+ # @param [String] buffer
515
+ # The buffer to parse.
516
+ #
517
+ # @param [:open, :closed] status
518
+ # Indicates whether the port status is open or closed.
519
+ #
520
+ # @return [Status]
521
+ # The parsed status record.
522
+ #
523
+ def self.parse_status6(buffer,status)
524
+ timestamp, ip_proto, port, reason, ttl, ip_version, ipv6_hi, ipv6_lo = buffer.unpack('L>CS>CCCQ>Q>')
525
+ timestamp ||= 0xffffffff
526
+ ip_proto ||= 0xff
527
+ port ||= 0xffff
528
+ reason ||= 0xff
529
+ ttl ||= 0xff
530
+ ip_version ||= 0xff
531
+ ipv6_hi ||= 0xffffffff_ffffffff
532
+ ipv6_lo ||= 0xffffffff_ffffffff
533
+
534
+ unless ip_version == 6
535
+ raise(CorruptedFile,"expected ip_version to be 6: #{ip_version.inspect}")
536
+ end
537
+
538
+ timestamp = decode_timestamp(timestamp)
539
+ ip_proto = lookup_ip_protocol(ip_proto)
540
+ reason = decode_reason(reason)
541
+ ipv6 = decode_ipv6(ipv6_hi,ipv6_lo)
542
+
543
+ return Status.new(
544
+ status,
545
+ ip_proto,
546
+ port,
547
+ reason,
548
+ ttl,
549
+ ipv6,
550
+ timestamp
551
+ )
552
+ end
553
+
554
+ #
555
+ # Parses a banner record.
556
+ #
557
+ # @param [String] buffer
558
+ # The buffer to parse.
559
+ #
560
+ # @return [Buffer]
561
+ # The parsed buffer record.
562
+ #
563
+ def self.parse_banner6(buffer)
564
+ timestamp, ip_proto, port, app_proto, ttl, ip_version, ipv6_hi, ipv6_lo, payload = buffer.unpack('L>CS>S>CCQ>Q>A*')
565
+ timestamp ||= 0xffffffff
566
+ protocol ||= 0xff
567
+ port ||= 0xffff
568
+ app_proto ||= 0xffff
569
+ ttl ||= 0xff
570
+ ip_version ||= 0xff
571
+ ipv6_hi ||= 0xffffffff_ffffffff
572
+ ipv6_lo ||= 0xffffffff_ffffffff
573
+
574
+ timestamp = decode_timestamp(timestamp)
575
+ ip_proto = lookup_ip_protocol(ip_proto)
576
+ app_proto = lookup_app_protocol(app_proto)
577
+ ipv6 = decode_ipv6(ipv6_hi,ipv6_lo)
578
+
579
+ return Banner.new(
580
+ ip_proto,
581
+ port,
582
+ ipv6,
583
+ timestamp,
584
+ app_proto,
585
+ payload
586
+ )
587
+ end
588
+
589
+ end
590
+ end
591
+ end