trackguard 0.27.0 → 0.28.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/app/assets/javascripts/controllers/page_tracker_controller.js +5 -6
- data/app/assets/stylesheets/trackguard/admin.css +42 -0
- data/app/controllers/concerns/trackguard/page_tracker.rb +1 -1
- data/app/controllers/trackguard/admin/analytics_controller.rb +1 -0
- data/app/controllers/trackguard/admin/base_controller.rb +1 -1
- data/app/controllers/trackguard/admin/visitors_controller.rb +4 -1
- data/app/controllers/trackguard/page_views_controller.rb +1 -1
- data/app/helpers/trackguard/hub_helper.rb +11 -0
- data/app/jobs/trackguard/detect_suspicious_visitors_job.rb +64 -10
- data/app/jobs/trackguard/track_page_view_job.rb +4 -23
- data/app/models/trackguard/page_view.rb +3 -0
- data/app/models/trackguard/visitor.rb +12 -4
- data/app/services/trackguard/track_page_view.rb +35 -0
- data/app/views/trackguard/admin/_visit_row.html.erb +34 -7
- data/lib/generators/trackguard/install_generator.rb +4 -0
- data/lib/generators/trackguard/templates/add_suspicious_state_to_trackguard_visitors.rb +12 -0
- data/lib/generators/trackguard/templates/add_tracking_layer_to_trackguard_visits.rb +5 -0
- data/lib/generators/trackguard/templates/create_trackguard_visits.rb +1 -0
- data/lib/tasks/trackguard.rake +17 -53
- data/lib/trackguard/adapters/base.rb +5 -4
- data/lib/trackguard/adapters/hub.rb +85 -0
- data/lib/trackguard/adapters/local.rb +3 -3
- data/lib/trackguard/engine.rb +3 -4
- data/lib/trackguard/version.rb +1 -1
- data/lib/trackguard.rb +10 -4
- data/trackguard.gemspec +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2629b3c673c2ac2c1f4e338dedc3abc7f9b572fb57e9b4f9091139809e362bc
|
|
4
|
+
data.tar.gz: 6b4657343eecf9de2b48bea0df3f24c4463109dc6ae521dc83dad79741bbfe99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8b5fc84be6801c276f5d7cb7dd0007a7b1c7d5d19d5db9858f2412396e3c99fc604e9276cc34ee0a05b4b63cc01f72fac8e176b1c2e852405813e0e484b4733
|
|
7
|
+
data.tar.gz: 96c791bce0d1cd72230e89e34a6335e4c7dfdbe818f90dccbd466bb3e908c2b9437e820dfe6afb9ced8a605e5293f341c65dd541e603a8bd9594210009c4dfe8
|
|
@@ -11,8 +11,7 @@ export default class extends Controller {
|
|
|
11
11
|
window.addEventListener("hashchange", this.boundHash)
|
|
12
12
|
|
|
13
13
|
// Cover initial load if turbo:load already fired before connect()
|
|
14
|
-
|
|
15
|
-
this.trackCurrent(true)
|
|
14
|
+
this.trackCurrent()
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
disconnect() {
|
|
@@ -20,14 +19,14 @@ export default class extends Controller {
|
|
|
20
19
|
window.removeEventListener("hashchange", this.boundHash)
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
trackCurrent(
|
|
22
|
+
trackCurrent() {
|
|
24
23
|
const path = window.location.pathname + window.location.hash
|
|
25
24
|
if (path === this.lastTracked) return
|
|
26
25
|
this.lastTracked = path
|
|
27
|
-
this.track(path
|
|
26
|
+
this.track(path)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
track(path
|
|
29
|
+
track(path) {
|
|
31
30
|
const token = document.querySelector('meta[name="csrf-token"]')?.content
|
|
32
31
|
const traceId = document.querySelector('meta[name="trace-id"]')?.content
|
|
33
32
|
const url = this.urlValue || document.querySelector('meta[name="trackguard-url"]')?.content
|
|
@@ -35,7 +34,7 @@ export default class extends Controller {
|
|
|
35
34
|
fetch(url, {
|
|
36
35
|
method: "POST",
|
|
37
36
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": token || "" },
|
|
38
|
-
body: JSON.stringify({ path, trace_id: traceId, ref
|
|
37
|
+
body: JSON.stringify({ path, trace_id: traceId, ref })
|
|
39
38
|
})
|
|
40
39
|
}
|
|
41
40
|
}
|
|
@@ -291,6 +291,10 @@
|
|
|
291
291
|
background: rgba(239, 68, 68, 0.05);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
.tg-row--suspicious {
|
|
295
|
+
background: rgba(251, 191, 36, 0.1);
|
|
296
|
+
}
|
|
297
|
+
|
|
294
298
|
.tg-row--whitelisted {
|
|
295
299
|
background: rgba(34, 197, 94, 0.05);
|
|
296
300
|
}
|
|
@@ -352,6 +356,14 @@
|
|
|
352
356
|
font-size: 0.875rem;
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
.tg-summary__layer {
|
|
360
|
+
width: 2.5rem;
|
|
361
|
+
text-align: center;
|
|
362
|
+
flex-shrink: 0;
|
|
363
|
+
color: #d1d5db;
|
|
364
|
+
font-size: 0.875rem;
|
|
365
|
+
}
|
|
366
|
+
|
|
355
367
|
.tg-summary__time {
|
|
356
368
|
font-size: 0.8125rem;
|
|
357
369
|
color: #9ca3af;
|
|
@@ -366,6 +378,13 @@
|
|
|
366
378
|
vertical-align: middle;
|
|
367
379
|
}
|
|
368
380
|
|
|
381
|
+
.tg-suspicious-icon {
|
|
382
|
+
width: 1rem;
|
|
383
|
+
height: 1rem;
|
|
384
|
+
color: #f59e0b;
|
|
385
|
+
vertical-align: middle;
|
|
386
|
+
}
|
|
387
|
+
|
|
369
388
|
.tg-whitelist-icon {
|
|
370
389
|
width: 1rem;
|
|
371
390
|
height: 1rem;
|
|
@@ -373,6 +392,27 @@
|
|
|
373
392
|
vertical-align: middle;
|
|
374
393
|
}
|
|
375
394
|
|
|
395
|
+
.tg-layer-badge {
|
|
396
|
+
display: inline-block;
|
|
397
|
+
font-size: 0.625rem;
|
|
398
|
+
font-weight: 600;
|
|
399
|
+
letter-spacing: 0.04em;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
border-radius: 3px;
|
|
402
|
+
padding: 0.125rem 0.3rem;
|
|
403
|
+
line-height: 1.4;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.tg-layer-badge--js {
|
|
407
|
+
background: #d1fae5;
|
|
408
|
+
color: #065f46;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.tg-layer-badge--backend {
|
|
412
|
+
background: #ffedd5;
|
|
413
|
+
color: #c2410c;
|
|
414
|
+
}
|
|
415
|
+
|
|
376
416
|
/* ── Detail panel ──────────────────────────────────────────────────── */
|
|
377
417
|
.tg-detail {
|
|
378
418
|
padding: 0 1rem 1rem 1rem;
|
|
@@ -428,6 +468,8 @@
|
|
|
428
468
|
|
|
429
469
|
.tg-dl__def--flagged { color: #dc2626; }
|
|
430
470
|
|
|
471
|
+
.tg-dl__def--suspicious { color: #d97706; }
|
|
472
|
+
|
|
431
473
|
.tg-dl__def--muted { color: #9ca3af; }
|
|
432
474
|
|
|
433
475
|
.tg-dl__def--whitelisted { color: #059669; }
|
|
@@ -64,6 +64,7 @@ module Trackguard
|
|
|
64
64
|
{
|
|
65
65
|
path: view.path,
|
|
66
66
|
ip: view.visitor&.ip,
|
|
67
|
+
suspicious_state: view.visitor.suspicious_state,
|
|
67
68
|
flagged_at: view.visitor.flagged_at,
|
|
68
69
|
flagged_by: view.visitor.flagged_by,
|
|
69
70
|
whitelisted: view.visitor.whitelisted_ip&.active? || false,
|
|
@@ -16,9 +16,11 @@ module Trackguard
|
|
|
16
16
|
# rubocop:disable Metrics/AbcSize
|
|
17
17
|
def flag
|
|
18
18
|
if @visitor.update(
|
|
19
|
+
suspicious_state: "blocked",
|
|
19
20
|
flagged_at: Time.current,
|
|
20
21
|
flag_reason: params[:flag_reason].presence,
|
|
21
22
|
flagged_by: params[:flagged_by].presence || Visitor::FLAGGED_BY.first,
|
|
23
|
+
suspicious_since_at: nil,
|
|
22
24
|
name: params[:name].presence || BlockedUserAgent.matching_pattern(@visitor.user_agent)
|
|
23
25
|
)
|
|
24
26
|
respond_to do |format|
|
|
@@ -35,7 +37,8 @@ module Trackguard
|
|
|
35
37
|
# rubocop:enable Metrics/AbcSize
|
|
36
38
|
|
|
37
39
|
def unflag
|
|
38
|
-
@visitor.update!(
|
|
40
|
+
@visitor.update!(suspicious_state: "normal", suspicious_since_at: nil,
|
|
41
|
+
flagged_at: nil, flag_reason: nil, flagged_by: nil)
|
|
39
42
|
respond_to do |format|
|
|
40
43
|
format.html { redirect_back_or_to after_action_path }
|
|
41
44
|
format.json { render json: { status: "ok", ip: @visitor.ip } }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trackguard
|
|
4
|
+
module HubHelper
|
|
5
|
+
def trackguard_hub_js_tag
|
|
6
|
+
return unless Trackguard.adapter.is_a?(Trackguard::Adapters::Hub)
|
|
7
|
+
|
|
8
|
+
tag.script(src: "https://app.trackguard.dev/track.js", data: { api_key: Trackguard.hub_api_key })
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -24,7 +24,7 @@ module Trackguard
|
|
|
24
24
|
.joins(:visitor)
|
|
25
25
|
.merge(Visitor.unflagged)
|
|
26
26
|
.preload(visitor: :whitelisted_ip)
|
|
27
|
-
.select(:visitor_id, :session_id, :referer, :path, :trace_id)
|
|
27
|
+
.select(:visitor_id, :session_id, :referer, :path, :trace_id, :tracking_layer)
|
|
28
28
|
.group_by(&:visitor)
|
|
29
29
|
|
|
30
30
|
return if views_by_visitor.empty?
|
|
@@ -44,18 +44,29 @@ module Trackguard
|
|
|
44
44
|
|
|
45
45
|
name = name_from_ua(visitor.user_agent)
|
|
46
46
|
|
|
47
|
+
if visitor.suspicious?
|
|
48
|
+
new_views = PageView
|
|
49
|
+
.where(visitor: visitor)
|
|
50
|
+
.where("created_at > ?", visitor.suspicious_since_at)
|
|
51
|
+
.select(:tracking_layer, :trace_id)
|
|
52
|
+
result = evaluate_suspicious_escalation(visitor, new_views)
|
|
53
|
+
return if %i[blocked recovered].include?(result)
|
|
54
|
+
elsif views.any?(&:backend_layer?) && views.none?(&:js_layer?)
|
|
55
|
+
mark_suspicious!(visitor, name)
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
if count >= HARD_FLAG_THRESHOLD
|
|
48
|
-
|
|
59
|
+
mark_blocked!(visitor, "#{count} page views in 24h (hard flag threshold)", name: name)
|
|
49
60
|
return
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
if (reason = ua_flag_reason(visitor.user_agent))
|
|
53
|
-
|
|
64
|
+
mark_blocked!(visitor, reason, name: name)
|
|
54
65
|
return
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
if (path = probe_path_hit(views))
|
|
58
|
-
|
|
69
|
+
mark_blocked!(visitor, "probe path hit: #{path}", name: name)
|
|
59
70
|
return
|
|
60
71
|
end
|
|
61
72
|
|
|
@@ -64,7 +75,7 @@ module Trackguard
|
|
|
64
75
|
return if count < MIN_VIEWS
|
|
65
76
|
|
|
66
77
|
if views.all? { |pv| pv.session_id.nil? && pv.referer.nil? } && views.map(&:path).uniq.size == 1
|
|
67
|
-
|
|
78
|
+
mark_blocked!(visitor, "no session, no referrer, single path hit", name: name)
|
|
68
79
|
return
|
|
69
80
|
end
|
|
70
81
|
|
|
@@ -86,10 +97,34 @@ module Trackguard
|
|
|
86
97
|
|
|
87
98
|
return if score < FLAG_SCORE_THRESHOLD
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
mark_blocked!(visitor, reasons.join("; "), name: name)
|
|
90
101
|
end
|
|
91
102
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
92
103
|
|
|
104
|
+
def evaluate_suspicious_escalation(visitor, new_views)
|
|
105
|
+
return :unchanged if new_views.empty?
|
|
106
|
+
|
|
107
|
+
js_ids = new_views.select { |pv| pv.tracking_layer == "js" }.map(&:trace_id).compact
|
|
108
|
+
|
|
109
|
+
if any_unpaired_backend?(new_views, js_ids)
|
|
110
|
+
mark_blocked!(visitor, "continued backend-only visits since suspicious flag")
|
|
111
|
+
:blocked
|
|
112
|
+
elsif paired_backend_trace_ids(new_views, js_ids).any?
|
|
113
|
+
mark_normal!(visitor)
|
|
114
|
+
:recovered
|
|
115
|
+
else
|
|
116
|
+
:unchanged
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def any_unpaired_backend?(views, js_trace_ids)
|
|
121
|
+
views.any? { |pv| pv.backend_layer? && !js_trace_ids.include?(pv.trace_id) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def paired_backend_trace_ids(views, js_trace_ids)
|
|
125
|
+
views.select(&:backend_layer?).map(&:trace_id).compact & js_trace_ids
|
|
126
|
+
end
|
|
127
|
+
|
|
93
128
|
def flag_shared_trace_id_visitors(cutoff)
|
|
94
129
|
shared = PageView
|
|
95
130
|
.where(created_at: cutoff..)
|
|
@@ -108,8 +143,8 @@ module Trackguard
|
|
|
108
143
|
.where("wi.id IS NULL OR wi.expires_at <= ?", Time.current)
|
|
109
144
|
.distinct
|
|
110
145
|
.each do |visitor|
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
mark_blocked!(visitor, "trace_id shared across multiple visitors (cross-visitor bot detected)",
|
|
147
|
+
name: name_from_ua(visitor.user_agent))
|
|
113
148
|
end
|
|
114
149
|
end
|
|
115
150
|
|
|
@@ -125,8 +160,27 @@ module Trackguard
|
|
|
125
160
|
"malformed user-agent (duplicate)" if user_agent.scan("Mozilla/5.0").size > 1
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
def
|
|
129
|
-
visitor.
|
|
163
|
+
def mark_suspicious!(visitor, name)
|
|
164
|
+
return if visitor.suspicious? || visitor.blocked?
|
|
165
|
+
|
|
166
|
+
visitor.update!(suspicious_state: "suspicious", suspicious_since_at: Time.current, name: name)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def mark_normal!(visitor)
|
|
170
|
+
return if visitor.normal? || visitor.blocked?
|
|
171
|
+
|
|
172
|
+
visitor.update!(suspicious_state: "normal", suspicious_since_at: nil)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def mark_blocked!(visitor, reason, name: nil)
|
|
176
|
+
visitor.update!(
|
|
177
|
+
suspicious_state: "blocked",
|
|
178
|
+
flagged_at: Time.current,
|
|
179
|
+
flag_reason: reason,
|
|
180
|
+
flagged_by: "Recurring Job",
|
|
181
|
+
suspicious_since_at: nil,
|
|
182
|
+
name: name
|
|
183
|
+
)
|
|
130
184
|
end
|
|
131
185
|
|
|
132
186
|
def name_from_ua(user_agent)
|
|
@@ -2,29 +2,10 @@ module Trackguard
|
|
|
2
2
|
class TrackPageViewJob < ApplicationJob
|
|
3
3
|
queue_as :default
|
|
4
4
|
|
|
5
|
-
def perform(path:, ip:, user_agent:, referer:, session_id: nil, trace_id: nil, source: nil,
|
|
6
|
-
http_method: nil)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
visitor = Visitor.find_or_create_by!(ip: ip) do |v|
|
|
10
|
-
v.user_agent = user_agent
|
|
11
|
-
v.first_seen_at = Time.current
|
|
12
|
-
v.last_seen_at = Time.current
|
|
13
|
-
end
|
|
14
|
-
visitor.update!(last_seen_at: Time.current, user_agent: user_agent)
|
|
15
|
-
|
|
16
|
-
if initial && trace_id.present?
|
|
17
|
-
existing = PageView.find_by(trace_id: trace_id, visitor: visitor)
|
|
18
|
-
if existing
|
|
19
|
-
if path.include?("#") && !existing.path.include?("#") && path.start_with?("#{existing.path}#")
|
|
20
|
-
existing.update!(path: path)
|
|
21
|
-
end
|
|
22
|
-
return
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
PageView.create_with(source:, referer:, http_method:)
|
|
27
|
-
.find_or_create_by!(path:, user_agent:, session_id: hashed_session_id, trace_id:, visitor:)
|
|
5
|
+
def perform(path:, ip:, user_agent:, referer:, session_id: nil, trace_id: nil, source: nil,
|
|
6
|
+
tracking_layer: nil, http_method: nil)
|
|
7
|
+
TrackPageView.call(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, tracking_layer:,
|
|
8
|
+
http_method:)
|
|
28
9
|
end
|
|
29
10
|
end
|
|
30
11
|
end
|
|
@@ -6,5 +6,8 @@ module Trackguard
|
|
|
6
6
|
scope :this_month, -> { where(created_at: 1.month.ago..) }
|
|
7
7
|
scope :with_referrer, -> { where.not(referer: [ nil, "" ]) }
|
|
8
8
|
scope :with_source, -> { where.not(source: [ nil, "" ]) }
|
|
9
|
+
|
|
10
|
+
def js_layer? = tracking_layer == "js"
|
|
11
|
+
def backend_layer? = tracking_layer == "backend"
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -2,17 +2,25 @@ module Trackguard
|
|
|
2
2
|
class Visitor < ApplicationRecord
|
|
3
3
|
self.table_name = "trackguard_visitors"
|
|
4
4
|
|
|
5
|
-
FLAGGED_BY
|
|
6
|
-
|
|
5
|
+
FLAGGED_BY = [ "User", "claw:auto", "Recurring Job", "Internal Automation", "External Automation" ].freeze
|
|
6
|
+
SUSPICIOUS_STATES = %w[normal suspicious blocked].freeze
|
|
7
|
+
CACHE_KEY = "trackguard/flagged_ips".freeze
|
|
7
8
|
|
|
8
9
|
validates :flagged_by, inclusion: { in: FLAGGED_BY }, allow_blank: true
|
|
10
|
+
validates :suspicious_state, inclusion: { in: SUSPICIOUS_STATES }
|
|
9
11
|
|
|
10
12
|
has_many :page_views, class_name: "Trackguard::PageView", foreign_key: "visitor_id"
|
|
11
13
|
has_many :blocked_requests, class_name: "Trackguard::BlockedRequest", foreign_key: "visitor_id"
|
|
12
14
|
has_one :whitelisted_ip, class_name: "Trackguard::WhitelistedIp", foreign_key: "visitor_id"
|
|
13
15
|
|
|
14
|
-
scope :unflagged,
|
|
15
|
-
scope :flagged,
|
|
16
|
+
scope :unflagged, -> { where(flagged_at: nil) }
|
|
17
|
+
scope :flagged, -> { where.not(flagged_at: nil) }
|
|
18
|
+
scope :suspicious_visitors, -> { where(suspicious_state: "suspicious") }
|
|
19
|
+
scope :blocked, -> { where(suspicious_state: "blocked") }
|
|
20
|
+
|
|
21
|
+
def normal? = suspicious_state == "normal"
|
|
22
|
+
def suspicious? = suspicious_state == "suspicious"
|
|
23
|
+
def blocked? = suspicious_state == "blocked"
|
|
16
24
|
|
|
17
25
|
def self.flagged?(ip)
|
|
18
26
|
flagged_ips = Rails.cache.fetch(CACHE_KEY, expires_in: 5.minutes) do
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Trackguard
|
|
2
|
+
class TrackPageView < ApplicationService
|
|
3
|
+
def initialize(path:, ip:, user_agent:, referer:, session_id: nil, trace_id: nil,
|
|
4
|
+
source: nil, tracking_layer: nil, http_method: nil, visitor_scope: {})
|
|
5
|
+
@path = path
|
|
6
|
+
@ip = ip
|
|
7
|
+
@user_agent = user_agent
|
|
8
|
+
@referer = referer
|
|
9
|
+
@session_id = session_id
|
|
10
|
+
@trace_id = trace_id
|
|
11
|
+
@source = source
|
|
12
|
+
@tracking_layer = tracking_layer
|
|
13
|
+
@http_method = http_method
|
|
14
|
+
@visitor_scope = visitor_scope
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
hashed_session_id = Digest::SHA256.hexdigest(@session_id) if @session_id.present?
|
|
19
|
+
|
|
20
|
+
visitor = Visitor.find_or_create_by!(ip: @ip, **@visitor_scope) do |v|
|
|
21
|
+
v.user_agent = @user_agent
|
|
22
|
+
v.first_seen_at = Time.current
|
|
23
|
+
v.last_seen_at = Time.current
|
|
24
|
+
end
|
|
25
|
+
visitor.update!(last_seen_at: Time.current, user_agent: @user_agent)
|
|
26
|
+
|
|
27
|
+
PageView.create!(
|
|
28
|
+
path: @path, user_agent: @user_agent, session_id: hashed_session_id,
|
|
29
|
+
trace_id: @trace_id, source: @source, referer: @referer,
|
|
30
|
+
http_method: @http_method, tracking_layer: @tracking_layer,
|
|
31
|
+
visitor: visitor, **@visitor_scope
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<% visitor = pv.visitor %>
|
|
2
|
-
<% flagged = visitor&.
|
|
2
|
+
<% flagged = visitor&.blocked? %>
|
|
3
|
+
<% suspicious = visitor&.suspicious? %>
|
|
3
4
|
<% whitelisted = visitor&.whitelisted_ip&.active? %>
|
|
4
|
-
<% row_classes = [ ("tg-row--flagged" if flagged), ("tg-row--whitelisted" if whitelisted) ].compact.join(" ") %>
|
|
5
|
+
<% row_classes = [ ("tg-row--flagged" if flagged), ("tg-row--suspicious" if suspicious), ("tg-row--whitelisted" if whitelisted) ].compact.join(" ") %>
|
|
5
6
|
<tr class="<%= row_classes %>">
|
|
6
7
|
<td class="tg-td--bare">
|
|
7
8
|
<details>
|
|
@@ -11,9 +12,13 @@
|
|
|
11
12
|
<span class="tg-summary__path"><%= pv.path %></span>
|
|
12
13
|
<span class="tg-summary__flag">
|
|
13
14
|
<% if flagged %>
|
|
14
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-flag-icon" title="
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-flag-icon" title="Blocked">
|
|
15
16
|
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
|
16
17
|
</svg>
|
|
18
|
+
<% elsif suspicious %>
|
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="tg-suspicious-icon" title="Suspicious">
|
|
20
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
|
21
|
+
</svg>
|
|
17
22
|
<% else %>
|
|
18
23
|
—
|
|
19
24
|
<% end %>
|
|
@@ -27,6 +32,15 @@
|
|
|
27
32
|
—
|
|
28
33
|
<% end %>
|
|
29
34
|
</span>
|
|
35
|
+
<span class="tg-summary__layer">
|
|
36
|
+
<% if pv.tracking_layer == "js" %>
|
|
37
|
+
<span class="tg-layer-badge tg-layer-badge--js">js</span>
|
|
38
|
+
<% elsif pv.tracking_layer == "backend" %>
|
|
39
|
+
<span class="tg-layer-badge tg-layer-badge--backend">srv</span>
|
|
40
|
+
<% else %>
|
|
41
|
+
—
|
|
42
|
+
<% end %>
|
|
43
|
+
</span>
|
|
30
44
|
<span class="tg-summary__time"><%= pv.created_at.strftime("%b %-d, %H:%M") %></span>
|
|
31
45
|
</summary>
|
|
32
46
|
<div class="tg-detail">
|
|
@@ -46,7 +60,7 @@
|
|
|
46
60
|
class: "tg-btn tg-btn--whitelist",
|
|
47
61
|
data: { confirm: "Whitelist #{visitor.ip} for 7 days?" } %>
|
|
48
62
|
<% end %>
|
|
49
|
-
<% if flagged %>
|
|
63
|
+
<% if flagged || suspicious %>
|
|
50
64
|
<%= button_to "Unflag", _actions[:unflag][:url],
|
|
51
65
|
method: _actions[:unflag][:method],
|
|
52
66
|
params: _actions[:unflag][:params],
|
|
@@ -88,15 +102,19 @@
|
|
|
88
102
|
<span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
|
|
89
103
|
</div>
|
|
90
104
|
<div class="tg-dl__row">
|
|
91
|
-
<span class="tg-dl__term">
|
|
105
|
+
<span class="tg-dl__term">Status</span>
|
|
92
106
|
<% if flagged %>
|
|
93
107
|
<span class="tg-dl__def tg-dl__def--flagged">
|
|
94
|
-
|
|
108
|
+
Blocked <%= visitor.flagged_at.strftime("%b %-d %Y, %H:%M") %>
|
|
95
109
|
<% if visitor.flagged_by.present? %> by <strong><%= visitor.flagged_by %></strong><% end %>
|
|
96
110
|
<% if visitor.flag_reason.present? %> — <%= visitor.flag_reason %><% end %>
|
|
97
111
|
</span>
|
|
112
|
+
<% elsif suspicious %>
|
|
113
|
+
<span class="tg-dl__def tg-dl__def--suspicious">
|
|
114
|
+
Suspicious since <%= visitor.suspicious_since_at.strftime("%b %-d %Y, %H:%M") %>
|
|
115
|
+
</span>
|
|
98
116
|
<% else %>
|
|
99
|
-
<span class="tg-dl__def tg-dl__def--muted">
|
|
117
|
+
<span class="tg-dl__def tg-dl__def--muted">Normal</span>
|
|
100
118
|
<% end %>
|
|
101
119
|
</div>
|
|
102
120
|
<div class="tg-dl__row">
|
|
@@ -130,6 +148,15 @@
|
|
|
130
148
|
<span class="tg-dl__term">Source</span>
|
|
131
149
|
<span class="tg-dl__def"><%= pv.source.presence || "—" %></span>
|
|
132
150
|
</div>
|
|
151
|
+
<div class="tg-dl__row">
|
|
152
|
+
<span class="tg-dl__term">Layer</span>
|
|
153
|
+
<span class="tg-dl__def">
|
|
154
|
+
<% if pv.tracking_layer == "js" %>JS (client)
|
|
155
|
+
<% elsif pv.tracking_layer == "backend" %>Backend (server)
|
|
156
|
+
<% else %><span class="tg-dl__def--muted">—</span>
|
|
157
|
+
<% end %>
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
133
160
|
</div>
|
|
134
161
|
</div>
|
|
135
162
|
</div>
|
|
@@ -18,6 +18,10 @@ module Trackguard
|
|
|
18
18
|
migration_template "create_trackguard_blocked_user_agents.rb",
|
|
19
19
|
"db/migrate/create_trackguard_blocked_user_agents.rb"
|
|
20
20
|
migration_template "create_trackguard_blocked_paths.rb", "db/migrate/create_trackguard_blocked_paths.rb"
|
|
21
|
+
migration_template "add_tracking_layer_to_trackguard_visits.rb",
|
|
22
|
+
"db/migrate/add_tracking_layer_to_trackguard_visits.rb"
|
|
23
|
+
migration_template "add_suspicious_state_to_trackguard_visitors.rb",
|
|
24
|
+
"db/migrate/add_suspicious_state_to_trackguard_visitors.rb"
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def print_next_steps
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AddSuspiciousStateToTrackguardVisitors < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
add_column :trackguard_visitors, :suspicious_state, :string, null: false, default: "normal"
|
|
4
|
+
add_column :trackguard_visitors, :suspicious_since_at, :datetime
|
|
5
|
+
|
|
6
|
+
reversible do |dir|
|
|
7
|
+
dir.up do
|
|
8
|
+
execute "UPDATE trackguard_visitors SET suspicious_state = 'blocked' WHERE flagged_at IS NOT NULL"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -10,6 +10,7 @@ class CreateTrackguardVisits < ActiveRecord::Migration[<%= ActiveRecord::Migrati
|
|
|
10
10
|
t.string :source
|
|
11
11
|
t.string :block_reason
|
|
12
12
|
t.string :http_method
|
|
13
|
+
t.string :tracking_layer
|
|
13
14
|
t.references :visitor, null: false, foreign_key: { to_table: :trackguard_visitors }
|
|
14
15
|
t.datetime :created_at, null: false
|
|
15
16
|
end
|
data/lib/tasks/trackguard.rake
CHANGED
|
@@ -72,41 +72,26 @@ namespace :trackguard do
|
|
|
72
72
|
next
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
base_version
|
|
76
|
-
|
|
77
|
-
template_dir = Trackguard::Engine.root.join("lib", "generators", "trackguard", "templates")
|
|
78
|
-
conn = ActiveRecord::Base.connection
|
|
75
|
+
base_version = File.basename(monolithic).match(/\A(\d{14})/)[1].to_i
|
|
76
|
+
template_dir = Trackguard::Engine.root.join("lib", "generators", "trackguard", "templates")
|
|
79
77
|
|
|
80
78
|
splits = [
|
|
81
|
-
[ base_version, "create_trackguard_visitors"
|
|
82
|
-
[ base_version + 1, "create_trackguard_visits"
|
|
83
|
-
[ base_version + 2, "create_trackguard_whitelisted_ips"
|
|
84
|
-
[ base_version + 3, "create_trackguard_blocked_user_agents"
|
|
85
|
-
[ base_version + 4, "create_trackguard_blocked_paths"
|
|
79
|
+
[ base_version, "create_trackguard_visitors" ],
|
|
80
|
+
[ base_version + 1, "create_trackguard_visits" ],
|
|
81
|
+
[ base_version + 2, "create_trackguard_whitelisted_ips" ],
|
|
82
|
+
[ base_version + 3, "create_trackguard_blocked_user_agents" ],
|
|
83
|
+
[ base_version + 4, "create_trackguard_blocked_paths" ]
|
|
86
84
|
]
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
# Tables absent from it are left for `rails generate trackguard:install` + `db:migrate`.
|
|
90
|
-
to_inject, to_defer = splits.partition { |_, _, table| monolithic_content.include?("create_table :#{table}") }
|
|
91
|
-
|
|
92
|
-
new_versions = to_inject.map { |ts, _, _| ts }.reject { |ts| ts == base_version }
|
|
93
|
-
create_list = to_inject.map { |ts, name, _| " db/migrate/#{ts}_#{name}.rb" }.join("\n")
|
|
86
|
+
create_list = splits.map { |ts, name| " db/migrate/#{ts}_#{name}.rb" }.join("\n")
|
|
94
87
|
|
|
95
88
|
puts "This will make the following changes to your application:"
|
|
96
89
|
puts ""
|
|
97
90
|
puts " Remove: db/migrate/#{File.basename(monolithic)}"
|
|
98
|
-
puts " Create:"
|
|
91
|
+
puts " Create (table/index creation guarded with if_not_exists):"
|
|
99
92
|
puts create_list
|
|
100
93
|
puts ""
|
|
101
|
-
puts "
|
|
102
|
-
puts " Update db/schema.rb if its version points at #{base_version}"
|
|
103
|
-
if to_defer.any?
|
|
104
|
-
puts ""
|
|
105
|
-
puts " Not in monolithic — deferred to install generator + db:migrate:"
|
|
106
|
-
to_defer.each { |_, name, _| puts " #{name}" }
|
|
107
|
-
end
|
|
108
|
-
puts ""
|
|
109
|
-
puts " Tables themselves are NOT touched. Bookkeeping only."
|
|
94
|
+
puts " Run `rails db:migrate` afterwards — existing tables and indexes are skipped safely."
|
|
110
95
|
puts ""
|
|
111
96
|
|
|
112
97
|
$stdout.print "Proceed? [y/N] "
|
|
@@ -118,44 +103,23 @@ namespace :trackguard do
|
|
|
118
103
|
|
|
119
104
|
puts ""
|
|
120
105
|
|
|
121
|
-
|
|
122
|
-
quoted = conn.quote(v.to_s)
|
|
123
|
-
unless conn.select_value("SELECT 1 FROM schema_migrations WHERE version = #{quoted}")
|
|
124
|
-
conn.execute("INSERT INTO schema_migrations (version) VALUES (#{quoted})")
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
last_injected = base_version
|
|
129
|
-
|
|
130
|
-
to_inject.each do |ts, name, _|
|
|
106
|
+
splits.each do |ts, name|
|
|
131
107
|
path = migrate_dir.join("#{ts}_#{name}.rb")
|
|
132
108
|
if path.exist?
|
|
133
109
|
puts " skip #{path.basename}"
|
|
134
110
|
else
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
raw = ERB.new(File.read(template_dir.join("#{name}.rb"))).result(binding)
|
|
112
|
+
guarded = raw
|
|
113
|
+
.gsub(/create_table (\S+) do/, 'create_table \1, if_not_exists: true do')
|
|
114
|
+
.gsub(/^(\s+add_index .+)$/, '\1, if_not_exists: true')
|
|
115
|
+
path.write(guarded)
|
|
137
116
|
puts " create #{path.basename}"
|
|
138
117
|
end
|
|
139
|
-
sm_insert.call(ts)
|
|
140
|
-
last_injected = ts
|
|
141
118
|
end
|
|
142
119
|
|
|
143
120
|
FileUtils.rm(monolithic)
|
|
144
121
|
puts " remove #{File.basename(monolithic)}"
|
|
145
|
-
# base_version stays in schema_migrations — it now belongs to create_trackguard_visitors
|
|
146
|
-
|
|
147
|
-
schema_path = Rails.root.join("db", "schema.rb")
|
|
148
|
-
if schema_path.exist? && last_injected != base_version
|
|
149
|
-
schema_content = schema_path.read
|
|
150
|
-
if schema_content.include?("version: #{base_version}")
|
|
151
|
-
schema_path.write(schema_content.sub("version: #{base_version}", "version: #{last_injected}"))
|
|
152
|
-
puts " update db/schema.rb: version #{base_version} → #{last_injected}"
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
122
|
|
|
156
|
-
|
|
157
|
-
puts "\nRun `rails generate trackguard:install && rails db:migrate` to create the remaining tables."
|
|
158
|
-
end
|
|
159
|
-
puts "\nDone. Run `rails db:migrate:status` to verify."
|
|
123
|
+
puts "\nDone. Run `rails db:migrate` to apply any missing migrations."
|
|
160
124
|
end
|
|
161
125
|
end
|
|
@@ -8,7 +8,8 @@ module Trackguard
|
|
|
8
8
|
def whitelisted_ip?(ip) = raise NotImplementedError, "#{self.class}#whitelisted_ip?"
|
|
9
9
|
def flagged_visitor?(ip) = raise NotImplementedError, "#{self.class}#flagged_visitor?"
|
|
10
10
|
|
|
11
|
-
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
11
|
+
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, tracking_layer:,
|
|
12
|
+
http_method:)
|
|
12
13
|
return if blocked_user_agent?(user_agent)
|
|
13
14
|
return if blocked_path?(path)
|
|
14
15
|
return if path.blank? || path.start_with?(Trackguard.admin_path)
|
|
@@ -16,7 +17,7 @@ module Trackguard
|
|
|
16
17
|
perform_track_page_view(
|
|
17
18
|
path: path, ip: ip, user_agent: user_agent, referer: referer,
|
|
18
19
|
session_id: session_id, trace_id: trace_id, source: source,
|
|
19
|
-
|
|
20
|
+
tracking_layer: tracking_layer, http_method: http_method
|
|
20
21
|
)
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -26,8 +27,8 @@ module Trackguard
|
|
|
26
27
|
|
|
27
28
|
protected
|
|
28
29
|
|
|
29
|
-
def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
30
|
-
http_method:)
|
|
30
|
+
def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
31
|
+
tracking_layer:, http_method:)
|
|
31
32
|
raise NotImplementedError, "#{self.class}#perform_track_page_view"
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Trackguard
|
|
7
|
+
module Adapters
|
|
8
|
+
class Hub < Base
|
|
9
|
+
CACHE_KEY = "trackguard/hub_rules"
|
|
10
|
+
STALE_KEY = "trackguard/hub_rules/stale"
|
|
11
|
+
ETAG_KEY = "trackguard/hub_rules/etag"
|
|
12
|
+
|
|
13
|
+
def blocked_user_agent?(user_agent)
|
|
14
|
+
rules.fetch(:blocked_user_agents, []).any? do |p|
|
|
15
|
+
user_agent.to_s.downcase.include?(p.downcase)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def blocked_path?(path)
|
|
20
|
+
rules.fetch(:blocked_paths, []).any? do |p|
|
|
21
|
+
path.to_s.downcase.include?(p.downcase)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def whitelisted_ip?(ip)
|
|
26
|
+
rules.fetch(:whitelisted_ips, []).include?(ip)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def flagged_visitor?(ip)
|
|
30
|
+
rules.fetch(:flagged_ips, []).include?(ip)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
|
|
34
|
+
# placeholder: hub adapter does not persist blocked requests locally
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
40
|
+
tracking_layer:, http_method:)
|
|
41
|
+
# placeholder: hub tracking not yet implemented
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def rules
|
|
47
|
+
Rails.cache.fetch(CACHE_KEY, expires_in: Trackguard.hub_rules_ttl, race_condition_ttl: 10) do
|
|
48
|
+
fresh = fetch_rules_from_hub
|
|
49
|
+
Rails.cache.write(STALE_KEY, fresh, expires_in: 24.hours)
|
|
50
|
+
fresh
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
Rails.logger.warn("[Trackguard::Adapters::Hub] Failed to fetch rules: #{e.message}")
|
|
54
|
+
Rails.cache.read(STALE_KEY) || {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch_rules_from_hub
|
|
58
|
+
uri = URI("#{Trackguard.hub_url}/api/rules")
|
|
59
|
+
request = Net::HTTP::Get.new(uri)
|
|
60
|
+
request["Authorization"] = "Bearer #{Trackguard.hub_secret_key}"
|
|
61
|
+
request["Accept"] = "application/json"
|
|
62
|
+
|
|
63
|
+
etag = Rails.cache.read(ETAG_KEY)
|
|
64
|
+
request["If-None-Match"] = etag if etag
|
|
65
|
+
|
|
66
|
+
response = Net::HTTP.start(
|
|
67
|
+
uri.hostname, uri.port,
|
|
68
|
+
use_ssl: uri.scheme == "https",
|
|
69
|
+
open_timeout: 3,
|
|
70
|
+
read_timeout: 5
|
|
71
|
+
) do |http|
|
|
72
|
+
http.request(request)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return Rails.cache.read(STALE_KEY) || {} if response.is_a?(Net::HTTPNotModified)
|
|
76
|
+
|
|
77
|
+
raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
78
|
+
|
|
79
|
+
fresh = JSON.parse(response.body, symbolize_names: true)
|
|
80
|
+
Rails.cache.write(ETAG_KEY, response["ETag"], expires_in: 24.hours) if response["ETag"]
|
|
81
|
+
fresh
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -31,8 +31,8 @@ module Trackguard
|
|
|
31
31
|
|
|
32
32
|
protected
|
|
33
33
|
|
|
34
|
-
def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
35
|
-
http_method:)
|
|
34
|
+
def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
|
|
35
|
+
tracking_layer:, http_method:)
|
|
36
36
|
TrackPageViewJob.perform_later(
|
|
37
37
|
path: path,
|
|
38
38
|
ip: ip,
|
|
@@ -41,7 +41,7 @@ module Trackguard
|
|
|
41
41
|
session_id: session_id,
|
|
42
42
|
trace_id: trace_id,
|
|
43
43
|
source: source,
|
|
44
|
-
|
|
44
|
+
tracking_layer: tracking_layer,
|
|
45
45
|
http_method: http_method
|
|
46
46
|
)
|
|
47
47
|
end
|
data/lib/trackguard/engine.rb
CHANGED
|
@@ -4,10 +4,9 @@ module Trackguard
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace Trackguard
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
7
|
+
config.to_prepare do
|
|
8
|
+
ActionController::Base.helper Trackguard::ApplicationHelper
|
|
9
|
+
ActionController::Base.helper Trackguard::HubHelper
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
initializer "trackguard.assets" do |app|
|
data/lib/trackguard/version.rb
CHANGED
data/lib/trackguard.rb
CHANGED
|
@@ -5,11 +5,13 @@ require "trackguard/engine"
|
|
|
5
5
|
require "trackguard/rack_attack"
|
|
6
6
|
require "trackguard/adapters/base"
|
|
7
7
|
require "trackguard/adapters/local"
|
|
8
|
+
require "trackguard/adapters/hub"
|
|
8
9
|
|
|
9
10
|
module Trackguard
|
|
10
11
|
class << self
|
|
11
|
-
attr_writer :authenticate_admin_with, :admin_layout, :admin_path, :back_label, :
|
|
12
|
-
:throttle_period
|
|
12
|
+
attr_writer :authenticate_admin_with, :admin_layout, :admin_path, :back_label, :local_api_token, :throttle_limit,
|
|
13
|
+
:throttle_period, :hub_secret_key, :hub_api_key, :hub_rules_ttl
|
|
14
|
+
attr_accessor :hub_url
|
|
13
15
|
|
|
14
16
|
def authenticate_admin_with
|
|
15
17
|
@authenticate_admin_with ||= proc {}
|
|
@@ -27,8 +29,8 @@ module Trackguard
|
|
|
27
29
|
@back_label ||= "Back to app"
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
def
|
|
31
|
-
@
|
|
32
|
+
def local_api_token
|
|
33
|
+
@local_api_token.to_s
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def throttle_limit
|
|
@@ -39,6 +41,10 @@ module Trackguard
|
|
|
39
41
|
@throttle_period ||= 60
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def hub_secret_key = @hub_secret_key.to_s
|
|
45
|
+
def hub_api_key = @hub_api_key.to_s
|
|
46
|
+
def hub_rules_ttl = @hub_rules_ttl ||= 5.minutes
|
|
47
|
+
|
|
42
48
|
def adapter
|
|
43
49
|
@adapter ||= Trackguard::Adapters::Local.new
|
|
44
50
|
end
|
data/trackguard.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trackguard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Krzysztof Rygielski
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- app/controllers/trackguard/admin/whitelisted_ips_controller.rb
|
|
59
59
|
- app/controllers/trackguard/page_views_controller.rb
|
|
60
60
|
- app/helpers/trackguard/application_helper.rb
|
|
61
|
+
- app/helpers/trackguard/hub_helper.rb
|
|
61
62
|
- app/jobs/trackguard/detect_suspicious_visitors_job.rb
|
|
62
63
|
- app/jobs/trackguard/track_blocked_request_job.rb
|
|
63
64
|
- app/jobs/trackguard/track_page_view_job.rb
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- app/models/trackguard/whitelisted_ip.rb
|
|
71
72
|
- app/services/trackguard/analytics_query.rb
|
|
72
73
|
- app/services/trackguard/application_service.rb
|
|
74
|
+
- app/services/trackguard/track_page_view.rb
|
|
73
75
|
- app/views/layouts/trackguard/admin.html.erb
|
|
74
76
|
- app/views/trackguard/admin/_nav.html.erb
|
|
75
77
|
- app/views/trackguard/admin/_stats_panel.html.erb
|
|
@@ -82,6 +84,8 @@ files:
|
|
|
82
84
|
- config/importmap.rb
|
|
83
85
|
- config/routes.rb
|
|
84
86
|
- lib/generators/trackguard/install_generator.rb
|
|
87
|
+
- lib/generators/trackguard/templates/add_suspicious_state_to_trackguard_visitors.rb
|
|
88
|
+
- lib/generators/trackguard/templates/add_tracking_layer_to_trackguard_visits.rb
|
|
85
89
|
- lib/generators/trackguard/templates/create_trackguard_blocked_paths.rb
|
|
86
90
|
- lib/generators/trackguard/templates/create_trackguard_blocked_user_agents.rb
|
|
87
91
|
- lib/generators/trackguard/templates/create_trackguard_visitors.rb
|
|
@@ -90,6 +94,7 @@ files:
|
|
|
90
94
|
- lib/tasks/trackguard.rake
|
|
91
95
|
- lib/trackguard.rb
|
|
92
96
|
- lib/trackguard/adapters/base.rb
|
|
97
|
+
- lib/trackguard/adapters/hub.rb
|
|
93
98
|
- lib/trackguard/adapters/local.rb
|
|
94
99
|
- lib/trackguard/engine.rb
|
|
95
100
|
- lib/trackguard/rack_attack.rb
|
|
@@ -100,6 +105,7 @@ licenses:
|
|
|
100
105
|
- MIT
|
|
101
106
|
metadata:
|
|
102
107
|
rubygems_mfa_required: 'true'
|
|
108
|
+
changelog_uri: https://github.com/riggy/trackguard/blob/main/CHANGELOG.md
|
|
103
109
|
rdoc_options: []
|
|
104
110
|
require_paths:
|
|
105
111
|
- lib
|