ruby-amass 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.
@@ -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