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.
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.
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.
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
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.
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.
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
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.
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.
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.
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