Antivirus

Document created by resplin Employee on Jun 6, 2015
Version 1Show Document
  • View in full screen mode

Obsolete Pages{{Obsolete}}

The official documentation is at: http://docs.alfresco.com



This page describes how to create an AMP file which will run an antivirus action on files inside Alfresco. It has been developed on a Linux machine running Alfresco 3.3 and using ClamAV.

When the action is run, the antivirus software will scan the file for viruses. If any are found, then an email will be sent to the creator of the file, and the file will be deleted. If no virus is found, then nothing happens. The action can be run as a space rule, or interactively.


Prerequisites


You need to have the antivirus software installed. I chose ClamAV, as it runs on Linux but checks for mainly for viruses targeting Windows. It can be called on a file from the command line like so:



$ clamdscan /tmp/eicar.com
/tmp/eicar.com: Eicar-Test-Signature FOUND


SCAN SUMMARY -----------
Infected files: 1
Time: 0.067 sec (0 m 0 s)

The command clamdscan assumes that you already have clamd running. The difference between clamdscan and clamscan is that clamdscan will run much faster, as it doesn't have to read all the virus definitions each time it is run. But it does require clamd to be running, which clamscan does not.

You also need the Alfresco SDK installed. I developed my AMP using the BasicAmpSample project as a starting point. Make sure that you can compile the sample projects in the SDK before creating a new one.

For Alfresco to send emails, you will need to make sure you have outboundSMTP.properties configured correctly.


Project layout


Figuring out a working layout for your AMP project is tricky. This works for me. My project is called antivirus-action:



antivirus-action
|-- build.xml
|-- config
|   `-- alfresco
|       `-- module
|           `-- antivirus-action
|               |-- alfresco-global.properties
|               |-- context
|               |   `-- antivirus-action.xml
|               |-- module-context.xml
|               |-- module.properties
|               `-- template
|                   `-- virus_found.ftl
`-- source
    `-- uk
        `-- ac
            `-- st_andrews
                `-- repo
                    `-- action
                        `-- executer
                            |-- AntivirusActionExecuter.java
                            `-- antivirus-action-messages.properties

My package name is uk.ac.st_andrews.repo.action.executer. If you are using this page to help you develop another AMP project, then you will want to change some or all of that package name. uk.ac.st_andrews is the name of my institution, and repo.action.executer is where my package fits into Alfresco.

The contents and role of all the files in the tree above will be discussed below.


Module properties


The module.properties file just contains a few strings which describe the properties of the module, as you might imagine.



module.id=antivirus-action
module.version=1.0
module.title=Antivirus Action
module.description=Integrates antivirus checking as an action in Alfresco
module.repo.version.min=3.3

The XML files


These are the files which hold everything together. The module-context.xml is looked for by Alfresco, so I think it has to be present. This is what mine looks like. It just points to a module specific file.



<beans>
  <import resource='classpath:alfresco/module/antivirus-action/context/antivirus-action.xml'/>
</beans>

The antivirus-action.xml file contains bean definitions for the Java class and an I18N strings resource bundle:



<beans>
  <bean id='antivirus-action-messages' class='org.alfresco.i18n.ResourceBundleBootstrapComponent'>
    <property name='resourceBundles'>
      <list>
        <value>uk.ac.st_andrews.repo.action.executer.antivirus-action-messages</value>
      </list>
    </property>
  </bean>
  <bean id='antivirus-action' class='uk.ac.st_andrews.repo.action.executer.AntivirusActionExecuter' parent='action-executer'>
    <property name='contentService'>
      <ref bean='contentService' />
    </property>
    <property name='nodeService'>
      <ref bean='nodeService' />
    </property>
    <property name='templateService'>
      <ref bean='templateService' />
    </property>
    <property name='actionService'>
      <ref bean='actionService' />
    </property>
    <property name='personService'>
      <ref bean='personService' />
    </property>
    <property name='fromEmail'>
      <value>${antivirus.mailer}</value>
    </property>
    <property name='command'>
      <bean class='org.alfresco.util.exec.RuntimeExec'>
        <property name='commandMap'>
          <map>
            <entry key='.*' value='${antivirus.exe} ${source}'/>
          </map>
        </property>
        <property name='errorCodes'>
          <value>1</value>
        </property>
      </bean>
    </property>
  </bean>
</beans>

As well as the usual properties which a bean might need set, the command property illustrates how to specify a command to execute. The command for the antivirus action is ${antivirus.exe} ${source}. The ${antivirus.exe} place holder is given in the properties file alfresco-global.properties, and ${source} gets filled in by the Java code.


The Java code


This is the Java class:



package uk.ac.st_andrews.repo.action.executer;

import java.util.List;
import java.io.File;
import java.util.Map;
import java.util.HashMap;

import org.alfresco.model.ContentModel;

import org.alfresco.error.AlfrescoRuntimeException;

import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.action.ActionService;

import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.repo.action.executer.MailActionExecuter;

