gophish-ruby 0.4.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.
@@ -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
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gophish
4
- VERSION = '0.4.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/gophish-ruby.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'gophish/group'
4
4
  require_relative 'gophish/template'
5
5
  require_relative 'gophish/page'
6
6
  require_relative 'gophish/smtp'
7
+ require_relative 'gophish/campaign'
7
8
 
8
9
  module Gophish
9
10
  class << self
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.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eli Sebastian Herrera Aguilar
@@ -103,6 +103,7 @@ 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