gophish-ruby 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,340 @@
1
+ # Getting Started Guide
2
+
3
+ This guide will help you get up and running with the Gophish Ruby SDK quickly.
4
+
5
+ ## Prerequisites
6
+
7
+ - Ruby >= 3.1.0
8
+ - A running Gophish server
9
+ - API access credentials for your Gophish instance
10
+
11
+ ## Installation
12
+
13
+ Add the gem to your project:
14
+
15
+ ```bash
16
+ # Add to Gemfile
17
+ echo 'gem "gophish-ruby"' >> Gemfile
18
+ bundle install
19
+
20
+ # Or install directly
21
+ gem install gophish-ruby
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Configure the SDK
27
+
28
+ First, configure the SDK with your Gophish server details:
29
+
30
+ ```ruby
31
+ require 'gophish-ruby'
32
+
33
+ Gophish.configure do |config|
34
+ config.url = "https://your-gophish-server.com"
35
+ config.api_key = "your-api-key-here"
36
+ config.verify_ssl = true
37
+ config.debug_output = false
38
+ end
39
+ ```
40
+
41
+ **Getting Your API Key:**
42
+
43
+ 1. Log into your Gophish admin panel
44
+ 2. Navigate to Settings → Users
45
+ 3. Click on your user account
46
+ 4. Copy the API Key from the user details
47
+
48
+ ### 2. Test the Connection
49
+
50
+ Verify your configuration works:
51
+
52
+ ```ruby
53
+ # Try to fetch existing groups (should return empty array if none exist)
54
+ begin
55
+ groups = Gophish::Group.all
56
+ puts "✓ Connected successfully! Found #{groups.length} groups."
57
+ rescue StandardError => e
58
+ puts "✗ Connection failed: #{e.message}"
59
+ puts "Please check your configuration."
60
+ end
61
+ ```
62
+
63
+ ### 3. Create Your First Group
64
+
65
+ ```ruby
66
+ # Create a simple group with one target
67
+ group = Gophish::Group.new(
68
+ name: "My First Group",
69
+ targets: [
70
+ {
71
+ first_name: "Test",
72
+ last_name: "User",
73
+ email: "test@example.com",
74
+ position: "Tester"
75
+ }
76
+ ]
77
+ )
78
+
79
+ if group.save
80
+ puts "✓ Group created successfully with ID: #{group.id}"
81
+ else
82
+ puts "✗ Failed to create group:"
83
+ group.errors.full_messages.each { |error| puts " - #{error}" }
84
+ end
85
+ ```
86
+
87
+ ## Common Workflows
88
+
89
+ ### Importing Targets from CSV
90
+
91
+ The most common use case is importing a list of targets from a CSV file:
92
+
93
+ ```ruby
94
+ # Prepare your CSV file with headers: First Name,Last Name,Email,Position
95
+ csv_content = <<~CSV
96
+ First Name,Last Name,Email,Position
97
+ John,Doe,john@company.com,Manager
98
+ Jane,Smith,jane@company.com,Developer
99
+ Bob,Wilson,bob@company.com,Analyst
100
+ CSV
101
+
102
+ # Create and import
103
+ group = Gophish::Group.new(name: "Company Employees")
104
+ group.import_csv(csv_content)
105
+
106
+ puts "Imported #{group.targets.length} targets"
107
+ group.save
108
+ ```
109
+
110
+ ### Reading from a File
111
+
112
+ ```ruby
113
+ # Read CSV from file
114
+ csv_content = File.read("employees.csv")
115
+
116
+ group = Gophish::Group.new(name: "Employees")
117
+ group.import_csv(csv_content)
118
+
119
+ if group.valid?
120
+ group.save
121
+ puts "Imported #{group.targets.length} employees"
122
+ else
123
+ puts "Validation errors:"
124
+ group.errors.full_messages.each { |error| puts " - #{error}" }
125
+ end
126
+ ```
127
+
128
+ ### Managing Existing Groups
129
+
130
+ ```ruby
131
+ # List all groups
132
+ puts "Existing groups:"
133
+ Gophish::Group.all.each do |group|
134
+ puts " #{group.id}: #{group.name} (#{group.targets.length} targets)"
135
+ end
136
+
137
+ # Find and update a specific group
138
+ group = Gophish::Group.find(1)
139
+ group.name = "Updated Group Name"
140
+
141
+ # Add new targets
142
+ group.targets << {
143
+ first_name: "New",
144
+ last_name: "Employee",
145
+ email: "new.employee@company.com",
146
+ position: "Intern"
147
+ }
148
+
149
+ group.save
150
+ ```
151
+
152
+ ### Deleting Groups
153
+
154
+ ```ruby
155
+ # Find and delete a group
156
+ group = Gophish::Group.find(1)
157
+ if group.destroy
158
+ puts "Group deleted successfully"
159
+ else
160
+ puts "Failed to delete group"
161
+ end
162
+ ```
163
+
164
+ ## Error Handling
165
+
166
+ Always handle errors gracefully in production code:
167
+
168
+ ```ruby
169
+ def create_group_safely(name, csv_file_path)
170
+ # Read CSV file
171
+ begin
172
+ csv_content = File.read(csv_file_path)
173
+ rescue Errno::ENOENT
174
+ puts "Error: CSV file not found at #{csv_file_path}"
175
+ return false
176
+ end
177
+
178
+ # Create group
179
+ group = Gophish::Group.new(name: name)
180
+
181
+ # Import CSV
182
+ begin
183
+ group.import_csv(csv_content)
184
+ rescue CSV::MalformedCSVError => e
185
+ puts "Error: Invalid CSV format - #{e.message}"
186
+ return false
187
+ end
188
+
189
+ # Validate
190
+ unless group.valid?
191
+ puts "Validation errors:"
192
+ group.errors.full_messages.each { |error| puts " - #{error}" }
193
+ return false
194
+ end
195
+
196
+ # Save
197
+ unless group.save
198
+ puts "Save failed:"
199
+ group.errors.full_messages.each { |error| puts " - #{error}" }
200
+ return false
201
+ end
202
+
203
+ puts "✓ Group '#{name}' created successfully with #{group.targets.length} targets"
204
+ true
205
+ end
206
+
207
+ # Usage
208
+ create_group_safely("Sales Team", "sales_team.csv")
209
+ ```
210
+
211
+ ## Development Setup
212
+
213
+ For development and testing, you might want to use different settings:
214
+
215
+ ```ruby
216
+ # Development configuration
217
+ if ENV['RAILS_ENV'] == 'development' || ENV['RUBY_ENV'] == 'development'
218
+ Gophish.configure do |config|
219
+ config.url = "https://localhost:3333"
220
+ config.api_key = "dev-api-key"
221
+ config.verify_ssl = false # For self-signed certificates
222
+ config.debug_output = true # See HTTP requests
223
+ end
224
+ else
225
+ # Production configuration
226
+ Gophish.configure do |config|
227
+ config.url = ENV['GOPHISH_URL']
228
+ config.api_key = ENV['GOPHISH_API_KEY']
229
+ config.verify_ssl = true
230
+ config.debug_output = false
231
+ end
232
+ end
233
+ ```
234
+
235
+ ## Validation and Data Quality
236
+
237
+ The SDK provides comprehensive validation:
238
+
239
+ ### Required Fields
240
+
241
+ All these fields are required for each target:
242
+
243
+ - `first_name`
244
+ - `last_name`
245
+ - `email` (must be valid format)
246
+ - `position`
247
+
248
+ ### Email Validation
249
+
250
+ The SDK validates email format using a regex pattern:
251
+
252
+ ```ruby
253
+ # Valid emails
254
+ "user@example.com"
255
+ "first.last@company.co.uk"
256
+ "user+tag@domain.org"
257
+
258
+ # Invalid emails (will cause validation errors)
259
+ "invalid-email"
260
+ "@domain.com"
261
+ "user@"
262
+ ""
263
+ ```
264
+
265
+ ### Custom Validation
266
+
267
+ You can check validation before saving:
268
+
269
+ ```ruby
270
+ group = Gophish::Group.new(name: "Test", targets: [...])
271
+
272
+ if group.valid?
273
+ puts "✓ All validations passed"
274
+ group.save
275
+ else
276
+ puts "✗ Validation failed:"
277
+
278
+ # Show all errors
279
+ group.errors.full_messages.each { |error| puts " - #{error}" }
280
+
281
+ # Check specific field errors
282
+ if group.errors[:name].any?
283
+ puts "Name issues: #{group.errors[:name].join(', ')}"
284
+ end
285
+
286
+ if group.errors[:targets].any?
287
+ puts "Target issues: #{group.errors[:targets].join(', ')}"
288
+ end
289
+ end
290
+ ```
291
+
292
+ ## Next Steps
293
+
294
+ Now that you have the basics:
295
+
296
+ 1. **Read the [API Reference](API_REFERENCE.md)** for detailed method documentation
297
+ 2. **Check out [Examples](EXAMPLES.md)** for more complex scenarios
298
+ 3. **Set up proper error handling** for production use
299
+ 4. **Consider security** - never log API keys or sensitive data
300
+
301
+ ## Troubleshooting
302
+
303
+ ### Common Issues
304
+
305
+ **"Connection refused" errors:**
306
+
307
+ - Check that your Gophish server is running
308
+ - Verify the URL is correct (include protocol: https://)
309
+ - Ensure the port is correct
310
+
311
+ **"SSL certificate verify failed" errors:**
312
+
313
+ - Set `config.verify_ssl = false` for self-signed certificates
314
+ - Or properly configure SSL certificates on your Gophish server
315
+
316
+ **"Invalid API key" errors:**
317
+
318
+ - Double-check your API key in the Gophish admin panel
319
+ - Ensure there are no extra spaces or characters
320
+
321
+ **CSV import fails:**
322
+
323
+ - Verify CSV headers exactly match: "First Name", "Last Name", "Email", "Position"
324
+ - Check for malformed CSV data (unescaped quotes, etc.)
325
+ - Ensure all required fields are present for each row
326
+
327
+ **Validation errors:**
328
+
329
+ - Check that all required fields are present
330
+ - Verify email addresses have valid format
331
+ - Ensure group name is not empty
332
+
333
+ ### Getting Help
334
+
335
+ If you encounter issues:
336
+
337
+ 1. Check the [API Reference](API_REFERENCE.md) for detailed documentation
338
+ 2. Look at [Examples](EXAMPLES.md) for working code samples
339
+ 3. Open an issue on [GitHub](https://github.com/EliSebastian/gophish-ruby/issues)
340
+ 4. Consult the [Gophish documentation](https://docs.getgophish.com/) for server-side issues
@@ -0,0 +1,206 @@
1
+ require 'httparty'
2
+ require 'active_support'
3
+ require 'active_model'
4
+ require 'active_record'
5
+ require 'json'
6
+ require 'uri'
7
+ require_relative 'configuration'
8
+
9
+ module Gophish
10
+ class Base
11
+ include HTTParty
12
+ include ActiveSupport::Inflector
13
+ include ActiveModel::Model
14
+ include ActiveModel::Attributes
15
+ include ActiveModel::Validations
16
+ include ActiveRecord::Callbacks
17
+
18
+ def initialize(attributes = {})
19
+ @persisted = false
20
+ @changed_attributes = {}
21
+ super(attributes)
22
+ end
23
+
24
+ def self.configuration
25
+ @configuration ||= {
26
+ base_uri: "#{Gophish.configuration.url}/api",
27
+ headers: { 'Authorization' => Gophish.configuration.api_key },
28
+ verify: Gophish.configuration.verify_ssl,
29
+ debug_output: Gophish.configuration.debug_output ? STDOUT : nil
30
+ }
31
+ end
32
+
33
+ def self.get(path, options = {})
34
+ options = configuration.merge options
35
+ options[:headers] = (options[:headers] || {}).merge(configuration[:headers])
36
+ super(path, options)
37
+ end
38
+
39
+ def self.post(path, options = {})
40
+ options = configuration.merge options
41
+ options[:headers] = (options[:headers] || {}).merge(configuration[:headers])
42
+ super(path, options)
43
+ end
44
+
45
+ def self.put(path, options = {})
46
+ options = configuration.merge options
47
+ options[:headers] = (options[:headers] || {}).merge(configuration[:headers])
48
+ super(path, options)
49
+ end
50
+
51
+ def self.delete(path, options = {})
52
+ options = configuration.merge options
53
+ options[:headers] = (options[:headers] || {}).merge(configuration[:headers])
54
+ super(path, options)
55
+ end
56
+
57
+ def self.resource_name
58
+ name.split('::').last.underscore.dasherize
59
+ end
60
+
61
+ def self.resource_path
62
+ "/#{resource_name.pluralize}"
63
+ end
64
+
65
+ def self.all
66
+ response = get "#{resource_path}/"
67
+ if response.response.code == '200'
68
+ response.parsed_response.map do |data|
69
+ instance = new filter_attributes(data)
70
+ instance.instance_variable_set :@persisted, true
71
+ instance
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.find(id)
77
+ response = get "#{resource_path}/#{id}"
78
+ raise StandardError, "Resource not found with id: #{id}" if response.response.code != '200'
79
+
80
+ data = JSON.parse response.body
81
+ instance = new filter_attributes(data)
82
+ instance.instance_variable_set :@persisted, true
83
+ instance
84
+ end
85
+
86
+ def save
87
+ return false unless valid?
88
+
89
+ return update_record if persisted?
90
+
91
+ create_record
92
+ end
93
+
94
+ def update_attributes(attributes)
95
+ attributes.each { |key, value| send "#{key}=", value if respond_to? "#{key}=" }
96
+ save
97
+ end
98
+
99
+ def destroy
100
+ return false unless persisted?
101
+ return false if id.nil?
102
+
103
+ response = self.class.delete "#{self.class.resource_path}/#{id}"
104
+ return handle_error_response response unless response.success?
105
+
106
+ @persisted = false
107
+ freeze
108
+ true
109
+ end
110
+
111
+ def persisted?
112
+ @persisted && !id.nil?
113
+ end
114
+
115
+ def new_record?
116
+ !persisted?
117
+ end
118
+
119
+ private
120
+
121
+ def update_attributes_from_response(parsed_response)
122
+ return unless parsed_response.is_a? Hash
123
+
124
+ parsed_response.each do |key, value|
125
+ send "#{key}=", value if respond_to? "#{key}="
126
+ end
127
+ @persisted = true
128
+ @changed_attributes.clear
129
+ end
130
+
131
+ def handle_error_response(response)
132
+ errors.add :base, response.parsed_response['message'] if response.parsed_response['success'] == false
133
+ false
134
+ end
135
+
136
+ private
137
+
138
+ def create_record
139
+ response = self.class.post "#{self.class.resource_path}/", request_options(body_for_create)
140
+ return handle_error_response response unless response.success?
141
+
142
+ update_attributes_from_response response.parsed_response
143
+ true
144
+ end
145
+
146
+ def update_record
147
+ return false if id.nil?
148
+ return true if @changed_attributes.empty?
149
+
150
+ response = self.class.put "#{self.class.resource_path}/#{id}/", request_options(body_for_update)
151
+ return handle_error_response response unless response.success?
152
+
153
+ update_attributes_from_response response.parsed_response
154
+ true
155
+ end
156
+
157
+ def request_options(body)
158
+ { body: body.to_json, headers: { 'Content-Type' => 'application/json' } }
159
+ end
160
+
161
+ def self.filter_attributes(data)
162
+ return data unless data.is_a? Hash
163
+
164
+ defined_attributes = attribute_names.map(&:to_s)
165
+
166
+ filtered_data = {}
167
+ data.each do |key, value|
168
+ snake_case_key = key.to_s.underscore
169
+ filtered_data[snake_case_key] = value if defined_attributes.include? snake_case_key
170
+ end
171
+
172
+ filtered_data
173
+ end
174
+
175
+ def body_for_create
176
+ raise NotImplementedError, 'You must implement the body_for_create method in your subclass'
177
+ end
178
+
179
+ def body_for_update
180
+ body_for_create
181
+ end
182
+
183
+ def attribute_changed?(attribute)
184
+ @changed_attributes.key? attribute.to_s
185
+ end
186
+
187
+ def changed_attributes
188
+ @changed_attributes.keys
189
+ end
190
+
191
+ def attribute_was(attribute)
192
+ @changed_attributes[attribute.to_s]
193
+ end
194
+
195
+ def []=(attribute, value)
196
+ attribute_str = attribute.to_s
197
+ current_value = send attribute if respond_to? attribute
198
+
199
+ unless current_value == value
200
+ @changed_attributes[attribute_str] = current_value
201
+ end
202
+
203
+ send "#{attribute}=", value if respond_to? "#{attribute}="
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,5 @@
1
+ module Gophish
2
+ class Configuration
3
+ attr_accessor :url, :api_key, :verify_ssl, :debug_output
4
+ end
5
+ end
@@ -0,0 +1,72 @@
1
+ require_relative 'base'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'csv'
4
+
5
+ module Gophish
6
+ class Group < Base
7
+ attribute :id, :integer
8
+ attribute :name, :string
9
+ attribute :modified_date, :string
10
+ attribute :targets
11
+
12
+ validates :name, presence: true
13
+ validates :targets, presence: true
14
+ validate :validate_targets_structure
15
+
16
+ def body_for_create
17
+ { name:, targets: }
18
+ end
19
+
20
+ def import_csv(csv_data)
21
+ targets_array = CSV.parse(csv_data, headers: true).map { |row| parse_csv_row row }
22
+ self.targets = targets_array
23
+ end
24
+
25
+ private
26
+
27
+ def validate_targets_structure
28
+ return if targets.blank?
29
+ return errors.add :targets, 'must be an array' unless targets.is_a? Array
30
+
31
+ targets.each_with_index { |target, index| validate_target target, index }
32
+ end
33
+
34
+ def parse_csv_row(row)
35
+ {
36
+ first_name: row['First Name'],
37
+ last_name: row['Last Name'],
38
+ email: row['Email'],
39
+ position: row['Position']
40
+ }
41
+ end
42
+
43
+ def validate_target(target, index)
44
+ return errors.add :targets, "item at index #{index} must be a hash" unless target.is_a? Hash
45
+
46
+ validate_target_email target, index
47
+ validate_required_field target, index, :position
48
+ validate_required_field target, index, :first_name
49
+ validate_required_field target, index, :last_name
50
+ end
51
+
52
+ def validate_target_email(target, index)
53
+ email = target[:email] || target['email']
54
+ if email.blank?
55
+ errors.add :targets, "item at index #{index} must have an email"
56
+ elsif !valid_email_format?(email)
57
+ errors.add :targets, "item at index #{index} must have a valid email format"
58
+ end
59
+ end
60
+
61
+ def validate_required_field(target, index, field)
62
+ value = target[field] || target[field.to_s]
63
+ return unless value.blank?
64
+
65
+ errors.add :targets, "item at index #{index} must have a #{field}"
66
+ end
67
+
68
+ def valid_email_format?(email)
69
+ email.match?(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gophish
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'gophish/version'
2
+ require_relative 'gophish/configuration'
3
+ require_relative 'gophish/base'
4
+ require_relative 'gophish/group'
5
+
6
+ module Gophish
7
+ class << self
8
+ attr_writer :configuration
9
+
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def configure
15
+ yield configuration
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module Gophish
2
+ module Ruby
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end