Creating Advanced Audit Trails using ActionFilters in ASP.NET MVC
As mentioned in my previous article on Implementing Simple Audit Trails using ActionFilters in ASP.NET MVC, accountability is such an important factor when working with systems that involve any level of security and confidentiality. In that post, we discussed creating a very basic [Audit] attribute
that you could be used to decorate some of your Controllers and Methods to store some of the basic request information within a database to act as a very simple Audit Trail.
In this follow-up, we will implement some additional functionality to help flesh-out the original implementation and look into storing some of the more relevant information through serializing some of the values from the Request string into JSON strings that we can store within our database, monitoring User "Sessions" to allow us to track a single user’s auditable actions during the lifespan of their Session and a few other fun things.
The Problem
The basic auditing functionality mentioned in the previous post simply logged the current user, their IP Address and the Controller Action that they were visiting, but that wasn't enough functionality. We need to be able to specify how granular the additional data stored within our Audit Logging should be to allow the Audit messages to be much more meaningful.
The Solution
In order to handle this, we will have to make several modifications to our existing Auditing ActionFilter by adding the following major features :
- Session Capturing - We will implement a feature that will create a relationship with all of the Audit messages during a specific user Session (from Authentication to Logout or Session expiration).
- Granular Data Storage - We will add in an additional feature that will allow us to specific how granular the data object that is stored for the value will be. This will range from a very simple audit as mentioned in the first post to a full serialization of the entire contents of the Request object.
- JSON Request Serialization - We will use the JSON format to serialize the Request object and it will be related to the depth that we are going to serialize the Request object.
So we will want to add some fields to our previous Audit class so that it can handle some of this extended functionality :
public class Audit
{
// A new SessionId that will be used to link an entire
// users "Session" of Audit Logs together to help
// identifier patterns involving erratic behavior
public string SessionID { get; set; }
public Guid AuditID { get; set; }
public string IPAddress { get; set; }
public string UserName { get; set; }
public string URLAccessed { get; set; }
public DateTime TimeAccessed { get; set; }
// A new Data property that is going to store JSON
// string objects that will later be able to be
// deserialized into objects if necessary to view
// details about a Request
public string Data { get; set; }
public Audit(){}
}
These two new properties will be enough to handle all of the extended functionality that we are planning to add.
Session Capturing
Session Capturing is going to be very basic and we are going to use some logic that was previously used in another post on ActionFilters as a very rough method of identifying a specific user based on factors such as their IP Address and User Agent to create a string that will generate a MD5 hash to act as our Session Identifier.
However, since we are focusing on Auditing within a secure area, we are going to assume that the current user has logged in and has been authenticated. This will allow us to use their FormsAuthentication cookie to seed our MD5 hash and to function as their Session Identifier throughout the extent of their Session :
var sessionIdentifier = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(request.Cookies[FormsAuthentication.FormsCookieName].Value)).Select(s => s.ToString("x2")));
This isn't completely necessary to use an MD5 hash to accomplish this, as the Authentication Cookie Name would likely function as a unique-enough identifier, but I am mentioning it if you want to use other values to generate the hash itself.
Granular Data Storage
I believe that it is important for specific actions to have more weight in terms of being audited than others. As such, you’ll likely want to store more information about an action that might handle very sensitive and easily erroneous data during a POST
event more than you would say simply viewing a record.
This is why we are going to add a flexible system of determining what information in the Request object should be serialized using a very simple number system :
- Audit Level 0 (No Serialization) - No actual JSON Request data is stored. The IPAddress and other related properties on the object will still be available but not any additional information.
- Audit Level 1 (Light Serialization) - Slightly more Request information is stored such as the Cookies, Headers and Files within the Request.
- Audit Level 2 (Custom Serialization) - This captures all of the above with the addition of the Form object, the QueryString collection and all of the Parameters that were passed in with the Request. Feel free to customize this or any of these levels to suit your needs based on what you are handling.
- Audit Level 3 (Full Request Serialization) - Serializing all of the Serializable fields will be covered in a later post or will be left as an exercise to the user.
The only actual code that we are going to use to add these features will be a property within our AuditAttribute class :
public int AuditingLevel { get; set; }
along with a method that will be used inside the attribute to apply the appropriate Serialization :
// This will serialize the Request object based on the
// level that you determine
private string SerializeRequest(HttpRequestBase request)
{
switch (AuditingLevel)
{
// No Request Data will be serialized
case 0:
default:
return "";
// Basic Request Serialization - just stores Data
case 1:
return Json.Encode(new { request.Cookies, request.Headers, request.Files});
// Middle Level - Customize to your Preferences
case 2:
return Json.Encode(new { request.Cookies, request.Headers, request.Files, request.Form, request.QueryString, request.Params});
// Highest Level - Serialize the entire Request object (As mentioned earlier, this will blow up)
case 3:
// We can't simply just Encode the entire
// request string due to circular references
// as well as objects that cannot "simply"
// be serialized such as Streams, References etc.
return Json.Encode(request);
}
}
You can see the Serialization portion is very straight-forward and can easily be modified for handle additional customization by adding additional properties to the anonymous object to be serialized, so knock yourself out.
Decorating Your Methods
Since the Auditing Attribute has a publicly accessible property to specify our level of serialization and auditing, it will allow you to easily and cleanly decorate both Controller Actions and entire Controllers themselves.
This added functionality not only requires you to write very little code, but it adds tremendous flexibility so that you can use a higher auditing level for more important actions that may need to be closely monitored as seen below :
[Audit]
public ActionResult Unimportant() { ... }
[Audit(AuditingLevel = 1)]
public ActionResult SlightlyImportant() { ... }
[Audit(AuditingLevel = 2)]
public ActionResult Important() { ... }
[Audit(AuditingLevel = 3]
public ActionResult Classified() { ... }
And that's basically all you need to actually determine which actions will be audited and the level that will be applied.
For this very basic example, simply hovering over the Data object will allow you to see some of the hidden values within it. This is just for example purposes, feel free to format this area however you see fitting. One option might be to consider adding a tooltip to display additional details when the user hovers over a specific record :
Just Getting Started
All of the examples mentioned within this and the previous post are just an idea of some of the possibilities that you can use Action Filters for within an Auditing scenario. It is by no means a complete system at this point, but should function as an excellent starting point for those looking to create a fully-functional Auditing system or Security Framework for their code base.
You can download this entire example to tinker with to your heart's desire and I hope that it helps to provide a base for improving the accountability and security within your current and future applications :