Well it’s been a while since I wrote something here, but now I came across something useful and worth sharing. In a few projects I work on, users upload content to share online. Now while we have virus scanners running, they only scan files saved on disk. I needed to figure out a better way of keeping my system virus free, and not write dangerous files to disk, to check if its dangerous!
After a scan on Google, I really struggled to find a good (complete) example for my needs. So I thought I’d share how I got a solution set up which may be helpful for a few others who are trying to do the same thing.(And also help remind me how I did it in a few months’ time.)
Step 1 Install ClamAv
Firstly, you need to download and install clamAv. You can get a copy from here and follow the step by step instructions below:
- Extract or install ClamAV, I have put mine in d:\clamAV
- Create a directory “db” for the virus definitions etc.
- Start command prompt as administrator and run freshclam, this will start the ClamAV update process
- Run clamd –install to set up clamAV as a service.
- Start this ClamAV service (called ClamWin Free Antivirus Scanner Service) and also set it up to automatically start when your system starts up
Step 2 – Create your test cases and classes
I find it a lot easier to start by creating some basic unit tests, and then work on integrating the bits I need to later on. I have included some snippets from my github project here, just to give an overview of the main areas. In our test cases, we will use a VirusScannerFactory to give us an implementation of IScanViruses. I think the most useful objects to scan are memory streams, byte arrays and files. I have created the interface for this below.
- /// Interface for us to implement for each Virus scanner
- public interface IScanViruses {
- /// Scans a file for a virus
- ScanResult ScanFile(string fullPath);
- /// Scans some bytes for a virus
- ScanResult ScanBytes(byte[] bytes);
- /// Scans a stream for a virus
- ScanResult ScanStream(Stream stream);
- }
In MVC the HttpPostedFileBase is used to represent the uploaded file. We will pass the HttpPostedFileBase.InputStream to our IScanViruses.ScanStream Method to make sure the stream is virus free. So in the example test cases, we will pass a clean string and read this into a memory stream. This can then be scanned for viruses. We also use the EICAR test string to make sure our virus scanner implementation works correctly too!
- /// Test scanning a memory stream, typically what we get from a MVC http file base
- [TestCase("Here is another nice clean string to scan for viruses", true)]
- [TestCase(@"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*", false)]
- public void Can_scan_memory_stream_for_virus(string stringData, bool expectedToBeVirusFree) {
- var stream = new MemoryStream(Encoding.ASCII.GetBytes(stringData));
- var scanner = VirusScannerFactory.GetVirusScanner();
- var result = scanner.ScanStream(stream);
- Assert.That(result.IsVirusFree, Is.EqualTo(expectedToBeVirusFree), result.Message);
- }
We now need to implement the IScanViruses using ClamAV. For this we will use nClam. You can install it from nuget. This little library gives us access to ClamClient which wraps ClamAv functionality in a neat little API. Our ClamAvScanner class can then be used to scan for viruses.
- /// Implmemntation of Clam AV to scan viruses
- public class ClamAvScanner : IScanViruses {
- /// Scans a file for viruses
- public ScanResult ScanFile(string pathToFile) {
- var clam = new ClamClient("localhost", 3310);
- return MapScanResult(clam.ScanFileOnServer(pathToFile));
- }
- /// Scans some bytes for virus
- public ScanResult ScanBytes(byte[] data) {
- var clam = new ClamClient("localhost", 3310);
- return MapScanResult(clam.SendAndScanFile(data));
- }
- /// Scans your data stream for virus
- public ScanResult ScanStream(Stream stream) {
- var clam = new ClamClient("localhost", 3310);
- return MapScanResult(clam.SendAndScanFile(stream));
- }
- /// helper method to map scan result
- private ScanResult MapScanResult(ClamScanResult scanResult) {
- var result = new ScanResult();
- switch (scanResult.Result) {
- case ClamScanResults.Unknown:
- result.Message = "Could not scan file";
- result.IsVirusFree = false;
- break;
- case ClamScanResults.Clean:
- result.Message = "No Virus found";
- result.IsVirusFree = true;
- break;
- case ClamScanResults.VirusDetected:
- result.Message = "Virus found: " + scanResult.InfectedFiles.First().VirusName;
- result.IsVirusFree = false;
- break;
- case ClamScanResults.Error:
- result.Message = string.Format("VIRUS SCAN ERROR! {0}", scanResult.RawResult);
- result.IsVirusFree = false;
- break;
- }
- return result;
- }
- }
Step 3 – Integrating it with MVC
Now that we have got some test cases passing, we can look at how we hook this up into our MVC Application. In this example, we have a simple page that allows users to upload a file. In the [HttpPost] Index method, we expect a HttpPostedFileBase parameter which we will pass directly to our Virus scanner. The result of the scan is then stored in temp data, so the user knows if it is virus free.
- /// Main controller
- public class HomeController : Controller {
- /// Get the upload view
- [HttpGet]
- public ActionResult Index() {
- return View();
- }
- /// Handle the file upload
- [HttpPost]
- public ActionResult Index(UploadViewModel model, HttpPostedFileBase file) {
- var scanner = VirusScannerFactory.GetVirusScanner();
- var result = scanner.ScanStream(file.InputStream);
- ViewBag.Message = result.Message;
- return View(model);
- }
- }
It’s quite tricky to test this on a real virus. I have used the test EICAR virus file, which is probably a safe way to test it out. A more robust solution may be to try it out some real viruses. This is another post on its own!
Here are some tips for a solution like this:
- Make sure that you have a scheduled task to keep ClamAV up to date
- Just in case, have a second virus scanner, that scans your uploads directory for viruses every now and then
- more tips to come…
A full copy of this project can be found on Github.
A special thanks to:
Ryan Hoffman, with some of this code influenced by his examples
The guys at ClamAV for their great work and keeping the software up to date