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

pvillard 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 a13e6c66e8 NIFI-14381: Add flow analysis rule to identify unset SSL 
Context Service controller services
a13e6c66e8 is described below

commit a13e6c66e849285b72d1fbfed0c1962455d528fe
Author: Matt Burgess <[email protected]>
AuthorDate: Thu Mar 20 19:15:37 2025 -0400

    NIFI-14381: Add flow analysis rule to identify unset SSL Context Service 
controller services
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #9813.
---
 .../nifi-standard-rules/pom.xml                    |  8 ++-
 .../rules/RequireServerSSLContextService.java      | 84 ++++++++++++++++++++++
 .../org.apache.nifi.flowanalysis.FlowAnalysisRule  |  1 +
 .../additionalDetails.md                           | 27 +++++++
 .../rules/AbstractFlowAnalaysisRuleTest.java       | 25 +++++++
 .../rules/RequireServerSSLContextServiceTest.java  | 60 ++++++++++++++++
 .../RequireServerSSLContextService.json            |  1 +
 ...RequireServerSSLContextService_noViolation.json |  1 +
 8 files changed, 206 insertions(+), 1 deletion(-)

diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/pom.xml 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/pom.xml
index dbebfb42e9..ff47668e79 100644
--- a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/pom.xml
+++ b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/pom.xml
@@ -23,7 +23,13 @@
     <artifactId>nifi-standard-rules</artifactId>
     <packaging>jar</packaging>
 
