You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by tu...@apache.org on 2022/09/19 17:51:27 UTC

[nifi] branch main updated: NIFI-10427 - Added ListBoxFile and FetchBoxFile processors

This is an automated email from the ASF dual-hosted git repository.

turcsanyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new dd9739a28c NIFI-10427 - Added ListBoxFile and FetchBoxFile processors
dd9739a28c is described below

commit dd9739a28ce87fd5f8508367eb3b920203f8b872
Author: Tamas Palfy <tp...@apache.org>
AuthorDate: Thu Sep 1 21:51:19 2022 +0200

    NIFI-10427 - Added ListBoxFile and FetchBoxFile processors
    
    This closes #6355.
    
    Signed-off-by: Peter Turcsanyi <tu...@apache.org>
---
 nifi-assembly/NOTICE                               |   5 +
 nifi-assembly/pom.xml                              |  18 ++
 .../nifi/util/StandardProcessorTestRunner.java     |  35 +++
 .../main/java/org/apache/nifi/util/TestRunner.java |  22 ++
 .../nifi-box-bundle/nifi-box-nar/pom.xml           |  78 +++++++
 .../src/main/resources/META-INF/LICENSE            | 202 ++++++++++++++++
 .../src/main/resources/META-INF/NOTICE             |  33 +++
 .../nifi-box-bundle/nifi-box-processors/pom.xml    |  71 ++++++
 .../apache/nifi/processors/box/BoxFileInfo.java    | 201 ++++++++++++++++
 .../nifi/processors/box/BoxFlowFileAttribute.java  |  43 ++++
 .../apache/nifi/processors/box/FetchBoxFile.java   | 153 ++++++++++++
 .../apache/nifi/processors/box/ListBoxFile.java    | 260 +++++++++++++++++++++
 .../services/org.apache.nifi.processor.Processor   |  16 ++
 .../nifi/processors/box/AbstractBoxFileIT.java     | 137 +++++++++++
 .../apache/nifi/processors/box/FetchBoxFileIT.java | 144 ++++++++++++
 .../apache/nifi/processors/box/ListBoxFileIT.java  | 156 +++++++++++++
 .../nifi/processors/box/ListBoxFileSimpleTest.java | 121 ++++++++++
 .../processors/box/ListBoxFileTestRunnerTest.java  | 180 ++++++++++++++
 .../processors/box/SimpleListBoxFileTestTrait.java |  92 ++++++++
 .../nifi-box-services-api-nar/pom.xml              |  50 ++++
 .../src/main/resources/META-INF/LICENSE            | 259 ++++++++++++++++++++
 .../src/main/resources/META-INF/NOTICE             |  45 ++++
 .../nifi-box-bundle/nifi-box-services-api/pom.xml  |  39 ++++
 .../box/controllerservices/BoxClientService.java   |  36 +++
 .../nifi-box-bundle/nifi-box-services-nar/pom.xml  |  51 ++++
 .../src/main/resources/META-INF/LICENSE            | 202 ++++++++++++++++
 .../src/main/resources/META-INF/NOTICE             |  33 +++
 .../nifi-box-bundle/nifi-box-services/pom.xml      |  52 +++++
 .../JsonConfigBasedBoxClientService.java           | 156 +++++++++++++
 .../org.apache.nifi.controller.ControllerService   |  15 ++
 .../additionalDetails.html                         |  49 ++++
 .../JsonConfigBasedBoxClientServiceIT.java         |  87 +++++++
 ...nConfigBasedBoxClientServiceTestRunnerTest.java |  81 +++++++
 nifi-nar-bundles/nifi-box-bundle/pom.xml           |  37 +++
 nifi-nar-bundles/pom.xml                           |   1 +
 35 files changed, 3160 insertions(+)

diff --git a/nifi-assembly/NOTICE b/nifi-assembly/NOTICE
index ad8350e3c5..714847e698 100644
--- a/nifi-assembly/NOTICE
+++ b/nifi-assembly/NOTICE
@@ -2262,6 +2262,11 @@ The following binary components are provided under the Apache Software License v
       The Dropbox Project
       Copyright 2013-2021 Dropbox Inc.
 
+  (ASLv2) The Box SDK for Java
+    The following NOTICE information applies:
+      The Box SDK for Java
+      Copyright 2019 Box, Inc. All rights reserved.
+
 ************************
 Common Development and Distribution License 1.1
 ************************
diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index 09da408a2e..e7410cbbfc 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -862,6 +862,24 @@ language governing permissions and limitations under the License. -->
             <version>1.18.0-SNAPSHOT</version>
             <type>nar</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-web-client-provider-service-nar</artifactId>
diff --git a/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java b/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
index a92cb34575..0aca359bdb 100644
--- a/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
+++ b/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
@@ -69,8 +69,10 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class StandardProcessorTestRunner implements TestRunner {
 
@@ -349,6 +351,39 @@ public class StandardProcessorTestRunner implements TestRunner {
         }
     }
 
