password_breach_alert 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +141 -0
- data/Rakefile +17 -0
- data/app/views/devise/mailer/password_breach_alert.html.erb +27 -0
- data/app/views/devise/mailer/password_breach_alert.text.erb +24 -0
- data/lib/common_password/devise.rb +10 -0
- data/lib/common_password/list.rb +15 -0
- data/lib/common_password/model.rb +48 -0
- data/lib/common_password/passwords.txt +9999 -0
- data/lib/devise/password_breach_alert.rb +32 -0
- data/lib/devise/password_breach_alert/locales/en.yml +34 -0
- data/lib/devise/password_breach_alert/locales/it.yml +34 -0
- data/lib/devise/password_breach_alert/model.rb +25 -0
- data/lib/generators/password_breach_alert_generator.rb +13 -0
- data/lib/generators/templates/create_breaches.rb +19 -0
- data/lib/password_breach_alert.rb +9 -0
- data/lib/password_breach_alert/api/base.rb +39 -0
- data/lib/password_breach_alert/api/breach.rb +15 -0
- data/lib/password_breach_alert/api/breachedaccount.rb +18 -0
- data/lib/password_breach_alert/breaches_filters.rb +8 -0
- data/lib/password_breach_alert/breaches_filters/after_user_last_checked_at.rb +34 -0
- data/lib/password_breach_alert/breaches_filters/all_with_user.rb +21 -0
- data/lib/password_breach_alert/breaches_filters/new_with_user.rb +21 -0
- data/lib/password_breach_alert/breaches_policies.rb +6 -0
- data/lib/password_breach_alert/breaches_policies/send_devise_notification.rb +12 -0
- data/lib/password_breach_alert/checker.rb +45 -0
- data/lib/password_breach_alert/mailer.rb +9 -0
- data/lib/password_breach_alert/models/breach.rb +55 -0
- data/lib/password_breach_alert/rails.rb +10 -0
- data/lib/password_breach_alert/railtie.rb +4 -0
- data/lib/password_breach_alert/version.rb +3 -0
- data/lib/pwned_password/devise.rb +13 -0
- data/lib/pwned_password/hooks.rb +6 -0
- data/lib/pwned_password/model.rb +66 -0
- data/lib/tasks/password_breach_alert.rake +15 -0
- data/lib/zxcvbn_password/devise.rb +20 -0
- data/lib/zxcvbn_password/email_tokeniser.rb +7 -0
- data/lib/zxcvbn_password/errors.rb +2 -0
- data/lib/zxcvbn_password/model.rb +84 -0
- data/lib/zxcvbn_password/tester.rb +36 -0
- metadata +261 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'devise'
|
4
|
+
require 'zxcvbn_password/devise'
|
5
|
+
require 'common_password/devise'
|
6
|
+
require 'pwned_password/devise'
|
7
|
+
require 'devise/password_breach_alert/model'
|
8
|
+
require 'password_breach_alert/breaches_policies'
|
9
|
+
require 'password_breach_alert/breaches_filters'
|
10
|
+
|
11
|
+
module Devise
|
12
|
+
extend ZxcvbnPassword::Devise
|
13
|
+
extend CommonPassword::Devise
|
14
|
+
extend PwnedPassword::Devise
|
15
|
+
mattr_accessor :pwned_active, :common_active, :zxcvbn_active
|
16
|
+
@@pwned_active = true
|
17
|
+
@@common_active = true
|
18
|
+
@@zxcvbn_active = true
|
19
|
+
|
20
|
+
mattr_accessor :breaches_policy, :breaches_filter, :breaches_filter_options
|
21
|
+
@@breaches_policy = 'PasswordBreachAlert::BreachesPolicies::SendDeviseNotification'
|
22
|
+
@@breaches_filter = 'PasswordBreachAlert::BreachesFilters::NewWithUser'
|
23
|
+
@@breaches_filter_options = -> { {} }
|
24
|
+
|
25
|
+
module PasswordBreachAlert
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Load default I18n
|
30
|
+
#
|
31
|
+
I18n.load_path.unshift File.join(File.dirname(__FILE__), 'password_breach_alert', 'locales', 'en.yml')
|
32
|
+
I18n.load_path.unshift File.join(File.dirname(__FILE__), 'password_breach_alert', 'locales', 'it.yml')
|
@@ -0,0 +1,34 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
attributes:
|
4
|
+
password_breach_alert/models/breach:
|
5
|
+
name: Name
|
6
|
+
title: Title
|
7
|
+
domain: Domain
|
8
|
+
breach_date: "Date of the breach"
|
9
|
+
added_date: "Date of the publication of the breach"
|
10
|
+
pwn_count: "Password count"
|
11
|
+
description: "Description"
|
12
|
+
logo_path: "Logo"
|
13
|
+
data_classes: "Classes of data breached"
|
14
|
+
devise:
|
15
|
+
mailer:
|
16
|
+
password_breach_alert:
|
17
|
+
subject: "Password breach alert"
|
18
|
+
hello: "Hello %{email},"
|
19
|
+
there_was_some_breach:
|
20
|
+
one: "We did some routine checks, and found out that an external service had a leak of sensitive data. It is possibile that amongs the violated credentials also those of your account are present. This is the info we recovered:"
|
21
|
+
other: "We did some routine checks, and found out that some external services had a leak of sensitive data. It is possibile that amongs the violated credentials also those of your account are present. This is the info we recovered:"
|
22
|
+
this_site_is_secure: "%{domain} is secure and we proactively work to avoid that other sites breaches could compromise users registered on %{domain}."
|
23
|
+
please_change_password: "For this reason, we suggest you to change your password, just as a precaution. It is possible that this is not necessary, but we'd rather bother you five minutes than risk that your account on %{domain} gets violated too."
|
24
|
+
errors:
|
25
|
+
messages:
|
26
|
+
common:
|
27
|
+
top100: 'The password you choose is extremely common: it appears in the list of the 100 most used password. To choose a secure password, you could compose togheter different words, possibly not too common. For example, "pepperoni" is easy too, but "pepperonipeskystaple" is easy to remember and hard to guess'
|
28
|
+
top1000: 'The password you choose is very common: it appears in the list of the 1000 most used password. To choose a more secure password, you could add a word or two. For example, "pepperoni" is too easy, but "pepperonipeskystaple" is easy to remember and hard to guess'
|
29
|
+
top10000: 'The password you choose is quite common. To choose a more secure password, you could add a word or two. For example, "pepperoni" is too easy, but "pepperonipeskystaple" is easy to remember and hard to guess'
|
30
|
+
pwned: "The password you choose has previously appeared in a data breach: this means that is insecure and should never be used. Please choose something harder to guess. For example, you can combine togheter different uncommon words."
|
31
|
+
zxcvbn: 'The password you choose is too easy to guess. In a scale from 0 to 4 it scores %{score}: an algorithm could guess it in %{crack_time_display}. To choose a more secure password, you could add a word or two. For example, "pepperoni" is too easy, but "pepperonipeskystaple" is easy to remember and hard to guess.'
|
32
|
+
time:
|
33
|
+
formats:
|
34
|
+
date_only: '%-d %b %Y'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
it:
|
2
|
+
activerecord:
|
3
|
+
attributes:
|
4
|
+
password_breach_alert/models/breach:
|
5
|
+
name: Nome
|
6
|
+
title: Titolo
|
7
|
+
domain: Domino
|
8
|
+
breach_date: "Data dell'intrusione"
|
9
|
+
added_date: "Data dell'aggiunta"
|
10
|
+
pwn_count: "Numero di password"
|
11
|
+
description: "Descrizione"
|
12
|
+
logo_path: "Logo"
|
13
|
+
data_classes: "Tipo di dati trapelati"
|
14
|
+
devise:
|
15
|
+
mailer:
|
16
|
+
password_breach_alert:
|
17
|
+
subject: "Password breach alert"
|
18
|
+
hello: "Caro %{email},"
|
19
|
+
there_was_some_breach:
|
20
|
+
one: "Abbiamo fatto alcuni controlli di routine, e scoperto che un servizio esterno ha avuto una perdita di dati sensibili. Da un primo controllo, è possibile che tra le credenziali violate siano presenti anche quelle del tuo account. Ecco l'informazione che abbiamo recuperato:"
|
21
|
+
other: "Abbiamo fatto alcuni controlli di routine, e scoperto che alcuni servizi esterni hanno avuto una perdita di dati sensibili. Da un primo controllo, è possibile che tra le credenziali violate siano presenti anche quelle del tuo account. Ecco l'informazione che abbiamo recuperato:"
|
22
|
+
this_site_is_secure: "%{domain} è sicuro e ci attiviamo proattivamente per evitare che le brecce di altri siti possano compromettere gli account registrati su %{domain}."
|
23
|
+
please_change_password: "Per questo motivo ti suggeriamo di cambiare la tua password, per precauzione. E' possibile che non sia necessario, ma preferiamo disturbarti cinque minuti piuttosto che rischiare che anche il tuo account su %{domain} venga violato."
|
24
|
+
errors:
|
25
|
+
messages:
|
26
|
+
common:
|
27
|
+
top100: 'La password che hai scelto è estremamente comune: compare nella lista delle 100 password più usate al mondo. Per scegliere una buona password potresti comporre insieme diverse parole, se possibili non troppo comuni. Ad esempio, anche "ananas" è una password troppo semplice, mentre "pizzamozzarellaananas" è facile da ricordare e difficile da indovinare'
|
28
|
+
top1000: 'La password che hai scelto è molto comune: compare nella lista delle 1000 password più usate al mondo. Per renderla più sicura, potresti aggiungere una o due parole. Ad esempio, "ananas" è una password troppo semplice, mentre "pizzamozzarellaananas" è facile da ricordare e difficile da indovinare'
|
29
|
+
top10000: 'La password che hai scelto è abbastanza comune. Per renderla più sicura, potresti aggiungere una o due parole. Ad esempio, "ananas" è una password troppo semplice, mentre "pizzamozzarellaananas" è facile da ricordare e difficile da indovinare'
|
30
|
+
pwned: "La password che hai scelto è presente in un database di password violate: questo significa che è insicura e non dovrebbe essere usata. Per favore, scegli una password più difficile da indovinare. Ad esempio, potresti combinare insieme alcune parole poco comuni."
|
31
|
+
zxcvbn: 'La password che hai scelto è troppo facile. In una scala da 0 a 4 la sua difficoltà è %{score}: un algoritmo la potrebbe indovinare in %{crack_time_display}. Per renderla più sicura, potresti aggiungere una o due parole. Ad esempio, "ananas" è una password troppo semplice, mentre "pizzamozzarellaananas" è facile da ricordare e difficile da indovinare.'
|
32
|
+
time:
|
33
|
+
formats:
|
34
|
+
date_only: '%-d %b %Y'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zxcvbn_password/model'
|
4
|
+
require 'common_password/model'
|
5
|
+
require 'pwned_password/model'
|
6
|
+
|
7
|
+
module Devise
|
8
|
+
module Models
|
9
|
+
module PasswordBreachAlert
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include ZxcvbnPassword::Model
|
12
|
+
include CommonPassword::Model
|
13
|
+
include PwnedPassword::Model
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
::Devise::Models.config(self, :pwned_active)
|
17
|
+
::Devise::Models.config(self, :common_active)
|
18
|
+
::Devise::Models.config(self, :zxcvbn_active)
|
19
|
+
::Devise::Models.config(self, :breaches_policy)
|
20
|
+
::Devise::Models.config(self, :breaches_filter)
|
21
|
+
::Devise::Models.config(self, :breaches_filter_options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class PasswordBreachAlertGenerator < Rails::Generators::Base
|
2
|
+
include Rails::Generators::Migration
|
3
|
+
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
|
6
|
+
def create_breaches_migration
|
7
|
+
time_now = Time.current.strftime('%Y%m%d%H%M%S')
|
8
|
+
template_name = 'create_breaches'
|
9
|
+
migration_file = "#{time_now}_#{template_name}"
|
10
|
+
|
11
|
+
copy_file "#{template_name}.rb", File.join('db', 'migrate', "#{migration_file}.rb")
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateBreaches < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :breaches do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :title
|
6
|
+
t.string :domain
|
7
|
+
t.datetime :breach_date
|
8
|
+
t.datetime :added_date
|
9
|
+
t.integer :pwn_count
|
10
|
+
t.string :description
|
11
|
+
t.string :logo_path
|
12
|
+
t.text :data_classes
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index :breaches, :added_date
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module PasswordBreachAlert
|
2
|
+
autoload :Mailer, 'password_breach_alert/mailer'
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'password_breach_alert/railtie'
|
6
|
+
require 'password_breach_alert/rails'
|
7
|
+
require 'password_breach_alert/checker'
|
8
|
+
require 'password_breach_alert/models/breach'
|
9
|
+
require 'devise/password_breach_alert'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module PasswordBreachAlert
|
2
|
+
module Api
|
3
|
+
# TODO: add some lock
|
4
|
+
|
5
|
+
module LastCalledAt
|
6
|
+
mattr_accessor :last_called_at
|
7
|
+
end
|
8
|
+
|
9
|
+
SLEEP_DURATION = 1.6.seconds
|
10
|
+
|
11
|
+
class Base
|
12
|
+
include LastCalledAt
|
13
|
+
|
14
|
+
DEFAULT_REQUEST_OPTIONS = {
|
15
|
+
'User-Agent' => 'PasswordBreachAlert'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(request_options = {})
|
19
|
+
@request_options = DEFAULT_REQUEST_OPTIONS.merge(request_options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_wait
|
23
|
+
return enum_for :with_wait unless block_given?
|
24
|
+
|
25
|
+
# TODO: with_advisory_lock('PasswordBreachAlert::Api') do
|
26
|
+
if last_called_at && last_called_at > SLEEP_DURATION.ago
|
27
|
+
sleep(last_called_at - SLEEP_DURATION.ago)
|
28
|
+
end
|
29
|
+
|
30
|
+
result = yield
|
31
|
+
|
32
|
+
self.last_called_at = Time.current
|
33
|
+
# TODO: end
|
34
|
+
|
35
|
+
result
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'password_breach_alert/api/base'
|
2
|
+
|
3
|
+
module PasswordBreachAlert
|
4
|
+
module Api
|
5
|
+
class Breach < PasswordBreachAlert::Api::Base
|
6
|
+
API_URL = 'https://haveibeenpwned.com/api/v2/breaches'.freeze
|
7
|
+
|
8
|
+
def call
|
9
|
+
with_wait do
|
10
|
+
JSON.parse(URI.parse(API_URL).open(@request_options).read)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'password_breach_alert/api/base'
|
2
|
+
|
3
|
+
module PasswordBreachAlert
|
4
|
+
module Api
|
5
|
+
class Breachedaccount < PasswordBreachAlert::Api::Base
|
6
|
+
API_URL = 'https://haveibeenpwned.com/api/v2/breachedaccount/'.freeze
|
7
|
+
|
8
|
+
def call(account, params = nil)
|
9
|
+
query = (params || {}).reverse_merge('truncateResponse': true).to_query
|
10
|
+
endpoint = "#{API_URL}#{account}?#{query}"
|
11
|
+
|
12
|
+
with_wait do
|
13
|
+
JSON.parse(URI.parse(endpoint).open(@request_options).read)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'password_breach_alert/breaches_filters/after_user_last_checked_at'
|
2
|
+
require 'password_breach_alert/breaches_filters/all_with_user'
|
3
|
+
require 'password_breach_alert/breaches_filters/new_with_user'
|
4
|
+
|
5
|
+
module PasswordBreachAlert
|
6
|
+
module BreachesFilters
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'password_breach_alert/api/breachedaccount'
|
2
|
+
|
3
|
+
module PasswordBreachAlert
|
4
|
+
module BreachesFilters
|
5
|
+
# returns the breaches with breach_date later then checked, then updates the date on user
|
6
|
+
class AfterUserLastCheckedAt
|
7
|
+
attr_reader :api, :field
|
8
|
+
|
9
|
+
def initialize(api: PasswordBreachAlert::Api::Breachedaccount.new, field: :password_breach_alert_checked_at)
|
10
|
+
@api = api
|
11
|
+
@field = field
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(user, _new_breaches, breaches)
|
15
|
+
unless user.respond_to?(field)
|
16
|
+
raise NotImplementedError.new("please add the column of method #{field} to #{user.class}")
|
17
|
+
end
|
18
|
+
|
19
|
+
unless user[field].nil? || user[field].is_a?(Time)
|
20
|
+
raise NotImplementedError.new("#{user.class}.#{field} should be a time")
|
21
|
+
end
|
22
|
+
|
23
|
+
api_breaches = api.call(user.email)
|
24
|
+
api_breaches_names = api_breaches.map { |api_breach| api_breach['Name'] }
|
25
|
+
|
26
|
+
scope = { name: api_breaches_names }
|
27
|
+
scope[:breach_date] = user[field]..Time.current if user[field]
|
28
|
+
|
29
|
+
user.update field => Time.current
|
30
|
+
breaches.where(scope)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'password_breach_alert/api/breachedaccount'
|
2
|
+
|
3
|
+
module PasswordBreachAlert
|
4
|
+
module BreachesFilters
|
5
|
+
# returns the breaches where the user appears
|
6
|
+
class AllWithUser
|
7
|
+
attr_reader :api
|
8
|
+
|
9
|
+
def initialize(api: PasswordBreachAlert::Api::Breachedaccount.new)
|
10
|
+
@api = api
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(user, _new_breaches, breaches)
|
14
|
+
api_breaches = api.call(user.email)
|
15
|
+
api_breaches_names = api_breaches.map { |api_breach| api_breach['Name'] }
|
16
|
+
|
17
|
+
breaches.where(name: api_breaches_names)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'password_breach_alert/api/breachedaccount'
|
2
|
+
|
3
|
+
module PasswordBreachAlert
|
4
|
+
module BreachesFilters
|
5
|
+
# returns the new_breaches where the user appears
|
6
|
+
class NewWithUser
|
7
|
+
attr_reader :api
|
8
|
+
|
9
|
+
def initialize(api: PasswordBreachAlert::Api::Breachedaccount.new)
|
10
|
+
@api = api
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(user, new_breaches, _breaches)
|
14
|
+
api_breaches = api.call(user.email)
|
15
|
+
api_breaches_names = api_breaches.map { |api_breach| api_breach['Name'] }
|
16
|
+
|
17
|
+
new_breaches.where(name: api_breaches_names)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module PasswordBreachAlert
|
2
|
+
module BreachesPolicies
|
3
|
+
# notify the user with an email if there are any breaches
|
4
|
+
class SendDeviseNotification
|
5
|
+
def call(user, breaches)
|
6
|
+
return if breaches.none?
|
7
|
+
|
8
|
+
user.send(:send_devise_notification, :password_breach_alert, breaches)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'password_breach_alert/api/breach'
|
2
|
+
require 'password_breach_alert/api/breachedaccount'
|
3
|
+
|
4
|
+
module PasswordBreachAlert
|
5
|
+
class Checker
|
6
|
+
attr_reader :breaches_filter, :breaches_policy
|
7
|
+
|
8
|
+
def self.default_breaches_filter
|
9
|
+
options = Devise.breaches_filter_options.call.reverse_merge(api: PasswordBreachAlert::Api::Breachedaccount.new)
|
10
|
+
Devise.breaches_filter.constantize.new(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.default_breaches_policy
|
14
|
+
Devise.breaches_policy.constantize.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(breaches_filter: self.class.default_breaches_filter, breaches_policy: self.class.default_breaches_policy)
|
18
|
+
@breaches_filter = breaches_filter
|
19
|
+
@breaches_policy = breaches_policy
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(users)
|
23
|
+
new_breaches = fetch_and_create_new_breaches
|
24
|
+
breaches = all_breaches
|
25
|
+
|
26
|
+
users.each do |user|
|
27
|
+
user_breaches = breaches_filter.call(user, new_breaches, breaches)
|
28
|
+
|
29
|
+
breaches_policy.call(user, user_breaches)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def fetch_and_create_new_breaches
|
36
|
+
api_breaches = PasswordBreachAlert::Api::Breach.new.call
|
37
|
+
|
38
|
+
PasswordBreachAlert::Models::Breach.create_new_from_api(api_breaches)
|
39
|
+
end
|
40
|
+
|
41
|
+
def all_breaches
|
42
|
+
PasswordBreachAlert::Models::Breach.all
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: breaches
|
4
|
+
#
|
5
|
+
# id :integer not null, primary key
|
6
|
+
# name :string
|
7
|
+
# title :string
|
8
|
+
# domain :string
|
9
|
+
# breach_date :datetime
|
10
|
+
# added_date :datetime
|
11
|
+
# pwn_count :integer
|
12
|
+
# description :string
|
13
|
+
# logo_path :string
|
14
|
+
# data_classes :text
|
15
|
+
# created_at :datetime not null
|
16
|
+
# updated_at :datetime not null
|
17
|
+
#
|
18
|
+
|
19
|
+
module PasswordBreachAlert
|
20
|
+
# rails generate password_breach_alert
|
21
|
+
module Models
|
22
|
+
class Breach < ActiveRecord::Base # rubocop:disable ApplicationRecord
|
23
|
+
serialize :data_classes, Array
|
24
|
+
|
25
|
+
validates :name, presence: true
|
26
|
+
|
27
|
+
scope :sorted, -> { order('added_date DESC') }
|
28
|
+
|
29
|
+
def self.create_new_from_api(api_breaches)
|
30
|
+
created_ids = []
|
31
|
+
present_names = Breach.pluck(:name)
|
32
|
+
|
33
|
+
api_breaches.each do |api_breach|
|
34
|
+
next if api_breach['Name'].in?(present_names)
|
35
|
+
|
36
|
+
breach = Breach.create(
|
37
|
+
name: api_breach['Name'],
|
38
|
+
title: api_breach['Title'],
|
39
|
+
domain: api_breach['Domain'],
|
40
|
+
breach_date: api_breach['BreachDate'],
|
41
|
+
added_date: api_breach['AddedDate'],
|
42
|
+
pwn_count: api_breach['PwnCount'],
|
43
|
+
description: api_breach['Description'],
|
44
|
+
logo_path: api_breach['LogoPath'],
|
45
|
+
data_classes: api_breach['DataClasses']
|
46
|
+
)
|
47
|
+
|
48
|
+
created_ids << breach.id
|
49
|
+
end
|
50
|
+
|
51
|
+
Breach.where(id: created_ids).sorted
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|