/* * 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 org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.flowfile.attributes.CoreAttributes; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.io.OutputStreamCallback; 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 org.apache.nifi.processors.evtx.parser.bxml.RootNode; import org.apache.nifi.util.MockFlowFile; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.XMLStreamException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class ParseEvtxTest { public static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); public static final String USER_DATA = "UserData"; public static final String EVENT_DATA = "EventData"; public static final Set DATA_TAGS = new HashSet<>(Arrays.asList(EVENT_DATA, USER_DATA)); @Mock FileHeaderFactory fileHeaderFactory; @Mock MalformedChunkHandler malformedChunkHandler; @Mock RootNodeHandlerFactory rootNodeHandlerFactory; @Mock ResultProcessor resultProcessor; @Mock ComponentLog componentLog; @Mock InputStream in; @Mock OutputStream out; @Mock FileHeader fileHeader; ParseEvtx parseEvtx; @Before public void setup() throws XMLStreamException, IOException { parseEvtx = new ParseEvtx(fileHeaderFactory, malformedChunkHandler, rootNodeHandlerFactory, resultProcessor); when(fileHeaderFactory.create(in, componentLog)).thenReturn(fileHeader); } @Test public void testGetNameFile() { String basename = "basename"; assertEquals(basename + ".xml", parseEvtx.getName(basename, null, null, ParseEvtx.XML_EXTENSION)); } @Test public void testGetNameFileChunk() { String basename = "basename"; assertEquals(basename + "-chunk1.xml", parseEvtx.getName(basename, 1, null, ParseEvtx.XML_EXTENSION)); } @Test public void testGetNameFileChunkRecord() { String basename = "basename"; assertEquals(basename + "-chunk1-record2.xml", parseEvtx.getName(basename, 1, 2, ParseEvtx.XML_EXTENSION)); } @Test public void testGetBasenameEvtxExtension() { String basename = "basename"; FlowFile flowFile = mock(FlowFile.class); when(flowFile.getAttribute(CoreAttributes.FILENAME.key())).thenReturn(basename + ".evtx"); assertEquals(basename, parseEvtx.getBasename(flowFile, componentLog)); verifyNoMoreInteractions(componentLog); } @Test public void testGetBasenameExtension() { String basename = "basename.wrongextension"; FlowFile flowFile = mock(FlowFile.class); ComponentLog componentLog = mock(ComponentLog.class); when(flowFile.getAttribute(CoreAttributes.FILENAME.key())).thenReturn(basename); assertEquals(basename, parseEvtx.getBasename(flowFile, componentLog)); verify(componentLog).warn(anyString(), isA(Object[].class)); } @Test public void testGetRelationships() { assertEquals(ParseEvtx.RELATIONSHIPS, parseEvtx.getRelationships()); } @Test public void testGetSupportedPropertyDescriptors() { assertEquals(ParseEvtx.PROPERTY_DESCRIPTORS, parseEvtx.getSupportedPropertyDescriptors()); } @Test public void testProcessFileGranularity() throws IOException, MalformedChunkException, XMLStreamException { String basename = "basename"; int chunkNum = 5; int offset = 10001; byte[] badChunk = {8}; RootNodeHandler rootNodeHandler = mock(RootNodeHandler.class); when(rootNodeHandlerFactory.create(out)).thenReturn(rootNodeHandler); ChunkHeader chunkHeader1 = mock(ChunkHeader.class); ChunkHeader chunkHeader2 = mock(ChunkHeader.class); Record record1 = mock(Record.class); Record record2 = mock(Record.class); Record record3 = mock(Record.class); RootNode rootNode1 = mock(RootNode.class); RootNode rootNode2 = mock(RootNode.class); RootNode rootNode3 = mock(RootNode.class); ProcessSession session = mock(ProcessSession.class); FlowFile flowFile = mock(FlowFile.class); AtomicReference<Exception> reference = new AtomicReference<>(); MalformedChunkException malformedChunkException = new MalformedChunkException("Test", null, offset, chunkNum, badChunk); when(record1.getRootNode()).thenReturn(rootNode1); when(record2.getRootNode()).thenReturn(rootNode2); when(record3.getRootNode()).thenReturn(rootNode3); when(fileHeader.hasNext()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); when(fileHeader.next()).thenThrow(malformedChunkException).thenReturn(chunkHeader1).thenReturn(chunkHeader2).thenReturn(null); when(chunkHeader1.hasNext()).thenReturn(true).thenReturn(false); when(chunkHeader1.next()).thenReturn(record1).thenReturn(null); when(chunkHeader2.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false); when(chunkHeader2.next()).thenReturn(record2).thenReturn(record3).thenReturn(null); parseEvtx.processFileGranularity(session, componentLog, flowFile, basename, reference, in, out); verify(malformedChunkHandler).handle(flowFile, session, parseEvtx.getName(basename, chunkNum, null, ParseEvtx.EVTX_EXTENSION), badChunk); verify(rootNodeHandler).handle(rootNode1); verify(rootNodeHandler).handle(rootNode2); verify(rootNodeHandler).handle(rootNode3); verify(rootNodeHandler).close(); } @Test public void testProcessChunkGranularity() throws IOException, MalformedChunkException, XMLStreamException { String basename = "basename"; int chunkNum = 5; int offset = 10001; byte[] badChunk = {8}; RootNodeHandler rootNodeHandler1 = mock(RootNodeHandler.class); RootNodeHandler rootNodeHandler2 = mock(RootNodeHandler.class); OutputStream out2 = mock(OutputStream.class); when(rootNodeHandlerFactory.create(out)).thenReturn(rootNodeHandler1); when(rootNodeHandlerFactory.create(out2)).thenReturn(rootNodeHandler2); ChunkHeader chunkHeader1 = mock(ChunkHeader.class); ChunkHeader chunkHeader2 = mock(ChunkHeader.class); Record record1 = mock(Record.class); Record record2 = mock(Record.class); Record record3 = mock(Record.class); RootNode rootNode1 = mock(RootNode.class); RootNode rootNode2 = mock(RootNode.class); RootNode rootNode3 = mock(RootNode.class); ProcessSession session = mock(ProcessSession.class); FlowFile flowFile = mock(FlowFile.class); FlowFile created1 = mock(FlowFile.class); FlowFile updated1 = mock(FlowFile.class); FlowFile created2 = mock(FlowFile.class); FlowFile updated2 = mock(FlowFile.class); MalformedChunkException malformedChunkException = new MalformedChunkException("Test", null, offset, chunkNum, badChunk); when(session.create(flowFile)).thenReturn(created1).thenReturn(created2).thenReturn(null); when(session.write(eq(created1), any(OutputStreamCallback.class))).thenAnswer(invocation -> { ((OutputStreamCallback) invocation.getArguments()[1]).process(out); return updated1; }); when(session.write(eq(created2), any(OutputStreamCallback.class))).thenAnswer(invocation -> { ((OutputStreamCallback) invocation.getArguments()[1]).process(out2); return updated2; }); when(record1.getRootNode()).thenReturn(rootNode1); when(record2.getRootNode()).thenReturn(rootNode2); when(record3.getRootNode()).thenReturn(rootNode3); when(fileHeader.hasNext()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); when(fileHeader.next()).thenThrow(malformedChunkException).thenReturn(chunkHeader1).thenReturn(chunkHeader2).thenReturn(null); when(chunkHeader1.hasNext()).thenReturn(true).thenReturn(false); when(chunkHeader1.next()).thenReturn(record1).thenReturn(null); when(chunkHeader2.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false); when(chunkHeader2.next()).thenReturn(record2).thenReturn(record3).thenReturn(null); parseEvtx.processChunkGranularity(session, componentLog, flowFile, basename, in); verify(malformedChunkHandler).handle(flowFile, session, parseEvtx.getName(basename, chunkNum, null, ParseEvtx.EVTX_EXTENSION), badChunk); verify(rootNodeHandler1).handle(rootNode1); verify(rootNodeHandler1).close(); verify(rootNodeHandler2).handle(rootNode2); verify(rootNodeHandler2).handle(rootNode3); verify(rootNodeHandler2).close(); } @Test public void testProcess1RecordGranularity() throws IOException, MalformedChunkException, XMLStreamException { String basename = "basename"; int chunkNum = 5; int offset = 10001; byte[] badChunk = {8}; RootNodeHandler rootNodeHandler1 = mock(RootNodeHandler.class); RootNodeHandler rootNodeHandler2 = mock(RootNodeHandler.class); RootNodeHandler rootNodeHandler3 = mock(RootNodeHandler.class); OutputStream out2 = mock(OutputStream.class); OutputStream out3 = mock(OutputStream.class); when(rootNodeHandlerFactory.create(out)).thenReturn(rootNodeHandler1); when(rootNodeHandlerFactory.create(out2)).thenReturn(rootNodeHandler2); when(rootNodeHandlerFactory.create(out3)).thenReturn(rootNodeHandler3); ChunkHeader chunkHeader1 = mock(ChunkHeader.class); ChunkHeader chunkHeader2 = mock(ChunkHeader.class); Record record1 = mock(Record.class); Record record2 = mock(Record.class); Record record3 = mock(Record.class); RootNode rootNode1 = mock(RootNode.class); RootNode rootNode2 = mock(RootNode.class); RootNode rootNode3 = mock(RootNode.class); ProcessSession session = mock(ProcessSession.class); FlowFile flowFile = mock(FlowFile.class); FlowFile created1 = mock(FlowFile.class); FlowFile updated1 = mock(FlowFile.class); FlowFile created2 = mock(FlowFile.class); FlowFile updated2 = mock(FlowFile.class); FlowFile created3 = mock(FlowFile.class); FlowFile updated3 = mock(FlowFile.class); MalformedChunkException malformedChunkException = new MalformedChunkException("Test", null, offset, chunkNum, badChunk); when(session.create(flowFile)).thenReturn(created1).thenReturn(created2).thenReturn(created3).thenReturn(null); when(session.write(eq(created1), any(OutputStreamCallback.class))).thenAnswer(invocation -> { ((OutputStreamCallback) invocation.getArguments()[1]).process(out); return updated1; }); when(session.write(eq(created2), any(OutputStreamCallback.class))).thenAnswer(invocation -> { ((OutputStreamCallback) invocation.getArguments()[1]).process(out2); return updated2; }); when(session.write(eq(created3), any(OutputStreamCallback.class))).thenAnswer(invocation -> { ((OutputStreamCallback) invocation.getArguments()[1]).process(out3); return updated3; }); when(record1.getRootNode()).thenReturn(rootNode1); when(record2.getRootNode()).thenReturn(rootNode2); when(record3.getRootNode()).thenReturn(rootNode3); when(fileHeader.hasNext()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); when(fileHeader.next()).thenThrow(malformedChunkException).thenReturn(chunkHeader1).thenReturn(chunkHeader2).thenReturn(null); when(chunkHeader1.hasNext()).thenReturn(true).thenReturn(false); when(chunkHeader1.next()).thenReturn(record1).thenReturn(null); when(chunkHeader2.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false); when(chunkHeader2.next()).thenReturn(record2).thenReturn(record3).thenReturn(null); parseEvtx.processRecordGranularity(session, componentLog, flowFile, basename, in); verify(malformedChunkHandler).handle(flowFile, session, parseEvtx.getName(basename, chunkNum, null, ParseEvtx.EVTX_EXTENSION), badChunk); verify(rootNodeHandler1).handle(rootNode1); verify(rootNodeHandler1).close(); verify(rootNodeHandler2).handle(rootNode2); verify(rootNodeHandler2).close(); verify(rootNodeHandler3).handle(rootNode3); verify(rootNodeHandler3).close(); } @Test public void fileGranularityLifecycleTest() throws IOException, ParserConfigurationException, SAXException { String baseName = "testFileName"; String name = baseName + ".evtx"; TestRunner testRunner = TestRunners.newTestRunner(ParseEvtx.class); testRunner.setProperty(ParseEvtx.GRANULARITY, ParseEvtx.FILE); Map<String, String> attributes = new HashMap<>(); attributes.put(CoreAttributes.FILENAME.key(), name); testRunner.enqueue(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx"), attributes); testRunner.run(); List<MockFlowFile> originalFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_ORIGINAL); assertEquals(1, originalFlowFiles.size()); MockFlowFile originalFlowFile = originalFlowFiles.get(0); originalFlowFile.assertAttributeEquals(CoreAttributes.FILENAME.key(), name); originalFlowFile.assertContentEquals(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx")); // We expect the same bad chunks no matter the granularity List<MockFlowFile> badChunkFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_BAD_CHUNK); assertEquals(2, badChunkFlowFiles.size()); badChunkFlowFiles.get(0).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 1, null, ParseEvtx.EVTX_EXTENSION)); badChunkFlowFiles.get(1).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 2, null, ParseEvtx.EVTX_EXTENSION)); List<MockFlowFile> failureFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_FAILURE); assertEquals(1, failureFlowFiles.size()); validateFlowFiles(failureFlowFiles); // We expect the same number of records to come out no matter the granularity assertEquals(960, validateFlowFiles(failureFlowFiles)); // Whole file fails if there is a failure parsing List<MockFlowFile> successFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_SUCCESS); assertEquals(0, successFlowFiles.size()); } @Test public void chunkGranularityLifecycleTest() throws IOException, ParserConfigurationException, SAXException { String baseName = "testFileName"; String name = baseName + ".evtx"; TestRunner testRunner = TestRunners.newTestRunner(ParseEvtx.class); Map<String, String> attributes = new HashMap<>(); attributes.put(CoreAttributes.FILENAME.key(), name); testRunner.enqueue(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx"), attributes); testRunner.run(); List<MockFlowFile> originalFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_ORIGINAL); assertEquals(1, originalFlowFiles.size()); MockFlowFile originalFlowFile = originalFlowFiles.get(0); originalFlowFile.assertAttributeEquals(CoreAttributes.FILENAME.key(), name); originalFlowFile.assertContentEquals(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx")); // We expect the same bad chunks no matter the granularity List<MockFlowFile> badChunkFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_BAD_CHUNK); assertEquals(2, badChunkFlowFiles.size()); badChunkFlowFiles.get(0).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 1, null, ParseEvtx.EVTX_EXTENSION)); badChunkFlowFiles.get(1).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 2, null, ParseEvtx.EVTX_EXTENSION)); List<MockFlowFile> failureFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_FAILURE); assertEquals(1, failureFlowFiles.size()); List<MockFlowFile> successFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_SUCCESS); assertEquals(8, successFlowFiles.size()); // We expect the same number of records to come out no matter the granularity assertEquals(960, validateFlowFiles(successFlowFiles) + validateFlowFiles(failureFlowFiles)); } @Test public void recordGranularityLifecycleTest() throws IOException, ParserConfigurationException, SAXException { String baseName = "testFileName"; String name = baseName + ".evtx"; TestRunner testRunner = TestRunners.newTestRunner(ParseEvtx.class); testRunner.setProperty(ParseEvtx.GRANULARITY, ParseEvtx.RECORD); Map<String, String> attributes = new HashMap<>(); attributes.put(CoreAttributes.FILENAME.key(), name); testRunner.enqueue(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx"), attributes); testRunner.run(); List<MockFlowFile> originalFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_ORIGINAL); assertEquals(1, originalFlowFiles.size()); MockFlowFile originalFlowFile = originalFlowFiles.get(0); originalFlowFile.assertAttributeEquals(CoreAttributes.FILENAME.key(), name); originalFlowFile.assertContentEquals(this.getClass().getClassLoader().getResourceAsStream("application-logs.evtx")); // We expect the same bad chunks no matter the granularity List<MockFlowFile> badChunkFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_BAD_CHUNK); assertEquals(2, badChunkFlowFiles.size()); badChunkFlowFiles.get(0).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 1, null, ParseEvtx.EVTX_EXTENSION)); badChunkFlowFiles.get(1).assertAttributeEquals(CoreAttributes.FILENAME.key(), parseEvtx.getName(baseName, 2, null, ParseEvtx.EVTX_EXTENSION)); List<MockFlowFile> failureFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_FAILURE); assertEquals(0, failureFlowFiles.size()); // Whole file fails if there is a failure parsing List<MockFlowFile> successFlowFiles = testRunner.getFlowFilesForRelationship(ParseEvtx.REL_SUCCESS); assertEquals(960, successFlowFiles.size()); // We expect the same number of records to come out no matter the granularity assertEquals(960, validateFlowFiles(successFlowFiles)); } private int validateFlowFiles(List<MockFlowFile> successFlowFiles) throws SAXException, IOException, ParserConfigurationException { assertTrue(successFlowFiles.size() > 0); int totalSize = 0; for (MockFlowFile successFlowFile : successFlowFiles) { // Verify valid XML output Document document = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(new ByteArrayInputStream(successFlowFile.toByteArray())); Element documentElement = document.getDocumentElement(); assertEquals(XmlRootNodeHandler.EVENTS, documentElement.getTagName()); NodeList eventNodes = documentElement.getChildNodes(); int length = eventNodes.getLength(); totalSize += length; assertTrue(length > 0); for (int i = 0; i < length; i++) { Node eventNode = eventNodes.item(i); assertEquals("Event", eventNode.getNodeName()); NodeList eventChildNodes = eventNode.getChildNodes(); assertEquals(2, eventChildNodes.getLength()); Node systemNode = eventChildNodes.item(0); assertEquals("System", systemNode.getNodeName()); NodeList childNodes = systemNode.getChildNodes(); String userId = ""; for (int i1 = 0; i1 < childNodes.getLength(); i1++) { Node systemChild = childNodes.item(i1); if ("Security".equals(systemChild.getNodeName())) { userId = systemChild.getAttributes().getNamedItem("UserID").getNodeValue(); } } Node eventDataNode = eventChildNodes.item(1); String eventDataNodeNodeName = eventDataNode.getNodeName(); assertTrue(DATA_TAGS.contains(eventDataNodeNodeName)); assertTrue(userId.length() == 0 || userId.startsWith("S-")); } } return totalSize; } }