ruby-amass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require 'amass/address'
2
+
3
+ module Amass
4
+ #
5
+ # A hostname.
6
+ #
7
+ # @api public
8
+ #
9
+ class Hostname
10
+
11
+ # The hostname.
12
+ #
13
+ # @return [String]
14
+ attr_reader :name
15
+
16
+ # The domain the hostname belongs to.
17
+ #
18
+ # @return [String, nil]
19
+ attr_reader :domain
20
+
21
+ # The addresses associated with the hostname.
22
+ #
23
+ # @return [Array<Address>]
24
+ attr_reader :addresses
25
+
26
+ # The tag from `amass`.
27
+ #
28
+ # @return [String, nil]
29
+ attr_reader :tag
30
+
31
+ # The source(s) that discovered the hostname.
32
+ #
33
+ # @return [Array<String>]
34
+ attr_reader :sources
35
+
36
+ #
37
+ # Initializes the hostname.
38
+ #
39
+ # @param [String] name
40
+ # The hostname.
41
+ #
42
+ # @param [String, nil] domain
43
+ # The domain the hostname belongs to.
44
+ #
45
+ # @param [Array<Address>] addresses
46
+ # The addresses associated with the hostname.
47
+ #
48
+ # @param [String, nil] tag
49
+ # The `amass` tag.
50
+ #
51
+ # @param [Array<String>] sources
52
+ # The source(s) that discovered the hostname.
53
+ #
54
+ def initialize(name: , domain: nil, addresses: [], tag: nil, sources: [])
55
+ @name = name
56
+ @domain = domain
57
+ @addresses = addresses
58
+ @tag = tag
59
+ @sources = sources
60
+ end
61
+
62
+ #
63
+ # Converts the hostname to a String.
64
+ #
65
+ # @return [String]
66
+ # The hostname.
67
+ #
68
+ def to_s
69
+ @name
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,124 @@
1
+ require 'amass/parsers/json'
2
+ require 'amass/parsers/txt'
3
+
4
+ module Amass
5
+ #
6
+ # Represents either a `.json` or `.txt` output file.
7
+ #
8
+ # ## Example
9
+ #
10
+ # require 'amass/output_file'
11
+ #
12
+ # output_file = Amass::OutputFile.new('/path/to/amass.json')
13
+ # output_file.each do |hostname|
14
+ # p hostname
15
+ # end
16
+ #
17
+ # @api public
18
+ #
19
+ class OutputFile
20
+
21
+ # Mapping of formats to parsers.
22
+ #
23
+ # @api semipublic
24
+ PARSERS = {
25
+ :json => Parsers::JSON,
26
+ :txt => Parsers::TXT
27
+ }
28
+
29
+ # The path to the output file.
30
+ #
31
+ # @return [String]
32
+ attr_reader :path
33
+
34
+ # The format of the output file.
35
+ #
36
+ # @return [:json, :txt]
37
+ attr_reader :format
38
+
39
+ # The parser for the output file format.
40
+ #
41
+ # @return [Parsers::JSON, Parsers::TXT]
42
+ #
43
+ # @api private
44
+ attr_reader :parser
45
+
46
+ #
47
+ # Initializes the output file.
48
+ #
49
+ # @param [String] path
50
+ # The path to the output file.
51
+ #
52
+ # @param [:json, :txt] format
53
+ # The optional format of the output file. If not given, it will be
54
+ # inferred by the path's file extension.
55
+ #
56
+ def initialize(path, format: self.class.infer_format(path))
57
+ @path = File.expand_path(path)
58
+ @format = format
59
+
60
+ @parser = PARSERS.fetch(format) do
61
+ raise(ArgumentError,"unrecognized file type: #{@path.inspect}")
62
+ end
63
+ end
64
+
65
+ # Mapping of file extensions to formats
66
+ #
67
+ # @api semipublic
68
+ FILE_FORMATS = {
69
+ '.json' => :json,
70
+ '.txt' => :txt
71
+ }
72
+
73
+ #
74
+ # Infers the format from the output file's extension name.
75
+ #
76
+ # @param [String] path
77
+ # The path to the output file.
78
+ #
79
+ # @return [:json, :txt]
80
+ # The output format inferred from the file's extension name.
81
+ #
82
+ # @raise [ArgumentError]
83
+ # The output format could not be inferred from the file's name.
84
+ #
85
+ # @api semipublic
86
+ #
87
+ def self.infer_format(path)
88
+ FILE_FORMATS.fetch(File.extname(path)) do
89
+ raise(ArgumentError,"could not infer format of #{path}")
90
+ end
91
+ end
92
+
93
+ #
94
+ # Parses the contents of the output file.
95
+ #
96
+ # @yield [hostname]
97
+ # The given block will be passed each parsed hostname.
98
+ #
99
+ # @yieldparam [Hostname] hostname
100
+ # A parsed hostname from the output file.
101
+ #
102
+ # @return [Enumerator]
103
+ # If no block is given, an Enumerator object will be returned.
104
+ #
105
+ def each(&block)
106
+ return enum_for(__method__) unless block
107
+
108
+ File.open(@path) do |file|
109
+ @parser.parse(file,&block)
110
+ end
111
+ end
112
+
113
+ #
114
+ # Converts the output file to a String.
115
+ #
116
+ # @return [String]
117
+ # The path to the output file.
118
+ #
119
+ def to_s
120
+ @path
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,80 @@
1
+ require 'amass/hostname'
2
+ require 'amass/address'
3
+
4
+ require 'json'
5
+
6
+ module Amass
7
+ module Parsers
8
+ #
9
+ # Parses single-line JSON hashes.
10
+ #
11
+ # @api semipublic
12
+ #
13
+ module JSON
14
+ #
15
+ # Parses a single-line of JSON.
16
+ #
17
+ # @param [IO] io
18
+ # The IO stream to parse.
19
+ #
20
+ # @yield [hostname]
21
+ # The given block will be passed each parsed hostname.
22
+ #
23
+ # @yieldparam [Hostname] hostname
24
+ # The parsed hostname.
25
+ #
26
+ # @return [Enumerator]
27
+ # If no block is given, an Enumerator will be returned.
28
+ #
29
+ def self.parse(io)
30
+ return enum_for(__method__,io) unless block_given?
31
+
32
+ io.each_line do |line|
33
+ line.chomp!
34
+ json = ::JSON.parse(line)
35
+
36
+ yield map_hostname(json)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ #
43
+ # Maps a JSON Hash to a {Hostname}.
44
+ #
45
+ # @param [Hash{Symbol => Object}] json
46
+ # The parsed JSON Hash.
47
+ #
48
+ # @return [Hostname]
49
+ # The resulting hostname.
50
+ #
51
+ def self.map_hostname(json)
52
+ Hostname.new(
53
+ name: json['name'],
54
+ domain: json['domain'],
55
+ addresses: json['addresses'].map(&method(:map_address)),
56
+ tag: json['tag'],
57
+ sources: json['sources']
58
+ )
59
+ end
60
+
61
+ #
62
+ # Maps a JSON Hash to an {Address}.
63
+ #
64
+ # @param [Hash{Symbol => Object}] json
65
+ # The parsed JSON Hash.
66
+ #
67
+ # @return [Address]
68
+ # The resulting address.
69
+ #
70
+ def self.map_address(json)
71
+ Address.new(
72
+ ip: json['ip'],
73
+ cidr: json['cidr'],
74
+ asn: json['asn'],
75
+ desc: json['desc']
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ require 'amass/hostname'
2
+
3
+ module Amass
4
+ module Parsers
5
+ #
6
+ # Parses single-line hostnames.
7
+ #
8
+ # @api semipublic
9
+ #
10
+ module TXT
11
+ #
12
+ # Parses a single line of plain-text.
13
+ #
14
+ # @param [IO] io
15
+ # The IO stream to parse.
16
+ #
17
+ # @yield [hostname]
18
+ # The given block will be passed each parsed hostname.
19
+ #
20
+ # @yieldparam [Hostname] hostname
21
+ # The parsed hostname.
22
+ #
23
+ # @return [Enumerator]
24
+ # If no block is given, an Enumerator will be returned.
25
+ #
26
+ def self.parse(io)
27
+ return enum_for(__method__,io) unless block_given?
28
+
29
+ io.each_line do |line|
30
+ line.chomp!
31
+
32
+ yield Hostname.new(name: line)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ module Amass
2
+ # ruby-amass version
3
+ VERSION = '0.1.0'
4
+ end
data/lib/amass.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'amass/command'
2
+ require 'amass/version'
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'amass/version'
14
+ Amass::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+ gem.metadata = gemspec['metadata'] if gemspec['metadata']
24
+
25
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
26
+
27
+ gem.files = `git ls-files`.split($/)
28
+ gem.files = glob[gemspec['files']] if gemspec['files']
29
+
30
+ gem.executables = gemspec.fetch('executables') do
31
+ glob['bin/*'].map { |path| File.basename(path) }
32
+ end
33
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
34
+
35
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
36
+ gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
37
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
38
+
39
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
40
+ %w[ext lib].select { |dir| File.directory?(dir) }
41
+ })
42
+
43
+ gem.requirements = Array(gemspec['requirements'])
44
+ gem.required_ruby_version = gemspec['required_ruby_version']
45
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
46
+ gem.post_install_message = gemspec['post_install_message']
47
+
48
+ split = lambda { |string| string.split(/,\s*/) }
49
+
50
+ if gemspec['dependencies']
51
+ gemspec['dependencies'].each do |name,versions|
52
+ gem.add_dependency(name,split[versions])
53
+ end
54
+ end
55
+
56
+ if gemspec['development_dependencies']
57
+ gemspec['development_dependencies'].each do |name,versions|
58
+ gem.add_development_dependency(name,split[versions])
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'amass/address'
3
+
4
+ describe Amass::Address do
5
+ let(:ip) { "93.184.216.34" }
6
+ let(:cidr) { "93.184.216.0/24" }
7
+ let(:asn) { 15133 }
8
+ let(:desc) { "EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business" }
9
+
10
+ subject { described_class.new(ip: ip, cidr: cidr, asn: asn, desc: desc) }
11
+
12
+ describe "#initialize" do
13
+ it "must set #ip" do
14
+ expect(subject.ip).to eq(ip)
15
+ end
16
+
17
+ it "must set #cidr" do
18
+ expect(subject.cidr).to eq(cidr)
19
+ end
20
+
21
+ it "must set #asn" do
22
+ expect(subject.asn).to eq(asn)
23
+ end
24
+
25
+ it "must set #desc" do
26
+ expect(subject.desc).to eq(desc)
27
+ end
28
+ end
29
+
30
+ describe "#to_s" do
31
+ it "must return the #ip" do
32
+ expect(subject.to_s).to eq(ip)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'amass/version'
3
+
4
+ describe Amass do
5
+ it "should have a VERSION constant" do
6
+ expect(subject.const_get('VERSION')).to_not be_empty
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ {"name":"www.example.com","domain":"example.com","addresses":[{"ip":"93.184.216.34","cidr":"93.184.216.0/24","asn":15133,"desc":"EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business"},{"ip":"2606:2800:220:1:248:1893:25c8:1946","cidr":"2606:2800:220::/48","asn":15133,"desc":"EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business"}],"tag":"cert","sources":["CertSpotter"]}
2
+ {"name":"example.com","domain":"example.com","addresses":[{"ip":"2606:2800:220:1:248:1893:25c8:1946","cidr":"2606:2800:220::/48","asn":15133,"desc":"EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business"},{"ip":"93.184.216.34","cidr":"93.184.216.0/24","asn":15133,"desc":"EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business"}],"tag":"cert","sources":["CertSpotter"]}
3
+ {"name":"waterregion643.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
4
+ {"name":"ringneo5.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
5
+ {"name":"ratara72.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
6
+ {"name":"serafim.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
7
+ {"name":"a8boyyy.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
8
+ {"name":"bikamzhaz.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"api","sources":["Sublist3rAPI"]}
9
+ {"name":"wwwakamai.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
10
+ {"name":"wwwakamaiint.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
11
+ {"name":"billing-serafim.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
12
+ {"name":"bikamzhazstaff.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
13
+ {"name":"bikamzhaz-6.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
14
+ {"name":"wwwakamaiintorigin.example.com","domain":"example.com","addresses":[{"ip":"54.144.128.85","cidr":"54.144.0.0/14","asn":14618,"desc":"AMAZON-AES - Amazon.com, Inc."}],"tag":"alt","sources":["Alterations"]}
@@ -0,0 +1,24 @@
1
+ example.com
2
+ wbldr2.example.com
3
+ qwewowgo40.example.com
4
+ intermarksavills.example.com
5
+ www.example.com
6
+ wild1990.example.com
7
+ artsupergol1.example.com
8
+ wild1990server.example.com
9
+ gaev.ilya.s.example.com
10
+ wbldr22.example.com
11
+ www-las.example.com
12
+ work1.rabot.example.com
13
+ wild1990serverphp.example.com
14
+ 69.48.131.151.static.example.com
15
+ email-wbldr22.example.com
16
+ wbldr22ops.example.com
17
+ wbldr22ids.example.com
18
+ wbldr22opsbrasil.example.com
19
+ email-wbldr22stage.example.com
20
+ email-wbldr22stageaws.example.com
21
+ wbldr22opsredirector.example.com
22
+ machinework1.rabot.example.com
23
+ wild1990serverphp-testing.example.com
24
+ 169.48.131.151.static.example.com
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+ require 'amass/hostname'
3
+
4
+ describe Amass::Hostname do
5
+ let(:name) { "www.example.com" }
6
+ let(:domain) { "example.com" }
7
+
8
+ let(:ip1) { "93.184.216.34" }
9
+ let(:cidr1) { "93.184.216.0/24" }
10
+ let(:asn1) { 15133 }
11
+ let(:desc1) { "EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business" }
12
+
13
+ let(:ip2) { "2606:2800:220:1:248:1893:25c8:1946" }
14
+ let(:cidr2) { "2606:2800:220::/48" }
15
+ let(:asn2) { 15133 }
16
+ let(:desc2) { "EDGECAST - MCI Communications Services, Inc. d/b/a Verizon Business" }
17
+
18
+ let(:tag) { "cert" }
19
+ let(:source1) { "CertSpotter" }
20
+
21
+ let(:addresses) do
22
+ [
23
+ Amass::Address.new(ip: ip1, cidr: cidr1, asn: asn1, desc: desc1),
24
+ Amass::Address.new(ip: ip2, cidr: cidr2, asn: asn2, desc: desc2)
25
+ ]
26
+ end
27
+
28
+ let(:sources) { [source1] }
29
+
30
+ subject do
31
+ described_class.new(
32
+ name: name,
33
+ domain: domain,
34
+ addresses: addresses,
35
+ tag: tag,
36
+ sources: sources
37
+ )
38
+ end
39
+
40
+ describe "#intialize" do
41
+ context "when only the name: keyword is given" do
42
+ subject { described_class.new(name: name) }
43
+
44
+ it "must set #name" do
45
+ expect(subject.name).to eq(name)
46
+ end
47
+
48
+ it "must initialize the Hostname's #domain to nil" do
49
+ expect(subject.domain).to be(nil)
50
+ end
51
+
52
+ it "must initialize the Hostname's #addresses to []" do
53
+ expect(subject.addresses).to eq([])
54
+ end
55
+
56
+ it "must initialize the Hostname's #tag to nil" do
57
+ expect(subject.tag).to be(nil)
58
+ end
59
+
60
+ it "must initialize the Hostname's #sources to []" do
61
+ expect(subject.sources).to eq([])
62
+ end
63
+ end
64
+
65
+ context "when all keyword arguments are given" do
66
+ it "must set #name" do
67
+ expect(subject.name).to eq(name)
68
+ end
69
+
70
+ it "must set #domain" do
71
+ expect(subject.domain).to be(domain)
72
+ end
73
+
74
+ it "must set #addresses" do
75
+ expect(subject.addresses).to eq(addresses)
76
+ end
77
+
78
+ it "must set #tag" do
79
+ expect(subject.tag).to eq(tag)
80
+ end
81
+
82
+ it "must set #sources" do
83
+ expect(subject.sources).to eq(sources)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe "#to_s" do
89
+ it "must return #name" do
90
+ expect(subject.to_s).to eq(name)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+ require 'amass/output_file'
3
+
4
+ describe Amass::OutputFile do
5
+ let(:fixtures_dir) { File.expand_path(File.join(__dir__,'fixtures')) }
6
+ describe ".infer_format" do
7
+ subject { described_class.infer_format(path) }
8
+
9
+ context "when the path ends in .json" do
10
+ let(:path) { '/path/to/file.json' }
11
+
12
+ it { expect(subject).to eq(:json) }
13
+ end
14
+
15
+ context "when the path ends in .txt" do
16
+ let(:path) { '/path/to/file.txt' }
17
+
18
+ it { expect(subject).to eq(:txt) }
19
+ end
20
+ end
21
+
22
+ describe "PARSERS" do
23
+ subject { described_class::PARSERS }
24
+
25
+ describe ":txt" do
26
+ it { expect(subject[:txt]).to eq(Amass::Parsers::TXT) }
27
+ end
28
+
29
+ describe ":json" do
30
+ it { expect(subject[:json]).to eq(Amass::Parsers::JSON) }
31
+ end
32
+ end
33
+
34
+ describe "FILE_FORMATS" do
35
+ subject { described_class::FILE_FORMATS }
36
+
37
+ describe ".txt" do
38
+ it { expect(subject['.txt']).to be(:txt) }
39
+ end
40
+
41
+ describe ".json" do
42
+ it { expect(subject['.json']).to be(:json) }
43
+ end
44
+ end
45
+
46
+ describe "#initialize" do
47
+ let(:path) { "/path/to/file.json" }
48
+
49
+ subject { described_class.new(path) }
50
+
51
+ it "must set #path" do
52
+ expect(subject.path).to eq(path)
53
+ end
54
+
55
+ it "must infer the format from the file's extname" do
56
+ expect(subject.format).to eq(:json)
57
+ end
58
+
59
+ it "must set #parser based on #format" do
60
+ expect(subject.parser).to eq(described_class::PARSERS[subject.format])
61
+ end
62
+
63
+ context "when given an format: keyword" do
64
+ let(:format) { :txt }
65
+
66
+ subject { described_class.new(path, format: format) }
67
+
68
+ it "must set #format" do
69
+ expect(subject.format).to be(format)
70
+ end
71
+
72
+ it "must set #parser based on the given format:" do
73
+ expect(subject.parser).to eq(described_class::PARSERS[format])
74
+ end
75
+ end
76
+ end
77
+
78
+ let(:path) { File.join(fixtures_dir,'enum','amass.json') }
79
+
80
+ subject { described_class.new(path) }
81
+
82
+ describe "#each" do
83
+ context "when a block is given" do
84
+ it "must yield each parsed Hostname object" do
85
+ yielded_hostnames = []
86
+
87
+ subject.each do |hostname|
88
+ yielded_hostnames << hostname
89
+ end
90
+
91
+ expect(yielded_hostnames).to_not be_empty
92
+ expect(yielded_hostnames).to all(be_kind_of(Amass::Hostname))
93
+ end
94
+ end
95
+
96
+ context "when no block is given" do
97
+ it "must return an Enumerator of the parsed Hostname objects" do
98
+ hostnames = subject.each.to_a
99
+
100
+ expect(hostnames).to_not be_empty
101
+ expect(hostnames).to all(be_kind_of(Amass::Hostname))
102
+ end
103
+ end
104
+ end
105
+
106
+ describe "#to_s" do
107
+ it "must return #path" do
108
+ expect(subject.to_s).to eq(path)
109
+ end
110
+ end
111
+ end