/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.nifi.processors.evtx; import com.google.common.annotations.VisibleForTesting; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.ReadsAttribute; import org.apache.nifi.annotation.behavior.ReadsAttributes; import org.apache.nifi.annotation.behavior.SideEffectFree; import org.apache.nifi.annotation.behavior.SupportsBatching; import org.apache.nifi.annotation.behavior.WritesAttribute; import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.flowfile.attributes.CoreAttributes; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processors.evtx.parser.ChunkHeader; import org.apache.nifi.processors.evtx.parser.FileHeader; import org.apache.nifi.processors.evtx.parser.FileHeaderFactory; import org.apache.nifi.processors.evtx.parser.MalformedChunkException; import org.apache.nifi.processors.evtx.parser.Record; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @SideEffectFree @EventDriven @SupportsBatching @InputRequirement(Requirement.INPUT_REQUIRED) @Tags({"logs", "windows", "event", "evtx", "message", "file"}) @CapabilityDescription("Parses the contents of a Windows Event Log file (evtx) and writes the resulting XML to the FlowFile") @ReadsAttributes({ @ReadsAttribute(attribute = "filename", description = "The filename of the evtx file") }) @WritesAttributes({ @WritesAttribute(attribute = "filename", description = "The output filename"), @WritesAttribute(attribute = "mime.type", description = "The output filetype (application/xml for success and failure relationships, original value for bad chunk and original relationships)"), }) public class ParseEvtx extends AbstractProcessor { public static final String RECORD = "Record"; public static final String CHUNK = "Chunk"; public static final String FILE = "File"; public static final String EVTX_EXTENSION = ".evtx"; public static final String XML_EXTENSION = ".xml"; @VisibleForTesting static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("Any FlowFile that was successfully converted from evtx to XML") .build(); @VisibleForTesting static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("Any FlowFile that encountered an exception during conversion will be transferred to this relationship with as much parsing as possible done") .build(); @VisibleForTesting static final Relationship REL_BAD_CHUNK = new Relationship.Builder() .name("bad chunk") .description("Any bad chunks of records will be transferred to this relationship in their original binary form") .build(); @VisibleForTesting static final Relationship REL_ORIGINAL = new Relationship.Builder() .name("original") .description("The unmodified input FlowFile will be transferred to this relationship") .build(); @VisibleForTesting static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE, REL_ORIGINAL, REL_BAD_CHUNK))); @VisibleForTesting static final PropertyDescriptor GRANULARITY = new PropertyDescriptor.Builder() .required(true) .name("granularity") .displayName("Granularity") .description("Output flow file for each Record, Chunk, or File encountered in the event log") .defaultValue(CHUNK) .allowableValues(RECORD, CHUNK, FILE) .build(); @VisibleForTesting static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = Collections.unmodifiableList(Arrays.asList(GRANULARITY)); private final FileHeaderFactory fileHeaderFactory; private final MalformedChunkHandler malformedChunkHandler; private final RootNodeHandlerFactory rootNodeHandlerFactory; private final ResultProcessor resultProcessor; public ParseEvtx() { this(FileHeader::new, new MalformedChunkHandler(REL_BAD_CHUNK), XmlRootNodeHandler::new, new ResultProcessor(REL_SUCCESS, REL_FAILURE)); } public ParseEvtx(FileHeaderFactory fileHeaderFactory, MalformedChunkHandler malformedChunkHandler, RootNodeHandlerFactory rootNodeHandlerFactory, ResultProcessor resultProcessor) { this.fileHeaderFactory = fileHeaderFactory; this.malformedChunkHandler = malformedChunkHandler; this.rootNodeHandlerFactory = rootNodeHandlerFactory; this.resultProcessor = resultProcessor; } protected String getName(String basename, Object chunkNumber, Object recordNumber, String extension) { StringBuilder stringBuilder = new StringBuilder(basename); if (chunkNumber != null) { stringBuilder.append("-chunk"); stringBuilder.append(chunkNumber); } if (recordNumber != null) { stringBuilder.append("-record"); stringBuilder.append(recordNumber); } stringBuilder.append(extension); return stringBuilder.toString(); } protected String getBasename(FlowFile flowFile, ComponentLog logger) { String basename = flowFile.getAttribute(CoreAttributes.FILENAME.key()); if (basename.endsWith(EVTX_EXTENSION)) { return basename.substring(0, basename.length() - EVTX_EXTENSION.length()); } else { logger.warn("Trying to parse file without .evtx extension {} from flowfile {}", new Object[]{basename, flowFile}); return basename; } } @Override public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { ComponentLog logger = getLogger(); final FlowFile flowFile = session.get(); if (flowFile == null) { return; } String basename = getBasename(flowFile, logger); String granularity = context.getProperty(GRANULARITY).getValue(); if (FILE.equals(granularity)) { // File granularity will emit a FlowFile for each input FlowFile original = session.clone(flowFile); AtomicReference<Exception> exceptionReference = new AtomicReference<>(null); FlowFile updated = session.write(flowFile, (in, out) -> { processFileGranularity(session, logger, original, basename, exceptionReference, in, out); }); session.transfer(original, REL_ORIGINAL); resultProcessor.process(session, logger, updated, exceptionReference.get(), getName(basename, null, null, XML_EXTENSION)); } else { session.read(flowFile, in -> { if (RECORD.equals(granularity)) { // Record granularity will emit a FlowFile for every record (event) processRecordGranularity(session, logger, flowFile, basename, in); } else if (CHUNK.equals(granularity)) { // Chunk granularity will emit a FlowFile for each chunk of the file processChunkGranularity(session, logger, flowFile, basename, in); } }); session.transfer(flowFile, REL_ORIGINAL); } } protected void processFileGranularity(ProcessSession session, ComponentLog componentLog, FlowFile original, String basename, AtomicReference<Exception> exceptionReference, InputStream in, OutputStream out) throws IOException { FileHeader fileHeader = fileHeaderFactory.create(in, componentLog); try (RootNodeHandler rootNodeHandler = rootNodeHandlerFactory.create(out)) { while (fileHeader.hasNext()) { try { ChunkHeader chunkHeader = fileHeader.next(); try { while (chunkHeader.hasNext()) { rootNodeHandler.handle(chunkHeader.next().getRootNode()); } } catch (IOException e) { malformedChunkHandler.handle(original, session, getName(basename, chunkHeader.getChunkNumber(), null, EVTX_EXTENSION), chunkHeader.getBinaryReader().getBytes()); exceptionReference.set(e); } } catch (MalformedChunkException e) { malformedChunkHandler.handle(original, session, getName(basename, e.getChunkNum(), null, EVTX_EXTENSION), e.getBadChunk()); } } } catch (IOException e) { exceptionReference.set(e); } } protected void processChunkGranularity(ProcessSession session, ComponentLog componentLog, FlowFile flowFile, String basename, InputStream in) throws IOException { FileHeader fileHeader = fileHeaderFactory.create(in, componentLog); while (fileHeader.hasNext()) { try { ChunkHeader chunkHeader = fileHeader.next(); FlowFile updated = session.create(flowFile); AtomicReference<Exception> exceptionReference = new AtomicReference<>(null); updated = session.write(updated, out -> { try (RootNodeHandler rootNodeHandler = rootNodeHandlerFactory.create(out)) { while (chunkHeader.hasNext()) { try { rootNodeHandler.handle(chunkHeader.next().getRootNode()); } catch (IOException e) { exceptionReference.set(e); break; } } } catch (IOException e) { exceptionReference.set(e); } }); Exception exception = exceptionReference.get(); resultProcessor.process(session, componentLog, updated, exception, getName(basename, chunkHeader.getChunkNumber(), null, XML_EXTENSION)); if (exception != null) { malformedChunkHandler.handle(flowFile, session, getName(basename, chunkHeader.getChunkNumber(), null, EVTX_EXTENSION), chunkHeader.getBinaryReader().getBytes()); } } catch (MalformedChunkException e) { malformedChunkHandler.handle(flowFile, session, getName(basename, e.getChunkNum(), null, EVTX_EXTENSION), e.getBadChunk()); } } } protected void processRecordGranularity(ProcessSession session, ComponentLog componentLog, FlowFile flowFile, String basename, InputStream in) throws IOException { FileHeader fileHeader = fileHeaderFactory.create(in, componentLog); while (fileHeader.hasNext()) { try { ChunkHeader chunkHeader = fileHeader.next(); while (chunkHeader.hasNext()) { FlowFile updated = session.create(flowFile); AtomicReference<Exception> exceptionReference = new AtomicReference<>(null); try { Record record = chunkHeader.next(); updated = session.write(updated, out -> { try (RootNodeHandler rootNodeHandler = rootNodeHandlerFactory.create(out)) { try { rootNodeHandler.handle(record.getRootNode()); } catch (IOException e) { exceptionReference.set(e); } } catch (IOException e) { exceptionReference.set(e); } }); resultProcessor.process(session, componentLog, updated, exceptionReference.get(), getName(basename, chunkHeader.getChunkNumber(), record.getRecordNum(), XML_EXTENSION)); } catch (Exception e) { exceptionReference.set(e); session.remove(updated); } if (exceptionReference.get() != null) { malformedChunkHandler.handle(flowFile, session, getName(basename, chunkHeader.getChunkNumber(), null, EVTX_EXTENSION), chunkHeader.getBinaryReader().getBytes()); } } } catch (MalformedChunkException e) { malformedChunkHandler.handle(flowFile, session, getName(basename, e.getChunkNum(), null, EVTX_EXTENSION), e.getBadChunk()); } } } @Override public Set<Relationship> getRelationships() { return RELATIONSHIPS; } @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return PROPERTY_DESCRIPTORS; } }