threatinator 0.1.1

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.
Files changed (219) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/CONTRIBUTING.md +119 -0
  4. data/Gemfile +28 -0
  5. data/LICENSE +165 -0
  6. data/README.md +45 -0
  7. data/Rakefile +45 -0
  8. data/VERSION +1 -0
  9. data/bin/threatinator +5 -0
  10. data/lib/threatinator.rb +3 -0
  11. data/lib/threatinator/action.rb +14 -0
  12. data/lib/threatinator/actions/list.rb +2 -0
  13. data/lib/threatinator/actions/list/action.rb +53 -0
  14. data/lib/threatinator/actions/list/config.rb +10 -0
  15. data/lib/threatinator/actions/run.rb +2 -0
  16. data/lib/threatinator/actions/run/action.rb +45 -0
  17. data/lib/threatinator/actions/run/config.rb +32 -0
  18. data/lib/threatinator/actions/run/coverage_observer.rb +54 -0
  19. data/lib/threatinator/actions/run/output_config.rb +59 -0
  20. data/lib/threatinator/cli.rb +13 -0
  21. data/lib/threatinator/cli/action_builder.rb +33 -0
  22. data/lib/threatinator/cli/list_action_builder.rb +19 -0
  23. data/lib/threatinator/cli/parser.rb +113 -0
  24. data/lib/threatinator/cli/run_action_builder.rb +41 -0
  25. data/lib/threatinator/config.rb +6 -0
  26. data/lib/threatinator/config/base.rb +35 -0
  27. data/lib/threatinator/config/feed_search.rb +25 -0
  28. data/lib/threatinator/decoder.rb +24 -0
  29. data/lib/threatinator/decoders/gzip.rb +30 -0
  30. data/lib/threatinator/event.rb +27 -0
  31. data/lib/threatinator/event_builder.rb +41 -0
  32. data/lib/threatinator/exceptions.rb +61 -0
  33. data/lib/threatinator/feed.rb +82 -0
  34. data/lib/threatinator/feed_builder.rb +156 -0
  35. data/lib/threatinator/feed_registry.rb +47 -0
  36. data/lib/threatinator/feed_runner.rb +118 -0
  37. data/lib/threatinator/fetcher.rb +22 -0
  38. data/lib/threatinator/fetchers/http.rb +46 -0
  39. data/lib/threatinator/filter.rb +12 -0
  40. data/lib/threatinator/filters/block.rb +18 -0
  41. data/lib/threatinator/filters/comments.rb +16 -0
  42. data/lib/threatinator/filters/whitespace.rb +19 -0
  43. data/lib/threatinator/output.rb +50 -0
  44. data/lib/threatinator/parser.rb +23 -0
  45. data/lib/threatinator/parsers/csv.rb +7 -0
  46. data/lib/threatinator/parsers/csv/parser.rb +77 -0
  47. data/lib/threatinator/parsers/getline.rb +8 -0
  48. data/lib/threatinator/parsers/getline/parser.rb +45 -0
  49. data/lib/threatinator/parsers/json.rb +8 -0
  50. data/lib/threatinator/parsers/json/adapters/oj.rb +65 -0
  51. data/lib/threatinator/parsers/json/parser.rb +45 -0
  52. data/lib/threatinator/parsers/json/record.rb +20 -0
  53. data/lib/threatinator/parsers/xml.rb +8 -0
  54. data/lib/threatinator/parsers/xml/node.rb +79 -0
  55. data/lib/threatinator/parsers/xml/node_builder.rb +39 -0
  56. data/lib/threatinator/parsers/xml/parser.rb +44 -0
  57. data/lib/threatinator/parsers/xml/path.rb +70 -0
  58. data/lib/threatinator/parsers/xml/pattern.rb +53 -0
  59. data/lib/threatinator/parsers/xml/record.rb +14 -0
  60. data/lib/threatinator/parsers/xml/sax_document.rb +64 -0
  61. data/lib/threatinator/plugin_loader.rb +115 -0
  62. data/lib/threatinator/plugins/output/csv.rb +47 -0
  63. data/lib/threatinator/plugins/output/null.rb +17 -0
  64. data/lib/threatinator/plugins/output/rubydebug.rb +16 -0
  65. data/lib/threatinator/property_definer.rb +101 -0
  66. data/lib/threatinator/record.rb +22 -0
  67. data/lib/threatinator/registry.rb +53 -0
  68. data/lib/threatinator/util.rb +15 -0
  69. data/spec/feeds/ET_compromised-ip_reputation_spec.rb +50 -0
  70. data/spec/feeds/alienvault-ip_reputation_spec.rb +50 -0
  71. data/spec/feeds/arbor_fastflux-domain_reputation_spec.rb +50 -0
  72. data/spec/feeds/arbor_ssh-ip_reputation_spec.rb +50 -0
  73. data/spec/feeds/autoshun_shunlist_spec.rb +42 -0
  74. data/spec/feeds/blocklist_de_apache-ip_reputation_spec.rb +50 -0
  75. data/spec/feeds/blocklist_de_bots-ip_reputation_spec.rb +50 -0
  76. data/spec/feeds/blocklist_de_ftp-ip_reputation_spec.rb +50 -0
  77. data/spec/feeds/blocklist_de_imap-ip_reputation_spec.rb +50 -0
  78. data/spec/feeds/blocklist_de_pop3-ip_reputation_spec.rb +50 -0
  79. data/spec/feeds/blocklist_de_proftpd-ip_reputation_spec.rb +50 -0
  80. data/spec/feeds/blocklist_de_sip-ip_reputation_spec.rb +50 -0
  81. data/spec/feeds/blocklist_de_ssh-ip_reputation_spec.rb +50 -0
  82. data/spec/feeds/blocklist_de_strongips-ip_reputation_spec.rb +50 -0
  83. data/spec/feeds/ciarmy-ip_reputation_spec.rb +50 -0
  84. data/spec/feeds/cruzit-ip_reputation_spec.rb +50 -0
  85. data/spec/feeds/dan_me_uk_torlist-ip_reputation_spec.rb +50 -0
  86. data/spec/feeds/data/ET_compromised-ip_reputation.txt +11 -0
  87. data/spec/feeds/data/alienvault-ip_reputation.txt +18 -0
  88. data/spec/feeds/data/arbor_domainlist.txt +11 -0
  89. data/spec/feeds/data/arbor_ssh.txt +16 -0
  90. data/spec/feeds/data/autoshun_shunlist.csv +20 -0
  91. data/spec/feeds/data/blocklist_de_apache-ip-reputation.txt +17 -0
  92. data/spec/feeds/data/blocklist_de_bots-ip-reputation.txt +15 -0
  93. data/spec/feeds/data/blocklist_de_ftp-ip-reputation.txt +7 -0
  94. data/spec/feeds/data/blocklist_de_imap-ip-reputation.txt +8 -0
  95. data/spec/feeds/data/blocklist_de_pop3-ip-reputation.txt +11 -0
  96. data/spec/feeds/data/blocklist_de_proftpd-ip-reputation.txt +12 -0
  97. data/spec/feeds/data/blocklist_de_sip-ip-reputation.txt +9 -0
  98. data/spec/feeds/data/blocklist_de_ssh-ip-reputation.txt +10 -0
  99. data/spec/feeds/data/blocklist_de_strongips-ip-reputation.txt +11 -0
  100. data/spec/feeds/data/ciarmy-ip-reputation.txt +11 -0
  101. data/spec/feeds/data/cruzit-ip-reputation.txt +14 -0
  102. data/spec/feeds/data/dan_me_uk_torlist-ip-reputation.txt +11 -0
  103. data/spec/feeds/data/dshield_topattackers.xml +4 -0
  104. data/spec/feeds/data/feodo_domainlist.txt +18 -0
  105. data/spec/feeds/data/feodo_iplist.txt +20 -0
  106. data/spec/feeds/data/infiltrated_iplist.txt +16 -0
  107. data/spec/feeds/data/malc0de_domainlist.txt +18 -0
  108. data/spec/feeds/data/malc0de_iplist.txt +14 -0
  109. data/spec/feeds/data/mirc_domainlist.txt +31 -0
  110. data/spec/feeds/data/nothink_irc_iplist.txt +14 -0
  111. data/spec/feeds/data/nothink_ssh_iplist.txt +10 -0
  112. data/spec/feeds/data/openbl_iplist.txt +12 -0
  113. data/spec/feeds/data/palevo_domainlist.txt +25 -0
  114. data/spec/feeds/data/palevo_iplist.txt +24 -0
  115. data/spec/feeds/data/phishtank-sample.json.gz +0 -0
  116. data/spec/feeds/data/spyeye_domainlist.txt +16 -0
  117. data/spec/feeds/data/spyeye_iplist.txt +19 -0
  118. data/spec/feeds/data/t-arend-de_ssh_iplist.txt +17 -0
  119. data/spec/feeds/data/the_haleys_ssh_iplist.txt +12 -0
  120. data/spec/feeds/data/yourcmc_ssh-ip_reputation.txt +27 -0
  121. data/spec/feeds/data/zeus-ip_reputation.txt +285 -0
  122. data/spec/feeds/data/zeus_domainlist.txt +27 -0
  123. data/spec/feeds/dshield_attackers-top1000_spec.rb +43 -0
  124. data/spec/feeds/feodo-domain_reputation_spec.rb +50 -0
  125. data/spec/feeds/feodo-ip_reputation_spec.rb +50 -0
  126. data/spec/feeds/infiltrated-ip_reputation_spec.rb +50 -0
  127. data/spec/feeds/malc0de-domain_reputation_spec.rb +50 -0
  128. data/spec/feeds/malc0de-ip_reputation_spec.rb +50 -0
  129. data/spec/feeds/mirc-domain_reputation_spec.rb +50 -0
  130. data/spec/feeds/nothink_irc-ip_reputation_spec.rb +50 -0
  131. data/spec/feeds/nothink_ssh-ip_reputation_spec.rb +50 -0
  132. data/spec/feeds/openbl-ip_reputation_spec.rb +50 -0
  133. data/spec/feeds/palevo-domain_reputation_spec.rb +50 -0
  134. data/spec/feeds/palevo-ip_reputation_spec.rb +50 -0
  135. data/spec/feeds/phishtank_spec.rb +45 -0
  136. data/spec/feeds/spyeye-domain_reputation_spec.rb +50 -0
  137. data/spec/feeds/spyeye-ip_reputation_spec.rb +50 -0
  138. data/spec/feeds/t-arend-de_ssh-ip_reputation_spec.rb +50 -0
  139. data/spec/feeds/the_haleys_ssh-ip_reputation_spec.rb +50 -0
  140. data/spec/feeds/yourcmc_ssh-ip_reputation_spec.rb +50 -0
  141. data/spec/feeds/zeus-domain_reputation_spec.rb +50 -0
  142. data/spec/feeds/zeus-ip_reputation_spec.rb +50 -0
  143. data/spec/fixtures/feed/provider1/feed1.feed +6 -0
  144. data/spec/fixtures/parsers/test.xml +13 -0
  145. data/spec/fixtures/parsers/test_self_closing.xml +20 -0
  146. data/spec/fixtures/plugins/bad/threatinator/plugins/test_error1/plugin.rb +1 -0
  147. data/spec/fixtures/plugins/bad/threatinator/plugins/test_missing1/plugin.rb +0 -0
  148. data/spec/fixtures/plugins/fake.rb +19 -0
  149. data/spec/fixtures/plugins/good/threatinator/plugins/test_type1/plugin_a.rb +8 -0
  150. data/spec/fixtures/plugins/good/threatinator/plugins/test_type1/plugin_b.rb +8 -0
  151. data/spec/fixtures/plugins/good/threatinator/plugins/test_type2/plugin_c.rb +8 -0
  152. data/spec/fixtures/plugins/good/threatinator/plugins/test_type2/plugin_d.rb +8 -0
  153. data/spec/fixtures/plugins/good/threatinator/plugins/test_type3/plugin_e.rb +8 -0
  154. data/spec/fixtures/plugins/good/threatinator/plugins/test_type3/plugin_f.rb +8 -0
  155. data/spec/spec_helper.rb +52 -0
  156. data/spec/support/bad_feeds/missing_fetcher.feed +7 -0
  157. data/spec/support/bad_feeds/missing_name.feed +6 -0
  158. data/spec/support/bad_feeds/missing_parser.feed +3 -0
  159. data/spec/support/bad_feeds/missing_provider.feed +5 -0
  160. data/spec/support/factories/event.rb +27 -0
  161. data/spec/support/factories/feed.rb +32 -0
  162. data/spec/support/factories/feed_builder.rb +65 -0
  163. data/spec/support/factories/feed_registry.rb +8 -0
  164. data/spec/support/factories/output.rb +11 -0
  165. data/spec/support/factories/record.rb +17 -0
  166. data/spec/support/factories/xml_node.rb +33 -0
  167. data/spec/support/helpers/io.rb +11 -0
  168. data/spec/support/helpers/models.rb +13 -0
  169. data/spec/support/shared/action_builder.rb +47 -0
  170. data/spec/support/shared/decoder.rb +70 -0
  171. data/spec/support/shared/feeds.rb +218 -0
  172. data/spec/support/shared/fetcher.rb +48 -0
  173. data/spec/support/shared/filter.rb +14 -0
  174. data/spec/support/shared/io-like.rb +7 -0
  175. data/spec/support/shared/output.rb +120 -0
  176. data/spec/support/shared/parsers.rb +51 -0
  177. data/spec/support/shared/record.rb +111 -0
  178. data/spec/threatinator/actions/list/action_spec.rb +93 -0
  179. data/spec/threatinator/actions/run/action_spec.rb +89 -0
  180. data/spec/threatinator/actions/run/config_spec.rb +39 -0
  181. data/spec/threatinator/actions/run/coverage_observer_spec.rb +116 -0
  182. data/spec/threatinator/actions/run/output_config_spec.rb +89 -0
  183. data/spec/threatinator/cli/list_action_builder_spec.rb +57 -0
  184. data/spec/threatinator/cli/run_action_builder_spec.rb +133 -0
  185. data/spec/threatinator/cli_spec.rb +175 -0
  186. data/spec/threatinator/config/base_spec.rb +39 -0
  187. data/spec/threatinator/config/feed_search_spec.rb +76 -0
  188. data/spec/threatinator/decoders/gzip_spec.rb +75 -0
  189. data/spec/threatinator/event_builder_spec.rb +33 -0
  190. data/spec/threatinator/event_spec.rb +30 -0
  191. data/spec/threatinator/feed_builder_spec.rb +636 -0
  192. data/spec/threatinator/feed_registry_spec.rb +198 -0
  193. data/spec/threatinator/feed_runner_spec.rb +155 -0
  194. data/spec/threatinator/feed_spec.rb +169 -0
  195. data/spec/threatinator/fetcher_spec.rb +12 -0
  196. data/spec/threatinator/fetchers/http_spec.rb +32 -0
  197. data/spec/threatinator/filter_spec.rb +13 -0
  198. data/spec/threatinator/filters/block_spec.rb +16 -0
  199. data/spec/threatinator/filters/comments_spec.rb +13 -0
  200. data/spec/threatinator/filters/whitespace_spec.rb +12 -0
  201. data/spec/threatinator/parser_spec.rb +13 -0
  202. data/spec/threatinator/parsers/csv/parser_spec.rb +202 -0
  203. data/spec/threatinator/parsers/getline/parser_spec.rb +83 -0
  204. data/spec/threatinator/parsers/json/parser_spec.rb +106 -0
  205. data/spec/threatinator/parsers/json/record_spec.rb +30 -0
  206. data/spec/threatinator/parsers/xml/node_spec.rb +335 -0
  207. data/spec/threatinator/parsers/xml/parser_spec.rb +263 -0
  208. data/spec/threatinator/parsers/xml/path_spec.rb +209 -0
  209. data/spec/threatinator/parsers/xml/pattern_spec.rb +72 -0
  210. data/spec/threatinator/parsers/xml/record_spec.rb +27 -0
  211. data/spec/threatinator/plugin_loader_spec.rb +238 -0
  212. data/spec/threatinator/plugins/output/csv_spec.rb +46 -0
  213. data/spec/threatinator/plugins/output/null_spec.rb +17 -0
  214. data/spec/threatinator/plugins/output/rubydebug_spec.rb +37 -0
  215. data/spec/threatinator/property_definer_spec.rb +155 -0
  216. data/spec/threatinator/record_spec.rb +19 -0
  217. data/spec/threatinator/registry_spec.rb +97 -0
  218. data/spec/threatinator/runner_spec.rb +273 -0
  219. metadata +376 -0
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/config/base'
3
+
4
+ describe Threatinator::Config::Base do
5
+ describe '.properties' do
6
+ class Foobar3 < Threatinator::Config::Base
7
+ attribute :deep1, String, description: "deep prop1"
8
+ attribute :deep2, String, description: "deep prop2"
9
+ end
10
+
11
+ class Foobar2 < Threatinator::Config::Base
12
+ attribute :nested_prop1, String, description: "Nested prop1"
13
+ attribute :nested_prop2, String, description: "Nested prop2"
14
+ attribute :deep, Foobar3
15
+ end
16
+
17
+ class Foobar1 < Threatinator::Config::Base
18
+ attribute :prop1, String, description: "This is prop1"
19
+ attribute :prop2, Boolean, description: "This is prop2"
20
+ attribute :nested1, Foobar2
21
+ attribute :nested2, Foobar2
22
+ end
23
+
24
+ it "should return a hash of all properties and nested properties" do
25
+ expect(Foobar1.properties).to match({
26
+ "prop1" => ["This is prop1", Axiom::Types::String],
27
+ "prop2" => ["This is prop2", Axiom::Types::Boolean],
28
+ "nested1.nested_prop1" => ["Nested prop1", Axiom::Types::String],
29
+ "nested1.nested_prop2" => ["Nested prop2", Axiom::Types::String],
30
+ "nested1.deep.deep1" => ["deep prop1", Axiom::Types::String],
31
+ "nested1.deep.deep2" => ["deep prop2", Axiom::Types::String],
32
+ "nested2.nested_prop1" => ["Nested prop1", Axiom::Types::String],
33
+ "nested2.nested_prop2" => ["Nested prop2", Axiom::Types::String],
34
+ "nested2.deep.deep1" => ["deep prop1", Axiom::Types::String],
35
+ "nested2.deep.deep2" => ["deep prop2", Axiom::Types::String]
36
+ })
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/config/feed_search'
3
+
4
+ describe Threatinator::Config::FeedSearch do
5
+ describe "#path" do
6
+ context "when :path not specified" do
7
+ it "returns an empty array" do
8
+ x = described_class.new
9
+ expect(x.path).to eq([])
10
+ end
11
+ end
12
+ context "when :path is an array of strings" do
13
+ it "returns the array of strings" do
14
+ x = described_class.new(path: ['foo', 'bar'])
15
+ expect(x.path).to eq(['foo', 'bar'])
16
+ end
17
+ end
18
+ end
19
+
20
+ describe "#exclude_default" do
21
+ context "when :exclude_default is not specified" do
22
+ it "returns false" do
23
+ expect(described_class.new.exclude_default).to eq(false)
24
+ end
25
+ end
26
+
27
+ context "when :exclude_default is set to true" do
28
+ it "returns true" do
29
+ x = described_class.new(exclude_default: true)
30
+ expect(x.exclude_default).to eq(true)
31
+ end
32
+ end
33
+
34
+ context "when :exclude_default is set to false" do
35
+ it "returns false" do
36
+ x = described_class.new(exclude_default: false)
37
+ expect(x.exclude_default).to eq(false)
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "#search_path" do
43
+ context "when neither :path nor :exclude_default are supplied" do
44
+ it "returns an array containing the default search path" do
45
+ x = described_class.new
46
+ expect(x.search_path).to eq([described_class::DEFAULT_FEED_PATH])
47
+ end
48
+ end
49
+
50
+ context "when :path is an array of paths" do
51
+ let(:paths) { ["foo", "bar"] }
52
+ let(:config) { described_class.new(path: paths) }
53
+ specify "the first search paths are those specified by :path" do
54
+ expect(config.search_path[0..1]).to eq(['foo', 'bar'])
55
+ end
56
+
57
+ context "when :exclude_default is not specified" do
58
+ specify "the default search path is appended" do
59
+ expect(config.search_path[-1]).to eq(described_class::DEFAULT_FEED_PATH)
60
+ end
61
+ end
62
+ context "when :exclude_default is false" do
63
+ let(:config) { described_class.new(path: paths, exclude_default: false) }
64
+ specify "the default search path is appended" do
65
+ expect(config.search_path[-1]).to eq(described_class::DEFAULT_FEED_PATH)
66
+ end
67
+ end
68
+ context "when :exclude_default is true" do
69
+ let(:config) { described_class.new(path: paths, exclude_default: true) }
70
+ specify "the default search path is not appended" do
71
+ expect(config.search_path[-1]).not_to eq(described_class::DEFAULT_FEED_PATH)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/decoders/gzip'
3
+ require 'stringio'
4
+ require 'zlib'
5
+
6
+ describe Threatinator::Decoders::Gzip do
7
+ let(:encode_data_proc) {
8
+ lambda do |data|
9
+ sio = StringIO.new
10
+ sio.set_encoding("binary")
11
+ gz = Zlib::GzipWriter.new(sio)
12
+ gz.write(data)
13
+ gz.close
14
+ sio.string
15
+ end
16
+ }
17
+
18
+ it_should_behave_like "a decoder" do
19
+ let(:decoder_opts) { {} }
20
+ end
21
+
22
+ let(:uncompressed_data) {
23
+ ret = "".encode("binary")
24
+ '!'.upto('~') { |a| '!'.upto('~') { |b| ret << "#{a}#{b}" } }
25
+ ret
26
+ }
27
+
28
+ let(:compressed_data) {
29
+ encode_data_proc.call(uncompressed_data)
30
+ }
31
+
32
+ let(:decoder) { Threatinator::Decoders::Gzip.new }
33
+
34
+
35
+ context "normal operation" do
36
+ it "should decompress a Gzip compressed stream" do
37
+ encoded_io = StringIO.new(compressed_data)
38
+ expect(decoder.decode(encoded_io).read).to eq(uncompressed_data)
39
+ end
40
+ end
41
+
42
+ context "handling truncated data" do
43
+ let(:truncated_data) {
44
+ data = compressed_data
45
+ data_len = data.length / 2
46
+ data[0..(data_len - 1)]
47
+ }
48
+
49
+ it_should_behave_like "a decoder encountering an error during decoding" do
50
+ let(:input_io) {StringIO.new(truncated_data)}
51
+ end
52
+ end
53
+
54
+ context "handling a gzip stream with no footer" do
55
+ let(:missing_footer_data) {
56
+ compressed_data[0..-9] # knock off the footer, which is 8 bytes long
57
+ }
58
+
59
+ it_should_behave_like "a decoder encountering an error during decoding" do
60
+ let(:input_io) {StringIO.new(missing_footer_data)}
61
+ end
62
+ end
63
+
64
+ context "handling a gzip with an invalid footer" do
65
+ let(:missing_footer_data) {
66
+ ret = compressed_data
67
+ ret[-9..-1] = "\0\0\0\0\0\0\0\0"
68
+ ret
69
+ }
70
+
71
+ it_should_behave_like "a decoder encountering an error during decoding" do
72
+ let(:input_io) { StringIO.new(missing_footer_data) }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/event_builder'
3
+
4
+ describe Threatinator::EventBuilder do
5
+ let(:feed) { build(:feed, provider: "my_provider", name: "my_feed" ) }
6
+ let(:event_builder) { described_class.new(feed) }
7
+ describe "#create_event" do
8
+ it "should yield an event" do
9
+ expect { |b| event_builder.create_event(&b) }.to yield_with_args(kind_of(Threatinator::Event))
10
+ end
11
+
12
+ it "should increment the count each time it has been called" do
13
+ expect(event_builder.count).to eq(0)
14
+ event_builder.create_event { |e| }
15
+ expect(event_builder.count).to eq(1)
16
+ 10.times do
17
+ event_builder.create_event { |e| }
18
+ end
19
+ expect(event_builder.count).to eq(11)
20
+ end
21
+
22
+ describe "the yielded event" do
23
+ let(:event) { e = nil; event_builder.create_event { |x| e = x }; e }
24
+ specify "#feed_name is set to the feed's provider" do
25
+ expect(event.feed_name).to eq('my_feed')
26
+ end
27
+ specify "#feed_provider is set to the feed's provider" do
28
+ expect(event.feed_provider).to eq('my_provider')
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/event'
3
+
4
+ describe Threatinator::Event do
5
+ describe "#type" do
6
+ it "should default to nil"
7
+ it "should return the value if set"
8
+ end
9
+
10
+ describe "#type=" do
11
+ Threatinator::Event::VALID_TYPES.each do |type|
12
+ it "should be possible to set to #{type.inspect}"
13
+ end
14
+ it "should raise an InvalidAttributeError if set to something other than a symbol"
15
+ end
16
+
17
+ describe "#ipv4s" do
18
+ it "should return an array"
19
+ end
20
+
21
+ describe "#fqdns" do
22
+ it "should return an array"
23
+ end
24
+
25
+ describe "#add_ipv4" do
26
+ end
27
+
28
+ describe "#add_fqdn" do
29
+ end
30
+ end
@@ -0,0 +1,636 @@
1
+ require 'spec_helper'
2
+ require 'threatinator/feed_builder'
3
+ require 'threatinator/record'
4
+
5
+ describe Threatinator::FeedBuilder do
6
+ let (:provider) { 'FakeSecureCo' }
7
+ let (:name) { 'MaliciousDataFeed' }
8
+ let(:builder) { described_class.new }
9
+
10
+ context "without having been configured" do
11
+ describe "#build" do
12
+ it "should raise an error" do
13
+ expect { builder.build() }.to raise_error { |error|
14
+ expect(error).to be_kind_of(Threatinator::Exceptions::InvalidAttributeError)
15
+ }
16
+ end
17
+ end
18
+ end
19
+
20
+ shared_examples_for "a filter builder" do
21
+ it "should be a Proc" do
22
+ expect(filter_builder).to be_a(::Proc)
23
+ end
24
+ it "should generate a filter when called" do
25
+ expect(filter_builder.call).to respond_to(:filter?)
26
+ end
27
+ end
28
+
29
+ shared_examples_for "a decoder builder" do
30
+ it "should be a Proc" do
31
+ expect(decoder_builder).to be_a(::Proc)
32
+ end
33
+ it "should generate a kind of Threatinator::Decoder when called" do
34
+ expect(decoder_builder.call).to be_kind_of Threatinator::Decoder
35
+ end
36
+ end
37
+
38
+ shared_examples_for "an alias of #decode_gzip" do
39
+ let(:method_name) { :decode_gzip }
40
+ let(:builder) { build(:feed_builder, :buildable) }
41
+ it "should return the builder" do
42
+ expect(builder.send(method_name)).to eq(builder)
43
+ end
44
+
45
+ specify "calling the method should add a decoder_builder proc that generates a Threatinator::Decoders::Gzip" do
46
+ builder.send(method_name)
47
+ feed = builder.build
48
+ expect(feed.decoder_builders.count).to eq(1)
49
+ decoder_builder = feed.decoder_builders.first
50
+ expect(decoder_builder).to be_a(::Proc)
51
+ expect(decoder_builder.call).to be_a(Threatinator::Decoders::Gzip)
52
+ end
53
+
54
+ specify "multiple calls should add as many decoder_builders in the built feed" do
55
+ 5.times do
56
+ builder.send(method_name)
57
+ end
58
+
59
+ feed = builder.build
60
+ expect(feed.decoder_builders.count).to eq(5)
61
+ end
62
+ end
63
+
64
+ describe "#decode_gzip" do
65
+ it_should_behave_like "an alias of #decode_gzip" do
66
+ let(:method_name) { :decode_gzip }
67
+ end
68
+ end
69
+
70
+ describe "#extract_gzip" do
71
+ it_should_behave_like "an alias of #decode_gzip" do
72
+ let(:method_name) { :extract_gzip }
73
+ end
74
+ end
75
+
76
+ describe "#gunzip" do
77
+ it_should_behave_like "an alias of #decode_gzip" do
78
+ let(:method_name) { :gunzip}
79
+ end
80
+ end
81
+
82
+ describe "#filter_whitespace" do
83
+ let(:builder) { build(:feed_builder, :buildable) }
84
+
85
+ it "should return the builder" do
86
+ expect(builder.filter_whitespace).to eq(builder)
87
+ end
88
+
89
+ context "the built feed" do
90
+ let(:feed) {
91
+ builder.filter_whitespace
92
+ builder.build
93
+ }
94
+ describe "#filter_builders" do
95
+ it "should have one item" do
96
+ expect(feed.filter_builders.length).to eq(1)
97
+ end
98
+
99
+ describe "the first item" do
100
+ let(:filter_builder) { feed.filter_builders[0] }
101
+ it_should_behave_like "a filter builder"
102
+
103
+ it "should build Threatinator::Filters::Whitespace when called" do
104
+ expect(filter_builder.call).to be_kind_of(Threatinator::Filters::Whitespace)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "#filter_comments" do
112
+ let(:builder) { build(:feed_builder, :buildable) }
113
+
114
+ it "should return the builder" do
115
+ expect(builder.filter_comments).to eq(builder)
116
+ end
117
+
118
+ context "the built feed" do
119
+ let(:feed) {
120
+ builder.filter_comments
121
+ builder.build
122
+ }
123
+ describe "#filter_builders" do
124
+ it "should have one item" do
125
+ expect(feed.filter_builders.length).to eq(1)
126
+ end
127
+
128
+ describe "the first item" do
129
+ let(:filter_builder) { feed.filter_builders[0] }
130
+ it_should_behave_like "a filter builder"
131
+ it "should build a Threatinator::Filters::Comments when called" do
132
+ expect(filter_builder.call).to be_kind_of(Threatinator::Filters::Comments)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "#filter" do
140
+ let(:builder) { build(:feed_builder, :buildable) }
141
+
142
+ it "should return the builder" do
143
+ expect(builder.filter() { }).to eq(builder)
144
+ end
145
+
146
+ context "the built feed, when one filter is specified" do
147
+ let(:feed) {
148
+ builder.filter do |line|
149
+ line == "FILTER1"
150
+ end
151
+ builder.build
152
+ }
153
+ describe "#filter_builders" do
154
+ it "should have one item" do
155
+ expect(feed.filter_builders.length).to eq(1)
156
+ end
157
+
158
+ describe "the first item" do
159
+ let(:filter_builder) { feed.filter_builders[0] }
160
+ it_should_behave_like "a filter builder"
161
+
162
+ it "should build a Threatinator::Filters::Block when called" do
163
+ expect(filter_builder.call).to be_kind_of(Threatinator::Filters::Block)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ context "the built feed, when three filters are specified" do
170
+ let(:feed) {
171
+ builder.filter do |record|
172
+ record.data == "FILTER1"
173
+ end
174
+ builder.filter_comments
175
+ builder.filter_whitespace
176
+ builder.build
177
+ }
178
+ describe "#filter_builders" do
179
+ it "should have three items" do
180
+ expect(feed.filter_builders.length).to eq(3)
181
+ end
182
+
183
+ describe "the first filter builder" do
184
+ let(:filter_builder) { feed.filter_builders[0] }
185
+ it_should_behave_like "a filter builder"
186
+ describe "when called" do
187
+ subject {filter_builder.call}
188
+ it { should be_kind_of(Threatinator::Filters::Block) }
189
+ it "should be the first filter we added" do
190
+ expect(subject.filter?(build(:record, data:"FILTER1"))).to eq(true)
191
+ expect(subject.filter?(build(:record, data:"#comment"))).to eq(false)
192
+ expect(subject.filter?(build(:record, data:" "))).to eq(false)
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ describe "the second filter builder" do
199
+ let(:filter_builder) { feed.filter_builders[1] }
200
+ it_should_behave_like "a filter builder"
201
+ describe "when called" do
202
+ subject {filter_builder.call}
203
+ it { should be_kind_of(Threatinator::Filters::Comments) }
204
+ it "should be the first filter we added" do
205
+ expect(subject.filter?(build(:record, data:"FILTER1"))).to eq(false)
206
+ expect(subject.filter?(build(:record, data:"#comment"))).to eq(true)
207
+ expect(subject.filter?(build(:record, data:" "))).to eq(false)
208
+ end
209
+ end
210
+ end
211
+
212
+ describe "the third filter builder" do
213
+ let(:filter_builder) { feed.filter_builders[2] }
214
+ it_should_behave_like "a filter builder"
215
+ describe "when called" do
216
+ let(:filter) { filter_builder.call }
217
+ subject { filter }
218
+ it { should be_kind_of(Threatinator::Filters::Whitespace) }
219
+ it "should be the first filter we added" do
220
+ expect(subject.filter?(build(:record, data:"FILTER1"))).to eq(false)
221
+ expect(subject.filter?(build(:record, data:"#comment"))).to eq(false)
222
+ expect(subject.filter?(build(:record, data:" "))).to eq(true)
223
+ end
224
+ end
225
+ end
226
+
227
+ end
228
+ end
229
+
230
+ context "the built feed, when no filters are specified" do
231
+ let(:feed) {
232
+ builder.build
233
+ }
234
+ describe "#filter_builders" do
235
+ it "should have no filter builders" do
236
+ expect(feed.filter_builders.length).to eq(0)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ describe "#provider" do
243
+ let(:builder) { build(:feed_builder, :without_provider) }
244
+
245
+ it "should return the builder" do
246
+ expect(builder.provider("asdf")).to eq(builder)
247
+ end
248
+
249
+ context "the built feed" do
250
+ let(:feed) {
251
+ builder.provider(provider)
252
+ builder.build
253
+ }
254
+ it "#provider should be correct" do
255
+ expect(feed.provider).to eq(provider)
256
+ end
257
+ end
258
+ end
259
+
260
+ describe "#name" do
261
+ let(:builder) { build(:feed_builder, :without_name) }
262
+
263
+ it "should return the builder" do
264
+ expect(builder.name("asdf")).to eq(builder)
265
+ end
266
+
267
+ context "the built feed" do
268
+ let(:feed) {
269
+ builder.name(name)
270
+ builder.build
271
+ }
272
+ it "#name should be correct" do
273
+ expect(feed.name).to eq(name)
274
+ end
275
+ end
276
+ end
277
+
278
+ describe "#fetch_http" do
279
+ let(:url) { 'http://foo.com/bar' }
280
+ let(:builder) { build(:feed_builder, :without_fetcher) }
281
+
282
+ it "should return the builder" do
283
+ expect(builder.fetch_http('http://foo.bar/')).to eq(builder)
284
+ end
285
+
286
+ context "the built feed" do
287
+ let(:feed) {
288
+ builder.fetch_http(url)
289
+ builder.build
290
+ }
291
+ describe "#fetcher_builder" do
292
+ let(:fetcher_builder) { feed.fetcher_builder }
293
+ it "should be a Proc" do
294
+ expect(fetcher_builder).to be_a(::Proc)
295
+ end
296
+ it "should return an instance of Threatinator::Fetchers::Http when called" do
297
+ expect(fetcher_builder.call).to be_a(Threatinator::Fetchers::Http)
298
+ end
299
+ it "should return a brand new instance of a Threatinator::Fetchers::Http with each call" do
300
+ expect(fetcher_builder.call).not_to be(fetcher_builder.call)
301
+ end
302
+ it "should return instances that are eql? to each other" do
303
+ expect(fetcher_builder.call).to eql(fetcher_builder.call)
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ shared_examples_for "a parser builder" do
310
+ it "should be a Proc" do
311
+ expect(parser_builder).to be_a(::Proc)
312
+ end
313
+ it "should return a kind of Threatinator::Parser when called" do
314
+ expect(parser_builder.call).to be_a(Threatinator::Parser)
315
+ end
316
+ it "should return a brand new instances of a parser with each call" do
317
+ expect(parser_builder.call).not_to be(parser_builder.call)
318
+ end
319
+ it "should return instances that are eql? to each other" do
320
+ expect(parser_builder.call).to eql(parser_builder.call)
321
+ end
322
+ end
323
+
324
+ describe "#parse_xml" do
325
+ let(:builder) { build(:feed_builder, :without_parser) }
326
+
327
+ it "should return the builder" do
328
+ expect(builder.parse_xml('/some/path') {}).to eq(builder)
329
+ end
330
+
331
+ context "the built feed" do
332
+ let(:parser_block) { lambda { } }
333
+ let(:feed) {
334
+ builder.parse_xml('/some/path', &parser_block)
335
+ builder.build
336
+ }
337
+ describe "#parser_builder" do
338
+ let(:parser_builder) { feed.parser_builder}
339
+ it_should_behave_like "a parser builder"
340
+ it "should return an instance of Threatinator::Parsers::XML::Parser when called" do
341
+ expect(parser_builder.call).to be_a(Threatinator::Parsers::XML::Parser)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ describe "#parse_json" do
348
+ let(:builder) { build(:feed_builder, :without_parser) }
349
+
350
+ it "should return the builder" do
351
+ expect(builder.parse_json() {}).to eq(builder)
352
+ end
353
+
354
+ context "the built feed" do
355
+ let(:parser_block) { lambda { } }
356
+ let(:feed) {
357
+ builder.parse_json(&parser_block)
358
+ builder.build
359
+ }
360
+ describe "#parser_builder" do
361
+ let(:parser_builder) { feed.parser_builder}
362
+ it_should_behave_like "a parser builder"
363
+ it "should return an instance of Threatinator::Parsers::JSON::Parser when called" do
364
+ expect(parser_builder.call).to be_a(Threatinator::Parsers::JSON::Parser)
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ describe "#parse_eachline" do
371
+ let(:builder) { build(:feed_builder, :without_parser) }
372
+
373
+ it "should return the builder" do
374
+ expect(builder.parse_eachline() {}).to eq(builder)
375
+ end
376
+
377
+ context "the built feed" do
378
+ let(:parser_block) { lambda { } }
379
+ let(:feed) {
380
+ builder.parse_eachline(separator: "\n", &parser_block)
381
+ builder.build
382
+ }
383
+ describe "#parser_builder" do
384
+ let(:parser_builder) { feed.parser_builder}
385
+ it_should_behave_like "a parser builder"
386
+ it "should return an instance of Threatinator::Parsers::Getline::Parser when called" do
387
+ expect(parser_builder.call).to be_a(Threatinator::Parsers::Getline::Parser)
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ describe "#parse_csv" do
394
+ let(:builder) { build(:feed_builder, :without_parser) }
395
+
396
+ it "should return the builder" do
397
+ expect(builder.parse_csv() {}).to eq(builder)
398
+ end
399
+
400
+ context "the built feed" do
401
+ let(:parser_block) { lambda { } }
402
+ let(:feed) {
403
+ builder.parse_csv({}, &parser_block)
404
+ builder.build
405
+ }
406
+ describe "#parser_builder" do
407
+ let(:parser_builder) { feed.parser_builder}
408
+ it_should_behave_like "a parser builder"
409
+ it "should return an instance of Threatinator::Parsers::CSV::Parser when called" do
410
+ expect(parser_builder.call).to be_a(Threatinator::Parsers::CSV::Parser)
411
+ end
412
+ end
413
+ end
414
+ end
415
+
416
+ shared_examples_for "a DSL loader" do
417
+ # Expects :feed_loader as a proc
418
+ let(:feed_string) {
419
+ 'provider "provider1"
420
+ name "feed1"
421
+ fetch_http("https://foobar/feed1.data")
422
+
423
+ parse_eachline(:separator => "\n") do |builder, line|
424
+ end'
425
+ }
426
+
427
+ it "should return an instance of Threatinator::FeedBuilder" do
428
+ expect(feed_loader.call(feed_string)).to be_a(Threatinator::FeedBuilder)
429
+ end
430
+
431
+ it "should return a builder after parsing" do
432
+ builder = feed_loader.call(feed_string)
433
+ expect(builder).to be_a(Threatinator::FeedBuilder)
434
+ feed = builder.build
435
+ expect(feed.provider).to eq("provider1")
436
+ expect(feed.name).to eq("feed1")
437
+ expect(feed.parser_builder.call).to eq(Threatinator::Parsers::Getline::Parser.new(separator: "\n"))
438
+ end
439
+
440
+ context "when str contains invalid syntax" do
441
+ let(:feed_string) { 'provider "my_provider"
442
+ name "my_provider"
443
+ foo = 123 456'}
444
+
445
+ it "should raise an error on a syntax error" do
446
+ expect {
447
+ feed_loader.call(feed_string)
448
+ }.to raise_error { |e|
449
+ expect(e).to be_a(SyntaxError)
450
+ }
451
+ end
452
+ end
453
+
454
+ describe "#build" do
455
+ it "should raise an InvalidAttributeError if the feed is empty" do
456
+ expect { feed_loader.call("").build }.to raise_error { |error|
457
+ expect(error).to be_kind_of(Threatinator::Exceptions::InvalidAttributeError)
458
+ }
459
+ end
460
+ it "should raise a InvalidAttributeError if the feed is missing a provider" do
461
+ feed_string = '
462
+ name "feed1"
463
+ fetch_http("https://foobar/feed1.data")
464
+ parse_eachline(:separator => "\n") {}'
465
+ expect do
466
+ feed_loader.call(feed_string).build
467
+ end.to raise_error { |e|
468
+ expect(e).to be_a(Threatinator::Exceptions::InvalidAttributeError)
469
+ expect(e.attribute).to eq(:provider)
470
+ }
471
+ end
472
+
473
+ it "should raise a InvalidAttributeError if the feed is missing a name" do
474
+ feed_string = '
475
+ provider "provider1"
476
+ fetch_http("https://foobar/feed1.data")
477
+ parse_eachline(:separator => "\n") {}'
478
+ expect do
479
+ feed_loader.call(feed_string).build
480
+ end.to raise_error { |e|
481
+ expect(e).to be_a(Threatinator::Exceptions::InvalidAttributeError)
482
+ expect(e.attribute).to eq(:name)
483
+ }
484
+ end
485
+
486
+ it "should raise a InvalidAttributeError if the feed is missing a fetcher statement" do
487
+ feed_string = '
488
+ provider "provider1"
489
+ name "feed1"
490
+ parse_eachline(:separator => "\n") {}'
491
+ expect do
492
+ feed_loader.call(feed_string).build
493
+ end.to raise_error { |e|
494
+ expect(e).to be_a(Threatinator::Exceptions::InvalidAttributeError)
495
+ expect(e.attribute).to eq(:fetcher_builder)
496
+ }
497
+ end
498
+
499
+ it "should raise a InvalidAttributeError if the feed is missing a parser statement" do
500
+ feed_string = '
501
+ provider "provider1"
502
+ name "feed1"
503
+ fetch_http("https://foobar/feed1.data")'
504
+ expect do
505
+ feed_loader.call(feed_string).build
506
+ end.to raise_error { |e|
507
+ expect(e).to be_a(Threatinator::Exceptions::InvalidAttributeError)
508
+ }
509
+ end
510
+
511
+ context "when configured to fetch a url and parse each line, the feed" do
512
+ let(:url) { "http://foo.com/bar" }
513
+ let(:feed_string) {
514
+ 'provider "my_feed_provider"
515
+ name "my_feed_name"
516
+
517
+ fetch_http("http://foo.com/bar")
518
+
519
+ filter do |line|
520
+ line =~ /Bad stuff/
521
+ end
522
+
523
+ filter_whitespace
524
+ filter_comments
525
+
526
+ parse_eachline(separator: "\0") do |*args|
527
+ # parsing stuff
528
+ end'
529
+ }
530
+ let(:feed) { feed_loader.call(feed_string).build() }
531
+
532
+ it "#provider should be correct" do
533
+ expect(feed.provider).to eq("my_feed_provider")
534
+ end
535
+
536
+ it "#name should be correct" do
537
+ expect(feed.name).to eq("my_feed_name")
538
+ end
539
+
540
+ it "#parser_builder should generate the proper Threatinator::Parsers::Getline::Parser" do
541
+ expect(feed.parser_builder.call).to eq(Threatinator::Parsers::Getline::Parser.new(separator:"\0"))
542
+ end
543
+
544
+ describe "#filter_builders" do
545
+ subject { feed.filter_builders }
546
+
547
+ it "should have three filter builders" do
548
+ expect(subject.length).to eq(3)
549
+ end
550
+
551
+ describe "filter 1" do
552
+ subject {feed.filter_builders[0].call}
553
+ it { should be_a(Threatinator::Filters::Block) }
554
+ end
555
+ describe "filter 2" do
556
+ subject {feed.filter_builders[1].call}
557
+ it { should be_a(Threatinator::Filters::Whitespace) }
558
+ end
559
+ describe "filter 3" do
560
+ subject {feed.filter_builders[2].call }
561
+ it { should be_a(Threatinator::Filters::Comments) }
562
+ end
563
+ end
564
+ end
565
+ end
566
+
567
+ end
568
+
569
+ describe :from_string do
570
+ it_should_behave_like "a DSL loader" do
571
+ let(:feed_loader) { lambda { |arg| Threatinator::FeedBuilder.from_string(arg) } }
572
+ end
573
+ end
574
+
575
+ describe :from_file do
576
+ before :each do
577
+ @tempdir = Dir.mktmpdir
578
+ end
579
+
580
+ after :each do
581
+ FileUtils.remove_entry_secure @tempdir
582
+ end
583
+
584
+ it_should_behave_like "a DSL loader" do
585
+ let(:feed_loader) {
586
+ lambda do |arg|
587
+ filename = File.join(@tempdir, "file.feed")
588
+ File.open(filename, "w") do |fio|
589
+ fio.write(arg)
590
+ end
591
+ Threatinator::FeedBuilder.from_file(filename)
592
+ end
593
+ }
594
+ end
595
+
596
+ let(:feedfile) {FEED_FIXTURES.join("provider1", "feed1.feed").to_s}
597
+ let(:missing_file) {FEED_FIXTURES.join("provider1","non-existant.feed").to_s}
598
+
599
+ it "should return a builder after parsing the file" do
600
+ builder = Threatinator::FeedBuilder.from_file(feedfile)
601
+ expect(builder).to be_a(Threatinator::FeedBuilder)
602
+ feed = builder.build
603
+ expect(feed.provider).to eq("provider1")
604
+ expect(feed.name).to eq("feed1")
605
+ end
606
+
607
+ it "should raise Threatinator::Exceptions::FeedFileNotFoundError if the feed file cannot be found" do
608
+ expect {
609
+ Threatinator::FeedBuilder.from_file(missing_file)
610
+ }.to raise_error(Threatinator::Exceptions::FeedFileNotFoundError)
611
+ end
612
+
613
+ it "should call from_string(data, filename, lineno)" do
614
+ data = File.read(feedfile)
615
+ expect(Threatinator::FeedBuilder).to receive(:from_string).with(data, feedfile, 0)
616
+ Threatinator::FeedBuilder.from_file(feedfile)
617
+ end
618
+
619
+ end
620
+
621
+ describe :from_dsl do
622
+ it_should_behave_like "a DSL loader" do
623
+ let(:feed_loader) {
624
+ lambda do |arg|
625
+ Threatinator::FeedBuilder.from_dsl do
626
+ eval(arg)
627
+ end
628
+ end
629
+ }
630
+ end
631
+
632
+
633
+ end
634
+ end
635
+
636
+