gophish-ruby 0.3.0 → 1.0.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 +4 -4
- data/CHANGELOG.md +99 -2
- data/README.md +491 -1
- data/docs/API_REFERENCE.md +925 -0
- data/docs/EXAMPLES.md +1635 -0
- data/docs/GETTING_STARTED.md +364 -1
- data/lib/gophish/campaign.rb +330 -0
- data/lib/gophish/smtp.rb +99 -0
- data/lib/gophish/template.rb +7 -2
- data/lib/gophish/version.rb +1 -1
- data/lib/gophish-ruby.rb +2 -0
- metadata +3 -1
@@ -0,0 +1,330 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative 'template'
|
3
|
+
require_relative 'page'
|
4
|
+
require_relative 'smtp'
|
5
|
+
require_relative 'group'
|
6
|
+
require 'active_support/core_ext/object/blank'
|
7
|
+
|
8
|
+
module Gophish
|
9
|
+
class Campaign < Base
|
10
|
+
attribute :id, :integer
|
11
|
+
attribute :name, :string
|
12
|
+
attribute :created_date, :string
|
13
|
+
attribute :launch_date, :string
|
14
|
+
attribute :send_by_date, :string
|
15
|
+
attribute :completed_date, :string
|
16
|
+
attribute :template
|
17
|
+
attribute :page
|
18
|
+
attribute :status, :string
|
19
|
+
attribute :results, default: -> { [] }
|
20
|
+
attribute :groups, default: -> { [] }
|
21
|
+
attribute :timeline, default: -> { [] }
|
22
|
+
attribute :smtp
|
23
|
+
attribute :url, :string
|
24
|
+
|
25
|
+
define_attribute_methods :id, :name, :created_date, :launch_date, :send_by_date,
|
26
|
+
:completed_date, :template, :page, :status, :results,
|
27
|
+
:groups, :timeline, :smtp, :url
|
28
|
+
|
29
|
+
validates :name, presence: true
|
30
|
+
validates :template, presence: true
|
31
|
+
validates :page, presence: true
|
32
|
+
validates :groups, presence: true
|
33
|
+
validates :smtp, presence: true
|
34
|
+
validates :url, presence: true
|
35
|
+
validate :validate_groups_structure
|
36
|
+
validate :validate_results_structure
|
37
|
+
validate :validate_timeline_structure
|
38
|
+
|
39
|
+
def body_for_create
|
40
|
+
build_campaign_payload
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.get_results(id)
|
44
|
+
response = get "#{resource_path}/#{id}/results"
|
45
|
+
raise StandardError, "Campaign not found with id: #{id}" if response.response.code != '200'
|
46
|
+
|
47
|
+
response.parsed_response
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.get_summary(id)
|
51
|
+
response = get "#{resource_path}/#{id}/summary"
|
52
|
+
raise StandardError, "Campaign not found with id: #{id}" if response.response.code != '200'
|
53
|
+
|
54
|
+
response.parsed_response
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.complete(id)
|
58
|
+
response = get "#{resource_path}/#{id}/complete"
|
59
|
+
raise StandardError, "Campaign not found with id: #{id}" if response.response.code != '200'
|
60
|
+
|
61
|
+
response.parsed_response
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_results
|
65
|
+
self.class.get_results id
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_summary
|
69
|
+
self.class.get_summary id
|
70
|
+
end
|
71
|
+
|
72
|
+
def complete!
|
73
|
+
response = self.class.complete id
|
74
|
+
self.status = 'Completed' if response['success']
|
75
|
+
response
|
76
|
+
end
|
77
|
+
|
78
|
+
def in_progress?
|
79
|
+
status == 'In progress'
|
80
|
+
end
|
81
|
+
|
82
|
+
def completed?
|
83
|
+
status == 'Completed'
|
84
|
+
end
|
85
|
+
|
86
|
+
def launched?
|
87
|
+
!launch_date.blank?
|
88
|
+
end
|
89
|
+
|
90
|
+
def has_send_by_date?
|
91
|
+
!send_by_date.blank?
|
92
|
+
end
|
93
|
+
|
94
|
+
# Custom getters to return proper class instances
|
95
|
+
def template
|
96
|
+
convert_to_instance super, Gophish::Template
|
97
|
+
end
|
98
|
+
|
99
|
+
def page
|
100
|
+
convert_to_instance super, Gophish::Page
|
101
|
+
end
|
102
|
+
|
103
|
+
def smtp
|
104
|
+
convert_to_instance super, Gophish::Smtp
|
105
|
+
end
|
106
|
+
|
107
|
+
def groups
|
108
|
+
convert_to_instances super, Gophish::Group
|
109
|
+
end
|
110
|
+
|
111
|
+
def results
|
112
|
+
convert_to_instances super, Result
|
113
|
+
end
|
114
|
+
|
115
|
+
def timeline
|
116
|
+
convert_to_instances super, Event
|
117
|
+
end
|
118
|
+
|
119
|
+
# Custom setters to handle both instances and hashes
|
120
|
+
def template=(value)
|
121
|
+
@template_raw = value
|
122
|
+
super(convert_from_instance(value))
|
123
|
+
end
|
124
|
+
|
125
|
+
def page=(value)
|
126
|
+
@page_raw = value
|
127
|
+
super(convert_from_instance(value))
|
128
|
+
end
|
129
|
+
|
130
|
+
def smtp=(value)
|
131
|
+
@smtp_raw = value
|
132
|
+
super(convert_from_instance(value))
|
133
|
+
end
|
134
|
+
|
135
|
+
def groups=(value)
|
136
|
+
@groups_raw = value
|
137
|
+
super(convert_from_instances(value))
|
138
|
+
end
|
139
|
+
|
140
|
+
def results=(value)
|
141
|
+
super(convert_from_instances(value))
|
142
|
+
end
|
143
|
+
|
144
|
+
def timeline=(value)
|
145
|
+
super(convert_from_instances(value))
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def build_campaign_payload
|
151
|
+
{
|
152
|
+
name: name,
|
153
|
+
template: serialize_object(@template_raw || template),
|
154
|
+
page: serialize_object(@page_raw || page),
|
155
|
+
groups: serialize_groups,
|
156
|
+
smtp: serialize_object(@smtp_raw || smtp),
|
157
|
+
url: url, launch_date: launch_date, send_by_date: send_by_date
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
def serialize_object(obj)
|
162
|
+
return obj if obj.is_a? Hash
|
163
|
+
return { name: obj } if obj.is_a? String
|
164
|
+
return { name: obj.name } if obj.respond_to? :name
|
165
|
+
|
166
|
+
obj
|
167
|
+
end
|
168
|
+
|
169
|
+
def serialize_groups
|
170
|
+
raw_groups = @groups_raw || groups
|
171
|
+
return raw_groups if raw_groups.empty?
|
172
|
+
|
173
|
+
raw_groups.map { |group| serialize_object group }
|
174
|
+
end
|
175
|
+
|
176
|
+
def validate_groups_structure
|
177
|
+
return if groups.blank?
|
178
|
+
return errors.add :groups, 'must be an array' unless groups.is_a? Array
|
179
|
+
return errors.add :groups, 'cannot be empty' if groups.empty?
|
180
|
+
|
181
|
+
groups.each_with_index { |group, index| validate_group group, index }
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_group(group, index)
|
185
|
+
return if group.is_a?(Hash) && (group[:name] || group['name'])
|
186
|
+
return if group.respond_to? :name
|
187
|
+
|
188
|
+
errors.add :groups, "item at index #{index} must have a name"
|
189
|
+
end
|
190
|
+
|
191
|
+
def validate_results_structure
|
192
|
+
return if results.blank?
|
193
|
+
return errors.add :results, 'must be an array' unless results.is_a? Array
|
194
|
+
|
195
|
+
results.each_with_index { |result, index| validate_result result, index }
|
196
|
+
end
|
197
|
+
|
198
|
+
def validate_result(result, index)
|
199
|
+
return errors.add :results, "item at index #{index} must be a hash" unless result.is_a? Hash
|
200
|
+
|
201
|
+
validate_result_field result, index, :email
|
202
|
+
end
|
203
|
+
|
204
|
+
def validate_timeline_structure
|
205
|
+
return if timeline.blank?
|
206
|
+
return errors.add :timeline, 'must be an array' unless timeline.is_a? Array
|
207
|
+
|
208
|
+
timeline.each_with_index { |event, index| validate_timeline_event event, index }
|
209
|
+
end
|
210
|
+
|
211
|
+
def validate_timeline_event(event, index)
|
212
|
+
return errors.add :timeline, "item at index #{index} must be a hash" unless event.is_a? Hash
|
213
|
+
|
214
|
+
validate_event_field event, index, :time
|
215
|
+
validate_event_field event, index, :message
|
216
|
+
end
|
217
|
+
|
218
|
+
def validate_result_field(result, index, field)
|
219
|
+
value = result[field] || result[field.to_s]
|
220
|
+
return unless value.blank?
|
221
|
+
|
222
|
+
errors.add :results, "item at index #{index} must have a #{field}"
|
223
|
+
end
|
224
|
+
|
225
|
+
def validate_event_field(event, index, field)
|
226
|
+
value = event[field] || event[field.to_s]
|
227
|
+
return unless value.blank?
|
228
|
+
|
229
|
+
errors.add :timeline, "item at index #{index} must have a #{field}"
|
230
|
+
end
|
231
|
+
|
232
|
+
# Helper methods for instance conversion
|
233
|
+
def convert_to_instance(value, klass)
|
234
|
+
return nil if value.nil?
|
235
|
+
return value if value.is_a? klass
|
236
|
+
return create_instance_safely value, klass if value.is_a? Hash
|
237
|
+
|
238
|
+
value
|
239
|
+
end
|
240
|
+
|
241
|
+
def create_instance_safely(hash, klass)
|
242
|
+
klass.new hash
|
243
|
+
rescue ActiveModel::UnknownAttributeError
|
244
|
+
# Filter out unknown attributes and try again
|
245
|
+
known_attributes = klass.attribute_names.map(&:to_s)
|
246
|
+
filtered_hash = hash.select { |k, _v| known_attributes.include? k.to_s }
|
247
|
+
klass.new filtered_hash
|
248
|
+
end
|
249
|
+
|
250
|
+
def convert_to_instances(values, klass)
|
251
|
+
return [] if values.blank?
|
252
|
+
return values unless values.is_a? Array
|
253
|
+
|
254
|
+
values.map { |value| convert_to_instance value, klass }
|
255
|
+
end
|
256
|
+
|
257
|
+
def convert_from_instance(value)
|
258
|
+
return value if value.nil? || value.is_a?(Hash)
|
259
|
+
|
260
|
+
value
|
261
|
+
end
|
262
|
+
|
263
|
+
def convert_from_instances(values)
|
264
|
+
return values if values.blank?
|
265
|
+
return values unless values.is_a? Array
|
266
|
+
|
267
|
+
values
|
268
|
+
end
|
269
|
+
|
270
|
+
class Result
|
271
|
+
include ActiveModel::Model
|
272
|
+
include ActiveModel::Attributes
|
273
|
+
|
274
|
+
attribute :id, :string
|
275
|
+
attribute :first_name, :string
|
276
|
+
attribute :last_name, :string
|
277
|
+
attribute :position, :string
|
278
|
+
attribute :email, :string
|
279
|
+
attribute :status, :string
|
280
|
+
attribute :ip, :string
|
281
|
+
attribute :latitude, :float
|
282
|
+
attribute :longitude, :float
|
283
|
+
attribute :send_date, :string
|
284
|
+
attribute :reported, :boolean, default: false
|
285
|
+
attribute :modified_date, :string
|
286
|
+
|
287
|
+
def reported?
|
288
|
+
reported == true
|
289
|
+
end
|
290
|
+
|
291
|
+
def clicked?
|
292
|
+
status == 'Clicked Link'
|
293
|
+
end
|
294
|
+
|
295
|
+
def opened?
|
296
|
+
status == 'Email Opened'
|
297
|
+
end
|
298
|
+
|
299
|
+
def sent?
|
300
|
+
status == 'Email Sent'
|
301
|
+
end
|
302
|
+
|
303
|
+
def submitted_data?
|
304
|
+
status == 'Submitted Data'
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
class Event
|
309
|
+
include ActiveModel::Model
|
310
|
+
include ActiveModel::Attributes
|
311
|
+
|
312
|
+
attribute :email, :string
|
313
|
+
attribute :time, :string
|
314
|
+
attribute :message, :string
|
315
|
+
attribute :details, :string
|
316
|
+
|
317
|
+
def has_details?
|
318
|
+
!details.blank?
|
319
|
+
end
|
320
|
+
|
321
|
+
def parsed_details
|
322
|
+
return {} if details.blank?
|
323
|
+
|
324
|
+
JSON.parse details
|
325
|
+
rescue JSON::ParserError
|
326
|
+
{}
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
data/lib/gophish/smtp.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
module Gophish
|
5
|
+
class Smtp < Base
|
6
|
+
def self.resource_path
|
7
|
+
'/smtp'
|
8
|
+
end
|
9
|
+
attribute :id, :integer
|
10
|
+
attribute :name, :string
|
11
|
+
attribute :username, :string
|
12
|
+
attribute :password, :string
|
13
|
+
attribute :host, :string
|
14
|
+
attribute :interface_type, :string, default: 'SMTP'
|
15
|
+
attribute :from_address, :string
|
16
|
+
attribute :ignore_cert_errors, :boolean, default: false
|
17
|
+
attribute :modified_date, :string
|
18
|
+
attribute :headers, default: -> { [] }
|
19
|
+
|
20
|
+
define_attribute_methods :id, :name, :username, :password, :host, :interface_type, :from_address,
|
21
|
+
:ignore_cert_errors, :modified_date, :headers
|
22
|
+
|
23
|
+
validates :name, presence: true
|
24
|
+
validates :host, presence: true
|
25
|
+
validates :from_address, presence: true
|
26
|
+
validate :validate_from_address_format
|
27
|
+
validate :validate_headers_structure
|
28
|
+
|
29
|
+
def body_for_create
|
30
|
+
{
|
31
|
+
name:, username:, password:, host:,
|
32
|
+
interface_type:,
|
33
|
+
from_address:,
|
34
|
+
ignore_cert_errors:,
|
35
|
+
headers:
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_header(key, value)
|
40
|
+
headers << { key:, value: }
|
41
|
+
headers_will_change!
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_header(key)
|
45
|
+
original_size = headers.size
|
46
|
+
headers.reject! { |header| header[:key] == key || header['key'] == key }
|
47
|
+
headers_will_change! if headers.size != original_size
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_headers?
|
51
|
+
!headers.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
def header_count
|
55
|
+
headers.length
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_authentication?
|
59
|
+
!username.blank? && !password.blank?
|
60
|
+
end
|
61
|
+
|
62
|
+
def ignores_cert_errors?
|
63
|
+
ignore_cert_errors == true
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def validate_from_address_format
|
69
|
+
return if from_address.blank?
|
70
|
+
|
71
|
+
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
72
|
+
|
73
|
+
unless from_address.match? email_regex
|
74
|
+
errors.add :from_address, 'must be a valid email format (email@domain.com)'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_headers_structure
|
79
|
+
return if headers.blank?
|
80
|
+
return errors.add :headers, 'must be an array' unless headers.is_a? Array
|
81
|
+
|
82
|
+
headers.each_with_index { |header, index| validate_header header, index }
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_header(header, index)
|
86
|
+
return errors.add :headers, "item at index #{index} must be a hash" unless header.is_a? Hash
|
87
|
+
|
88
|
+
validate_header_field header, index, :key
|
89
|
+
validate_header_field header, index, :value
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_header_field(header, index, field)
|
93
|
+
value = header[field] || header[field.to_s]
|
94
|
+
return unless value.blank?
|
95
|
+
|
96
|
+
errors.add :headers, "item at index #{index} must have a #{field}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/gophish/template.rb
CHANGED
@@ -6,20 +6,21 @@ module Gophish
|
|
6
6
|
class Template < Base
|
7
7
|
attribute :id, :integer
|
8
8
|
attribute :name, :string
|
9
|
+
attribute :envelope_sender, :string
|
9
10
|
attribute :subject, :string
|
10
11
|
attribute :text, :string
|
11
12
|
attribute :html, :string
|
12
13
|
attribute :modified_date, :string
|
13
14
|
attribute :attachments, default: -> { [] }
|
14
15
|
|
15
|
-
define_attribute_methods :id, :name, :subject, :text, :html, :modified_date, :attachments
|
16
|
+
define_attribute_methods :id, :name, :envelope_sender, :subject, :text, :html, :modified_date, :attachments
|
16
17
|
|
17
18
|
validates :name, presence: true
|
18
19
|
validate :validate_content_presence
|
19
20
|
validate :validate_attachments_structure
|
20
21
|
|
21
22
|
def body_for_create
|
22
|
-
{ name:, subject:, text:, html:, attachments: }
|
23
|
+
{ name:, envelope_sender:, subject:, text:, html:, attachments: }
|
23
24
|
end
|
24
25
|
|
25
26
|
def self.import_email(content, convert_links: false)
|
@@ -57,6 +58,10 @@ module Gophish
|
|
57
58
|
attachments.length
|
58
59
|
end
|
59
60
|
|
61
|
+
def has_envelope_sender?
|
62
|
+
!envelope_sender.blank?
|
63
|
+
end
|
64
|
+
|
60
65
|
private
|
61
66
|
|
62
67
|
def encode_content(content)
|
data/lib/gophish/version.rb
CHANGED
data/lib/gophish-ruby.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gophish-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eli Sebastian Herrera Aguilar
|
@@ -103,9 +103,11 @@ files:
|
|
103
103
|
- docs/GETTING_STARTED.md
|
104
104
|
- lib/gophish-ruby.rb
|
105
105
|
- lib/gophish/base.rb
|
106
|
+
- lib/gophish/campaign.rb
|
106
107
|
- lib/gophish/configuration.rb
|
107
108
|
- lib/gophish/group.rb
|
108
109
|
- lib/gophish/page.rb
|
110
|
+
- lib/gophish/smtp.rb
|
109
111
|
- lib/gophish/template.rb
|
110
112
|
- lib/gophish/version.rb
|
111
113
|
- sig/gophish/ruby.rbs
|