trackguard 0.16.1 → 0.19.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/images/trackguard/logo.png +0 -0
- data/app/assets/stylesheets/trackguard/admin.css +29 -29
- data/app/controllers/trackguard/admin/visitors_controller.rb +4 -1
- data/app/jobs/trackguard/detect_suspicious_visitors_job.rb +14 -7
- data/app/models/trackguard/blocked_user_agent.rb +7 -0
- data/app/services/trackguard/page_view_recorder.rb +3 -11
- data/app/views/layouts/trackguard/admin.html.erb +1 -21
- data/app/views/trackguard/admin/dashboards/show.html.erb +5 -0
- data/app/views/trackguard/admin/visits/index.html.erb +5 -0
- data/db/migrate/20260505191009_add_name_to_trackguard_visitors.rb +5 -0
- data/lib/generators/trackguard/templates/add_visitor_name.rb +7 -0
- data/lib/generators/trackguard/upgrade_generator.rb +4 -0
- data/lib/trackguard/adapters/base.rb +19 -0
- data/lib/trackguard/adapters/local.rb +43 -0
- data/lib/trackguard/rack_attack.rb +8 -6
- data/lib/trackguard/version.rb +1 -1
- data/lib/trackguard.rb +20 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90c710c8d2149047c75e5b99151b708e9741e29385d17de7c4f101ad9db697cf
|
|
4
|
+
data.tar.gz: 49aaf485a932c241d9c18986120f639305485f4c316653923e9ad9a018566e01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1a17b99009baaacee17c4473bf33b811870df411541cee1c055807f2b8981b4ab09ba0c28eeee15adc0a8ab87081f6b463b494435618cafafb2aea424b95226
|
|
7
|
+
data.tar.gz: 8da17d592514268ced0ab53cb0c2331671eea10702deaac46264f90874f2af33e60f952a1c89b7c1f920b6ad35ad93297bfe6ab786c796816c3704d35a17424d
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
|
|
8
8
|
font-size: 16px;
|
|
9
9
|
line-height: 1.5;
|
|
10
|
-
background-color: #
|
|
10
|
+
background-color: #0f0909;
|
|
11
11
|
color: #e0e0e0;
|
|
12
12
|
-webkit-font-smoothing: antialiased;
|
|
13
13
|
min-height: 100vh;
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
/* ── Header ────────────────────────────────────────────────────────── */
|
|
24
24
|
.tg-header {
|
|
25
|
-
background: #
|
|
26
|
-
border-bottom: 1px solid #
|
|
25
|
+
background: #190e0e;
|
|
26
|
+
border-bottom: 1px solid #2e1515;
|
|
27
27
|
padding: 0.875rem 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
font-size: 0.9375rem;
|
|
41
41
|
font-weight: 600;
|
|
42
42
|
letter-spacing: 0.03em;
|
|
43
|
-
color: #
|
|
43
|
+
color: #f87171;
|
|
44
44
|
text-decoration: none;
|
|
45
45
|
flex: 1;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
.tg-brand__logo {
|
|
49
49
|
width: 20px;
|
|
50
|
-
height:
|
|
50
|
+
height: auto;
|
|
51
51
|
flex-shrink: 0;
|
|
52
|
-
filter: drop-shadow(0 0 4px rgba(
|
|
52
|
+
filter: drop-shadow(0 0 4px rgba(239, 68, 68, 0.4));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/* ── Back link ─────────────────────────────────────────────────────── */
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
|
|
77
77
|
/* ── Nav ───────────────────────────────────────────────────────────── */
|
|
78
78
|
.tg-nav {
|
|
79
|
-
background: #
|
|
80
|
-
border-bottom: 1px solid #
|
|
79
|
+
background: #190e0e;
|
|
80
|
+
border-bottom: 1px solid #2e1515;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
.tg-nav > .tg-container {
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
.tg-nav__link:hover { color: #e0e0e0; }
|
|
98
98
|
|
|
99
99
|
.tg-nav__link--active {
|
|
100
|
-
color: #
|
|
101
|
-
border-bottom-color: #
|
|
100
|
+
color: #f87171;
|
|
101
|
+
border-bottom-color: #e53e3e;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/* ── Main ──────────────────────────────────────────────────────────── */
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.tg-stat {
|
|
131
|
-
background: #
|
|
132
|
-
border: 1px solid #
|
|
131
|
+
background: #190e0e;
|
|
132
|
+
border: 1px solid #2e1515;
|
|
133
133
|
border-radius: 8px;
|
|
134
134
|
padding: 1.25rem;
|
|
135
135
|
}
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
.tg-panel {
|
|
170
|
-
background: #
|
|
171
|
-
border: 1px solid #
|
|
170
|
+
background: #190e0e;
|
|
171
|
+
border: 1px solid #2e1515;
|
|
172
172
|
border-radius: 8px;
|
|
173
173
|
padding: 1rem;
|
|
174
174
|
margin-bottom: 1rem;
|
|
@@ -211,14 +211,14 @@
|
|
|
211
211
|
font-size: 0.8125rem;
|
|
212
212
|
color: #e0e0e0;
|
|
213
213
|
text-decoration: none;
|
|
214
|
-
background: #
|
|
214
|
+
background: #2e1515;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
.tg-pagination__link:hover { background: #
|
|
217
|
+
.tg-pagination__link:hover { background: #3d1c1c; }
|
|
218
218
|
|
|
219
219
|
.tg-pagination__link--active {
|
|
220
|
-
background: #
|
|
221
|
-
color: #
|
|
220
|
+
background: #e53e3e;
|
|
221
|
+
color: #ffffff;
|
|
222
222
|
font-weight: 600;
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -249,14 +249,14 @@
|
|
|
249
249
|
text-transform: uppercase;
|
|
250
250
|
letter-spacing: 0.08em;
|
|
251
251
|
color: #666666;
|
|
252
|
-
border-bottom: 1px solid #
|
|
252
|
+
border-bottom: 1px solid #2e1515;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
.tg-th--right { text-align: right; }
|
|
256
256
|
|
|
257
257
|
.tg-td {
|
|
258
258
|
padding: 0.75rem 0.875rem;
|
|
259
|
-
border-bottom: 1px solid #
|
|
259
|
+
border-bottom: 1px solid #2e1515;
|
|
260
260
|
color: #f0f0f0;
|
|
261
261
|
font-weight: 500;
|
|
262
262
|
}
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
|
|
272
272
|
.tg-td--bare {
|
|
273
273
|
padding: 0;
|
|
274
|
-
border-bottom: 1px solid #
|
|
274
|
+
border-bottom: 1px solid #2e1515;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/* ── Empty state ───────────────────────────────────────────────────── */
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
/* ── Detail panel ──────────────────────────────────────────────────── */
|
|
362
362
|
.tg-detail {
|
|
363
363
|
padding: 0 1rem 1rem 1rem;
|
|
364
|
-
background: #
|
|
364
|
+
background: #190e0e;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
.tg-detail__grid {
|
|
@@ -423,7 +423,7 @@
|
|
|
423
423
|
align-items: center;
|
|
424
424
|
gap: 0.5rem;
|
|
425
425
|
padding: 0.75rem 0 0;
|
|
426
|
-
border-top: 1px solid #
|
|
426
|
+
border-top: 1px solid #2e1515;
|
|
427
427
|
margin-top: 0.75rem;
|
|
428
428
|
}
|
|
429
429
|
|
|
@@ -436,8 +436,8 @@
|
|
|
436
436
|
|
|
437
437
|
.tg-input {
|
|
438
438
|
flex: 1;
|
|
439
|
-
background: #
|
|
440
|
-
border: 1px solid #
|
|
439
|
+
background: #0f0909;
|
|
440
|
+
border: 1px solid #2e1515;
|
|
441
441
|
border-radius: 4px;
|
|
442
442
|
color: #e0e0e0;
|
|
443
443
|
font-size: 0.8125rem;
|
|
@@ -445,7 +445,7 @@
|
|
|
445
445
|
outline: none;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
.tg-input:focus { border-color: #
|
|
448
|
+
.tg-input:focus { border-color: #e53e3e; }
|
|
449
449
|
.tg-input::placeholder { color: #444; }
|
|
450
450
|
|
|
451
451
|
.tg-btn {
|
|
@@ -461,8 +461,8 @@
|
|
|
461
461
|
.tg-btn--danger { background: #7f1d1d; color: #fca5a5; }
|
|
462
462
|
.tg-btn--danger:hover { background: #991b1b; }
|
|
463
463
|
|
|
464
|
-
.tg-btn--ghost { background: #
|
|
465
|
-
.tg-btn--ghost:hover { background: #
|
|
464
|
+
.tg-btn--ghost { background: #2e1515; color: #e0e0e0; }
|
|
465
|
+
.tg-btn--ghost:hover { background: #3d1c1c; }
|
|
466
466
|
|
|
467
467
|
.tg-btn--whitelist { background: #064e3b; color: #6ee7b7; }
|
|
468
468
|
.tg-btn--whitelist:hover { background: #065f46; }
|
|
@@ -476,4 +476,4 @@
|
|
|
476
476
|
.tg-stats { grid-template-columns: 1fr; }
|
|
477
477
|
.tg-detail__grid { grid-template-columns: 1fr; }
|
|
478
478
|
.tg-summary__ip { display: none; }
|
|
479
|
-
}
|
|
479
|
+
}
|
|
@@ -11,11 +11,13 @@ module Trackguard
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# rubocop:disable Metrics/AbcSize
|
|
14
15
|
def flag
|
|
15
16
|
if @visitor.update(
|
|
16
17
|
flagged_at: Time.current,
|
|
17
18
|
flag_reason: params[:flag_reason].presence,
|
|
18
|
-
flagged_by: params[:flagged_by].presence || Visitor::FLAGGED_BY.first
|
|
19
|
+
flagged_by: params[:flagged_by].presence || Visitor::FLAGGED_BY.first,
|
|
20
|
+
name: params[:name].presence || BlockedUserAgent.matching_pattern(@visitor.user_agent)
|
|
19
21
|
)
|
|
20
22
|
respond_to do |format|
|
|
21
23
|
format.html { redirect_back_or_to dashboard_path }
|
|
@@ -28,6 +30,7 @@ module Trackguard
|
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
|
33
|
+
# rubocop:enable Metrics/AbcSize
|
|
31
34
|
|
|
32
35
|
def unflag
|
|
33
36
|
@visitor.update!(flagged_at: nil, flag_reason: nil, flagged_by: nil)
|
|
@@ -43,13 +43,15 @@ module Trackguard
|
|
|
43
43
|
return if count.zero?
|
|
44
44
|
return if visitor.whitelisted_ip&.active?
|
|
45
45
|
|
|
46
|
+
name = name_from_ua(visitor.user_agent)
|
|
47
|
+
|
|
46
48
|
if count >= HARD_FLAG_THRESHOLD
|
|
47
|
-
flag!(visitor, "#{count} page views in 24h (hard flag threshold)")
|
|
49
|
+
flag!(visitor, "#{count} page views in 24h (hard flag threshold)", name: name)
|
|
48
50
|
return
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
if (reason = ua_flag_reason(visitor.user_agent))
|
|
52
|
-
flag!(visitor, reason)
|
|
54
|
+
flag!(visitor, reason, name: name)
|
|
53
55
|
return
|
|
54
56
|
end
|
|
55
57
|
|
|
@@ -58,7 +60,7 @@ module Trackguard
|
|
|
58
60
|
return if count < MIN_VIEWS
|
|
59
61
|
|
|
60
62
|
if views.all? { |pv| pv.session_id.nil? && pv.referer.nil? && pv.path == "/" }
|
|
61
|
-
flag!(visitor, "no session, no referrer, single root hit")
|
|
63
|
+
flag!(visitor, "no session, no referrer, single root hit", name: name)
|
|
62
64
|
return
|
|
63
65
|
end
|
|
64
66
|
|
|
@@ -85,7 +87,7 @@ module Trackguard
|
|
|
85
87
|
|
|
86
88
|
return if score < FLAG_SCORE_THRESHOLD
|
|
87
89
|
|
|
88
|
-
flag!(visitor, reasons.join("; "))
|
|
90
|
+
flag!(visitor, reasons.join("; "), name: name)
|
|
89
91
|
end
|
|
90
92
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
91
93
|
|
|
@@ -107,7 +109,8 @@ module Trackguard
|
|
|
107
109
|
.where("wi.id IS NULL OR wi.expires_at <= ?", Time.current)
|
|
108
110
|
.distinct
|
|
109
111
|
.each do |visitor|
|
|
110
|
-
flag!(visitor, "trace_id shared across multiple visitors (cross-visitor bot detected)"
|
|
112
|
+
flag!(visitor, "trace_id shared across multiple visitors (cross-visitor bot detected)",
|
|
113
|
+
name: name_from_ua(visitor.user_agent))
|
|
111
114
|
end
|
|
112
115
|
end
|
|
113
116
|
|
|
@@ -115,8 +118,12 @@ module Trackguard
|
|
|
115
118
|
"blank or minimal user-agent" if user_agent.blank? || user_agent.to_s.length < 10
|
|
116
119
|
end
|
|
117
120
|
|
|
118
|
-
def flag!(visitor, reason)
|
|
119
|
-
visitor.update!(flagged_at: Time.current, flag_reason: reason, flagged_by: "claw:auto")
|
|
121
|
+
def flag!(visitor, reason, name: nil)
|
|
122
|
+
visitor.update!(flagged_at: Time.current, flag_reason: reason, flagged_by: "claw:auto", name: name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def name_from_ua(user_agent)
|
|
126
|
+
BlockedUserAgent.matching_pattern(user_agent)
|
|
120
127
|
end
|
|
121
128
|
|
|
122
129
|
def blank_ratio(views, attr)
|
|
@@ -12,5 +12,12 @@ module Trackguard
|
|
|
12
12
|
end
|
|
13
13
|
patterns.any? { |p| user_agent.to_s.downcase.include?(p.downcase) }
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
def self.matching_pattern(user_agent)
|
|
17
|
+
patterns = Rails.cache.fetch(CACHE_KEY, expires_in: 10.minutes) do
|
|
18
|
+
pluck(:pattern)
|
|
19
|
+
end
|
|
20
|
+
patterns.find { |p| user_agent.to_s.downcase.include?(p.downcase) }
|
|
21
|
+
end
|
|
15
22
|
end
|
|
16
23
|
end
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
class PageViewRecorder < ApplicationService
|
|
3
|
-
BOT_REGEX = /
|
|
4
|
-
Googlebot|Bingbot|Slurp|DuckDuckBot|Baidu|YandexBot|
|
|
5
|
-
facebookexternalhit|Twitterbot|LinkedInBot|
|
|
6
|
-
curl|wget|python-requests|python-urllib|
|
|
7
|
-
Go-http-client|libwww|Java|Ruby|
|
|
8
|
-
bot|crawl|spider
|
|
9
|
-
/ix
|
|
10
|
-
|
|
11
3
|
def initialize(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source: nil, initial: false,
|
|
12
4
|
http_method: nil)
|
|
13
5
|
@path = path.to_s
|
|
@@ -22,11 +14,11 @@ module Trackguard
|
|
|
22
14
|
end
|
|
23
15
|
|
|
24
16
|
def call
|
|
25
|
-
|
|
26
|
-
return if
|
|
17
|
+
adapter = Trackguard.adapter
|
|
18
|
+
return if adapter.blocked_user_agent?(@user_agent)
|
|
27
19
|
return if @path.blank? || @path.start_with?("/admin")
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
adapter.track_page_view(
|
|
30
22
|
path: @path,
|
|
31
23
|
ip: @ip,
|
|
32
24
|
user_agent: @user_agent,
|
|
@@ -11,27 +11,7 @@
|
|
|
11
11
|
<div class="tg-container">
|
|
12
12
|
<div class="tg-header__inner">
|
|
13
13
|
<span class="tg-brand">
|
|
14
|
-
|
|
15
|
-
<defs>
|
|
16
|
-
<linearGradient id="tg-shield-grad" x1="0" y1="0" x2="0" y2="1">
|
|
17
|
-
<stop offset="0%" stop-color="#1e3a5f"/>
|
|
18
|
-
<stop offset="100%" stop-color="#0d1f3c"/>
|
|
19
|
-
</linearGradient>
|
|
20
|
-
</defs>
|
|
21
|
-
<!-- Shield -->
|
|
22
|
-
<path d="M10 1L19 4.5V11.5C19 16.4 15 20.3 10 21.5C5 20.3 1 16.4 1 11.5V4.5Z"
|
|
23
|
-
fill="url(#tg-shield-grad)" stroke="#3b82f6" stroke-width="1.25" stroke-linejoin="round"/>
|
|
24
|
-
<!-- Left rail -->
|
|
25
|
-
<line x1="7.5" y1="6" x2="7.5" y2="17" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
|
26
|
-
<!-- Right rail -->
|
|
27
|
-
<line x1="12.5" y1="6" x2="12.5" y2="17" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
|
28
|
-
<!-- Cross-tie 1 -->
|
|
29
|
-
<line x1="6.25" y1="8" x2="13.75" y2="8" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
30
|
-
<!-- Cross-tie 2 -->
|
|
31
|
-
<line x1="6.25" y1="11.5" x2="13.75" y2="11.5" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
32
|
-
<!-- Cross-tie 3 -->
|
|
33
|
-
<line x1="6.25" y1="15" x2="13.75" y2="15" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
34
|
-
</svg>
|
|
14
|
+
<%= image_tag "trackguard/logo.png", class: "tg-brand__logo", alt: "Trackguard" %>
|
|
35
15
|
Trackguard
|
|
36
16
|
</span>
|
|
37
17
|
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
<%= hidden_field_tag :id, visitor.id %>
|
|
152
152
|
<%= f.submit "Flag", class: "tg-btn tg-btn--danger" %>
|
|
153
153
|
<%= f.text_field :flag_reason, placeholder: "Flag reason (optional)", class: "tg-input", autocomplete: "off" %>
|
|
154
|
+
<%= f.text_field :name, placeholder: "Name (optional, auto-detected if blank)", class: "tg-input", autocomplete: "off" %>
|
|
154
155
|
<% end %>
|
|
155
156
|
<% end %>
|
|
156
157
|
</div>
|
|
@@ -175,6 +176,10 @@
|
|
|
175
176
|
<span class="tg-dl__term">User agent</span>
|
|
176
177
|
<span class="tg-dl__def tg-dl__def--break"><%= visitor&.user_agent.presence || "—" %></span>
|
|
177
178
|
</div>
|
|
179
|
+
<div class="tg-dl__row">
|
|
180
|
+
<span class="tg-dl__term">Name</span>
|
|
181
|
+
<span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
|
|
182
|
+
</div>
|
|
178
183
|
<div class="tg-dl__row">
|
|
179
184
|
<span class="tg-dl__term">Flag status</span>
|
|
180
185
|
<% if flagged %>
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
<%= hidden_field_tag :id, visitor.id %>
|
|
64
64
|
<%= f.submit "Flag", class: "tg-btn tg-btn--danger" %>
|
|
65
65
|
<%= f.text_field :flag_reason, placeholder: "Flag reason (optional)", class: "tg-input", autocomplete: "off" %>
|
|
66
|
+
<%= f.text_field :name, placeholder: "Name (optional, auto-detected if blank)", class: "tg-input", autocomplete: "off" %>
|
|
66
67
|
<% end %>
|
|
67
68
|
<% end %>
|
|
68
69
|
</div>
|
|
@@ -87,6 +88,10 @@
|
|
|
87
88
|
<span class="tg-dl__term">User agent</span>
|
|
88
89
|
<span class="tg-dl__def tg-dl__def--break"><%= visitor&.user_agent.presence || "—" %></span>
|
|
89
90
|
</div>
|
|
91
|
+
<div class="tg-dl__row">
|
|
92
|
+
<span class="tg-dl__term">Name</span>
|
|
93
|
+
<span class="tg-dl__def"><%= visitor&.name.presence || "—" %></span>
|
|
94
|
+
</div>
|
|
90
95
|
<div class="tg-dl__row">
|
|
91
96
|
<span class="tg-dl__term">Flag status</span>
|
|
92
97
|
<% if flagged %>
|
|
@@ -15,6 +15,10 @@ module Trackguard
|
|
|
15
15
|
migration_template "add_trackguard_visits.rb", "db/migrate/add_trackguard_visits.rb"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def create_visitor_name_migration_file
|
|
19
|
+
migration_template "add_visitor_name.rb", "db/migrate/add_visitor_name.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
def print_next_steps
|
|
19
23
|
say "\nNext steps:", :green
|
|
20
24
|
say " 1. rails db:migrate"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trackguard
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
def blocked_user_agent?(user_agent) = raise NotImplementedError, "#{self.class}#blocked_user_agent?"
|
|
7
|
+
def whitelisted_ip?(ip) = raise NotImplementedError, "#{self.class}#whitelisted_ip?"
|
|
8
|
+
def flagged_visitor?(ip) = raise NotImplementedError, "#{self.class}#flagged_visitor?"
|
|
9
|
+
|
|
10
|
+
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, initial:, http_method:)
|
|
11
|
+
raise NotImplementedError, "#{self.class}#track_page_view"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
|
|
15
|
+
raise NotImplementedError, "#{self.class}#track_blocked_request"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trackguard
|
|
4
|
+
module Adapters
|
|
5
|
+
class Local < Base
|
|
6
|
+
def blocked_user_agent?(user_agent)
|
|
7
|
+
BlockedUserAgent.blocked?(user_agent)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def whitelisted_ip?(ip)
|
|
11
|
+
WhitelistedIp.whitelisted?(ip)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def flagged_visitor?(ip)
|
|
15
|
+
Visitor.flagged?(ip)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, initial:, http_method:)
|
|
19
|
+
TrackPageViewJob.perform_later(
|
|
20
|
+
path: path,
|
|
21
|
+
ip: ip,
|
|
22
|
+
user_agent: user_agent,
|
|
23
|
+
referer: referer,
|
|
24
|
+
session_id: session_id,
|
|
25
|
+
trace_id: trace_id,
|
|
26
|
+
source: source,
|
|
27
|
+
initial: initial,
|
|
28
|
+
http_method: http_method
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
|
|
33
|
+
TrackBlockedRequestJob.perform_later(
|
|
34
|
+
ip: ip,
|
|
35
|
+
user_agent: user_agent,
|
|
36
|
+
path: path,
|
|
37
|
+
http_method: http_method,
|
|
38
|
+
block_reason: block_reason
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -5,20 +5,22 @@ require "rack/attack"
|
|
|
5
5
|
module Trackguard
|
|
6
6
|
module RackAttack
|
|
7
7
|
def self.configure
|
|
8
|
+
adapter = Trackguard.adapter
|
|
9
|
+
|
|
8
10
|
::Rack::Attack.safelist("trackguard/allow local") do |req|
|
|
9
11
|
[ "127.0.0.1", "::1" ].include?(req.ip)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
::Rack::Attack.safelist("trackguard/allow whitelisted ips") do |req|
|
|
13
|
-
|
|
15
|
+
adapter.whitelisted_ip?(req.ip)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
::Rack::Attack.blocklist("trackguard/block known scanners") do |req|
|
|
17
|
-
|
|
19
|
+
adapter.blocked_user_agent?(req.user_agent)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
::Rack::Attack.blocklist("trackguard/flagged visitors") do |req|
|
|
21
|
-
|
|
23
|
+
adapter.flagged_visitor?(req.ip)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
::Rack::Attack.throttle(
|
|
@@ -27,15 +29,15 @@ module Trackguard
|
|
|
27
29
|
period: Trackguard.throttle_period, &:ip
|
|
28
30
|
)
|
|
29
31
|
|
|
30
|
-
subscribe_to_blocked_requests
|
|
32
|
+
subscribe_to_blocked_requests(adapter)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def self.subscribe_to_blocked_requests
|
|
35
|
+
def self.subscribe_to_blocked_requests(adapter)
|
|
34
36
|
@subscribe_to_blocked_requests ||= ActiveSupport::Notifications.subscribe("rack.attack") do |*, payload|
|
|
35
37
|
req = payload[:request]
|
|
36
38
|
next unless req.env["rack.attack.match_type"] == :blocklist
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
adapter.track_blocked_request(
|
|
39
41
|
ip: req.ip,
|
|
40
42
|
user_agent: req.user_agent.to_s,
|
|
41
43
|
path: req.path,
|
data/lib/trackguard/version.rb
CHANGED
data/lib/trackguard.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "trackguard/version"
|
|
4
4
|
require "trackguard/engine"
|
|
5
5
|
require "trackguard/rack_attack"
|
|
6
|
+
require "trackguard/adapters/base"
|
|
7
|
+
require "trackguard/adapters/local"
|
|
6
8
|
|
|
7
9
|
module Trackguard
|
|
8
10
|
class << self
|
|
@@ -36,5 +38,23 @@ module Trackguard
|
|
|
36
38
|
def throttle_period
|
|
37
39
|
@throttle_period ||= 60
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def adapter
|
|
43
|
+
@adapter ||= Trackguard::Adapters::Local.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def adapter=(value)
|
|
47
|
+
@adapter = resolve_adapter(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_adapter(value)
|
|
53
|
+
case value
|
|
54
|
+
when Symbol then Trackguard::Adapters.const_get(value.to_s.camelize).new
|
|
55
|
+
when Class then value.new
|
|
56
|
+
else value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
39
59
|
end
|
|
40
60
|
end
|
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.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Krzysztof Rygielski
|
|
@@ -43,6 +43,7 @@ executables: []
|
|
|
43
43
|
extensions: []
|
|
44
44
|
extra_rdoc_files: []
|
|
45
45
|
files:
|
|
46
|
+
- app/assets/images/trackguard/logo.png
|
|
46
47
|
- app/assets/javascripts/controllers/page_tracker_controller.js
|
|
47
48
|
- app/assets/stylesheets/trackguard/admin.css
|
|
48
49
|
- app/controllers/concerns/trackguard/page_tracker.rb
|
|
@@ -72,12 +73,16 @@ files:
|
|
|
72
73
|
- app/views/trackguard/admin/visits/index.html.erb
|
|
73
74
|
- config/importmap.rb
|
|
74
75
|
- config/routes.rb
|
|
76
|
+
- db/migrate/20260505191009_add_name_to_trackguard_visitors.rb
|
|
75
77
|
- lib/generators/trackguard/install_generator.rb
|
|
76
78
|
- lib/generators/trackguard/templates/add_trackguard_visits.rb
|
|
79
|
+
- lib/generators/trackguard/templates/add_visitor_name.rb
|
|
77
80
|
- lib/generators/trackguard/templates/create_trackguard_tables.rb
|
|
78
81
|
- lib/generators/trackguard/upgrade_generator.rb
|
|
79
82
|
- lib/tasks/trackguard.rake
|
|
80
83
|
- lib/trackguard.rb
|
|
84
|
+
- lib/trackguard/adapters/base.rb
|
|
85
|
+
- lib/trackguard/adapters/local.rb
|
|
81
86
|
- lib/trackguard/engine.rb
|
|
82
87
|
- lib/trackguard/rack_attack.rb
|
|
83
88
|
- lib/trackguard/version.rb
|