ruby-masscan 0.1.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.
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