import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.TemplateService;

import org.alfresco.service.cmr.security.PersonService;

import org.alfresco.util.TempFileProvider;
import org.alfresco.util.exec.RuntimeExec;
import org.alfresco.util.exec.RuntimeExec.ExecutionResult;

/**
* action executer
*
* @author Swithun Crowe
*/
public class AntivirusActionExecuter extends ActionExecuterAbstractBase
{
   /**
    * Action constants
    */
   public static final String NAME = 'antivirus-action';
   public static final String VAR_SOURCE = 'source';
  
   private ContentService contentService;
   private NodeService nodeService;
   private TemplateService templateService;
   private ActionService actionService;
   private PersonService personService;

   private RuntimeExec command;
  
   private String fromEmail;
  
   /**
    * @param contentService The contentService to set.
    */
   public void setContentService(ContentService contentService)
     {
        this.contentService = contentService;
     }

   /**
    * @param nodeService The nodeService to set.
    */
   public void setNodeService(NodeService nodeService)
     {
        this.nodeService = nodeService;
     }

   /**
    * @param templateService The templateService to set.
    */
   public void setTemplateService(TemplateService templateService)
     {
        this.templateService = templateService;
     }

   /**
    * @param actionService The actionService to set.
    */
   public void setActionService(ActionService actionService)
     {
        this.actionService = actionService;
     }

   /**
    * @param personService The personService to set.
    */
   public void setPersonService(PersonService personService)
     {
        this.personService = personService;
     }
  
   /**
    * @param fromEmail The email address that messages are sent from
    */
   public void setFromEmail(String fromEmail)
     {
        this.fromEmail = fromEmail;
     }
  
   /**
    * @param command The antivirus command
    */
   public void setCommand(RuntimeExec command)
     {
        this.command = command;
     }
  
   @Override
   public void init()
       {
          super.init();
       }
  
   @Override
   protected void addParameterDefinitions(List<ParameterDefinition> paramList)
       {
          // no params
       }
  
   @Override
   protected void executeImpl(final Action ruleAction,
                              final NodeRef actionedUponNodeRef)
       {
          // put content into temp file
          ContentReader reader = contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT);
          String fileName = (String) nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_NAME);
          File sourceFile = TempFileProvider.createTempFile('anti_virus_check_', '_' + fileName);
          reader.getContent(sourceFile);

          // add the source property
          Map<String, String> properties = new HashMap<String, String>(5);
          properties.put(VAR_SOURCE, sourceFile.getAbsolutePath());
         
          // execute the transformation command
          ExecutionResult result = null;
          try
            {
               result = command.execute(properties);
            }
          catch (Throwable e)
            {
               throw new AlfrescoRuntimeException('Antivirus check error: \n' + command, e);
            }
         
          // check
          if (!result.getSuccess())
            {
               //throw new AlfrescoRuntimeException('Antivirus check error: \n' + result);
               // try sending email using template
               try
                 {
                    // try to get document creator's email address
                    String creatorName = (String) nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_CREATOR);
                    if (null == creatorName || 0 == creatorName.length())
                      {
                         throw new Exception('couldn't get creator's name');
                      }
                   
                    NodeRef creator = personService.getPerson(creatorName);
                    if (null == creator)
                      {
                         throw new Exception('couldn't get creator');
                      }
                   
                    String creatorEmail = (String) nodeService.getProperty(creator, ContentModel.PROP_EMAIL);
                    if (null == creatorEmail || 0 == creatorEmail.length())
                      {
                         throw new Exception('couldn't get creator's email address');
                      }
                   
                    // put together message
                    String emailTemplate = 'alfresco/module/antivirus-action/template/virus_found.ftl';
                    Map<String, Object> model = new HashMap<String, Object>(8, 1.0f);
                    model.put('filename', fileName);
                    model.put('message', result);
                   
                    String emailMsg = templateService.processTemplate('freemarker', emailTemplate,  model);
                   
                    // send email message
                    Action emailAction = actionService.createAction('mail');
                    emailAction.setParameterValue(MailActionExecuter.PARAM_TO, creatorEmail);
                    emailAction.setParameterValue(MailActionExecuter.PARAM_FROM, fromEmail);
                    emailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, 'Virus found in ' + fileName);
                    emailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, emailMsg);
                    emailAction.setExecuteAsynchronously(true);
                    actionService.executeAction(emailAction, null);
                   
                    // delete node
                    nodeService.addAspect(actionedUponNodeRef, ContentModel.ASPECT_TEMPORARY, null);
                    nodeService.deleteNode(actionedUponNodeRef);
                 }
               catch (Exception e)
                 {
                    throw new AlfrescoRuntimeException('Failed to send email:\n' + e.getMessage());
                 }
            }
       }
}

The class contains several setter methods. You will see that these match the properties in the bean XML file. The required method addParameterDefinitions is empty, as no parameters are needed, and init just calls the super class. All the work is done in the executeImpl method.