-    <dependencies />
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-ssl-context-service-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
     
     <build>
         <plugins>
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextService.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextService.java
new file mode 100644
index 0000000000..68588b2f2c
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextService.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.flowanalysis.rules;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.documentation.UseCase;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedConfigurableExtension;
+import org.apache.nifi.flowanalysis.AbstractFlowAnalysisRule;
+import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
+import org.apache.nifi.util.StringUtils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+
+@Tags({"component", "processor", "controller service", "type", "ssl", "tls", 
"listen"})
+@CapabilityDescription("Produces rule violations for each component (i.e. 
processors or controller services) having a property "
+        + "identifying an SSLContextService that is not set.")
+@UseCase(
+        description = "Ensure that an SSL Context has been configured for the 
specified components. This helps avoid ports being opened for insecure 
(plaintext, e.g.) communications.",
+        configuration = """
+                To avoid the violation, ensure that the "SSL Context Service" 
property is set for the specified component(s).
+                """
+)
+public class RequireServerSSLContextService extends AbstractFlowAnalysisRule {
+
+    private final static List<String> COMPONENT_TYPES = List.of(
+            "org.apache.nifi.processors.standard.ListenFTP",
+            "org.apache.nifi.processors.standard.ListenHTTP",
+            "org.apache.nifi.processors.standard.ListenTCP",
+            "org.apache.nifi.processors.standard.ListenSyslog",
+            "org.apache.nifi.processors.standard.HandleHttpRequest",
+            "org.apache.nifi.websocket.jetty.JettyWebSocketServer"
+    );
+
+    @Override
+    public Collection<ComponentAnalysisResult> 
analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) 
{
+        Collection<ComponentAnalysisResult> results = new HashSet<>();
+
+        if (component instanceof VersionedConfigurableExtension 
versionedConfigurableExtension) {
+
+            String encounteredComponentType = 
versionedConfigurableExtension.getType();
+
+            if (COMPONENT_TYPES.contains(encounteredComponentType)) {
+                // Loop over the properties for this component looking for an 
SSLContextService
+                
versionedConfigurableExtension.getProperties().forEach((propertyName, 
propertyValue) -> {
+
+                    // If the SSL Context property exists and the value is not 
set, report a violation
+                    if (("SSL Context Service".equalsIgnoreCase(propertyName) 
|| "ssl-context-service".equalsIgnoreCase(propertyName))
+                            && StringUtils.isEmpty(propertyValue)) {
+
+                        String encounteredSimpleComponentType = 
encounteredComponentType.substring(encounteredComponentType.lastIndexOf(".") + 
1);
+                        ComponentAnalysisResult result = new 
ComponentAnalysisResult(
+                                component.getInstanceIdentifier(),
+                                "'" + encounteredSimpleComponentType + "' must 
specify an SSL Context Service"
+                        );
+
+                        results.add(result);
+
+                    }
+                });
+            }
+        }
+        return results;
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/META-INF/services/org.apache.nifi.flowanalysis.FlowAnalysisRule
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/META-INF/services/org.apache.nifi.flowanalysis.FlowAnalysisRule
index f5f1df7eb4..165b44830c 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/META-INF/services/org.apache.nifi.flowanalysis.FlowAnalysisRule
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/META-INF/services/org.apache.nifi.flowanalysis.FlowAnalysisRule
@@ -14,5 +14,6 @@
 # limitations under the License.
 
 org.apache.nifi.flowanalysis.rules.DisallowComponentType
+org.apache.nifi.flowanalysis.rules.RequireServerSSLContextService
 org.apache.nifi.flowanalysis.rules.RestrictBackpressureSettings
 org.apache.nifi.flowanalysis.rules.RestrictFlowFileExpiration
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/docs/org.apache.nifi.flowanalysis.rules.RequireCustomSSLContext/additionalDetails.md
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/docs/org.apache.nifi.flowanalysis.rules.RequireCustomSSLContext/additionalDetails.md
new file mode 100644
index 0000000000..56be33fab7
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/docs/org.apache.nifi.flowanalysis.rules.RequireCustomSSLContext/additionalDetails.md
@@ -0,0 +1,27 @@
+<!--
+  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.
+-->
+
+# RequireCustomSSLContext
+
+## Usage Information
+
+This rule should be applied to prevent flow designers from opening ports for 
insecure connections. The list of components to which this rule applies is as 
follows:
+
+- **ListenFTP**
+- **ListenHTTP**
+- **ListenTCP**
+- **ListenSyslog**
+- **HandleHttpRequest**
+- **JettyWebSocketServer**
\ No newline at end of file
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/AbstractFlowAnalaysisRuleTest.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/AbstractFlowAnalaysisRuleTest.java
index 04b2d38acb..25b1f087c4 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/AbstractFlowAnalaysisRuleTest.java
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/AbstractFlowAnalaysisRuleTest.java
@@ -23,7 +23,9 @@ import org.apache.nifi.components.PropertyValue;
 import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
 import org.apache.nifi.flowanalysis.AbstractFlowAnalysisRule;
+import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
 import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
 import org.apache.nifi.flowanalysis.GroupAnalysisResult;
 import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
@@ -39,6 +41,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 import static org.mockito.ArgumentMatchers.any;
 
@@ -62,6 +65,7 @@ public abstract class AbstractFlowAnalaysisRuleTest<T extends 
AbstractFlowAnalys
 
     @BeforeEach
     public void setup() {
+        properties.clear();
         rule = initializeRule();
         
Mockito.lenient().when(flowAnalysisRuleContext.getProperty(any())).thenAnswer(invocation
 -> {
             return properties.get(invocation.getArgument(0));
@@ -72,6 +76,9 @@ public abstract class AbstractFlowAnalaysisRuleTest<T extends 
AbstractFlowAnalys
         
Mockito.lenient().when(validationContext.getProperty(any())).thenAnswer(invocation
 -> {
             return properties.get(invocation.getArgument(0));
         });
+        
Mockito.lenient().when(flowAnalysisRuleContext.getProperties()).thenAnswer(invocation
 -> {
+            return properties;
+        });
     }
 
     protected void setProperty(PropertyDescriptor propertyDescriptor, String 
value) {
@@ -87,4 +94,22 @@ public abstract class AbstractFlowAnalaysisRuleTest<T 
extends AbstractFlowAnalys
         final Collection<GroupAnalysisResult> actual = 
rule.analyzeProcessGroup(getProcessGroup(flowDefinition), 
flowAnalysisRuleContext);
         assertIterableEquals(expected, actual.stream().map(r -> 
r.getComponent().get().getInstanceIdentifier()).sorted().toList());
     }
+
+
+    protected void testAnalyzeProcessors(String flowDefinition, 
List<ComponentAnalysisResult> expected) throws Exception {
+        VersionedProcessGroup rootPG = getProcessGroup(flowDefinition);
+        for (VersionedProcessor processor : rootPG.getProcessors()) {
+            final Collection<ComponentAnalysisResult> actual = 
rule.analyzeComponent(processor, flowAnalysisRuleContext);
+            final int expectedSize = (expected == null) ? 0 : expected.size();
+            final int actualSize = (actual == null) ? 0 : actual.size();
+            assertEquals(expectedSize, actualSize);
+            if (actualSize > 0) {
+                final ComponentAnalysisResult[] actualArray = 
actual.toArray(new ComponentAnalysisResult[actualSize]);
+
+                for (int i = 0; i < expectedSize; i++) {
+                    assertEquals(expected.get(i).getIssueId(), 
actualArray[i].getIssueId());
+                }
+            }
+        }
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextServiceTest.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextServiceTest.java
new file mode 100644
index 0000000000..ebe6176659
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/RequireServerSSLContextServiceTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.rules;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
+import org.apache.nifi.ssl.SSLContextProvider;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+public class RequireServerSSLContextServiceTest extends 
AbstractFlowAnalaysisRuleTest<RequireServerSSLContextService> {
+
+    public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new 
PropertyDescriptor.Builder()
+            .name("SSL Context Service")
+            .description("SSL Context Service enables support for HTTPS")
+            .required(false)
+            .identifiesControllerService(SSLContextProvider.class)
+            .build();
+    @Override
+    protected RequireServerSSLContextService initializeRule() {
+        return new RequireServerSSLContextService();
+    }
+
+    @Test
+    public void testNoViolations() throws Exception {
+        setProperty(SSL_CONTEXT_SERVICE, 
"9c50e433-c2aa-3d19-aae6-20299f4ac38c");
+        testAnalyzeProcessors(
+                
"src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService_noViolation.json",
+                List.of()
+        );
+    }
+
+    @Test
+    public void testViolations() throws Exception {
+        setProperty(SSL_CONTEXT_SERVICE, null);
+
+        ComponentAnalysisResult expectedResult = new 
ComponentAnalysisResult("b5734be4-0195-1000-0e75-bc0f150d06bd", "'ListenHTTP' 
is not allowed");
+        testAnalyzeProcessors(
+                
"src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService.json",
+                List.of(
+                        expectedResult  // processor ListenHttp with no 
SSLContextService set
+                )
+        );
+    }
+}
\ No newline at end of file
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService.json
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService.json
new file mode 100644
index 0000000000..2ede157d3a
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService.json
@@ -0,0 +1 @@
+{"flowContents":{"identifier":"5d78473b-43d3-33b6-beed-83d2ae71a73d","instanceIdentifier":"b56b0714-0195-1000-2a87-e8aa1ca3849f","name":"NiFi
 
Flow","comments":"","position":{"x":0.0,"y":0.0},"processGroups":[],"remoteProcessGroups":[],"processors":[{"identifier":"da98e020-2712-38fa-b2c9-88e0d3bb25b8","instanceIdentifier":"b5734be4-0195-1000-0e75-bc0f150d06bd","name":"ListenHTTP","comments":"","position":{"x":-51.5,"y":-241.0},"type":"org.apache.nifi.processors.standard.ListenHTTP","bundl
 [...]
\ No newline at end of file
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService_noViolation.json
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService_noViolation.json
new file mode 100644
index 0000000000..98c1aba573
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/resources/RequireServerSSLContextService/RequireServerSSLContextService_noViolation.json
@@ -0,0 +1 @@
+{"flowContents":{"identifier":"5d78473b-43d3-33b6-beed-83d2ae71a73d","instanceIdentifier":"b56b0714-0195-1000-2a87-e8aa1ca3849f","name":"NiFi
 
Flow","comments":"","position":{"x":0.0,"y":0.0},"processGroups":[],"remoteProcessGroups":[],"processors":[{"identifier":"da98e020-2712-38fa-b2c9-88e0d3bb25b8","instanceIdentifier":"b5734be4-0195-1000-0e75-bc0f150d06bd","name":"ListenHTTP","comments":"","position":{"x":-51.5,"y":-241.0},"type":"org.apache.nifi.processors.standard.ListenHTTP","bundl
 [...]
\ No newline at end of file

Reply via email to