/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.core.plugins.plugins.base; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.IOUtils; import org.apache.xmlbeans.XmlException; import org.roda.core.common.PremisV3Utils; import org.roda.core.common.iterables.CloseableIterable; import org.roda.core.data.common.RodaConstants; import org.roda.core.data.common.RodaConstants.PreservationEventType; import org.roda.core.data.exceptions.AlreadyExistsException; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.exceptions.RODAException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.v2.LiteOptionalWithCause; import org.roda.core.data.v2.common.OptionalWithCause; import org.roda.core.data.v2.ip.AIP; import org.roda.core.data.v2.ip.AIPState; import org.roda.core.data.v2.ip.File; import org.roda.core.data.v2.ip.Representation; import org.roda.core.data.v2.ip.StoragePath; import org.roda.core.data.v2.ip.metadata.Fixity; import org.roda.core.data.v2.ip.metadata.LinkingIdentifier; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.PluginType; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.jobs.Report.PluginState; import org.roda.core.data.v2.risks.Risk; import org.roda.core.data.v2.risks.RiskIncidence; import org.roda.core.data.v2.risks.RiskIncidence.INCIDENCE_STATUS; import org.roda.core.data.v2.validation.ValidationException; import org.roda.core.data.v2.validation.ValidationIssue; import org.roda.core.data.v2.validation.ValidationReport; import org.roda.core.index.IndexService; import org.roda.core.model.ModelService; import org.roda.core.model.utils.ModelUtils; import org.roda.core.plugins.AbstractPlugin; import org.roda.core.plugins.Plugin; import org.roda.core.plugins.PluginException; import org.roda.core.plugins.RODAObjectProcessingLogic; import org.roda.core.plugins.orchestrate.SimpleJobPluginInfo; import org.roda.core.plugins.plugins.PluginHelper; import org.roda.core.storage.Binary; import org.roda.core.storage.StorageService; import org.roda.core.util.FileUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AIPCorruptionRiskAssessmentPlugin extends AbstractPlugin<AIP> { private static final Logger LOGGER = LoggerFactory.getLogger(AIPCorruptionRiskAssessmentPlugin.class); private static List<String> risks; static { risks = new ArrayList<>(); risks.add("urn:fixityplugin:r1"); } @Override public void init() { // do nothing } @Override public void shutdown() { // do nothing } @Override public String getName() { return "AIP corruption risk assessment"; } @Override public String getDescription() { return "Computes the fixity/checksum information of files inside an Archival Information Package (AIP) and verifies if this " + "information differs from the information stored in the preservation metadata (i.e. PREMIS objects). If so, it creates a " + "new risk called “File(s) corrupted due to hardware malfunction or human intervention“ and assigns the corrupted file to " + "that risk in the Risk register.\nWithin the repository, fixity checking is used to ensure that digital files have not been " + "affected by data rot or other digital preservation dangers. By itself, fixity checking does not ensure the preservation " + "of a digital file. Instead, it allows a repository to identify which corrupted files to replace with a clean copy from " + "the producer or from a backup."; } @Override public String getVersionImpl() { return "1.0"; } @Override public Report execute(IndexService index, ModelService model, StorageService storage, List<LiteOptionalWithCause> liteList) throws PluginException { return PluginHelper.processObjects(this, new RODAObjectProcessingLogic<AIP>() { @Override public void process(IndexService index, ModelService model, StorageService storage, Report report, Job cachedJob, SimpleJobPluginInfo jobPluginInfo, Plugin<AIP> plugin, AIP object) { processAIP(index, model, storage, report, jobPluginInfo, cachedJob, object); } }, index, model, storage, liteList); } private void processAIP(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, Job job, AIP aip) { boolean aipFailed = false; List<String> passedFiles = new ArrayList<>(); List<LinkingIdentifier> sources = new ArrayList<>(); ValidationReport validationReport = new ValidationReport(); for (Representation r : aip.getRepresentations()) { LOGGER.debug("Checking fixity for files in representation {} of AIP {}", r.getId(), aip.getId()); try { boolean recursive = true; CloseableIterable<OptionalWithCause<File>> allFiles = model.listFilesUnder(aip.getId(), r.getId(), recursive); for (OptionalWithCause<File> oFile : allFiles) { if (oFile.isPresent()) { File file = oFile.get(); if (!file.isDirectory()) { StoragePath storagePath = ModelUtils.getFileStoragePath(file); Binary currentFileBinary = storage.getBinary(storagePath); Binary premisFile = model.retrievePreservationFile(file); List<Fixity> fixities = PremisV3Utils.extractFixities(premisFile); sources.add(PluginHelper.getLinkingIdentifier(aip.getId(), file.getRepresentationId(), file.getPath(), file.getId(), RodaConstants.PRESERVATION_LINKING_OBJECT_SOURCE)); String fileEntry = file.getRepresentationId() + (file.getPath().isEmpty() ? "" : '/' + String.join("/", file.getPath())) + '/' + file.getId(); if (fixities != null) { boolean passedFixity = true; // get all necessary hash algorithms Set<String> algorithms = new HashSet<>(); for (Fixity f : fixities) { algorithms.add(f.getMessageDigestAlgorithm()); } // calculate hashes try { Map<String, String> checksums = FileUtility .checksums(currentFileBinary.getContent().createInputStream(), algorithms); for (Fixity f : fixities) { String checksum = checksums.get(f.getMessageDigestAlgorithm()); if (!f.getMessageDigest().trim().equalsIgnoreCase(checksum.trim())) { passedFixity = false; ValidationIssue issue = new ValidationIssue( fileEntry + " (Checksums: [" + f.getMessageDigest().trim() + ", " + checksum.trim() + "])"); validationReport.addIssue(issue); break; } } } catch (NoSuchAlgorithmException | IOException e) { passedFixity = false; ValidationIssue issue = new ValidationIssue("Could not check fixity: " + e.getMessage()); validationReport.addIssue(issue); LOGGER.debug("Could not check fixity", e); } if (passedFixity) { passedFiles.add(fileEntry); } else { aipFailed = true; createIncidence(model, file); } } } } } IOUtils.closeQuietly(allFiles); } catch (IOException | RODAException | XmlException e) { LOGGER.error("Error processing Representation {}", r.getId(), e); } } try { Report reportItem = PluginHelper.initPluginReportItem(this, aip.getId(), AIP.class, AIPState.ACTIVE); if (aipFailed) { reportItem.setPluginState(PluginState.FAILURE).setHtmlPluginDetails(true) .setPluginDetails(validationReport.toHtml(false, false, false, "Corrupted files and their checksums")); jobPluginInfo.incrementObjectsProcessedWithFailure(); PluginHelper.createPluginEvent(this, aip.getId(), model, index, sources, null, PluginState.FAILURE, validationReport.toHtml(false, false, false, "Corrupted files and their checksums"), true); } else { reportItem.setPluginState(PluginState.SUCCESS).setPluginDetails("Fixity checking ran successfully"); jobPluginInfo.incrementObjectsProcessedWithSuccess(); PluginHelper.createPluginEvent(this, aip.getId(), model, index, sources, null, PluginState.SUCCESS, "", true); } report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException | ValidationException | AlreadyExistsException e) { LOGGER.error("Could not create a Fixity Plugin event"); } } private void createIncidence(ModelService model, File file) throws RequestNotValidException, GenericException, AuthorizationDeniedException, AlreadyExistsException, NotFoundException { Risk risk = PluginHelper.createRiskIfNotExists(model, risks.get(0), getClass().getClassLoader()); RiskIncidence incidence = new RiskIncidence(); incidence.setDetectedOn(new Date()); incidence.setDetectedBy(this.getName()); incidence.setRiskId(risks.get(0)); incidence.setAipId(file.getAipId()); incidence.setRepresentationId(file.getRepresentationId()); incidence.setFilePath(file.getPath()); incidence.setFileId(file.getId()); incidence.setObjectClass(AIP.class.getSimpleName()); incidence.setStatus(INCIDENCE_STATUS.UNMITIGATED); incidence.setSeverity(risk.getPreMitigationSeverityLevel()); model.createRiskIncidence(incidence, false); } @Override public Plugin<AIP> cloneMe() { return new AIPCorruptionRiskAssessmentPlugin(); } @Override public PluginType getType() { return PluginType.AIP_TO_AIP; } @Override public boolean areParameterValuesValid() { return true; } @Override public PreservationEventType getPreservationEventType() { return PreservationEventType.FIXITY_CHECK; } @Override public String getPreservationEventDescription() { return "Computed the fixity information of files inside the AIP and compared to fixity information recorded in preservation metadata"; } @Override public String getPreservationEventSuccessMessage() { return "Fixity of files inside the AIP has been assessed and there was no evidence of corruption"; } @Override public String getPreservationEventFailureMessage() { return "Test of the fixity information of files inside AIPs failed"; } @Override public Report beforeAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { // do nothing return null; } @Override public Report afterAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { // do nothing return null; } @Override public List<String> getCategories() { return Arrays.asList(RodaConstants.PLUGIN_CATEGORY_RISK_MANAGEMENT); } @Override public List<Class<AIP>> getObjectClasses() { return Arrays.asList(AIP.class); } }