Monthly Archives: December 2010

Protección antivirus en Alfresco ECM (segunda parte)

En la primera parte se vio como implementar una solución de antivirus escaneando todo el repositorio y que podía programarse por ejemplo en la “crontab” del sistema operativo.
Otra forma de realizar la detección de un virus que suba a Alfresco es mediante un escaneo “on-demand”, es decir, bajo demanda, y en este sentido cuando sea actualizado el contenido del documento. Dentro de las posibilidades que tenemos, de realizarlo de esta forma, podemos enviar un stream de datos del documento hacia el antivirus o bien ejecutar el propio antivirus pasándole exactamente el fichero a escanear. Ambas soluciones son posibles en ClamAV y en cualquiera de los dos casos el antivirus devuelve un código de error que será 0 si no hay infección y distinto si ha sido detectado, el fichero no existe, etc.
Por otra parte, Alfresco ECM disponde de las llamadas “policies” para ejecutar clases de Java o scripts de JavaScript cuando se produce algún “evento”. De todos los posibles tanto a nivel de contenido y de nodos, vamos a usar los siguientes:
Interface
Method
org.alfresco.repo.content.ContentServicePolicies
onContentUpdate
onContentRead
Usando OnContentUpdate y OnContentRead se puede lanzar la detección cuando sean leídos los documentos y/o cuando son actualizados. En el ejemplo se va a utilizar el evento OnContentUpdate para que cuando se realice una actualización de este (en el momento en el que se hace un COMMIT) se lance el antivirus y si este devuelve un código de error se añada un aspecto “infected” con dos propiedades, la fecha de detección y si ha sido “limpiado”.

Primero definimos los beans que usamos en el “behavior”: alfviral-behavior-context.xml

    
       
            true
        property>
       
           
                classpath:alfresco/extension/alfviral-behavior.properties
           
       
   
   
    <bean id="AlfViralBehavior" class="com.fegor.alfresco.behavior.OnUpdateReadScan"
        init-method=”init”>
       
           
       
       
           
       
       
           
       
       
            ${alfviral.command}
       
       
            ${dir.contentstore}
                  
    bean>

Seguidamente del fichero de propiedades: alfviral-behavior.properties

alfviral.command=/usr/bin/clamscan

Montamos el modelo de datos: alfviral-model-context.xml

       
   
       
           
                alfresco/extension/alfviralModel.xml
           
       
     

