You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2023/08/29 20:57:14 UTC

[nifi] branch main updated: NIFI-8650 Added Flow Analysis Rules

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

exceptionfactory 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 d4147ca442 NIFI-8650 Added Flow Analysis Rules
d4147ca442 is described below

commit d4147ca442087f8bd7d73decbcb03780c80a0d50
Author: Tamas Palfy <ta...@gmail.com>
AuthorDate: Wed Mar 17 19:24:44 2021 +0100

    NIFI-8650 Added Flow Analysis Rules
    
    - Added new extensible Component Type: FlowAnalysisRule
    - Added DisallowComponentType Rule implementation
    - Flow Analysis Rules can be managed from the UI under Controller Settings -> Flow Analysis Rules
    - Flow Analysis Rules can be set up with an enforcement policy of WARN or ENFORCE
    - Flow Analysis Rules can evaluate an individual Component or a Process Group
    
    This closes #7191
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../java/org/apache/nifi/action/Component.java     |    1 +
 .../apache/nifi/annotation/behavior/Stateful.java  |    2 +-
 .../documentation/CapabilityDescription.java       |    3 +-
 .../nifi/annotation/documentation/SeeAlso.java     |    3 +-
 .../apache/nifi/annotation/documentation/Tags.java |    3 +-
 .../apache/nifi/annotation/lifecycle/OnAdded.java  |    5 +-
 .../nifi/annotation/lifecycle/OnRemoved.java       |    5 +-
 .../apache/nifi/controller/ControllerService.java  |    3 +-
 .../VersionedControllerServiceLookup.java}         |   16 +-
 .../documentation/AbstractDocumentationWriter.java |   11 +
 .../apache/nifi/documentation/ExtensionType.java   |    2 +
 ...ationFlowAnalysisRuleInitializationContext.java |   61 +
 .../java/org/apache/nifi/flow/ComponentType.java   |    1 +
 .../nifi/flow/VersionedConfigurableComponent.java  |    2 +-
 .../nifi/flow/VersionedFlowAnalysisRule.java       |   50 +
 .../nifi/flow/VersionedPropertyDescriptor.java     |   10 +
 .../nifi/flowanalysis/AbstractAnalysisResult.java  |   67 +
 .../flowanalysis/AbstractFlowAnalysisRule.java     |   53 +
 .../nifi/flowanalysis/ComponentAnalysisResult.java |   45 +
 .../EnforcementPolicy.java}                        |   20 +-
 .../nifi/flowanalysis/FlowAnalysisContext.java     |   50 +
 .../apache/nifi/flowanalysis/FlowAnalysisRule.java |   69 +
 .../nifi/flowanalysis/FlowAnalysisRuleContext.java |   53 +
 .../FlowAnalysisRuleInitializationContext.java     |   34 +-
 .../FlowAnalysisRuleState.java}                    |   13 +-
 .../nifi/flowanalysis/GroupAnalysisResult.java     |  112 ++
 .../flowanalysis/VerifiableFlowAnalysisRule.java   |   66 +
 .../org/apache/nifi/reporting/ComponentType.java   |    5 +
 .../java/org/apache/nifi/util/NiFiProperties.java  |    3 +
 .../images/add-flow-analysis-rule-window.png       |  Bin 0 -> 83135 bytes
 .../configure-flow-analysis-rule-properties.png    |  Bin 0 -> 81126 bytes
 .../configure-flow-analysis-rule-settings.png      |  Bin 0 -> 79023 bytes
 .../flow-analysis-rules-configure-buttons.png      |  Bin 0 -> 17828 bytes
 .../images/flow-analysis-rules-info-buttons.png    |  Bin 0 -> 13352 bytes
 .../asciidoc/images/flow-analysis-rules-tab.png    |  Bin 0 -> 171567 bytes
 .../main/asciidoc/images/settings-general-tab.png  |  Bin 24008 -> 73516 bytes
 nifi-docs/src/main/asciidoc/user-guide.adoc        |   59 +-
 .../nifi/web/api/dto/AnalyzeFlowRequestDTO.java}   |   36 +-
 .../api/dto/AnalyzeFlowRequestUpdateStepDTO.java   |   12 +-
 .../nifi/web/api/dto/FlowAnalysisRuleDTO.java      |  293 ++++
 .../web/api/dto/FlowAnalysisRuleViolationDTO.java  |  153 +++
 .../api/dto/status/FlowAnalysisRuleStatusDTO.java  |   27 +-
 .../web/api/entity/AnalyzeFlowRequestEntity.java   |   44 +
 .../web/api/entity/ControllerBulletinsEntity.java  |   14 +
 .../web/api/entity/FlowAnalysisResultEntity.java   |   59 +
 .../web/api/entity/FlowAnalysisRuleEntity.java     |   79 ++
 .../entity/FlowAnalysisRuleRunStatusEntity.java}   |   40 +-
 .../api/entity/FlowAnalysisRuleTypesEntity.java    |   43 +
 .../web/api/entity/FlowAnalysisRulesEntity.java}   |   33 +-
 .../nifi/web/api/entity/RuleViolationEntity.java   |   87 ++
 .../apache/nifi/documentation/DocGenerator.java    |    2 +
 .../authorization/resource/ResourceFactory.java    |    3 +
 .../nifi/authorization/resource/ResourceType.java  |    1 +
 .../http/StandardHttpResponseMapper.java           |   10 +
 .../AnalyzeFlowRequestEndpointMerger.java          |   96 ++
 .../endpoints/ComponentStateEndpointMerger.java    |    4 +-
 .../ControllerBulletinsEndpointMerger.java         |   15 +
 .../http/endpoints/FlowAnalysisEndpointMerger.java |   62 +
 .../endpoints/FlowAnalysisRuleEndpointMerger.java  |   98 ++
 .../FlowAnalysisRuleTypesEndpointMerger.java       |   51 +
 .../endpoints/FlowAnalysisRulesEndpointMerger.java |   71 +
 .../endpoints/RuleViolationEndpointMerger.java     |   50 +
 .../http/endpoints/VerifyConfigEndpointMerger.java |    2 +
 .../manager/FlowAnalysisResultEntityMerger.java    |   74 +
 .../manager/FlowAnalysisRuleEntityMerger.java      |  109 ++
 .../manager/FlowAnalysisRulesEntityMerger.java     |   40 +
 .../apache/nifi/cluster/manager/StatusMerger.java  |   13 +
 .../AnalyzeFlowRequestEndpointMergerTest.java      |  163 +++
 .../FlowAnalysisResultEntityMergerTest.java        |  258 ++++
 .../nifi/controller/flow/AbstractFlowManager.java  |   98 +-
 .../AbstractFlowAnalysisRuleContext.java           |  104 ++
 .../flowanalysis/AbstractFlowAnalysisRuleNode.java |  376 +++++
 .../flowanalysis/FlowAnalysisRuleDetails.java      |   50 +
 .../StandardFlowAnalysisInitializationContext.java |  110 ++
 .../service/StandardControllerServiceNode.java     |    6 +
 .../service/StandardControllerServiceProvider.java |   63 +-
 .../nifi/events/VolatileBulletinRepository.java    |    4 +
 .../StandardVersionedComponentSynchronizer.java    |   79 ++
 .../VersionedComponentSynchronizer.java            |   17 +
 .../StandardValidationContextFactory.java          |   30 +-
 .../flow/mapping/NiFiRegistryFlowMapper.java       |   25 +-
 .../flow/mapping/StandardComparableDataFlow.java   |   23 +-
 .../nifi/controller/AbstractComponentNode.java     |   25 +
 .../nifi/controller/FlowAnalysisRuleNode.java      |   91 ++
 .../org/apache/nifi/controller/ProcessorNode.java  |    7 +
 .../apache/nifi/controller/ReloadComponent.java    |   12 +
 .../nifi/controller/ValidationContextFactory.java  |    5 +
 .../apache/nifi/controller/flow/FlowManager.java   |   34 +-
 .../nifi/controller/flow/VersionedDataflow.java    |   10 +
 .../FlowAnalysisRuleInstantiationException.java    |   16 +-
 .../flowanalysis/FlowAnalysisRuleProvider.java     |   99 ++
 .../nifi/controller/flowanalysis/FlowAnalyzer.java |   49 +
 .../registry/flow/mapping/ComponentIdLookup.java   |   17 +-
 .../mapping/VersionedComponentStateLookup.java     |   20 +
 .../org/apache/nifi/validation/RuleViolation.java  |  193 +++
 .../nifi/validation/RuleViolationsManager.java     |   84 ++
 .../validation/TriggerValidationTask.java          |    4 +
 .../apache/nifi/controller/ExtensionBuilder.java   |  120 +-
 .../org/apache/nifi/controller/FlowController.java |  130 +-
 .../nifi/controller/StandardReloadComponent.java   |   51 +
 .../nifi/controller/XmlFlowSynchronizer.java       |  154 ++-
 .../nifi/controller/flow/StandardFlowManager.java  |   75 +
 .../controller/flowanalysis/FlowAnalysisUtil.java  |   54 +
 .../flowanalysis/StandardFlowAnalysisContext.java  |   82 ++
 .../StandardFlowAnalysisRuleContext.java           |   62 +
 .../flowanalysis/StandardFlowAnalysisRuleNode.java |  111 ++
 .../inheritance/BundleCompatibilityCheck.java      |   14 +
 .../serialization/AffectedComponentSet.java        |   34 +-
 .../serialization/ComponentSetFilter.java          |    8 +
 .../serialization/FlowFromDOMFactory.java          |   19 +
 .../serialization/RunningComponentSetFilter.java   |   10 +
 .../serialization/StandardFlowSerializer.java      |   24 +
 .../serialization/VersionedDataflowMapper.java     |   25 +
 .../serialization/VersionedFlowSynchronizer.java   |   86 +-
 .../nifi/fingerprint/FingerprintFactory.java       |   52 +
 .../nifi/flowanalysis/GhostFlowAnalysisRule.java   |   90 ++
 .../nifi/flowanalysis/StandardFlowAnalyzer.java    |  252 ++++
 .../nifi/flowanalysis/TriggerFlowAnalysisTask.java |   47 +
 .../nifi/logging/FlowAnalysisRuleLogObserver.java  |   50 +
 .../nifi/spring/FlowControllerFactoryBean.java     |   13 +-
 .../apache/nifi/validation/RuleViolationKey.java   |   56 +
 .../validation/StandardRuleViolationsManager.java  |  192 +++
 .../src/main/resources/FlowConfiguration.xsd       |   38 +
 .../src/main/resources/nifi-context.xml            |    4 +
 .../nifi/controller/StandardFlowServiceTest.java   |    2 +-
 .../nifi/controller/StandardProcessorNodeIT.java   |  186 +--
 .../apache/nifi/controller/TestFlowController.java |    7 +-
 .../reporting/TestStandardReportingContext.java    |    2 +-
 .../scheduling/ProcessorLifecycleIT.java           |    2 +-
 .../serialization/StandardFlowSerializerTest.java  |    2 +-
 .../nifi/flowanalysis/AbstractFlowAnalysisIT.java  |  126 ++
 .../flowanalysis/FlowAnalysisRuleLifeCycleIT.java  |  197 +++
 .../apache/nifi/flowanalysis/FlowAnalyzerIT.java   | 1437 ++++++++++++++++++++
 .../nifi/integration/FrameworkIntegrationTest.java |    9 +-
 .../flowanalysis/DelegateFlowAnalysisRule.java     |   64 +
 ...pache.nifi.controller.flowanalysis.FlowAnalyzer |   14 +
 .../conf/flow-json-missing-component-id.json       |    1 +
 .../ConfigurableComponentInitializerFactory.java   |    3 +
 .../nifi/init/FlowAnalysisRuleInitializer.java     |   61 +
 .../MockFlowAnalysisRuleInitializationContext.java |   55 +
 .../org/apache/nifi/nar/ExtensionDefinition.java   |    2 +-
 .../nifi/nar/NarThreadContextClassLoader.java      |    2 +
 .../nar/StandardExtensionDiscoveringManager.java   |    4 +-
 .../apache/nifi/headless/HeadlessNiFiServer.java   |    3 +-
 .../main/java/org/apache/nifi/nar/NarUnpacker.java |    4 +-
 .../nifi-framework/nifi-resources/pom.xml          |    3 +
 .../src/main/resources/conf/nifi.properties        |    3 +
 .../apache/nifi/audit/ComponentStateAuditor.java   |   47 +
 .../apache/nifi/audit/FlowAnalysisRuleAuditor.java |  373 +++++
 .../nifi/authorization/AuthorizableHolder.java     |   17 +-
 .../nifi/authorization/AuthorizableLookup.java     |    8 +
 .../nifi/authorization/ComponentAuthorizable.java  |    9 +-
 .../nifi/authorization/ConnectionAuthorizable.java |    9 +-
 .../authorization/ProcessGroupAuthorizable.java    |    9 +-
 .../nifi/authorization/PublicPortAuthorizable.java |   10 +-
 .../authorization/StandardAuthorizableLookup.java  |   74 +
 .../org/apache/nifi/web/NiFiServiceFacade.java     |  157 ++-
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  332 ++++-
 .../apache/nifi/web/api/ControllerResource.java    |  965 ++++++++++++-
 .../java/org/apache/nifi/web/api/FlowResource.java |  139 ++
 .../apache/nifi/web/api/ProcessGroupResource.java  |  266 ++++
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |   90 ++
 .../org/apache/nifi/web/api/dto/EntityFactory.java |   31 +
 .../nifi/web/controller/ControllerFacade.java      |   13 +
 .../org/apache/nifi/web/dao/ComponentStateDAO.java |   18 +
 .../apache/nifi/web/dao/FlowAnalysisRuleDAO.java   |  132 ++
 .../web/dao/impl/StandardComponentStateDAO.java    |   11 +
 .../web/dao/impl/StandardFlowAnalysisRuleDAO.java  |  365 +++++
 .../FlowAnalysisRuleProviderFactoryBean.java       |   54 +
 .../src/main/resources/nifi-web-api-context.xml    |   16 +-
 .../nifi/web/StandardNiFiServiceFacadeTest.java    |  141 +-
 .../nifi/web/docs/DocumentationController.java     |   10 +-
 .../src/main/webapp/WEB-INF/jsp/documentation.jsp  |   35 +
 .../src/main/webapp/js/application.js              |    5 +-
 .../nifi-framework/nifi-web/nifi-web-ui/pom.xml    |    2 +
 .../src/main/resources/filters/canvas.properties   |    1 +
 .../src/main/webapp/WEB-INF/pages/canvas.jsp       |    2 +
 .../canvas/flow-analysis-rule-configuration.jsp    |   86 ++
 .../canvas/new-flow-analysis-rule-dialog.jsp       |   61 +
 .../WEB-INF/partials/canvas/settings-content.jsp   |    5 +-
 .../nifi-web-ui/src/main/webapp/css/canvas.css     |    1 +
 .../webapp/css/new-flow-analysis-rule-dialog.css   |  105 ++
 .../webapp/js/nf/canvas/nf-canvas-bootstrap.js     |   12 +-
 .../webapp/js/nf/canvas/nf-component-version.js    |   20 +
 .../webapp/js/nf/canvas/nf-controller-service.js   |   48 +-
 .../webapp/js/nf/canvas/nf-flow-analysis-rule.js   |  790 +++++++++++
 .../src/main/webapp/js/nf/canvas/nf-settings.js    |  935 ++++++++++++-
 .../main/webapp/js/nf/history/nf-history-table.js  |    2 +-
 .../nifi-standard-bundle/nifi-standard-nar/pom.xml |    4 +
 .../pom.xml                                        |   35 +-
 .../flowanalysis/rules/DisallowComponentType.java  |   85 ++
 .../org.apache.nifi.flowanalysis.FlowAnalysisRule  |   16 +
 nifi-nar-bundles/nifi-standard-bundle/pom.xml      |    6 +
 .../registry/flow/diff/ComparableDataFlow.java     |    3 +
 .../nifi/registry/flow/diff/DifferenceType.java    |    5 +
 .../flow/diff/StandardComparableDataFlow.java      |   23 +-
 .../registry/flow/diff/StandardFlowComparator.java |   13 +
 .../src/main/webapp/js/application.js              |    5 +-
 .../java/org/apache/nifi/nar/ExtensionMapping.java |   29 +-
 .../stateless/engine/StatelessFlowManager.java     |   14 +
 .../stateless/engine/StatelessReloadComponent.java |    7 +
 .../flow/StandardStatelessDataflowFactory.java     |    7 +-
 ...ntrollerServiceReferencingFlowAnalysisRule.java |   46 +
 ...EnsureFlowAnalysisRuleConfigurationCorrect.java |  138 ++
 ...SensitiveDynamicPropertiesFlowAnalysisRule.java |   24 +-
 .../flowanalysis/StatefulFlowAnalysisRule.java     |   64 +
 .../org.apache.nifi.flowanalysis.FlowAnalysisRule  |   19 +
 .../impl/client/nifi/impl/JerseyFlowClient.java    |    2 +-
 208 files changed, 14107 insertions(+), 406 deletions(-)

diff --git a/nifi-api/src/main/java/org/apache/nifi/action/Component.java b/nifi-api/src/main/java/org/apache/nifi/action/Component.java
index 77c84a881e..9a651858f1 100644
--- a/nifi-api/src/main/java/org/apache/nifi/action/Component.java
+++ b/nifi-api/src/main/java/org/apache/nifi/action/Component.java
@@ -31,6 +31,7 @@ public enum Component {
     Connection,
     ControllerService,
     ReportingTask,
+    FlowAnalysisRule,
     FlowRegistryClient,
     ParameterContext,
     ParameterProvider,
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java b/nifi-api/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java
index 5d99cef5e5..d4c0ad5d0c 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java
@@ -28,7 +28,7 @@ import org.apache.nifi.components.state.StateManager;
 
 /**
  * <p>
- * Annotation that a Processor, ReportingTask, ParameterProvider, or Controller Service can use to indicate
+ * Annotation that a Processor, ReportingTask, FlowAnalysisRule, ParameterProvider, or Controller Service can use to indicate
  * that the component makes use of the {@link StateManager}. This annotation provides the
  * user with a description of what information is being stored so that the user is able to
  * understand what is shown to them and know what they are clearing should they choose to
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java
index 51272f05f1..5ac0e97959 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java
@@ -26,7 +26,8 @@ import java.lang.annotation.Target;
 /**
  * Annotation that may be placed on a {@link org.apache.nifi.processor.Processor Processor},
  * {@link org.apache.nifi.controller.ControllerService ControllerService},
- * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
  * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} allowing for a
  * description to be provided. This description can be provided to a user in
  * logs, UI, etc.
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java
index 0958751560..5eae15920c 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java
@@ -27,7 +27,8 @@ import org.apache.nifi.components.ConfigurableComponent;
 /**
  * Annotation that may be placed on a null {@link org.apache.nifi.processor.Processor Processor},
  * {@link org.apache.nifi.controller.ControllerService ControllerService},
- * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
  * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} that indicates
  * this component is related to the components listed.
  *
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/Tags.java b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/Tags.java
index cfde4e73f0..a22cb2813b 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/Tags.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/documentation/Tags.java
@@ -26,7 +26,8 @@ import java.lang.annotation.Target;
 /**
  * Annotation that can be applied to a {@link org.apache.nifi.processor.Processor Processor},
  * {@link org.apache.nifi.controller.ControllerService ControllerService},
- * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
  * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} in order to
  * associate tags (keywords) with the component. These tags do not affect the
  * component in any way but serve as additional documentation and can be used to
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java b/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java
index f3b04e941a..4933a3806b 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java
@@ -27,8 +27,9 @@ import java.lang.annotation.Target;
  * <p>
  * Marker annotation a {@link org.apache.nifi.processor.Processor Processor},
  * {@link org.apache.nifi.controller.ControllerService ControllerService},
- * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient}
- * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
  * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
  * can use to indicate a method should be called whenever the component is added
  * to the flow. This method will be called once for the entire life of a
diff --git a/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java b/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java
index 3e6dcdaa38..172979e1af 100644
--- a/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java
+++ b/nifi-api/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java
@@ -29,8 +29,9 @@ import org.apache.nifi.processor.ProcessContext;
  * <p>
  * Marker annotation a {@link org.apache.nifi.processor.Processor Processor},
  * {@link org.apache.nifi.controller.ControllerService ControllerService},
- * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient}
- * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
  * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
  * can use to indicate a method should be called whenever the component is
  * removed from the flow. This method will be called once for the entire life of
diff --git a/nifi-api/src/main/java/org/apache/nifi/controller/ControllerService.java b/nifi-api/src/main/java/org/apache/nifi/controller/ControllerService.java
index d5a9279de6..ac746e0fc5 100644
--- a/nifi-api/src/main/java/org/apache/nifi/controller/ControllerService.java
+++ b/nifi-api/src/main/java/org/apache/nifi/controller/ControllerService.java
@@ -26,11 +26,12 @@ import org.apache.nifi.processor.ProcessSessionFactory;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.reporting.ReportingTask;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 
 /**
  * <p>
  * This interface provides a mechanism for creating services that are shared
- * among all {@link Processor}s, {@link ReportingTask}s, {@link ParameterProvider}s and other
+ * among all {@link Processor}s, {@link ReportingTask}s, {@link FlowAnalysisRule}s, {@link ParameterProvider}s and other
  * {@code ControllerService}s.
  * </p>
  *
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-api/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java
similarity index 67%
copy from nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
copy to nifi-api/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java
index c0eda7e230..899bcbcbb0 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java
@@ -14,14 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.documentation;
+package org.apache.nifi.controller;
 
-public enum ExtensionType {
-    PROCESSOR,
+import org.apache.nifi.flow.VersionedControllerService;
 
-    CONTROLLER_SERVICE,
+public interface VersionedControllerServiceLookup {
 
-    REPORTING_TASK,
-
-    PARAMETER_PROVIDER;
+    /**
+     * @param serviceIdentifier of controller service
+     * @return the VersionedControllerService with the given
+     * identifier
+     */
+    VersionedControllerService getVersionedControllerService(String serviceIdentifier);
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java b/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java
index 7015fba8f5..f2b311b1a5 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java
+++ b/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java
@@ -44,9 +44,11 @@ import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.documentation.init.DocumentationControllerServiceInitializationContext;
+import org.apache.nifi.documentation.init.DocumentationFlowAnalysisRuleInitializationContext;
 import org.apache.nifi.documentation.init.DocumentationParameterProviderInitializationContext;
 import org.apache.nifi.documentation.init.DocumentationProcessorInitializationContext;
 import org.apache.nifi.documentation.init.DocumentationReportingInitializationContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.parameter.ParameterProvider;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.processor.Relationship;
@@ -87,6 +89,8 @@ public abstract class AbstractDocumentationWriter implements ExtensionDocumentat
                 initialize((ControllerService) component);
             } else if (component instanceof ReportingTask) {
                 initialize((ReportingTask) component);
+            } else if (component instanceof FlowAnalysisRule) {
+                initialize((FlowAnalysisRule) component);
             } else if (component instanceof ParameterProvider) {
                 initialize((ParameterProvider) component);
             }
@@ -107,6 +111,10 @@ public abstract class AbstractDocumentationWriter implements ExtensionDocumentat
         reportingTask.initialize(new DocumentationReportingInitializationContext());
     }
 
+    protected void initialize(final FlowAnalysisRule flowAnalysisRule) throws InitializationException {
+        flowAnalysisRule.initialize(new DocumentationFlowAnalysisRuleInitializationContext());
+    }
+
     protected void initialize(final ParameterProvider parameterProvider) throws InitializationException {
         parameterProvider.initialize(new DocumentationParameterProviderInitializationContext());
     }
@@ -260,6 +268,9 @@ public abstract class AbstractDocumentationWriter implements ExtensionDocumentat
         if (component instanceof ReportingTask) {
             return ExtensionType.REPORTING_TASK;
         }
+        if (component instanceof ReportingTask) {
+            return ExtensionType.FLOW_ANALYSIS_RULE;
+        }
         if (component instanceof ParameterProvider) {
             return ExtensionType.PARAMETER_PROVIDER;
         }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
index c0eda7e230..00d7058f9a 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
@@ -23,5 +23,7 @@ public enum ExtensionType {
 
     REPORTING_TASK,
 
+    FLOW_ANALYSIS_RULE,
+
     PARAMETER_PROVIDER;
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java
new file mode 100644
index 0000000000..9082b5a014
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java
@@ -0,0 +1,61 @@
+/*
+ * 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.documentation.init;
+
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleInitializationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.io.File;
+import java.util.UUID;
+
+public class DocumentationFlowAnalysisRuleInitializationContext implements FlowAnalysisRuleInitializationContext {
+    private final String id = UUID.randomUUID().toString();
+    private final ComponentLog componentLog = new NopComponentLog();
+    private final NodeTypeProvider nodeTypeProvider = new StandaloneNodeTypeProvider();
+    private final String name = "name";
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public ComponentLog getLogger() {
+        return componentLog;
+    }
+
+    @Override
+    public NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+
+    @Override
+    public String getKerberosServicePrincipal() {
+        return null;
+    }
+
+    @Override
+    public File getKerberosServiceKeytab() {
+        return null;
+    }
+
+    @Override
+    public File getKerberosConfigurationFile() {
+        return null;
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flow/ComponentType.java b/nifi-api/src/main/java/org/apache/nifi/flow/ComponentType.java
index 954fcc5483..da0450ab1d 100644
--- a/nifi-api/src/main/java/org/apache/nifi/flow/ComponentType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flow/ComponentType.java
@@ -31,6 +31,7 @@ public enum ComponentType {
     LABEL("Label"),
     CONTROLLER_SERVICE("Controller Service"),
     REPORTING_TASK("Reporting Task"),
+    FLOW_ANALYSIS_RULE("Flow Analysis Rule"),
     PARAMETER_CONTEXT("Parameter Context"),
     PARAMETER_PROVIDER("Parameter Provider"),
     TEMPLATE("Template"),
diff --git a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
index dc3265ee32..60f7b679fa 100644
--- a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
@@ -25,7 +25,7 @@ public interface VersionedConfigurableComponent {
 
     Map<String, VersionedPropertyDescriptor> getPropertyDescriptors();
 
-    void setPropertyDescriptors(Map<String,VersionedPropertyDescriptor> propertyDescriptors);
+    void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors);
 
     Map<String,String> getProperties();
 
diff --git a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java
new file mode 100644
index 0000000000..233fd4adb0
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java
@@ -0,0 +1,50 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
+
+public class VersionedFlowAnalysisRule extends VersionedConfigurableExtension {
+
+    private ScheduledState scheduledState;
+    private EnforcementPolicy enforcementPolicy;
+
+    @ApiModelProperty("How to handle violations.")
+    public EnforcementPolicy getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    public void setEnforcementPolicy(EnforcementPolicy enforcementPolicy) {
+        this.enforcementPolicy = enforcementPolicy;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.FLOW_ANALYSIS_RULE;
+    }
+
+    @ApiModelProperty("Indicates the scheduled state for the flow analysis rule")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(final ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java
index 035291ce89..25ded8cf54 100644
--- a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java
@@ -24,6 +24,7 @@ public class VersionedPropertyDescriptor {
     private String displayName;
     private boolean identifiesControllerService;
     private boolean sensitive;
+    private boolean dynamic;
     private VersionedResourceDefinition resourceDefinition;
 
     @ApiModelProperty("The name of the property")
@@ -62,6 +63,15 @@ public class VersionedPropertyDescriptor {
         this.sensitive = sensitive;
     }
 
+    @ApiModelProperty("Whether or not the property is user-defined")
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+    public void setDynamic(boolean dynamic) {
+        this.dynamic = dynamic;
+    }
+
     @ApiModelProperty("Returns the Resource Definition that defines which type(s) of resource(s) this property references, if any")
     public VersionedResourceDefinition getResourceDefinition() {
         return resourceDefinition;
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java
new file mode 100644
index 0000000000..0eccba1ba7
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java
@@ -0,0 +1,67 @@
+/*
+ * 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.flowanalysis;
+
+import java.util.StringJoiner;
+
+/**
+ * Abstract class holding information about a {@link FlowAnalysisRule} violation.
+ */
+public abstract class AbstractAnalysisResult {
+    protected final String issueId;
+    protected final String message;
+    protected final String explanation;
+
+    protected AbstractAnalysisResult(final String issueId, final String message, final String explanation) {
+        this.issueId = issueId;
+        this.message = message;
+        this.explanation = explanation;
+    }
+
+    /**
+     * @return A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     * Newer analysis runs may produce a result with the same issueId in which case the old one will
+     * be overwritten (or recreated if it is the same in other aspects as well).
+     * However, if the previous result was disabled the new one will be disabled as well.
+     */
+    public String getIssueId() {
+        return issueId;
+    }
+
+    /**
+     * @return the rule violation message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * @return a detailed explanation of the nature of the violation
+     */
+    public String getExplanation() {
+        return explanation;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
+            .add("issueId='" + issueId + "'")
+            .add("message='" + message + "'")
+            .add("explanation='" + explanation + "'")
+            .toString();
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.java
new file mode 100644
index 0000000000..e1b2ae96ef
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.java
@@ -0,0 +1,53 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.reporting.InitializationException;
+
+public abstract class AbstractFlowAnalysisRule extends AbstractConfigurableComponent implements FlowAnalysisRule {
+    private String identifier;
+    private String description;
+
+    private ComponentLog logger;
+
+    @Override
+    public void initialize(FlowAnalysisRuleInitializationContext context) throws InitializationException {
+        identifier = context.getIdentifier();
+        description = getClass().getSimpleName() + "[id=" + identifier + "]";
+        logger = context.getLogger();
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String toString() {
+        return description;
+    }
+
+    /**
+     * @return the logger that has been provided to the component by the
+     * framework in its initialize method
+     */
+    protected ComponentLog getLogger() {
+        return logger;
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.java
new file mode 100644
index 0000000000..047c4345d5
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.java
@@ -0,0 +1,45 @@
+/*
+ * 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.flowanalysis;
+
+/**
+ * Holds information about a component violating a {@link FlowAnalysisRule}
+ */
+public class ComponentAnalysisResult extends AbstractAnalysisResult {
+    /**
+     * @param issueId A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                be overwritten (or recreated if it is the same in other aspects as well).
+     *                However, if the previous result was disabled the new one will be disabled as well.
+     * @param message A violation message
+     */
+    public ComponentAnalysisResult(final String issueId, final String message) {
+        this(issueId, message, null);
+    }
+
+    /**
+     * @param issueId A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                be overwritten (or recreated if it is the same in other aspects as well).
+     *                However, if the previous result was disabled the new one will be disabled as well.
+     * @param message A violation message
+     * @param explanation A detailed explanation of the violation
+     */
+    public ComponentAnalysisResult(final String issueId, final String message, final String explanation) {
+        super(issueId, message, explanation);
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java
similarity index 69%
copy from nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
copy to nifi-api/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java
index c0eda7e230..c1cda44119 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java
@@ -14,14 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.documentation;
+package org.apache.nifi.flowanalysis;
 
-public enum ExtensionType {
-    PROCESSOR,
-
-    CONTROLLER_SERVICE,
-
-    REPORTING_TASK,
-
-    PARAMETER_PROVIDER;
+public enum EnforcementPolicy {
+    /**
+     * Rules with this enforcement policy only warns about rule violations.
+     */
+    WARN,
+    /**
+     * Rules with this enforcement policy also invalidate the corresponding components and fixing
+     *  these problems are to be considered mandatory.
+     */
+    ENFORCE;
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java
new file mode 100644
index 0000000000..330cba842a
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.controller.VersionedControllerServiceLookup;
+
+import java.util.Optional;
+
+/**
+ * Used for accessing flow- or other analysis-related information
+ */
+public interface FlowAnalysisContext {
+    /**
+     * @return the {@link VersionedControllerServiceLookup} which can be used to obtain
+     * Versioned Controller Services during flow analysis
+     */
+    VersionedControllerServiceLookup getVersionedControllerServiceLookup();
+
+    /**
+     * @return the currently configured maximum number of threads that can be
+     * used for executing processors at any given time.
+     */
+    int getMaxTimerDrivenThreadCount();
+
+    /**
+     * @return <code>true</code> if this instance of NiFi is configured to be part of a cluster, <code>false</code>
+     * if this instance of NiFi is a standalone instance
+     */
+    boolean isClustered();
+
+    /**
+     * @return an Optional with the ID of this node in the cluster, or empty if either this node is not clustered or the Node Identifier
+     * has not yet been established
+     */
+    Optional<String> getClusterNodeIdentifier();
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.java
new file mode 100644
index 0000000000..78aaf5ad81
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.java
@@ -0,0 +1,69 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A single rule that can analyze components or a flow (represented by a process group)
+ */
+public interface FlowAnalysisRule extends ConfigurableComponent {
+    /**
+     * Provides the Flow Analysis Rule with access to objects that may be of use
+     * throughout its lifecycle
+     *
+     * @param context see {@link FlowAnalysisRuleInitializationContext}
+     * @throws org.apache.nifi.reporting.InitializationException if unable to initialize
+     */
+    void initialize(FlowAnalysisRuleInitializationContext context) throws InitializationException;
+
+    /**
+     * Analyze a component provided by the framework.
+     * This is a callback method invoked by the framework.
+     * It should be expected that this method will be called with any and all available components.
+     *
+     * @param component the component to be analyzed
+     * @param context   see {@link FlowAnalysisRuleContext}
+     * @return a collection of {@link ComponentAnalysisResult} as the result of the analysis of the given component
+     */
+    default Collection<ComponentAnalysisResult> analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) {
+        return Collections.emptySet();
+    }
+
+    /**
+     * Analyze a flow or a part of it, represented by a process group.
+     * This is a callback method invoked by the framework.
+     * It should be expected that this method will be called by the root process group and all of its child process groups.
+     * In case a flow analysis is requested for a particular process group this method will be called for all it's child
+     * process groups as well.
+     *
+     * @param processGroup the process group to be analyzed
+     * @param context      see {@link FlowAnalysisRuleContext}
+     * @return a collection of {@link GroupAnalysisResult} as the result of the analysis.
+     * One {@link GroupAnalysisResult} in the collection can either refer to a component within the analyzed process group,
+     * to a child process group or the entirety of the process group
+     */
+    default Collection<GroupAnalysisResult> analyzeProcessGroup(VersionedProcessGroup processGroup, FlowAnalysisRuleContext context) {
+        return Collections.emptySet();
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.java
new file mode 100644
index 0000000000..06e905fa9a
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.context.PropertyContext;
+
+import java.util.Map;
+
+/**
+ * This interface provides a bridge between the NiFi Framework and a
+ * {@link FlowAnalysisRule}. This context allows a FlowAnalysisRule to access
+ * configuration supplied by the user.
+ */
+public interface FlowAnalysisRuleContext extends PropertyContext {
+    /**
+     * @return the name of the rule that is being triggered
+     */
+    String getRuleName();
+
+    /**
+     * @return a Map of all known {@link PropertyDescriptor}s to their
+     * configured properties. This Map will contain a <code>null</code> for any
+     * Property that has not been configured by the user, even if the
+     * PropertyDescriptor has a default value
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this component
+     */
+    StateManager getStateManager();
+
+    /**
+     * @return a FlowAnalysisContext that can be used to access flow- or other analysis-related information
+     */
+    FlowAnalysisContext getFlowAnalysisContext();
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java
similarity index 51%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
copy to nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java
index 89fbd6592c..08c375e3de 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java
@@ -14,27 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.authorization;
+package org.apache.nifi.flowanalysis;
 
-import org.apache.nifi.authorization.resource.Authorizable;
-import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
 
 /**
- * Authorizable for a PublicPort.
+ * Provides configuration information to a
+ * FlowAnalysisRule at the time of initialization
  */
-public interface PublicPortAuthorizable {
+public interface FlowAnalysisRuleInitializationContext extends KerberosContext {
+
+    /**
+     * @return the identifier for the FlowAnalysisRule
+     */
+    String getIdentifier();
+
     /**
-     * Returns the authorizable for this PublicGroupPort. Non null
-     *
-     * @return authorizable
+     * @return a logger that can be used to log important events in a standard
+     * way and generate bulletins when appropriate
      */
-    Authorizable getAuthorizable();
+    ComponentLog getLogger();
 
     /**
-     * Checks the authorization for the specified user.
-     *
-     * @param user user
-     * @return authorization result
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     * @since Apache NiFi 1.5.0
      */
-    AuthorizationResult checkAuthorization(NiFiUser user);
+    NodeTypeProvider getNodeTypeProvider();
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java
similarity index 84%
copy from nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
copy to nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java
index c0eda7e230..79731baaa8 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java
@@ -14,14 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.documentation;
+package org.apache.nifi.flowanalysis;
 
-public enum ExtensionType {
-    PROCESSOR,
-
-    CONTROLLER_SERVICE,
-
-    REPORTING_TASK,
-
-    PARAMETER_PROVIDER;
+public enum FlowAnalysisRuleState {
+    ENABLED,
+    DISABLED;
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java
new file mode 100644
index 0000000000..5366bf781f
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java
@@ -0,0 +1,112 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.flow.VersionedComponent;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Holds information about a {@link FlowAnalysisRule} violation after analyzing (a part of) the flow, represented by a process group.
+ * One such analysis can result in multiple instances of this class.
+ */
+public class GroupAnalysisResult extends AbstractAnalysisResult {
+    private final Optional<VersionedComponent> component;
+
+    private GroupAnalysisResult(final String issueId, final String message, final String explanation, final Optional<VersionedComponent> component) {
+        super(issueId, message, explanation);
+        this.component = component;
+    }
+
+    /**
+     * @return the component this result corresponds to or empty if this result corresponds to the entirety of the process group that was analyzed
+     */
+    public Optional<VersionedComponent> getComponent() {
+        return component;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
+                .add("issueId='" + issueId + "'")
+                .add("message='" + message + "'")
+                .add("explanation='" + explanation + "'")
+                .add("component='" + component + "'")
+                .toString();
+    }
+
+    /**
+     * Build a new analysis result tied to the currently analyzed process group
+     *
+     * @param issueId     A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                    Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                    be overwritten (or recreated if it is the same in other aspects as well).
+     *                    However, if the previous result was disabled the new one will be disabled as well.
+     * @param message     A violation message
+     * @return a Builder for a new analysis result instance tied to the currently analyzed process group
+     */
+    public static Builder forGroup(final String issueId, final String message) {
+        return new Builder(null, issueId, message);
+    }
+
+    /**
+     * Build a new analysis result tied to a component.
+     * Note that the result will be scoped to the process group of the component and not the currently analyzed group.
+     * This means that even when a new analysis is run against that process group, this result will become obsolete.
+     *
+     * @param component The component that this result is tied to
+     * @param issueId   A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                  Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                  be overwritten (or recreated if it is the same in other aspects as well).
+     *                  However, if the previous result was disabled the new one will be disabled as well.
+     * @param message   A violation message
+     * @return a Builder for a new analysis result tied to a component
+     */
+    public static Builder forComponent(final VersionedComponent component, final String issueId, final String message) {
+        return new Builder(component, issueId, message);
+    }
+
+    public static class Builder {
+        private final VersionedComponent component;
+        private final String issueId;
+        private final String message;
+        private String explanation;
+
+        private Builder(final VersionedComponent component, final String issueId, final String message) {
+            this.component = component;
+            this.issueId = issueId;
+            this.message = message;
+        }
+
+        /**
+         * @param explanation A detailed explanation of the violation
+         * @return this Builder
+         */
+        public Builder explanation(final String explanation) {
+            this.explanation = explanation;
+            return this;
+        }
+
+        /**
+         * @return the flow analysis result
+         */
+        public GroupAnalysisResult build() {
+            return new GroupAnalysisResult(issueId, message, explanation, Optional.ofNullable(component));
+        }
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.java b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.java
new file mode 100644
index 0000000000..aa6b08e71c
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.java
@@ -0,0 +1,66 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.util.List;
+
+/**
+ * <p>
+ *     Any Flow Analysis Rule that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Flow Analysis Rule. This allows the Flow Analysis Rule to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Flow Analysis Rule is not valid, it cannot be started. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when starting a component).
+ * </p>
+ *
+ * <p>
+ *     Verification is allowed to be run only when a Flow Analysis Rule is fully stopped. I.e., it has no active threads
+ *     and currently has a state of STOPPED. Therefore, any initialization logic that may need to be performed
+ *     before the Flow Analysis Rule is triggered may also be required for verification. However, the framework is not responsible
+ *     for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ConfigurationContext, ComponentLog)} itself.
+ *     The {@link #verify(ConfigurationContext, ComponentLog)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableFlowAnalysisRule {
+
+    /**
+     * Verifies that the configuration defined by the given ConfigurationContext is valid.
+     *
+     * @param context the Configuration Context that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     *
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ConfigurationContext context, ComponentLog verificationLogger);
+
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/reporting/ComponentType.java b/nifi-api/src/main/java/org/apache/nifi/reporting/ComponentType.java
index 46966ce76c..95cec812a2 100644
--- a/nifi-api/src/main/java/org/apache/nifi/reporting/ComponentType.java
+++ b/nifi-api/src/main/java/org/apache/nifi/reporting/ComponentType.java
@@ -46,6 +46,11 @@ public enum ComponentType {
      */
     REPORTING_TASK,
 
+    /**
+     * Bulletin is associated with a Flow Analysis Rule
+     */
+    FLOW_ANALYSIS_RULE,
+
     /**
      * Bulletin is associated with a Process Group
      */
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index 404f7929b8..5eb1164b25 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -304,6 +304,9 @@ public class NiFiProperties extends ApplicationProperties {
     public static final String ANALYTICS_CONNECTION_MODEL_SCORE_NAME = "nifi.analytics.connection.model.score.name";
     public static final String ANALYTICS_CONNECTION_MODEL_SCORE_THRESHOLD = "nifi.analytics.connection.model.score.threshold";
 
+    // flow analysis properties
+    public static final String BACKGROUND_FLOW_ANALYSIS_SCHEDULE = "nifi.flow.analysis.background.task.schedule";
+
     // runtime monitoring properties
     public static final String MONITOR_LONG_RUNNING_TASK_SCHEDULE = "nifi.monitor.long.running.task.schedule";
     public static final String MONITOR_LONG_RUNNING_TASK_THRESHOLD = "nifi.monitor.long.running.task.threshold";
diff --git a/nifi-docs/src/main/asciidoc/images/add-flow-analysis-rule-window.png b/nifi-docs/src/main/asciidoc/images/add-flow-analysis-rule-window.png
new file mode 100644
index 0000000000..da5affbba3
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/add-flow-analysis-rule-window.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-properties.png b/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-properties.png
new file mode 100644
index 0000000000..43f93b3f00
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-properties.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-settings.png b/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-settings.png
new file mode 100644
index 0000000000..ede5c8fa8c
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/configure-flow-analysis-rule-settings.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-configure-buttons.png b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-configure-buttons.png
new file mode 100644
index 0000000000..131b8e6780
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-configure-buttons.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-info-buttons.png b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-info-buttons.png
new file mode 100644
index 0000000000..93f561cfba
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-info-buttons.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-tab.png b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-tab.png
new file mode 100644
index 0000000000..545250adec
Binary files /dev/null and b/nifi-docs/src/main/asciidoc/images/flow-analysis-rules-tab.png differ
diff --git a/nifi-docs/src/main/asciidoc/images/settings-general-tab.png b/nifi-docs/src/main/asciidoc/images/settings-general-tab.png
index a5ef06ded6..6d1e496b79 100644
Binary files a/nifi-docs/src/main/asciidoc/images/settings-general-tab.png and b/nifi-docs/src/main/asciidoc/images/settings-general-tab.png differ
diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc
index 94a003ac92..e9ca5ad656 100644
--- a/nifi-docs/src/main/asciidoc/user-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/user-guide.adoc
@@ -95,6 +95,8 @@ UI may become unavailable.
 
 *Reporting Task*: Reporting Tasks run in the background to provide statistical reports about what is happening in the NiFi instance. The DFM adds and configures Reporting Tasks in the User Interface as desired. Common reporting tasks include the ControllerStatusReportingTask, MonitorDiskUsage reporting task, MonitorMemory reporting task, and the StandardGangliaReporter.
 
+*Flow Analysis Rules*: Flow Analysis Rules can analyze components or (parts of) the flow. They may produce rule violations which can help adjust or maintain optimal flow design. The DFM adds and configures Flow Analysis Rules in the User Interface as desired.
+
 *Parameter Provider*: Parameter Providers can provide parameters from an external source to Parameter Contexts.  The parameters of a Parameter Provider may be fetched and applied to all referencing Parameter Contexts.
 
 *Funnel*: A funnel is a NiFi component that is used to combine the data from several Connections into a single Connection.
@@ -193,7 +195,7 @@ The available global access policies are:
 |======================
 |Policy  |Privilege
 |view the UI   |Allows users to view the UI
-|access the controller  |Allows users to view and modify the controller including Management Controller Services, Reporting Tasks, Registry Clients, Parameter Providers and nodes in the cluster
+|access the controller  |Allows users to view and modify the controller including Management Controller Services, Reporting Tasks, Flow Analysis Rules, Registry Clients, Parameter Providers and nodes in the cluster
 |query provenance     |Allows users to submit a provenance search and request even lineage
 |access restricted components     |Allows users to create/modify restricted components assuming other permissions are sufficient. The restricted
 components may indicate which specific permissions are required. Permissions can be granted for specific restrictions or be granted regardless
@@ -453,10 +455,10 @@ choosing `Configure`.
 
 [[component-versioning]]
 === Component Versions
-You have access to information about the version of your Processors, Controller Services, and Reporting Tasks.
+You have access to information about the version of your Processors, Controller Services, Reporting Tasks and Flow Analysis Rules.
 This is especially useful when you are working within a clustered environment with multiple NiFi instances running
 different versions of a component or if you have upgraded to a newer version of a processor. The Add Processor,
-Add Controller Service, and Add Reporting Task dialogs include a column identifying the component version, as well
+Add Controller Service, Add Reporting Task and Add Flow Analysis Rule dialogs include a column identifying the component version, as well
 as the name of the component, the organization or group that created the component, and the NAR bundle that contains
 the component.
 
@@ -897,7 +899,7 @@ The values of properties in the flow, including sensitive properties, can be par
  - A sensitive property can only reference a Sensitive Parameter
  - A non-sensitive property can only reference a Non-Sensitive Parameter
  - Properties that reference Controller Services can not use Parameters
- - Parameters cannot be referenced in Reporting Tasks or in Management Controller Services
+ - Parameters cannot be referenced in Reporting Tasks, Flow Analysis Rules or in Management Controller Services
 
 The UI indicates whether a Parameter can be used for a property value.
 
@@ -1286,9 +1288,9 @@ For more information, see the <<administration-guide.adoc#custom_properties,Cust
 [[Controller_Services]]
 === Controller Services
 
-Controller Services are shared services that can be used by reporting tasks, processors, and other services to utilize for configuration or task execution.
+Controller Services are shared services that can be used by reporting tasks, flow analysis rules, processors, and other services to utilize for configuration or task execution.
 
-IMPORTANT: Controller Services defined on the controller level are limited to reporting tasks and other services defined there. Controller Services for use by processors in your dataflow must be defined in the configuration of the root process group or sub-process group(s) where they will be used.
+IMPORTANT: Controller Services defined on the controller level are limited to reporting tasks, flow analysis rules and other services defined there. Controller Services for use by processors in your dataflow must be defined in the configuration of the root process group or sub-process group(s) where they will be used.
 
 NOTE: If your NiFi instance is secured, your ability to view and add Controller Services is dependent on the privileges assigned to you. If you do not have access to one or more Controller Services, you are not able to see or access it in the UI. Access privileges can be assigned on a global or Controller Service-specific basis (see <<UI-with-multi-tenant-authorization>> for more information).
 
@@ -1299,7 +1301,7 @@ To add a Management Controller Service, select Controller Settings from the Glob
 
 image:controller-settings-selection.png["Global Menu - Controller Settings"]
 
-This displays the NiFi Settings window. The window has five tabs: General, Management Controller Services, Reporting Tasks, Registry Clients and Parameter Providers. The General tab provides settings for the overall maximum thread counts of the instance.
+This displays the NiFi Settings window. The window has six tabs: General, Management Controller Services, Reporting Tasks, Flow Analysis Rules, Registry Clients and Parameter Providers. The General tab provides settings for the overall maximum thread counts of the instance.
 
 image:settings-general-tab.png["Controller Settings General Tab"]
 
@@ -1399,6 +1401,49 @@ The Comments tab is just an open-text field, where the DFM may include comments
 
 When you want to run the Reporting Task, click the "Start" button (image:iconStart.png["Start Button"]).
 
+[[Flow_Analysis_Rules]]
+=== Flow Analysis Rules
+
+Flow Analysis Rules can analyze components or (parts of) the flow. They may produce rule violations which can help adjust or maintain optimal flow design.
+Each rule can either be a Recommendation or a Policy which can be set on the Configure Flow Analysis Rule window.
+Rule violations of Recommendation type rules can be reported and viewed later but otherwise have no impact on functionality.
+Rule violations of Policy type rules can also be reported and viewed later but also impacts functionality: components that violate a Policy become invalid and remain
+so until the rule violation is resolved.
+The DFM adds and configures Flow Analysis Rules similar to the process for Controller Services. To add a Flow Analysis Rule, select Controller Settings from the Global Menu.
+
+image:controller-settings-selection.png["Global Menu - Controller Settings"]
+
+This displays the NiFi Settings window. Select the Flow Analysis Rules tab and click the `+` button in the upper-right corner to create a new Flow Analysis Rule.
+
+image:flow-analysis-rules-tab.png["Flow Analysis Rules Tab"]
+
+The Add Flow Analysis Rule window opens. This window is similar to the Add Processor window. It provides a list of the available Flow Analysis Rules on the right and a tag cloud, showing the most common category tags used for Flow Analysis Rules, on the left. The DFM may click any tag in the tag cloud in order to narrow down the list of Flow Analysis Rules to those that fit the categories desired. The DFM may also use the Filter field at the top-right of the window to search for the desi [...]
+
+image:add-flow-analysis-rule-window.png["Add Flow Analysis Rule Window"]
+
+Once a Flow Analysis Rule has been added, the DFM may configure it by clicking the "Configure" button in the far-right column (when the rule is disabled). Other buttons in this column include "Enable", "Disable", "View Configuration", "Remove", "State" and "Access Policies".
+
+image:flow-analysis-rules-configure-buttons.png["Flow Analysis Rules Configure Buttons"]
+
+You can obtain information about Flow Analysis Rules by clicking the "View Details", "Usage", and "Alerts" buttons in the left-hand column.
+
+image:flow-analysis-rules-info-buttons.png["Flow Analysis Rules Information Buttons"]
+
+When the DFM clicks the "Configure" button, a Configure Flow Analysis Rule window opens. It has three tabs: Settings, Properties, and Comments. This window is similar to the Configure Processor window. The Settings tab provides a place for the DFM to give the Flow Analysis Rule a unique name (if desired). It also lists the UUID, Type, and Bundle information for the rule and provides a setting for its type (Recommendation or Policy). The DFM may hover the mouse over the question mark icon [...]
+
+image:configure-flow-analysis-rule-settings.png["Configure Flow Analysis Rule Settings"]
+
+The Properties tab lists the various properties that may be configured for the rule. The DFM may hover the mouse over the question mark icons to see more information about each property.
+
+image:configure-flow-analysis-rule-properties.png["Configure Flow Analysis Rule Properties"]
+
+The Comments tab is just an open-text field, where the DFM may include comments about the rule. After configuring the Flow Analysis Rule, click "Apply" to save the configuration and close the window, or click "Cancel" to discard the changes and close the window.
+
+When you want the Flow Analysis Rule to be active, click the "Enable" button (image:iconEnable.png["Enable Button"]).
+
+When you want the Flow Analysis Rule to be inactive, click the "Disable" button (image:iconDisable.png["Disable Button"]).
+Disabling a rule also renders all corresponding violations null and void.
+
 
 [[Connecting_Components]]
 === Connecting Components
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestDTO.java
similarity index 51%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestDTO.java
index 89fbd6592c..aa204879b5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestDTO.java
@@ -14,27 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.authorization;
+package org.apache.nifi.web.api.dto;
 
-import org.apache.nifi.authorization.resource.Authorizable;
-import org.apache.nifi.authorization.user.NiFiUser;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
 
 /**
- * Authorizable for a PublicPort.
+ * A request to analyze (a part) of the flow.
  */
-public interface PublicPortAuthorizable {
-    /**
-     * Returns the authorizable for this PublicGroupPort. Non null
-     *
-     * @return authorizable
-     */
-    Authorizable getAuthorizable();
+@XmlType(name = "analyzeFlowRequest")
+public class AnalyzeFlowRequestDTO extends AsynchronousRequestDTO<AnalyzeFlowRequestUpdateStepDTO> {
+    public AnalyzeFlowRequestDTO() {
+    }
+
+    private String processGroupId;
 
     /**
-     * Checks the authorization for the specified user.
+     * The id of the process group representing (a part of) the flow to be analyzed.
      *
-     * @param user user
-     * @return authorization result
+     * @return The id
      */
-    AuthorizationResult checkAuthorization(NiFiUser user);
+    @ApiModelProperty("The id of the process group representing (a part of) the flow to be analyzed.")
+    public String getProcessGroupId() {
+        return this.processGroupId;
+    }
+
+    public void setProcessGroupId(final String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
 }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestUpdateStepDTO.java
similarity index 80%
copy from nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestUpdateStepDTO.java
index c0eda7e230..db43abd327 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AnalyzeFlowRequestUpdateStepDTO.java
@@ -14,14 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.documentation;
+package org.apache.nifi.web.api.dto;
 
-public enum ExtensionType {
-    PROCESSOR,
+import javax.xml.bind.annotation.XmlType;
 
-    CONTROLLER_SERVICE,
-
-    REPORTING_TASK,
-
-    PARAMETER_PROVIDER;
+@XmlType(name = "analyzeFlowRequestUpdateStep")
+public class AnalyzeFlowRequestUpdateStepDTO extends UpdateStepDTO {
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleDTO.java
new file mode 100644
index 0000000000..e9233a0ddb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleDTO.java
@@ -0,0 +1,293 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+@XmlType(name = "flowAnalysisRule")
+public class FlowAnalysisRuleDTO extends ComponentDTO {
+    public static final String VALID = "VALID";
+    public static final String INVALID = "INVALID";
+    public static final String VALIDATING = "VALIDATING";
+
+    private String name;
+    private String type;
+    private BundleDTO bundle;
+    private String state;
+    private String comments;
+    private Boolean persistsState;
+    private Boolean restricted;
+    private Boolean deprecated;
+    private Boolean isExtensionMissing;
+    private Boolean multipleVersionsAvailable;
+    private Boolean supportsSensitiveDynamicProperties;
+
+    private String enforcementPolicy;
+
+    private Map<String, String> properties;
+    private Map<String, PropertyDescriptorDTO> descriptors;
+    private Set<String> sensitiveDynamicPropertyNames;
+
+    private Collection<String> validationErrors;
+    private String validationStatus;
+
+    /**
+     * @return user-defined name of the flow analysis rule
+     */
+    @ApiModelProperty(
+            value = "The name of the flow analysis rule."
+    )
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return user-defined comments for the flow analysis rule
+     */
+    @ApiModelProperty(
+            value = "The comments of the flow analysis rule."
+    )
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    /**
+     * @return type of flow analysis rule
+     */
+    @ApiModelProperty(
+            value = "The fully qualified type of the flow analysis rule."
+    )
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    /**
+     * The details of the artifact that bundled this flow analysis rule type.
+     *
+     * @return The bundle details
+     */
+    @ApiModelProperty(
+            value = "The details of the artifact that bundled this flow analysis rule type."
+    )
+    public BundleDTO getBundle() {
+        return bundle;
+    }
+
+    public void setBundle(BundleDTO bundle) {
+        this.bundle = bundle;
+    }
+    /**
+     * @return whether this flow analysis rule persists state
+     */
+    @ApiModelProperty(
+        value = "Whether the flow analysis rule persists state."
+    )
+    public Boolean getPersistsState() {
+        return persistsState;
+    }
+
+    public void setPersistsState(Boolean persistsState) {
+        this.persistsState = persistsState;
+    }
+
+    /**
+     * @return whether this flow analysis rule requires elevated privileges
+     */
+    @ApiModelProperty(
+            value = "Whether the flow analysis rule requires elevated privileges."
+    )
+    public Boolean getRestricted() {
+        return restricted;
+    }
+
+    public void setRestricted(Boolean restricted) {
+        this.restricted = restricted;
+    }
+
+    /**
+     * @return Whether the flow analysis rule has been deprecated.
+     */
+    @ApiModelProperty(
+            value = "Whether the flow analysis rule has been deprecated."
+    )
+    public Boolean getDeprecated() {
+        return deprecated;
+    }
+
+    public void setDeprecated(Boolean deprecated) {
+        this.deprecated = deprecated;
+    }
+
+    /**
+     * @return whether the underlying extension is missing
+     */
+    @ApiModelProperty(
+            value = "Whether the underlying extension is missing."
+    )
+    public Boolean getExtensionMissing() {
+        return isExtensionMissing;
+    }
+
+    public void setExtensionMissing(Boolean extensionMissing) {
+        isExtensionMissing = extensionMissing;
+    }
+
+    /**
+     * @return whether this flow analysis rule has multiple versions available
+     */
+    @ApiModelProperty(
+            value = "Whether the flow analysis rule has multiple versions available."
+    )
+    public Boolean getMultipleVersionsAvailable() {
+        return multipleVersionsAvailable;
+    }
+
+    public void setMultipleVersionsAvailable(Boolean multipleVersionsAvailable) {
+        this.multipleVersionsAvailable = multipleVersionsAvailable;
+    }
+
+    /**
+     * @return whether this flow analysis rule supports sensitive dynamic properties
+     */
+    @ApiModelProperty(
+            value = "Whether the flow analysis rule supports sensitive dynamic properties."
+    )
+    public Boolean getSupportsSensitiveDynamicProperties() {
+        return supportsSensitiveDynamicProperties;
+    }
+
+    public void setSupportsSensitiveDynamicProperties(final Boolean supportsSensitiveDynamicProperties) {
+        this.supportsSensitiveDynamicProperties = supportsSensitiveDynamicProperties;
+    }
+
+    /**
+     * @return current scheduling state of the flow analysis rule
+     */
+    @ApiModelProperty(
+            value = "The state of the flow analysis rule.",
+            allowableValues = "ENABLED, DISABLED"
+    )
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    /**
+     * @return Enforcement Policy
+     */
+    @ApiModelProperty(
+            value = "Enforcement Policy."
+    )
+    public String getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    public void setEnforcementPolicy(String enforcementPolicy) {
+        this.enforcementPolicy = enforcementPolicy;
+    }
+
+    /**
+     * @return flow analysis rule's properties
+     */
+    @ApiModelProperty(
+            value = "The properties of the flow analysis rule."
+    )
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * @return Map of property name to descriptor
+     */
+    @ApiModelProperty(
+            value = "The descriptors for the flow analysis rules properties."
+    )
+    public Map<String, PropertyDescriptorDTO> getDescriptors() {
+        return descriptors;
+    }
+
+    public void setDescriptors(Map<String, PropertyDescriptorDTO> descriptors) {
+        this.descriptors = descriptors;
+    }
+
+    /**
+     * @return Set of sensitive dynamic property names
+     */
+    @ApiModelProperty(
+            value = "Set of sensitive dynamic property names"
+    )
+    public Set<String> getSensitiveDynamicPropertyNames() {
+        return sensitiveDynamicPropertyNames;
+    }
+
+    public void setSensitiveDynamicPropertyNames(final Set<String> sensitiveDynamicPropertyNames) {
+        this.sensitiveDynamicPropertyNames = sensitiveDynamicPropertyNames;
+    }
+
+    /**
+     * Gets the validation errors from this flow analysis rule. These validation errors represent the problems with the flow analysis rule that must be resolved before it can be scheduled to run.
+     *
+     * @return The validation errors
+     */
+    @ApiModelProperty(
+            value = "Gets the validation errors from the flow analysis rule. These validation errors represent the problems with the flow analysis rule that must be resolved before "
+                    + "it can be scheduled to run."
+    )
+    public Collection<String> getValidationErrors() {
+        return validationErrors;
+    }
+
+    public void setValidationErrors(Collection<String> validationErrors) {
+        this.validationErrors = validationErrors;
+    }
+
+    @ApiModelProperty(value = "Indicates whether the Flow Analysis Rule is valid, invalid, or still in the process of validating (i.e., it is unknown whether or not the Flow Analysis Rule is valid)",
+        accessMode = ApiModelProperty.AccessMode.READ_ONLY,
+        allowableValues = VALID + ", " + INVALID + ", " + VALIDATING)
+    public String getValidationStatus() {
+        return validationStatus;
+    }
+
+    public void setValidationStatus(String validationStatus) {
+        this.validationStatus = validationStatus;
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleViolationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleViolationDTO.java
new file mode 100644
index 0000000000..f8745cf468
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowAnalysisRuleViolationDTO.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.web.api.dto;
+
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * A result of a rule violation produced during a flow analysis
+ */
+@XmlType(name = "flowAnalysisRuleViolation")
+public class FlowAnalysisRuleViolationDTO {
+    public FlowAnalysisRuleViolationDTO() {
+    }
+
+    private String enforcementPolicy;
+    private String scope;
+    private String subjectId;
+    private String subjectDisplayName;
+    private String groupId;
+    private String ruleId;
+    private String issueId;
+    private String violationMessage;
+
+    private PermissionsDTO subjectPermissionDto;
+
+    private boolean enabled;
+
+    /**
+     * @return the enforcement policy of the rule that produced this result
+     */
+    public String getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    public void setEnforcementPolicy(String enforcementPolicy) {
+        this.enforcementPolicy = enforcementPolicy;
+    }
+
+    /**
+     * @return the scope of the result
+     */
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+
+    /**
+     * @return the id of the subject that violated the rule
+     */
+    public String getSubjectId() {
+        return subjectId;
+    }
+
+    public void setSubjectId(String subjectId) {
+        this.subjectId = subjectId;
+    }
+
+    /**
+     * @return the displayed name of the subject that violated the rule
+     */
+    public String getSubjectDisplayName() {
+        return subjectDisplayName;
+    }
+
+    public void setSubjectDisplayName(String subjectDisplayName) {
+        this.subjectDisplayName = subjectDisplayName;
+    }
+
+    /**
+     * @return group id - if this violation is a result of a component analysis, then the id of the group of the component.
+     * If this violation is a result of a group analysis, then the id of that group itself.
+     */
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    /**
+     * @return the id of the rule that produced this result
+     */
+    public String getRuleId() {
+        return ruleId;
+    }
+
+    public void setRuleId(String ruleId) {
+        this.ruleId = ruleId;
+    }
+
+    /**
+     * @return a rule-defined id that corresponds to a unique type of issue recognized by the rule
+     */
+    public String getIssueId() {
+        return issueId;
+    }
+
+    public void setIssueId(String issueId) {
+        this.issueId = issueId;
+    }
+
+    /**
+     * @return the violation message
+     */
+    public String getViolationMessage() {
+        return violationMessage;
+    }
+
+    public void setViolationMessage(String violationMessage) {
+        this.violationMessage = violationMessage;
+    }
+
+
+    /**
+     * @return true if this result should be in effect, false otherwise
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * @return a permission object for the subject that violated the rule
+     */
+    public PermissionsDTO getSubjectPermissionDto() {
+        return subjectPermissionDto;
+    }
+
+    public void setSubjectPermissionDto(PermissionsDTO subjectPermissionDto) {
+        this.subjectPermissionDto = subjectPermissionDto;
+    }
+}
diff --git a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/FlowAnalysisRuleStatusDTO.java
similarity index 58%
copy from nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/FlowAnalysisRuleStatusDTO.java
index dc3265ee32..41bec2b115 100644
--- a/nifi-api/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/FlowAnalysisRuleStatusDTO.java
@@ -14,21 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.flow;
 
-import java.util.Map;
+package org.apache.nifi.web.api.dto.status;
 
-/**
- * A component that has property descriptors and can be configured with values for those properties.
- */
-public interface VersionedConfigurableComponent {
-
-    Map<String, VersionedPropertyDescriptor> getPropertyDescriptors();
+import io.swagger.annotations.ApiModelProperty;
 
-    void setPropertyDescriptors(Map<String,VersionedPropertyDescriptor> propertyDescriptors);
+import javax.xml.bind.annotation.XmlType;
 
-    Map<String,String> getProperties();
-
-    void setProperties(Map<String,String> properties);
+/**
+ * DTO for serializing the status of a FlowAnalysisRule.
+ */
+@XmlType(name = "flowAnalysisRuleStatus")
+public class FlowAnalysisRuleStatusDTO extends ComponentStatusDTO {
 
+    @ApiModelProperty(value = "The run status of this FlowAnalysisRule",
+        accessMode = ApiModelProperty.AccessMode.READ_ONLY,
+        allowableValues = "ENABLED, DISABLED")
+    @Override
+    public String getRunStatus() {
+        return super.getRunStatus();
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AnalyzeFlowRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AnalyzeFlowRequestEntity.java
new file mode 100644
index 0000000000..ffde8cedc9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AnalyzeFlowRequestEntity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.AnalyzeFlowRequestDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to an AnalyzeRequestDTO.
+ */
+@XmlRootElement(name = "dropRequestEntity")
+public class AnalyzeFlowRequestEntity extends Entity {
+
+    private AnalyzeFlowRequestDTO analyzeFlowRequest;
+
+    /**
+     * The AnalyzeFlowRequestDTO that is being serialized.
+     *
+     * @return The AnalyzeFlowRequestDTO object
+     */
+    public AnalyzeFlowRequestDTO getAnalyzeFlowRequest() {
+        return analyzeFlowRequest;
+    }
+
+    public void setAnalyzeFlowRequest(AnalyzeFlowRequestDTO analyzeFlowRequest) {
+        this.analyzeFlowRequest = analyzeFlowRequest;
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ControllerBulletinsEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ControllerBulletinsEntity.java
index 0f29ceedd1..bd8c98dfb0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ControllerBulletinsEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ControllerBulletinsEntity.java
@@ -31,6 +31,7 @@ public class ControllerBulletinsEntity extends Entity {
     private List<BulletinEntity> bulletins;
     private List<BulletinEntity> controllerServiceBulletins;
     private List<BulletinEntity> reportingTaskBulletins;
+    private List<BulletinEntity> flowAnalysisRuleBulletins;
     private List<BulletinEntity> parameterProviderBulletins;
     private List<BulletinEntity> flowRegistryClientBulletins;
 
@@ -70,6 +71,18 @@ public class ControllerBulletinsEntity extends Entity {
         this.reportingTaskBulletins = reportingTaskBulletins;
     }
 
+    /**
+     * @return Flow Analysis Rule bulletins to be reported to the user
+     */
+    @ApiModelProperty("Flow Analysis Rule bulletins to be reported to the user.")
+    public List<BulletinEntity> getFlowAnalysisRuleBulletins() {
+        return flowAnalysisRuleBulletins;
+    }
+
+    public void setFlowAnalysisRuleBulletins(List<BulletinEntity> flowAnalysisRuleBulletins) {
+        this.flowAnalysisRuleBulletins = flowAnalysisRuleBulletins;
+    }
+
     /**
      * @return Parameter provider bulletins to be reported to the user
      */
@@ -100,6 +113,7 @@ public class ControllerBulletinsEntity extends Entity {
         other.setBulletins(getBulletins() == null ? null : new ArrayList<>(getBulletins()));
         other.setControllerServiceBulletins(getControllerServiceBulletins() == null ? null : new ArrayList<>(getControllerServiceBulletins()));
         other.setReportingTaskBulletins(getReportingTaskBulletins() == null ? null : new ArrayList<>(getReportingTaskBulletins()));
+        other.setFlowAnalysisRuleBulletins(getFlowAnalysisRuleBulletins() == null ? null : new ArrayList<>(getFlowAnalysisRuleBulletins()));
         other.setParameterProviderBulletins(getParameterProviderBulletins() == null ? null : new ArrayList<>(getParameterProviderBulletins()));
         other.setFlowRegistryClientBulletins(getFlowRegistryClientBulletins() == null ? null : new ArrayList<>(getFlowRegistryClientBulletins()));
         return other;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisResultEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisResultEntity.java
new file mode 100644
index 0000000000..e3645aca04
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisResultEntity.java
@@ -0,0 +1,59 @@
+/*
+ * 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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleViolationDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API.
+ *  This particular entity holds a reference to a collection of {@link FlowAnalysisRuleDTO} and another collection of {@link FlowAnalysisRuleViolationDTO}.
+ */
+@XmlRootElement(name = "flowAnalysisResultEntity")
+public class FlowAnalysisResultEntity extends Entity {
+    public FlowAnalysisResultEntity() {
+    }
+
+    private List<FlowAnalysisRuleDTO> rules = new ArrayList<>();
+    private List<FlowAnalysisRuleViolationDTO> ruleViolations = new ArrayList<>();
+
+    /**
+     * @return set of flow analysis rules that are being serialized
+     */
+    public List<FlowAnalysisRuleDTO> getRules() {
+        return rules;
+    }
+
+    public void setRules(List<FlowAnalysisRuleDTO> rules) {
+        this.rules = rules;
+    }
+
+    /**
+     * @return set of flow analysis results that are being serialized
+     */
+    public List<FlowAnalysisRuleViolationDTO> getRuleViolations() {
+        return ruleViolations;
+    }
+
+    public void setRuleViolations(List<FlowAnalysisRuleViolationDTO> ruleViolations) {
+        this.ruleViolations = ruleViolations;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleEntity.java
new file mode 100644
index 0000000000..62cf524f7a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleEntity.java
@@ -0,0 +1,79 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.PermissionsDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
+import org.apache.nifi.web.api.dto.status.FlowAnalysisRuleStatusDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a flow analysis rule.
+ */
+@XmlRootElement(name = "flowAnalysisRuleEntity")
+public class FlowAnalysisRuleEntity extends ComponentEntity implements Permissible<FlowAnalysisRuleDTO>, OperationPermissible {
+    private FlowAnalysisRuleDTO component;
+    private PermissionsDTO operatePermissions;
+    private FlowAnalysisRuleStatusDTO status;
+
+    /**
+     * @return flow analysis rule that is being serialized
+     */
+    @Override
+    public FlowAnalysisRuleDTO getComponent() {
+        return component;
+    }
+
+    @Override
+    public void setComponent(FlowAnalysisRuleDTO component) {
+        this.component = component;
+    }
+
+    /**
+     * @return The permissions for this component operations
+     */
+    @ApiModelProperty(
+            value = "The permissions for this component operations."
+    )
+    @Override
+    public PermissionsDTO getOperatePermissions() {
+        return operatePermissions;
+    }
+
+    @Override
+    public void setOperatePermissions(PermissionsDTO permissions) {
+        this.operatePermissions = permissions;
+    }
+
+    /**
+     * @return The status for this FlowAnalysisRule
+     */
+    @ApiModelProperty(
+            value = "The status for this FlowAnalysisRule.",
+            readOnly = true
+    )
+    public FlowAnalysisRuleStatusDTO getStatus() {
+        return status;
+    }
+
+    public void setStatus(FlowAnalysisRuleStatusDTO status) {
+        this.status = status;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleRunStatusEntity.java
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleRunStatusEntity.java
index 89fbd6592c..135aee06c7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleRunStatusEntity.java
@@ -14,27 +14,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.authorization;
+package org.apache.nifi.web.api.entity;
 
-import org.apache.nifi.authorization.resource.Authorizable;
-import org.apache.nifi.authorization.user.NiFiUser;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
 
 /**
- * Authorizable for a PublicPort.
+ * Run status for a given FlowAnalysisRule.
  */
-public interface PublicPortAuthorizable {
-    /**
-     * Returns the authorizable for this PublicGroupPort. Non null
-     *
-     * @return authorizable
-     */
-    Authorizable getAuthorizable();
+@XmlType(name = "flowAnalysisRuleRunStatus")
+public class FlowAnalysisRuleRunStatusEntity extends ComponentRunStatusEntity {
+
+    private static final String[] SUPPORTED_STATE = {"ENABLED", "DISABLED"};
+
+    @Override
+    protected String[] getSupportedState() {
+        return SUPPORTED_STATE;
+    }
 
     /**
-     * Checks the authorization for the specified user.
-     *
-     * @param user user
-     * @return authorization result
+     * State of this FlowAnalysisRule.
+     * @return The state
      */
-    AuthorizationResult checkAuthorization(NiFiUser user);
+    @ApiModelProperty(
+            value = "The state of the FlowAnalysisRule.",
+            allowableValues = "ENABLED, DISABLED"
+    )
+    public String getState() {
+        return super.getState();
+    }
+
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleTypesEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleTypesEntity.java
new file mode 100644
index 0000000000..dec570ae2f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRuleTypesEntity.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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.DocumentedTypeDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of flow analysis rule types.
+ */
+@XmlRootElement(name = "flowAnalysisRuleTypesEntity")
+public class FlowAnalysisRuleTypesEntity extends Entity {
+
+    private Set<DocumentedTypeDTO> flowAnalysisRuleTypes;
+
+    /**
+     * @return set of reporting task types that are being serialized
+     */
+    public Set<DocumentedTypeDTO> getFlowAnalysisRuleTypes() {
+        return flowAnalysisRuleTypes;
+    }
+
+    public void setFlowAnalysisRuleTypes(Set<DocumentedTypeDTO> enforcementPolicies) {
+        this.flowAnalysisRuleTypes = enforcementPolicies;
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRulesEntity.java
similarity index 51%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRulesEntity.java
index 89fbd6592c..3d7f57e57b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/PublicPortAuthorizable.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowAnalysisRulesEntity.java
@@ -14,27 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.authorization;
+package org.apache.nifi.web.api.entity;
 
-import org.apache.nifi.authorization.resource.Authorizable;
-import org.apache.nifi.authorization.user.NiFiUser;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
 
 /**
- * Authorizable for a PublicPort.
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of flow analysis rules.
  */
-public interface PublicPortAuthorizable {
-    /**
-     * Returns the authorizable for this PublicGroupPort. Non null
-     *
-     * @return authorizable
-     */
-    Authorizable getAuthorizable();
+@XmlRootElement(name = "flowAnalysisRulesEntity")
+public class FlowAnalysisRulesEntity extends Entity {
+
+    private Set<FlowAnalysisRuleEntity> flowAnalysisRules;
 
     /**
-     * Checks the authorization for the specified user.
-     *
-     * @param user user
-     * @return authorization result
+     * @return set of flow analysis rules that are being serialized
      */
-    AuthorizationResult checkAuthorization(NiFiUser user);
+    public Set<FlowAnalysisRuleEntity> getFlowAnalysisRules() {
+        return flowAnalysisRules;
+    }
+
+    public void setFlowAnalysisRules(Set<FlowAnalysisRuleEntity> flowAnalysisRules) {
+        this.flowAnalysisRules = flowAnalysisRules;
+    }
+
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RuleViolationEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RuleViolationEntity.java
new file mode 100644
index 0000000000..c34c7bb292
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RuleViolationEntity.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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API.
+ *  Used to manage flow analysis rule violations.
+ */
+@XmlRootElement(name = "ruleViolationEntity")
+public class RuleViolationEntity extends Entity {
+    private String scope;
+    private String subjectId;
+    private String ruleId;
+    private String issueId;
+    private Boolean enabled;
+
+    /**
+     * @return the scope of the analysis that produced this result.
+     */
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+
+    /**
+     * @return the id of the subject that violated the rule.
+     */
+    public String getSubjectId() {
+        return subjectId;
+    }
+
+    public void setSubjectId(String subjectId) {
+        this.subjectId = subjectId;
+    }
+
+    /**
+     * @return the id of the rule that produced this result
+     */
+    public String getRuleId() {
+        return ruleId;
+    }
+
+    public void setRuleId(String ruleId) {
+        this.ruleId = ruleId;
+    }
+
+    /**
+     * @return a rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     */
+    public String getIssueId() {
+        return issueId;
+    }
+
+    public void setIssueId(String issueId) {
+        this.issueId = issueId;
+    }
+
+    /**
+     * @return true if this violation should be in effect, false otherwise
+     */
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java
index 5145de77ff..e9cef0b5cb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java
@@ -22,6 +22,7 @@ import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.documentation.html.HtmlDocumentationWriter;
 import org.apache.nifi.documentation.html.HtmlProcessorDocumentationWriter;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.nar.ExtensionDefinition;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.ExtensionMapping;
@@ -63,6 +64,7 @@ public class DocGenerator {
         documentConfigurableComponent(extensionManager.getExtensions(Processor.class), explodedNiFiDocsDir, extensionManager);
         documentConfigurableComponent(extensionManager.getExtensions(ControllerService.class), explodedNiFiDocsDir, extensionManager);
         documentConfigurableComponent(extensionManager.getExtensions(ReportingTask.class), explodedNiFiDocsDir, extensionManager);
+        documentConfigurableComponent(extensionManager.getExtensions(FlowAnalysisRule.class), explodedNiFiDocsDir, extensionManager);
         documentConfigurableComponent(extensionManager.getExtensions(ParameterProvider.class), explodedNiFiDocsDir, extensionManager);
     }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceFactory.java
index ca33647d5f..c91db0e1ce 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceFactory.java
@@ -575,6 +575,9 @@ public final class ResourceFactory {
                     case ReportingTask:
                         componentType = "Reporting Task";
                         break;
+                    case FlowAnalysisRule:
+                        componentType = "Flow Analysis Rule";
+                        break;
                     case Label:
                         componentType = "Label";
                         break;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceType.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceType.java
index d6602c7360..3ddd2c1f2f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceType.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/ResourceType.java
@@ -34,6 +34,7 @@ public enum ResourceType {
     Proxy("/proxy"),
     RemoteProcessGroup("/remote-process-groups"),
     ReportingTask("/reporting-tasks"),
+    FlowAnalysisRule("/controller/flow-analysis-rules"),
     Resource("/resources"),
     SiteToSite("/site-to-site"),
     DataTransfer("/data-transfer"),
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
index 8ea6352014..264827ef79 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
@@ -34,6 +34,10 @@ import org.apache.nifi.cluster.coordination.http.endpoints.CountersEndpointMerge
 import org.apache.nifi.cluster.coordination.http.endpoints.CurrentUserEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.DropAllFlowFilesRequestEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.DropRequestEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.FlowAnalysisEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.FlowAnalysisRuleEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.FlowAnalysisRuleTypesEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.FlowAnalysisRulesEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.FlowConfigurationEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.FlowMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.FlowRegistryClientEndpointMerger;
@@ -75,6 +79,7 @@ import org.apache.nifi.cluster.coordination.http.endpoints.ReplayLastEventEndpoi
 import org.apache.nifi.cluster.coordination.http.endpoints.ReportingTaskEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ReportingTaskTypesEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ReportingTasksEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.RuleViolationEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.RuntimeManifestEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.SearchUsersEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.StatusHistoryEndpointMerger;
@@ -144,6 +149,10 @@ public class StandardHttpResponseMapper implements HttpResponseMapper {
         endpointMergers.add(new ControllerServiceReferenceEndpointMerger());
         endpointMergers.add(new ReportingTaskEndpointMerger());
         endpointMergers.add(new ReportingTasksEndpointMerger());
+        endpointMergers.add(new FlowAnalysisRuleEndpointMerger());
+        endpointMergers.add(new FlowAnalysisRulesEndpointMerger());
+        endpointMergers.add(new FlowAnalysisEndpointMerger());
+        endpointMergers.add(new RuleViolationEndpointMerger());
         endpointMergers.add(new DropRequestEndpointMerger());
         endpointMergers.add(new DropAllFlowFilesRequestEndpointMerger());
         endpointMergers.add(new ListFlowFilesEndpointMerger());
@@ -156,6 +165,7 @@ public class StandardHttpResponseMapper implements HttpResponseMapper {
         endpointMergers.add(new ProcessorTypesEndpointMerger());
         endpointMergers.add(new ControllerServiceTypesEndpointMerger());
         endpointMergers.add(new ReportingTaskTypesEndpointMerger());
+        endpointMergers.add(new FlowAnalysisRuleTypesEndpointMerger());
         endpointMergers.add(new PrioritizerTypesEndpointMerger());
         endpointMergers.add(new ControllerConfigurationEndpointMerger());
         endpointMergers.add(new CurrentUserEndpointMerger());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMerger.java
new file mode 100644
index 0000000000..563512287c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMerger.java
@@ -0,0 +1,96 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.AnalyzeFlowRequestDTO;
+import org.apache.nifi.web.api.entity.AnalyzeFlowRequestEntity;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class AnalyzeFlowRequestEndpointMerger extends AbstractSingleDTOEndpoint<AnalyzeFlowRequestEntity, AnalyzeFlowRequestDTO> {
+    public static final Pattern ANALYZE_FLOW_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/flow-analysis/[a-f0-9\\-]{36}");
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        if (
+            ("POST".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method))
+                && ANALYZE_FLOW_URI_PATTERN.matcher(uri.getPath()).matches()
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected Class<AnalyzeFlowRequestEntity> getEntityClass() {
+        return AnalyzeFlowRequestEntity.class;
+    }
+
+    @Override
+    protected AnalyzeFlowRequestDTO getDto(AnalyzeFlowRequestEntity entity) {
+        AnalyzeFlowRequestDTO dto = entity.getAnalyzeFlowRequest();
+
+        return dto;
+    }
+
+    @Override
+    protected void mergeResponses(
+        AnalyzeFlowRequestDTO clientDto,
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap,
+        Set<NodeResponse> successfulResponses,
+        Set<NodeResponse> problematicResponses
+    ) {
+        Collection<String> failureReasons = new HashSet<>();
+        if (clientDto.getFailureReason() != null) {
+            failureReasons.add(clientDto.getFailureReason());
+        }
+
+        for (final AnalyzeFlowRequestDTO requestDto : dtoMap.values()) {
+            if (!requestDto.isComplete()) {
+                clientDto.setComplete(false);
+            }
+            if (requestDto.getFailureReason() != null) {
+                failureReasons.add(requestDto.getFailureReason());
+            }
+            if (requestDto.getLastUpdated() != null && (clientDto.getLastUpdated() == null || requestDto.getLastUpdated().after(clientDto.getLastUpdated()))) {
+                clientDto.setLastUpdated(requestDto.getLastUpdated());
+            }
+            if (requestDto.getPercentCompleted() < clientDto.getPercentCompleted()) {
+                clientDto.setPercentCompleted(requestDto.getPercentCompleted());
+                clientDto.setState(requestDto.getState());
+            }
+        }
+
+        String failureReason = failureReasons.stream()
+            .filter(Objects::nonNull)
+            .collect(Collectors.joining("\n"));
+        if (!failureReason.isEmpty()) {
+            clientDto.setFailureReason(failureReason);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ComponentStateEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ComponentStateEndpointMerger.java
index 3b03ed0b1e..9a57fa8957 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ComponentStateEndpointMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ComponentStateEndpointMerger.java
@@ -37,6 +37,7 @@ public class ComponentStateEndpointMerger extends AbstractSingleDTOEndpoint<Comp
     public static final Pattern PROCESSOR_STATE_URI_PATTERN = Pattern.compile("/nifi-api/processors/[a-f0-9\\-]{36}/state");
     public static final Pattern CONTROLLER_SERVICE_STATE_URI_PATTERN = Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}/state");
     public static final Pattern REPORTING_TASK_STATE_URI_PATTERN = Pattern.compile("/nifi-api/reporting-tasks/[a-f0-9\\-]{36}/state");
+    public static final Pattern FLOW_ANALYSIS_RULE_STATE_URI_PATTERN = Pattern.compile("/nifi-api/controller/flow-analysis-rules/[a-f0-9\\-]{36}/state");
 
     @Override
     public boolean canHandle(URI uri, String method) {
@@ -46,7 +47,8 @@ public class ComponentStateEndpointMerger extends AbstractSingleDTOEndpoint<Comp
 
         return PROCESSOR_STATE_URI_PATTERN.matcher(uri.getPath()).matches()
             || CONTROLLER_SERVICE_STATE_URI_PATTERN.matcher(uri.getPath()).matches()
-            || REPORTING_TASK_STATE_URI_PATTERN.matcher(uri.getPath()).matches();
+            || REPORTING_TASK_STATE_URI_PATTERN.matcher(uri.getPath()).matches()
+            || FLOW_ANALYSIS_RULE_STATE_URI_PATTERN.matcher(uri.getPath()).matches();
     }
 
     @Override
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerBulletinsEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerBulletinsEndpointMerger.java
index a4b0f70729..e5efb57493 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerBulletinsEndpointMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerBulletinsEndpointMerger.java
@@ -57,6 +57,7 @@ public class ControllerBulletinsEndpointMerger extends AbstractSingleEntityEndpo
         final Map<NodeIdentifier, List<BulletinEntity>> bulletinDtos = new HashMap<>();
         final Map<NodeIdentifier, List<BulletinEntity>> controllerServiceBulletinDtos = new HashMap<>();
         final Map<NodeIdentifier, List<BulletinEntity>> reportingTaskBulletinDtos = new HashMap<>();
+        final Map<NodeIdentifier, List<BulletinEntity>> flowAnalysisRuleBulletinDtos = new HashMap<>();
         final Map<NodeIdentifier, List<BulletinEntity>> flowRegistryClientBulletinDtos = new HashMap<>();
         final Map<NodeIdentifier, List<BulletinEntity>> parameterProviderBulletinDtos = new HashMap<>();
         for (final Map.Entry<NodeIdentifier, ControllerBulletinsEntity> entry : entityMap.entrySet()) {
@@ -92,6 +93,15 @@ public class ControllerBulletinsEndpointMerger extends AbstractSingleEntityEndpo
                     reportingTaskBulletinDtos.computeIfAbsent(nodeIdentifier, nodeId -> new ArrayList<>()).add(bulletin);
                 });
             }
+            if (entity.getFlowAnalysisRuleBulletins() != null) {
+                entity.getFlowAnalysisRuleBulletins().forEach(bulletin -> {
+                    if (bulletin.getNodeAddress() == null) {
+                        bulletin.setNodeAddress(nodeAddress);
+                    }
+
+                    flowAnalysisRuleBulletinDtos.computeIfAbsent(nodeIdentifier, nodeId -> new ArrayList<>()).add(bulletin);
+                });
+            }
             if (entity.getFlowRegistryClientBulletins() != null) {
                 entity.getFlowRegistryClientBulletins().forEach(bulletin -> {
                     if (bulletin.getNodeAddress() == null) {
@@ -115,12 +125,14 @@ public class ControllerBulletinsEndpointMerger extends AbstractSingleEntityEndpo
         clientEntity.setBulletins(BulletinMerger.mergeBulletins(bulletinDtos, entityMap.size()));
         clientEntity.setControllerServiceBulletins(BulletinMerger.mergeBulletins(controllerServiceBulletinDtos, entityMap.size()));
         clientEntity.setReportingTaskBulletins(BulletinMerger.mergeBulletins(reportingTaskBulletinDtos, entityMap.size()));
+        clientEntity.setFlowAnalysisRuleBulletins(BulletinMerger.mergeBulletins(flowAnalysisRuleBulletinDtos, entityMap.size()));
         clientEntity.setFlowRegistryClientBulletins(BulletinMerger.mergeBulletins(flowRegistryClientBulletinDtos, entityMap.size()));
 
         // sort the bulletins
         Collections.sort(clientEntity.getBulletins(), BULLETIN_COMPARATOR);
         Collections.sort(clientEntity.getControllerServiceBulletins(), BULLETIN_COMPARATOR);
         Collections.sort(clientEntity.getReportingTaskBulletins(), BULLETIN_COMPARATOR);
+        Collections.sort(clientEntity.getFlowAnalysisRuleBulletins(), BULLETIN_COMPARATOR);
         Collections.sort(clientEntity.getParameterProviderBulletins(), BULLETIN_COMPARATOR);
 
         // prune the response to only include the max number of bulletins
@@ -133,6 +145,9 @@ public class ControllerBulletinsEndpointMerger extends AbstractSingleEntityEndpo
         if (clientEntity.getReportingTaskBulletins().size() > MAX_BULLETINS_PER_COMPONENT) {
             clientEntity.setReportingTaskBulletins(clientEntity.getReportingTaskBulletins().subList(0, MAX_BULLETINS_PER_COMPONENT));
         }
+        if (clientEntity.getFlowAnalysisRuleBulletins().size() > MAX_BULLETINS_PER_COMPONENT) {
+            clientEntity.setFlowAnalysisRuleBulletins(clientEntity.getFlowAnalysisRuleBulletins().subList(0, MAX_BULLETINS_PER_COMPONENT));
+        }
         if (clientEntity.getParameterProviderBulletins().size() > MAX_BULLETINS_PER_COMPONENT) {
             clientEntity.setParameterProviderBulletins(clientEntity.getParameterProviderBulletins().subList(0, MAX_BULLETINS_PER_COMPONENT));
         }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisEndpointMerger.java
new file mode 100644
index 0000000000..71ca65c054
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisEndpointMerger.java
@@ -0,0 +1,62 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.FlowAnalysisResultEntityMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class FlowAnalysisEndpointMerger extends AbstractSingleEntityEndpoint<FlowAnalysisResultEntity> implements EndpointResponseMerger {
+    public static final String GET_ALL_FLOW_ANALYSIS_RESULTS_URI = "/nifi-api/flow/flow-analysis/result";
+    public static final Pattern GET_GROUP_FLOW_ANALYSIS_RESULTS_URI_PATTERN = Pattern.compile("/nifi-api/flow/flow-analysis/result/[a-f0-9\\-]{36}");
+
+    private final FlowAnalysisResultEntityMerger flowAnalysisResultEntityMerger = new FlowAnalysisResultEntityMerger();
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        if ("GET".equalsIgnoreCase(method)
+            && (GET_ALL_FLOW_ANALYSIS_RESULTS_URI.equals(uri.getPath()) ||  GET_GROUP_FLOW_ANALYSIS_RESULTS_URI_PATTERN.matcher(uri.getPath()).matches())
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected Class<FlowAnalysisResultEntity> getEntityClass() {
+        return FlowAnalysisResultEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(
+        FlowAnalysisResultEntity clientEntity,
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap,
+        Set<NodeResponse> successfulResponses,
+        Set<NodeResponse> problematicResponses
+    ) {
+        flowAnalysisResultEntityMerger.merge(clientEntity, entityMap);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleEndpointMerger.java
new file mode 100644
index 0000000000..1f123fd59b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleEndpointMerger.java
@@ -0,0 +1,98 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.manager.FlowAnalysisRuleEntityMerger;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class FlowAnalysisRuleEndpointMerger extends AbstractSingleEntityEndpoint<FlowAnalysisRuleEntity> implements EndpointResponseMerger {
+    private static final Collection<Endpoint> SUPPORTED_ENDPOINTS = Arrays.asList(
+      new Endpoint("/nifi-api/controller/flow-analysis-rules", "POST"),
+      new Endpoint(Pattern.compile("/nifi-api/controller/flow-analysis-rules/[a-f0-9\\-]{36}"), "GET", "PUT", "DELETE"),
+      new Endpoint(Pattern.compile("/nifi-api/controller/flow-analysis-rules/[a-f0-9\\-]{36}/run-status"), "PUT")
+    );
+
+
+    private final FlowAnalysisRuleEntityMerger flowAnalysisRuleEntityMerger = new FlowAnalysisRuleEntityMerger();
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        boolean canHandle = SUPPORTED_ENDPOINTS.stream()
+                .filter(supportedEndpoint -> supportedEndpoint.canHandle(uri, method))
+                .findAny()
+                .isPresent();
+
+        return canHandle;
+    }
+
+    @Override
+    protected Class<FlowAnalysisRuleEntity> getEntityClass() {
+        return FlowAnalysisRuleEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(
+        FlowAnalysisRuleEntity clientEntity,
+        Map<NodeIdentifier, FlowAnalysisRuleEntity> entityMap,
+        Set<NodeResponse> successfulResponses,
+        Set<NodeResponse> problematicResponses
+    ) {
+        flowAnalysisRuleEntityMerger.merge(clientEntity, entityMap);
+    }
+
+    private static class Endpoint {
+        final List<String> httpMethods;
+        final String uri;
+        final Pattern uriPattern;
+
+        public Endpoint(final String uri, final String... httpMethods) {
+            this.httpMethods = Arrays.asList(httpMethods);
+            this.uri = uri;
+            this.uriPattern = null;
+        }
+
+        public Endpoint(Pattern uriPattern, String... httpMethods) {
+            this.httpMethods = Arrays.asList(httpMethods);
+            this.uri = null;
+            this.uriPattern = uriPattern;
+        }
+
+        public boolean canHandle(URI uri, String method) {
+            boolean canHandle =
+                    httpMethods.stream().filter(httpMethod -> httpMethod.equalsIgnoreCase(method)).findAny().isPresent()
+                    && (
+                            this.uri != null && this.uri.equals(uri.getPath())
+                            ||
+                            this.uriPattern != null && this.uriPattern.matcher(uri.getPath()).matches()
+                    );
+
+            return canHandle;
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleTypesEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleTypesEndpointMerger.java
new file mode 100644
index 0000000000..b2203ff1ac
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRuleTypesEndpointMerger.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.manager.DocumentedTypesMerger;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.DocumentedTypeDTO;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleTypesEntity;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+
+public class FlowAnalysisRuleTypesEndpointMerger extends AbstractNodeStatusEndpoint<FlowAnalysisRuleTypesEntity, Set<DocumentedTypeDTO>> {
+    public static final String FLOW_ANALYSIS_RULE_TYPES_URI_PATTERN = "/nifi-api/flow/flow-analysis-rule-types";
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        return "GET".equalsIgnoreCase(method) && FLOW_ANALYSIS_RULE_TYPES_URI_PATTERN.equals(uri.getPath());
+    }
+
+    @Override
+    protected Class<FlowAnalysisRuleTypesEntity> getEntityClass() {
+        return FlowAnalysisRuleTypesEntity.class;
+    }
+
+    @Override
+    protected Set<DocumentedTypeDTO> getDto(FlowAnalysisRuleTypesEntity entity) {
+        return entity.getFlowAnalysisRuleTypes();
+    }
+
+    @Override
+    protected void mergeResponses(Set<DocumentedTypeDTO> clientDto, Map<NodeIdentifier, Set<DocumentedTypeDTO>> dtoMap, NodeIdentifier selectedNodeId) {
+        DocumentedTypesMerger.mergeDocumentedTypes(clientDto, dtoMap);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRulesEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRulesEndpointMerger.java
new file mode 100644
index 0000000000..4801964a36
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowAnalysisRulesEndpointMerger.java
@@ -0,0 +1,71 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.manager.FlowAnalysisRulesEntityMerger;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
+import org.apache.nifi.web.api.entity.FlowAnalysisRulesEntity;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class FlowAnalysisRulesEndpointMerger implements EndpointResponseMerger {
+    public static final String FLOW_ANALYSIS_RULES_URI = "/nifi-api/controller/flow-analysis-rules";
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        return "GET".equalsIgnoreCase(method) && FLOW_ANALYSIS_RULES_URI.equals(uri.getPath());
+    }
+
+    @Override
+    public final NodeResponse merge(final URI uri, final String method, final Set<NodeResponse> successfulResponses, final Set<NodeResponse> problematicResponses, final NodeResponse clientResponse) {
+        if (!canHandle(uri, method)) {
+            throw new IllegalArgumentException("Cannot use Endpoint Mapper of type " + getClass().getSimpleName() + " to map responses for URI " + uri + ", HTTP Method " + method);
+        }
+
+        final FlowAnalysisRulesEntity responseEntity = clientResponse.getClientResponse().readEntity(FlowAnalysisRulesEntity.class);
+        final Set<FlowAnalysisRuleEntity> flowAnalysisRulesEntities = responseEntity.getFlowAnalysisRules();
+
+        final Map<String, Map<NodeIdentifier, FlowAnalysisRuleEntity>> entityMap = new HashMap<>();
+        for (final NodeResponse nodeResponse : successfulResponses) {
+            final FlowAnalysisRulesEntity nodeResponseEntity = nodeResponse == clientResponse ? responseEntity : nodeResponse.getClientResponse().readEntity(FlowAnalysisRulesEntity.class);
+            final Set<FlowAnalysisRuleEntity> nodeFlowAnalysisRuleEntities = nodeResponseEntity.getFlowAnalysisRules();
+
+            for (final FlowAnalysisRuleEntity nodeFlowAnalysisRuleEntity : nodeFlowAnalysisRuleEntities) {
+                final NodeIdentifier nodeId = nodeResponse.getNodeId();
+                Map<NodeIdentifier, FlowAnalysisRuleEntity> innerMap = entityMap.get(nodeId);
+                if (innerMap == null) {
+                    innerMap = new HashMap<>();
+                    entityMap.put(nodeFlowAnalysisRuleEntity.getId(), innerMap);
+                }
+
+                innerMap.put(nodeResponse.getNodeId(), nodeFlowAnalysisRuleEntity);
+            }
+        }
+
+        FlowAnalysisRulesEntityMerger.mergeFlowAnalysisRules(flowAnalysisRulesEntities, entityMap);
+
+        // create a new client response
+        return new NodeResponse(clientResponse, responseEntity);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RuleViolationEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RuleViolationEndpointMerger.java
new file mode 100644
index 0000000000..68beb44763
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RuleViolationEndpointMerger.java
@@ -0,0 +1,50 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.RuleViolationEntity;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+
+public class RuleViolationEndpointMerger extends AbstractSingleEntityEndpoint<RuleViolationEntity> implements EndpointResponseMerger {
+    public static final String UPDATE_RULE_VIOLATION_URI = "/nifi-api/controller/analyze-flow/update-rule-violation";
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        if ("PUT".equalsIgnoreCase(method) && UPDATE_RULE_VIOLATION_URI.equals(uri.getPath())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected Class<RuleViolationEntity> getEntityClass() {
+        return RuleViolationEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(RuleViolationEntity clientEntity, Map<NodeIdentifier, RuleViolationEntity> entityMap, Set<NodeResponse> successfulResponses, Set<NodeResponse> problematicResponses) {
+        // Nothing to do, if there are no issues the entities are the same
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VerifyConfigEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VerifyConfigEndpointMerger.java
index f4f90770f4..50e062d852 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VerifyConfigEndpointMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VerifyConfigEndpointMerger.java
@@ -33,6 +33,7 @@ public class VerifyConfigEndpointMerger extends AbstractSingleEntityEndpoint<Ver
     public static final Pattern VERIFY_PROCESSOR_CONFIG_URI_PATTERN = Pattern.compile("/nifi-api/processors/[a-f0-9\\-]{36}/config/verification-requests(/[a-f0-9\\-]{36})?");
     public static final Pattern VERIFY_CONTROLLER_SERVICE_CONFIG_URI_PATTERN = Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}/config/verification-requests(/[a-f0-9\\-]{36})?");
     public static final Pattern VERIFY_REPORTING_TASK_CONFIG_URI_PATTERN = Pattern.compile("/nifi-api/reporting-tasks/[a-f0-9\\-]{36}/config/verification-requests(/[a-f0-9\\-]{36})?");
+    public static final Pattern VERIFY_FLOW_ANALYSIS_RULE_URI_PATTERN = Pattern.compile("/nifi-api/controller/flow-analysis-rules/[a-f0-9\\-]{36}/config/verification-requests(/[a-f0-9\\-]{36})?");
     public static final Pattern VERIFY_PARAMETER_PROVIDER_CONFIG_URI_PATTERN = Pattern.compile("/nifi-api/parameter-providers/[a-f0-9\\-]{36}/config/verification-requests(/[a-f0-9\\-]{36})?");
 
     @Override
@@ -45,6 +46,7 @@ public class VerifyConfigEndpointMerger extends AbstractSingleEntityEndpoint<Ver
         return VERIFY_PROCESSOR_CONFIG_URI_PATTERN.matcher(uri.getPath()).matches()
             || VERIFY_CONTROLLER_SERVICE_CONFIG_URI_PATTERN.matcher(uri.getPath()).matches()
             || VERIFY_REPORTING_TASK_CONFIG_URI_PATTERN.matcher(uri.getPath()).matches()
+            || VERIFY_FLOW_ANALYSIS_RULE_URI_PATTERN.matcher(uri.getPath()).matches()
             || VERIFY_PARAMETER_PROVIDER_CONFIG_URI_PATTERN.matcher(uri.getPath()).matches();
     }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMerger.java
new file mode 100644
index 0000000000..0ca480d91a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMerger.java
@@ -0,0 +1,74 @@
+/*
+ * 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.cluster.manager;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.validation.RuleViolationKey;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleViolationDTO;
+import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class FlowAnalysisResultEntityMerger {
+    public void merge(FlowAnalysisResultEntity clientEntity, Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap) {
+        List<FlowAnalysisRuleDTO> aggregateRules = clientEntity.getRules();
+        entityMap.values().stream()
+                .map(FlowAnalysisResultEntity::getRules)
+                .forEach(aggregateRules::addAll);
+
+        Map<RuleViolationKey, FlowAnalysisRuleViolationDTO> mergedViolations = clientEntity.getRuleViolations().stream().collect(Collectors.toMap(
+                violation -> new RuleViolationKey(
+                        violation.getScope(),
+                        violation.getSubjectId(),
+                        violation.getRuleId(),
+                        violation.getIssueId()
+                ),
+                violation -> violation
+        ));
+
+        for (final Map.Entry<NodeIdentifier, FlowAnalysisResultEntity> entry : entityMap.entrySet()) {
+            final FlowAnalysisResultEntity entity = entry.getValue();
+
+            entity.getRuleViolations().forEach(violation -> mergedViolations
+                    .compute(
+                            new RuleViolationKey(
+                                    violation.getScope(),
+                                    violation.getSubjectId(),
+                                    violation.getRuleId(),
+                                    violation.getIssueId()
+                            ),
+                            (ruleViolationKey, storedViolation) -> {
+                                if (storedViolation != null) {
+                                    PermissionsDtoMerger.mergePermissions(violation.getSubjectPermissionDto(), storedViolation.getSubjectPermissionDto());
+                                }
+
+                                return violation;
+                            }
+                    )
+            );
+        }
+
+        List<FlowAnalysisRuleViolationDTO> authorizedViolations = mergedViolations.values().stream()
+                .filter(violation -> violation.getSubjectPermissionDto().getCanRead())
+                .collect(Collectors.toList());
+
+        clientEntity.setRuleViolations(authorizedViolations);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRuleEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRuleEntityMerger.java
new file mode 100644
index 0000000000..7d06a4a6b3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRuleEntityMerger.java
@@ -0,0 +1,109 @@
+/*
+ * 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.cluster.manager;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class FlowAnalysisRuleEntityMerger implements ComponentEntityMerger<FlowAnalysisRuleEntity> {
+
+    @Override
+    public void merge(FlowAnalysisRuleEntity clientEntity, Map<NodeIdentifier, FlowAnalysisRuleEntity> entityMap) {
+        ComponentEntityMerger.super.merge(clientEntity, entityMap);
+        for (Map.Entry<NodeIdentifier, FlowAnalysisRuleEntity> entry : entityMap.entrySet()) {
+            final FlowAnalysisRuleEntity entityStatus = entry.getValue();
+            if (clientEntity != entityStatus) {
+                StatusMerger.merge(clientEntity.getStatus(), entityStatus.getStatus());
+            }
+        }
+    }
+
+    /**
+     * Merges the FlowAnalysisRuleEntity responses.
+     *
+     * @param clientEntity the entity being returned to the client
+     * @param entityMap all node responses
+     */
+    @Override
+    public void mergeComponents(final FlowAnalysisRuleEntity clientEntity, final Map<NodeIdentifier, FlowAnalysisRuleEntity> entityMap) {
+        final FlowAnalysisRuleDTO clientDto = clientEntity.getComponent();
+        final Map<NodeIdentifier, FlowAnalysisRuleDTO> dtoMap = new HashMap<>();
+        for (final Map.Entry<NodeIdentifier, FlowAnalysisRuleEntity> entry : entityMap.entrySet()) {
+            final FlowAnalysisRuleEntity nodeFlowAnalysisRuleEntity = entry.getValue();
+            final FlowAnalysisRuleDTO nodeFlowAnalysisRuleDto = nodeFlowAnalysisRuleEntity.getComponent();
+            dtoMap.put(entry.getKey(), nodeFlowAnalysisRuleDto);
+        }
+
+        mergeDtos(clientDto, dtoMap);
+    }
+
+    private static void mergeDtos(final FlowAnalysisRuleDTO clientDto, final Map<NodeIdentifier, FlowAnalysisRuleDTO> dtoMap) {
+        // if unauthorized for the client dto, simple return
+        if (clientDto == null) {
+            return;
+        }
+
+        final Map<String, Set<NodeIdentifier>> validationErrorMap = new HashMap<>();
+        final Map<String, Map<NodeIdentifier, PropertyDescriptorDTO>> propertyDescriptorMap = new HashMap<>();
+
+        for (final Map.Entry<NodeIdentifier, FlowAnalysisRuleDTO> nodeEntry : dtoMap.entrySet()) {
+            final FlowAnalysisRuleDTO nodeFlowAnalysisRule = nodeEntry.getValue();
+
+            // consider the node flow analysis rule if authorized
+            if (nodeFlowAnalysisRule != null) {
+                final NodeIdentifier nodeId = nodeEntry.getKey();
+
+                // merge the validation errors
+                ErrorMerger.mergeErrors(validationErrorMap, nodeId, nodeFlowAnalysisRule.getValidationErrors());
+
+                // aggregate the property descriptors
+                if (nodeFlowAnalysisRule.getDescriptors() != null) {
+                    nodeFlowAnalysisRule.getDescriptors().values().stream().forEach(propertyDescriptor -> {
+                        propertyDescriptorMap.computeIfAbsent(propertyDescriptor.getName(), nodeIdToPropertyDescriptor -> new HashMap<>()).put(nodeId, propertyDescriptor);
+                    });
+                }
+            }
+        }
+
+        // merge property descriptors
+        for (Map<NodeIdentifier, PropertyDescriptorDTO> propertyDescriptorByNodeId : propertyDescriptorMap.values()) {
+            final Collection<PropertyDescriptorDTO> nodePropertyDescriptors = propertyDescriptorByNodeId.values();
+            if (!nodePropertyDescriptors.isEmpty()) {
+                // get the name of the property descriptor and find that descriptor being returned to the client
+                final PropertyDescriptorDTO propertyDescriptor = nodePropertyDescriptors.iterator().next();
+                final PropertyDescriptorDTO clientPropertyDescriptor = clientDto.getDescriptors().get(propertyDescriptor.getName());
+                PropertyDescriptorDtoMerger.merge(clientPropertyDescriptor, propertyDescriptorByNodeId);
+            }
+        }
+
+        final Set<String> validationStatuses = dtoMap.values().stream()
+            .map(FlowAnalysisRuleDTO::getValidationStatus)
+            .collect(Collectors.toSet());
+        clientDto.setValidationStatus(ErrorMerger.mergeValidationStatus(validationStatuses));
+
+        // set the merged the validation errors
+        clientDto.setValidationErrors(ErrorMerger.normalizedMergedErrors(validationErrorMap, dtoMap.size()));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRulesEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRulesEntityMerger.java
new file mode 100644
index 0000000000..21c39ff119
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/FlowAnalysisRulesEntityMerger.java
@@ -0,0 +1,40 @@
+/*
+ * 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.cluster.manager;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
+
+import java.util.Map;
+import java.util.Set;
+
+public class FlowAnalysisRulesEntityMerger {
+
+    private static final FlowAnalysisRuleEntityMerger flowAnalysisRuleEntityMerger = new FlowAnalysisRuleEntityMerger();
+
+    /**
+     * Merges multiple FlowAnalysisRuleEntity responses.
+     *
+     * @param flowAnalysisRuleEntities entities being returned to the client
+     * @param entityMap all node responses
+     */
+    public static void mergeFlowAnalysisRules(final Set<FlowAnalysisRuleEntity> flowAnalysisRuleEntities, final Map<String, Map<NodeIdentifier, FlowAnalysisRuleEntity>> entityMap) {
+        for (final FlowAnalysisRuleEntity entity : flowAnalysisRuleEntities) {
+            flowAnalysisRuleEntityMerger.merge(entity, entityMap.get(entity.getId()));
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
index 836d36d19d..9e45ccb32f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
@@ -45,6 +45,7 @@ import org.apache.nifi.web.api.dto.status.ConnectionStatusPredictionsSnapshotDTO
 import org.apache.nifi.web.api.dto.status.ConnectionStatusSnapshotDTO;
 import org.apache.nifi.web.api.dto.status.ControllerServiceStatusDTO;
 import org.apache.nifi.web.api.dto.status.ControllerStatusDTO;
+import org.apache.nifi.web.api.dto.status.FlowAnalysisRuleStatusDTO;
 import org.apache.nifi.web.api.dto.status.NodeConnectionStatusSnapshotDTO;
 import org.apache.nifi.web.api.dto.status.NodePortStatusSnapshotDTO;
 import org.apache.nifi.web.api.dto.status.NodeProcessGroupStatusSnapshotDTO;
@@ -1060,4 +1061,16 @@ public class StatusMerger {
             target.setValidationStatus(ValidationStatus.INVALID.name());
         }
     }
+
+    public static void merge(final FlowAnalysisRuleStatusDTO target, final FlowAnalysisRuleStatusDTO toMerge) {
+        if (target == null || toMerge == null) {
+            return;
+        }
+
+        if (ValidationStatus.VALIDATING.name().equalsIgnoreCase(toMerge.getValidationStatus())) {
+            target.setValidationStatus(ValidationStatus.VALIDATING.name());
+        } else if (ValidationStatus.INVALID.name().equalsIgnoreCase(toMerge.getRunStatus())) {
+            target.setValidationStatus(ValidationStatus.INVALID.name());
+        }
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMergerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMergerTest.java
new file mode 100644
index 0000000000..9fcdc75637
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/AnalyzeFlowRequestEndpointMergerTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.util.EqualsWrapper;
+import org.apache.nifi.web.api.dto.AnalyzeFlowRequestDTO;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+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.function.Function;
+
+import static org.mockito.Mockito.mock;
+
+public class AnalyzeFlowRequestEndpointMergerTest {
+
+    private AnalyzeFlowRequestEndpointMerger testSubject;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        testSubject = new AnalyzeFlowRequestEndpointMerger();
+    }
+
+    @Test
+    public void testAllRequestsWaiting() throws Exception {
+        // GIVEN
+        AnalyzeFlowRequestDTO clientDto = createRequest(false, null, "WAITING");
+
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap = new HashMap<>();
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(false, null, "WAITING"));
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(false, null, "WAITING"));
+
+        AnalyzeFlowRequestDTO expected = createRequest(false, null, "WAITING");
+
+        // WHEN
+        testSubject.mergeResponses(clientDto, dtoMap, null, null);
+
+        // THEN
+        this.assertEquals(expected, clientDto);
+    }
+
+    @Test
+    public void testClientRequestWaitingOthersComplete() throws Exception {
+        // GIVEN
+        AnalyzeFlowRequestDTO clientDto = createRequest(false, null, "WAITING");
+
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap = new HashMap<>();
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, null, "COMPLETE"));
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, null, "COMPLETE"));
+
+        AnalyzeFlowRequestDTO expected = createRequest(false, null, "WAITING");
+
+        // WHEN
+        testSubject.mergeResponses(clientDto, dtoMap, null, null);
+
+        // THEN
+        this.assertEquals(expected, clientDto);
+    }
+
+    @Test
+    public void testOneNonClientRequestCompleteOthersWaiting() throws Exception {
+        // GIVEN
+        AnalyzeFlowRequestDTO clientDto = createRequest(false, null, "WAITING");
+
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap = new HashMap<>();
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(false, null, "WAITING"));
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, null, "COMPLETE"));
+
+        AnalyzeFlowRequestDTO expected = createRequest(false, null, "WAITING");
+
+        // WHEN
+        testSubject.mergeResponses(clientDto, dtoMap, null, null);
+
+        // THEN
+        this.assertEquals(expected, clientDto);
+    }
+
+    @Test
+    public void testAllRequestsComplete() throws Exception {
+        // GIVEN
+        AnalyzeFlowRequestDTO clientDto = createRequest(true, null, "COMPLETE");
+
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap = new HashMap<>();
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, null, "COMPLETE"));
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, null, "COMPLETE"));
+
+        AnalyzeFlowRequestDTO expected = createRequest(true, null, "COMPLETE");
+
+        // WHEN
+        testSubject.mergeResponses(clientDto, dtoMap, null, null);
+
+        // THEN
+        this.assertEquals(expected, clientDto);
+    }
+
+    @Test
+    public void testMergeFailures() throws Exception {
+        // GIVEN
+        AnalyzeFlowRequestDTO clientDto = createRequest(true, "failure1", "FAILURE");
+
+        Map<NodeIdentifier, AnalyzeFlowRequestDTO> dtoMap = new HashMap<>();
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, "failure2", "FAILURE"));
+        dtoMap.put(mock(NodeIdentifier.class), createRequest(true, "failure3", "FAILURE"));
+
+        Set<String> expectedFailures = new HashSet<>(Arrays.asList("failure1", "failure2", "failure3"));
+
+        // WHEN
+        testSubject.mergeResponses(clientDto, dtoMap, null, null);
+
+        // THEN
+        HashSet<String> actualFailures = new HashSet<>(Arrays.asList(clientDto.getFailureReason().split("\n")));
+
+        Assertions.assertEquals(expectedFailures, actualFailures);
+    }
+
+    private AnalyzeFlowRequestDTO createRequest(
+        Boolean complete,
+        String failureReason,
+        String state
+    ) {
+        AnalyzeFlowRequestDTO request = new AnalyzeFlowRequestDTO();
+
+        request.setComplete(complete);
+        request.setFailureReason(failureReason);
+        request.setState(state);
+
+        return request;
+    }
+
+    private void assertEquals(AnalyzeFlowRequestDTO expected, AnalyzeFlowRequestDTO clientDto) {
+        List<Function<AnalyzeFlowRequestDTO, Object>> equalityCheckers = Arrays.asList(
+            AnalyzeFlowRequestDTO::isComplete,
+            AnalyzeFlowRequestDTO::getFailureReason,
+            AnalyzeFlowRequestDTO::getState
+        );
+
+        Assertions.assertEquals(
+            new EqualsWrapper<>(expected, equalityCheckers),
+            new EqualsWrapper<>(clientDto, equalityCheckers)
+        );
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMergerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMergerTest.java
new file mode 100644
index 0000000000..a76daf61d7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/manager/FlowAnalysisResultEntityMergerTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.cluster.manager;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.util.EqualsWrapper;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleViolationDTO;
+import org.apache.nifi.web.api.dto.PermissionsDTO;
+import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FlowAnalysisResultEntityMergerTest {
+    public static final NodeIdentifier NODE_ID_1 = nodeIdOf("id1");
+    public static final NodeIdentifier NODE_ID_2 = nodeIdOf("id2");
+
+    private FlowAnalysisResultEntityMerger testSubject;
+
+    @BeforeEach
+    void setUp() {
+        testSubject = new FlowAnalysisResultEntityMerger();
+    }
+
+    @Test
+    void differentViolationsAreMerged() {
+        // GIVEN
+        FlowAnalysisResultEntity clientEntity = resultEntityOf(
+                listOf(ruleOf("ruleId")),
+                listOf(ruleViolationOf("ruleId", true, true))
+        );
+
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap = resultEntityMapOf(
+                resultEntityOf(
+                        listOf(ruleOf("ruleId1")),
+                        listOf(ruleViolationOf("ruleId1", true, true))
+                ),
+                resultEntityOf(
+                        listOf(ruleOf("ruleId2")),
+                        listOf(ruleViolationOf("ruleId2", true, true))
+                )
+        );
+
+        FlowAnalysisResultEntity expectedClientEntity = resultEntityOf(
+                listOf(ruleOf("ruleId"), ruleOf("ruleId1"), ruleOf("ruleId2")),
+                listOf(
+                        ruleViolationOf("ruleId", true, true),
+                        ruleViolationOf("ruleId1", true, true),
+                        ruleViolationOf("ruleId2", true, true)
+                )
+        );
+
+        testMerge(clientEntity, entityMap, expectedClientEntity);
+    }
+
+    @Test
+    void violationThatCannotBeReadOnAnyNodeIsOmitted() {
+        // GIVEN
+        String ruleId = "ruleWithViolationThatCantBeReadOnOneNode";
+
+        FlowAnalysisResultEntity clientEntity = resultEntityOf(
+                listOf(ruleOf(ruleId)),
+                listOf(ruleViolationOf(ruleId, true, true))
+        );
+
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap = resultEntityMapOf(
+                resultEntityOf(
+                        listOf(ruleOf(ruleId)),
+                        listOf(ruleViolationOf(ruleId, false, true))
+                ),
+                resultEntityOf(
+                        listOf(ruleOf(ruleId)),
+                        listOf(ruleViolationOf(ruleId, true, true))
+                )
+        );
+
+        FlowAnalysisResultEntity expectedClientEntity = resultEntityOf(
+                listOf(ruleOf(ruleId)),
+                listOf()
+        );
+
+        testMerge(clientEntity, entityMap, expectedClientEntity);
+    }
+
+    @Test
+    void evenWhenViolationIsOmittedTheRuleIsNot() {
+        // GIVEN
+        FlowAnalysisResultEntity clientEntity = resultEntityOf(
+                listOf(),
+                listOf()
+        );
+
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap = resultEntityMapOf(
+                resultEntityOf(
+                        listOf(ruleOf("notOmittedRuleButOmittedViolation")),
+                        listOf(ruleViolationOf("notOmittedRuleButOmittedViolation", false, true))
+                ),
+                resultEntityOf(
+                        listOf(),
+                        listOf()
+                )
+        );
+
+        FlowAnalysisResultEntity expectedClientEntity = resultEntityOf(
+                listOf(ruleOf("notOmittedRuleButOmittedViolation")),
+                listOf()
+        );
+
+        testMerge(clientEntity, entityMap, expectedClientEntity);
+    }
+
+    @Test
+    void violationThatCannotBeWrittenIsNotOmitted() {
+        // GIVEN
+        String ruleId = "ruleWithViolationThatCantBeWrittenOnOneNode";
+
+        FlowAnalysisResultEntity clientEntity = resultEntityOf(
+                listOf(ruleOf(ruleId)),
+                listOf(ruleViolationOf(ruleId, true, false))
+        );
+
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap = resultEntityMapOf(
+                resultEntityOf(
+                        listOf(ruleOf(ruleId)),
+                        listOf(ruleViolationOf(ruleId, true, false))
+                ),
+                resultEntityOf(
+                        listOf(ruleOf(ruleId)),
+                        listOf(ruleViolationOf(ruleId, true, false))
+                )
+        );
+
+        FlowAnalysisResultEntity expectedClientEntity = clientEntity;
+
+        testMerge(clientEntity, entityMap, expectedClientEntity);
+    }
+
+    private void testMerge(FlowAnalysisResultEntity clientEntity, Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap, FlowAnalysisResultEntity expectedClientEntity) {
+        // GIVEN
+        List<Function<FlowAnalysisRuleDTO, Object>> rulePropertiesProviders = Arrays.asList(FlowAnalysisRuleDTO::getId);
+        List<Function<FlowAnalysisRuleViolationDTO, Object>> list = Arrays.asList(
+                FlowAnalysisRuleViolationDTO::getRuleId,
+                FlowAnalysisRuleViolationDTO::isEnabled,
+                ruleViolation -> ruleViolation.getSubjectPermissionDto().getCanRead(),
+                ruleViolation -> ruleViolation.getSubjectPermissionDto().getCanWrite()
+        );
+        List<Function<FlowAnalysisResultEntity, Object>> resultEntityEqualsPropertiesProviders = Arrays.asList(
+                resultEntity -> new HashSet<>(EqualsWrapper.wrapList(resultEntity.getRules(), rulePropertiesProviders)),
+                resultEntity -> new HashSet<>(EqualsWrapper.wrapList(resultEntity.getRuleViolations(), list))
+        );
+
+        // WHEN
+        testSubject.merge(clientEntity, entityMap);
+
+        // THEN
+        assertEquals(new EqualsWrapper<>(
+                expectedClientEntity,
+                resultEntityEqualsPropertiesProviders
+        ), new EqualsWrapper<>(
+                clientEntity,
+                resultEntityEqualsPropertiesProviders
+        ));
+    }
+
+    @NotNull
+    private static NodeIdentifier nodeIdOf(String nodeId) {
+        NodeIdentifier nodeIdentifier = new NodeIdentifier(nodeId, "unimportant", 1, "unimportant", 1, "unimportant", 1, 1, false);
+        return nodeIdentifier;
+    }
+
+    @NotNull
+    private static FlowAnalysisRuleDTO ruleOf(String ruleId) {
+        FlowAnalysisRuleDTO rule = new FlowAnalysisRuleDTO();
+
+        rule.setId(ruleId);
+
+        return rule;
+    }
+
+    @NotNull
+    private static FlowAnalysisRuleViolationDTO ruleViolationOf(
+            String ruleId,
+            boolean canRead,
+            boolean canWrite
+    ) {
+        FlowAnalysisRuleViolationDTO ruleViolation = new FlowAnalysisRuleViolationDTO();
+
+        ruleViolation.setRuleId(ruleId);
+        ruleViolation.setSubjectPermissionDto(permissionOf(canRead, canWrite));
+
+        return ruleViolation;
+    }
+
+    @NotNull
+    private static PermissionsDTO permissionOf(boolean canRead, boolean canWrite) {
+        PermissionsDTO subjectPermissionDto = new PermissionsDTO();
+
+        subjectPermissionDto.setCanRead(canRead);
+        subjectPermissionDto.setCanWrite(canWrite);
+
+        return subjectPermissionDto;
+    }
+
+    @NotNull
+    private static FlowAnalysisResultEntity resultEntityOf(List<FlowAnalysisRuleDTO> rules, List<FlowAnalysisRuleViolationDTO> ruleViolations) {
+        FlowAnalysisResultEntity clientEntity = new FlowAnalysisResultEntity();
+
+        clientEntity.setRules(rules);
+        clientEntity.setRuleViolations(ruleViolations);
+
+        return clientEntity;
+    }
+
+    @NotNull
+    private static Map<NodeIdentifier, FlowAnalysisResultEntity> resultEntityMapOf(FlowAnalysisResultEntity clientEntity1, FlowAnalysisResultEntity clientEntity2) {
+        Map<NodeIdentifier, FlowAnalysisResultEntity> entityMap = new HashMap<>();
+
+        entityMap.put(NODE_ID_1, clientEntity1);
+        entityMap.put(NODE_ID_2, clientEntity2);
+
+        return entityMap;
+    }
+
+    @NotNull
+    private static <T> List<T> listOf(T... items) {
+        List<T> itemSet = new ArrayList<>();
+        for (T item : items) {
+            itemSet.add(item);
+
+        }
+        return itemSet;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flow/AbstractFlowManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flow/AbstractFlowManager.java
index b39ca0f418..a9a2c02635 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flow/AbstractFlowManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flow/AbstractFlowManager.java
@@ -26,9 +26,11 @@ import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.ParameterProviderNode;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.controller.repository.FlowFileEventRepository;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
@@ -49,6 +51,7 @@ import org.apache.nifi.registry.flow.FlowRegistryClientNode;
 import org.apache.nifi.remote.PublicPort;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.util.ReflectionUtils;
+import org.apache.nifi.validation.RuleViolationsManager;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -74,6 +77,7 @@ public abstract class AbstractFlowManager implements FlowManager {
     private final ConcurrentMap<String, Port> allOutputPorts = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, Funnel> allFunnels = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, ReportingTaskNode> allReportingTasks = new ConcurrentHashMap<>();
+    private final ConcurrentMap<String, FlowAnalysisRuleNode> allFlowAnalysisRules = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, ParameterProviderNode> allParameterProviders = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, FlowRegistryClientNode> allFlowRegistryClients = new ConcurrentHashMap<>();
 
@@ -83,6 +87,8 @@ public abstract class AbstractFlowManager implements FlowManager {
 
     private volatile ControllerServiceProvider controllerServiceProvider;
     private volatile PythonBridge pythonBridge;
+    private volatile FlowAnalyzer flowAnalyzer;
+    private volatile RuleViolationsManager ruleViolationsManager;
     private volatile ProcessGroup rootGroup;
 
     private final ThreadLocal<Boolean> withParameterContextResolution = ThreadLocal.withInitial(() -> false);
@@ -94,9 +100,16 @@ public abstract class AbstractFlowManager implements FlowManager {
         this.flowInitializedCheck = flowInitializedCheck;
     }
 
-    public void initialize(final ControllerServiceProvider controllerServiceProvider, final PythonBridge pythonBridge) {
+    public void initialize(
+            final ControllerServiceProvider controllerServiceProvider,
+            final PythonBridge pythonBridge,
+            final FlowAnalyzer flowAnalyzer,
+            final RuleViolationsManager ruleViolationsManager
+    ) {
         this.controllerServiceProvider = controllerServiceProvider;
         this.pythonBridge = pythonBridge;
+        this.flowAnalyzer = flowAnalyzer;
+        this.ruleViolationsManager = ruleViolationsManager;
     }
 
     public ProcessGroup getGroup(final String id) {
@@ -108,7 +121,10 @@ public abstract class AbstractFlowManager implements FlowManager {
     }
 
     public void onProcessGroupRemoved(final ProcessGroup group) {
-        allProcessGroups.remove(group.getIdentifier());
+        String identifier = group.getIdentifier();
+        allProcessGroups.remove(identifier);
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public void onProcessorAdded(final ProcessorNode procNode) {
@@ -120,6 +136,8 @@ public abstract class AbstractFlowManager implements FlowManager {
         flowFileEventRepository.purgeTransferEvents(identifier);
         allProcessors.remove(identifier);
         pythonBridge.onProcessorRemoved(identifier, procNode.getComponentType(), procNode.getBundleCoordinate().getVersion());
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public Set<ProcessorNode> findAllProcessors(final Predicate<ProcessorNode> filter) {
@@ -178,6 +196,8 @@ public abstract class AbstractFlowManager implements FlowManager {
         String identifier = connection.getIdentifier();
         flowFileEventRepository.purgeTransferEvents(identifier);
         allConnections.remove(identifier);
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public Connection getConnection(final String id) {
@@ -226,6 +246,7 @@ public abstract class AbstractFlowManager implements FlowManager {
         componentCounts.put("Processors", allProcessors.size());
         componentCounts.put("Controller Services", getAllControllerServices().size());
         componentCounts.put("Reporting Tasks", getAllReportingTasks().size());
+        componentCounts.put("Flow Analysis Rules", getAllFlowAnalysisRules().size());
         componentCounts.put("Process Groups", allProcessGroups.size() - 2); // -2 to account for the root group because we don't want it in our counts and the 'root group alias' key.
         componentCounts.put("Remote Process Groups", getRootGroup().findAllRemoteProcessGroups().size());
         componentCounts.put("Parameter Providers", getAllParameterProviders().size());
@@ -286,6 +307,7 @@ public abstract class AbstractFlowManager implements FlowManager {
 
         getRootControllerServices().forEach(this::removeRootControllerService);
         getAllReportingTasks().forEach(this::removeReportingTask);
+        getAllFlowAnalysisRules().forEach(this::removeFlowAnalysisRule);
         getAllParameterProviders().forEach(this::removeParameterProvider);
 
         getAllFlowRegistryClients().forEach(this::removeFlowRegistryClientNode);
@@ -306,6 +328,10 @@ public abstract class AbstractFlowManager implements FlowManager {
             reportingTask.verifyCanDelete();
         }
 
+        for (final FlowAnalysisRuleNode flowAnalysisRule : getAllFlowAnalysisRules()) {
+            flowAnalysisRule.verifyCanDelete();
+        }
+
         for (final ParameterProviderNode parameterProvider : getAllParameterProviders()) {
             parameterProvider.verifyCanDelete();
         }
@@ -333,6 +359,8 @@ public abstract class AbstractFlowManager implements FlowManager {
         String identifier = inputPort.getIdentifier();
         flowFileEventRepository.purgeTransferEvents(identifier);
         allInputPorts.remove(identifier);
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public Port getInputPort(final String id) {
@@ -347,6 +375,8 @@ public abstract class AbstractFlowManager implements FlowManager {
         String identifier = outputPort.getIdentifier();
         flowFileEventRepository.purgeTransferEvents(identifier);
         allOutputPorts.remove(identifier);
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public Port getOutputPort(final String id) {
@@ -361,6 +391,8 @@ public abstract class AbstractFlowManager implements FlowManager {
         String identifier = funnel.getIdentifier();
         flowFileEventRepository.purgeTransferEvents(identifier);
         allFunnels.remove(identifier);
+
+        ruleViolationsManager.removeRuleViolationsForSubject(identifier);
     }
 
     public Funnel getFunnel(final String id) {
@@ -435,6 +467,58 @@ public abstract class AbstractFlowManager implements FlowManager {
         allReportingTasks.put(taskNode.getIdentifier(), taskNode);
     }
 
+    @Override
+    public FlowAnalysisRuleNode createFlowAnalysisRule(final String type, final String id, final BundleCoordinate bundleCoordinate, final boolean firstTimeAdded) {
+        return createFlowAnalysisRule(type, id, bundleCoordinate, Collections.emptySet(), firstTimeAdded, true, null);
+    }
+
+    @Override
+    public FlowAnalysisRuleNode getFlowAnalysisRuleNode(final String taskId) {
+        return allFlowAnalysisRules.get(taskId);
+    }
+
+    @Override
+    public void removeFlowAnalysisRule(final FlowAnalysisRuleNode flowAnalysisRuleNode) {
+        final FlowAnalysisRuleNode existing = allFlowAnalysisRules.get(flowAnalysisRuleNode.getIdentifier());
+        if (existing == null || existing != flowAnalysisRuleNode) {
+            throw new IllegalStateException("Flow Analysis Rule " + flowAnalysisRuleNode + " does not exist in this Flow");
+        }
+
+        flowAnalysisRuleNode.verifyCanDelete();
+
+        final Class<?> taskClass = flowAnalysisRuleNode.getFlowAnalysisRule().getClass();
+        try (final NarCloseable x = NarCloseable.withComponentNarLoader(getExtensionManager(), taskClass, flowAnalysisRuleNode.getFlowAnalysisRule().getIdentifier())) {
+            ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, flowAnalysisRuleNode.getFlowAnalysisRule(), flowAnalysisRuleNode.getConfigurationContext());
+        }
+
+        for (final Map.Entry<PropertyDescriptor, String> entry : flowAnalysisRuleNode.getEffectivePropertyValues().entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+            if (descriptor.getControllerServiceDefinition() != null) {
+                final String value = entry.getValue() == null ? descriptor.getDefaultValue() : entry.getValue();
+                if (value != null) {
+                    final ControllerServiceNode serviceNode = controllerServiceProvider.getControllerServiceNode(value);
+                    if (serviceNode != null) {
+                        serviceNode.removeReference(flowAnalysisRuleNode, descriptor);
+                    }
+                }
+            }
+        }
+
+        allFlowAnalysisRules.remove(flowAnalysisRuleNode.getIdentifier());
+        LogRepositoryFactory.removeRepository(flowAnalysisRuleNode.getIdentifier());
+
+        getExtensionManager().removeInstanceClassLoader(flowAnalysisRuleNode.getIdentifier());
+    }
+
+    @Override
+    public Set<FlowAnalysisRuleNode> getAllFlowAnalysisRules() {
+        return new HashSet<>(allFlowAnalysisRules.values());
+    }
+
+    protected void onFlowAnalysisRuleAdded(final FlowAnalysisRuleNode flowAnalysisRuleNode) {
+        allFlowAnalysisRules.put(flowAnalysisRuleNode.getIdentifier(), flowAnalysisRuleNode);
+    }
+
     @Override
     public ParameterProviderNode getParameterProvider(final String id) {
         return id == null ? null : allParameterProviders.get(id);
@@ -607,4 +691,14 @@ public abstract class AbstractFlowManager implements FlowManager {
     }
 
     protected abstract Authorizable getParameterContextParent();
+
+    @Override
+    public FlowAnalyzer getFlowAnalyzer() {
+        return flowAnalyzer;
+    }
+
+    @Override
+    public RuleViolationsManager getRuleViolationsManager() {
+        return ruleViolationsManager;
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleContext.java
new file mode 100644
index 0000000000..a1b0cf93ef
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleContext.java
@@ -0,0 +1,104 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.attribute.expression.language.PreparedQuery;
+import org.apache.nifi.attribute.expression.language.Query;
+import org.apache.nifi.attribute.expression.language.StandardPropertyValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.resource.ResourceContext;
+import org.apache.nifi.components.resource.StandardResourceContext;
+import org.apache.nifi.components.resource.StandardResourceReferenceFactory;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public abstract class AbstractFlowAnalysisRuleContext implements FlowAnalysisRuleContext {
+    private final FlowAnalysisRuleNode flowAnalysisRuleNode;
+    private final Map<PropertyDescriptor, String> properties;
+    private final ControllerServiceProvider serviceProvider;
+    private final Map<PropertyDescriptor, PreparedQuery> preparedQueries;
+    private final ParameterLookup parameterLookup;
+    private final VariableRegistry variableRegistry;
+
+    public AbstractFlowAnalysisRuleContext(
+            FlowAnalysisRuleNode flowAnalysisRule,
+            Map<PropertyDescriptor, String> properties,
+            ControllerServiceProvider controllerServiceProvider,
+            ParameterLookup parameterLookup,
+            VariableRegistry variableRegistry
+    ) {
+        this.flowAnalysisRuleNode = flowAnalysisRule;
+        this.properties = Collections.unmodifiableMap(properties);
+        this.serviceProvider = controllerServiceProvider;
+        this.preparedQueries = new HashMap<>();
+        this.parameterLookup = parameterLookup;
+        this.variableRegistry = variableRegistry;
+
+        for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) {
+            final PropertyDescriptor desc = entry.getKey();
+            String value = entry.getValue();
+            if (value == null) {
+                value = desc.getDefaultValue();
+            }
+
+            final PreparedQuery pq = Query.prepare(value);
+            preparedQueries.put(desc, pq);
+        }
+    }
+
+    protected FlowAnalysisRule getFlowAnalysisRule() {
+        return flowAnalysisRuleNode.getFlowAnalysisRule();
+    }
+
+    @Override
+    public Map<PropertyDescriptor, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public Map<String, String> getAllProperties() {
+        final Map<String,String> propValueMap = new LinkedHashMap<>();
+        for (final Map.Entry<PropertyDescriptor, String> entry : getProperties().entrySet()) {
+            propValueMap.put(entry.getKey().getName(), entry.getValue());
+        }
+        return propValueMap;
+    }
+
+    @Override
+    public PropertyValue getProperty(final PropertyDescriptor property) {
+        final PropertyDescriptor descriptor = flowAnalysisRuleNode.getPropertyDescriptor(property.getName());
+        if (descriptor == null) {
+            return null;
+        }
+
+        final String configuredValue = properties.get(property);
+        final ResourceContext resourceContext = new StandardResourceContext(new StandardResourceReferenceFactory(), descriptor);
+        return new StandardPropertyValue(resourceContext, configuredValue == null ? descriptor.getDefaultValue() : configuredValue, serviceProvider, parameterLookup, preparedQueries.get(property),
+            variableRegistry);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleNode.java
new file mode 100644
index 0000000000..7393a5ae66
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/AbstractFlowAnalysisRuleNode.java
@@ -0,0 +1,376 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.annotation.lifecycle.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.components.validation.ValidationTrigger;
+import org.apache.nifi.controller.AbstractComponentNode;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.TerminationAwareLogger;
+import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.StandardConfigurationContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleState;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
+import org.apache.nifi.flowanalysis.VerifiableFlowAnalysisRule;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.logging.StandardLoggingContext;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.InstanceClassLoader;
+import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.processor.SimpleProcessLogger;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.util.CharacterFilterUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.ReflectionUtils;
+import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
+import org.apache.nifi.validation.RuleViolationsManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+public abstract class AbstractFlowAnalysisRuleNode extends AbstractComponentNode implements FlowAnalysisRuleNode {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final AtomicReference<FlowAnalysisRuleDetails> flowAnalysisRuleRef;
+    private final ControllerServiceLookup serviceLookup;
+    private final RuleViolationsManager ruleViolationsManager;
+
+    private volatile String comment;
+    private EnforcementPolicy enforcementPolicy;
+
+    private volatile FlowAnalysisRuleState state = FlowAnalysisRuleState.DISABLED;
+
+    public AbstractFlowAnalysisRuleNode(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule, final String id,
+                                        final ControllerServiceProvider controllerServiceProvider,
+                                        final ValidationContextFactory validationContextFactory,
+                                        final RuleViolationsManager ruleViolationsManager,
+                                        final ComponentVariableRegistry variableRegistry,
+                                        final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger) {
+
+        this(flowAnalysisRule, id, controllerServiceProvider, validationContextFactory, ruleViolationsManager,
+                flowAnalysisRule.getComponent().getClass().getSimpleName(), flowAnalysisRule.getComponent().getClass().getCanonicalName(),
+                variableRegistry, reloadComponent, extensionManager, validationTrigger, false);
+    }
+
+
+    public AbstractFlowAnalysisRuleNode(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule, final String id, final ControllerServiceProvider controllerServiceProvider,
+                                        final ValidationContextFactory validationContextFactory, final RuleViolationsManager ruleViolationsManager,
+                                        final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry,
+                                        final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger,
+                                        final boolean isExtensionMissing) {
+
+        super(id, validationContextFactory, controllerServiceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent,
+                extensionManager, validationTrigger, isExtensionMissing);
+        this.flowAnalysisRuleRef = new AtomicReference<>(new FlowAnalysisRuleDetails(flowAnalysisRule));
+        this.serviceLookup = controllerServiceProvider;
+        this.ruleViolationsManager = ruleViolationsManager;
+        this.enforcementPolicy = EnforcementPolicy.WARN;
+    }
+
+    @Override
+    public EnforcementPolicy getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    @Override
+    public void setEnforcementPolicy(EnforcementPolicy enforcementPolicy) {
+        this.enforcementPolicy = enforcementPolicy;
+    }
+
+    @Override
+    public ConfigurableComponent getComponent() {
+        return flowAnalysisRuleRef.get().getFlowAnalysisRule();
+    }
+
+    @Override
+    public BundleCoordinate getBundleCoordinate() {
+        return flowAnalysisRuleRef.get().getBundleCoordinate();
+    }
+
+    @Override
+    public TerminationAwareLogger getLogger() {
+        return flowAnalysisRuleRef.get().getComponentLog();
+    }
+
+    @Override
+    public FlowAnalysisRule getFlowAnalysisRule() {
+        return flowAnalysisRuleRef.get().getFlowAnalysisRule();
+    }
+
+    @Override
+    public void setFlowAnalysisRule(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule) {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot modify Flow Analysis Rule configuration while it is enabled");
+        }
+        this.flowAnalysisRuleRef.set(new FlowAnalysisRuleDetails(flowAnalysisRule));
+    }
+
+    @Override
+    public void reload(final Set<URL> additionalUrls) throws FlowAnalysisRuleInstantiationException {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot reload Flow Analysis Rule while it is enabled");
+        }
+        String additionalResourcesFingerprint = ClassLoaderUtils.generateAdditionalUrlsFingerprint(additionalUrls, determineClasloaderIsolationKey());
+        setAdditionalResourcesFingerprint(additionalResourcesFingerprint);
+        getReloadComponent().reload(this, getCanonicalClassName(), getBundleCoordinate(), additionalUrls);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return FlowAnalysisRuleState.ENABLED.equals(state);
+    }
+
+    @Override
+    public boolean isValidationNecessary() {
+        return !isEnabled() || getValidationStatus() != ValidationStatus.VALID;
+    }
+
+    @Override
+    public ConfigurationContext getConfigurationContext() {
+        return new StandardConfigurationContext(this, serviceLookup, null, getVariableRegistry());
+    }
+
+    @Override
+    public void verifyModifiable() throws IllegalStateException {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot modify Flow Analysis Rule while it is enabled");
+        }
+    }
+
+    @Override
+    public FlowAnalysisRuleState getState() {
+        return state;
+    }
+
+    @Override
+    public String getComments() {
+        return comment;
+    }
+
+    @Override
+    public void setComments(final String comment) {
+        this.comment = CharacterFilterUtils.filterInvalidXmlCharacters(comment);
+    }
+
+    @Override
+    public void verifyCanDelete() {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot delete " + getFlowAnalysisRule().getIdentifier() + " because it is enabled");
+        }
+    }
+
+    @Override
+    public void verifyCanDisable() {
+        if (!isEnabled()) {
+            throw new IllegalStateException("Cannot disable " + getFlowAnalysisRule().getIdentifier() + " because it is already disabled");
+        }
+    }
+
+    @Override
+    public void verifyCanEnable() {
+        if (getValidationStatus() == ValidationStatus.INVALID) {
+            throw new IllegalStateException("Cannot enable " + getFlowAnalysisRule().getIdentifier() + " because it is in INVALID status");
+        }
+
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot enable " + getFlowAnalysisRule().getIdentifier() + " because it is not disabled");
+        }
+    }
+
+    @Override
+    public void verifyCanEnable(final Set<ControllerServiceNode> ignoredServices) {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot enable " + getFlowAnalysisRule().getIdentifier() + " because it is not disabled");
+        }
+
+        final Collection<ValidationResult> validationResults = getValidationErrors(ignoredServices);
+        if (!validationResults.isEmpty()) {
+            throw new IllegalStateException(this + " cannot be enabled because it is not currently valid");
+        }
+    }
+
+    @Override
+    public void verifyCanUpdate() {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot update " + getFlowAnalysisRule().getIdentifier() + " because it is currently enabled");
+        }
+    }
+
+    @Override
+    public void verifyCanClearState() {
+        verifyCanUpdate();
+    }
+
+    @Override
+    public String getProcessGroupIdentifier() {
+        return null;
+    }
+
+    @Override
+    public ParameterLookup getParameterLookup() {
+        return ParameterLookup.EMPTY;
+    }
+
+    @Override
+    public String toString() {
+        FlowAnalysisRule flowAnalysisRule = flowAnalysisRuleRef.get().getFlowAnalysisRule();
+        try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), flowAnalysisRule.getClass(), flowAnalysisRule.getIdentifier())) {
+            return getFlowAnalysisRule().toString();
+        }
+    }
+
+    @Override
+    public void enable() {
+        verifyCanEnable();
+        setState(FlowAnalysisRuleState.ENABLED, OnEnabled.class);
+    }
+
+    @Override
+    public void disable() {
+        verifyCanDisable();
+        setState(FlowAnalysisRuleState.DISABLED, OnDisabled.class);
+
+        ruleViolationsManager.removeRuleViolationsForRule(getIdentifier());
+        ruleViolationsManager.cleanUp();
+    }
+
+    private void setState(FlowAnalysisRuleState newState, Class<? extends Annotation> annotation) {
+        final ConfigurationContext configContext = new StandardConfigurationContext(this, this.serviceLookup, null, getVariableRegistry());
+
+        try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), getFlowAnalysisRule().getClass(), getIdentifier())) {
+            ReflectionUtils.invokeMethodsWithAnnotation(annotation, getFlowAnalysisRule(), configContext);
+
+            this.state = newState;
+
+            log.debug("Successfully {} {}", newState.toString().toLowerCase(), this);
+        } catch (Exception e) {
+            final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e;
+
+            final ComponentLog componentLog = new SimpleProcessLogger(getIdentifier(), getFlowAnalysisRule(), new StandardLoggingContext(null));
+
+            componentLog.error("Failed to invoke {} method", cause);
+
+            log.error("Failed to invoke {} method of {} due to {}", annotation.getSimpleName(), getFlowAnalysisRule(), cause.toString());
+        }
+    }
+
+    @Override
+    public void verifyCanPerformVerification() {
+        if (isEnabled()) {
+            throw new IllegalStateException("Cannot perform verification of " + this + " because Flow Analysis Rule is not fully stopped");
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfiguration(final ConfigurationContext context, final ComponentLog logger, final ExtensionManager extensionManager) {
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+
+        try {
+            verifyCanPerformVerification();
+
+            final long startNanos = System.nanoTime();
+            // Call super's verifyConfig, which will perform component validation
+            results.addAll(super.verifyConfig(context.getProperties(), context.getAnnotationData(), null));
+            final long validationComplete = System.nanoTime();
+
+            // If any invalid outcomes from validation, we do not want to perform additional verification, because we only run additional verification when the component is valid.
+            // This is done in order to make it much simpler to develop these verifications, since the developer doesn't have to worry about whether or not the given values are valid.
+            if (!results.isEmpty() && results.stream().anyMatch(result -> result.getOutcome() == ConfigVerificationResult.Outcome.FAILED)) {
+                return results;
+            }
+
+            final FlowAnalysisRule flowAnalysisRule = getFlowAnalysisRule();
+            if (flowAnalysisRule instanceof VerifiableFlowAnalysisRule) {
+                logger.debug("{} is a VerifiableFlowAnalysisRule. Will perform full verification of configuration.", this);
+                final VerifiableFlowAnalysisRule verifiable = (VerifiableFlowAnalysisRule) flowAnalysisRule;
+
+                // Check if the given configuration requires a different classloader than the current configuration
+                final boolean classpathDifferent = isClasspathDifferent(context.getProperties());
+
+                if (classpathDifferent) {
+                    // Create a classloader for the given configuration and use that to verify the component's configuration
+                    final Bundle bundle = extensionManager.getBundle(getBundleCoordinate());
+                    final Set<URL> classpathUrls = getAdditionalClasspathResources(context.getProperties().keySet(), descriptor -> context.getProperty(descriptor).getValue());
+
+                    final String classloaderIsolationKey = getClassLoaderIsolationKey(context);
+                    final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
+                    try (final InstanceClassLoader detectedClassLoader = extensionManager.createInstanceClassLoader(getComponentType(), getIdentifier(), bundle, classpathUrls, false,
+                                    classloaderIsolationKey)) {
+                        Thread.currentThread().setContextClassLoader(detectedClassLoader);
+                        results.addAll(verifiable.verify(context, logger));
+                    } finally {
+                        Thread.currentThread().setContextClassLoader(currentClassLoader);
+                    }
+                } else {
+                    // Verify the configuration, using the component's classloader
+                    try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, flowAnalysisRule.getClass(), getIdentifier())) {
+                        results.addAll(verifiable.verify(context, logger));
+                    }
+                }
+
+                final long validationNanos = validationComplete - startNanos;
+                final long verificationNanos = System.nanoTime() - validationComplete;
+                logger.debug("{} completed full configuration validation in {} plus {} for validation",
+                    this, FormatUtils.formatNanos(verificationNanos, false), FormatUtils.formatNanos(validationNanos, false));
+            } else {
+                logger.debug("{} is not a VerifiableFlowAnalysisRule, so will not perform full verification of configuration. Validation took {}", this,
+                    FormatUtils.formatNanos(validationComplete - startNanos, false));
+            }
+        } catch (final Throwable t) {
+            logger.error("Failed to perform verification of Flow Analysis Rule's configuration for {}", this, t);
+
+            results.add(new ConfigVerificationResult.Builder()
+                .outcome(ConfigVerificationResult.Outcome.FAILED)
+                .verificationStepName("Perform Verification")
+                .explanation("Encountered unexpected failure when attempting to perform verification: " + t)
+                .build());
+        }
+
+        return results;
+    }
+
+    public Optional<ProcessGroup> getParentProcessGroup() {
+        return Optional.empty();
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleDetails.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleDetails.java
new file mode 100644
index 0000000000..8bf4095181
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleDetails.java
@@ -0,0 +1,50 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.TerminationAwareLogger;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+
+/**
+ * Holder for StandardFlowAnalysisRuleNode to atomically swap out the component.
+ */
+class FlowAnalysisRuleDetails {
+
+    private final FlowAnalysisRule flowAnalysisRule;
+    private final TerminationAwareLogger componentLog;
+    private final BundleCoordinate bundleCoordinate;
+
+    public FlowAnalysisRuleDetails(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule) {
+        this.flowAnalysisRule = flowAnalysisRule.getComponent();
+        this.componentLog = flowAnalysisRule.getLogger();
+        this.bundleCoordinate = flowAnalysisRule.getBundleCoordinate();
+    }
+
+    public FlowAnalysisRule getFlowAnalysisRule() {
+        return flowAnalysisRule;
+    }
+
+    public TerminationAwareLogger getComponentLog() {
+        return componentLog;
+    }
+
+    public BundleCoordinate getBundleCoordinate() {
+        return bundleCoordinate;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisInitializationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisInitializationContext.java
new file mode 100644
index 0000000000..19e4226e16
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisInitializationContext.java
@@ -0,0 +1,110 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.controller.kerberos.KerberosConfig;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleInitializationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.io.File;
+import java.util.Set;
+
+public class StandardFlowAnalysisInitializationContext implements FlowAnalysisRuleInitializationContext, ControllerServiceLookup {
+    private final String id;
+    private final ComponentLog logger;
+    private final ControllerServiceProvider serviceProvider;
+    private final KerberosConfig kerberosConfig;
+    private final NodeTypeProvider nodeTypeProvider;
+
+    public StandardFlowAnalysisInitializationContext(
+            String id,
+            ComponentLog logger,
+            ControllerServiceProvider serviceProvider,
+            KerberosConfig kerberosConfig,
+            NodeTypeProvider nodeTypeProvider
+    ) {
+        this.id = id;
+        this.serviceProvider = serviceProvider;
+        this.logger = logger;
+        this.kerberosConfig = kerberosConfig;
+        this.nodeTypeProvider = nodeTypeProvider;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public Set<String> getControllerServiceIdentifiers(final Class<? extends ControllerService> serviceType) {
+        return serviceProvider.getControllerServiceIdentifiers(serviceType, null);
+    }
+
+    @Override
+    public ControllerService getControllerService(final String identifier) {
+        return serviceProvider.getControllerService(identifier);
+    }
+
+    @Override
+    public boolean isControllerServiceEnabled(final ControllerService service) {
+        return serviceProvider.isControllerServiceEnabled(service);
+    }
+
+    @Override
+    public boolean isControllerServiceEnabled(final String serviceIdentifier) {
+        return serviceProvider.isControllerServiceEnabled(serviceIdentifier);
+    }
+
+    @Override
+    public boolean isControllerServiceEnabling(final String serviceIdentifier) {
+        return serviceProvider.isControllerServiceEnabling(serviceIdentifier);
+    }
+
+    @Override
+    public String getControllerServiceName(final String serviceIdentifier) {
+        return serviceProvider.getControllerServiceName(serviceIdentifier);
+    }
+
+    @Override
+    public ComponentLog getLogger() {
+        return logger;
+    }
+
+    @Override
+    public String getKerberosServicePrincipal() {
+        return kerberosConfig.getPrincipal();
+    }
+
+    @Override
+    public File getKerberosServiceKeytab() {
+        return kerberosConfig.getKeytabLocation();
+    }
+
+    @Override
+    public File getKerberosConfigurationFile() {
+        return kerberosConfig.getConfigFile();
+    }
+
+    @Override
+    public NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index 0d3af3aa12..a304451d07 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -833,4 +833,10 @@ public class StandardControllerServiceNode extends AbstractComponentNode impleme
         }
     }
 
+
+    @Override
+    protected void performFlowAnalysisOnThis() {
+        Optional.ofNullable(getValidationContextFactory().getFlowAnalyzer())
+            .ifPresent(flowAnalyzer -> flowAnalyzer.analyzeControllerService(this));
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
index 8ca995da9e..c6420e4ecf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
@@ -19,6 +19,7 @@ package org.apache.nifi.controller.service;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.controller.ComponentNode;
 import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
@@ -113,6 +114,7 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
         // or a service that references this controller service, etc.
         final List<ProcessorNode> processors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class);
         final List<ReportingTaskNode> reportingTasks = serviceNode.getReferences().findRecursiveReferences(ReportingTaskNode.class);
+        final List<FlowAnalysisRuleNode> flowAnalysisRuleNodes = serviceNode.getReferences().findRecursiveReferences(FlowAnalysisRuleNode.class);
 
         // verify that  we can start all components (that are not disabled) before doing anything
         for (final ProcessorNode node : processors) {
@@ -133,6 +135,15 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
                 node.verifyCanStart();
             }
         }
+        for (final FlowAnalysisRuleNode node : flowAnalysisRuleNodes) {
+            if (candidates != null && !candidates.contains(node)) {
+                continue;
+            }
+
+            if (!node.isEnabled()) {
+                node.verifyCanEnable();
+            }
+        }
 
         // start all of the components that are not disabled
         final Set<ComponentNode> updated = new HashSet<>();
@@ -156,6 +167,16 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
                 updated.add(node);
             }
         }
+        for (final FlowAnalysisRuleNode node : flowAnalysisRuleNodes) {
+            if (candidates != null && !candidates.contains(node)) {
+                continue;
+            }
+
+            if (!node.isEnabled()) {
+                node.enable();
+                updated.add(node);
+            }
+        }
 
         return updated;
     }
@@ -166,6 +187,7 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
         // or a service that references this controller service, etc.
         final List<ProcessorNode> processors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class);
         final List<ReportingTaskNode> reportingTasks = serviceNode.getReferences().findRecursiveReferences(ReportingTaskNode.class);
+        final List<FlowAnalysisRuleNode> flowAnalysisRuleNodes = serviceNode.getReferences().findRecursiveReferences(FlowAnalysisRuleNode.class);
 
         final Map<ComponentNode, Future<Void>> updated = new HashMap<>();
 
@@ -180,6 +202,11 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
                 node.verifyCanStop();
             }
         }
+        for (final FlowAnalysisRuleNode node : flowAnalysisRuleNodes) {
+            if (node.isEnabled()) {
+                node.verifyCanDisable();
+            }
+        }
 
         // stop all of the components that are running
         for (final ProcessorNode node : processors) {
@@ -194,6 +221,20 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
                 updated.put(node, future);
             }
         }
+        for (final FlowAnalysisRuleNode node : flowAnalysisRuleNodes) {
+            if (node.isEnabled()) {
+                final CompletableFuture<Void> future = new CompletableFuture<>();
+                processScheduler.submitFrameworkTask(() -> {
+                    try {
+                        node.disable();
+                        future.complete(null);
+                    } catch (final Exception e) {
+                        future.completeExceptionally(e);
+                    }
+                });
+                updated.put(node, future);
+            }
+        }
 
         return updated;
     }
@@ -504,16 +545,19 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
             if (serviceNode == null) {
                 final ReportingTaskNode taskNode = flowManager.getReportingTaskNode(componentId);
                 if (taskNode == null) {
-                    final ParameterProviderNode parameterProviderNode = flowManager.getParameterProvider(componentId);
-                    if (parameterProviderNode == null) {
-                        final FlowRegistryClientNode flowRegistryClientNode = flowManager.getFlowRegistryClient(componentId);
-                        if (flowRegistryClientNode == null) {
-                            throw new IllegalStateException("Could not find any Processor, Reporting Task, Parameter Provider, or Controller Service with identifier " + componentId);
+                    final FlowAnalysisRuleNode flowAnalysisRuleNode = flowManager.getFlowAnalysisRuleNode(componentId);
+                    if (flowAnalysisRuleNode == null) {
+                        final ParameterProviderNode parameterProviderNode = flowManager.getParameterProvider(componentId);
+                        if (parameterProviderNode == null) {
+                            final FlowRegistryClientNode flowRegistryClientNode = flowManager.getFlowRegistryClient(componentId);
+                            if (flowRegistryClientNode == null) {
+                                throw new IllegalStateException("Could not find any Processor, Reporting Task, Parameter Provider, or Controller Service with identifier " + componentId);
+                            }
                         }
                     }
                 }
 
-                // We have confirmed that the component is a reporting task or parameter provider. We can only reference Controller Services
+                // We have confirmed that the component is a reporting task or a flow analysis rule or parameter provider. We can only reference Controller Services
                 // that are scoped at the FlowController level in this case.
                 final ControllerServiceNode rootServiceNode = flowManager.getRootControllerService(serviceIdentifier);
                 return (rootServiceNode == null) ? null : rootServiceNode.getProxiedControllerService();
@@ -638,6 +682,8 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
         LogRepositoryFactory.removeRepository(serviceNode.getIdentifier());
         extensionManager.removeInstanceClassLoader(serviceNode.getIdentifier());
         serviceCache.remove(serviceNode.getIdentifier());
+
+        flowManager.getRuleViolationsManager().removeRuleViolationsForSubject(serviceNode.getIdentifier());
     }
 
     @Override
@@ -695,6 +741,7 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
     public void verifyCanScheduleReferencingComponents(final ControllerServiceNode serviceNode) {
         final List<ControllerServiceNode> referencingServices = serviceNode.getReferences().findRecursiveReferences(ControllerServiceNode.class);
         final List<ReportingTaskNode> referencingReportingTasks = serviceNode.getReferences().findRecursiveReferences(ReportingTaskNode.class);
+        final List<FlowAnalysisRuleNode> referencingFlowAnalysisRuleNodes = serviceNode.getReferences().findRecursiveReferences(FlowAnalysisRuleNode.class);
         final List<ProcessorNode> referencingProcessors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class);
 
         final Set<ControllerServiceNode> referencingServiceSet = new HashSet<>(referencingServices);
@@ -705,6 +752,10 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
             }
         }
 
+        for (final FlowAnalysisRuleNode ruleNode : referencingFlowAnalysisRuleNodes) {
+            ruleNode.verifyCanEnable(referencingServiceSet);
+        }
+
         for (final ProcessorNode procNode : referencingProcessors) {
             if (procNode.getScheduledState() != ScheduledState.DISABLED) {
                 procNode.verifyCanStart();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
index 3ed19ce5f0..8d20da5879 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
@@ -42,6 +42,7 @@ public class VolatileBulletinRepository implements BulletinRepository {
     private static final String CONTROLLER_BULLETIN_STORE_KEY = "CONTROLLER";
     private static final String SERVICE_BULLETIN_STORE_KEY = "SERVICE";
     private static final String REPORTING_TASK_BULLETIN_STORE_KEY = "REPORTING_TASK";
+    private static final String FLOW_ANALYSIS_RULE_BULLETIN_STORE_KEY = "FLOW_ANALYSIS_RULE";
     private static final String FLOW_REGISTRY_CLIENT_STORE_KEY = "FLOW_REGISTRY_CLIENT";
     private static final String PARAMETER_PROVIDER_BULLETIN_STORE_KEY = "PARAMETER_PROVIDER";
 
@@ -301,6 +302,8 @@ public class VolatileBulletinRepository implements BulletinRepository {
                 return SERVICE_BULLETIN_STORE_KEY;
             case REPORTING_TASK:
                 return REPORTING_TASK_BULLETIN_STORE_KEY;
+            case FLOW_ANALYSIS_RULE:
+                return FLOW_ANALYSIS_RULE_BULLETIN_STORE_KEY;
             case PARAMETER_PROVIDER:
                 return PARAMETER_PROVIDER_BULLETIN_STORE_KEY;
             case FLOW_REGISTRY_CLIENT:
@@ -315,6 +318,7 @@ public class VolatileBulletinRepository implements BulletinRepository {
             case FLOW_CONTROLLER:
             case CONTROLLER_SERVICE:
             case REPORTING_TASK:
+            case FLOW_ANALYSIS_RULE:
             case PARAMETER_PROVIDER:
             case FLOW_REGISTRY_CLIENT:
                 return true;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
index 8871ea219c..45e2a414b8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
@@ -29,6 +29,7 @@ import org.apache.nifi.connectable.Position;
 import org.apache.nifi.connectable.Size;
 import org.apache.nifi.controller.BackoffMechanism;
 import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.PropertyConfiguration;
@@ -37,6 +38,7 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.Template;
 import org.apache.nifi.controller.Triggerable;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
 import org.apache.nifi.controller.label.Label;
 import org.apache.nifi.controller.queue.FlowFileQueue;
 import org.apache.nifi.controller.queue.LoadBalanceCompression;
@@ -57,6 +59,7 @@ import org.apache.nifi.flow.VersionedComponent;
 import org.apache.nifi.flow.VersionedConnection;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFlowCoordinates;
 import org.apache.nifi.flow.VersionedFunnel;
 import org.apache.nifi.flow.VersionedLabel;
@@ -3665,6 +3668,82 @@ public class StandardVersionedComponentSynchronizer implements VersionedComponen
         }
     }
 
+    @Override
+    public void synchronize(final FlowAnalysisRuleNode flowAnalysisRule, final VersionedFlowAnalysisRule proposed, final FlowSynchronizationOptions synchronizationOptions)
+            throws FlowSynchronizationException, TimeoutException, InterruptedException, FlowAnalysisRuleInstantiationException {
+
+        if (flowAnalysisRule == null && proposed == null) {
+            return;
+        }
+
+        synchronizationOptions.getComponentScheduler().pause();
+        try {
+            // If flow analysis rule is not null, make sure that it's disabled.
+            if (flowAnalysisRule != null && flowAnalysisRule.isEnabled()) {
+                flowAnalysisRule.disable();
+            }
+
+            if (proposed == null) {
+                flowAnalysisRule.verifyCanDelete();
+                context.getFlowManager().removeFlowAnalysisRule(flowAnalysisRule);
+                LOG.info("Successfully synchronized {} by removing it from the flow", flowAnalysisRule);
+            } else if (flowAnalysisRule == null) {
+                final FlowAnalysisRuleNode added = addFlowAnalysisRule(proposed);
+                LOG.info("Successfully synchronized {} by adding it to the flow", added);
+            } else {
+                updateFlowAnalysisRule(flowAnalysisRule, proposed);
+                LOG.info("Successfully synchronized {} by updating it to match proposed version", flowAnalysisRule);
+            }
+        } finally {
+            synchronizationOptions.getComponentScheduler().resume();
+        }
+    }
+
+    private FlowAnalysisRuleNode addFlowAnalysisRule(final VersionedFlowAnalysisRule flowAnalysisRule) throws FlowAnalysisRuleInstantiationException {
+        final BundleCoordinate coordinate = toCoordinate(flowAnalysisRule.getBundle());
+        final FlowAnalysisRuleNode ruleNode = context.getFlowManager().createFlowAnalysisRule(flowAnalysisRule.getType(), flowAnalysisRule.getInstanceIdentifier(), coordinate, false);
+        updateFlowAnalysisRule(ruleNode, flowAnalysisRule);
+        return ruleNode;
+    }
+
+    private void updateFlowAnalysisRule(final FlowAnalysisRuleNode flowAnalysisRule, final VersionedFlowAnalysisRule proposed)
+            throws FlowAnalysisRuleInstantiationException {
+        LOG.debug("Updating Flow Analysis Rule {}", flowAnalysisRule);
+
+        flowAnalysisRule.pauseValidationTrigger();
+        try {
+            flowAnalysisRule.setName(proposed.getName());
+            flowAnalysisRule.setComments(proposed.getComments());
+            flowAnalysisRule.setEnforcementPolicy(proposed.getEnforcementPolicy());
+
+            if (!isEqual(flowAnalysisRule.getBundleCoordinate(), proposed.getBundle())) {
+                final BundleCoordinate newBundleCoordinate = toCoordinate(proposed.getBundle());
+                final List<PropertyDescriptor> descriptors = new ArrayList<>(flowAnalysisRule.getProperties().keySet());
+                final Set<URL> additionalUrls = flowAnalysisRule.getAdditionalClasspathResources(descriptors);
+                context.getReloadComponent().reload(flowAnalysisRule, proposed.getType(), newBundleCoordinate, additionalUrls);
+            }
+
+            final Set<String> sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(flowAnalysisRule, proposed.getProperties(), proposed.getPropertyDescriptors().values());
+            flowAnalysisRule.setProperties(proposed.getProperties(), false, sensitiveDynamicPropertyNames);
+
+            switch (proposed.getScheduledState()) {
+                case DISABLED:
+                    if (flowAnalysisRule.isEnabled()) {
+                        flowAnalysisRule.disable();
+                    }
+                    break;
+                case ENABLED:
+                    if (!flowAnalysisRule.isEnabled()) {
+                        flowAnalysisRule.enable();
+                    }
+                    break;
+            }
+            notifyScheduledStateChange(flowAnalysisRule, syncOptions, proposed.getScheduledState());
+        } finally {
+            flowAnalysisRule.resumeValidationTrigger();
+        }
+    }
+
     private <T extends org.apache.nifi.components.VersionedComponent & Connectable> boolean matchesId(final T component, final String id) {
         return id.equals(component.getIdentifier()) || id.equals(component.getVersionedComponentId().orElse(NiFiRegistryFlowMapper.generateVersionedComponentId(component.getIdentifier())));
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/VersionedComponentSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/VersionedComponentSynchronizer.java
index dbe7f0c6f7..b2eee71bb8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/VersionedComponentSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/VersionedComponentSynchronizer.java
@@ -20,15 +20,18 @@ package org.apache.nifi.flow.synchronization;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
 import org.apache.nifi.controller.label.Label;
 import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.flow.VersionedConnection;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFunnel;
 import org.apache.nifi.flow.VersionedLabel;
 import org.apache.nifi.flow.VersionedParameterContext;
@@ -174,6 +177,20 @@ public interface VersionedComponentSynchronizer {
     void synchronize(ReportingTaskNode reportingTask, VersionedReportingTask proposed, FlowSynchronizationOptions synchronizationOptions)
             throws FlowSynchronizationException, TimeoutException, InterruptedException, ReportingTaskInstantiationException;
 
+    /**
+     * Synchronizes the given Flow Analysis Rule to match the proposed one, or deletes the Flow Analysis Rule if the proposed is <code>null</code>.
+     * If the given Flow Analysis Rule is <code>null</code>, creates it.
+     *
+     * @param flowAnalysisRule the flow analysis rule to synchronize
+     * @param proposed the proposed/desired state
+     * @param synchronizationOptions options for how to synchronize the flow
+     *
+     * @throws IllegalStateException if the flow analysis rule cannot be updated due to the state of the flow
+     * @throws FlowSynchronizationException if unable to synchronize the flow analysis rule with the proposed version
+     * @throws TimeoutException if the flow analysis rule is being removed and takes longer to stop than the timeout allowed by the {@link FlowSynchronizationOptions synchronization options}.
+     */
+    void synchronize(FlowAnalysisRuleNode flowAnalysisRule, VersionedFlowAnalysisRule proposed, FlowSynchronizationOptions synchronizationOptions)
+            throws FlowSynchronizationException, TimeoutException, InterruptedException, FlowAnalysisRuleInstantiationException;
 
     /**
      * Synchronizes the given Remote Process Group to match the proposed one, or deletes the rpg if the proposed is <code>null</code>. If the given rpg is <code>null</code>, creates it and adds
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardValidationContextFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardValidationContextFactory.java
index 2299165290..62017fbf09 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardValidationContextFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardValidationContextFactory.java
@@ -20,9 +20,11 @@ import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.controller.PropertyConfiguration;
 import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.parameter.ParameterContext;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.validation.RuleViolationsManager;
 
 import java.util.Map;
 
@@ -30,10 +32,26 @@ public class StandardValidationContextFactory implements ValidationContextFactor
 
     private final ControllerServiceProvider serviceProvider;
     private final VariableRegistry variableRegistry;
+    private final RuleViolationsManager ruleViolationsManager;
+    private final FlowAnalyzer flowAnalyzer;
 
-    public StandardValidationContextFactory(final ControllerServiceProvider serviceProvider, final VariableRegistry variableRegistry) {
+    public StandardValidationContextFactory(
+        final ControllerServiceProvider serviceProvider,
+        final VariableRegistry variableRegistry
+    ) {
+        this(serviceProvider, variableRegistry, null, null);
+    }
+
+    public StandardValidationContextFactory(
+        final ControllerServiceProvider serviceProvider,
+        final VariableRegistry variableRegistry,
+        final RuleViolationsManager ruleViolationsManager,
+        final FlowAnalyzer flowAnalyzer
+    ) {
         this.serviceProvider = serviceProvider;
         this.variableRegistry = variableRegistry;
+        this.ruleViolationsManager = ruleViolationsManager;
+        this.flowAnalyzer = flowAnalyzer;
     }
 
     @Override
@@ -41,4 +59,14 @@ public class StandardValidationContextFactory implements ValidationContextFactor
                                                   final ParameterContext parameterContext, final boolean validateConnections) {
         return new StandardValidationContext(serviceProvider, properties, annotationData, groupId, componentId, variableRegistry, parameterContext, validateConnections);
     }
+
+    @Override
+    public RuleViolationsManager getRuleViolationsManager() {
+        return ruleViolationsManager;
+    }
+
+    @Override
+    public FlowAnalyzer getFlowAnalyzer() {
+        return flowAnalyzer;
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
index f6ae9fc1d7..882772e5ef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -29,6 +29,7 @@ import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.ComponentNode;
 import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.PropertyConfiguration;
@@ -52,6 +53,7 @@ import org.apache.nifi.flow.PortType;
 import org.apache.nifi.flow.Position;
 import org.apache.nifi.flow.VersionedConnection;
 import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFlowCoordinates;
 import org.apache.nifi.flow.VersionedFlowRegistryClient;
 import org.apache.nifi.flow.VersionedFunnel;
@@ -479,6 +481,26 @@ public class NiFiRegistryFlowMapper {
         return versionedTask;
     }
 
+    public VersionedFlowAnalysisRule mapFlowAnalysisRule(final FlowAnalysisRuleNode flowAnalysisRuleNode, final ControllerServiceProvider serviceProvider) {
+        final VersionedFlowAnalysisRule versionedRule = new VersionedFlowAnalysisRule();
+        versionedRule.setIdentifier(flowAnalysisRuleNode.getIdentifier());
+        if (flowMappingOptions.isMapInstanceIdentifiers()) {
+            versionedRule.setInstanceIdentifier(flowAnalysisRuleNode.getIdentifier());
+        }
+        versionedRule.setBundle(mapBundle(flowAnalysisRuleNode.getBundleCoordinate()));
+        versionedRule.setComments(flowAnalysisRuleNode.getComments());
+        versionedRule.setComponentType(ComponentType.FLOW_ANALYSIS_RULE);
+        versionedRule.setName(flowAnalysisRuleNode.getName());
+
+        versionedRule.setProperties(mapProperties(flowAnalysisRuleNode, serviceProvider));
+        versionedRule.setPropertyDescriptors(mapPropertyDescriptors(flowAnalysisRuleNode, serviceProvider, Collections.emptySet(), Collections.emptyMap()));
+        versionedRule.setEnforcementPolicy(flowAnalysisRuleNode.getEnforcementPolicy());
+        versionedRule.setType(flowAnalysisRuleNode.getCanonicalClassName());
+        versionedRule.setScheduledState(flowMappingOptions.getStateLookup().getState(flowAnalysisRuleNode));
+
+        return versionedRule;
+    }
+
     public VersionedParameterProvider mapParameterProvider(final ParameterProviderNode parameterProviderNode, final ControllerServiceProvider serviceProvider) {
         final VersionedParameterProvider versionedParameterProvider = new VersionedParameterProvider();
         versionedParameterProvider.setIdentifier(parameterProviderNode.getIdentifier());
@@ -572,7 +594,7 @@ public class NiFiRegistryFlowMapper {
         return mapped;
     }
 
-    private String encrypt(final String value) {
+    protected String encrypt(final String value) {
         if (value == null) {
             return null;
         }
@@ -619,6 +641,7 @@ public class NiFiRegistryFlowMapper {
             versionedDescriptor.setName(descriptor.getName());
             versionedDescriptor.setDisplayName(descriptor.getDisplayName());
             versionedDescriptor.setSensitive(descriptor.isSensitive());
+            versionedDescriptor.setDynamic(descriptor.isDynamic());
 
             final VersionedResourceDefinition versionedResourceDefinition = mapResourceDefinition(descriptor.getResourceDefinition());
             versionedDescriptor.setResourceDefinition(versionedResourceDefinition);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
index a810898d7b..684dd8e4cb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
@@ -17,6 +17,7 @@
 
 package org.apache.nifi.registry.flow.mapping;
 
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFlowRegistryClient;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedParameterProvider;
@@ -34,21 +35,30 @@ public class StandardComparableDataFlow implements ComparableDataFlow {
     private final VersionedProcessGroup contents;
     private final Set<VersionedControllerService> controllerLevelServices;
     private final Set<VersionedReportingTask> reportingTasks;
+    private final Set<VersionedFlowAnalysisRule> flowAnalysisRules;
     private final Set<VersionedParameterContext> parameterContexts;
     private final Set<VersionedParameterProvider> parameterProviders;
     private final Set<VersionedFlowRegistryClient> flowRegistryClients;
 
     public StandardComparableDataFlow(final String name, final VersionedProcessGroup contents) {
-        this(name, contents, Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
+        this(name, contents, Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
     }
 
-    public StandardComparableDataFlow(final String name, final VersionedProcessGroup contents, final Set<VersionedControllerService> controllerLevelServices,
-                                      final Set<VersionedReportingTask> reportingTasks, final Set<VersionedParameterContext> parameterContexts,
-                                      final Set<VersionedParameterProvider> parameterProviders, final Set<VersionedFlowRegistryClient> flowRegistryClients) {
+    public StandardComparableDataFlow(
+            final String name,
+            final VersionedProcessGroup contents,
+            final Set<VersionedControllerService> controllerLevelServices,
+            final Set<VersionedReportingTask> reportingTasks,
+            final Set<VersionedFlowAnalysisRule> flowAnalysisRules,
+            final Set<VersionedParameterContext> parameterContexts,
+            final Set<VersionedParameterProvider> parameterProviders,
+            final Set<VersionedFlowRegistryClient> flowRegistryClients
+    ) {
         this.name = name;
         this.contents = contents;
         this.controllerLevelServices = controllerLevelServices == null ? Collections.emptySet() : new HashSet<>(controllerLevelServices);
         this.reportingTasks = reportingTasks == null ? Collections.emptySet() : new HashSet<>(reportingTasks);
+        this.flowAnalysisRules = flowAnalysisRules == null ? Collections.emptySet() : new HashSet<>(flowAnalysisRules);
         this.parameterContexts = parameterContexts == null ? Collections.emptySet() : new HashSet<>(parameterContexts);
         this.parameterProviders = parameterProviders == null ? Collections.emptySet() : new HashSet<>(parameterProviders);
         this.flowRegistryClients = flowRegistryClients == null ? Collections.emptySet() : new HashSet<>(flowRegistryClients);
@@ -74,6 +84,11 @@ public class StandardComparableDataFlow implements ComparableDataFlow {
         return reportingTasks;
     }
 
+    @Override
+    public Set<VersionedFlowAnalysisRule> getFlowAnalysisRules() {
+        return flowAnalysisRules;
+    }
+
     @Override
     public Set<VersionedParameterContext> getParameterContexts() {
         return parameterContexts;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
index e9d060ce05..3094b5d058 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
@@ -43,6 +43,7 @@ import org.apache.nifi.controller.service.ControllerServiceDisabledException;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
 import org.apache.nifi.flow.ExecutionEngine;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.nar.ExtensionManager;
@@ -61,6 +62,8 @@ import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.util.CharacterFilterUtils;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
+import org.apache.nifi.validation.RuleViolation;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -758,6 +761,7 @@ public abstract class AbstractComponentNode implements ComponentNode {
 
     protected Collection<ValidationResult> computeValidationErrors(final ValidationContext validationContext) {
         Throwable failureCause = null;
+
         try {
             if (!sensitiveDynamicPropertyNames.get().isEmpty() && !isSupportsSensitiveDynamicProperties()) {
                 return Collections.singletonList(
@@ -786,6 +790,24 @@ public abstract class AbstractComponentNode implements ComponentNode {
             final Collection<ValidationResult> referencedServiceValidationResults = validateReferencedControllerServices(validationContext);
             validationResults.addAll(referencedServiceValidationResults);
 
+            performFlowAnalysisOnThis();
+
+            RuleViolationsManager ruleViolationsManager = getValidationContextFactory().getRuleViolationsManager();
+            if (ruleViolationsManager != null) {
+                Collection<RuleViolation> ruleViolations = ruleViolationsManager.getRuleViolationsForSubject(getIdentifier());
+                for (RuleViolation ruleViolation : ruleViolations) {
+                    if (ruleViolation.getEnforcementPolicy() == EnforcementPolicy.ENFORCE) {
+                        validationResults.add(
+                            new ValidationResult.Builder()
+                                .subject(getComponent().getClass().getSimpleName())
+                                .valid(false)
+                                .explanation(ruleViolation.getViolationMessage())
+                                .build()
+                        );
+                    }
+                }
+            }
+
             logger.debug("Computed validation errors with Validation Context {}; results = {}", validationContext, validationResults);
 
             return validationResults;
@@ -994,6 +1016,9 @@ public abstract class AbstractComponentNode implements ComponentNode {
         return null;
     }
 
+    protected void performFlowAnalysisOnThis() {
+    }
+
     private ValidationResult createInvalidResult(final String serviceId, final String propertyName, final String explanation) {
         return new ValidationResult.Builder()
             .input(serviceId)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/FlowAnalysisRuleNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/FlowAnalysisRuleNode.java
new file mode 100644
index 0000000000..302e21800b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/FlowAnalysisRuleNode.java
@@ -0,0 +1,91 @@
+/*
+ * 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.controller;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleState;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.nar.ExtensionManager;
+
+import java.util.List;
+import java.util.Set;
+
+public interface FlowAnalysisRuleNode extends ComponentNode {
+    FlowAnalysisRule getFlowAnalysisRule();
+
+    void setEnforcementPolicy(EnforcementPolicy enforcementPolicy);
+
+    /**
+     * @return the enforcement policy of the flow analysis rule
+     */
+    EnforcementPolicy getEnforcementPolicy();
+
+    void setFlowAnalysisRule(LoggableComponent<FlowAnalysisRule> flowAnalysisRule);
+
+    FlowAnalysisRuleContext getFlowAnalysisRuleContext();
+
+    ConfigurationContext getConfigurationContext();
+
+    /**
+     * @return the current state of the flow analysis rule
+     */
+    FlowAnalysisRuleState getState();
+
+    boolean isEnabled();
+
+    String getComments();
+
+    void setComments(String comment);
+
+    void verifyCanDisable();
+
+    void verifyCanEnable();
+
+    void verifyCanEnable(Set<ControllerServiceNode> ignoredServices);
+
+    void verifyCanDelete();
+
+    void verifyCanUpdate();
+
+    void verifyCanClearState();
+
+    /**
+     * Verifies that the Flow Analysis Rule is in a state in which it can verify a configuration by calling
+     * {@link #verifyConfiguration(ConfigurationContext, ComponentLog, ExtensionManager)}.
+     *
+     * @throws IllegalStateException if not in a state in which configuration can be verified
+     */
+    void verifyCanPerformVerification();
+
+    /**
+     * Verifies that the given configuration is valid for the Flow Analysis Rule
+     *
+     * @param context the configuration to verify
+     * @param logger a logger that can be used when performing verification
+     * @param extensionManager extension manager that is used for obtaining appropriate NAR ClassLoaders
+     * @return a list of results indicating whether or not the given configuration is valid
+     */
+    List<ConfigVerificationResult> verifyConfiguration(ConfigurationContext context, ComponentLog logger, ExtensionManager extensionManager);
+
+    void enable();
+
+    void disable();
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
index bd10e11382..32ce17b775 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
@@ -37,6 +37,7 @@ import org.apache.nifi.scheduling.SchedulingStrategy;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
@@ -284,6 +285,12 @@ public abstract class ProcessorNode extends AbstractComponentNode implements Con
      */
     public abstract ScheduledState getDesiredState();
 
+    @Override
+    protected void performFlowAnalysisOnThis() {
+        Optional.ofNullable(getValidationContextFactory().getFlowAnalyzer())
+            .ifPresent(flowAnalyzer -> flowAnalyzer.analyzeProcessor(this));
+    }
+
     /**
      * This method will be called once the processor's configuration has been restored (on startup, reload, e.g.)
      *
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ReloadComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ReloadComponent.java
index a1fe7a1322..bd86139115 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ReloadComponent.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ReloadComponent.java
@@ -19,6 +19,7 @@ package org.apache.nifi.controller;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.controller.exception.ControllerServiceInstantiationException;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
 import org.apache.nifi.controller.parameter.ParameterProviderInstantiationException;
 import org.apache.nifi.controller.flowrepository.FlowRepositoryClientInstantiationException;
 import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException;
@@ -69,6 +70,17 @@ public interface ReloadComponent {
     void reload(ReportingTaskNode existingNode, String newType, BundleCoordinate bundleCoordinate, Set<URL> additionalUrls)
             throws ReportingTaskInstantiationException;
 
+    /**
+     * Changes the underlying FlowAnalysisRule held by the node to an instance of the new type.
+     *
+     * @param existingNode the FlowAnalysisRuleNode being updated
+     * @param newType the fully qualified class name of the new type
+     * @param bundleCoordinate the bundle coordinate of the new type
+     * @param additionalUrls additional URLs to be added to the instance class loader of the new component
+     * @throws FlowAnalysisRuleInstantiationException if unable to create an instance of the new type
+     */
+    void reload(FlowAnalysisRuleNode existingNode, String newType, BundleCoordinate bundleCoordinate, Set<URL> additionalUrls)
+        throws FlowAnalysisRuleInstantiationException;
     /**
      * Changes the underlying ParameterProvider held by the node to an instance of the new type.
      *
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ValidationContextFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ValidationContextFactory.java
index bf1d060263..cbab247e13 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ValidationContextFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ValidationContextFactory.java
@@ -18,7 +18,9 @@ package org.apache.nifi.controller;
 
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.validation.RuleViolationsManager;
 
 import java.util.Map;
 
@@ -27,4 +29,7 @@ public interface ValidationContextFactory {
     ValidationContext newValidationContext(Map<PropertyDescriptor, PropertyConfiguration> properties, String annotationData, String groupId, String componentId, ParameterContext parameterContext,
                                            boolean validateConnections);
 
+    RuleViolationsManager getRuleViolationsManager();
+
+    FlowAnalyzer getFlowAnalyzer();
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java
index c33a80d228..e8ea95582d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java
@@ -21,10 +21,12 @@ import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.controller.label.Label;
 import org.apache.nifi.controller.parameter.ParameterProviderLookup;
 import org.apache.nifi.controller.service.ControllerServiceNode;
@@ -34,6 +36,7 @@ import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.parameter.Parameter;
 import org.apache.nifi.parameter.ParameterContext;
 import org.apache.nifi.parameter.ParameterContextManager;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.apache.nifi.parameter.ParameterProviderConfiguration;
 import org.apache.nifi.registry.flow.FlowRegistryClientNode;
 import org.apache.nifi.web.api.dto.FlowSnippetDTO;
@@ -392,7 +395,7 @@ public interface FlowManager extends ParameterProviderLookup {
 
     /**
      * @return the number of each type of component (Processor, Controller Service, Process Group, Funnel, Input Port, Output Port,
-     * Parameter Provider, Reporting Task, Remote Process Group)
+     * Parameter Provider, Reporting Task, Flow Analysis Rule, Remote Process Group)
      */
     Map<String, Integer> getComponentCounts();
 
@@ -403,10 +406,39 @@ public interface FlowManager extends ParameterProviderLookup {
      * Controller Services
      * Templates
      * Reporting Tasks
+     * Flow Analysis Rules
      * Parameter Contexts
      * Flow Registries
      *
      * @throws IllegalStateException if any of the components is not in a state that it can be deleted.
      */
     void purge();
+
+    // Flow Analysis
+    FlowAnalysisRuleNode createFlowAnalysisRule(
+        final String type,
+        final String id,
+        final BundleCoordinate bundleCoordinate,
+        final boolean firstTimeAdded
+    );
+
+    FlowAnalysisRuleNode createFlowAnalysisRule(
+        final String type,
+        final String id,
+        final BundleCoordinate bundleCoordinate,
+        final Set<URL> additionalUrls,
+        final boolean firstTimeAdded,
+        final boolean register,
+        final String classloaderIsolationKey
+    );
+
+    FlowAnalysisRuleNode getFlowAnalysisRuleNode(final String taskId);
+
+    void removeFlowAnalysisRule(final FlowAnalysisRuleNode reportingTask);
+
+    Set<FlowAnalysisRuleNode> getAllFlowAnalysisRules();
+
+    FlowAnalyzer getFlowAnalyzer();
+
+    RuleViolationsManager getRuleViolationsManager();
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/VersionedDataflow.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/VersionedDataflow.java
index 1f37fbc1c1..0f0031e337 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/VersionedDataflow.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/VersionedDataflow.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.controller.flow;
 
 import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFlowRegistryClient;
 import org.apache.nifi.flow.VersionedParameterProvider;
 import org.apache.nifi.flow.VersionedProcessGroup;
@@ -35,6 +36,7 @@ public class VersionedDataflow {
     private List<VersionedParameterProvider> parameterProviders;
     private List<VersionedControllerService> controllerServices;
     private List<VersionedReportingTask> reportingTasks;
+    private List<VersionedFlowAnalysisRule> flowAnalysisRules;
     private Set<VersionedTemplate> templates;
     private VersionedProcessGroup rootGroup;
 
@@ -86,6 +88,14 @@ public class VersionedDataflow {
         this.reportingTasks = reportingTasks;
     }
 
+    public List<VersionedFlowAnalysisRule> getFlowAnalysisRules() {
+        return flowAnalysisRules;
+    }
+
+    public void setFlowAnalysisRules(List<VersionedFlowAnalysisRule> flowAnalysisRules) {
+        this.flowAnalysisRules = flowAnalysisRules;
+    }
+
     public List<VersionedParameterProvider> getParameterProviders() {
         return parameterProviders;
     }
diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleInstantiationException.java
similarity index 66%
copy from nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleInstantiationException.java
index c0eda7e230..9918a46e46 100644
--- a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleInstantiationException.java
@@ -14,14 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.documentation;
+package org.apache.nifi.controller.flowanalysis;
 
-public enum ExtensionType {
-    PROCESSOR,
+public class FlowAnalysisRuleInstantiationException extends Exception {
 
-    CONTROLLER_SERVICE,
+    private static final long serialVersionUID = 1l;
 
-    REPORTING_TASK,
+    public FlowAnalysisRuleInstantiationException(final String className, final Throwable t) {
+        super(className, t);
+    }
+
+    public FlowAnalysisRuleInstantiationException(final String message) {
+        super(message);
+    }
 
-    PARAMETER_PROVIDER;
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleProvider.java
new file mode 100644
index 0000000000..4f2dd1e2d0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisRuleProvider.java
@@ -0,0 +1,99 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.nar.ExtensionManager;
+
+import java.util.Set;
+
+/**
+ * A FlowAnalysisRuleProvider is responsible for providing management of, and
+ * access to, Flow Analysis Rules
+ */
+public interface FlowAnalysisRuleProvider {
+
+    /**
+     * Creates a new instance of a flow analysis rule
+     *
+     * @param type the type (fully qualified class name) of the flow analysis rule
+     * to instantiate
+     * @param id the identifier for the Flow Analysis Rule
+     * @param bundleCoordinate the bundle coordinate for the type of flow analysis rule
+     * @param firstTimeAdded whether or not this is the first time that the
+     * flow analysis rule is being added to the flow. I.e., this will be true only
+     * when the user adds the flow analysis rule to the flow, not when the flow is
+     * being restored after a restart of the software
+     *
+     * @return the FlowAnalysisRuleNode that is used to manage the flow analysis rule
+     *
+     * @throws FlowAnalysisRuleInstantiationException if unable to create the
+     * Flow Analysis Rule
+     */
+    FlowAnalysisRuleNode createFlowAnalysisRule(String type, String id, BundleCoordinate bundleCoordinate, boolean firstTimeAdded) throws FlowAnalysisRuleInstantiationException;
+
+    /**
+     * @param identifier of node
+     * @return the flow analysis rule that has the given identifier, or
+     * <code>null</code> if no flow analysis rule exists with that ID
+     */
+    FlowAnalysisRuleNode getFlowAnalysisRuleNode(String identifier);
+
+    /**
+     * @return a Set of all Flow Analysis Rules that exist for this service
+     * provider
+     */
+    Set<FlowAnalysisRuleNode> getAllFlowAnalysisRules();
+
+    /**
+     * Removes the given flow analysis rule from the flow
+     *
+     * @param flowAnalysisRule
+     *
+     * @throws IllegalStateException if the flow analysis rule cannot be removed
+     * because it is not disabled, or if the flow analysis rule is not known in the
+     * flow
+     */
+    void removeFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule);
+
+    /**
+     * Enables the flow analysis rule
+     *
+     * @param flowAnalysisRule
+     *
+     * @throws IllegalStateException if the FlowAnalysisRule's state is not
+     * DISABLED
+     */
+    void enableFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule);
+
+    /**
+     * Disables the flow analysis rule
+     *
+     * @param flowAnalysisRule
+     *
+     * @throws IllegalStateException if the FlowAnalysisRule's state is not
+     * ENABLED
+     */
+    void disableFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule);
+
+    /**
+     * @return the ExtensionManager instance used by this provider
+     */
+    ExtensionManager getExtensionManager();
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalyzer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalyzer.java
new file mode 100644
index 0000000000..1c6be53371
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalyzer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+/**
+ * Analyzes components, parts or the entirety of the flow.
+ */
+public interface FlowAnalyzer {
+    /**
+     * Analyzes a processor
+     *
+     * @param processorNode the processor (as a node) to be analyzed
+     */
+    void analyzeProcessor(ProcessorNode processorNode);
+
+    /**
+     * Analyzes a controller service
+     *
+     * @param controllerServiceNode the controller service (as a node) to be analyzed
+     */
+    void analyzeControllerService(ControllerServiceNode controllerServiceNode);
+
+    /**
+     * Analyze the flow or a part of it
+     *
+     * @param processGroup The process group (as a {@link org.apache.nifi.flow.VersionedComponent VersionedComponent})
+     *                     representing (a part of) the flow to be analyzed. Recursive - all child process groups will
+     *                     be analyzed as well.
+     */
+    void analyzeProcessGroup(VersionedProcessGroup processGroup);
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/ComponentIdLookup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/ComponentIdLookup.java
index a8039ec2b6..a8bfde92ad 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/ComponentIdLookup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/ComponentIdLookup.java
@@ -20,8 +20,10 @@ package org.apache.nifi.registry.flow.mapping;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.function.Function;
 
 public interface ComponentIdLookup {
+    Function<String, String> DEFAULT_VERSIONED_UUID_GENERATOR = componentId -> UUID.nameUUIDFromBytes(componentId.getBytes(StandardCharsets.UTF_8)).toString();
 
     /**
      * Given a component identifier and an optional Versioned Component ID, returns the identifier to use for the component
@@ -29,20 +31,27 @@ public interface ComponentIdLookup {
      * @param componentId the ID of the component
      * @return the ID to use for mapping a component to a Versioned Component
      */
-    String getComponentId(Optional<String> currentVersionedId, String componentId);
+    default String getComponentId(Optional<String> currentVersionedId, String componentId) {
+        return getComponentId(currentVersionedId, componentId, DEFAULT_VERSIONED_UUID_GENERATOR);
+    }
 
+    String getComponentId(Optional<String> currentVersionedId, String componentId, Function<String, String> versionedUuidGenerator);
 
     /**
      * Uses the Versioned Component ID, if it is present, or else generates a new Versioned Component ID based on the Component ID
      */
     ComponentIdLookup VERSIONED_OR_GENERATE = new ComponentIdLookup() {
         @Override
-        public String getComponentId(final Optional<String> currentVersionedId, final String componentId) {
+        public String getComponentId(
+                final Optional<String> currentVersionedId,
+                final String componentId,
+                final Function<String, String> versionedUuidGenerator
+        ) {
             if (currentVersionedId.isPresent()) {
                 return currentVersionedId.get();
             }
 
-            return UUID.nameUUIDFromBytes(componentId.getBytes(StandardCharsets.UTF_8)).toString();
+            return versionedUuidGenerator.apply(componentId);
         }
     };
 
@@ -51,5 +60,5 @@ public interface ComponentIdLookup {
     /**
      * Always uses the Component ID
      */
-    ComponentIdLookup USE_COMPONENT_ID = (versioned, componentId) -> componentId;
+    ComponentIdLookup USE_COMPONENT_ID = (versioned, componentId, versionedUuidGenerator) -> componentId;
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentStateLookup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentStateLookup.java
index f66871425d..91d7fc7d48 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentStateLookup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentStateLookup.java
@@ -18,10 +18,12 @@
 package org.apache.nifi.registry.flow.mapping;
 
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleState;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.StatelessGroupScheduledState;
 
@@ -32,6 +34,8 @@ public interface VersionedComponentStateLookup {
 
     ScheduledState getState(ReportingTaskNode taskNode);
 
+    ScheduledState getState(FlowAnalysisRuleNode ruleNode);
+
     ScheduledState getState(ControllerServiceNode serviceNode);
 
     ScheduledState getState(ProcessGroup processGroup);
@@ -56,6 +60,11 @@ public interface VersionedComponentStateLookup {
             return taskNode.getScheduledState() == org.apache.nifi.controller.ScheduledState.DISABLED ? ScheduledState.DISABLED : ScheduledState.ENABLED;
         }
 
+        @Override
+        public ScheduledState getState(final FlowAnalysisRuleNode ruleNode) {
+            return ruleNode.getState() == FlowAnalysisRuleState.DISABLED ? ScheduledState.DISABLED : ScheduledState.ENABLED;
+        }
+
         @Override
         public ScheduledState getState(final ControllerServiceNode serviceNode) {
             return ScheduledState.DISABLED;
@@ -86,6 +95,17 @@ public interface VersionedComponentStateLookup {
             return map(taskNode.getScheduledState());
         }
 
+        @Override
+        public ScheduledState getState(final FlowAnalysisRuleNode ruleNode) {
+            switch (ruleNode.getState()) {
+                case DISABLED:
+                    return ScheduledState.DISABLED;
+                case ENABLED:
+                default:
+                    return ScheduledState.ENABLED;
+            }
+        }
+
         @Override
         public ScheduledState getState(final ControllerServiceNode serviceNode) {
             switch (serviceNode.getState()) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolation.java
new file mode 100644
index 0000000000..d09cbd7b79
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolation.java
@@ -0,0 +1,193 @@
+/*
+ * 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.validation;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
+
+import java.util.StringJoiner;
+
+/**
+ * A result of a rule violation produced during a flow analysis.
+ * Violations produced by previous analysis runs may be overridden by new ones.
+ * A violation is identified by the scope, subjectId, ruleId and issueId properties.
+ */
+public class RuleViolation {
+    private final EnforcementPolicy enforcementPolicy;
+    private final String scope;
+    private final String subjectId;
+    private final String subjectDisplayName;
+    private final String groupId;
+    private final String ruleId;
+    private final String issueId;
+    private final String violationMessage;
+    private final String violationExplanation;
+
+    private boolean available;
+
+    public RuleViolation(
+            EnforcementPolicy enforcementPolicy,
+            String scope,
+            String subjectId,
+            String subjectDisplayName,
+            String groupId,
+            String ruleId,
+            String issueId,
+            String violationMessage,
+            String violationExplanation
+    ) {
+        this.enforcementPolicy = enforcementPolicy;
+        this.scope = scope;
+        this.subjectId = subjectId;
+        this.subjectDisplayName = subjectDisplayName;
+        this.groupId = groupId;
+        this.ruleId = ruleId;
+        this.issueId = issueId;
+        this.violationMessage = violationMessage;
+        this.violationExplanation = violationExplanation;
+        this.available = true;
+    }
+
+    /**
+     * @return the type of the rule that produced this violation
+     */
+    public EnforcementPolicy getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    /**
+     * @return the scope of the analysis that produced this violation. The id of the component or the process group that was
+     * the subject of the analysis.
+     */
+    public String getScope() {
+        return scope;
+    }
+
+    /**
+     * @return the id of the subject that violated the rule (component or a process group).
+     */
+    public String getSubjectId() {
+        return subjectId;
+    }
+
+    /**
+     * @return the displayed name of the subject that violated the rule
+     */
+    public String getSubjectDisplayName() {
+        return subjectDisplayName;
+    }
+
+    /**
+     * @return group id - if this violation is a result of a component analysis, then the id of the group of the component.
+     * If this violation is a result of a group analysis, then the id of that group itself.
+     */
+    public String getGroupId() {
+        return groupId;
+    }
+
+    /**
+     * @return a rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     */
+    public String getIssueId() {
+        return issueId;
+    }
+
+    /**
+     * @return the id of the rule that produced this violation
+     */
+    public String getRuleId() {
+        return ruleId;
+    }
+
+    /**
+     * @return the violation message
+     */
+    public String getViolationMessage() {
+        return violationMessage;
+    }
+
+    /**
+     * @return a detailed explanation of the nature of the violation
+     */
+    public String getViolationExplanation() {
+        return violationExplanation;
+    }
+
+    /**
+     * Used by the framework internally to manage lifecycle
+     */
+    public boolean isAvailable() {
+        return available;
+    }
+
+    public void setAvailable(boolean available) {
+        this.available = available;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(",\n\t", RuleViolation.class.getSimpleName() + "[\n\t", "\n]")
+            .add("enforcementPolicy=" + enforcementPolicy)
+            .add("scope='" + scope + "'")
+            .add("subjectId='" + subjectId + "'")
+            .add("subjectDisplayName='" + subjectDisplayName + "'")
+            .add("groupId='" + groupId + "'")
+            .add("issueId='" + issueId + "'")
+            .add("ruleId='" + ruleId + "'")
+            .add("violationMessage='" + violationMessage + "'")
+            .add("violationExplanation='" + violationExplanation + "'")
+            .add("available=" + available)
+            .toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+
+        if (o == null || getClass() != o.getClass()) return false;
+
+        RuleViolation that = (RuleViolation) o;
+
+        return new EqualsBuilder()
+            .append(enforcementPolicy, that.enforcementPolicy)
+            .append(scope, that.scope)
+            .append(subjectId, that.subjectId)
+            .append(groupId, that.groupId)
+            .append(issueId, that.issueId)
+            .append(ruleId, that.ruleId)
+            .append(violationMessage, that.violationMessage)
+            .append(violationExplanation, that.violationExplanation)
+            .append(available, that.available)
+            .isEquals();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder(17, 37)
+            .append(enforcementPolicy)
+            .append(scope)
+            .append(subjectId)
+            .append(groupId)
+            .append(issueId)
+            .append(ruleId)
+            .append(violationMessage)
+            .append(violationExplanation)
+            .append(available)
+            .toHashCode();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolationsManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolationsManager.java
new file mode 100644
index 0000000000..9b7047cc9c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/validation/RuleViolationsManager.java
@@ -0,0 +1,84 @@
+/*
+ * 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.validation;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Manages {@link RuleViolation}s produced during flow analysis
+ */
+public interface RuleViolationsManager {
+    /**
+     * Add or update rule violations created during the analysis of a component
+     *
+     * @param subjectId  The id of the component that was analyzed
+     * @param violations The violations to be added or updated
+     */
+    void upsertComponentViolations(String subjectId, Collection<RuleViolation> violations);
+
+    /**
+     * Add or update rule violations created during the analysis of a process group
+     *
+     * @param processGroup              The process group that was analyzed
+     * @param violations                Violations to be added that scoped to a process group (the one that was analyzed or one of it's children)
+     * @param componentToRuleViolations Violations to be added scoped to components under the analyzed process group (or one of it's children)
+     */
+    void upsertGroupViolations(VersionedProcessGroup processGroup, Collection<RuleViolation> violations, Map<VersionedComponent, Collection<RuleViolation>> componentToRuleViolations);
+
+    /**
+     * Returns rule violations tied to a component or process group with a given id
+     *
+     * @param subjectId The id of the component or process group
+     * @return Violations tied to a component or process group with the given subjectId
+     */
+    Collection<RuleViolation> getRuleViolationsForSubject(String subjectId);
+
+    /**
+     * Returns a list of violations with the given groupId (non-recursive)
+     *
+     * @return Violations with the given groupId
+     */
+    Collection<RuleViolation> getRuleViolationsForGroup(String groupId);
+
+    /**
+     * @return All current rule violations
+     */
+    Collection<RuleViolation> getAllRuleViolations();
+
+    /**
+     * Remove all rule violations tied to a component or process group with a given id
+     *
+     * @param subjectId The id of the component or process group
+     */
+    void removeRuleViolationsForSubject(String subjectId);
+
+    /**
+     * Remove all rule violations produced by the rule with a given id
+     *
+     * @param ruleId The id of the rule
+     */
+    void removeRuleViolationsForRule(String ruleId);
+
+    /**
+     * Removes empty entries from the map storing the rule violations
+     */
+    void cleanUp();
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java
index 63bba9769d..69c32bf746 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java
@@ -46,6 +46,10 @@ public class TriggerValidationTask implements Runnable {
                 validationTrigger.trigger(node);
             }
 
+            for (final ComponentNode node : flowManager.getAllFlowAnalysisRules()) {
+                validationTrigger.trigger(node);
+            }
+
             for (final ComponentNode node : flowManager.getAllParameterProviders()) {
                 validationTrigger.trigger(node);
             }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
index e4ad7d49b9..af2400372a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
@@ -29,6 +29,10 @@ import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.components.state.StateManagerProvider;
 import org.apache.nifi.components.validation.ValidationTrigger;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
+import org.apache.nifi.controller.flowanalysis.StandardFlowAnalysisInitializationContext;
+import org.apache.nifi.controller.flowanalysis.StandardFlowAnalysisRuleNode;
 import org.apache.nifi.controller.flowrepository.FlowRepositoryClientInstantiationException;
 import org.apache.nifi.controller.kerberos.KerberosConfig;
 import org.apache.nifi.controller.parameter.ParameterProviderInstantiationException;
@@ -43,6 +47,9 @@ import org.apache.nifi.controller.service.GhostControllerService;
 import org.apache.nifi.controller.service.StandardControllerServiceInitializationContext;
 import org.apache.nifi.controller.service.StandardControllerServiceInvocationHandler;
 import org.apache.nifi.controller.service.StandardControllerServiceNode;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleInitializationContext;
+import org.apache.nifi.flowanalysis.GhostFlowAnalysisRule;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.logging.LoggingContext;
 import org.apache.nifi.logging.StandardLoggingContext;
@@ -76,6 +83,7 @@ import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.reporting.ReportingInitializationContext;
 import org.apache.nifi.reporting.ReportingTask;
 import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -106,6 +114,8 @@ public class ExtensionBuilder {
     private ReloadComponent reloadComponent;
     private FlowController flowController;
     private StateManagerProvider stateManagerProvider;
+    private RuleViolationsManager ruleViolationsManager;
+    private FlowAnalyzer flowAnalyzer;
     private String classloaderIsolationKey;
     private SSLContext systemSslContext;
     private PythonBridge pythonBridge;
@@ -183,6 +193,16 @@ public class ExtensionBuilder {
         return this;
     }
 
+    public ExtensionBuilder ruleViolationsManager(RuleViolationsManager ruleViolationsManager) {
+        this.ruleViolationsManager = ruleViolationsManager;
+        return this;
+    }
+
+    public ExtensionBuilder flowAnalyzer(FlowAnalyzer flowAnalyzer) {
+        this.flowAnalyzer = flowAnalyzer;
+        return this;
+    }
+
     public ExtensionBuilder extensionManager(final ExtensionManager extensionManager) {
         this.extensionManager = extensionManager;
         return this;
@@ -436,10 +456,57 @@ public class ExtensionBuilder {
         }
     }
 
+    public FlowAnalysisRuleNode buildFlowAnalysisRuleNode() {
+        if (identifier == null) {
+            throw new IllegalStateException("FlowAnalysisRule ID must be specified");
+        }
+        if (type == null) {
+            throw new IllegalStateException("FlowAnalysisRule Type must be specified");
+        }
+        if (bundleCoordinate == null) {
+            throw new IllegalStateException("Bundle Coordinate must be specified");
+        }
+        if (extensionManager == null) {
+            throw new IllegalStateException("Extension Manager must be specified");
+        }
+        if (serviceProvider == null) {
+            throw new IllegalStateException("Controller Service Provider must be specified");
+        }
+        if (nodeTypeProvider == null) {
+            throw new IllegalStateException("Node Type Provider must be specified");
+        }
+        if (variableRegistry == null) {
+            throw new IllegalStateException("Variable Registry must be specified");
+        }
+        if (reloadComponent == null) {
+            throw new IllegalStateException("Reload Component must be specified");
+        }
+        if (flowController == null) {
+            throw new IllegalStateException("FlowController must be specified");
+        }
+
+        boolean creationSuccessful = true;
+        LoggableComponent<FlowAnalysisRule> loggableComponent;
+        try {
+            loggableComponent = createLoggableFlowAnalysisRule();
+        } catch (final FlowAnalysisRuleInstantiationException rtie) {
+            logger.error("Could not create FlowAnalysisRule of type {} for ID {}; creating \"Ghost\" implementation", type, identifier, rtie);
+            final GhostFlowAnalysisRule ghostFlowAnalysisRule = new GhostFlowAnalysisRule();
+            ghostFlowAnalysisRule.setIdentifier(identifier);
+            ghostFlowAnalysisRule.setCanonicalClassName(type);
+            loggableComponent = new LoggableComponent<>(ghostFlowAnalysisRule, bundleCoordinate, null);
+            creationSuccessful = false;
+        }
+
+        final FlowAnalysisRuleNode flowAnalysisRuleNode = createFlowAnalysisRuleNode(loggableComponent, creationSuccessful);
+
+        return flowAnalysisRuleNode;
+    }
+
 
     private ProcessorNode createProcessorNode(final LoggableComponent<Processor> processor, final String componentType, final boolean extensionMissing) {
         final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry);
+        final ValidationContextFactory validationContextFactory = createValidationContextFactory(serviceProvider, componentVarRegistry);
 
         final ProcessorNode procNode = new StandardProcessorNode(processor, identifier, validationContextFactory, processScheduler, serviceProvider,
                     componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, extensionMissing);
@@ -450,10 +517,9 @@ public class ExtensionBuilder {
         return procNode;
     }
 
-
     private ReportingTaskNode createReportingTaskNode(final LoggableComponent<ReportingTask> reportingTask, final boolean creationSuccessful) {
         final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry);
+        final ValidationContextFactory validationContextFactory = createValidationContextFactory(serviceProvider, componentVarRegistry);
         final ReportingTaskNode taskNode;
         if (creationSuccessful) {
             taskNode = new StandardReportingTaskNode(reportingTask, identifier, flowController, processScheduler,
@@ -471,9 +537,13 @@ public class ExtensionBuilder {
         return taskNode;
     }
 
+    private StandardValidationContextFactory createValidationContextFactory(ControllerServiceProvider serviceProvider, VariableRegistry variableRegistry) {
+        return new StandardValidationContextFactory(serviceProvider, variableRegistry, ruleViolationsManager, flowAnalyzer);
+    }
+
     private ParameterProviderNode createParameterProviderNode(final LoggableComponent<ParameterProvider> parameterProvider, final boolean creationSuccessful) {
         final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry);
+        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry, ruleViolationsManager, flowAnalyzer);
         final ParameterProviderNode parameterProviderNode;
         if (creationSuccessful) {
             parameterProviderNode = new StandardParameterProviderNode(parameterProvider, identifier, flowController,
@@ -495,7 +565,7 @@ public class ExtensionBuilder {
 
     private FlowRegistryClientNode createFlowRegistryClientNode(final LoggableComponent<FlowRegistryClient> client, final boolean creationSuccessful) {
         final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry);
+        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry, ruleViolationsManager, flowAnalyzer);
         final FlowRegistryClientNode clientNode;
 
         if (creationSuccessful) {
@@ -609,7 +679,7 @@ public class ExtensionBuilder {
             final LoggableComponent<ControllerService> proxiedLoggableComponent = new LoggableComponent<>(proxiedService, bundleCoordinate, terminationAwareLogger);
 
             final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-            final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry);
+            final ValidationContextFactory validationContextFactory = createValidationContextFactory(serviceProvider, componentVarRegistry);
             final ControllerServiceNode serviceNode = new StandardControllerServiceNode(originalLoggableComponent, proxiedLoggableComponent, invocationHandler,
                     identifier, validationContextFactory, serviceProvider, componentVarRegistry, reloadComponent, extensionManager, validationTrigger);
             serviceNode.setName(rawClass.getSimpleName());
@@ -696,7 +766,7 @@ public class ExtensionBuilder {
         final ControllerServiceInvocationHandler invocationHandler = new StandardControllerServiceInvocationHandler(extensionManager, ghostService);
 
         final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
-        final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, variableRegistry);
+        final ValidationContextFactory validationContextFactory = createValidationContextFactory(serviceProvider, variableRegistry);
         final ControllerServiceNode serviceNode = new StandardControllerServiceNode(proxiedLoggableComponent, proxiedLoggableComponent, invocationHandler, identifier,
                 validationContextFactory, serviceProvider, componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, true);
 
@@ -747,6 +817,42 @@ public class ExtensionBuilder {
         }
     }
 
+    private LoggableComponent<FlowAnalysisRule> createLoggableFlowAnalysisRule() throws FlowAnalysisRuleInstantiationException {
+        try {
+            final LoggableComponent<FlowAnalysisRule> loggableComponent = createLoggableComponent(FlowAnalysisRule.class, new StandardLoggingContext(null));
+
+            final String taskName = loggableComponent.getComponent().getClass().getSimpleName();
+            final FlowAnalysisRuleInitializationContext config = new StandardFlowAnalysisInitializationContext(identifier,
+                    loggableComponent.getLogger(), serviceProvider, kerberosConfig, nodeTypeProvider);
+
+            loggableComponent.getComponent().initialize(config);
+
+            return loggableComponent;
+        } catch (final Exception e) {
+            throw new FlowAnalysisRuleInstantiationException(type, e);
+        }
+    }
+
+    private FlowAnalysisRuleNode createFlowAnalysisRuleNode(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule, final boolean creationSuccessful) {
+        final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry);
+        final ValidationContextFactory validationContextFactory = createValidationContextFactory(serviceProvider, componentVarRegistry);
+        final FlowAnalysisRuleNode ruleNode;
+        if (creationSuccessful) {
+            ruleNode = new StandardFlowAnalysisRuleNode(flowAnalysisRule, identifier, flowController,
+                validationContextFactory, ruleViolationsManager, componentVarRegistry, reloadComponent, extensionManager, validationTrigger);
+            ruleNode.setName(ruleNode.getFlowAnalysisRule().getClass().getSimpleName());
+        } else {
+            final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type;
+            final String componentType = "(Missing) " + simpleClassName;
+
+            ruleNode = new StandardFlowAnalysisRuleNode(flowAnalysisRule, identifier, flowController, validationContextFactory, ruleViolationsManager,
+                    componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, true);
+            ruleNode.setName(componentType);
+        }
+
+        return ruleNode;
+    }
+
     private LoggableComponent<FlowRegistryClient> createLoggableFlowRegistryClient() throws FlowRepositoryClientInstantiationException {
         try {
             final LoggableComponent<FlowRegistryClient> clientComponent = createLoggableComponent(FlowRegistryClient.class, new StandardLoggingContext(null));
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index f9a2b402f6..c57cdf950b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -18,6 +18,8 @@ package org.apache.nifi.controller;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.admin.service.AuditService;
+import org.apache.nifi.flowanalysis.StandardFlowAnalyzer;
+import org.apache.nifi.flowanalysis.TriggerFlowAnalysisTask;
 import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
 import org.apache.nifi.annotation.notification.PrimaryNodeState;
 import org.apache.nifi.authorization.Authorizer;
@@ -57,6 +59,9 @@ import org.apache.nifi.controller.cluster.Heartbeater;
 import org.apache.nifi.controller.exception.CommunicationsException;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.controller.flow.StandardFlowManager;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleProvider;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisUtil;
 import org.apache.nifi.controller.kerberos.KerberosConfig;
 import org.apache.nifi.controller.leader.election.LeaderElectionManager;
 import org.apache.nifi.controller.leader.election.LeaderElectionStateChangeListener;
@@ -144,6 +149,7 @@ import org.apache.nifi.events.EventReporter;
 import org.apache.nifi.flow.Bundle;
 import org.apache.nifi.flow.VersionedConnection;
 import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.groups.BundleUpdateStrategy;
@@ -179,6 +185,7 @@ import org.apache.nifi.python.PythonProcessConfig;
 import org.apache.nifi.registry.VariableRegistry;
 import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RemoteResourceManager;
@@ -206,6 +213,7 @@ import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.ReflectionUtils;
 import org.apache.nifi.util.concurrency.TimedLock;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.apache.nifi.web.api.dto.status.StatusHistoryDTO;
 import org.apache.nifi.web.revision.RevisionManager;
 import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
@@ -244,11 +252,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import static java.util.Objects.requireNonNull;
 
-public class FlowController implements ReportingTaskProvider, Authorizable, NodeTypeProvider {
+public class FlowController implements ReportingTaskProvider, FlowAnalysisRuleProvider, Authorizable, NodeTypeProvider {
     private static final String STANDARD_PYTHON_BRIDGE_IMPLEMENTATION_CLASS = "org.apache.nifi.py4j.StandardPythonBridge";
 
     // default repository implementations
@@ -322,11 +331,13 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
     private final LeaderElectionManager leaderElectionManager;
     private final ClusterCoordinator clusterCoordinator;
     private final FlowEngine validationThreadPool;
+    private final FlowEngine flowAnalysisThreadPool;
     private final ValidationTrigger validationTrigger;
     private final ReloadComponent reloadComponent;
     private final ProvenanceAuthorizableFactory provenanceAuthorizableFactory;
     private final UserAwareEventAccess eventAccess;
     private final ParameterContextManager parameterContextManager;
+    private final StandardFlowAnalyzer flowAnalyzer;
     private final StandardFlowManager flowManager;
     private final RepositoryContextFactory repositoryContextFactory;
     private final RingBufferGarbageCollectionLog gcLog;
@@ -408,7 +419,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             final BulletinRepository bulletinRepo,
             final VariableRegistry variableRegistry,
             final ExtensionDiscoveringManager extensionManager,
-            final StatusHistoryRepository statusHistoryRepository) {
+            final StatusHistoryRepository statusHistoryRepository,
+            final RuleViolationsManager ruleViolationsManager) {
 
         return new FlowController(
                 flowFileEventRepo,
@@ -425,7 +437,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
                 /* variable registry */ variableRegistry,
                 extensionManager,
                 null,
-                statusHistoryRepository);
+                statusHistoryRepository,
+                ruleViolationsManager);
     }
 
     public static FlowController createClusteredInstance(
@@ -442,7 +455,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             final VariableRegistry variableRegistry,
             final ExtensionDiscoveringManager extensionManager,
             final RevisionManager revisionManager,
-            final StatusHistoryRepository statusHistoryRepository) {
+            final StatusHistoryRepository statusHistoryRepository,
+            final RuleViolationsManager ruleViolationsManager) {
 
         final FlowController flowController = new FlowController(
                 flowFileEventRepo,
@@ -459,7 +473,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
                 variableRegistry,
                 extensionManager,
                 revisionManager,
-                statusHistoryRepository);
+                statusHistoryRepository,
+                ruleViolationsManager);
 
         return flowController;
     }
@@ -480,7 +495,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             final VariableRegistry variableRegistry,
             final ExtensionDiscoveringManager extensionManager,
             final RevisionManager revisionManager,
-            final StatusHistoryRepository statusHistoryRepository) {
+            final StatusHistoryRepository statusHistoryRepository,
+            final RuleViolationsManager ruleViolationsManager) {
 
         maxTimerDrivenThreads = new AtomicInteger(10);
 
@@ -552,7 +568,21 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
 
         parameterContextManager = new StandardParameterContextManager();
         repositoryContextFactory = new RepositoryContextFactory(contentRepository, flowFileRepository, flowFileEventRepository, counterRepositoryRef.get(), provenanceRepository, stateManagerProvider);
-        flowManager = new StandardFlowManager(nifiProperties, sslContext, this, flowFileEventRepository, parameterContextManager);
+
+        this.flowAnalysisThreadPool = new FlowEngine(1, "Background Flow Analysis", true);
+        flowAnalyzer = new StandardFlowAnalyzer(
+                ruleViolationsManager,
+                this,
+                extensionManager
+        );
+
+        flowManager = new StandardFlowManager(
+                nifiProperties,
+                sslContext,
+                this,
+                flowFileEventRepository,
+                parameterContextManager
+        );
 
         controllerServiceProvider = new StandardControllerServiceProvider(processScheduler, bulletinRepository, flowManager, extensionManager);
         controllerServiceResolver = new StandardControllerServiceResolver(authorizer, flowManager, new NiFiRegistryFlowMapper(extensionManager),
@@ -572,7 +602,13 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
         pythonBundle = PythonBundle.create(nifiProperties, pythonBridgeClassLoader);
         extensionManager.discoverPythonExtensions(pythonBundle);
 
-        flowManager.initialize(controllerServiceProvider, pythonBridge);
+        flowManager.initialize(
+                controllerServiceProvider,
+                pythonBridge,
+                flowAnalyzer,
+                ruleViolationsManager
+        );
+        flowAnalyzer.initialize(controllerServiceProvider);
 
         final QuartzSchedulingAgent quartzSchedulingAgent = new QuartzSchedulingAgent(this, timerDrivenEngineRef.get(), repositoryContextFactory);
         final TimerDrivenSchedulingAgent timerDrivenAgent = new TimerDrivenSchedulingAgent(this, timerDrivenEngineRef.get(), repositoryContextFactory, this.nifiProperties);
@@ -1105,6 +1141,14 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             }
         }
 
+        for (final FlowAnalysisRuleNode ruleNode : getAllFlowAnalysisRules()) {
+            final FlowAnalysisRule rule = ruleNode.getFlowAnalysisRule();
+
+            try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, rule.getClass(), rule.getIdentifier())) {
+                ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, rule, ruleNode.getConfigurationContext());
+            }
+        }
+
         for (final ParameterProviderNode parameterProviderNode : flowManager.getAllParameterProviders()) {
             final ParameterProvider provider = parameterProviderNode.getParameterProvider();
 
@@ -1129,6 +1173,19 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             LOG.debug("Triggering initial validation of all components");
             final long start = System.nanoTime();
 
+            Supplier<VersionedProcessGroup> rootProcessGroupSupplier = () -> {
+                ProcessGroup rootProcessGroup = getFlowManager().getRootGroup();
+
+                NiFiRegistryFlowMapper mapper = FlowAnalysisUtil.createMapper(getExtensionManager());
+
+                InstantiatedVersionedProcessGroup versionedRootProcessGroup = mapper.mapNonVersionedProcessGroup(
+                    rootProcessGroup,
+                    controllerServiceProvider
+                );
+
+                return versionedRootProcessGroup;
+            };
+
             final ValidationTrigger triggerIfValidating = new ValidationTrigger() {
                 @Override
                 public void triggerAsync(final ComponentNode component) {
@@ -1155,11 +1212,13 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
                 }
             };
 
+            new TriggerFlowAnalysisTask(flowAnalyzer, rootProcessGroupSupplier).run();
             new TriggerValidationTask(flowManager, triggerIfValidating).run();
 
             final long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
             LOG.info("Performed initial validation of all components in {} milliseconds", millis);
 
+            scheduleBackgroundFlowAnalysis(rootProcessGroupSupplier);
             // Trigger component validation to occur every 5 seconds.
             validationThreadPool.scheduleWithFixedDelay(new TriggerValidationTask(flowManager, validationTrigger), 5, 5, TimeUnit.SECONDS);
 
@@ -1236,6 +1295,21 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
         }
     }
 
+    private void scheduleBackgroundFlowAnalysis(Supplier<VersionedProcessGroup> rootProcessGroupSupplier) {
+        try {
+            final long scheduleMillis = parseDurationPropertyToMillis(NiFiProperties.BACKGROUND_FLOW_ANALYSIS_SCHEDULE);
+
+            flowAnalysisThreadPool.scheduleWithFixedDelay(
+                new TriggerFlowAnalysisTask(flowManager.getFlowAnalyzer(), rootProcessGroupSupplier),
+                scheduleMillis,
+                scheduleMillis,
+                TimeUnit.MILLISECONDS
+            );
+        } catch (Exception e) {
+            LOG.warn("Could not initialize TriggerFlowAnalysisTask.", e);
+        }
+    }
+
     private void scheduleLongRunningTaskMonitor() {
         longRunningTaskMonitorThreadPool.ifPresent(flowEngine -> {
             try {
@@ -1424,6 +1498,7 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             }
 
             validationThreadPool.shutdown();
+            flowAnalysisThreadPool.shutdown();
             clusterTaskExecutor.shutdownNow();
 
             if (zooKeeperStateServer != null) {
@@ -1627,6 +1702,11 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
                 return delegate.getState(taskNode);
             }
 
+            @Override
+            public org.apache.nifi.flow.ScheduledState getState(final FlowAnalysisRuleNode ruleNode) {
+                return delegate.getState(ruleNode);
+            }
+
             @Override
             public org.apache.nifi.flow.ScheduledState getState(final ControllerServiceNode serviceNode) {
                 return delegate.getState(serviceNode);
@@ -3342,4 +3422,38 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
             return primary;
         }
     }
+
+    // Flow Analysis
+    @Override
+    public FlowAnalysisRuleNode createFlowAnalysisRule(String type, String id, BundleCoordinate bundleCoordinate, boolean firstTimeAdded) throws FlowAnalysisRuleInstantiationException {
+        return flowManager.createFlowAnalysisRule(type, id, bundleCoordinate, firstTimeAdded);
+    }
+
+    @Override
+    public FlowAnalysisRuleNode getFlowAnalysisRuleNode(String identifier) {
+        return flowManager.getFlowAnalysisRuleNode(identifier);
+    }
+
+    @Override
+    public Set<FlowAnalysisRuleNode> getAllFlowAnalysisRules() {
+        return flowManager.getAllFlowAnalysisRules();
+    }
+
+    @Override
+    public void removeFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule) {
+        flowManager.removeFlowAnalysisRule(flowAnalysisRule);
+    }
+
+    @Override
+    public void enableFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule) {
+        flowAnalysisRule.verifyCanEnable();
+        flowAnalysisRule.reloadAdditionalResourcesIfNecessary();
+        flowAnalysisRule.enable();
+    }
+
+    @Override
+    public void disableFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule) {
+        flowAnalysisRule.verifyCanDisable();
+        flowAnalysisRule.disable();
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java
index 212908cba0..d546077211 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java
@@ -21,10 +21,12 @@ import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.controller.exception.ControllerServiceInstantiationException;
 import org.apache.nifi.controller.parameter.ParameterProviderInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
 import org.apache.nifi.controller.flowrepository.FlowRepositoryClientInstantiationException;
 import org.apache.nifi.controller.service.ControllerServiceInvocationHandler;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.StandardConfigurationContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.logging.LogRepositoryFactory;
 import org.apache.nifi.nar.ExtensionManager;
@@ -219,6 +221,55 @@ public class StandardReloadComponent implements ReloadComponent {
         flowController.getValidationTrigger().triggerAsync(existingNode);
     }
 
+    @Override
+    public void reload(final FlowAnalysisRuleNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set<URL> additionalUrls)
+        throws FlowAnalysisRuleInstantiationException {
+        if (existingNode == null) {
+            throw new IllegalStateException("Existing FlowAnalysisRuleNode cannot be null");
+        }
+
+        final String id = existingNode.getFlowAnalysisRule().getIdentifier();
+
+        // ghost components will have a null logger
+        if (existingNode.getLogger() != null) {
+            existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate});
+        }
+
+        final ExtensionManager extensionManager = flowController.getExtensionManager();
+
+        // createFlowAnalysisRule will create a new instance class loader for the same id so
+        // save the instance class loader to use it for calling OnRemoved on the existing processor
+        final ClassLoader existingInstanceClassLoader = extensionManager.getInstanceClassLoader(id);
+
+        // call OnRemoved for the existing flow analysis rule using the previous instance class loader
+        final ConfigurationContext configurationContext = existingNode.getConfigurationContext();
+        try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) {
+            ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getFlowAnalysisRule(), configurationContext);
+        } finally {
+            extensionManager.closeURLClassLoader(id, existingInstanceClassLoader);
+        }
+
+        // set firstTimeAdded to true so lifecycle annotations get fired, but don't register this node
+        // attempt the creation to make sure it works before firing the OnRemoved methods below
+        final String classloaderIsolationKey = existingNode.getClassLoaderIsolationKey(configurationContext);
+        final FlowAnalysisRuleNode newNode = flowController.getFlowManager().createFlowAnalysisRule(newType, id, bundleCoordinate, additionalUrls, true, false, classloaderIsolationKey);
+
+        // set the new flow analysis rule into the existing node
+        final ComponentLog componentLogger = new SimpleProcessLogger(id, existingNode.getFlowAnalysisRule(), new StandardLoggingContext(null));
+        final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger);
+        LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger);
+
+        final LoggableComponent<FlowAnalysisRule> newFlowAnalysisRule = new LoggableComponent<>(newNode.getFlowAnalysisRule(), newNode.getBundleCoordinate(), terminationAwareLogger);
+        existingNode.setFlowAnalysisRule(newFlowAnalysisRule);
+        existingNode.setExtensionMissing(newNode.isExtensionMissing());
+
+        // need to refresh the properties in case we are changing from ghost component to real component
+        existingNode.refreshProperties();
+
+        logger.debug("Triggering async validation of {} due to flow analysis rule reload", existingNode);
+        flowController.getValidationTrigger().triggerAsync(existingNode);
+    }
+
     @Override
     public void reload(final ParameterProviderNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set<URL> additionalUrls)
             throws ParameterProviderInstantiationException {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/XmlFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/XmlFlowSynchronizer.java
index fe95c83aa4..1c94022e7d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/XmlFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/XmlFlowSynchronizer.java
@@ -33,6 +33,7 @@ import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
 import org.apache.nifi.connectable.Size;
 import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
 import org.apache.nifi.controller.inheritance.AuthorizerCheck;
 import org.apache.nifi.controller.inheritance.BundleCompatibilityCheck;
 import org.apache.nifi.controller.inheritance.ConnectionMissingCheck;
@@ -56,6 +57,8 @@ import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.encrypt.PropertyEncryptor;
 import org.apache.nifi.events.BulletinFactory;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleState;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
 import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.groups.BundleUpdateStrategy;
 import org.apache.nifi.groups.FlowFileConcurrency;
@@ -91,6 +94,7 @@ import org.apache.nifi.web.api.dto.ComponentReferenceDTO;
 import org.apache.nifi.web.api.dto.ConnectableDTO;
 import org.apache.nifi.web.api.dto.ConnectionDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
 import org.apache.nifi.web.api.dto.FlowRegistryClientDTO;
 import org.apache.nifi.web.api.dto.FlowSnippetDTO;
 import org.apache.nifi.web.api.dto.FunnelDTO;
@@ -141,6 +145,7 @@ import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.zip.GZIPInputStream;
 
 /**
@@ -330,6 +335,7 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
         final Set<String> missingComponents = new HashSet<>();
         flowManager.getAllControllerServices().stream().filter(ComponentNode::isExtensionMissing).forEach(cs -> missingComponents.add(cs.getIdentifier()));
         flowManager.getAllReportingTasks().stream().filter(ComponentNode::isExtensionMissing).forEach(r -> missingComponents.add(r.getIdentifier()));
+        flowManager.getAllFlowAnalysisRules().stream().filter(ComponentNode::isExtensionMissing).forEach(r -> missingComponents.add(r.getIdentifier()));
         flowManager.getAllParameterProviders().stream().filter(ComponentNode::isExtensionMissing).forEach(r -> missingComponents.add(r.getIdentifier()));
         flowManager.getAllFlowRegistryClients().stream().filter(ComponentNode::isExtensionMissing).forEach(c -> missingComponents.add(c.getIdentifier()));
         root.findAllProcessors().stream().filter(AbstractComponentNode::isExtensionMissing).forEach(p -> missingComponents.add(p.getIdentifier()));
@@ -381,8 +387,12 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
         }
     }
 
-    private void updateFlow(final FlowController controller, final Document configuration, final DataFlow existingFlow, final boolean existingFlowEmpty)
-            throws ReportingTaskInstantiationException {
+    private void updateFlow(
+        final FlowController controller,
+        final Document configuration,
+        final DataFlow existingFlow,
+        final boolean existingFlowEmpty
+    ) throws ReportingTaskInstantiationException, FlowAnalysisRuleInstantiationException {
         final boolean flowAlreadySynchronized = controller.isFlowSynchronized();
         final FlowManager flowManager = controller.getFlowManager();
 
@@ -481,6 +491,21 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
             reportingTaskNodesToDTOs.put(reportingTask, dto);
         }
 
+        // get all the flow analysis rule elements
+        final Element flowAnalysisRulesElement = DomUtils.getChild(rootElement, "flowAnalysisRules");
+        final List<Element> flowAnalysisRuleElements = new ArrayList<>();
+        if (flowAnalysisRulesElement != null) {
+            flowAnalysisRuleElements.addAll(DomUtils.getChildElementsByTagName(flowAnalysisRulesElement, "flowAnalysisRule"));
+        }
+
+        // get/create all the flow analysis rule nodes and DTOs, but don't apply their state yet
+        final Map<FlowAnalysisRuleNode, FlowAnalysisRuleDTO> flowAnalysisRuleNodesToDTOs = new HashMap<>();
+        for (final Element taskElement : flowAnalysisRuleElements) {
+            final FlowAnalysisRuleDTO dto = FlowFromDOMFactory.getFlowAnalysisRule(taskElement, encryptor, encodingVersion);
+            final FlowAnalysisRuleNode flowAnalysisRule = getOrCreateFlowAnalysisRule(controller, dto, flowAlreadySynchronized, existingFlowEmpty);
+            flowAnalysisRuleNodesToDTOs.put(flowAnalysisRule, dto);
+        }
+
         final Element controllerServicesElement = DomUtils.getChild(rootElement, "controllerServices");
         if (controllerServicesElement != null) {
             final List<Element> serviceElements = DomUtils.getChildElementsByTagName(controllerServicesElement, "controllerService");
@@ -495,10 +520,13 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
                     serviceElements, controller, group, encryptor, encodingVersion);
 
                 // If we are moving controller services to the root group we also need to see if any reporting tasks
-                // reference them, and if so we need to clone the CS and update the reporting task reference
+                // or flow analysis rules reference them, and if so we need to clone the CS and update the task- and rule references
                 if (group != null) {
-                    // find all the controller service ids referenced by reporting tasks
-                    final Set<String> controllerServicesInReportingTasks = reportingTaskNodesToDTOs.keySet().stream()
+                    // find all the controller service ids referenced by reporting tasks and flow analysis rules
+                    final Set<String> controllerServicesInReportingTasksAndFlowAnalysisRules = Stream.concat(
+                        reportingTaskNodesToDTOs.keySet().stream(),
+                        flowAnalysisRuleNodesToDTOs.keySet().stream()
+                    )
                         .flatMap(r -> r.getEffectivePropertyValues().entrySet().stream())
                         .filter(e -> e.getKey().getControllerServiceDefinition() != null)
                         .map(Map.Entry::getValue)
@@ -510,9 +538,9 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
                             .map(Map.Entry::getValue)
                             .collect(Collectors.toSet());
 
-                    // find the controller service nodes for each id referenced by a reporting task
+                    // find the controller service nodes for each id referenced by a reporting task or flow analysis rule
                     final Set<ControllerServiceNode> controllerServicesToClone = controllerServices.keySet().stream()
-                        .filter(cs -> controllerServicesInReportingTasks.contains(cs.getIdentifier())
+                        .filter(cs -> controllerServicesInReportingTasksAndFlowAnalysisRules.contains(cs.getIdentifier())
                                 || controllerServicesInParameterProviders.contains(cs.getIdentifier()))
                         .collect(Collectors.toSet());
 
@@ -552,6 +580,11 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
         for (Map.Entry<ReportingTaskNode, ReportingTaskDTO> entry : reportingTaskNodesToDTOs.entrySet()) {
             applyReportingTaskScheduleState(controller, entry.getValue(), entry.getKey(), flowAlreadySynchronized, existingFlowEmpty);
         }
+
+        // now that controller services are loaded and enabled we can apply the state to each flow analysis rule
+        for (Map.Entry<FlowAnalysisRuleNode, FlowAnalysisRuleDTO> entry : flowAnalysisRuleNodesToDTOs.entrySet()) {
+            applyFlowAnalysisRuleState(controller, entry.getValue(), entry.getKey(), flowAlreadySynchronized, existingFlowEmpty);
+        }
     }
 
     private ParameterContext createParameterContext(final ParameterContextDTO dto, final FlowManager flowManager) {
@@ -684,6 +717,14 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
             }
         }
 
+        final Element flowAnalysisRulesElement = DomUtils.getChild(rootElement, "flowAnalysisRules");
+        if (flowAnalysisRulesElement != null) {
+            final List<Element> flowAnalysisRulesElements = DomUtils.getChildElementsByTagName(flowAnalysisRulesElement, "flowAnalysisRule");
+            if (!flowAnalysisRulesElements.isEmpty()) {
+                return false;
+            }
+        }
+
         final Element parameterProvidersElement = DomUtils.getChild(rootElement, "parameterProviders");
         if (parameterProvidersElement != null) {
             final List<Element> providerElements = DomUtils.getChildElementsByTagName(parameterProvidersElement, "parameterProvider");
@@ -929,6 +970,105 @@ public class XmlFlowSynchronizer implements FlowSynchronizer {
         }
     }
 
+    private FlowAnalysisRuleNode getOrCreateFlowAnalysisRule(final FlowController controller, final FlowAnalysisRuleDTO dto, final boolean controllerInitialized, final boolean existingFlowEmpty)
+        throws FlowAnalysisRuleInstantiationException {
+        // create a new flow analysis rule node when the controller is not initialized or the flow is empty
+        if (!controllerInitialized || existingFlowEmpty) {
+            BundleCoordinate coordinate;
+            try {
+                coordinate = BundleUtils.getCompatibleBundle(extensionManager, dto.getType(), dto.getBundle());
+            } catch (final IllegalStateException e) {
+                final BundleDTO bundleDTO = dto.getBundle();
+                if (bundleDTO == null) {
+                    coordinate = BundleCoordinate.UNKNOWN_COORDINATE;
+                } else {
+                    coordinate = new BundleCoordinate(bundleDTO.getGroup(), bundleDTO.getArtifact(), bundleDTO.getVersion());
+                }
+            }
+
+            final FlowAnalysisRuleNode flowAnalysisRule = controller.createFlowAnalysisRule(dto.getType(), dto.getId(), coordinate, false);
+            flowAnalysisRule.setName(dto.getName());
+            flowAnalysisRule.setComments(dto.getComments());
+
+            flowAnalysisRule.setEnforcementPolicy(EnforcementPolicy.valueOf(dto.getEnforcementPolicy()));
+
+            final Set<String> sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(dto.getSensitiveDynamicPropertyNames(), flowAnalysisRule);
+            flowAnalysisRule.setProperties(dto.getProperties(), false, sensitiveDynamicPropertyNames);
+
+            return flowAnalysisRule;
+        } else {
+            // otherwise return the existing flow analysis rule node
+            return controller.getFlowAnalysisRuleNode(dto.getId());
+        }
+    }
+
+    private void applyFlowAnalysisRuleState(final FlowController controller, final FlowAnalysisRuleDTO dto, final FlowAnalysisRuleNode flowAnalysisRule,
+                                            final boolean controllerInitialized, final boolean existingFlowEmpty) {
+        if (!controllerInitialized || existingFlowEmpty) {
+            applyNewFlowAnalysisRuleState(controller, dto, flowAnalysisRule);
+        } else {
+            applyExistingFlowAnalysisRuleState(controller, dto, flowAnalysisRule);
+        }
+    }
+
+    private void applyNewFlowAnalysisRuleState(final FlowController controller, final FlowAnalysisRuleDTO dto, final FlowAnalysisRuleNode flowAnalysisRule) {
+        if (autoResumeState) {
+            if (FlowAnalysisRuleState.ENABLED.name().equals(dto.getState())) {
+                try {
+                    controller.enableFlowAnalysisRule(flowAnalysisRule);
+                } catch (final Exception e) {
+                    logger.error("Failed to enable {} due to {}", flowAnalysisRule, e);
+                    if (logger.isDebugEnabled()) {
+                        logger.error("", e);
+                    }
+                    controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin(
+                            "Flow Analysis Rules", Severity.ERROR.name(), "Failed to start " + flowAnalysisRule + " due to " + e));
+                }
+            } else if (FlowAnalysisRuleState.DISABLED.name().equals(dto.getState())) {
+                try {
+                    controller.disableFlowAnalysisRule(flowAnalysisRule);
+                } catch (final Exception e) {
+                    logger.error("Failed to mark {} as disabled due to {}", flowAnalysisRule, e);
+                    if (logger.isDebugEnabled()) {
+                        logger.error("", e);
+                    }
+                    controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin(
+                            "Flow Analysis Rules", Severity.ERROR.name(), "Failed to mark " + flowAnalysisRule + " as disabled due to " + e));
+                }
+            }
+        }
+    }
+
+    private void applyExistingFlowAnalysisRuleState(final FlowController controller, final FlowAnalysisRuleDTO dto, final FlowAnalysisRuleNode node) {
+        if (!node.getState().name().equals(dto.getState())) {
+            try {
+                switch (FlowAnalysisRuleState.valueOf(dto.getState())) {
+                    case DISABLED:
+                        if (node.isEnabled()) {
+                            controller.disableFlowAnalysisRule(node);
+                        }
+                        break;
+                    case ENABLED:
+                        if (!node.isEnabled()) {
+                            controller.enableFlowAnalysisRule(node);
+                        }
+                        break;
+                }
+            } catch (final IllegalStateException ise) {
+                logger.error("Failed to change State of {} from {} to {} due to {}", node, node.getState().name(), dto.getState(), ise.toString());
+                logger.error("", ise);
+
+                // create bulletin for node
+                controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin("Node Reconnection", Severity.ERROR.name(),
+                        "Failed to change State of " + node + " from " + node.getState().name() + " to " + dto.getState() + " due to " + ise.toString()));
+
+                // create bulletin at Controller level.
+                controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin("Node Reconnection", Severity.ERROR.name(),
+                        "Failed to change State of " + node + " from " + node.getState().name() + " to " + dto.getState() + " due to " + ise.toString()));
+            }
+        }
+    }
+
     private ControllerServiceState getFinalTransitionState(final ControllerServiceState state) {
         switch (state) {
             case DISABLED:
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
index 58afd013eb..f594d31f39 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
@@ -36,6 +36,7 @@ import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.controller.ExtensionBuilder;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.FlowSnippet;
 import org.apache.nifi.controller.ParameterProviderNode;
@@ -55,12 +56,14 @@ import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.service.StandardConfigurationContext;
 import org.apache.nifi.deprecation.log.DeprecationLogger;
 import org.apache.nifi.deprecation.log.DeprecationLoggerFactory;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.groups.StandardProcessGroup;
 import org.apache.nifi.groups.StatelessGroupNodeFactory;
 import org.apache.nifi.logging.ControllerServiceLogObserver;
+import org.apache.nifi.logging.FlowAnalysisRuleLogObserver;
 import org.apache.nifi.logging.FlowRegistryClientLogObserver;
 import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.logging.LogRepository;
@@ -346,6 +349,8 @@ public class StandardFlowManager extends AbstractFlowManager implements FlowMana
             .addClasspathUrls(additionalUrls)
             .kerberosConfig(flowController.createKerberosConfig(nifiProperties))
             .extensionManager(extensionManager)
+            .flowAnalyzer(getFlowAnalyzer())
+            .ruleViolationsManager(getRuleViolationsManager())
             .classloaderIsolationKey(classloaderIsolationKey)
             .pythonBridge(flowController.getPythonBridge())
             .buildProcessor();
@@ -521,6 +526,72 @@ public class StandardFlowManager extends AbstractFlowManager implements FlowMana
         return taskNode;
     }
 
+    @Override
+    public FlowAnalysisRuleNode createFlowAnalysisRule(
+            final String type,
+            final String id,
+            final BundleCoordinate bundleCoordinate,
+            final Set<URL> additionalUrls,
+            final boolean firstTimeAdded,
+            final boolean register,
+            final String classloaderIsolationKey
+    ) {
+        requireNonNull(type);
+        requireNonNull(id);
+        requireNonNull(bundleCoordinate);
+
+        // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader
+        final LogRepository logRepository = LogRepositoryFactory.getRepository(id);
+        final ExtensionManager extensionManager = flowController.getExtensionManager();
+
+        final FlowAnalysisRuleNode flowAnalysisRuleNode = new ExtensionBuilder()
+            .identifier(id)
+            .type(type)
+            .bundleCoordinate(bundleCoordinate)
+            .controllerServiceProvider(flowController.getControllerServiceProvider())
+            .processScheduler(processScheduler)
+            .nodeTypeProvider(flowController)
+            .validationTrigger(flowController.getValidationTrigger())
+            .reloadComponent(flowController.getReloadComponent())
+            .variableRegistry(flowController.getVariableRegistry())
+            .addClasspathUrls(additionalUrls)
+            .kerberosConfig(flowController.createKerberosConfig(nifiProperties))
+            .flowController(flowController)
+            .extensionManager(extensionManager)
+            .flowAnalyzer(getFlowAnalyzer())
+            .ruleViolationsManager(getRuleViolationsManager())
+            .classloaderIsolationKey(classloaderIsolationKey)
+            .buildFlowAnalysisRuleNode();
+
+        LogRepositoryFactory.getRepository(flowAnalysisRuleNode.getIdentifier()).setLogger(flowAnalysisRuleNode.getLogger());
+
+        if (firstTimeAdded) {
+            FlowAnalysisRule flowAnalysisRule = flowAnalysisRuleNode.getFlowAnalysisRule();
+            final Class<? extends ConfigurableComponent> flowAnalysisRuleClass = flowAnalysisRule.getClass();
+            final String identifier = flowAnalysisRule.getIdentifier();
+
+            try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), flowAnalysisRuleClass, identifier)) {
+                ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, flowAnalysisRule);
+                logDeprecationNotice(flowAnalysisRule);
+
+                if (flowController.isInitialized()) {
+                    ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, flowAnalysisRule, flowAnalysisRuleNode.getConfigurationContext());
+                }
+            } catch (final Exception e) {
+                throw new ComponentLifeCycleException("Failed to invoke On-Added Lifecycle methods of " + flowAnalysisRule, e);
+            }
+        }
+
+        if (register) {
+            onFlowAnalysisRuleAdded(flowAnalysisRuleNode);
+
+            // Register log observer to provide bulletins when flow analysis rule logs anything at WARN level or above
+            logRepository.addObserver(LogLevel.WARN, new FlowAnalysisRuleLogObserver(bulletinRepository, flowAnalysisRuleNode));
+        }
+
+        return flowAnalysisRuleNode;
+    }
+
     @Override
     public ParameterProviderNode createParameterProvider(final String type, final String id, final BundleCoordinate bundleCoordinate,
                                                          final Set<URL> additionalUrls, final boolean firstTimeAdded, final boolean registerLogObserver) {
@@ -625,6 +696,8 @@ public class StandardFlowManager extends AbstractFlowManager implements FlowMana
 
         processScheduler.submitFrameworkTask(() -> flowController.getStateManagerProvider().onComponentRemoved(service.getIdentifier()));
 
+        processScheduler.submitFrameworkTask(() -> getRuleViolationsManager().removeRuleViolationsForSubject(service.getIdentifier()));
+
         extensionManager.removeInstanceClassLoader(service.getIdentifier());
 
         logger.info("{} removed from Flow Controller", service);
@@ -651,6 +724,8 @@ public class StandardFlowManager extends AbstractFlowManager implements FlowMana
             .kerberosConfig(flowController.createKerberosConfig(nifiProperties))
             .stateManagerProvider(flowController.getStateManagerProvider())
             .extensionManager(extensionManager)
+            .flowAnalyzer(getFlowAnalyzer())
+            .ruleViolationsManager(getRuleViolationsManager())
             .classloaderIsolationKey(classloaderIsolationKey)
             .buildControllerService();
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java
new file mode 100644
index 0000000000..2d3330da94
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java
@@ -0,0 +1,54 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.registry.flow.mapping.ComponentIdLookup;
+import org.apache.nifi.registry.flow.mapping.FlowMappingOptions;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup;
+
+public class FlowAnalysisUtil {
+    public static final String ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE = "*****";
+
+    public static NiFiRegistryFlowMapper createMapper(ExtensionManager extensionManager) {
+        NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper(
+                extensionManager,
+                new FlowMappingOptions.Builder()
+                        .mapPropertyDescriptors(true)
+                        .mapControllerServiceReferencesToVersionedId(true)
+                        .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP)
+                        .componentIdLookup(ComponentIdLookup.USE_COMPONENT_ID)
+                        .mapSensitiveConfiguration(true)
+                        .sensitiveValueEncryptor(value -> ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE)
+                        .build()
+        ) {
+            @Override
+            public String getGroupId(String groupId) {
+                return groupId;
+            }
+
+            @Override
+            protected String encrypt(String value) {
+                return ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE;
+            }
+        };
+
+        return mapper;
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisContext.java
new file mode 100644
index 0000000000..70775f285d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisContext.java
@@ -0,0 +1,82 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.VersionedControllerServiceLookup;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flowanalysis.FlowAnalysisContext;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Optional;
+
+public class StandardFlowAnalysisContext implements FlowAnalysisContext {
+    private final FlowController flowController;
+
+    public StandardFlowAnalysisContext(final FlowController flowController) {
+        this.flowController = flowController;
+    }
+
+    @Override
+    public VersionedControllerServiceLookup getVersionedControllerServiceLookup() {
+        VersionedControllerServiceLookup versionedControllerServiceLookup = id -> {
+            ControllerServiceProvider controllerServiceProvider = flowController.getControllerServiceProvider();
+            ExtensionManager extensionManager = flowController.getExtensionManager();
+
+            NiFiRegistryFlowMapper mapper = FlowAnalysisUtil.createMapper(extensionManager);
+
+            ControllerServiceNode controllerServiceNode = controllerServiceProvider.getControllerServiceNode(id);
+
+            VersionedControllerService versionedControllerService = mapper.mapControllerService(
+                    controllerServiceNode,
+                    controllerServiceProvider,
+                    Collections.emptySet(),
+                    new HashMap<>()
+            );
+
+            return versionedControllerService;
+        };
+
+        return versionedControllerServiceLookup;
+    }
+
+    @Override
+    public int getMaxTimerDrivenThreadCount() {
+        return flowController.getMaxTimerDrivenThreadCount();
+    }
+
+    @Override
+    public boolean isClustered() {
+        return flowController.isConfiguredForClustering();
+    }
+
+    @Override
+    public Optional<String> getClusterNodeIdentifier() {
+        final NodeIdentifier nodeId = flowController.getNodeId();
+
+        final Optional<String> nodeIdOptional = Optional.ofNullable(nodeId)
+                .map(NodeIdentifier::getId);
+
+        return nodeIdOptional;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleContext.java
new file mode 100644
index 0000000000..b80714e29d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleContext.java
@@ -0,0 +1,62 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.flowanalysis.FlowAnalysisContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.registry.VariableRegistry;
+
+import java.util.Map;
+
+public class StandardFlowAnalysisRuleContext extends AbstractFlowAnalysisRuleContext implements FlowAnalysisRuleContext {
+    private final String ruleName;
+    private final FlowController flowController;
+    private final FlowAnalysisContext flowAnalysisContext;
+
+    public StandardFlowAnalysisRuleContext(
+            final String ruleName,
+            final StandardFlowAnalysisRuleNode flowAnalysisRuleNode,
+            final Map<PropertyDescriptor, String> properties,
+            final FlowController flowController,
+            final ParameterLookup parameterLookup,
+            final VariableRegistry variableRegistry
+    ) {
+        super(flowAnalysisRuleNode, properties, flowController.getControllerServiceProvider(), parameterLookup, variableRegistry);
+        this.ruleName = ruleName;
+        this.flowController = flowController;
+        this.flowAnalysisContext = new StandardFlowAnalysisContext(flowController);
+    }
+
+    @Override
+    public String getRuleName() {
+        return ruleName;
+    }
+
+    @Override
+    public StateManager getStateManager() {
+        return flowController.getStateManagerProvider().getStateManager(getFlowAnalysisRule().getIdentifier());
+    }
+
+    @Override
+    public FlowAnalysisContext getFlowAnalysisContext() {
+        return flowAnalysisContext;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleNode.java
new file mode 100644
index 0000000000..5c01d88d50
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flowanalysis/StandardFlowAnalysisRuleNode.java
@@ -0,0 +1,111 @@
+/*
+ * 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.controller.flowanalysis;
+
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.ResourceFactory;
+import org.apache.nifi.authorization.resource.ResourceType;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationTrigger;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.validation.RuleViolationsManager;
+
+import java.util.Collections;
+import java.util.List;
+
+public class StandardFlowAnalysisRuleNode extends AbstractFlowAnalysisRuleNode implements FlowAnalysisRuleNode {
+
+    private final FlowController flowController;
+
+    public StandardFlowAnalysisRuleNode(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule, final String id, final FlowController controller,
+                                        final ValidationContextFactory validationContextFactory, final RuleViolationsManager ruleViolationsManager,
+                                        final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager extensionManager,
+                                        final ValidationTrigger validationTrigger) {
+        super(flowAnalysisRule, id, controller.getControllerServiceProvider(), validationContextFactory, ruleViolationsManager, variableRegistry, reloadComponent, extensionManager, validationTrigger);
+        this.flowController = controller;
+    }
+
+    public StandardFlowAnalysisRuleNode(final LoggableComponent<FlowAnalysisRule> flowAnalysisRule, final String id, final FlowController controller,
+                                        final ValidationContextFactory validationContextFactory, final RuleViolationsManager ruleViolationsManager,
+                                        final String componentType, final String canonicalClassName, final ComponentVariableRegistry variableRegistry,
+                                        final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) {
+        super(flowAnalysisRule, id, controller.getControllerServiceProvider(), validationContextFactory, ruleViolationsManager, componentType, canonicalClassName,
+            variableRegistry, reloadComponent, extensionManager, validationTrigger, isExtensionMissing);
+        this.flowController = controller;
+    }
+
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return flowController;
+    }
+
+    @Override
+    public Resource getResource() {
+        return ResourceFactory.getComponentResource(ResourceType.FlowAnalysisRule, getIdentifier(), getName());
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return getFlowAnalysisRule().getClass().isAnnotationPresent(Restricted.class);
+    }
+
+    @Override
+    public Class<?> getComponentClass() {
+        return getFlowAnalysisRule().getClass();
+    }
+
+    @Override
+    public boolean isDeprecated() {
+        return getFlowAnalysisRule().getClass().isAnnotationPresent(DeprecationNotice.class);
+    }
+
+    @Override
+    public FlowAnalysisRuleContext getFlowAnalysisRuleContext() {
+        return new StandardFlowAnalysisRuleContext(
+            getName(),
+            this,
+            getEffectivePropertyValues(),
+            flowController,
+            ParameterLookup.EMPTY,
+            getVariableRegistry()
+        );
+    }
+
+    @Override
+    protected List<ValidationResult> validateConfig() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    protected ParameterContext getParameterContext() {
+        return null;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/inheritance/BundleCompatibilityCheck.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/inheritance/BundleCompatibilityCheck.java
index 78919602d6..5d5f3e42ac 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/inheritance/BundleCompatibilityCheck.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/inheritance/BundleCompatibilityCheck.java
@@ -20,6 +20,7 @@ import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.cluster.protocol.DataFlow;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.flow.VersionedDataflow;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedFlowRegistryClient;
 import org.apache.nifi.controller.serialization.FlowFromDOMFactory;
 import org.apache.nifi.flow.Bundle;
@@ -91,6 +92,19 @@ public class BundleCompatibilityCheck implements FlowInheritabilityCheck {
             }
         }
 
+        if (dataflow.getFlowAnalysisRules() != null) {
+            for (final VersionedFlowAnalysisRule rule : dataflow.getFlowAnalysisRules()) {
+                if (missingComponents.contains(rule.getInstanceIdentifier())) {
+                    continue;
+                }
+
+                if (isMissing(rule.getBundle(), extensionManager)) {
+                    return FlowInheritability.notInheritable(String.format("Flow Analysis Rule with ID %s and type %s requires bundle %s, but that bundle cannot be found in this NiFi instance",
+                        rule.getInstanceIdentifier(), rule.getType(), rule.getBundle()));
+                }
+            }
+        }
+
         if (dataflow.getParameterProviders() != null) {
             for (final VersionedParameterProvider task : dataflow.getParameterProviders()) {
                 if (missingComponents.contains(task.getInstanceIdentifier())) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/AffectedComponentSet.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/AffectedComponentSet.java
index acfad4dd7d..eef51fba63 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/AffectedComponentSet.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/AffectedComponentSet.java
@@ -22,6 +22,7 @@ import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.AbstractComponentNode;
 import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
@@ -38,6 +39,7 @@ import org.apache.nifi.flow.ConnectableComponentType;
 import org.apache.nifi.flow.ExecutionEngine;
 import org.apache.nifi.flow.VersionedComponent;
 import org.apache.nifi.flow.VersionedConnection;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleState;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.groups.StatelessGroupScheduledState;
@@ -82,6 +84,7 @@ public class AffectedComponentSet {
     private final Set<ProcessorNode> processors = new HashSet<>();
     private final Set<ControllerServiceNode> controllerServices = new HashSet<>();
     private final Set<ReportingTaskNode> reportingTasks = new HashSet<>();
+    private final Set<FlowAnalysisRuleNode> flowAnalysisRules = new HashSet<>();
     private final Set<ParameterProviderNode> parameterProviders = new HashSet<>();
     private final Set<FlowRegistryClientNode> flowRegistryClients = new HashSet<>();
     private final Set<ProcessGroup> statelessProcessGroups = new HashSet<>();
@@ -204,6 +207,8 @@ public class AffectedComponentSet {
                 addProcessor((ProcessorNode) reference);
             } else if (reference instanceof ReportingTaskNode) {
                 addReportingTask((ReportingTaskNode) reference);
+            } else if (reference instanceof FlowAnalysisRuleNode) {
+                addFlowAnalysisRule((FlowAnalysisRuleNode) reference);
             } else if (reference instanceof ParameterProviderNode) {
                 addParameterProvider((ParameterProviderNode) reference);
             } else if (reference instanceof FlowRegistryClientNode) {
@@ -248,6 +253,24 @@ public class AffectedComponentSet {
         return false;
     }
 
+    public void addFlowAnalysisRule(final FlowAnalysisRuleNode rule) {
+        if (rule == null) {
+            return;
+        }
+
+        flowAnalysisRules.add(rule);
+    }
+
+    public boolean isFlowAnalysisRuleAffected(final String flowAnalysisRuleId) {
+        for (final FlowAnalysisRuleNode ruleNode : flowAnalysisRules) {
+            if (ruleNode.getIdentifier().equals(flowAnalysisRuleId)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public void addParameterProvider(final ParameterProviderNode parameterProvider) {
         if (parameterProvider == null) {
             return;
@@ -566,6 +589,9 @@ public class AffectedComponentSet {
             case REPORTING_TASK:
                 addReportingTask(flowManager.getReportingTaskNode(componentId));
                 break;
+            case FLOW_ANALYSIS_RULE:
+                addFlowAnalysisRule(flowManager.getFlowAnalysisRuleNode(componentId));
+                break;
         }
     }
 
@@ -587,6 +613,7 @@ public class AffectedComponentSet {
         processors.stream().filter(this::isActive).forEach(active::addProcessor);
         reportingTasks.stream().filter(task -> task.getScheduledState() == ScheduledState.STARTING || task.getScheduledState() == ScheduledState.RUNNING || task.isRunning())
             .forEach(active::addReportingTask);
+        flowAnalysisRules.stream().filter(rule -> rule.getState() == FlowAnalysisRuleState.ENABLED).forEach(active::addFlowAnalysisRule);
         controllerServices.stream().filter(service -> ACTIVE_CONTROLLER_SERVICE_STATES.contains(service.getState()))
             .forEach(active::addControllerServiceWithoutReferences);
 
@@ -595,7 +622,6 @@ public class AffectedComponentSet {
         return active;
     }
 
-
     private boolean isActive(final ProcessorNode processor) {
         // We consider component active if it's starting, running, or has active threads. The call to ProcessorNode.isRunning() will only return true if it has active threads or a scheduled
         // state of RUNNING but not if it has a scheduled state of STARTING. We also consider if the processor is to be started once the flow controller has been fully initialized, as
@@ -620,6 +646,7 @@ public class AffectedComponentSet {
         remoteOutputPorts.forEach(port -> port.getRemoteProcessGroup().startTransmitting(port));
         processors.forEach(processor -> processor.getProcessGroup().startProcessor(processor, false));
         reportingTasks.forEach(flowController::startReportingTask);
+        flowAnalysisRules.forEach(flowController::enableFlowAnalysisRule);
         statelessProcessGroups.forEach(group -> group.startProcessing());
     }
 
@@ -631,6 +658,7 @@ public class AffectedComponentSet {
         processors.removeIf(filter::testProcessor);
         controllerServices.removeIf(filter::testControllerService);
         reportingTasks.removeIf(filter::testReportingTask);
+        flowAnalysisRules.removeIf(filter::testFlowAnalysisRule);
         flowRegistryClients.removeIf(filter::testFlowRegistryClient);
         statelessProcessGroups.removeIf(filter::testStatelessGroup);
     }
@@ -652,6 +680,7 @@ public class AffectedComponentSet {
         remoteOutputPorts.stream().filter(port -> port.getProcessGroup().findRemoteGroupPort(port.getIdentifier()) != null).forEach(existing::addRemoteOutputPort);
         processors.stream().filter(processor -> processor.getProcessGroup().getProcessor(processor.getIdentifier()) != null).forEach(existing::addProcessor);
         reportingTasks.stream().filter(task -> flowController.getReportingTaskNode(task.getIdentifier()) != null).forEach(existing::addReportingTask);
+        flowAnalysisRules.stream().filter(rule -> flowController.getFlowAnalysisRuleNode(rule.getIdentifier()) != null).forEach(existing::addFlowAnalysisRule);
         controllerServices.stream().filter(service -> serviceProvider.getControllerServiceNode(service.getIdentifier()) != null).forEach(existing::addControllerServiceWithoutReferences);
         flowRegistryClients.stream().filter(client -> flowManager.getFlowRegistryClient(client.getIdentifier()) != null).forEach(existing::addFlowRegistryClient);
         statelessProcessGroups.stream().filter(group -> flowManager.getGroup(group.getIdentifier()) != null).forEach(existing::addStatelessGroup);
@@ -675,6 +704,7 @@ public class AffectedComponentSet {
         remoteOutputPorts.stream().filter(this::isStartable).forEach(startable::addRemoteOutputPort);
         processors.stream().filter(this::isStartable).forEach(startable::addProcessor);
         reportingTasks.stream().filter(this::isStartable).forEach(startable::addReportingTask);
+        flowAnalysisRules.stream().filter(this::isStartable).forEach(startable::addFlowAnalysisRule);
         controllerServices.stream().filter(this::isStartable).forEach(startable::addControllerServiceWithoutReferences);
         statelessProcessGroups.stream().filter(this::isStartable).forEach(startable::addStatelessGroup);
 
@@ -729,6 +759,7 @@ public class AffectedComponentSet {
         remoteOutputPorts.forEach(port -> port.getRemoteProcessGroup().stopTransmitting(port));
         processors.forEach(processor -> processor.getProcessGroup().stopProcessor(processor));
         reportingTasks.forEach(flowController::stopReportingTask);
+        flowAnalysisRules.forEach(flowController::disableFlowAnalysisRule);
         statelessProcessGroups.forEach(group -> group.stopProcessing());
 
         waitForConnectablesStopped();
@@ -822,6 +853,7 @@ public class AffectedComponentSet {
             ", flowRegistryCliens=" + flowRegistryClients +
             ", controllerServices=" + controllerServices +
             ", reportingTasks=" + reportingTasks +
+            ", flowAnalysisRules=" + flowAnalysisRules +
             ", statelessProcessGroups=" + statelessProcessGroups +
             "]";
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ComponentSetFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ComponentSetFilter.java
index 8f4fa10278..027ad0b19d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ComponentSetFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ComponentSetFilter.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.controller.serialization;
 
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.service.ControllerServiceNode;
@@ -30,6 +31,8 @@ public interface ComponentSetFilter {
 
     boolean testReportingTask(ReportingTaskNode reportingTask);
 
+    boolean testFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule);
+
     boolean testControllerService(ControllerServiceNode controllerService);
 
     boolean testInputPort(Port port);
@@ -59,6 +62,11 @@ public interface ComponentSetFilter {
                 return !original.testReportingTask(reportingTask);
             }
 
+            @Override
+            public boolean testFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule) {
+                return !original.testFlowAnalysisRule(flowAnalysisRule);
+            }
+
             @Override
             public boolean testControllerService(final ControllerServiceNode controllerService) {
                 return !original.testControllerService(controllerService);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
index 9471f7bc32..926665a73f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
@@ -33,6 +33,7 @@ import org.apache.nifi.web.api.dto.BundleDTO;
 import org.apache.nifi.web.api.dto.ConnectableDTO;
 import org.apache.nifi.web.api.dto.ConnectionDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
 import org.apache.nifi.web.api.dto.FlowRegistryClientDTO;
 import org.apache.nifi.web.api.dto.FlowSnippetDTO;
 import org.apache.nifi.web.api.dto.FunnelDTO;
@@ -154,6 +155,24 @@ public class FlowFromDOMFactory {
         return dto;
     }
 
+    public static FlowAnalysisRuleDTO getFlowAnalysisRule(Element element, PropertyEncryptor encryptor, FlowEncodingVersion flowEncodingVersion) {
+        final FlowAnalysisRuleDTO dto = new FlowAnalysisRuleDTO();
+
+        dto.setId(getString(element, "id"));
+        dto.setName(getString(element, "name"));
+        dto.setComments(getString(element, "comment"));
+        dto.setType(getString(element, "class"));
+        dto.setBundle(getBundle(DomUtils.getChild(element, "bundle")));
+
+        dto.setEnforcementPolicy(getString(element, "enforcementPolicy"));
+        dto.setState(getString(element, "state"));
+
+        dto.setSensitiveDynamicPropertyNames(getSensitivePropertyNames(element));
+        dto.setProperties(getProperties(element, encryptor, flowEncodingVersion));
+
+        return dto;
+    }
+
     public static FlowRegistryClientDTO getFlowRegistryClient(final Element element, final PropertyEncryptor encryptor, final FlowEncodingVersion flowEncodingVersion) {
         final FlowRegistryClientDTO dto = new FlowRegistryClientDTO();
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/RunningComponentSetFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/RunningComponentSetFilter.java
index 0b1984a0b3..cb0db371ca 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/RunningComponentSetFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/RunningComponentSetFilter.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.controller.serialization;
 
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.flow.VersionedDataflow;
@@ -25,6 +26,7 @@ import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.flow.ExecutionEngine;
 import org.apache.nifi.flow.ScheduledState;
 import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.flow.VersionedPort;
 import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedProcessor;
@@ -41,6 +43,7 @@ public class RunningComponentSetFilter implements ComponentSetFilter {
     private final Map<String, VersionedControllerService> controllerServices = new HashMap<>();
     private final Map<String, VersionedProcessor> processors = new HashMap<>();
     private final Map<String, VersionedReportingTask> reportingTasks = new HashMap<>();
+    private final Map<String, VersionedFlowAnalysisRule> flowAnalysisRules = new HashMap<>();
     private final Map<String, VersionedPort> inputPorts = new HashMap<>();
     private final Map<String, VersionedPort> outputPorts = new HashMap<>();
     private final Map<String, VersionedRemoteGroupPort> remoteInputPorts = new HashMap<>();
@@ -50,6 +53,7 @@ public class RunningComponentSetFilter implements ComponentSetFilter {
     public RunningComponentSetFilter(final VersionedDataflow dataflow) {
         dataflow.getControllerServices().forEach(service -> controllerServices.put(service.getInstanceIdentifier(), service));
         dataflow.getReportingTasks().forEach(task -> reportingTasks.put(task.getInstanceIdentifier(), task));
+        dataflow.getFlowAnalysisRules().forEach(rule -> flowAnalysisRules.put(rule.getInstanceIdentifier(), rule));
         flatten(dataflow.getRootGroup());
     }
 
@@ -91,6 +95,12 @@ public class RunningComponentSetFilter implements ComponentSetFilter {
         return versionedReportingTask != null && versionedReportingTask.getScheduledState() == ScheduledState.RUNNING;
     }
 
+    @Override
+    public boolean testFlowAnalysisRule(FlowAnalysisRuleNode flowAnalysisRule) {
+        final VersionedFlowAnalysisRule versionedFlowAnalysisRule = flowAnalysisRules.get(flowAnalysisRule.getIdentifier());
+        return versionedFlowAnalysisRule != null && versionedFlowAnalysisRule.getScheduledState() == ScheduledState.ENABLED;
+    }
+
     @Override
     public boolean testControllerService(final ControllerServiceNode controllerService) {
         final VersionedControllerService versionedService = controllerServices.get(controllerService.getIdentifier());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
index 89f4501493..b4f3cc4abf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
@@ -24,6 +24,7 @@ import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
 import org.apache.nifi.connectable.Size;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
@@ -122,6 +123,12 @@ public class StandardFlowSerializer implements FlowSerializer<Document> {
                 addReportingTask(reportingTasksNode, taskNode, encryptor);
             }
 
+            final Element flowAnalysisRulesNode = doc.createElement("flowAnalysisRules");
+            rootNode.appendChild(flowAnalysisRulesNode);
+            for (final FlowAnalysisRuleNode flowAnalysisRuleNode : controller.getAllFlowAnalysisRules()) {
+                addFlowAnalysisRule(flowAnalysisRulesNode, flowAnalysisRuleNode, encryptor);
+            }
+
             final Element parameterProvidersNode = doc.createElement("parameterProviders");
             rootNode.appendChild(parameterProvidersNode);
             for (final ParameterProviderNode providerNode : controller.getFlowManager().getAllParameterProviders()) {
@@ -677,6 +684,23 @@ public class StandardFlowSerializer implements FlowSerializer<Document> {
         element.appendChild(taskElement);
     }
 
+    private void addFlowAnalysisRule(Element flowAnalysisRulesNode, FlowAnalysisRuleNode flowAnalysisRuleNode, final PropertyEncryptor encryptor) {
+        final Element taskElement = flowAnalysisRulesNode.getOwnerDocument().createElement("flowAnalysisRule");
+        addTextElement(taskElement, "id", flowAnalysisRuleNode.getIdentifier());
+        addTextElement(taskElement, "name", flowAnalysisRuleNode.getName());
+        addTextElement(taskElement, "comment", flowAnalysisRuleNode.getComments());
+        addTextElement(taskElement, "class", flowAnalysisRuleNode.getCanonicalClassName());
+
+        addBundle(taskElement, flowAnalysisRuleNode.getBundleCoordinate());
+
+        addTextElement(taskElement, "enforcementPolicy", flowAnalysisRuleNode.getEnforcementPolicy().name());
+        addTextElement(taskElement, "state", flowAnalysisRuleNode.getState().name());
+
+        addConfiguration(taskElement, flowAnalysisRuleNode.getRawPropertyValues(), flowAnalysisRuleNode.getAnnotationData(), encryptor);
+
+        flowAnalysisRulesNode.appendChild(taskElement);
+    }
+
     public static void addParameterProvider(final Element element, final ParameterProviderNode providerNode, final PropertyEncryptor encryptor) {
         final Element taskElement = element.getOwnerDocument().createElement("parameterProvider");
         addTextElement(taskElement, "id", providerNode.getIdentifier());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
index 840fa28b6a..3c8cfcc62d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.controller.serialization;
 
 import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ParameterProviderNode;
 import org.apache.nifi.controller.ProcessorNode;
@@ -25,6 +26,7 @@ import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.Template;
 import org.apache.nifi.controller.flow.VersionedDataflow;
 import org.apache.nifi.controller.flow.VersionedFlowEncodingVersion;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.controller.flow.VersionedTemplate;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.flow.ScheduledState;
@@ -86,6 +88,7 @@ public class VersionedDataflowMapper {
         dataflow.setParameterContexts(mapParameterContexts());
         dataflow.setRegistries(mapRegistries());
         dataflow.setReportingTasks(mapReportingTasks());
+        dataflow.setFlowAnalysisRules(mapFlowAnalysisRules());
         dataflow.setParameterProviders(mapParameterProviders());
         dataflow.setRootGroup(mapRootGroup());
         dataflow.setTemplates(mapTemplates());
@@ -138,6 +141,17 @@ public class VersionedDataflowMapper {
         return reportingTasks;
     }
 
+    private List<VersionedFlowAnalysisRule> mapFlowAnalysisRules() {
+        final List<VersionedFlowAnalysisRule> flowAnalysisRules = new ArrayList<>();
+
+        for (final FlowAnalysisRuleNode ruleNode : flowController.getAllFlowAnalysisRules()) {
+            final VersionedFlowAnalysisRule versionedFlowAnalysisRule = flowMapper.mapFlowAnalysisRule(ruleNode, flowController.getControllerServiceProvider());
+            flowAnalysisRules.add(versionedFlowAnalysisRule);
+        }
+
+        return flowAnalysisRules;
+    }
+
     private List<VersionedParameterProvider> mapParameterProviders() {
         final List<VersionedParameterProvider> parameterProviders = new ArrayList<>();
 
@@ -205,6 +219,17 @@ public class VersionedDataflowMapper {
                 return map(taskNode.getScheduledState());
             }
 
+            @Override
+            public ScheduledState getState(final FlowAnalysisRuleNode ruleNode) {
+                switch (ruleNode.getState()) {
+                    case DISABLED:
+                        return ScheduledState.DISABLED;
+                    case ENABLED:
+                    default:
+                        return ScheduledState.ENABLED;
+                }
+            }
+
             @Override
             public ScheduledState getState(final ControllerServiceNode serviceNode) {
                 switch (serviceNode.getState()) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
index 2455f12af1..5f1e5ecbb8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
@@ -29,6 +29,7 @@ import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Position;
 import org.apache.nifi.controller.AbstractComponentNode;
 import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.MissingBundleException;
 import org.apache.nifi.controller.ParameterProviderNode;
@@ -40,6 +41,8 @@ import org.apache.nifi.controller.UninheritableFlowException;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.controller.flow.VersionedDataflow;
 import org.apache.nifi.controller.flow.VersionedFlowEncodingVersion;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
+import org.apache.nifi.flow.VersionedFlowAnalysisRule;
 import org.apache.nifi.controller.flow.VersionedTemplate;
 import org.apache.nifi.controller.inheritance.AuthorizerCheck;
 import org.apache.nifi.controller.inheritance.BundleCompatibilityCheck;
@@ -250,6 +253,20 @@ public class VersionedFlowSynchronizer implements FlowSynchronizer {
             }
         }
 
+        if (dataflow.getFlowAnalysisRules() == null) {
+            dataflow.setFlowAnalysisRules(new ArrayList<>());
+        }
+        for (final VersionedFlowAnalysisRule flowAnalysisRule : dataflow.getFlowAnalysisRules()) {
+            if (missingComponentIds.contains(flowAnalysisRule.getInstanceIdentifier())) {
+                continue;
+            }
+
+            final Bundle compatibleBundle = getCompatibleBundle(flowAnalysisRule.getBundle(), extensionManager, flowAnalysisRule.getType());
+            if (compatibleBundle != null) {
+                flowAnalysisRule.setBundle(compatibleBundle);
+            }
+        }
+
         if (dataflow.getRegistries() == null) {
             dataflow.setRegistries(new ArrayList<>());
         }
@@ -385,6 +402,7 @@ public class VersionedFlowSynchronizer implements FlowSynchronizer {
                 inheritParameterProviders(controller, versionedFlow, affectedComponentSet);
                 inheritParameterContexts(controller, versionedFlow);
                 inheritReportingTasks(controller, versionedFlow, affectedComponentSet);
+                inheritFlowAnalysisRules(controller, versionedFlow, affectedComponentSet);
                 inheritRegistries(controller, versionedFlow, affectedComponentSet);
 
                 final ComponentIdGenerator componentIdGenerator = (proposedId, instanceId, destinationGroupId) -> instanceId;
@@ -453,16 +471,30 @@ public class VersionedFlowSynchronizer implements FlowSynchronizer {
 
         final VersionedDataflow clusterVersionedFlow = proposedFlow.getVersionedDataflow();
         final ComparableDataFlow clusterDataFlow = new StandardComparableDataFlow(
-            "Cluster Flow", clusterVersionedFlow.getRootGroup(), toSet(clusterVersionedFlow.getControllerServices()), toSet(clusterVersionedFlow.getReportingTasks()),
-            toSet(clusterVersionedFlow.getParameterContexts()), toSet(clusterVersionedFlow.getParameterProviders()), toSet(clusterVersionedFlow.getRegistries()));
+            "Cluster Flow",
+            clusterVersionedFlow.getRootGroup(),
+            toSet(clusterVersionedFlow.getControllerServices()),
+            toSet(clusterVersionedFlow.getReportingTasks()),
+            toSet(clusterVersionedFlow.getFlowAnalysisRules()),
+            toSet(clusterVersionedFlow.getParameterContexts()),
+            toSet(clusterVersionedFlow.getParameterProviders()),
+            toSet(clusterVersionedFlow.getRegistries())
+        );
 
         final VersionedProcessGroup proposedRootGroup = clusterVersionedFlow.getRootGroup();
         final String proposedRootGroupId = proposedRootGroup == null ? null : proposedRootGroup.getInstanceIdentifier();
 
         final VersionedDataflow existingVersionedFlow = existingFlow.getVersionedDataflow() == null ? createEmptyVersionedDataflow(proposedRootGroupId) : existingFlow.getVersionedDataflow();
         final ComparableDataFlow localDataFlow = new StandardComparableDataFlow(
-                "Local Flow", existingVersionedFlow.getRootGroup(), toSet(existingVersionedFlow.getControllerServices()), toSet(existingVersionedFlow.getReportingTasks()),
-                toSet(existingVersionedFlow.getParameterContexts()),toSet(existingVersionedFlow.getParameterProviders()), toSet(existingVersionedFlow.getRegistries()));
+            "Local Flow",
+            existingVersionedFlow.getRootGroup(),
+            toSet(existingVersionedFlow.getControllerServices()),
+            toSet(existingVersionedFlow.getReportingTasks()),
+            toSet(existingVersionedFlow.getFlowAnalysisRules()),
+            toSet(existingVersionedFlow.getParameterContexts()),
+            toSet(existingVersionedFlow.getParameterProviders()),
+            toSet(existingVersionedFlow.getRegistries())
+        );
 
         final FlowComparator flowComparator = new StandardFlowComparator(localDataFlow, clusterDataFlow, Collections.emptySet(),
             differenceDescriptor, encryptor::decrypt, VersionedComponent::getInstanceIdentifier, FlowComparatorVersionedStrategy.DEEP);
@@ -485,6 +517,7 @@ public class VersionedFlowSynchronizer implements FlowSynchronizer {
         dataflow.setParameterProviders(Collections.emptyList());
         dataflow.setRegistries(Collections.emptyList());
         dataflow.setReportingTasks(Collections.emptyList());
+        dataflow.setFlowAnalysisRules(Collections.emptyList());
 
         final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
         rootGroup.setInstanceIdentifier(rootGroupId);
@@ -655,6 +688,51 @@ public class VersionedFlowSynchronizer implements FlowSynchronizer {
         }
     }
 
+    private void inheritFlowAnalysisRules(
+        final FlowController controller,
+        final VersionedDataflow dataflow,
+        final AffectedComponentSet affectedComponentSet
+    ) throws FlowAnalysisRuleInstantiationException {
+        for (final VersionedFlowAnalysisRule versionedFlowAnalysisRule : dataflow.getFlowAnalysisRules()) {
+            final FlowAnalysisRuleNode existing = controller.getFlowAnalysisRuleNode(versionedFlowAnalysisRule.getInstanceIdentifier());
+            if (existing == null) {
+                addFlowAnalysisRule(controller, versionedFlowAnalysisRule);
+            } else if (affectedComponentSet.isFlowAnalysisRuleAffected(existing.getIdentifier())) {
+                updateFlowAnalysisRule(existing, versionedFlowAnalysisRule, controller);
+            }
+        }
+    }
+
+    private void addFlowAnalysisRule(final FlowController controller, final VersionedFlowAnalysisRule flowAnalysisRule) throws FlowAnalysisRuleInstantiationException {
+        final BundleCoordinate coordinate = createBundleCoordinate(flowAnalysisRule.getBundle(), flowAnalysisRule.getType());
+
+        final FlowAnalysisRuleNode ruleNode = controller.createFlowAnalysisRule(flowAnalysisRule.getType(), flowAnalysisRule.getInstanceIdentifier(), coordinate, false);
+        updateFlowAnalysisRule(ruleNode, flowAnalysisRule, controller);
+    }
+
+    private void updateFlowAnalysisRule(final FlowAnalysisRuleNode ruleNode, final VersionedFlowAnalysisRule flowAnalysisRule, final FlowController controller) {
+        ruleNode.setName(flowAnalysisRule.getName());
+        ruleNode.setComments(flowAnalysisRule.getComments());
+        ruleNode.setEnforcementPolicy(flowAnalysisRule.getEnforcementPolicy());
+
+        final Set<String> sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(ruleNode, flowAnalysisRule);
+        final Map<String, String> decryptedProperties = decryptProperties(flowAnalysisRule.getProperties(), controller.getEncryptor());
+        ruleNode.setProperties(decryptedProperties, false, sensitiveDynamicPropertyNames);
+
+        switch (flowAnalysisRule.getScheduledState()) {
+            case DISABLED:
+                if (ruleNode.isEnabled()) {
+                    controller.disableFlowAnalysisRule(ruleNode);
+                }
+                break;
+            case ENABLED:
+                if (!ruleNode.isEnabled()) {
+                    controller.enableFlowAnalysisRule(ruleNode);
+                }
+                break;
+        }
+    }
+
     private void inheritParameterProviders(final FlowController controller, final VersionedDataflow dataflow, final AffectedComponentSet affectedComponentSet) {
         if (dataflow.getParameterProviders() == null) {
             return;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
index ba7bd2483e..1a2402483e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
@@ -31,6 +31,7 @@ import org.apache.nifi.util.DomUtils;
 import org.apache.nifi.util.LoggingXmlParserErrorHandler;
 import org.apache.nifi.web.api.dto.BundleDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
+import org.apache.nifi.web.api.dto.FlowAnalysisRuleDTO;
 import org.apache.nifi.web.api.dto.FlowRegistryClientDTO;
 import org.apache.nifi.web.api.dto.ParameterProviderDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
@@ -285,6 +286,36 @@ public class FingerprintFactory {
             }
         }
 
+        final Element flowAnalysisRulesElem = DomUtils.getChild(flowControllerElem, "flowAnalysisRules");
+        if (flowAnalysisRulesElem != null) {
+            final List<FlowAnalysisRuleDTO> flowAnalysisRuleDtos = new ArrayList<>();
+            for (final Element ruleElem : DomUtils.getChildElementsByTagName(flowAnalysisRulesElem, "flowAnalysisRule")) {
+                final FlowAnalysisRuleDTO dto = FlowFromDOMFactory.getFlowAnalysisRule(ruleElem, encryptor, encodingVersion);
+                flowAnalysisRuleDtos.add(dto);
+            }
+
+            Collections.sort(flowAnalysisRuleDtos, new Comparator<FlowAnalysisRuleDTO>() {
+                @Override
+                public int compare(final FlowAnalysisRuleDTO o1, final FlowAnalysisRuleDTO o2) {
+                    if (o1 == null && o2 == null) {
+                        return 0;
+                    }
+                    if (o1 == null && o2 != null) {
+                        return 1;
+                    }
+                    if (o1 != null && o2 == null) {
+                        return -1;
+                    }
+
+                    return o1.getId().compareTo(o2.getId());
+                }
+            });
+
+            for (final FlowAnalysisRuleDTO dto : flowAnalysisRuleDtos) {
+                addFlowAnalysisRuleFingerprint(builder, dto);
+            }
+        }
+
         final Element parameterProvidersElem = DomUtils.getChild(flowControllerElem, "parameterProviders");
         if (parameterProvidersElem != null) {
             final List<ParameterProviderDTO> parameterProviderDtos = new ArrayList<>();
@@ -850,6 +881,27 @@ public class FingerprintFactory {
         addPropertiesFingerprint(builder, configurableComponent, dto.getProperties());
     }
 
+    private void addFlowAnalysisRuleFingerprint(final StringBuilder builder, final FlowAnalysisRuleDTO dto) {
+        builder.append(dto.getId());
+        builder.append(dto.getType());
+        builder.append(dto.getName());
+
+        addBundleFingerprint(builder, dto.getBundle());
+
+        builder.append(dto.getComments());
+        builder.append(dto.getEnforcementPolicy());
+        builder.append(dto.getState());
+
+        // get the temp instance of the FlowAnalysisRule so that we know the default property values
+        final BundleCoordinate coordinate = getCoordinate(dto.getType(), dto.getBundle());
+        final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(dto.getType(), coordinate);
+        if (configurableComponent == null) {
+            logger.warn("Unable to get FlowAnalysisRule of type {}; its default properties will be fingerprinted instead of being ignored.", dto.getType());
+        }
+
+        addPropertiesFingerprint(builder, configurableComponent, dto.getProperties());
+    }
+
     private void addParameterProviderFingerprint(final StringBuilder builder, final ParameterProviderDTO dto) {
         builder.append(dto.getId());
         builder.append(dto.getType());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/GhostFlowAnalysisRule.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/GhostFlowAnalysisRule.java
new file mode 100644
index 0000000000..5a204c3850
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/GhostFlowAnalysisRule.java
@@ -0,0 +1,90 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.reporting.InitializationException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class GhostFlowAnalysisRule implements FlowAnalysisRule {
+
+    private String id;
+    private String canonicalClassName;
+    private ComponentLog logger;
+
+    public void setIdentifier(final String id) {
+        this.id = id;
+    }
+
+    public void setCanonicalClassName(final String canonicalClassName) {
+        this.canonicalClassName = canonicalClassName;
+    }
+
+    @Override
+    public Collection<ValidationResult> validate(final ValidationContext context) {
+        return Collections.singleton(new ValidationResult.Builder()
+            .input("Any Property")
+            .subject("Missing Flow Analysis Rule")
+            .valid(false)
+            .explanation("Flow Analysis Rule is of type " + canonicalClassName + ", but this is not a valid Flow Analysis Rule type")
+            .build());
+    }
+
+    @Override
+    public PropertyDescriptor getPropertyDescriptor(final String name) {
+        return buildDescriptor(name);
+    }
+
+    private PropertyDescriptor buildDescriptor(final String propertyName) {
+        return new PropertyDescriptor.Builder()
+            .name(propertyName)
+            .description(propertyName)
+            .required(true)
+            .sensitive(true)
+            .build();
+    }
+
+    @Override
+    public void onPropertyModified(final PropertyDescriptor descriptor, String oldValue, String newValue) {
+    }
+
+    @Override
+    public List<PropertyDescriptor> getPropertyDescriptors() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public String toString() {
+        return "GhostFlowAnalysisRule[id=" + id + "]";
+    }
+
+    @Override
+    public void initialize(FlowAnalysisRuleInitializationContext context) throws InitializationException {
+        this.logger = context.getLogger();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/StandardFlowAnalyzer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/StandardFlowAnalyzer.java
new file mode 100644
index 0000000000..bd55cf0af8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/StandardFlowAnalyzer.java
@@ -0,0 +1,252 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleProvider;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisUtil;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedConnection;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+import org.apache.nifi.validation.RuleViolation;
+import org.apache.nifi.validation.RuleViolationsManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * {@link FlowAnalyzer} that uses {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRules}.
+ */
+public class StandardFlowAnalyzer implements FlowAnalyzer {
+    private Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final RuleViolationsManager ruleViolationsManager;
+
+    private final FlowAnalysisRuleProvider flowAnalysisRuleProvider;
+    private final ExtensionManager extensionManager;
+
+    private ControllerServiceProvider controllerServiceProvider;
+
+    public StandardFlowAnalyzer(
+            final RuleViolationsManager ruleViolationsManager,
+            final FlowAnalysisRuleProvider flowAnalysisRuleProvider,
+            final ExtensionManager extensionManager
+    ) {
+        this.ruleViolationsManager = ruleViolationsManager;
+        this.flowAnalysisRuleProvider = flowAnalysisRuleProvider;
+        this.extensionManager = extensionManager;
+    }
+
+    public void initialize(final ControllerServiceProvider controllerServiceProvider) {
+        this.controllerServiceProvider = controllerServiceProvider;
+    }
+
+    @Override
+    public void analyzeProcessor(ProcessorNode processorNode) {
+        logger.debug("Running analysis on {}", processorNode);
+
+        final NiFiRegistryFlowMapper mapper = createMapper();
+
+        VersionedProcessor versionedProcessor = mapper.mapProcessor(
+                processorNode,
+                controllerServiceProvider,
+                Collections.emptySet(),
+                new HashMap<>()
+        );
+
+        analyzeComponent(versionedProcessor);
+    }
+
+    @Override
+    public void analyzeControllerService(ControllerServiceNode controllerServiceNode) {
+        logger.debug("Running analysis on {}", controllerServiceNode);
+
+        final NiFiRegistryFlowMapper mapper = createMapper();
+
+        VersionedControllerService versionedControllerService = mapper.mapControllerService(
+                controllerServiceNode,
+                controllerServiceProvider,
+                Collections.emptySet(),
+                new HashMap<>()
+        );
+
+        analyzeComponent(versionedControllerService);
+    }
+
+    private void analyzeComponent(VersionedComponent component) {
+        long start = System.currentTimeMillis();
+
+        String componentId = component.getIdentifier();
+        Set<FlowAnalysisRuleNode> flowAnalysisRules = flowAnalysisRuleProvider.getAllFlowAnalysisRules();
+
+        Set<RuleViolation> violations = flowAnalysisRules.stream()
+                .filter(FlowAnalysisRuleNode::isEnabled)
+                .flatMap(flowAnalysisRuleNode -> {
+                    String ruleId = flowAnalysisRuleNode.getIdentifier();
+
+                    try {
+                        Collection<ComponentAnalysisResult> analysisResults = flowAnalysisRuleNode
+                                .getFlowAnalysisRule()
+                                .analyzeComponent(component, flowAnalysisRuleNode.getFlowAnalysisRuleContext());
+
+                        return analysisResults.stream()
+                                .map(analysisResult -> new RuleViolation(
+                                        flowAnalysisRuleNode.getEnforcementPolicy(),
+                                        componentId,
+                                        componentId,
+                                        getDisplayName(component),
+                                        component.getGroupIdentifier(),
+                                        ruleId,
+                                        analysisResult.getIssueId(),
+                                        analysisResult.getMessage(),
+                                        analysisResult.getExplanation()
+                                ));
+                    } catch (Exception e) {
+                        logger.error("FlowAnalysis error while running '{}' against '{}'", flowAnalysisRuleNode.getName(), component, e);
+                        return Stream.empty();
+                    }
+                })
+                .collect(Collectors.toSet());
+
+        ruleViolationsManager.upsertComponentViolations(componentId, violations);
+
+        long end = System.currentTimeMillis();
+        long durationMs = end - start;
+
+        logger.trace("Flow Analysis of component '{}' took {} ms", componentId, durationMs);
+    }
+
+    @Override
+    public void analyzeProcessGroup(VersionedProcessGroup processGroup) {
+        logger.debug("Running analysis on process group {}.", processGroup.getIdentifier());
+
+        long start = System.currentTimeMillis();
+
+        Set<FlowAnalysisRuleNode> flowAnalysisRules = flowAnalysisRuleProvider.getAllFlowAnalysisRules();
+
+        Collection<RuleViolation> groupViolations = new HashSet<>();
+        Map<VersionedComponent, Collection<RuleViolation>> componentToRuleViolations = new HashMap<>();
+
+        analyzeProcessGroup(processGroup, flowAnalysisRules, groupViolations, componentToRuleViolations);
+
+        ruleViolationsManager.upsertGroupViolations(processGroup, groupViolations, componentToRuleViolations);
+
+        long end = System.currentTimeMillis();
+        long durationMs = end - start;
+
+        logger.debug("Flow Analysis of process group '{}' took {} ms", processGroup.getIdentifier(), durationMs);
+    }
+
+    private void analyzeProcessGroup(
+            VersionedProcessGroup processGroup,
+            Set<FlowAnalysisRuleNode> flowAnalysisRules,
+            Collection<RuleViolation> groupViolations,
+            Map<VersionedComponent, Collection<RuleViolation>> componentToRuleViolations
+    ) {
+        String groupId = processGroup.getIdentifier();
+
+        flowAnalysisRules.stream()
+                .filter(FlowAnalysisRuleNode::isEnabled)
+                .forEach(flowAnalysisRuleNode -> {
+                    String ruleId = flowAnalysisRuleNode.getIdentifier();
+
+                    try {
+                        Collection<GroupAnalysisResult> analysisResults = flowAnalysisRuleNode.getFlowAnalysisRule().analyzeProcessGroup(
+                                processGroup,
+                                flowAnalysisRuleNode.getFlowAnalysisRuleContext()
+                        );
+
+                        analysisResults.forEach(analysisResult -> {
+                            Optional<VersionedComponent> componentOptional = analysisResult.getComponent();
+
+                            if (componentOptional.isPresent()) {
+                                VersionedComponent component = componentOptional.get();
+
+                                componentToRuleViolations.computeIfAbsent(component, __ -> new HashSet<>())
+                                        .add(new RuleViolation(
+                                                flowAnalysisRuleNode.getEnforcementPolicy(),
+                                                component.getGroupIdentifier(),
+                                                component.getIdentifier(),
+                                                getDisplayName(component),
+                                                component.getGroupIdentifier(),
+                                                ruleId,
+                                                analysisResult.getIssueId(),
+                                                analysisResult.getMessage(),
+                                                analysisResult.getExplanation()
+                                        ));
+
+                            } else {
+                                groupViolations.add(new RuleViolation(
+                                        flowAnalysisRuleNode.getEnforcementPolicy(),
+                                        groupId,
+                                        groupId,
+                                        getDisplayName(processGroup),
+                                        groupId,
+                                        ruleId,
+                                        analysisResult.getIssueId(),
+                                        analysisResult.getMessage(),
+                                        analysisResult.getExplanation()
+                                ));
+                            }
+                        });
+                    } catch (Exception e) {
+                        logger.error("FlowAnalysis error while running '{}' against group '{}'", flowAnalysisRuleNode.getName(), groupId, e);
+                    }
+                });
+
+        processGroup.getProcessors().forEach(processor -> analyzeComponent(processor));
+        processGroup.getControllerServices().forEach(controllerService -> analyzeComponent(controllerService));
+
+        processGroup.getProcessGroups().forEach(childProcessGroup -> analyzeProcessGroup(childProcessGroup, flowAnalysisRules, groupViolations, componentToRuleViolations));
+    }
+
+    private String getDisplayName(VersionedComponent component) {
+        final String displayName;
+
+        if (component instanceof VersionedConnection) {
+            VersionedConnection connection = (VersionedConnection) component;
+            displayName = connection.getSource().getName() + " > " + connection.getSelectedRelationships().stream().collect(Collectors.joining(","));
+        } else {
+            displayName = component.getName();
+        }
+
+        return displayName;
+    }
+
+    private NiFiRegistryFlowMapper createMapper() {
+        NiFiRegistryFlowMapper mapper = FlowAnalysisUtil.createMapper(extensionManager);
+
+        return mapper;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/TriggerFlowAnalysisTask.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/TriggerFlowAnalysisTask.java
new file mode 100644
index 0000000000..56f86c3ade
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/flowanalysis/TriggerFlowAnalysisTask.java
@@ -0,0 +1,47 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.function.Supplier;
+
+public class TriggerFlowAnalysisTask implements Runnable {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final FlowAnalyzer flowAnalyzer;
+    private final Supplier<VersionedProcessGroup> rootProcessGroupSupplier;
+
+    public TriggerFlowAnalysisTask(FlowAnalyzer flowAnalyzer, Supplier<VersionedProcessGroup> rootProcessGroupSupplier) {
+        this.flowAnalyzer = flowAnalyzer;
+        this.rootProcessGroupSupplier = rootProcessGroupSupplier;
+    }
+
+    @Override
+    public void run() {
+        try {
+            logger.debug("Triggering analysis of entire flow");
+
+            flowAnalyzer.analyzeProcessGroup(rootProcessGroupSupplier.get());
+        } catch (final Throwable t) {
+            logger.error("Encountered unexpected error when attempting to analyze flow", t);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
new file mode 100644
index 0000000000..bb4ad15611
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
@@ -0,0 +1,50 @@
+/*
+ * 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.logging;
+
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.events.BulletinFactory;
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.BulletinRepository;
+import org.apache.nifi.reporting.ComponentType;
+import org.apache.nifi.reporting.Severity;
+
+public class FlowAnalysisRuleLogObserver implements LogObserver {
+    private final BulletinRepository bulletinRepository;
+    private final FlowAnalysisRuleNode flowAnalysisRuleNode;
+
+    public FlowAnalysisRuleLogObserver(BulletinRepository bulletinRepository, FlowAnalysisRuleNode flowAnalysisRuleNode) {
+        this.bulletinRepository = bulletinRepository;
+        this.flowAnalysisRuleNode = flowAnalysisRuleNode;
+    }
+
+    @Override
+    public void onLogMessage(final LogMessage message) {
+        // Map LogLevel.WARN to Severity.WARNING so that we are consistent with the Severity enumeration. Else, just use whatever
+        // the LogLevel is (INFO and ERROR map directly and all others we will just accept as they are).
+        final String bulletinLevel = message.getLogLevel() == LogLevel.WARN ? Severity.WARNING.name() : message.getLogLevel().toString();
+
+        final Bulletin bulletin = BulletinFactory.createBulletin(null, flowAnalysisRuleNode.getIdentifier(), ComponentType.FLOW_ANALYSIS_RULE,
+            flowAnalysisRuleNode.getName(), "Log Message", bulletinLevel, message.getMessage());
+        bulletinRepository.addBulletin(bulletin);
+    }
+
+    @Override
+    public String getComponentDescription() {
+        return flowAnalysisRuleNode.toString();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
index 4f15aaeec4..1e56425eb4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
@@ -30,6 +30,7 @@ import org.apache.nifi.nar.ExtensionDiscoveringManager;
 import org.apache.nifi.registry.VariableRegistry;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.apache.nifi.web.revision.RevisionManager;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.FactoryBean;
@@ -54,6 +55,7 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
     private LeaderElectionManager leaderElectionManager;
     private ExtensionDiscoveringManager extensionManager;
     private RevisionManager revisionManager;
+    private RuleViolationsManager ruleViolationsManager;
     private StatusHistoryRepository statusHistoryRepository;
 
     @Override
@@ -78,7 +80,8 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
                     variableRegistry,
                     extensionManager,
                     revisionManager,
-                    statusHistoryRepository);
+                    statusHistoryRepository,
+                    ruleViolationsManager);
             } else {
                 flowController = FlowController.createStandaloneInstance(
                     flowFileEventRepository,
@@ -89,9 +92,9 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
                     bulletinRepository,
                     variableRegistry,
                     extensionManager,
-                    statusHistoryRepository);
+                    statusHistoryRepository,
+                    ruleViolationsManager);
             }
-
         }
 
         return flowController;
@@ -154,6 +157,10 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
         this.revisionManager = revisionManager;
     }
 
+    public void setRuleViolationsManager(final RuleViolationsManager ruleViolationsManager) {
+        this.ruleViolationsManager = ruleViolationsManager;
+    }
+
     public void setStatusHistoryRepository(final StatusHistoryRepository statusHistoryRepository) {
         this.statusHistoryRepository = statusHistoryRepository;
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/RuleViolationKey.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/RuleViolationKey.java
new file mode 100644
index 0000000000..7f3eb9ba08
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/RuleViolationKey.java
@@ -0,0 +1,56 @@
+/*
+ * 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.validation;
+
+import java.util.Objects;
+
+/**
+ * Used only as keys in a map
+ */
+public class RuleViolationKey {
+    private final String scope;
+    private final String subjectId;
+    private final String ruleId;
+    private final String issueId;
+
+    public RuleViolationKey(RuleViolation violation) {
+        this(violation.getScope(), violation.getSubjectId(), violation.getRuleId(), violation.getIssueId());
+    }
+
+    public RuleViolationKey(String scope, String subjectId, String ruleId, String issueId) {
+        this.scope = scope;
+        this.subjectId = subjectId;
+        this.ruleId = ruleId;
+        this.issueId = issueId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        RuleViolationKey that = (RuleViolationKey) o;
+        return Objects.equals(scope, that.scope)
+            && Objects.equals(subjectId, that.subjectId)
+            && Objects.equals(ruleId, that.ruleId)
+            && Objects.equals(issueId, that.issueId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(scope, subjectId, ruleId, issueId);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/StandardRuleViolationsManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/StandardRuleViolationsManager.java
new file mode 100644
index 0000000000..5e232dcc1a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/validation/StandardRuleViolationsManager.java
@@ -0,0 +1,192 @@
+/*
+ * 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.validation;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
+
+/**
+ * RuleViolations are stored in memory. The datastructure housing the instances is a 2-level map of maps, aiming to balance
+ *  complexity and performance.
+ *  <br /><br />
+ *  The outer map key is the id of the component that is responsible for the violation. This way every component can fetch its "own"
+ *  violations efficiently during their validation phases.
+ *  The inner map key is a composite object that uniquely identifies a violation based on certain properties of the violation.
+ *  <br /><br />
+ *  When new violations are added the map goes through state changes vaguely similar to a mark and sweep algorithm.
+ *  First, the set of violations that are supposed to be a previous version of the new set are "marked" by setting their 'available' flags to 'false'.
+ *  Then we add the new violations, some of which may replace previous ones. By replacing a previous violation it automatically loses
+ *  the "mark" i.e. its 'available' flag will be set to 'true'.
+ *  Finally, all violations that are still "marked" are removed.
+ *  <br /><br />
+ *  This way operations can differentiate between "create" and "update" and can even preserve state during the update if the need
+ *  arises.
+ */
+public class StandardRuleViolationsManager implements RuleViolationsManager {
+    public StandardRuleViolationsManager() {
+    }
+
+    private final ConcurrentMap<String, ConcurrentMap<RuleViolationKey, RuleViolation>> subjectIdToRuleViolation = new ConcurrentHashMap<>();
+
+    @Override
+    public void upsertComponentViolations(String componentId, Collection<RuleViolation> violations) {
+        ConcurrentMap<RuleViolationKey, RuleViolation> componentRuleViolations = subjectIdToRuleViolation
+            .computeIfAbsent(componentId, __ -> new ConcurrentHashMap<>());
+
+        synchronized (componentRuleViolations) {
+            // Mark
+            componentRuleViolations.values().stream()
+                .filter(violation -> violation.getScope().equals(componentId))
+                .forEach(violation -> violation.setAvailable(false));
+
+            // Add/Update
+            violations.forEach(violation -> componentRuleViolations
+                .compute(new RuleViolationKey(violation), (ruleViolationKey, currentViolation) -> violation)
+            );
+
+            // Sweep
+            componentRuleViolations.entrySet().removeIf(keyAndViolation -> {
+                RuleViolation violation = keyAndViolation.getValue();
+
+                return violation.getScope().equals(componentId)
+                    && !violation.isAvailable();
+            });
+        }
+    }
+
+    @Override
+    public synchronized void upsertGroupViolations(
+        VersionedProcessGroup processGroup,
+        Collection<RuleViolation> groupViolations,
+        Map<VersionedComponent, Collection<RuleViolation>> componentToRuleViolations
+    ) {
+        // Mark
+        hideGroupViolations(processGroup);
+
+        // Add/Update
+        // We have 2 sets of violations.
+        //
+        // 'groupViolations' are the result of rules being violated by no particular components.
+        //  The responsible entity ('subject') is deemed to be a ProcessGroup. Violations in this collection can correspond to different ProcessGroups
+        //  as child ProcessGroups create their own violations during the analysis of a ProcessGroup.
+        //
+        // 'componentToRuleViolations' are violations grouped by the components. One component can produce multiple violations.
+
+        // Add 'groupViolations' to the stored map.
+        groupViolations.forEach(groupViolation -> {
+            subjectIdToRuleViolation
+                .computeIfAbsent(groupViolation.getSubjectId(), __ -> new ConcurrentHashMap<>())
+                .compute(new RuleViolationKey(groupViolation), (ruleViolationKey, currentViolation) -> groupViolation);
+        });
+
+        // Add 'componentToRuleViolations' to the sored map.
+        componentToRuleViolations.forEach((component, componentViolations) -> {
+            ConcurrentMap<RuleViolationKey, RuleViolation> componentRuleViolations = subjectIdToRuleViolation
+                .computeIfAbsent(component.getIdentifier(), __ -> new ConcurrentHashMap<>());
+
+            componentViolations.forEach(componentViolation -> componentRuleViolations
+                .compute(new RuleViolationKey(componentViolation), (ruleViolationKey, currentViolation) -> componentViolation)
+            );
+        });
+
+        // Sweep
+        purgeGroupViolations(processGroup);
+    }
+
+    private void hideGroupViolations(VersionedProcessGroup processGroup) {
+        String groupId = processGroup.getIdentifier();
+
+        subjectIdToRuleViolation.values().stream()
+            .map(Map::values).flatMap(Collection::stream)
+            .filter(violation -> violation.getScope().equals(groupId))
+            .forEach(violation -> violation.setAvailable(false));
+
+        processGroup.getProcessGroups().forEach(childProcessGroup -> hideGroupViolations(childProcessGroup));
+    }
+
+    private void purgeGroupViolations(VersionedProcessGroup processGroup) {
+        String groupId = processGroup.getIdentifier();
+
+        subjectIdToRuleViolation.values().forEach(violationMap ->
+            violationMap.entrySet().removeIf(keyAndViolation -> {
+                RuleViolation violation = keyAndViolation.getValue();
+
+                return violation.getScope().equals(groupId)
+                    && !violation.isAvailable();
+            }));
+
+        processGroup.getProcessGroups().forEach(childProcessGroup -> purgeGroupViolations(childProcessGroup));
+    }
+
+    @Override
+    public Collection<RuleViolation> getRuleViolationsForSubject(String subjectId) {
+        HashSet<RuleViolation> ruleViolationsForSubject = Optional.ofNullable(subjectIdToRuleViolation.get(subjectId))
+            .map(Map::values)
+            .map(HashSet::new)
+            .orElse(new HashSet<>());
+
+        return ruleViolationsForSubject;
+    }
+
+    @Override
+    public Collection<RuleViolation> getRuleViolationsForGroup(String groupId) {
+        Set<RuleViolation> groupViolations = subjectIdToRuleViolation.values().stream()
+            .map(Map::values).flatMap(Collection::stream)
+            .filter(violation -> violation.getGroupId().equals(groupId))
+            .collect(Collectors.toSet());
+
+        return groupViolations;
+    }
+
+    @Override
+    public Collection<RuleViolation> getAllRuleViolations() {
+        Set<RuleViolation> allRuleViolations = subjectIdToRuleViolation.values().stream()
+            .map(Map::values).flatMap(Collection::stream)
+            .collect(Collectors.toSet());
+
+        return allRuleViolations;
+    }
+
+    @Override
+    public void removeRuleViolationsForSubject(String subjectId) {
+        subjectIdToRuleViolation.remove(subjectId);
+    }
+
+    @Override
+    public void removeRuleViolationsForRule(String ruleId) {
+        subjectIdToRuleViolation.values().stream()
+            .forEach(
+                violationMap -> violationMap
+                    .entrySet()
+                    .removeIf(keyAndViolation -> keyAndViolation.getValue().getRuleId().equals(ruleId))
+            );
+    }
+
+    @Override
+    public void cleanUp() {
+        subjectIdToRuleViolation.entrySet().removeIf(subjectIdAndViolationMap -> subjectIdAndViolationMap.getValue().isEmpty());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
index 1f5bacf799..bce3f0dc99 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
@@ -39,6 +39,8 @@
 
             <xs:element name="reportingTasks" type="ReportingTasksType" minOccurs="0" maxOccurs="1" />
 
+            <xs:element name="flowAnalysisRules" type="FlowAnalysisRulesType" minOccurs="0" maxOccurs="1" />
+
             <xs:element name="parameterProviders" type="ParameterProvidersType" minOccurs="0" maxOccurs="1" />
         </xs:sequence>
         <xs:attribute name="encoding-version" type="xs:string"/>
@@ -194,6 +196,20 @@
         </xs:restriction>
     </xs:simpleType>
 
+    <xs:simpleType name="FlowAnalysisRuleEnforcementPolicy">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="ENFORCE"></xs:enumeration>
+            <xs:enumeration value="WARN"></xs:enumeration>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:simpleType name="FlowAnalysisState">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="ENABLED"></xs:enumeration>
+            <xs:enumeration value="DISABLED"></xs:enumeration>
+        </xs:restriction>
+    </xs:simpleType>
+
     <xs:complexType name="ProcessGroupType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
@@ -549,6 +565,28 @@
         </xs:sequence>
     </xs:complexType>
 
+    <xs:complexType name="FlowAnalysisRulesType">
+        <xs:sequence>
+            <xs:element name="flowAnalysisRule" type="FlowAnalysisRuleType" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="FlowAnalysisRuleType">
+        <xs:sequence>
+            <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="name" type="NonEmptyStringType" />
+            <xs:element name="comment" type="xs:string" />
+            <xs:element name="class" type="NonEmptyStringType" />
+            <xs:element name="bundle" type="BundleType" />
+
+            <xs:element name="enforcementPolicy" type="FlowAnalysisRuleEnforcementPolicy"/>
+            <xs:element name="state" type="FlowAnalysisState" />
+
+            <xs:element name="property" type="PropertyType" minOccurs="0" maxOccurs="unbounded"/>
+            <xs:element name="annotationData" type="xs:string" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
     <xs:complexType name="ParameterProvidersType">
         <xs:sequence>
             <xs:element name="parameterProvider" type="ParameterProviderType" minOccurs="0" maxOccurs="unbounded" />
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
index 3453fee0ad..fccb4d0dd8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
@@ -39,6 +39,9 @@
     <bean id="extensionManager" class="org.apache.nifi.spring.ExtensionManagerFactoryBean">
     </bean>
 
+    <bean id="ruleViolationsManager" class="org.apache.nifi.validation.StandardRuleViolationsManager">
+    </bean>
+
     <!-- flow controller -->
     <bean id="flowController" class="org.apache.nifi.spring.FlowControllerFactoryBean">
         <property name="properties" ref="nifiProperties"/>
@@ -51,6 +54,7 @@
         <property name="leaderElectionManager" ref="leaderElectionManager" />
         <property name="extensionManager" ref="extensionManager" />
         <property name="revisionManager" ref="revisionManager" />
+        <property name="ruleViolationsManager" ref="ruleViolationsManager" />
         <property name="statusHistoryRepository" ref="statusHistoryRepository" />
     </bean>
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
index 74bf1c5504..a07727a489 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
@@ -92,7 +92,7 @@ public class StandardFlowServiceTest {
         revisionManager = mock(RevisionManager.class);
         extensionManager = mock(ExtensionDiscoveringManager.class);
         flowController = FlowController.createStandaloneInstance(mockFlowFileEventRepository, properties, authorizer, mockAuditService, mockEncryptor,
-                                        new VolatileBulletinRepository(), variableRegistry, extensionManager, statusHistoryRepository);
+                                        new VolatileBulletinRepository(), variableRegistry, extensionManager, statusHistoryRepository, null);
         flowService = StandardFlowService.createStandaloneInstance(flowController, properties, revisionManager, authorizer,
                 FlowSerializationStrategy.WRITE_XML_AND_JSON);
         statusHistoryRepository = mock(StatusHistoryRepository.class);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardProcessorNodeIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardProcessorNodeIT.java
index 7b110c7bba..09c19fa8ec 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardProcessorNodeIT.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardProcessorNodeIT.java
@@ -30,6 +30,8 @@ import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.components.state.StateManagerProvider;
 import org.apache.nifi.controller.exception.ControllerServiceInstantiationException;
 import org.apache.nifi.controller.exception.ProcessorInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisRuleInstantiationException;
+import org.apache.nifi.controller.flowanalysis.FlowAnalyzer;
 import org.apache.nifi.controller.kerberos.KerberosConfig;
 import org.apache.nifi.controller.repository.FlowFileEventRepository;
 import org.apache.nifi.controller.scheduling.LifecycleState;
@@ -47,6 +49,7 @@ import org.apache.nifi.nar.NarClassLoader;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
 import org.apache.nifi.nar.SystemBundle;
+import org.apache.nifi.parameter.ParameterContext;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.ProcessContext;
 import org.apache.nifi.processor.ProcessSession;
@@ -70,6 +73,7 @@ import org.apache.nifi.util.MockPropertyValue;
 import org.apache.nifi.util.MockVariableRegistry;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.SynchronousValidationTrigger;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
@@ -218,7 +222,7 @@ public class StandardProcessorNodeIT {
         final FlowController flowController = FlowController.createStandaloneInstance(mock(FlowFileEventRepository.class), nifiProperties,
             mock(Authorizer.class), mock(AuditService.class), null, new VolatileBulletinRepository(),
             new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths()),
-                extensionManager, mock(StatusHistoryRepository.class));
+                extensionManager, mock(StatusHistoryRepository.class), null);
 
         // Init processor
         final PropertyDescriptor classpathProp = new PropertyDescriptor.Builder().name("Classpath Resources")
@@ -270,7 +274,7 @@ public class StandardProcessorNodeIT {
         final FlowController flowController = FlowController.createStandaloneInstance(mock(FlowFileEventRepository.class), nifiProperties,
                 mock(Authorizer.class), mock(AuditService.class), null, new VolatileBulletinRepository(),
                 new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths()),
-                extensionManager, mock(StatusHistoryRepository.class));
+                extensionManager, mock(StatusHistoryRepository.class), null);
 
         // Init processor
         final DynamicPropertiesTestProcessor processor = new DynamicPropertiesTestProcessor();
@@ -598,6 +602,11 @@ public class StandardProcessorNodeIT {
             reload(newType, additionalUrls);
         }
 
+        @Override
+        public void reload(FlowAnalysisRuleNode existingNode, String newType, BundleCoordinate bundleCoordinate, Set<URL> additionalUrls) throws FlowAnalysisRuleInstantiationException {
+            reload(newType, additionalUrls);
+        }
+
         @Override
         public void reload(ParameterProviderNode existingNode, String newType, BundleCoordinate bundleCoordinate, Set<URL> additionalUrls) {
             reload(newType, additionalUrls);
@@ -635,96 +644,111 @@ public class StandardProcessorNodeIT {
 
 
     private ValidationContextFactory createValidationContextFactory() {
-        return (properties, annotationData, groupId, componentId, context, validateConnections) -> new ValidationContext() {
-
-            @Override
-            public ControllerServiceLookup getControllerServiceLookup() {
-                return null;
-            }
-
+        return new ValidationContextFactory() {
             @Override
-            public ValidationContext getControllerServiceValidationContext(ControllerService controllerService) {
-                return null;
+            public ValidationContext newValidationContext(Map<PropertyDescriptor, PropertyConfiguration> properties, String annotationData, String groupId, String componentId,
+                                                          ParameterContext context, boolean validateConnections) {
+                return new ValidationContext() {
+
+                    @Override
+                    public ControllerServiceLookup getControllerServiceLookup() {
+                        return null;
+                    }
+
+                    @Override
+                    public ValidationContext getControllerServiceValidationContext(ControllerService controllerService) {
+                        return null;
+                    }
+
+                    @Override
+                    public ExpressionLanguageCompiler newExpressionLanguageCompiler() {
+                        return null;
+                    }
+
+                    @Override
+                    public PropertyValue getProperty(PropertyDescriptor property) {
+                        final PropertyConfiguration configuration = properties.get(property);
+                        return newPropertyValue(configuration == null ? null : configuration.getRawValue());
+                    }
+
+                    @Override
+                    public PropertyValue newPropertyValue(String value) {
+                        return new MockPropertyValue(value);
+                    }
+
+                    @Override
+                    public Map<PropertyDescriptor, String> getProperties() {
+                        final Map<PropertyDescriptor, String> propertyMap = new HashMap<>();
+                        properties.forEach((k, v) -> propertyMap.put(k, v == null ? null : v.getRawValue()));
+                        return propertyMap;
+                    }
+
+                    @Override
+                    public Map<String, String> getAllProperties() {
+                        final Map<String, String> propValueMap = new LinkedHashMap<>();
+                        for (final Map.Entry<PropertyDescriptor, String> entry : getProperties().entrySet()) {
+                            propValueMap.put(entry.getKey().getName(), entry.getValue());
+                        }
+                        return propValueMap;
+                    }
+
+                    @Override
+                    public String getAnnotationData() {
+                        return null;
+                    }
+
+                    @Override
+                    public boolean isValidationRequired(ControllerService service) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isExpressionLanguagePresent(String value) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isExpressionLanguageSupported(String propertyName) {
+                        return false;
+                    }
+
+                    @Override
+                    public String getProcessGroupIdentifier() {
+                        return groupId;
+                    }
+
+                    @Override
+                    public Collection<String> getReferencedParameters(final String propertyName) {
+                        return Collections.emptyList();
+                    }
+
+                    @Override
+                    public boolean isParameterDefined(final String parameterName) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isParameterSet(final String parameterName) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isDependencySatisfied(final PropertyDescriptor propertyDescriptor, final Function<String, PropertyDescriptor> propertyDescriptorLookup) {
+                        return false;
+                    }
+                };
             }
 
             @Override
-            public ExpressionLanguageCompiler newExpressionLanguageCompiler() {
+            public RuleViolationsManager getRuleViolationsManager() {
                 return null;
             }
 
             @Override
-            public PropertyValue getProperty(PropertyDescriptor property) {
-                final PropertyConfiguration configuration = properties.get(property);
-                return newPropertyValue(configuration == null ? null : configuration.getRawValue());
-            }
-
-            @Override
-            public PropertyValue newPropertyValue(String value) {
-                return new MockPropertyValue(value);
-            }
-
-            @Override
-            public Map<PropertyDescriptor, String> getProperties() {
-                final Map<PropertyDescriptor, String> propertyMap = new HashMap<>();
-                properties.forEach((k, v) -> propertyMap.put(k, v == null ? null : v.getRawValue()));
-                return propertyMap;
-            }
-
-            @Override
-            public Map<String, String> getAllProperties() {
-                final Map<String,String> propValueMap = new LinkedHashMap<>();
-                for (final Map.Entry<PropertyDescriptor, String> entry : getProperties().entrySet()) {
-                    propValueMap.put(entry.getKey().getName(), entry.getValue());
-                }
-                return propValueMap;
-            }
-
-            @Override
-            public String getAnnotationData() {
+            public FlowAnalyzer getFlowAnalyzer() {
                 return null;
             }
-
-            @Override
-            public boolean isValidationRequired(ControllerService service) {
-                return false;
-            }
-
-            @Override
-            public boolean isExpressionLanguagePresent(String value) {
-                return false;
-            }
-
-            @Override
-            public boolean isExpressionLanguageSupported(String propertyName) {
-                return false;
-            }
-
-            @Override
-            public String getProcessGroupIdentifier() {
-                return groupId;
-            }
-
-            @Override
-            public Collection<String> getReferencedParameters(final String propertyName) {
-                return Collections.emptyList();
-            }
-
-            @Override
-            public boolean isParameterDefined(final String parameterName) {
-                return false;
-            }
-
-            @Override
-            public boolean isParameterSet(final String parameterName) {
-                return false;
-            }
-
-            @Override
-            public boolean isDependencySatisfied(final PropertyDescriptor propertyDescriptor, final Function<String, PropertyDescriptor> propertyDescriptorLookup) {
-                return false;
-            }
         };
-
     }
 
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
index efd126f771..b20d40b817 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
@@ -79,6 +79,7 @@ import org.apache.nifi.scheduling.ExecutionNode;
 import org.apache.nifi.scheduling.SchedulingStrategy;
 import org.apache.nifi.services.FlowService;
 import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.validation.RuleViolationsManager;
 import org.apache.nifi.web.api.dto.BundleDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
 import org.apache.nifi.web.api.dto.FlowSnippetDTO;
@@ -212,7 +213,8 @@ public class TestFlowController {
 
         bulletinRepo = mock(BulletinRepository.class);
         controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
-                auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository);
+                auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository,
+                mock(RuleViolationsManager.class));
 
         final XmlFlowSynchronizer xmlFlowSynchronizer = new XmlFlowSynchronizer(nifiProperties, extensionManager);
         final VersionedFlowSynchronizer versionedFlowSynchronizer = new VersionedFlowSynchronizer(extensionManager,
@@ -589,7 +591,7 @@ public class TestFlowController {
 
         controller.shutdown(true);
         controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
-                auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository);
+                auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository, null);
         controller.synchronize(standardFlowSynchronizer, proposedDataFlow, mock(FlowService.class), BundleUpdateStrategy.IGNORE_BUNDLE);
         assertEquals(authFingerprint, authorizer.getFingerprint());
     }
@@ -1376,6 +1378,7 @@ public class TestFlowController {
         versionedDataflow.setParameterContexts(Collections.emptyList());
         versionedDataflow.setControllerServices(Collections.emptyList());
         versionedDataflow.setReportingTasks(Collections.emptyList());
+        versionedDataflow.setFlowAnalysisRules(Collections.emptyList());
         versionedDataflow.setTemplates(Collections.emptySet());
 
         final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
index 9a7f03570f..0e252c36ac 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
@@ -129,7 +129,7 @@ public class TestStandardReportingContext {
 
         bulletinRepo = Mockito.mock(BulletinRepository.class);
         controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor,
-                bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository);
+                bulletinRepo, variableRegistry, extensionManager, statusHistoryRepository, null);
     }
 
     @AfterEach
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/ProcessorLifecycleIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/ProcessorLifecycleIT.java
index 274bc771aa..10d2278cf8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/ProcessorLifecycleIT.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/ProcessorLifecycleIT.java
@@ -560,7 +560,7 @@ public class ProcessorLifecycleIT {
         final FlowController flowController = FlowController.createStandaloneInstance(mock(FlowFileEventRepository.class), nifiProperties,
                 mock(Authorizer.class), mock(AuditService.class), null, new VolatileBulletinRepository(),
                 new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths()),
-                extensionManager, mock(StatusHistoryRepository.class));
+                extensionManager, mock(StatusHistoryRepository.class), null);
 
         final FlowManager flowManager = flowController.getFlowManager();
         this.processScheduler = flowController.getProcessScheduler();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
index e8a89dbb25..023e49fa66 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
@@ -99,7 +99,7 @@ public class StandardFlowSerializerTest {
 
         final BulletinRepository bulletinRepo = Mockito.mock(BulletinRepository.class);
         controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
-            auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, Mockito.mock(StatusHistoryRepository.class));
+            auditService, encryptor, bulletinRepo, variableRegistry, extensionManager, Mockito.mock(StatusHistoryRepository.class), null);
 
         serializer = new StandardFlowSerializer();
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisIT.java
new file mode 100644
index 0000000000..f1432604ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisIT.java
@@ -0,0 +1,126 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.flowanalysis.FlowAnalysisUtil;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.integration.FrameworkIntegrationTest;
+import org.apache.nifi.integration.flowanalysis.DelegateFlowAnalysisRule;
+import org.apache.nifi.integration.processors.NopProcessor;
+import org.apache.nifi.nar.SystemBundle;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
+
+public abstract class AbstractFlowAnalysisIT extends FrameworkIntegrationTest {
+    protected NiFiRegistryFlowMapper mapper;
+
+    @BeforeEach()
+    public void setUpAbstractFlowAnalysisIT() throws Exception {
+        mapper = FlowAnalysisUtil.createMapper(getFlowController().getExtensionManager());
+    }
+
+    protected ProcessGroup createProcessGroup(ProcessGroup parent) {
+        String id = UUID.randomUUID().toString();
+
+        ProcessGroup processGroup = getFlowController().getFlowManager().createProcessGroup(id);
+
+        processGroup.setName(id);
+        processGroup.setParent(parent);
+
+        parent.addProcessGroup(processGroup);
+
+        return processGroup;
+    }
+
+    protected ProcessorNode createProcessorNode(ProcessGroup processGroup) {
+        ProcessorNode processorNode = getFlowController().getFlowManager().createProcessor(
+            NopProcessor.class.getSimpleName(),
+            UUID.randomUUID().toString(),
+            SystemBundle.SYSTEM_BUNDLE_COORDINATE,
+            Collections.emptySet(),
+            true,
+            true,
+            null
+        );
+
+        processGroup.addProcessor(processorNode);
+
+        return processorNode;
+    }
+
+    protected Connection createConnection(
+            ProcessGroup processGroup,
+            String name,
+            Connectable source,
+            Connectable destination,
+            Collection<String> relationshipNames
+    ) {
+        Connection connection = getFlowController().getFlowManager().createConnection(
+            UUID.randomUUID().toString(),
+            name,
+            source,
+            destination,
+            relationshipNames
+        );
+
+        processGroup.addConnection(connection);
+
+        return connection;
+    }
+
+    protected VersionedProcessGroup mapProcessGroup(ProcessGroup processGroup) {
+        return mapper.mapProcessGroup(
+            processGroup,
+            getFlowController().getControllerServiceProvider(),
+            getFlowController().getFlowManager(),
+            true
+        );
+    }
+
+    protected FlowAnalysisRuleNode createAndEnableFlowAnalysisRuleNode(FlowAnalysisRule flowAnalysisRule) {
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createFlowAnalysisRuleNode(flowAnalysisRule);
+
+        flowAnalysisRuleNode.enable();
+
+        return flowAnalysisRuleNode;
+    }
+
+    protected FlowAnalysisRuleNode createFlowAnalysisRuleNode(FlowAnalysisRule flowAnalysisRule) {
+        final FlowAnalysisRuleNode flowAnalysisRuleNode = getFlowController().getFlowManager().createFlowAnalysisRule(
+            DelegateFlowAnalysisRule.class.getName(),
+            UUID.randomUUID().toString(),
+            SystemBundle.SYSTEM_BUNDLE_COORDINATE,
+            Collections.emptySet(),
+            true,
+            true,
+            null
+        );
+
+        ((DelegateFlowAnalysisRule) flowAnalysisRuleNode.getFlowAnalysisRule()).setDelegate(flowAnalysisRule);
+
+        return flowAnalysisRuleNode;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleLifeCycleIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleLifeCycleIT.java
new file mode 100644
index 0000000000..6c1cab7d0e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleLifeCycleIT.java
@@ -0,0 +1,197 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.scheduling.TestStandardProcessScheduler;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.nar.SystemBundle;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class FlowAnalysisRuleLifeCycleIT extends AbstractFlowAnalysisIT {
+    @Test
+    public void testCreateRules() throws Exception {
+        // GIVEN
+        FlowAnalysisRuleNode rule1 = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+        FlowAnalysisRuleNode rule2 = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        Set<FlowAnalysisRuleNode> expected = new HashSet<FlowAnalysisRuleNode>() {{
+            add(rule1);
+            add(rule2);
+        }};
+
+        // WHEN
+        Set<FlowAnalysisRuleNode> actual = getFlowController().getFlowManager().getAllFlowAnalysisRules();
+
+        // THEN
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testCannotDeleteEnabledRule() throws Exception {
+        // GIVEN
+        FlowAnalysisRuleNode rule = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        // WHEN
+        try {
+            getFlowController().getFlowManager().removeFlowAnalysisRule(rule);
+            fail();
+        } catch (IllegalStateException e) {
+            // THEN
+            assertEquals(
+                "Cannot delete " + rule.getIdentifier() + " because it is enabled",
+                e.getMessage()
+            );
+        }
+    }
+
+    @Test
+    public void testDeleteRule() throws Exception {
+        // GIVEN
+        FlowAnalysisRuleNode rule1 = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+        FlowAnalysisRuleNode rule2 = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        Set<FlowAnalysisRuleNode> expected = new HashSet<FlowAnalysisRuleNode>() {{
+            add(rule2);
+        }};
+
+        // WHEN
+        rule1.disable();
+        getFlowController().getFlowManager().removeFlowAnalysisRule(rule1);
+        Set<FlowAnalysisRuleNode> actual = getFlowController().getFlowManager().getAllFlowAnalysisRules();
+
+        // THEN
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testCannotEnableEnabledRule() throws Exception {
+        // GIVEN
+        FlowAnalysisRuleNode rule = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        // WHEN
+        try {
+            rule.enable();
+            fail();
+        } catch (IllegalStateException e) {
+            // THEN
+            assertEquals(
+                "Cannot enable " + rule.getIdentifier() + " because it is not disabled",
+                e.getMessage()
+            );
+        }
+    }
+
+    @Test
+    public void testCannotDisableDisabledRule() throws Exception {
+        // GIVEN
+        FlowAnalysisRuleNode rule = createFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        // WHEN
+        try {
+            rule.disable();
+            fail();
+        } catch (IllegalStateException e) {
+            // THEN
+            assertEquals(
+                "Cannot disable " + rule.getIdentifier() + " because it is already disabled",
+                e.getMessage()
+            );
+        }
+    }
+
+    @Test
+    public void testEnableAndDisableServiceEnablesAndDisablesReferencingRule() throws Exception {
+        // GIVEN
+        PropertyDescriptor controllerServiceReferencingPropertyDescriptor = new PropertyDescriptor.Builder()
+            .name("controllerService")
+            .identifiesControllerService(TestStandardProcessScheduler.SimpleTestService.class)
+            .required(true)
+            .build();
+
+        FlowAnalysisRuleNode rule = createFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+            @Override
+            protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+                List<PropertyDescriptor> propertyDescriptors = new ArrayList<>();
+                propertyDescriptors.add(controllerServiceReferencingPropertyDescriptor);
+
+                return propertyDescriptors;
+            }
+        });
+
+        final ControllerServiceNode service = getFlowController().getFlowManager().createControllerService(
+            TestStandardProcessScheduler.SimpleTestService.class.getName(),
+            UUID.randomUUID().toString(),
+            SystemBundle.SYSTEM_BUNDLE_COORDINATE,
+            Collections.emptySet(),
+            true,
+            true,
+            null
+        );
+        getFlowController().getFlowManager().addRootControllerService(service);
+        service.resetValidationState();
+
+        Map<String, String> ruleProperties = new HashMap<>();
+        ruleProperties.put(controllerServiceReferencingPropertyDescriptor.getName(), service.getIdentifier());
+        rule.setProperties(ruleProperties);
+
+        // WHEN
+        // THEN
+        assertFalse(rule.isEnabled());
+
+        getFlowController().getControllerServiceProvider().enableControllerService(service).get();
+        service.awaitEnabled(5, TimeUnit.SECONDS);
+        rule.resetValidationState();
+        getFlowController().getControllerServiceProvider().scheduleReferencingComponents(service);
+
+        assertTrue(rule.isEnabled());
+
+        getFlowController().getControllerServiceProvider().disableControllerService(service).get();
+        rule.resetValidationState();
+        Map<ComponentNode, Future<Void>> futureMap = getFlowController().getControllerServiceProvider().unscheduleReferencingComponents(service);
+        for (Future<Void> future : futureMap.values()) {
+            future.get();
+        }
+
+        assertFalse(rule.isEnabled());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalyzerIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalyzerIT.java
new file mode 100644
index 0000000000..10f1cb93ba
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/flowanalysis/FlowAnalyzerIT.java
@@ -0,0 +1,1437 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.FlowAnalysisRuleNode;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.integration.cs.CounterControllerService;
+import org.apache.nifi.validation.RuleViolation;
+import org.apache.nifi.validation.RuleViolationsManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FlowAnalyzerIT extends AbstractFlowAnalysisIT {
+    public static final String EXPLANATION_PREFIX = "explanation_";
+
+    private ExecutorService executorService;
+    private StandardFlowAnalyzer standardFlowAnalyzer;
+
+    private AtomicReference<CountDownLatch> startRunAnalysis = new AtomicReference<>();
+    private AtomicReference<CountDownLatch> finishAnalysis = new AtomicReference<>();
+
+    private AtomicReference<RuntimeException> groupAnalysisError = new AtomicReference<>();
+
+    @BeforeEach
+    public void setUpFlowAnalyzerIT() throws Exception {
+        // By default, we don't wait
+        startRunAnalysis.set(new CountDownLatch(0));
+        finishAnalysis.set(new CountDownLatch(0));
+
+        executorService = Executors.newSingleThreadExecutor();
+        standardFlowAnalyzer = new StandardFlowAnalyzer(
+                getFlowController().getFlowManager().getRuleViolationsManager(),
+                getFlowController(),
+                getExtensionManager()
+        ) {
+            @Override
+            public void analyzeProcessGroup(VersionedProcessGroup processGroup) {
+                if (groupAnalysisError.get() != null) {
+                    throw groupAnalysisError.get();
+                }
+
+                super.analyzeProcessGroup(processGroup);
+
+                try {
+                    finishAnalysis.get().await();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+
+            }
+        };
+        standardFlowAnalyzer.initialize(getFlowController().getControllerServiceProvider());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        executorService.shutdown();
+    }
+
+    @Test
+    public void testAnalyzeProcessorNoRule() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        Collection<RuleViolation> expected = new HashSet<>();
+
+        // WHEN
+        // THEN
+        testAnalyzeProcessor(processorNode, expected);
+
+    }
+
+    @Test
+    public void testAnalyzeProcessorNoViolation() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+        });
+
+        Collection<RuleViolation> expected = new HashSet<>();
+
+        // WHEN
+        // THEN
+        testAnalyzeProcessor(processorNode, expected);
+    }
+
+    @Test
+    public void testAnalyzeProcessorDisableRuleBeforeAnalysis() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createAndEnableFlowAnalysisRuleNode(
+                analyzingComponent("disappearing_issueId", "Violation removed when rule is disabled")
+        );
+        flowAnalysisRuleNode.disable();
+
+        Collection<RuleViolation> expected = new HashSet<>();
+
+        // WHEN
+        // THEN
+        testAnalyzeProcessor(processorNode, expected);
+    }
+
+    @Test
+    public void testAnalyzeProcessorProduceViolation() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        String issueId = "issueId";
+        String violationMessage = "Violation";
+
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createAndEnableFlowAnalysisRuleNode(
+                analyzingComponent(issueId, violationMessage)
+        );
+
+        Collection<RuleViolation> expected = new HashSet<>(Arrays.asList(
+                createRuleViolation(
+                        issueId,
+                        violationMessage,
+                        flowAnalysisRuleNode,
+                        processorNode.getIdentifier(),
+                        processorNode.getIdentifier(),
+                        processorNode.getName(),
+                        processorNode.getProcessGroupIdentifier()
+                )
+        ));
+
+        // WHEN
+        // THEN
+        testAnalyzeProcessor(processorNode, expected);
+    }
+
+    @Test
+    public void testAnalyzeProcessorProduceMultipleViolations() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        String issueId1 = "issueId1";
+        String violationMessage1 = "Violation 1";
+
+        String issueId2 = "issueId2";
+        String violationMessage2 = "Violation 2";
+
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createAndEnableFlowAnalysisRuleNode(
+                analyzingComponent(new HashMap<String, String>() {{
+                    put(issueId1, violationMessage1);
+                    put(issueId2, violationMessage2);
+                }})
+        );
+
+        Collection<RuleViolation> expected = new HashSet<>(Arrays.asList(
+                createRuleViolation(
+                        issueId1,
+                        violationMessage1,
+                        flowAnalysisRuleNode,
+                        processorNode.getIdentifier(),
+                        processorNode.getIdentifier(),
+                        processorNode.getName(),
+                        processorNode.getProcessGroupIdentifier()
+                ),
+                createRuleViolation(
+                        issueId2,
+                        violationMessage2,
+                        flowAnalysisRuleNode,
+                        processorNode.getIdentifier(),
+                        processorNode.getIdentifier(),
+                        processorNode.getName(),
+                        processorNode.getProcessGroupIdentifier()
+                )
+        ));
+
+        // WHEN
+        // THEN
+        testAnalyzeProcessor(processorNode, expected);
+    }
+
+    @Test
+    public void testAnalyzeProcessorThenAnalyzeAgainWithDifferentResult() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        String issueId = "issueId";
+        String violationMessage1 = "Previous violation gets overwritten";
+        String violationMessage2 = "New violation";
+
+        AtomicReference<String> violationMessageHolder = new AtomicReference<>(violationMessage1);
+
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+            @Override
+            public Collection<ComponentAnalysisResult> analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) {
+                ComponentAnalysisResult result = new ComponentAnalysisResult(
+                        issueId,
+                        violationMessageHolder.get(),
+                        EXPLANATION_PREFIX + violationMessageHolder.get()
+                );
+
+                return Collections.singleton(result);
+            }
+        });
+
+        Collection<RuleViolation> expected = new HashSet<>(Arrays.asList(
+                createRuleViolation(
+                        issueId,
+                        violationMessage2,
+                        flowAnalysisRuleNode,
+                        processorNode.getIdentifier(),
+                        processorNode.getIdentifier(),
+                        processorNode.getName(),
+                        processorNode.getProcessGroupIdentifier()
+                )
+        ));
+
+        // WHEN
+        standardFlowAnalyzer.analyzeProcessor(processorNode);
+        violationMessageHolder.set(violationMessage2);
+        standardFlowAnalyzer.analyzeProcessor(processorNode);
+
+        // THEN
+        checkActualViolations(expected);
+    }
+
+
+    @Test
+    public void testAnalyzeProcessorProduceViolationFirstNoViolationSecond() throws Exception {
+        // GIVEN
+        ProcessorNode processorNode = createProcessorNode((context, session) -> {
+        });
+
+        String issueId = "disappearing_issueId";
+        String violationMessage = "Violation removed when second analysis doesn't reproduce it";
+
+        AtomicReference<String> violationMessageHolder = new AtomicReference<>(violationMessage);
+
+        FlowAnalysisRuleNode flowAnalysisRuleNode = createAndEnableFlowAnalysisRuleNode(new AbstractFlowAnalysisRule() {
+            @Override
+            public Collection<ComponentAnalysisResult> analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) {
+                String violationMessage = violationMessageHolder.get();
+
+                if (violationMessage != null) {
+                    ComponentAnalysisResult result = new ComponentAnalysisResult(
+                            issueId,
+                            violationMessageHolder.get()
+                    );
+
+                    return Collections.singleton(result);
+                } else {
+                    return Collections.emptySet();
+                }
+            }
+        });
+
+        Collection<RuleViolation> expected = new HashSet<>();
+
+        // WHEN
+        standardFlowAnalyzer.analyzeProcessor(processorNode);
+        violationMessageHolder.set(null);
+        standardFlowAnalyzer.analyzeProcessor(processorNode);
... 9017 lines suppressed ...