+    @Override
+    public void assertAttributes(
+        Relationship relationship,
+        Set<String> checkedAttributeNames,
+        Set<Map<String, String>> expectedAttributes
+    ) {
+        assertTransferCount(relationship, expectedAttributes.size());
+        List<MockFlowFile> flowFiles = getFlowFilesForRelationship(relationship);
+
+        Set<Map<String, String>> actualAttributes = flowFiles.stream()
+            .map(flowFile -> flowFile.getAttributes().entrySet().stream()
+                .filter(attributeNameAndValue -> checkedAttributeNames.contains(attributeNameAndValue.getKey()))
+                .filter(entry -> entry.getKey() != null && entry.getValue() != null)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
+
+            )
+            .collect(Collectors.toSet());
+
+        assertEquals(expectedAttributes, actualAttributes);
+    }
+
+    @Override
+    public void assertContents(Relationship relationship, List<String> expectedContent) {
+        assertTransferCount(relationship, expectedContent.size());
+        List<MockFlowFile> flowFiles = getFlowFilesForRelationship(relationship);
+
+        List<String> actualContent = flowFiles.stream()
+            .map(flowFile -> flowFile.getContent())
+            .collect(Collectors.toList());
+
+        assertEquals(expectedContent, actualContent);
+    }
+
     @Override
     public void assertAllFlowFilesTransferred(final Relationship relationship, final int count) {
         assertAllFlowFilesTransferred(relationship);
diff --git a/nifi-mock/src/main/java/org/apache/nifi/util/TestRunner.java b/nifi-mock/src/main/java/org/apache/nifi/util/TestRunner.java
index fde8c6a8b1..7b01ed9709 100644
--- a/nifi-mock/src/main/java/org/apache/nifi/util/TestRunner.java
+++ b/nifi-mock/src/main/java/org/apache/nifi/util/TestRunner.java
@@ -36,6 +36,7 @@ import java.io.InputStream;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Predicate;
 
 public interface TestRunner {
@@ -320,6 +321,27 @@ public interface TestRunner {
      */
     void assertAllFlowFiles(Relationship relationship, FlowFileValidator validator);
 
+    /**
+     * Asserts that flowfiles on a given relationship have a certain set of attributes.
+     *
+     * @param relationship The relationship on which to check the attributes of flowfiles
+     * @param checkedAttributeNames The names of attributes that should be checked
+     * @param expectedAttributes The expected attributes of all flowfiles
+     */
+    void assertAttributes(
+        Relationship relationship,
+        Set<String> checkedAttributeNames,
+        Set<Map<String, String>> expectedAttributes
+    );
+
+    /**
+     * Asserts that flowfiles on a given relationship have certain contents.
+     *
+     * @param relationship The relationship on which to check the contents of flowfiles
+     * @param expectedContent The expected contents of all flowfiles
+     */
+    public void assertContents(Relationship relationship, List<String> expectedContent);
+
     /**
      * Assert that the number of FlowFiles transferred to the given relationship
      * is equal to the given count
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/pom.xml
new file mode 100644
index 0000000000..62d2ee430c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/pom.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-box-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-nar</artifactId>
+    <packaging>nar</packaging>
+    <properties>
+        <maven.javadoc.skip>true</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-processors</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>license-maven-plugin</artifactId>
+                <version>1.8</version>
+                <configuration>
+                    <!--
+                        mvn clean license:add-third-party license:download-licenses
+                    -->
+                    <projectName>NiFi Box Processors</projectName>
+                    <licenseName>Commercial License</licenseName>
+                    <organizationName>NiFi</organizationName>
+
+                    <licenseFile>${basedir}/src/main/resources/META-INF/LICENSE</licenseFile>
+                    <useMissingFile>true</useMissingFile>
+
+                    <outputDirectory>${project.basedir}/target/classes/META-INF/licenses</outputDirectory>
+                    <licensesOutputDirectory>target/classes/META-INF/licenses/text</licensesOutputDirectory>
+
+                    <includedScopes>compile</includedScopes>
+                    <excludedScopes>test|provided|runtime|system</excludedScopes>
+                    <excludedGroups>org.apache.nifi</excludedGroups>
+                    <licenseMerges>
+                        <licenseMerge>Apache License 2.0|The Apache Software License|Version 2.0,Apache License, Version
+                            2.0|The Apache Software License, Version 2.0|Apache License, Version 2.0|Apache 2
+                        </licenseMerge>
+                    </licenseMerges>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000000..21ca241f55
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-nar/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,33 @@
+nifi-box-nar
+Copyright 2015-2020 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+******************
+Apache Software License v2
+******************
+
+The following binary components are provided under the Apache Software License v2
+
+  (ASLv2) Jackson Databind
+    The following NOTICE information applies:
+      Jackson Databind
+      Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi
+
+      Jackson is a high-performance, Free/Open Source JSON processing library.
+      It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+      been in development since 2007.
+
+      It is currently developed by a community of developers, as well as supported
+      commercially by FasterXML.com.
+
+  (ASLv2) Jackson Core
+    The following NOTICE information applies:
+      Jackson Core
+      Copyright Tatu Saloranta
+
+  (ASLv2) Jackson Annotations
+    The following NOTICE information applies:
+      Jackson Annotations
+      Copyright (c) 2019 Tatu Saloranta
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
new file mode 100644
index 0000000000..3d45235104
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-box-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-processors</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-listed-entity</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-mock</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record-serialization-service-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-schema-registry-service-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-distributed-cache-client-service-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record-serialization-services</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
new file mode 100644
index 0000000000..38cddb65f0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFileInfo.java
@@ -0,0 +1,201 @@
+/*
+ * 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.box;
+
+import org.apache.nifi.processor.util.list.ListableEntity;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class BoxFileInfo implements ListableEntity {
+    public static final String ID = "box.id";
+    public static final String FILENAME = "filename";
+    public static final String PATH = "path";
+    public static final String SIZE = "box.size";
+    public static final String TIMESTAMP = "box.timestamp";
+
+    private  static final RecordSchema SCHEMA;
+
+    static {
+        final List<RecordField> recordFields = new ArrayList<>();
+
+        recordFields.add(new RecordField(ID, RecordFieldType.STRING.getDataType(), false));
+        recordFields.add(new RecordField(FILENAME, RecordFieldType.STRING.getDataType(), false));
+        recordFields.add(new RecordField(PATH, RecordFieldType.STRING.getDataType(), false));
+        recordFields.add(new RecordField(SIZE, RecordFieldType.LONG.getDataType(), false));
+        recordFields.add(new RecordField(TIMESTAMP, RecordFieldType.LONG.getDataType(), false));
+
+        SCHEMA = new SimpleRecordSchema(recordFields);
+    }
+
+    private final String id;
+    private final String fileName;
+    private final String path;
+    private final long size;
+    private final long createdTime;
+    private final long modifiedTime;
+
+    public String getId() {
+        return id;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public long getCreatedTime() {
+        return createdTime;
+    }
+
+    public long getModifiedTime() {
+        return modifiedTime;
+    }
+
+    @Override
+    public Record toRecord() {
+        final Map<String, Object> values = new HashMap<>();
+
+        values.put(ID, getId());
+        values.put(FILENAME, getName());
+        values.put(PATH, getPath());
+        values.put(SIZE, getSize());
+        values.put(TIMESTAMP, getTimestamp());
+
+        return new MapRecord(SCHEMA, values);
+    }
+
+    public static RecordSchema getRecordSchema() {
+        return SCHEMA;
+    }
+
+    public static final class Builder {
+        private String id;
+        private String fileName;
+        private String path;
+        private long size;
+        private long createdTime;
+        private long modifiedTime;
+
+        public Builder id(String id) {
+            this.id = id;
+            return this;
+        }
+
+        public Builder fileName(String fileName) {
+            this.fileName = fileName;
+            return this;
+        }
+
+        public Builder path(String path) {
+            this.path = path;
+            return this;
+        }
+
+        public Builder size(long size) {
+            this.size = size;
+            return this;
+        }
+
+        public Builder createdTime(long createdTime) {
+            this.createdTime = createdTime;
+            return this;
+        }
+
+        public Builder modifiedTime(long modifiedTime) {
+            this.modifiedTime = modifiedTime;
+            return this;
+        }
+
+        public BoxFileInfo build() {
+            return new BoxFileInfo(this);
+        }
+
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        BoxFileInfo other = (BoxFileInfo) obj;
+        if (id == null) {
+            if (other.id != null) {
+                return false;
+            }
+        } else if (!id.equals(other.id)) {
+            return false;
+        }
+        return true;
+    }
+
+    private BoxFileInfo(final Builder builder) {
+        this.id = builder.id;
+        this.fileName = builder.fileName;
+        this.path = builder.path;
+        this.size = builder.size;
+        this.createdTime = builder.createdTime;
+        this.modifiedTime = builder.modifiedTime;
+    }
+
+    @Override
+    public String getName() {
+        return getFileName();
+    }
+
+    @Override
+    public String getIdentifier() {
+        return getId();
+    }
+
+    @Override
+    public long getTimestamp() {
+        long timestamp = Math.max(getCreatedTime(), getModifiedTime());
+
+        return timestamp;
+    }
+
+    @Override
+    public long getSize() {
+        return size;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
new file mode 100644
index 0000000000..e86ecc673a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxFlowFileAttribute.java
@@ -0,0 +1,43 @@
+/*
+ * 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.box;
+
+import java.util.function.Function;
+
+public enum BoxFlowFileAttribute {
+    ID(BoxFileInfo.ID, BoxFileInfo::getId),
+    FILENAME(BoxFileInfo.FILENAME, BoxFileInfo::getName),
+    PATH(BoxFileInfo.PATH, BoxFileInfo::getPath),
+    SIZE(BoxFileInfo.SIZE, fileInfo -> String.valueOf(fileInfo.getSize())),
+    TIMESTAMP(BoxFileInfo.TIMESTAMP, fileInfo -> String.valueOf(fileInfo.getTimestamp()));
+
+    private final String name;
+    private final Function<BoxFileInfo, String> fromFileInfo;
+
+    BoxFlowFileAttribute(String attributeName, Function<BoxFileInfo, String> fromFileInfo) {
+        this.name = attributeName;
+        this.fromFileInfo = fromFileInfo;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getValue(BoxFileInfo fileInfo) {
+        return fromFileInfo.apply(fileInfo);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
new file mode 100644
index 0000000000..cad434b553
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFile.java
@@ -0,0 +1,153 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxAPIResponseException;
+import com.box.sdk.BoxFile;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+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.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+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.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"box", "storage", "fetch"})
+@CapabilityDescription("Fetches files from a Box Folder. Designed to be used in tandem with ListBoxFile.")
+@SeeAlso({ListBoxFile.class})
+@WritesAttributes({
+    @WritesAttribute(attribute = FetchBoxFile.ERROR_CODE_ATTRIBUTE, description = "The error code returned by Box when the fetch of a file fails"),
+    @WritesAttribute(attribute = FetchBoxFile.ERROR_MESSAGE_ATTRIBUTE, description = "The error message returned by Box when the fetch of a file fails")
+})
+public class FetchBoxFile extends AbstractProcessor {
+    public static final String ERROR_CODE_ATTRIBUTE = "error.code";
+    public static final String ERROR_MESSAGE_ATTRIBUTE = "error.message";
+
+    public static final PropertyDescriptor FILE_ID = new PropertyDescriptor
+        .Builder().name("box-file-id")
+        .displayName("File ID")
+        .description("The ID of the File to fetch")
+        .required(true)
+        .defaultValue("${box.id}")
+        .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .build();
+
+    public static final Relationship REL_SUCCESS =
+        new Relationship.Builder()
+            .name("success")
+            .description("A FlowFile will be routed here for each successfully fetched File.")
+            .build();
+
+    public static final Relationship REL_FAILURE =
+        new Relationship.Builder().name("failure")
+            .description("A FlowFile will be routed here for each File for which fetch was attempted but failed.")
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+        BoxClientService.BOX_CLIENT_SERVICE,
+        FILE_ID
+    ));
+
+    public static final Set<Relationship> relationships = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+        REL_SUCCESS,
+        REL_FAILURE
+    )));
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return relationships;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) throws IOException {
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        String fileId = context.getProperty(FILE_ID).evaluateAttributeExpressions(flowFile).getValue();
+        FlowFile outFlowFile = flowFile;
+        try {
+            outFlowFile = fetchFile(fileId, session, outFlowFile);
+
+            session.transfer(outFlowFile, REL_SUCCESS);
+        } catch (BoxAPIResponseException e) {
+            handleErrorResponse(session, fileId, flowFile, e);
+        } catch (Exception e) {
+            handleUnexpectedError(session, flowFile, fileId, e);
+        }
+    }
+
+    FlowFile fetchFile(String fileId, ProcessSession session, FlowFile outFlowFile) {
+        BoxFile boxFile = new BoxFile(boxAPIConnection, fileId);
+
+        outFlowFile = session.write(outFlowFile, outputStream -> boxFile.download(outputStream));
+
+        return outFlowFile;
+    }
+
+    private void handleErrorResponse(ProcessSession session, String fileId, FlowFile outFlowFile, BoxAPIResponseException e) {
+        getLogger().error("Couldn't fetch file with id '{}'", fileId, e);
+
+        outFlowFile = session.putAttribute(outFlowFile, ERROR_CODE_ATTRIBUTE, "" + e.getResponseCode());
+        outFlowFile = session.putAttribute(outFlowFile, ERROR_MESSAGE_ATTRIBUTE, e.getMessage());
+
+        session.transfer(outFlowFile, REL_FAILURE);
+    }
+
+    private void handleUnexpectedError(ProcessSession session, FlowFile flowFile, String fileId, Exception e) {
+        getLogger().error("Unexpected error while fetching and processing file with id '{}'", fileId, e);
+
+        flowFile = session.putAttribute(flowFile, ERROR_MESSAGE_ATTRIBUTE, e.getMessage());
+
+        session.transfer(flowFile, REL_FAILURE);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
new file mode 100644
index 0000000000..8fcb9bb62f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFile.java
@@ -0,0 +1,260 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import com.box.sdk.BoxItem;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+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.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processor.util.list.AbstractListProcessor;
+import org.apache.nifi.processor.util.list.ListedEntityTracker;
+import org.apache.nifi.serialization.record.RecordSchema;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Lists files in a Box folder. " +
+    "Each listed file may result in one FlowFile, the metadata being written as FlowFile attributes. " +
+    "Or - in case the 'Record Writer' property is set - the entire result is written as records to a single FlowFile. " +
+    "This Processor is designed to run on Primary Node only in a cluster. If the primary node changes, the new Primary Node will pick up where the " +
+    "previous node left off without duplicating all of the data.")
+@SeeAlso({FetchBoxFile.class})
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@WritesAttributes({@WritesAttribute(attribute = BoxFileInfo.ID, description = "The id of the file"),
+    @WritesAttribute(attribute = BoxFileInfo.FILENAME, description = "The name of the file"),
+    @WritesAttribute(attribute = BoxFileInfo.PATH, description = "The path of the file on Box"),
+    @WritesAttribute(attribute = BoxFileInfo.SIZE, description = "The size of the file (in bytes)"),
+    @WritesAttribute(attribute = BoxFileInfo.TIMESTAMP, description = "The last modified time of the file.")})
+@Stateful(scopes = {Scope.CLUSTER}, description = "The processor stores necessary data to be able to keep track what files have been listed already." +
+    " What exactly needs to be stored depends on the 'Listing Strategy'.")
+public class ListBoxFile extends AbstractListProcessor<BoxFileInfo> {
+    public static final PropertyDescriptor FOLDER_ID = new PropertyDescriptor.Builder()
+        .name("box-folder-id")
+        .displayName("Folder ID")
+        .description("The ID of the folder from which to pull list of files.")
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+        .required(true)
+        .build();
+
+    public static final PropertyDescriptor RECURSIVE_SEARCH = new PropertyDescriptor.Builder()
+        .name("recursive-search")
+        .displayName("Search Recursively")
+        .description("When 'true', will include list of files from sub-folders." +
+            " Otherwise, will return only files that are within the folder defined by the 'Folder ID' property.")
+        .required(true)
+        .defaultValue("true")
+        .allowableValues("true", "false")
+        .build();
+
+    public static final PropertyDescriptor MIN_AGE = new PropertyDescriptor.Builder()
+        .name("min-age")
+        .displayName("Minimum File Age")
+        .description("The minimum age a file must be in order to be considered; any files younger than this will be ignored.")
+        .required(true)
+        .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+        .defaultValue("0 sec")
+        .build();
+
+    public static final PropertyDescriptor LISTING_STRATEGY = new PropertyDescriptor.Builder()
+        .fromPropertyDescriptor(AbstractListProcessor.LISTING_STRATEGY)
+        .allowableValues(BY_TIMESTAMPS, BY_ENTITIES, BY_TIME_WINDOW, NO_TRACKING)
+        .build();
+
+    public static final PropertyDescriptor TRACKING_STATE_CACHE = new PropertyDescriptor.Builder()
+        .fromPropertyDescriptor(ListedEntityTracker.TRACKING_STATE_CACHE)
+        .dependsOn(LISTING_STRATEGY, BY_ENTITIES)
+        .build();
+
+    public static final PropertyDescriptor TRACKING_TIME_WINDOW = new PropertyDescriptor.Builder()
+        .fromPropertyDescriptor(ListedEntityTracker.TRACKING_TIME_WINDOW)
+        .dependsOn(LISTING_STRATEGY, BY_ENTITIES)
+        .build();
+
+    public static final PropertyDescriptor INITIAL_LISTING_TARGET = new PropertyDescriptor.Builder()
+        .fromPropertyDescriptor(ListedEntityTracker.INITIAL_LISTING_TARGET)
+        .dependsOn(LISTING_STRATEGY, BY_ENTITIES)
+        .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+        BoxClientService.BOX_CLIENT_SERVICE,
+        FOLDER_ID,
+        RECURSIVE_SEARCH,
+        MIN_AGE,
+        LISTING_STRATEGY,
+        TRACKING_STATE_CACHE,
+        TRACKING_TIME_WINDOW,
+        INITIAL_LISTING_TARGET,
+        RECORD_WRITER
+    ));
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    protected Map<String, String> createAttributes(
+        final BoxFileInfo entity,
+        final ProcessContext context
+    ) {
+        final Map<String, String> attributes = new HashMap<>();
+
+        for (BoxFlowFileAttribute attribute : BoxFlowFileAttribute.values()) {
+            Optional.ofNullable(attribute.getValue(entity))
+                .ifPresent(value -> attributes.put(attribute.getName(), value));
+        }
+
+        return attributes;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) throws IOException {
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+    }
+
+    @Override
+    protected String getListingContainerName(final ProcessContext context) {
+        return String.format("Box Folder [%s]", getPath(context));
+    }
+
+    @Override
+    protected String getPath(final ProcessContext context) {
+        return context.getProperty(FOLDER_ID).evaluateAttributeExpressions().getValue();
+    }
+
+    @Override
+    protected boolean isListingResetNecessary(final PropertyDescriptor property) {
+        return LISTING_STRATEGY.equals(property)
+            || FOLDER_ID.equals(property)
+            || RECURSIVE_SEARCH.equals(property);
+    }
+
+    @Override
+    protected Scope getStateScope(final PropertyContext context) {
+        return Scope.CLUSTER;
+    }
+
+    @Override
+    protected RecordSchema getRecordSchema() {
+        return BoxFileInfo.getRecordSchema();
+    }
+
+    @Override
+    protected String getDefaultTimePrecision() {
+        return PRECISION_SECONDS.getValue();
+    }
+
+    @Override
+    protected List<BoxFileInfo> performListing(
+        final ProcessContext context,
+        final Long minTimestamp,
+        final ListingMode listingMode
+    ) throws IOException {
+        final List<BoxFileInfo> listing = new ArrayList<>();
+
+        final String folderId = context.getProperty(FOLDER_ID).evaluateAttributeExpressions().getValue();
+        final Boolean recursive = context.getProperty(RECURSIVE_SEARCH).asBoolean();
+        final Long minAge = context.getProperty(MIN_AGE).asTimePeriod(TimeUnit.MILLISECONDS);
+
+        long createdAtMax = Instant.now().toEpochMilli() - minAge;
+        listFolder(listing, folderId, recursive, createdAtMax);
+        return listing;
+    }
+
+    private void listFolder(List<BoxFileInfo> listing, String folderId, Boolean recursive, long createdAtMax) {
+        BoxFolder folder = getFolder(folderId);
+        for (BoxItem.Info itemInfo : folder.getChildren(
+            "id",
+            "name",
+            "item_status",
+            "size",
+            "created_at",
+            "modified_at",
+            "content_created_at",
+            "content_modified_at",
+            "path_collection"
+        )) {
+            if (itemInfo instanceof BoxFile.Info) {
+                BoxFile.Info info = (BoxFile.Info) itemInfo;
+
+                long createdAt = itemInfo.getCreatedAt().getTime();
+
+                if (createdAt <= createdAtMax) {
+                    BoxFileInfo boxFileInfo = new BoxFileInfo.Builder()
+                        .id(info.getID())
+                        .fileName(info.getName())
+                        .path("/" + info.getPathCollection().stream()
+                            .filter(pathItemInfo -> !pathItemInfo.getID().equals("0"))
+                            .map(BoxItem.Info::getName)
+                            .collect(Collectors.joining("/")))
+                        .size(info.getSize())
+                        .createdTime(info.getCreatedAt().getTime())
+                        .modifiedTime(info.getModifiedAt().getTime())
+                        .build();
+                    listing.add(boxFileInfo);
+                }
+            } else if (recursive && itemInfo instanceof BoxFolder.Info) {
+                BoxFolder.Info info = (BoxFolder.Info) itemInfo;
+                listFolder(listing, info.getID(), recursive, createdAtMax);
+            }
+        }
+    }
+
+    BoxFolder getFolder(String folderId) {
+        return new BoxFolder(boxAPIConnection, folderId);
+    }
+
+    @Override
+    protected Integer countUnfilteredListing(final ProcessContext context) throws IOException {
+        return performListing(context, null, ListingMode.CONFIGURATION_VERIFICATION).size();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
new file mode 100644
index 0000000000..09755f9be5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -0,0 +1,16 @@
+# 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.
+org.apache.nifi.processors.box.ListBoxFile
+org.apache.nifi.processors.box.FetchBoxFile
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
new file mode 100644
index 0000000000..8c0c94f567
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/AbstractBoxFileIT.java
@@ -0,0 +1,137 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxConfig;
+import com.box.sdk.BoxDeveloperEditionAPIConnection;
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Set the following constants before running:<br />
+ * <br />
+ * FOLDER_ID - The ID of a Folder the test can use to create files and sub-folders within.<br />
+ * ACCOUNT_ID - The ID of the Account owning the Folder.<br />
+ * APP_CONFIG_FILE - An App Settings Configuration JSON file.
+ * Read nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/docs/org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService/additionalDetails.html for details.<br />
+ * <br />
+ * Created files and folders are cleaned up, but it's advisable to dedicate a folder for this test so that it can be cleaned up easily should the test fail to do so.
+ */
+public abstract class AbstractBoxFileIT<T extends Processor> {
+    static final String FOLDER_ID = "";
+    static final String ACCOUNT_ID = "";
+    static final String APP_CONFIG_FILE = "";
+
+    protected static final String DEFAULT_FILE_CONTENT = "test_content";
+    public static final String MAIN_FOLDER_NAME = "main";
+
+    protected T testSubject;
+    protected TestRunner testRunner;
+
+    protected BoxAPIConnection boxAPIConnection;
+
+    protected String targetFolderName;
+    protected String mainFolderId;
+
+    protected abstract T createTestSubject();
+
+    @BeforeEach
+    protected void init() throws Exception {
+        try (
+            Reader reader = new FileReader(APP_CONFIG_FILE);
+        ) {
+            BoxConfig boxConfig = BoxConfig.readFrom(reader);
+            boxAPIConnection = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(boxConfig);
+            boxAPIConnection.asUser(ACCOUNT_ID);
+        }
+
+        targetFolderName = new BoxFolder(boxAPIConnection, FOLDER_ID).getInfo("name").getName();
+
+        BoxFolder.Info mainFolderInfo = createFolder(MAIN_FOLDER_NAME, FOLDER_ID);
+        mainFolderId = mainFolderInfo.getID();
+
+        testSubject = createTestSubject();
+        testRunner = createTestRunner();
+    }
+
+    @AfterEach
+    protected void tearDown() {
+        if (boxAPIConnection != null) {
+            BoxFolder folder = new BoxFolder(boxAPIConnection, mainFolderId);
+            folder.delete(true);
+        }
+    }
+
+    protected TestRunner createTestRunner() throws Exception{
+        TestRunner testRunner = TestRunners.newTestRunner(testSubject);
+
+        BoxClientService boxClientService = mock(BoxClientService.class);
+        doReturn(boxClientService.toString()).when(boxClientService).getIdentifier();
+        doReturn(boxAPIConnection).when(boxClientService).getBoxApiConnection();
+
+        testRunner.addControllerService(boxClientService.getIdentifier(), boxClientService);
+        testRunner.enableControllerService(boxClientService);
+        testRunner.setProperty(BoxClientService.BOX_CLIENT_SERVICE, boxClientService.getIdentifier());
+
+        return testRunner;
+    }
+
+    protected BoxFolder.Info createFolder(String folderName, String parentFolderId) {
+        BoxFolder parentFolder = new BoxFolder(boxAPIConnection, parentFolderId);
+        BoxFolder.Info folderInfo = parentFolder.createFolder(folderName);
+
+        return folderInfo;
+    }
+
+    protected BoxFile.Info createFileWithDefaultContent(String name, String folderId) {
+        return createFile(name, DEFAULT_FILE_CONTENT, folderId);
+    }
+
+    protected BoxFile.Info createFile(String name, String fileContent, String folderId) {
+        BoxFolder folder = new BoxFolder(boxAPIConnection, folderId);
+
+        BoxFile.Info fileInfo = folder.uploadFile(new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)), name);
+
+        return fileInfo;
+    }
+
+    protected Set<String> getCheckedAttributeNames() {
+        Set<String> checkedAttributeNames = Arrays.stream(BoxFlowFileAttribute.values())
+            .map(BoxFlowFileAttribute::getName)
+            .collect(Collectors.toSet());
+
+        return checkedAttributeNames;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
new file mode 100644
index 0000000000..fb34d46344
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileIT.java
@@ -0,0 +1,144 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxFile;
+import org.apache.nifi.util.MockFlowFile;
+import org.junit.jupiter.api.Test;
+
+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.AtomicBoolean;
+
+/**
+ * See Javadoc {@link AbstractBoxFileIT} for instructions how to run this test.
+ */
+public class FetchBoxFileIT extends AbstractBoxFileIT<FetchBoxFile> {
+    @Override
+    public FetchBoxFile createTestSubject() {
+        FetchBoxFile testSubject = new FetchBoxFile();
+
+        return testSubject;
+    }
+
+    @Test
+    void testFetchSingleFile() throws Exception {
+        // GIVEN
+        BoxFile.Info file = createFileWithDefaultContent("test_file.txt", mainFolderId);
+
+        Map<String, String> inputFlowFileAttributes = new HashMap<>();
+        inputFlowFileAttributes.put("box.id", file.getID());
+        inputFlowFileAttributes.put("filename", file.getName());
+
+        HashSet<Map<String, String>> expectedAttributes = new HashSet<>(Arrays.asList(inputFlowFileAttributes));
+        List<String> expectedContent = Arrays.asList(DEFAULT_FILE_CONTENT);
+
+        // WHEN
+        testRunner.enqueue("unimportant_data", inputFlowFileAttributes);
+        testRunner.run();
+
+        // THEN
+        testRunner.assertTransferCount(FetchBoxFile.REL_FAILURE, 0);
+
+        testRunner.assertAttributes(FetchBoxFile.REL_SUCCESS, getCheckedAttributeNames(), expectedAttributes);
+        testRunner.assertContents(FetchBoxFile.REL_SUCCESS, expectedContent);
+    }
+
+    @Test
+    void testInputFlowFileReferencesMissingFile() throws Exception {
+        // GIVEN
+        Map<String, String> inputFlowFileAttributes = new HashMap<>();
+        inputFlowFileAttributes.put("box.id", "111");
+        inputFlowFileAttributes.put("filename", "missing_filename");
+
+        Set<Map<String, String>> expectedFailureAttributes = new HashSet<>(Arrays.asList(
+            new HashMap<String, String>() {{
+                put("box.id", "111");
+                put("filename", "missing_filename");
+                put("error.code", "404");
+            }}
+        ));
+
+        // WHEN
+        testRunner.enqueue("unimportant_data", inputFlowFileAttributes);
+        testRunner.run();
+
+        // THEN
+        testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 0);
+
+        testRunner.assertAttributes(FetchBoxFile.REL_FAILURE, getCheckedAttributeNames(), expectedFailureAttributes);
+    }
+
+    @Test
+    void testInputFlowFileThrowsExceptionBeforeFetching() throws Exception {
+        // GIVEN
+        BoxFile.Info file = createFileWithDefaultContent("test_file.txt", mainFolderId);
+
+        Map<String, String> inputFlowFileAttributes = new HashMap<>();
+        inputFlowFileAttributes.put("box.id", file.getID());
+        inputFlowFileAttributes.put("filename", file.getName());
+
+        MockFlowFile input = new MockFlowFile(1) {
+            AtomicBoolean throwException = new AtomicBoolean(true);
+
+            @Override
+            public boolean isPenalized() {
+                // We want to throw exception only once because the exception handling itself calls this again
+                if (throwException.get()) {
+                    throwException.set(false);
+                    throw new RuntimeException("Intentional exception");
+                } else {
+                    return super.isPenalized();
+                }
+            }
+
+            @Override
+            public Map<String, String> getAttributes() {
+                return inputFlowFileAttributes;
+            }
+        };
+
+        Set<Map<String, String>> expectedFailureAttributes = new HashSet<>(Arrays.asList(
+            new HashMap<String, String>() {{
+                putAll(inputFlowFileAttributes);
+            }}
+        ));
+
+        // WHEN
+        testRunner.enqueue(input);
+        testRunner.run();
+
+        // THEN
+        testRunner.assertTransferCount(FetchBoxFile.REL_SUCCESS, 0);
+
+        testRunner.assertAttributes(FetchBoxFile.REL_FAILURE, getCheckedAttributeNames(), expectedFailureAttributes);
+    }
+
+    public Set<String> getCheckedAttributeNames() {
+        Set<String> checkedAttributeNames = new HashSet<>();
+
+        checkedAttributeNames.add(BoxFlowFileAttribute.ID.getName());
+        checkedAttributeNames.add(BoxFlowFileAttribute.FILENAME.getName());
+        checkedAttributeNames.add(FetchBoxFile.ERROR_CODE_ATTRIBUTE);
+
+        return checkedAttributeNames;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
new file mode 100644
index 0000000000..c0337524d6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileIT.java
@@ -0,0 +1,156 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.util.MockFlowFile;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * See Javadoc {@link AbstractBoxFileIT} for instructions how to run this test.
+ */
+public class ListBoxFileIT extends AbstractBoxFileIT<ListBoxFile> {
+    @BeforeEach
+    public void init() throws Exception {
+        super.init();
+        testRunner.setProperty(ListBoxFile.FOLDER_ID, mainFolderId);
+    }
+
+    @Override
+    public ListBoxFile createTestSubject() {
+        ListBoxFile testSubject = new ListBoxFile();
+
+        return testSubject;
+    }
+
+    @Test
+    void listFilesFrom3LayerDeepDirectoryTree() throws Exception {
+        // GIVEN
+        BoxFolder.Info main_sub1 = createFolder("main_sub1", mainFolderId);
+        BoxFolder.Info main_sub2 = createFolder("main_sub2", mainFolderId);
+
+        BoxFolder.Info main_sub1_sub1 = createFolder("main_sub1_sub1", main_sub1.getID());
+
+        createFileWithDefaultContent("main_file1", mainFolderId);
+        createFileWithDefaultContent("main_file2", mainFolderId);
+        createFileWithDefaultContent("main_file3", mainFolderId);
+
+        createFileWithDefaultContent("main_sub1_file1", main_sub1.getID());
+
+        createFileWithDefaultContent("main_sub2_file1", main_sub2.getID());
+        createFileWithDefaultContent("main_sub2_file2", main_sub2.getID());
+
+        createFileWithDefaultContent("main_sub1_sub1_file1", main_sub1_sub1.getID());
+        createFileWithDefaultContent("main_sub1_sub1_file2", main_sub1_sub1.getID());
+        createFileWithDefaultContent("main_sub1_sub1_file3", main_sub1_sub1.getID());
+
+        Set<String> expectedFileNames = new HashSet<>(Arrays.asList(
+            "main_file1", "main_file2", "main_file3",
+            "main_sub1_file1",
+            "main_sub2_file1", "main_sub2_file2",
+            "main_sub1_sub1_file1", "main_sub1_sub1_file2", "main_sub1_sub1_file3"
+        ));
+
+        // The creation of the files are not (completely) synchronized.
+        Thread.sleep(2000);
+
+        // WHEN
+        testRunner.run();
+
+        // THEN
+        Set<String> actualFileNames = getActualFileNames();
+
+        assertEquals(expectedFileNames, actualFileNames);
+
+        // Next, list a sub folder, non-recursively this time. (Changing these properties will clear the Processor state as well
+        //  so all files are eligible for listing again.)
+
+        // GIVEN
+        testRunner.clearTransferState();
+
+        expectedFileNames = new HashSet<>(Arrays.asList(
+            "main_sub1_file1"
+        ));
+
+        // WHEN
+        testRunner.setProperty(ListBoxFile.FOLDER_ID, main_sub1.getID());
+        testRunner.setProperty(ListBoxFile.RECURSIVE_SEARCH, "false");
+        testRunner.run();
+
+        // THEN
+        actualFileNames = getActualFileNames();
+
+        assertEquals(expectedFileNames, actualFileNames);
+    }
+
+    @Test
+    void doNotListTooYoungFilesWhenMinAgeIsSet() throws Exception {
+        // GIVEN
+        testRunner.setProperty(ListBoxFile.MIN_AGE, "15 s");
+
+        createFileWithDefaultContent("main_file", mainFolderId);
+
+        // Make sure the file 'arrives' and could be listed
+        Thread.sleep(5000);
+
+        // WHEN
+        testRunner.run();
+
+        // THEN
+        Set<String> actualFileNames = getActualFileNames();
+
+        assertEquals(Collections.emptySet(), actualFileNames);
+
+        // Next, wait for another 10+ seconds for MIN_AGE to expire then list again
+
+        // GIVEN
+        Thread.sleep(10000);
+
+        Set<String> expectedFileNames = new HashSet<>(Arrays.asList(
+            "main_file"
+        ));
+
+        // WHEN
+        testRunner.run();
+
+        // THEN
+        actualFileNames = getActualFileNames();
+
+        assertEquals(expectedFileNames, actualFileNames);
+    }
+
+    private Set<String> getActualFileNames() {
+        List<MockFlowFile> successFlowFiles = testRunner.getFlowFilesForRelationship(ListBoxFile.REL_SUCCESS);
+
+        Set<String> actualFileNames = successFlowFiles.stream()
+            .map(flowFile -> flowFile.getAttribute("filename"))
+            .collect(Collectors.toSet());
+
+        return actualFileNames;
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java
new file mode 100644
index 0000000000..0fb2f7daa1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileSimpleTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.util.EqualsWrapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.apache.nifi.util.EqualsWrapper.wrapList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class ListBoxFileSimpleTest implements SimpleListBoxFileTestTrait {
+    private ListBoxFile testSubject;
+
+    private ProcessContext mockProcessContext;
+    private BoxClientService mockBoxClientService;
+    private PropertyValue mockBoxClientServicePropertyValue;
+    private BoxAPIConnection mockBoxAPIConnection;
+
+    private BoxFolder mockBoxFolder;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        mockProcessContext = mock(ProcessContext.class, Answers.RETURNS_DEEP_STUBS);
+        mockBoxClientService = mock(BoxClientService.class);
+        mockBoxClientServicePropertyValue = mock(PropertyValue.class);
+        mockBoxAPIConnection = mock(BoxAPIConnection.class);
+
+        mockBoxFolder = mock(BoxFolder.class);
+
+        testSubject = new ListBoxFile() {
+            @Override
+            BoxFolder getFolder(String folderId) {
+                return mockBoxFolder;
+            }
+        };
+
+        doReturn(mockBoxClientServicePropertyValue).when(mockProcessContext).getProperty(BoxClientService.BOX_CLIENT_SERVICE);
+        doReturn(mockBoxClientService).when(mockBoxClientServicePropertyValue).asControllerService(BoxClientService.class);
+        doReturn(mockBoxAPIConnection).when(mockBoxClientService).getBoxApiConnection();
+
+        testSubject.onScheduled(mockProcessContext);
+    }
+
+    @Test
+    void testCreatedListableEntityContainsCorrectData() throws Exception {
+        // GIVEN
+        Long minTimestamp = 0L;
+
+        String id = "id_1";
+        String filename = "file_name_1";
+        List<String> pathParts = Arrays.asList("path", "to", "file");
+        Long size = 125L;
+        long createdTime = 123456L;
+        long modifiedTime = 234567L;
+
+        mockFetchedFileList(id, filename, pathParts, size, createdTime, modifiedTime);
+
+        List<BoxFileInfo> expected = Arrays.asList(
+            new BoxFileInfo.Builder()
+                .id(id)
+                .fileName(filename)
+                .path("/path/to/file")
+                .size(size)
+                .createdTime(createdTime)
+                .modifiedTime(modifiedTime)
+                .build()
+        );
+
+        // WHEN
+        List<BoxFileInfo> actual = testSubject.performListing(mockProcessContext, minTimestamp, null);
+
+        // THEN
+        List<Function<BoxFileInfo, Object>> propertyProviders = Arrays.asList(
+            BoxFileInfo::getId,
+            BoxFileInfo::getIdentifier,
+            BoxFileInfo::getName,
+            BoxFileInfo::getPath,
+            BoxFileInfo::getSize,
+            BoxFileInfo::getTimestamp,
+            BoxFileInfo::getCreatedTime,
+            BoxFileInfo::getModifiedTime
+        );
+
+        List<EqualsWrapper<BoxFileInfo>> expectedWrapper = wrapList(expected, propertyProviders);
+        List<EqualsWrapper<BoxFileInfo>> actualWrapper = wrapList(actual, propertyProviders);
+
+        assertEquals(expectedWrapper, actualWrapper);
+    }
+
+    @Override
+    public BoxFolder getMockBoxFolder() {
+        return mockBoxFolder;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java
new file mode 100644
index 0000000000..bfab87c957
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileTestRunnerTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.json.JsonRecordSetWriter;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class ListBoxFileTestRunnerTest implements SimpleListBoxFileTestTrait {
+    private ListBoxFile testSubject;
+    private TestRunner testRunner;
+
+    private BoxAPIConnection mockBoxAPIConnection;
+
+    private BoxFolder mockBoxFolder;
+
+    private String folderId = "folderId";
+
+    @BeforeEach
+    void setUp() throws Exception {
+        mockBoxAPIConnection = mock(BoxAPIConnection.class);
+        mockBoxFolder = mock(BoxFolder.class);
+
+        testSubject = new ListBoxFile() {
+            @Override
+            BoxFolder getFolder(String folderId) {
+                return mockBoxFolder;
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+        testRunner.setProperty(ListBoxFile.FOLDER_ID, folderId);
+
+        BoxClientService boxClientService = mock(BoxClientService.class);
+        doReturn(boxClientService.toString()).when(boxClientService).getIdentifier();
+        doReturn(mockBoxAPIConnection).when(boxClientService).getBoxApiConnection();
+
+        testRunner.addControllerService(boxClientService.getIdentifier(), boxClientService);
+        testRunner.enableControllerService(boxClientService);
+        testRunner.setProperty(BoxClientService.BOX_CLIENT_SERVICE, boxClientService.getIdentifier());
+    }
+
+    @Test
+    void testOutputAsAttributesWhereTimestampIsModifiedTime() throws Exception {
+        // GIVEN
+        String id = "id_1";
+        String filename = "file_name_1";
+        List<String> pathParts = Arrays.asList("path", "to", "file");
+        Long size = 125L;
+        Long createdTime = 123456L;
+        Long modifiedTime = 123456L + 1L;
+
+        // WHEN
+        // THEN
+        testOutputAsAttributes(
+            id,
+            filename,
+            pathParts,
+            size,
+            createdTime,
+            modifiedTime,
+            modifiedTime,
+            "/path/to/file"
+        );
+    }
+
+    @Test
+    void testOutputAsContent() throws Exception {
+        // GIVEN
+        String id = "id_1";
+        String filename = "file_name_1";
+        List<String> pathParts = Arrays.asList("path", "to", "file");
+        Long size = 125L;
+        Long createdTime = 123456L;
+        Long modifiedTime = 123456L + 1L;
+
+        addJsonRecordWriterFactory();
+
+        mockFetchedFileList(id, filename, pathParts, size, createdTime, modifiedTime);
+
+        List<String> expectedContents = Arrays.asList(
+            "[" +
+                "{" +
+                "\"box.id\":\"" + id + "\"," +
+                "\"filename\":\"" + filename + "\"," +
+                "\"path\":\"/path/to/file\"," +
+                "\"box.size\":" + size + "," +
+                "\"box.timestamp\":" + modifiedTime +
+                "}" +
+                "]");
+
+        // WHEN
+        testRunner.run();
+
+        // THEN
+        testRunner.assertContents(ListBoxFile.REL_SUCCESS, expectedContents);
+    }
+
+    private void addJsonRecordWriterFactory() throws InitializationException {
+        RecordSetWriterFactory recordSetWriter = new JsonRecordSetWriter();
+        testRunner.addControllerService("record_writer", recordSetWriter);
+        testRunner.enableControllerService(recordSetWriter);
+        testRunner.setProperty(ListBoxFile.RECORD_WRITER, "record_writer");
+    }
+
+    private void testOutputAsAttributes(
+        String id,
+        String filename,
+        Collection<String> pathParts,
+        Long size,
+        Long createdTime,
+        Long modifiedTime,
+        Long expectedTimestamp,
+        String expectedPath
+    ) {
+        // GIVEN
+        mockFetchedFileList(id, filename, pathParts, size, createdTime, modifiedTime);
+
+        Map<String, String> inputFlowFileAttributes = new HashMap<>();
+        inputFlowFileAttributes.put("box.id", id);
+        inputFlowFileAttributes.put("filename", filename);
+        inputFlowFileAttributes.put("path", expectedPath);
+        inputFlowFileAttributes.put("box.size", "" + size);
+        inputFlowFileAttributes.put("box.timestamp", "" + expectedTimestamp);
+
+        HashSet<Map<String, String>> expectedAttributes = new HashSet<>(Arrays.asList(inputFlowFileAttributes));
+
+        // WHEN
+        testRunner.run();
+
+        // THEN
+        testRunner.assertAttributes(ListBoxFile.REL_SUCCESS, getCheckedAttributeNames(), expectedAttributes);
+    }
+
+    private Set<String> getCheckedAttributeNames() {
+        Set<String> checkedAttributeNames = Arrays.stream(BoxFlowFileAttribute.values())
+            .map(BoxFlowFileAttribute::getName)
+            .collect(Collectors.toSet());
+
+        return checkedAttributeNames;
+    }
+
+    @Override
+    public BoxFolder getMockBoxFolder() {
+        return mockBoxFolder;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java
new file mode 100644
index 0000000000..64a2474927
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/SimpleListBoxFileTestTrait.java
@@ -0,0 +1,92 @@
+/*
+ * 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.box;
+
+import com.box.sdk.BoxFile;
+import com.box.sdk.BoxFolder;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public interface SimpleListBoxFileTestTrait {
+    BoxFolder getMockBoxFolder();
+
+    default void mockFetchedFileList(
+        String id,
+        String filename,
+        Collection<String> pathParts,
+        Long size,
+        Long createdTime,
+        Long modifiedTime
+    ) {
+        doReturn(
+            Arrays.asList(
+                createFileInfo(
+                    id,
+                    filename,
+                    pathParts,
+                    size,
+                    createdTime,
+                    modifiedTime
+                )
+            )
+        ).when(getMockBoxFolder()).getChildren("id",
+            "name",
+            "item_status",
+            "size",
+            "created_at",
+            "modified_at",
+            "content_created_at",
+            "content_modified_at",
+            "path_collection");
+    }
+
+    default BoxFile.Info createFileInfo(
+        String id,
+        String name,
+        Collection<String> pathParts,
+        Long size,
+        Long createdTime,
+        Long modifiedTime
+    ) {
+        BoxFile.Info fileInfo = mock(BoxFile.Info.class);
+
+        List<BoxFolder.Info> pathCollection = pathParts.stream().map(pathPart -> {
+            BoxFolder.Info folderInfo = mock(BoxFolder.Info.class);
+            when(folderInfo.getName()).thenReturn(pathPart);
+            when(folderInfo.getID()).thenReturn("not0");
+
+            return folderInfo;
+        }).collect(Collectors.toList());
+
+        when(fileInfo.getID()).thenReturn(id);
+        when(fileInfo.getName()).thenReturn(name);
+        when(fileInfo.getPathCollection()).thenReturn(pathCollection);
+        when(fileInfo.getSize()).thenReturn(size);
+        when(fileInfo.getCreatedAt()).thenReturn(new Date(createdTime));
+        when(fileInfo.getModifiedAt()).thenReturn(new Date(modifiedTime));
+
+        return fileInfo;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/pom.xml
new file mode 100644
index 0000000000..9ecc760deb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>nifi-box-bundle</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-services-api-nar</artifactId>
+    <packaging>nar</packaging>
+    <properties>
+        <maven.javadoc.skip>false</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-standard-services-api-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000000..899ec5f145
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,259 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+APACHE NIFI SUBCOMPONENTS:
+
+The Apache NiFi project contains subcomponents with separate copyright
+notices and license terms. Your use of the source code for the these
+subcomponents is subject to the terms and conditions of the following
+licenses.
+
+  The binary distribution of this product bundles
+  'Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs',
+  'Bouncy Castle Provider' and
+  'Bouncy Castle ASN.1 Extension and Utility APIs'
+  under the Bouncy Castle (an MIT style) license.
+
+    Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+  The binary distribution of this product bundles
+  'Minimal JSON'
+  under the MIT license.
+
+    Copyright (c) 2014, 2015 EclipseSource.
+
+    Permission is hereby granted, free of charge, to any person obtaining
+    a copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+    OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000000..7e97606452
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api-nar/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,45 @@
+nifi-box-nar
+Copyright 2015-2020 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+******************
+Apache Software License v2
+******************
+
+The following binary components are provided under the Apache Software License v2
+
+  (ASLv2) The Box SDK for Java
+    The following NOTICE information applies:
+      The Box SDK for Java
+      Copyright 2019 Box, Inc. All rights reserved.
+
+  (ASLv2) JOSE4j
+    The following NOTICE information applies:
+      JOSE4j
+      Copyright 2012-2015 Brian Campbell
+
+************
+MIT
+************
+
+The following binary components are provided under the MIT license.
+
+  Minimal JSON
+    The following NOTICE information applies:
+      Minimal JSON
+      Copyright (c) 2014, 2015 EclipseSource.
+
+
+************
+Bouncy Castle (MIT style)
+************
+
+The following binary components are provided under the Bouncy Castle (an MIT style) license.
+
+  Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs
+  Bouncy Castle Provider
+  Bouncy Castle ASN.1 Extension and Utility APIs
+    The following NOTICE information applies:
+      Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/pom.xml
new file mode 100644
index 0000000000..cc8d8c780e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>nifi-box-bundle</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-services-api</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.box</groupId>
+            <artifactId>box-java-sdk</artifactId>
+            <version>3.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/src/main/java/org/apache/nifi/box/controllerservices/BoxClientService.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/src/main/java/org/apache/nifi/box/controllerservices/BoxClientService.java
new file mode 100644
index 0000000000..e369e80910
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-api/src/main/java/org/apache/nifi/box/controllerservices/BoxClientService.java
@@ -0,0 +1,36 @@
+/*
+ * 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.box.controllerservices;
+
+import com.box.sdk.BoxAPIConnection;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ControllerService;
+
+/**
+ * Provides Box client objects through which Box API calls can be used.
+ */
+public interface BoxClientService extends ControllerService {
+    PropertyDescriptor BOX_CLIENT_SERVICE = new PropertyDescriptor.Builder()
+        .name("box-client-service")
+        .displayName("Box Client Service")
+        .description("Controller Service used to obtain a Box API connection.")
+        .identifiesControllerService(BoxClientService.class)
+        .required(true)
+        .build();
+
+    BoxAPIConnection getBoxApiConnection();
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/pom.xml
new file mode 100644
index 0000000000..a5a4bcfb3d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>nifi-box-bundle</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-services-nar</artifactId>
+    <packaging>nar</packaging>
+    <properties>
+        <maven.javadoc.skip>false</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000000..21ca241f55
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services-nar/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,33 @@
+nifi-box-nar
+Copyright 2015-2020 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+******************
+Apache Software License v2
+******************
+
+The following binary components are provided under the Apache Software License v2
+
+  (ASLv2) Jackson Databind
+    The following NOTICE information applies:
+      Jackson Databind
+      Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi
+
+      Jackson is a high-performance, Free/Open Source JSON processing library.
+      It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+      been in development since 2007.
+
+      It is currently developed by a community of developers, as well as supported
+      commercially by FasterXML.com.
+
+  (ASLv2) Jackson Core
+    The following NOTICE information applies:
+      Jackson Core
+      Copyright Tatu Saloranta
+
+  (ASLv2) Jackson Annotations
+    The following NOTICE information applies:
+      Jackson Annotations
+      Copyright (c) 2019 Tatu Saloranta
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/pom.xml b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/pom.xml
new file mode 100644
index 0000000000..1dfc3951cf
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/pom.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>nifi-box-bundle</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-services</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-box-services-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-utils</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-json-utils</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-mock</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientService.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientService.java
new file mode 100644
index 0000000000..15ef3edc53
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientService.java
@@ -0,0 +1,156 @@
+/*
+ * 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.box.controllerservices;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxConfig;
+import com.box.sdk.BoxDeveloperEditionAPIConnection;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.JsonValidator;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@CapabilityDescription("Provides Box client objects through which Box API calls can be used.")
+@Tags({"box", "client", "provider"})
+public class JsonConfigBasedBoxClientService extends AbstractControllerService implements BoxClientService {
+    public static final PropertyDescriptor ACCOUNT_ID = new PropertyDescriptor.Builder()
+        .name("box-account-id")
+        .displayName("Account ID")
+        .description("The ID of the Box account who owns the accessed resource. Same as 'User Id' under 'App Info' in the App 'General Settings'.")
+        .required(true)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+        .build();
+
+    public static final PropertyDescriptor APP_CONFIG_FILE = new PropertyDescriptor.Builder()
+        .name("app-config-file")
+        .displayName("App Config File")
+        .description("Full path of an App config JSON file. See Additional Details for more information.")
+        .required(false)
+        .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+        .build();
+
+    public static final PropertyDescriptor APP_CONFIG_JSON = new PropertyDescriptor.Builder()
+        .name("app-config-json")
+        .displayName("App Config JSON")
+        .description("The raw JSON containing an App config. See Additional Details for more information.")
+        .required(false)
+        .sensitive(true)
+        .addValidator(JsonValidator.INSTANCE)
+        .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+        .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+        ACCOUNT_ID,
+        APP_CONFIG_FILE,
+        APP_CONFIG_JSON
+    ));
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @Override
+    public final List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) {
+        boxAPIConnection = createBoxApiConnection(context);
+    }
+
+    @Override
+    protected Collection<ValidationResult> customValidate(ValidationContext validationContext) {
+        Collection<ValidationResult> validationResults = new ArrayList<>(super.customValidate(validationContext));
+
+        if (validationContext.getProperty(APP_CONFIG_FILE).isSet() && validationContext.getProperty(APP_CONFIG_JSON).isSet()) {
+            validationResults.add(new ValidationResult.Builder()
+                .subject("App configuration")
+                .valid(false)
+                .explanation(String.format("'%s' and '%s' cannot be configured at the same time",
+                    APP_CONFIG_FILE.getDisplayName(),
+                    APP_CONFIG_JSON.getDisplayName())
+                )
+                .build());
+        }
+
+        if (!validationContext.getProperty(APP_CONFIG_FILE).isSet() && !validationContext.getProperty(APP_CONFIG_JSON).isSet()) {
+            validationResults.add(new ValidationResult.Builder()
+                .subject("App configuration")
+                .valid(false)
+                .explanation(String.format("either '%s' or '%s' must be configured",
+                    APP_CONFIG_FILE.getDisplayName(),
+                    APP_CONFIG_JSON.getDisplayName())
+                )
+                .build());
+        }
+
+        return validationResults;
+    }
+
+    @Override
+    public BoxAPIConnection getBoxApiConnection() {
+        return boxAPIConnection;
+    }
+
+    private BoxAPIConnection createBoxApiConnection(ConfigurationContext context) {
+        BoxAPIConnection api;
+
+        String accountId = context.getProperty(ACCOUNT_ID).evaluateAttributeExpressions().getValue();
+
+        BoxConfig boxConfig;
+        if (context.getProperty(APP_CONFIG_FILE).isSet()) {
+            String appConfigFile = context.getProperty(APP_CONFIG_FILE).evaluateAttributeExpressions().getValue();
+            try (
+                Reader reader = new FileReader(appConfigFile);
+            ) {
+                boxConfig = BoxConfig.readFrom(reader);
+            } catch (FileNotFoundException e) {
+                throw new ProcessException("Couldn't find Box config file", e);
+            } catch (IOException e) {
+                throw new ProcessException("Couldn't read Box config file", e);
+            }
+        } else {
+            String appConfig = context.getProperty(APP_CONFIG_JSON).evaluateAttributeExpressions().getValue();
+            boxConfig = BoxConfig.readFrom(appConfig);
+        }
+
+        api = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(boxConfig);
+
+        api.asUser(accountId);
+
+        return api;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 0000000000..b5a3654c2d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/docs/org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService/additionalDetails.html b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/docs/org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService/additionalDetails.html
new file mode 100644
index 0000000000..e66f9eba0f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/docs/org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService/additionalDetails.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--
+  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.
+-->
+<head>
+    <meta charset="utf-8" />
+    <title>JsonConfigBasedBoxClientService</title>
+
+    <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css" />
+</head>
+<body>
+
+<h1>Setting up a Box App</h1>
+
+<p>This processor requires a pre-configured App under the Account owning the resources being accessed (https://app.box.com/developers/console).</p>
+<p>The App should have the following configuration:</p>
+<ul>
+    <li>
+        If you create a new App, select 'Server Authentication (with JWT)' authentication method.
+        <br/>If you want to use an existing App, choose one with 'OAuth 2.0 with JSON Web Tokens (Server Authentication)' as Authentication method.
+    </li>
+    <li>Should have a 'Client ID' and 'Client Secret'.</li>
+    <li>'App Access Level' should be 'App + Enterprise Access'.</li>
+    <li>'Application Scopes' should have 'Write all files and folders in Box' enabled.</li>
+    <li>'Advanced Features' should have 'Generate user access tokens' and 'Make API calls using the as-user header' enabled.</li>
+    <li>
+        Under 'Add and Manage Public Keys' generate a Public/Private Keypair and download the configuration JSON file (under App Settings). The full path of this file should be set in the 'Box Config File' property.
+        <br/>Note that you can only download the configuration JSON with the keypair details only once, when you generate the keypair. Also this is the only time Box will show you the private key.
+        <br/>If you want to download the configuration JSON file later (under 'App Settings') - or if you want to use your own keypair - after you download it you need to edit the file and add the keypair details manually.
+    </li>
+    <li>
+        After all settings are done, the App needs to be reauthorized on the admin page. ('Reauthorize App' at https://app.box.com/master/custom-apps.)
+        <br/>If the app is configured for the first time it needs to be added ('Add App') before it can be authorized.
+    </li>
+</ul>
+</body>
+</html>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceIT.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceIT.java
new file mode 100644
index 0000000000..9cd9411b52
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceIT.java
@@ -0,0 +1,87 @@
+/*
+ * 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.box.controllerservices;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxFolder;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Set the following constants before running:<br />
+ * <br />
+ * FOLDER_ID - The ID of a Folder the test can use to retrieve metadata for.<br />
+ * ACCOUNT_ID - The ID of the Account owning the Folder.<br />
+ * APP_CONFIG_FILE - An App Settings Configuration JSON file.
+ * Read nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/main/resources/docs/org.apache.nifi.box.controllerservices.JsonConfigBasedBoxClientService/additionalDetails.html for details.<br />
+ */
+public class JsonConfigBasedBoxClientServiceIT {
+    static final String FOLDER_ID = "";
+    static final String ACCOUNT_ID = "";
+    static final String APP_CONFIG_FILE = "";
+
+    protected JsonConfigBasedBoxClientService testSubject;
+    protected TestRunner testRunner;
+
+    @BeforeEach
+    protected void init() throws Exception {
+        testSubject = new JsonConfigBasedBoxClientService();
+
+        Processor dummyProcessor = mock(Processor.class);
+        testRunner = TestRunners.newTestRunner(dummyProcessor);
+
+        testRunner.addControllerService(testSubject.getClass().getSimpleName(), testSubject);
+    }
+
+    @Test
+    void testWithAppConfigJson() throws Exception {
+        // GIVEN
+        String appConfigJson = new String(Files.readAllBytes(Paths.get(APP_CONFIG_FILE)));
+
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, ACCOUNT_ID);
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_JSON, appConfigJson);
+        testRunner.enableControllerService(testSubject);
+
+        BoxAPIConnection boxAPIConnection = testSubject.getBoxApiConnection();
+
+        // WHEN
+        // THEN
+        new BoxFolder(boxAPIConnection, FOLDER_ID).getInfo("name").getName();
+    }
+
+    @Test
+    void testWithAppConfigFile() throws Exception {
+        // GIVEN
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, ACCOUNT_ID);
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_FILE, APP_CONFIG_FILE);
+        testRunner.enableControllerService(testSubject);
+
+        BoxAPIConnection boxAPIConnection = testSubject.getBoxApiConnection();
+
+        // WHEN
+        // THEN
+        new BoxFolder(boxAPIConnection, FOLDER_ID).getInfo("name").getName();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceTestRunnerTest.java b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceTestRunnerTest.java
new file mode 100644
index 0000000000..55acfbe149
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/nifi-box-services/src/test/java/org/apache/nifi/box/controllerservices/JsonConfigBasedBoxClientServiceTestRunnerTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.box.controllerservices;
+
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.Mockito.mock;
+
+public class JsonConfigBasedBoxClientServiceTestRunnerTest {
+    private JsonConfigBasedBoxClientService testSubject;
+    private TestRunner testRunner;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        testSubject = new JsonConfigBasedBoxClientService();
+
+        Processor dummyProcessor = mock(Processor.class);
+        testRunner = TestRunners.newTestRunner(dummyProcessor);
+
+        testRunner.addControllerService(testSubject.getClass().getSimpleName(), testSubject);
+    }
+
+    @Test
+    void validWhenAppConfigFileIsSet() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, "account_id");
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_FILE, "pom.xml");
+        testRunner.assertValid(testSubject);
+    }
+
+    @Test
+    void validWhenAppConfigJsonIsSet() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, "account_id");
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_JSON, "{}");
+        testRunner.assertValid(testSubject);
+    }
+
+    @Test
+    void invalidWhenAccountIdIsMissing() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_JSON, "{}");
+        testRunner.assertNotValid(testSubject);
+    }
+
+    @Test
+    void invalidWhenAppConfigFileAndAppConfigJsonIsMissing() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, "account_id");
+        testRunner.assertNotValid(testSubject);
+    }
+
+    @Test
+    void invalidWhenBothAppConfigFileAndAppConfigJsonIsSet() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, "account_id");
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_FILE, "pom.xml");
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_JSON, "{}");
+        testRunner.assertNotValid(testSubject);
+    }
+
+    @Test
+    void invalidWhenAppConfigJsonIsNotValid() {
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.ACCOUNT_ID, "account_id");
+        testRunner.setProperty(testSubject, JsonConfigBasedBoxClientService.APP_CONFIG_JSON, "not_valid_json_string");
+        testRunner.assertNotValid(testSubject);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-box-bundle/pom.xml b/nifi-nar-bundles/nifi-box-bundle/pom.xml
new file mode 100644
index 0000000000..c63a5a9c75
--- /dev/null
+++ b/nifi-nar-bundles/nifi-box-bundle/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-nar-bundles</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-box-bundle</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>nifi-box-nar</module>
+        <module>nifi-box-processors</module>
+        <module>nifi-box-services</module>
+        <module>nifi-box-services-api-nar</module>
+        <module>nifi-box-services-api</module>
+        <module>nifi-box-services-nar</module>
+    </modules>
+</project>
diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml
index 41cfc449b4..c6e743ab75 100755
--- a/nifi-nar-bundles/pom.xml
+++ b/nifi-nar-bundles/pom.xml
@@ -114,6 +114,7 @@
         <module>nifi-dropbox-bundle</module>
         <module>nifi-airtable-bundle</module>
         <module>nifi-workday-bundle</module>
+        <module>nifi-box-bundle</module>
     </modules>
 
     <build>