El modelo: alfviralModel.xml

     
   Alfresco Virus Alarm Model
   Fernando González Ruano (twitter://fegorara)
   1.0
  
     
     
  
  
     
  
   
       
            Infected
           
               
                    d:date
                    false
               
               
                    d:boolean
                    false
               
           
       
   

La parte para el cliente web del browser: web-client-config-custom.xml


  
     
        
        
     
  
  
     
        
     
  

El fichero de propiedades para las etiquetas: webclient.properties

ava_date=Fecha de detecciu00F3n
ava_clean=u00BFDesinfectado?

Y por último la clase java: OnUpdateReadScan.java

package com.fegor.alfresco.behavior;

import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.repo.content.ContentServicePolicies;
import org.alfresco.repo.policy.Behaviour;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.repository.ContentReader;

/**
 * Integrates antivirus scanning documents for alfresco
 *
 * Implements the policies of “OnContentUpdate” and “OnContentRead”.
 *
 * @author Fernando González Ruano (fegor)
 */
public class OnUpdateReadScan
    implements ContentServicePolicies.OnContentUpdatePolicy,
                ContentServicePolicies.OnContentReadPolicy
{
    private Logger logger = Logger.getLogger(OnUpdateReadScan.class);

    // behaviours
    private Behaviour onContentUpdate;
    private Behaviour onContentRead;
   
    // dependencias
    private PolicyComponent policyComponent;
    private ContentService contentService;
    private NodeService nodeService;
   
    // configuration
    private List command;
    private String store;
    private final String NAMESPACE_ALFVIRAL_CONTENT_MODEL = “alfviral.model”;
    private final QName ASPECT_INFECTED = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, “infected”);
    private final QName PROP_INFECTED_DATE = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, “date”);
    private final QName PROP_INFECTED_CLEAN = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, “clean”);  
   
    Map aspectValues = new HashMap();

    // método de inicio
    public void init ()
    {
        if (logger.isDebugEnabled()) logger.debug(“Start OnUpdateReadScan.”);
       
        // crear behaviours
        this.onContentUpdate = new JavaBehaviour(this,
                “onContentUpdate”,
                NotificationFrequency.TRANSACTION_COMMIT);
       
        this.onContentRead = new JavaBehaviour(this,
                “onContentRead”,
                NotificationFrequency.TRANSACTION_COMMIT);
       
        // binding “policies”
        this.policyComponent.bindClassBehaviour(QName.createQName
                (NamespaceService.ALFRESCO_URI,
                “onContentUpdate”),
                “cm:content”, this.onContentUpdate);
       
        this.policyComponent.bindClassBehaviour(QName.createQName
                (NamespaceService.ALFRESCO_URI,
                “onContentRead”),
                “cm:content”, this.onContentRead);
    }
   
    @Override
    public void onContentUpdate (NodeRef nodeRef, boolean flag)
    {
        ContentReader contentReader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
       
        // full path of file
        String contentUrl = contentReader.getContentUrl();
        String contentPath = contentUrl.replaceFirst(“store:/”, this.store);
       
        if (logger.isDebugEnabled())
        {
            logger.debug(“(Update) “+this.command+” “+contentPath);
        }
        else if (logger.isInfoEnabled())
        {
            logger.info(“(Update) Llamando al script de escaneo de virus para “+nodeRef.getId());
        }
       
        try
        {
            // execute command “antivir contentPath”
            this.command.add(contentPath);
            ProcessBuilder pb = new ProcessBuilder(this.command);
            Process process = pb.start();
           
            int intResult = process.waitFor();

            logger.debug(“(Update) Resultado del escaneo: “+intResult);
           
            // if result is not 0, file is infected
            if (intResult != 0)
            {
                logger.info(“ALERTA ** El fichero: “+contentReader.getContentUrl()+” está infectado. **”);
               
                // add aspect Infected is not assigned              
                if (!nodeService.hasAspect(nodeRef, this.ASPECT_INFECTED))
                {  
                    this.aspectValues.put(this.PROP_INFECTED_DATE, new Date());
                    this.aspectValues.put(this.PROP_INFECTED_CLEAN, false);               
                    nodeService.addAspect(nodeRef, this.ASPECT_INFECTED, this.aspectValues);
                   
                    // TODO Other actions… quarantine, delete, send email, etc.
                }
                else
                {
                    logger.debug(“Este fichero se detectó como infectado anteriormente.”);                   
                }
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
   
    @Override
    public void onContentRead (NodeRef nodeRef)
    {
        // TODO Actions for read content
    }
   
    // Setters…
    public void setPolicyComponent(PolicyComponent policyComponent)
    {
        this.policyComponent = policyComponent;
    }
   
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
   
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
   
    public void setStore(String store)
    {
        this.store = store;
    }
   
    public void setCommand(List command)
    {
        this.command = command;
    }   
}

A partir de aquí…

Esto es solo un ejemplo de la multitud de acciones y configuraciones que pueden realizarse en este sentido, por ejemplo, falta el código para el evento OnContentRead, falta alguna acción de mover los documentos infectados a algún espacio de cuarentena, avisar al administrador y al usuario de la detección, internacionalizar, etc. pero creo que es un buen punto de partida para que cada cual adapte esta solución a su manera. No os lo voy a dar todo hecho ¿verdad?… 😉

Finalmente dos capturas de pantalla:


Un extracto del log (poniendo en el log4j.properties el valor log4j.logger.com.fegor=debug):

 

10:44:16,141 User:admin DEBUG [alfresco.behavior.OnUpdateReadScan] (Update) [/usr/local/bin/clamscan, /home/alfresco/enterprise/alf_data/contentstore/2010/12/30/8/3/30a7961f-cdef-4410-9ba0-ca94a8542d03.bin, /home/alfresco/enterprise/alf_data/contentstore/2010/12/30/10/42/4a6a4884-c14e-4b24-ae22-c1e2a9839593.bin, /home/alfresco/enterprise/alf_data/contentstore/2010/12/30/10/42/4a6a4884-c14e-4b24-ae22-c1e2a9839593.bin] /home/alfresco/enterprise/alf_data/contentstore/2010/12/30/10/44/b9b5c69c-cff5-400e-9f63-4888ca745749.bin
10:44:20,125 User:admin DEBUG [alfresco.behavior.OnUpdateReadScan] (Update) Resultado del escaneo: 1
10:44:20,126 User:admin INFO  [alfresco.behavior.OnUpdateReadScan] ALERTA ** El fichero: store://2010/12/30/10/44/b9b5c69c-cff5-400e-9f63-4888ca745749.bin está infectado. **

Y algo de bibliografía que podéis consultar:

Libro: Alfresco Developer Guide
Autor: Jeff Potts
ISBN: 978-1-847193-11-7

Web de Jeff Potts: http://ecmarchitect.com/
Wiki de Alfresco: http://wiki.alfresco.com/wiki/Policy_Component
Web de ClamAV: http://www.clamav.net/lang/en/

Protección antivirus en Alfresco ECM (primera parte)

Alfresco ECM no viene con antivirus de serie, al igual que otras aplicaciones y programas como puede ser OpenOffice.org, etc. dejando esta tarea al sistema operativo y las soluciones antivirus elegidas.
La solución de usar un antivirus directamente sobre Alfresco plantea varias soluciones cada una con sus pros y sus contras así como el impacto en el sistema que puede ser mayor o menor.
.
Se pueden plantear por tanto varias soluciones de escaneo con los documentos subidos a Alfresco, que pueden ser:
1. Escaneo del repositorio localizando el fichero infectado directamente y dejando a posteriori la localización del NodeRef.
2. Escaneo bajo demanda al subir un documento, haciendo para ello que Alfresco ECM redirija el contenido hacia un antivirus que permita la lectura de un stream de datos.
Ambas soluciones son viables en Alfresco aunque su función es distinta, en la primera se escanea todo el repositorio, dejándose generalmente para tareas nocturnas o de fin de semana y aseguran tener un repositorio saneado con el tiempo. Esto causa impacto en los discos que cada cierto tiempo tienen que realizar la tarea de principio a fin y que puede ser muy larga dependiendo del tamaño del repositorio.
La segunda solución es limpia y permite que cualquier documento subido a Alfresco ECM haya sido previamente escaneado, bloqueando si es necesaria, su subida si se detecta un código maligno. Esta solución impacta directamente a la hora de la subida del documento pero nos asegura que los archivos subidos estén limpios sin tener que ser examinados posteriormente.
Una solución muy completa es la de unir ambas soluciones de forma que siempre sean escaneados los documentos al ser subidos y creados en Alfresco ECM y a su vez, que cada cierto tiempo se compruebe la “salud” de los documentos pasando un escaneo completo ya que además se escanearán con la última actualización del antivirus lo que puede detectar documentos con virus que inicialmente no fueron detectados.
— Una primera solución —
Para la primera solución, a su vez, se puede abordar desde distintas perspectivas, bien desde un plano más externo a Alfresco ECM y otro directamente desarrollando parte de la solución como una extensión de este.
En cualquier caso hay que elegir una solución antivirus que pueda servirnos para estos propósitos. En mi caso he elegido la solución ClamAV ya que permite muchas formas de interactuar, es OpenSource y podemos usarlo en varias plataformas (Linux, BSD, AIS, Solaris, etc.)
Para instalar ClamAV solo hay que instalarlo vía apt-get/aptitude, yum, etc. o bien bajando el fichero directamente y compilándolo, generalmente con la secuencia:
./configure
make
make install
Una vez instalado (en el servidor, se entiende) podemos hacer uso del comando “clamscan” para escanear el repositorio. (NOTA: No es objetivo de este post la configuración de clamav, freshclam, etc.)
El uso de clamscan es muy sencillo, básicamente hay que poner las opciones necesarias y el directorio/fichero a escanear. Aquí vamos a usar las opciones de visualizar solo los ficheros infectados y que además sea de forma recursiva.
Un ejemplo:
clamscan -i -r /alfresco/alf_data/
Bien, hasta aquí correcto, nos listará los virus encontrados, pero ahora necesitamos saber a qué documento de alfresco corresponde en su entorno. Como sabemos, en Alfresco, ni siquiera el nombre del documento es correcto para poder localizarlo, en todo caso podría ser si conocemos la ruta completa, aunque no sería lo mejor; pero si podemos usar el UUID o también llamado nodeRef.
Aquí podemos usar también varias formas, bien haciendo llamadas RESTful/SOAP a WebServices o WebScripts de Alfresco ECM en el que hay que extenderlo para poder realizar las llamadas necesarias y que este nos devuelva los valores o bien realizar las acciones que necesitamos; o usar directamente consultas SQL a la base de datos utilizada. En este primer caso vamos a usar una consulta SQL hacia nuestra instalación en MySQL.
La consulta podría ser algo asi:

SELECT alf_node.uuid 
    FROM alf_node_properties
        INNER JOIN alf_node
            ON alf_node.id = alf_node_properties.node_id
        INNER JOIN alf_qname
            ON alf_qname.id = alf_node_properties.qname_id
WHERE alf_node_properties.node_id =
(SELECT alf_node_properties.node_id 
    FROM alf_content_url
                INNER JOIN alf_content_data
                        ON alf_content_url.id = alf_content_data.content_url_id
        INNER JOIN alf_node_properties
            ON alf_content_data.id = alf_node_properties.long_value
        INNER JOIN alf_qname
            ON alf_qname.id = alf_node_properties.qname_id
    WHERE alf_content_url.content_url = ‘${FUID}’
    AND alf_qname.local_name = ‘content’)
AND alf_qname.local_name = ‘name’;
 

Donde ${FUID} es la localización física del fichero en el contentstore o File Unique IDentifier, lo que Alfresco ECM llama normalmente contentUrl.
Bien, entonces, ¿Cómo podríamos hacer para que si un fichero es localizado con un virus, nuestro sistema Linux realice una llamada a Alfresco para que, por ejemplo lo renombre, ponga un aviso en el título, envíe correos electrónicos, etc.?
Para esto vamos a usar además un comando más, el comando wget (o curl si se prefiere) que puede realizar peticiones GET a un servidor para poder llamar a Alfresco ECM via RESTful y realizar una llamada al WebScript necesario.
Lo primero es el script que usaremos dentro del cron del sistema operativo (en este caso realizado para Linux CentOS con bash):
——————– alfviral.sh
#!/bin/bash

# Variables de configuración
#

ALFUSER=admin
ALFPASSWD=admin
ALFRESCO_URL=http://localhost:8080/alfresco
DIR_ROOT=/home/alfresco/alfresco-enterprise-3.3.4/alf_data
USERNAME=alfresco
PASSWD=alfresco
DATABASE=alfresco_enterprise_334
HOST=localhost
PORT=3306

PROG=$0
PROGDIR=`dirname “$PROG”`

SCANRES_FILE=scanres.txt
NODEREFS_FILE=noderefs.txt
DOCNAMES_FILE=docnames.txt

# Crea lista de ficheros infectados
#
echo “Creando lista de ficheros infectados…”
rm -f ${PROGDIR}/${SCANRES_FILE} 2>/dev/null
CONTENTSTORE=${DIR_ROOT}/contentstore
clamscan -i -r ${CONTENTSTORE} | awk -F: ‘$1~/.bin/{print “store:/”$1}’ | sed s:${CONTENTSTORE}::g >${PROGDIR}/${SCANRES_FILE}

if [ ! -s ${PROGDIR}/${SCANRES_FILE} ]
then
    echo “No hay ficheros infectados.”
    exit 0
fi

# Crea lista de NodeRefs de los ficheros
#
echo “Creando referencias NodeRefs de los FUID…”
rm -f ${PROGDIR}/${NODEREFS_FILE} 2>/dev/null
for FUID in $(cat ${PROGDIR}/${SCANRES_FILE})
do
    mysql -u${USERNAME} -p${PASSWD} -D${DATABASE} -h${HOST} -P${PORT} –skip-column-names –raw –silent >>${PROGDIR}/${NODEREFS_FILE} <
SELECT alf_node.uuid 
    FROM alf_node_properties
        INNER JOIN alf_node
            ON alf_node.id = alf_node_properties.node_id
        INNER JOIN alf_qname
            ON alf_qname.id = alf_node_properties.qname_id
WHERE alf_node_properties.node_id =
(SELECT alf_node_properties.node_id 
    FROM alf_content_url
                INNER JOIN alf_content_data
                        ON alf_content_url.id = alf_content_data.content_url_id
        INNER JOIN alf_node_properties
            ON alf_content_data.id = alf_node_properties.long_value
        INNER JOIN alf_qname
            ON alf_qname.id = alf_node_properties.qname_id
    WHERE alf_content_url.content_url = ‘${FUID}’
    AND alf_qname.local_name = ‘content’)
AND alf_qname.local_name = ‘name’;
q
STOP
done

if [ ! -s ${PROGDIR}/${NODEREFS_FILE} ]
then
    echo “¡No se han encontrado referencias a los ficheros!”
    exit 1
fi

# Lanza las llamadas a Alfresco hacia el webscript
#
echo “Llamando a Alfresco…”
rm -f ${PROGDIR}/${DOCNAMES_FILE} 2>/dev/null
ALF_TICKET=`curl “http://localhost:8080/alfresco/service/api/login?u=${ALFUSER}&pw=${ALFPASSWD}” | grep TICKET_ | sed ‘s:::g’ | sed ‘s:::g’`
for NODEREF in $(cat ${PROGDIR}/${NODEREFS_FILE})
do
    curl “${ALFRESCO_URL}/service/protect/alfviral?nref=${NODEREF}&alf_ticket=${ALF_TICKET}” >>${PROGDIR}/${DOCNAMES_FILE}
    echo “” >>${PROGDIR}/${DOCNAMES_FILE}
done——————– alfviral.sh

Y en segundo lugar crear un WebScript que realice las tareas necesarias:
——————– alfviral.get.desc.xml
<webscript>  <shortname>Alfresco Virus Alertshortname>
  <description>Alfresco Virus Alertdescription>
   <url>/protect/alfviral?nref={nref}url>
   <format default=”text”>extension</format>
   <authentication>user</authentication>
   <transaction>required</transaction>
webscript>
——————– alfviral.get.desc.xml

——————– alfviral.get.js
// chequeo de parámetros
if (args.nref == undefined || args.nref.length == 0)
{
    status.code = 400;
    status.message = “Es necesario indicar el nref.”;
    status.redirect = true;
}
else
{
    // buscar el documento por su nodeRef
    var nodes = search.luceneSearch(“ID:”workspace://SpacesStore/” + args.nref + “””);

    // renombrar el documento
    var name_infected = “”;
    name_infected = nodes[0].name;
    if (name_infected.indexOf(“_INFECTADO”) == -1)
    {
        nodes[0].name = name_infected + “_INFECTADO”;
        nodes[0].save();
        if (logger.isLoggingEnabled())
            logger.log(“El documento: ” + nodes[0].name + ” ha sido renombrado por estar infectado.”);
    }
    
    model.name_infected = nodes[0].name;
}

——————– alfviral.get.js

——————– alfviral.get.text.ftl
${name_infected}
——————– alfviral.get.text.ftl

——————– alfviral.get.html.400.ftl

${status.message}
body>
html>

——————– alfviral.get.html.400.ftl
Poniendo la llamada en nuestra crontab podemos escanear periódicamente nuestro repositorio y actuar en consecuecia.
En este caso, simplemente se han renombrado los documentos infectados, como puede verse en la imagen:

Para la segunda entrega seguiremos abordando nuevas posibilidades para mantener “sano” nuestro repositorio.

¡Ah!, y FELIZ NAVIDAD A TODOS