The content of the node (file) to be scanned is put into a temporary file. This file is scanned. If a virus is found, then the email address of the creator of the node is obtained and an email is sent to them. The node is then deleted. The email message is generated by processing a Freemarker template.


I18N strings


The file antivirus-action-messages.properties (also in package uk.ac.st_andrews.repo.action.executer) contains the I18N strings used by the action. This resource bundle gets loaded by the bean XML file for the action (antivirus-action.xml).



antivirus-action.title=Antivirus scan
antivirus-action.description=This will check file for viruses
antivirus-action.summary=This will check file for viruses

The other properties file used, alfresco-global.properties gets picked up by Alfresco without you having to refer to it anywhere. I have put in properties which you may want to change without recompiling the project, i.e. the antivirus command to use and the email address to use as the from address when sending messages.



antivirus.exe=/usr/bin/clamdscan
antivirus.mailer=some@email.address

Template


The virus_found.ftl template contains the email message body, with place holders for the name of the file being scanned and the output from the antivirus software.



The file (${filename}) you ingested into Alfresco contains a virus. This is
the output from the anti-virus software:

${message}

The file will be deleted.

Regards

Alfresco

The place holders are filled in by the Java code.


Compiling the project


The build.xml file is a modification of the one found in the BasicAmpSample project.



<project name='antivirus-action' default='package-amp' basedir='.'>
  <property name='project.dir' value='.'/>
  <property name='build.dir' value='${project.dir}/build'/>
  <property name='config.dir' value='${project.dir}/config'/>
  <property name='jar.file' value='${build.dir}/lib/antivirus-action.jar'/>
  <property name='amp.file' value='${build.dir}/dist/antivirus-action.amp'/>
 
  <property name='properties.files.tocopy' value='**/*.properties' />

 
  <target name='mkdirs'>
    <mkdir dir='${build.dir}/dist' />
    <mkdir dir='${build.dir}/lib' />
    <mkdir dir='${build.dir}/classes' />
  </target>
 
  <path id='class.path'>
    <dirset dir='${build.dir}' />
    <fileset dir='../../lib/server' includes='**/*.jar'/>
  </path>
 
  <target name='compile' depends='mkdirs'>
    <javac classpathref='class.path' srcdir='${project.dir}/source' destdir='${build.dir}/classes' />
    <copy todir='${build.dir}/classes'>
      <fileset dir='${project.dir}/source' includes='${properties.files.tocopy}'/>
    </copy>
  </target>
 
  <target name='package-jar' depends='compile'>
    <jar destfile='${jar.file}'>
      <fileset dir='${build.dir}/classes' />
    </jar>
  </target>
 
  <target name='package-amp' depends='package-jar' description='Package the Module' >
    <zip destfile='${amp.file}' >
      <fileset dir='${build.dir}' includes='lib/*.jar' />
      <fileset dir='${project.dir}' includes='config/**/*.*' excludes='**/module.properties' />
      <fileset dir='${project.dir}/config/alfresco/module/antivirus-action' includes='module.properties' />
    </zip>
  </target>
</project>

The default target for ant is to build the AMP file. It can then be inserted into alfresco.war using alfresco-mmt.jar, e.g.



$ java -jar bin/alfresco-mmt.jar install antivirus-action.amp tomcat/webapps/alfresco.war -preview
Installing AMP 'antivirus-action.amp' into WAR 'tomcat/webapps/alfresco.war'
Adding files relating to version '1.0' of module 'antivirus-action'
   - File '/WEB-INF/lib/antivirus-action.jar' added to war from amp
   - File '/WEB-INF/classes/alfresco/module/antivirus-action/template/virus_found.ftl' added to war from amp
   - Directory '/WEB-INF/classes/alfresco/module/antivirus-action/template' added to war
   - File '/WEB-INF/classes/alfresco/module/antivirus-action/alfresco-global.properties' added to war from amp
   - File '/WEB-INF/classes/alfresco/module/antivirus-action/context/antivirus-action.xml' added to war from amp
   - Directory '/WEB-INF/classes/alfresco/module/antivirus-action/context' added to war
   - File '/WEB-INF/classes/alfresco/module/antivirus-action/module-context.xml' added to war from amp
   - Directory '/WEB-INF/classes/alfresco/module/antivirus-action' added to war

Run without the -preview switch to actually install the AMP. You may need to delete the exploded tomcat/webapps/alfresco directory. Or you can use the apply_amps.sh script.


Running the action


Once the AMP has been added to Alfresco, it should turn up as an action in the Run action menu. You can run the action on any files in Alfresco. Or, the action can be put into a rule, so that, say, any files being added to a space will be scanned for viruses.

A sample (and hopefully harmless) virus can be obtained from eicar for testing.


Conclusion


A page like this one would have been very handy when I was developing this project. Hopefully it is general enough to be useful for other similar projects, but detailed enough to give you an idea of how and why it works.
AMP
SDK
3.3

Attachments

    Outcomes