Repository navigation
Expand file tree
/
Copy pathmodels.go
More file actions
219 lines (202 loc) · 7.33 KB
/
Copy pathmodels.go
File metadata and controls
219 lines (202 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package guardagent
import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"sync/atomic"
"time"
)
// Metric type values accepted by the ingestion API SecurityMetric model
// (guard-core-api telemetry_models.py). SendMetric validates against this
// set exactly as the Python agent's Literal type does.
const (
MetricRequestCount = "request_count"
MetricResponseTime = "response_time"
MetricErrorRate = "error_rate"
MetricBandwidthUsage = "bandwidth_usage"
MetricThreatLevel = "threat_level"
MetricBlockRate = "block_rate"
MetricCacheHitRate = "cache_hit_rate"
)
// Agent status values reported on POST /api/v1/status, matching the
// ingestion API contract ("healthy, degraded, failed").
const (
StatusHealthy = "healthy"
StatusDegraded = "degraded"
StatusFailed = "failed"
)
var knownMetricTypes = map[string]struct{}{
MetricRequestCount: {},
MetricResponseTime: {},
MetricErrorRate: {},
MetricBandwidthUsage: {},
MetricThreatLevel: {},
MetricBlockRate: {},
MetricCacheHitRate: {},
}
func validMetricType(t string) bool {
_, ok := knownMetricTypes[t]
return ok
}
// KnownEventTypes lists every event type the guard ecosystem emits,
// mirroring guard_agent.models.KNOWN_EVENT_TYPES (39 entries, including
// the user_agent_blocked and decorator_violation renames and the
// security_headers_applied and csp_violation tails). It is documentation,
// not validation: like the Python agent, SendEvent accepts any non-empty
// event type so new engine events cannot be dropped at the consumer.
var KnownEventTypes = []string{
"ip_banned",
"ip_unbanned",
"ip_blocked",
"ip_ban_failed",
"rate_limited",
"rate_limit_script_reloaded",
"suspicious_request",
"cloud_blocked",
"country_blocked",
"penetration_attempt",
"behavioral_violation",
"user_agent_blocked",
"custom_request_check",
"decorator_violation",
"decoding_error",
"detection_engine_callback_error",
"geo_lookup_failed",
"https_enforced",
"pattern_anomaly_slow_execution",
"pattern_anomaly_timeout",
"pattern_anomaly_statistical_anomaly",
"redis_connection",
"redis_error",
"dynamic_rule_applied",
"dynamic_rule_updated",
"path_excluded",
"route_unresolved",
"pattern_detected",
"pattern_added",
"pattern_removed",
"access_denied",
"authentication_failed",
"content_filtered",
"emergency_mode_activated",
"emergency_mode_block",
"dynamic_rule_violation",
"security_bypass",
"security_headers_applied",
"csp_violation",
}
// IsKnownEventType reports whether event_type is in KnownEventTypes.
func IsKnownEventType(eventType string) bool {
for _, known := range KnownEventTypes {
if known == eventType {
return true
}
}
return false
}
// SecurityEvent is one security telemetry event. The JSON shape matches
// guard_agent.models.SecurityEvent field for field (ip_address,
// action_taken, reason and metadata are always present, as in the
// reference; optional string fields are omitted where the reference sends
// null, which the API treats the same); the ingestion API
// requires timestamp and event_type and allows extra fields.
type SecurityEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
IPAddress string `json:"ip_address"`
Country string `json:"country,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
ActionTaken string `json:"action_taken"`
Reason string `json:"reason"`
Endpoint string `json:"endpoint,omitempty"`
Method string `json:"method,omitempty"`
StatusCode *int `json:"status_code"`
ResponseTime *float64 `json:"response_time"`
DecoratorType string `json:"decorator_type,omitempty"`
RuleType string `json:"rule_type,omitempty"`
PatternMatched string `json:"pattern_matched,omitempty"`
HandlerName string `json:"handler_name,omitempty"`
Metadata map[string]any `json:"metadata"`
}
// SecurityMetric is one numeric telemetry sample. MetricType must be one of
// the Metric* constants.
type SecurityMetric struct {
Timestamp time.Time `json:"timestamp"`
MetricType string `json:"metric_type"`
Value float64 `json:"value"`
Endpoint string `json:"endpoint,omitempty"`
Tags map[string]string `json:"tags"`
}
// eventBatch is the wire payload for POST /api/v1/events and
// POST /api/v1/metrics, matching BatchTelemetryRequest.
type eventBatch struct {
ProjectID string `json:"project_id"`
Events []SecurityEvent `json:"events"`
Metrics []SecurityMetric `json:"metrics"`
BatchID string `json:"batch_id"`
CreatedAt time.Time `json:"created_at"`
Compressed bool `json:"compressed"`
AgentVersion string `json:"agent_version"`
GuardVersion string `json:"guard_version,omitempty"`
GuardCoreVersion string `json:"guard_core_version,omitempty"`
}
// agentStatusPayload is the wire payload for POST /api/v1/status, matching
// AgentStatusRequest.
type agentStatusPayload struct {
Timestamp time.Time `json:"timestamp"`
Status string `json:"status"`
Uptime float64 `json:"uptime"`
EventsSent int64 `json:"events_sent"`
EventsFailed int64 `json:"events_failed"`
BufferSize int `json:"buffer_size"`
LastFlush *time.Time `json:"last_flush"`
Errors []string `json:"errors"`
}
// uuidFallbackCounter keeps generated identifiers unique if crypto/rand
// ever fails, so identifier generation can never panic or return "".
var uuidFallbackCounter atomic.Uint64
// cryptoRandRead is the entropy source for identifiers and encryption
// nonces. It is a variable so tests can exercise the fail-open fallback
// paths deterministically; production code always uses crypto/rand.Read.
var cryptoRandRead = rand.Read
// newUUID4 returns a random RFC 4122 version 4 UUID string.
func newUUID4() string {
var b [16]byte
if _, err := cryptoRandRead(b[:]); err != nil {
binary.BigEndian.PutUint64(b[0:8], uint64(time.Now().UnixNano()))
binary.BigEndian.PutUint64(b[8:16], uuidFallbackCounter.Add(1))
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// randomHex returns n random bytes as hex.
func randomHex(n int) string {
buf := make([]byte, n)
if _, err := cryptoRandRead(buf); err != nil {
return fmt.Sprintf("%0*x", n*2, uuidFallbackCounter.Add(1))
}
return hex.EncodeToString(buf)
}
// newBatchID returns "{unix_millis}-{8 hex chars}", the shape the Python
// agent uses. It is generated once per batch attempt group so retries and
// 413 splits of the same batch stay idempotent-friendly.
func newBatchID() string {
return fmt.Sprintf("%d-%s", time.Now().UnixMilli(), randomHex(4))
}
// normalizeItems gives nil metadata and tags the empty object the reference
// serializes ({}), so the wire shape never carries null where Python sends {}.
func normalizeItems(events []SecurityEvent, metrics []SecurityMetric) {
for i := range events {
if events[i].Metadata == nil {
events[i].Metadata = map[string]any{}
}
}
for i := range metrics {
if metrics[i].Tags == nil {
metrics[i].Tags = map[string]string{}
}
}
}