Tuesday, November 18, 2014

Alfrescoでリアルタイムウイルススキャンを実装する


こんにちは。おおたにです。

今回は、Alfrescoでファイルアップロード時にウイルススキャンを行うようなカスタムモジュールを作ってみたいと思います。想定している環境は、以下のとおりです。Alfrescoについては、多分このままもしくは多少の修正で5.0.xでも動くと思います。
  • Alfresco : Alfresco Community Edition 4.2.f
  • OS : CentOS 6系
  • ウイルススキャンツール : Clam AntiVirus


準備


まずは、AlfrescoとClam AntiVirusをインストールします。AlfrescoのインストールはAlfresco4.0をインストールしてみよう(インストーラ編)Alfresco4.2をインストールしてみよう(手動インストール編)あたりを参考にしてください。

次に、Clam AntiVirusをインストールします。CentOSへのインストールはこちらの記事が参考になります。基本的にはファイルアップロードのタイミングでスキャンコマンドを実行するだけなので、自動起動や定期実行の設定は必要ありません。clamscanコマンドがパス指定なしで実行できるようになっていればOKです。


カスタムモジュールの開発


では、早速コードを見てみましょう。今回作ったコード/設定ファイルは以下の4つになります。なお、サンプルコードはこちらからダウンロードすることができます。
  • VirusScanActionExecuter.java : ウイルススキャン処理の実装
  • VirusDetectException.java : ウイルス検知時に投げるException
  • VirusScan.java : OnContentUpdatePolicyを利用したコンテンツ実体更新フック
  • alfresco-virusscan-sample-context.xml :VirusScanActionExecuterとVirusScanのSpring bean定義ファイル

VirusScanActionExecuter.java

ウイルススキャン処理を、ActionExecuterとして実装します。ActionExecuterとして実装するとActionServiceから名前を指定するだけで処理をキックできるため、Alfresco内での再利用性が高まります。具体的には、ActionExecuterAbstractBaseのexecuteImplに処理を実装します。

public class VirusScanActionExecuter extends ActionExecuterAbstractBase {
...
    @Override
    protected void executeImpl(Action action, NodeRef nodeRef) {
        String fileName = (String) fileFolderService.getFileInfo(nodeRef).getName();
        LOGGER.debug("Virus scan start : " + fileName);

        // Output content to temporary file
        ContentReader contentReader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        File targetFile = null;
        try {
            targetFile = TempFileProvider.createTempFile(contentReader.getContentInputStream(), "virusscan_", ".bin");
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        // Execute virus scan
        Map<String, String> properties = new HashMap<String, String>();
        properties.put("target", targetFile.getAbsolutePath());
        ExecutionResult result = virusScanCommand.execute(properties);

        // Check result and delete infected file.
        int exitValue = result.getExitValue();
        if (exitValue == 0) {
            LOGGER.debug("Virus not detected : " + fileName);
        } else if (exitValue == 1) {
            fileFolderService.delete(nodeRef);
            throw new VirusDetectException("Virus detected and cleaned up : " + fileName);
        } else {
            LOGGER.error("Unable to scan : " + result);
        }
    }
...
}

VirusScan.java

OnContentUpdatePolicyを利用して、コンテンツ実体更新時(正確にはそのトランザクションのコミット時)にウイルススキャン処理をキックします。

public class VirusScan extends TransactionListenerAdapter implements OnContentUpdatePolicy, InitializingBean {
...
    @Override
    public void onContentUpdate(NodeRef nodeRef, boolean newContent) {
        if (!nodeService.getType(nodeRef).equals(ContentModel.TYPE_CONTENT) || nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT) == null) {
            return;
        }
        AlfrescoTransactionSupport.bindListener(this);
        AlfrescoTransactionSupport.bindResource(NODE_KEY, nodeRef);
    }

    @Override
    public void beforeCommit(boolean readOnly)
    {
        final NodeRef nodeRef = AlfrescoTransactionSupport.getResource(NODE_KEY);
        String fileName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);

        LOGGER.debug("beforeCommit start : " + fileName + ", readonly : " + readOnly);
        actionService.executeAction(actionService.createAction("virus-scan-action"), nodeRef);
        LOGGER.debug("beforeCommit end : " + fileName);
    }
}

alfresco-virusscan-sample-context.xml

上記2つの実装をSpring beanとして定義します。VirusScanActionExecuterはvirus-scan-actionという名前で登録し、Java APIやJavaScript API、REST APIからこの名前でウイルススキャン処理を実行できるようになります。

コマンドの中身はLinux用になっているのでOSに応じて変更もしくはOSごとに指定してください。

<bean id="virus-scan-action" class="jp.aegif.alfresco.sample.action.executer.VirusScanActionExecuter" parent="action-executer">
   <property name="contentService">
      <ref bean="ContentService" />
   </property>
   <property name="fileFolderService">
      <ref bean="FileFolderService" />
   </property>
   <property name="virusScanCommand">
      <bean class="org.alfresco.util.exec.RuntimeExec">
         <property name="commandsAndArguments">
            <map>
               <entry key=".*">
                  <list>
                     <value>clamscan</value>
                     <value>--stdout</value>
                     <value>--no-summary</value>
                     <value>${target}</value>
                  </list>
               </entry>
            </map>
         </property>
      </bean>
   </property>
</bean>


テストしてみよう


では、カスタムモジュールを適用し、ウイルススキャン機能をテストしてみましょう。

JavaクラスファイルはalfrescoVirusScanSample.jarなどにパッケージングして<tomcat_dir>/webapps/alfresco/WEB-INF/lib/にコピーし、alfresco-virusscan-sample-context.xmlは<tomcat_dir>/shared/classes/alfresco/extension/にコピーします。もちろんAlfrescoを再起動する必要があります。

そして、クライアントPCはリアルタイムスキャンが無効になっていることを確認します。クラ イアントPCにリアルタイムスキャンが設定されていると、テスト用のウイルスファイルが自動除去されてしまってテストができません。そして、テスト用のウイルスファイルをこちらからダウンロードしておきます。

Alfrescoが起動したら、Alfrescoにログインして先ほどのテスト用ウイルスファイルをアップロードします。以下のようにアップロードに失敗し、ウイルスが検知された旨が表示されればOKです。




さらに、catalina.out等のログにはVirusDetectExceptionが出力されているはずです。

ERROR [extensions.webscripts.AbstractRuntime] [http-bio-8080-exec-19] Exception from executeScript - redirecting to status template error: 10120029 Virus detected and cleaned up : eicar.com.txt
 jp.aegif.alfresco.sample.action.executer.VirusDetectException: 10120029 Virus detected and cleaned up : eicar.com.txt
 at jp.aegif.alfresco.sample.action.executer.VirusScanActionExecuter.executeImpl(VirusScanActionExecuter.java:68)
 ...

まとめ


テストはうまくいきましたでしょうか?以上でウイルススキャンを行うカスタムモジュールの基本的な部分ができたかと思います。実際に利用するにあたっては、さらに以下のようなことを考慮する必要があるかもしれません。
  • 1トランザクションで複数のファイル(ノード)が作成されるような場合には対応していない
    • ウイルススキャンアクション呼び出し部分に工夫が必要
  • clamscanはパフォーマンスが良くない
    • ウイルス定義を予めメモリに展開しておくclamdscanの利用
    • Clam AntiVirus以外のウイルススキャンツールの利用
  • ファイルアップロード時ではなく、アクションメニューから手動で実行したい
    • ウイルススキャンをActionExecuterとして実装しているので簡単に実現できます(何人かが既に実装して公開しています) 

No comments: