-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcas-pipeline.conf
More file actions
291 lines (261 loc) · 10.4 KB
/
Copy pathcas-pipeline.conf
File metadata and controls
291 lines (261 loc) · 10.4 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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# This pipeline is designed to process CAS audit log files.
# It uses a combination of grok, kv, dissect, and ruby filters to parse
# the log data into a structured format for further analysis.
input {
file {
# Use a generic path for the log file. Update this to your specific log file location.
path => "/var/log/cas/*_audit.log"
start_position => "beginning"
# The sincedb_path is set to /dev/null for testing. For production,
# consider setting a relative path, e.g., "./.sincedb_cas-audit".
sincedb_path => "/var/log/logstash/sincedb/sincedb_cas-audit"
codec => multiline {
pattern => "Audit trail record BEGIN"
negate => true
what => "previous"
}
}
}
filter {
# The mutate filter is used to modify fields in the event.
mutate {
# We use gsub to remove unwanted characters and header lines.
# The first gsub removes ANSI escape codes, which are sometimes present in the logs.
# The second gsub removes the leading header line for each audit trail record.
gsub => [
"message", "(\e\[m)?\e\[(\d+;)*\d+m", "",
"message", "^\s*20\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} INFO.*Audit trail record BEGIN\n=============================================================\n", ""
]
}
# The grok filter is the main workhorse for parsing unstructured log data.
# This pattern is designed to extract key fields from the audit trail body.
grok {
match => {
"message" => "WHEN: %{TIMESTAMP_ISO8601:timestamp}\nWHO: %{DATA:subject}\nWHAT: %{GREEDYDATA:what}\nACTION: %{WORD:action}\nCLIENT_IP: %{DATA:ip_address}\nSERVER_IP: %{DATA:server_ip}"
}
}
# The date filter parses the `timestamp` field and sets it as the `@timestamp` field,
# which is the standard Logstash field for event time.
date {
match => [ "timestamp", "ISO8601" ]
target => "@timestamp"
}
# The dissect filter extracts fields from the file path.
# This uses the preferred 'idp_protocol' and 'service_protocol' names.
dissect {
mapping => {
"[log][file][path]" => "/var/log/cas/%{idp_protocol}_service_%{service_protocol}_idp_audit.log"
}
}
mutate {
# Add a new field `connection_protocol` for easier searching and visualization.
add_field => {
"connection_protocol" => "%{idp_protocol}_to_%{service_protocol}"
}
}
# This block determines the outcome based on the ACTION field.
# "SUCCESS" and "CREATED" actions are mapped to "success".
# "FAILED", "DENIED", and "NOT_FOUND" actions are mapped to "failure".
# All other actions are considered "unknown".
if [action] =~ "SUCCESS|CREATED" {
mutate {
add_field => { "outcome" => "success" }
}
} else if [action] =~ "FAILED|DENIED|NOT_FOUND" {
mutate {
add_field => { "outcome" => "failure" }
}
} else {
mutate {
add_field => { "outcome" => "unknown" }
}
}
# This conditional logic ensures that specific parsing rules are only applied
# to AUTHENTICATION_SUCCESS events, making the pipeline more efficient.
if [action] == "AUTHENTICATION_SUCCESS" {
# Check if the Identity Provider protocol is SAML
if [idp_protocol] == "saml" {
# Extract the SAML IdP entityID from the "issuerId" URI
grok {
match => { "what" => "issuerId=%{URI:origin_system}," }
}
}
# Check if the Identity Provider protocol is OIDC
else if [idp_protocol] == "oidc" {
# Extract the OP issuer from the "iss" URI.
grok {
match => { "what" => "iss=%{URI:origin_system}," }
}
}
# Remove the trailing comma from the extracted IdP SAML/OIDC issuer URI
mutate {
gsub => [ "origin_system", ",$", "" ]
}
# Extract the service URI and use it for both the object and destination_system
grok {
# Extract the service URI being at the end of the line
# by matching both the trailing comma and the final curly bracket.
match => { "what" => "service=%{URI:destination_system}(?:,|})" }
add_field => { "object" => "%{destination_system}" }
}
}
# Handle SERVICE_ACCESS_ENFORCEMENT_TRIGGERED events.
if [action] == "SERVICE_ACCESS_ENFORCEMENT_TRIGGERED" {
# Strip the outer { and } from the 'what' field
ruby {
code => '
raw = event.get("what")
if raw.is_a?(String) && raw.start_with?("{") && raw.end_with?("}")
event.set("what_stripped", raw[1..-2])
end
'
}
# Parse key=value pairs into [what_fields]
kv {
source => "what_stripped"
target => "what_fields"
field_split => ", "
value_split => "="
trim_key => " "
trim_value => " "
include_keys => ["service", "requiredAttributes"]
}
# Set object and destination_system from service String
if [what_fields][service] {
mutate {
add_field => {
"object" => "%{[what_fields][service]}"
"destination_system" => "%{[what_fields][service]}"
}
}
}
# Set context from requiredAttributes JSON
if [what_fields][requiredAttributes] {
json {
source => "[what_fields][requiredAttributes]"
target => "[context][required_attributes]"
tag_on_failure => ["_jsonparsefailure_requiredAttributes"]
}
}
# Override outcome to success if result=Service Access Granted
if [what_stripped] =~ /result=Service Access Granted/ {
mutate {
replace => { "outcome" => "success" }
}
}
# Cleanup temporary fields
mutate {
remove_field => ["what_stripped", "what_fields"]
}
}
# Make a best effort guess for destination system / object value
if [action] in [ "DELEGATED_CLIENT_SUCCESS", "SERVICE_TICKET_CREATED", "SERVICE_TICKET_VALIDATE_SUCCESS", "OAUTH2_USER_PROFILE_CREATED", "OAUTH2_ACCESS_TOKEN_REQUEST_CREATED", "SAML2_RESPONSE_CREATED" ] {
# Extract the service URI and use it for both the object and destination_system
grok {
# Extract the service URI being at the end of the line
# by matching both the trailing comma and the final curly bracket.
match => { "what" => "service=%{URI:destination_system}(?:,|})" }
add_field => { "object" => "%{destination_system}" }
}
}
# The geoip filter enriches the event with geographical information based on the IP address.
if [ip_address] {
geoip {
source => "ip_address"
target => "geoip"
# Use a generic path for the GeoLite2 database. Logstash usually looks in its own 'data' directory.
# Replace this with the actual path to your database file if the generic path fails.
database => "geoip/Geoacumen-Country.mmdb"
}
}
# The ruby filter is used to create a structured 'accounting' field.
ruby {
# The following is a definition of the fields to be collected:
#
# timestamp: The event time with timezone information.
# source: The system/component/service generating this piece of accounting data (hardcoded as "cas").
# subject: The subject (e.g. end-user) triggering the event.
# object: The resource being accessed, e.g., a dataset, file, computer, or service.
# operation: The operation being attempted (mapped to the CAS action)
# outcome: The outcome associated with the action, i.e., one of failure, success, unknown.
# origin_system: The system where the event originated, e.g. the home organisation in case of authentication.
# destination_system: The system the user is going to, e.g., services the user is trying to access.
# connection_protocol: What protocol was used for the connection, encoded as <IDP_PROTOCOL>to<SERVICE_PROTOCL), e.g. saml-to-saml
# ip_address: The network address of the device or system from which the event was initiated.
#
# The following fields are also collected but may not always be present:
# context: The context of the event, e.g. requiredAttributes in the case of authZ events
# geoip_country: The country name derived from the IP address.
# geoip_country_code: The two-letter country code.
#
# The following accounting properties are not currently being extracted or supported by this pipeline,
# but are included here for documentation and future implementation:
#
# reason: A reference to a policy rule or other information covering why the action was allowed or denied.
# correlation_id: Identifier used to correlate information between the Accounting Services and other components of the AAAI system as well as end services
#
code => '
acct = {
"timestamp" => event.get("timestamp"),
"subject" => event.get("subject"),
"operation" => event.get("action"),
"outcome" => event.get("outcome"),
"ip_address" => event.get("ip_address"),
"connection_protocol" => event.get("connection_protocol"),
"source" => "cas"
}
# Add origin_system to the accounting object only if it exists
if event.get("origin_system")
acct["origin_system"] = event.get("origin_system")
end
# Add object to the accounting object only if it exists
if event.get("object")
acct["object"] = event.get("object")
end
# Add destination_system to the accounting object only if it exists
if event.get("destination_system")
acct["destination_system"] = event.get("destination_system")
end
# Add context to the accounting object only if it exists and is not empty
if event.get("context")
acct["context"] = event.get("context")
end
if event.get("[geoip][geo][country_iso_code]")
acct["geoip_country_code"] = event.get("[geoip][geo][country_iso_code]")
end
event.set("accounting", acct)
'
}
# Keep only the accounting object
ruby {
code => '
if event.get("accounting")
accounting_data = event.get("accounting")
event.to_hash.keys.each { |k| event.remove(k) unless k.start_with?("@") }
accounting_data.each { |k,v| event.set(k, v) }
end
'
}
}
output {
# Uncomment for local stdout output for debugging
stdout {
codec => rubydebug
}
# Send only the accounting object to Elasticsearch
#if [accounting] {
# ruby {
# code => '
# acct = event.get("accounting")
# event.overwrite(acct) if acct
# '
# }
# elasticsearch {
# hosts => ["http://localhost:9200"]
# index => "cas-audit-%{+YYYY.MM.dd}"
# # You can also add authentication if your Elasticsearch requires it
# # user => "elastic"
# # password => "changeme"
# }
#}
}