11package com .uid2 .shared .audit ;
22
3+ import com .fasterxml .jackson .databind .JsonNode ;
4+ import com .fasterxml .jackson .databind .ObjectMapper ;
35import io .vertx .core .MultiMap ;
46import io .vertx .core .buffer .Buffer ;
57import io .vertx .core .json .Json ;
68import io .vertx .core .json .JsonArray ;
79import io .vertx .core .json .JsonObject ;
810import io .vertx .ext .web .RequestBody ;
911import io .vertx .ext .web .RoutingContext ;
12+ import lombok .Getter ;
1013import org .slf4j .Logger ;
1114import org .slf4j .LoggerFactory ;
1215import io .vertx .core .http .HttpServerRequest ;
1316import io .vertx .core .http .HttpServerResponse ;
1417import java .time .Instant ;
1518import java .util .*;
19+ import java .util .regex .Matcher ;
20+ import java .util .regex .Pattern ;
1621
1722public class Audit {
1823
@@ -30,6 +35,9 @@ static class AuditRecord {
3035 private final JsonObject queryParams ;
3136 private final String uidInstanceId ;
3237 private final String requestBody ;
38+ private final StringBuilder toJsonValidationErrorMessageBuilder = new StringBuilder ();
39+ @ Getter
40+ private String toJsonValidationErrorMessage = "" ;
3341
3442 private AuditRecord (Builder builder ) {
3543 this .timestamp = Instant .now ();
@@ -53,20 +61,150 @@ public JsonObject toJson() {
5361 .put ("source" , source )
5462 .put ("status" , status )
5563 .put ("method" , method )
56- .put ("endpoint" , endpoint )
57- .put ("trace_id" , traceId )
58- .put ("uid_instance_id" , uidInstanceId )
59- .put ("actor" , actor );
60- if (uidTraceId != null ) {
64+ .put ("endpoint" , endpoint );
65+
66+ if (traceId != null && validateId (traceId , "trace_id" )) {
67+ json .put ("trace_id" , traceId );
68+ }
69+
70+ if (uidTraceId != null && validateId (uidTraceId , "uid_trace_id" )) {
6171 json .put ("uid_trace_id" , uidTraceId );
6272 }
73+
74+ if (uidInstanceId != null && validateId (uidInstanceId , "uid_instance_id" )) {
75+ json .put ("uid_instance_id" , uidInstanceId );
76+ }
6377 actor .put ("id" , this .getLogIdentifier (json ));
64- json .put ("actor" , actor );
65- if (queryParams != null ) json .put ("query_params" , queryParams );
66- if (requestBody != null ) json .put ("request_body" , requestBody );
78+ if (validateJsonObjectParams (actor , "actor" )) {
79+ json .put ("actor" , actor );
80+ }
81+ if (queryParams != null && validateJsonObjectParams (queryParams , "query_params" )) json .put ("query_params" , queryParams );
82+ if (requestBody != null ) {
83+ String sanitizedRequestBody = sanitizeRequestBody (requestBody );
84+ if (!sanitizedRequestBody .isEmpty ()) {
85+ json .put ("request_body" , sanitizedRequestBody );
86+ }
87+ }
88+ toJsonValidationErrorMessage = toJsonValidationErrorMessageBuilder .isEmpty ()? "" : "Audit log failure: " + toJsonValidationErrorMessageBuilder .toString ();
6789 return json ;
6890 }
6991
92+ private boolean validateJsonObjectParams (JsonObject jsonObject , String propertyName ) {
93+ Set <String > keysToRemove = new HashSet <>();
94+
95+ for (String key : jsonObject .fieldNames ()) {
96+ String val = jsonObject .getString (key );
97+
98+ boolean containsNoSecret = validateNoSecrets (key , propertyName ) && validateNoSecrets (val , propertyName );
99+ boolean containsNoSQL = validateNoSQL (key , propertyName ) && validateNoSQL (val , propertyName );
100+
101+ if (!(containsNoSecret && containsNoSQL )) {
102+ keysToRemove .add (key );
103+ }
104+
105+ int parameter_max_length = 1000 ;
106+ if (val != null && val .length () > parameter_max_length ) {
107+ val = val .substring (0 , parameter_max_length );
108+ toJsonValidationErrorMessageBuilder .append (String .format (
109+ "The %s is too long in the audit log: %s. " , propertyName , key ));
110+ }
111+
112+ jsonObject .put (key , val );
113+ }
114+
115+ for (String key : keysToRemove ) {
116+ jsonObject .remove (key );
117+ }
118+
119+ return !jsonObject .isEmpty ();
120+ }
121+
122+ private boolean validateJsonArrayParams (JsonArray jsonArray , String propertyName ) {
123+ JsonArray newJsonArray = new JsonArray ();
124+
125+ for (Object object : jsonArray ) {
126+ if (object instanceof JsonObject ) {
127+ if (validateJsonObjectParams ((JsonObject )object , propertyName )) {
128+ newJsonArray .add (object );
129+ }
130+ } else {
131+ toJsonValidationErrorMessageBuilder .append ("The request body is a JSON array, but one of its elements is not a JSON object." );
132+ }
133+ }
134+
135+ jsonArray .clear ();
136+ jsonArray .addAll (newJsonArray );
137+
138+ return !jsonArray .isEmpty ();
139+ }
140+
141+ private String sanitizeRequestBody (String requestBody ) {
142+ ObjectMapper mapper = new ObjectMapper ();
143+ String sanitizedRequestBody = "" ;
144+
145+ try {
146+ JsonNode root = mapper .readTree (requestBody );
147+
148+ if (root .isObject ()) {
149+ JsonObject jsonObject = new JsonObject (mapper .writeValueAsString (root ));
150+ if (validateJsonObjectParams (jsonObject , "request_body" )) sanitizedRequestBody = jsonObject .toString ();
151+ } else if (root .isArray ()) {
152+ JsonArray jsonArray = new JsonArray (mapper .writeValueAsString (root ));
153+ if (validateJsonArrayParams (jsonArray , "request_body" )) sanitizedRequestBody = jsonArray .toString ();
154+ } else {
155+ toJsonValidationErrorMessageBuilder .append ("The request body of audit log is not a JSON object or array. " );
156+ }
157+ } catch (Exception e ) {
158+ toJsonValidationErrorMessageBuilder .append ("The request body of audit log is Invalid JSON: " ).append (e .getMessage ());
159+
160+ }
161+
162+ int request_body_max_length = 10000 ;
163+ if (sanitizedRequestBody .length () > request_body_max_length ) {
164+ sanitizedRequestBody = sanitizedRequestBody .substring (0 , request_body_max_length );
165+ toJsonValidationErrorMessageBuilder .append ("Request body is too long in the audit log: %s. " );
166+ }
167+ return sanitizedRequestBody ;
168+ }
169+
170+ private boolean validateNoSecrets (String fieldValue , String propertyName ) {
171+ if (fieldValue == null || fieldValue .isEmpty ()) {
172+ return true ;
173+ }
174+ Pattern uid2_key_pattern = Pattern .compile ("(UID2|EUID)-[A-Za-z]-[A-Za-z]-[A-Za-z0-9_-]+" );
175+ Matcher matcher = uid2_key_pattern .matcher (fieldValue );
176+ if (matcher .find ()) {
177+ toJsonValidationErrorMessageBuilder .append (String .format ("Secret found in the audit log: %s. " , propertyName ));
178+ return false ;
179+ } else {
180+ return true ;
181+ }
182+ }
183+
184+ private boolean validateNoSQL (String fieldValue , String propertyName ) {
185+ if (fieldValue == null || fieldValue .isEmpty ()) {
186+ return true ;
187+ }
188+ Pattern sql_injection_pattern = Pattern .compile (
189+ "(?i)(\\ bselect\\ b\\ s+.+\\ s+\\ bfrom\\ b|\\ bunion\\ b\\ s+\\ bselect\\ b|\\ binsert\\ b\\ s+\\ binto\\ b|\\ bdrop\\ b\\ s+\\ btable\\ b|--|#|\\ bor\\ b|\\ band\\ b|\\ blike\\ b|\\ bin\\ b\\ s*\\ (|;)"
190+ );
191+ if (sql_injection_pattern .matcher (fieldValue ).find ()) {
192+ toJsonValidationErrorMessageBuilder .append (String .format ("SQL injection found in the audit log: %s. " , propertyName ));
193+ return false ;
194+ } else {
195+ return true ;
196+ }
197+ }
198+
199+ private boolean validateId (String uidInstanceId , String propertyName ) {
200+ if (uidInstanceId .length () < 100 && validateNoSecrets (uidInstanceId , propertyName ) && validateNoSQL (uidInstanceId , propertyName ) ) {
201+ return true ;
202+ } else {
203+ toJsonValidationErrorMessageBuilder .append (String .format ("Malformed %s found in the audit log. " , propertyName ));
204+ return false ;
205+ }
206+ }
207+
70208 private String getLogIdentifier (JsonObject logObject ) {
71209 JsonObject actor = logObject .getJsonObject ("actor" );
72210 String email = (actor != null ) ? actor .getString ("email" ) : null ;
@@ -130,6 +268,7 @@ public Audit (String source) {
130268 public static final String UID_TRACE_ID_HEADER = "UID-Trace-Id" ;
131269 public static final String UID_INSTANCE_ID_HEADER = "UID-Instance-Id" ;
132270 private static final Logger LOGGER = LoggerFactory .getLogger (Audit .class );
271+ private static final String UNKNOWN_ID = "unknown" ;
133272
134273 private static Set <String > flattenToDotNotation (JsonObject json , String parentKey ) {
135274 Set <String > keys = new HashSet <>();
@@ -240,7 +379,7 @@ private String filterJsonArrayBody(JsonArray bodyJson, Set<String> allowedKeys)
240379 }
241380
242381 private String defaultIfNull (String s ) {
243- return s != null ? s : "unknown" ;
382+ return s != null ? s : UNKNOWN_ID ;
244383 }
245384
246385 private String defaultIfNull (String s , String defaultValue ) {
@@ -301,7 +440,12 @@ public void log(RoutingContext ctx, AuditParams params) {
301440 }
302441
303442 AuditRecord auditRecord = builder .build ();
304- LOGGER .info (auditRecord .toString ());
443+ String auditRecordString = auditRecord .toString ();
444+ if (!auditRecord .getToJsonValidationErrorMessage ().isEmpty ()) {
445+ LOGGER .error (auditRecord .getToJsonValidationErrorMessage () + auditRecordString );
446+ }
447+ LOGGER .info (auditRecordString );
448+
305449 } catch (Exception e ) {
306450 LOGGER .warn ("Failed to log audit record" , e );
307451 }
0 commit comments