Introduction
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 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 Logging in until 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
{
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; }
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:
private string SerializeRequest(HttpRequestBase request)
{
switch (AuditingLevel)
{
case 0:
default:
return "";
case 1:
return Json.Encode(new { request.Cookies, request.Headers, request.Files});
case 2:
return Json.Encode(new { request.Cookies, request.Headers,
request.Files, request.Form, request.QueryString, request.Params});
case 3:
return Json.Encode(request);
}
}
You can see the Serialization portion is very straight-forward and can easily be modified for handling additional customization by adding additional properties to the anonymous object to be serialized, so knock yourself out.
Decorating Your Methods (tastefully)
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.
A bare-bones example of an Advanced Audit Log, which details the major information of the Audit class.
and 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.)
Hovering over the Data area of the Audit Log will reveal additional details about the Request object.
Consider This A Starting Point
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 hearts desire and I hope that it helps to provide a base for improving the accountability and security within your current and future applications: