Commit 949c44cc authored by Matija Obreza's avatar Matija Obreza
Browse files

Virus scanning with remote clamd

parent 96a3a5da
...@@ -28,3 +28,20 @@ the BytesStorageService of the RepositoryServiceImpl. ...@@ -28,3 +28,20 @@ the BytesStorageService of the RepositoryServiceImpl.
s3.region=eu-west-1 s3.region=eu-west-1
Make sure these credentials have read, write and delete permissions on the selected bucket. Make sure these credentials have read, write and delete permissions on the selected bucket.
## Virus scanner
ClamAV is currently supported. Make sure you include the
`org.genesys.filerepository.service.aspect.VirusScannerAspect` in your application
configuration and then register the `VirusScanner` instance:
```java
@Bean
public VirusScanner virusScanner() {
ClamAVScanner clamav = new ClamAVScanner();
clamav.setClamAvHost(System.getenv(CLAMD_HOSTNAME));
clamav.setClamAvPort(Integer.parseInt(System.getenv(CLAMD_PORT)));
return clamav;
}
```
...@@ -177,5 +177,10 @@ ...@@ -177,5 +177,10 @@
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
<version>${tika.version}</version> <version>${tika.version}</version>
</dependency> </dependency>
<dependency>
<groupId>xyz.capybara</groupId>
<artifactId>clamav-client</artifactId>
<version>1.0.4</version>
</dependency>
</dependencies> </dependencies>
</project> </project>
/*
* Copyright 2017 Global Crop Diversity Trust, www.croptrust.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.service;
public class VirusFoundException extends Exception {
private static final long serialVersionUID = 3070028170493142618L;
public VirusFoundException(String message) {
super(message);
}
}
/*
* Copyright 2017 Global Crop Diversity Trust, www.croptrust.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.service;
import java.io.IOException;
/**
* The VirusScanner
*/
public interface VirusScanner {
void scan(byte[] bytes) throws VirusFoundException, IOException;
}
/*
* Copyright 2016 Global Crop Diversity Trust, www.croptrust.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.service.aspect;
import java.io.IOException;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.genesys.filerepository.service.VirusFoundException;
import org.genesys.filerepository.service.VirusScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Scans files with VirusScanner before storing to the byteService.
*
* @author Matija Obreza
*/
@Aspect
@Component
public class VirusScanAspect {
/** The Constant LOG. */
private final static Logger LOG = LoggerFactory.getLogger(VirusScanAspect.class);
/** The VirusScanner */
@Autowired(required = false)
private VirusScanner virusScanner;
@Before(value = "execution(void org.genesys.filerepository.service.BytesStorageService.upsert(..)) && args(path,filename,data)")
public void beforeBytesUpsert(String path, String filename, byte[] data) throws Throwable {
if (virusScanner != null) {
LOG.debug("Scaning data for viruses before storing path={} filename={}", path, filename);
try {
virusScanner.scan(data);
} catch (VirusFoundException e) {
throw new IOException(e);
}
} else {
LOG.warn("Virus scanner is not available, storing bytes without scanning.");
}
}
}
/*
* Copyright 2017 Global Crop Diversity Trust, www.croptrust.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.service.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.genesys.filerepository.service.VirusFoundException;
import org.genesys.filerepository.service.VirusScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import xyz.capybara.clamav.ClamavClient;
import xyz.capybara.clamav.commands.scan.result.ScanResult;
import xyz.capybara.clamav.commands.scan.result.ScanResult.Status;
import xyz.capybara.clamav.configuration.Platform;
import xyz.capybara.clamav.exceptions.ClamavException;
import xyz.capybara.clamav.exceptions.CommunicationException;
/**
* Scan bytes using ClamAV
*/
public class ClamAVScanner implements VirusScanner, InitializingBean {
private final static Logger LOG = LoggerFactory.getLogger(ClamAVScanner.class);
private String clamAvHost;
private int clamAvPort;
private ClamavClient client;
public void setClamAvHost(String clamAvHost) {
this.clamAvHost = clamAvHost;
}
public void setClamAvPort(int clamAvPort) {
this.clamAvPort = clamAvPort;
}
@Override
public void afterPropertiesSet() throws Exception {
try {
reconnect();
} catch (CommunicationException e) {
LOG.warn("Could not connect to clamd. Will retry on file upload...");
}
}
private void reconnect() throws ClamavException {
LOG.info("Connecting to clamd at {}:{}", clamAvHost, clamAvPort);
ClamavClient c = new ClamavClient(clamAvHost, clamAvPort, Platform.UNIX);
c.ping();
LOG.info("Connected to cland at {}:{} version={}", clamAvHost, clamAvPort, c.version());
synchronized (this) {
this.client = c;
}
}
@Override
public void scan(byte[] bytes) throws VirusFoundException, IOException {
synchronized (this) {
if (client == null) {
try {
reconnect();
} catch (ClamavException e) {
LOG.error(e.getMessage());
throw new IOException("Could not connect to clamd", e);
}
}
}
try (InputStream is = new ByteArrayInputStream(bytes)) {
ScanResult scanResult = client.scan(is);
if (scanResult.getStatus() == Status.VIRUS_FOUND) {
StringBuffer sb = new StringBuffer();
for (String key : scanResult.getFoundViruses().keySet()) {
for (String virus : scanResult.getFoundViruses().get(key)) {
LOG.error("In file={}: Found virus {}", key, virus);
sb.append(virus).append("; ");
}
}
throw new VirusFoundException(sb.toString());
} else {
LOG.debug("Data scanned and found virus-free");
}
} catch (CommunicationException e) {
LOG.warn(e.getMessage());
this.client = null;
} catch (ClamavException e) {
LOG.warn(e.getMessage(), e);
}
}
}
/*
* Copyright 2017 Global Crop Diversity Trust, www.croptrust.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.service;
import java.io.File;
import java.io.IOException;
import org.genesys.filerepository.service.aspect.VirusScanAspect;
import org.genesys.filerepository.service.impl.ClamAVScanner;
import org.genesys.filerepository.service.impl.FilesystemStorageServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.StringUtils;
/**
* Test the virus scanner
*
* <pre>
* # start clamavd
* docker run -d --name clamavd -p 3310:3310 dinkel/clamavd
* # set environment vars and run tests
* CLAMD_HOSTNAME=192.168.99.100 CLAMD_PORT=3310 mvn test
* </pre>
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { VirusScannerTest.Config.class })
public class VirusScannerTest {
@Configuration
@EnableAspectJAutoProxy
public static class Config {
private static final String CLAMD_PORT = "CLAMD_PORT";
private static final String CLAMD_HOSTNAME = "CLAMD_HOSTNAME";
/**
* Bytes storage service.
*
* @return the bytes storage service
*/
@Bean(name = "bytesStorageService")
public BytesStorageService bytesStorageService() {
final File repoDir = new File("data");
final FilesystemStorageServiceImpl storageService = new FilesystemStorageServiceImpl();
storageService.setRepositoryBaseDirectory(repoDir);
return storageService;
}
@Bean
public VirusScanAspect virusScanAspect() {
return new VirusScanAspect();
}
@Bean
public VirusScanner virusScanner() {
ClamAVScanner clamav = new ClamAVScanner();
String clamdHostname = System.getenv(CLAMD_HOSTNAME);
if (StringUtils.isEmpty(clamdHostname)) {
System.err.println("Not initializing Virus scanner, CLAMD_HOSTNAME environment var not set");
return null;
}
String clamdPort = System.getenv(CLAMD_PORT);
clamav.setClamAvHost(clamdHostname);
clamav.setClamAvPort(Integer.parseInt(clamdPort));
return clamav;
}
}
/** The Constant SOME_BYTES. */
private static final byte[] SOME_BYTES = "filecontents".getBytes();
/** The Constant EMPTY_BYTES. */
private static final byte[] EMPTY_BYTES = new byte[0];
private static final byte[] EICAR_TEST = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*".getBytes();
@Autowired(required = false)
private VirusScanner virusScanner;
@Autowired
private BytesStorageService bytesStorageService;
@Test
public void testVirusScannerNoVirusNull() throws VirusFoundException, IOException {
if (virusScanner != null) {
virusScanner.scan(EMPTY_BYTES);
}
}
@Test
public void testVirusScannerNoVirusSome() throws VirusFoundException, IOException {
if (virusScanner != null) {
virusScanner.scan(SOME_BYTES);
}
}
@Test(expected = VirusFoundException.class)
public void testVirusScannerStandardVirus() throws VirusFoundException, IOException {
if (virusScanner != null) {
virusScanner.scan(EICAR_TEST);
} else {
throw new VirusFoundException("Scanner not available, so fake it!");
}
}
@Test(expected = IOException.class)
public void testVirusScannerStandardVirus2() throws IOException {
if (virusScanner != null) {
bytesStorageService.upsert("test", "file", EICAR_TEST);
} else {
throw new IOException("Scanner not available, so fake it!");
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment