Skip to content

Commit bb4fa4d

Browse files
committed
NIFI-14381: Add flow analysis rule to identify unset SSL Context Service controller services
1 parent 9fdf8f7 commit bb4fa4d

File tree

7 files changed

+223
-1
lines changed

7 files changed

+223
-1
lines changed

nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
<artifactId>nifi-standard-rules</artifactId>
2424
<packaging>jar</packaging>
2525

26-
<dependencies />
26+
<dependencies>
27+
<dependency>
28+
<groupId>org.apache.nifi</groupId>
29+
<artifactId>nifi-ssl-context-service-api</artifactId>
30+
<scope>compile</scope>
31+
</dependency>
32+
</dependencies>
2733

2834
<build>
2935
<plugins>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.flowanalysis.rules;
18+
19+
import org.apache.nifi.annotation.documentation.CapabilityDescription;
20+
import org.apache.nifi.annotation.documentation.Tags;
21+
import org.apache.nifi.annotation.documentation.UseCase;
22+
import org.apache.nifi.components.PropertyDescriptor;
23+
import org.apache.nifi.controller.ControllerService;
24+
import org.apache.nifi.flow.VersionedComponent;
25+
import org.apache.nifi.flow.VersionedExtensionComponent;
26+
import org.apache.nifi.flowanalysis.AbstractFlowAnalysisRule;
27+
import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
28+
import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
29+
import org.apache.nifi.processor.util.StandardValidators;
30+
import org.apache.nifi.ssl.SSLContextProvider;
31+
import org.apache.nifi.util.StringUtils;
32+
33+
import java.util.Arrays;
34+
import java.util.Collection;
35+
import java.util.HashSet;
36+
import java.util.LinkedHashSet;
37+
import java.util.List;
38+
import java.util.Set;
39+
40+
41+
@Tags({"component", "processor", "controller service", "type"})
42+
@CapabilityDescription("Produces rule violations for each component (i.e. processors or controller services) having a property "
43+
+ "identifying an SSLContextService that is not set.")
44+
@UseCase(
45+
description = "Ensure that no ports can be opened for insecure (plaintext, e.g.) communications.",
46+
configuration = """
47+
To avoid the violation, ensure that the "SSL Context Service" property is set for the specified component(s).
48+
"""
49+
)
50+
public class RequireSecureConnection extends AbstractFlowAnalysisRule {
51+
public static final PropertyDescriptor COMPONENT_TYPE = new PropertyDescriptor.Builder()
52+
.name("component-type")
53+
.displayName("Component Type(s)")
54+
.description("A comma-separated list of component types. Components of the given type that have a property identifying an SSL Context Service will produce a rule violation "
55+
+ "if the service is not set. If no components are specified (i.e. this property is blank), the rule will apply to all components.")
56+
.required(false)
57+
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
58+
.build();
59+
60+
private final static List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
61+
COMPONENT_TYPE
62+
);
63+
64+
@Override
65+
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
66+
return PROPERTY_DESCRIPTORS;
67+
}
68+
69+
@Override
70+
public Collection<ComponentAnalysisResult> analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) {
71+
Collection<ComponentAnalysisResult> results = new HashSet<>();
72+
Set<String> componentTypes = new LinkedHashSet<>();
73+
if (context.getProperty(COMPONENT_TYPE).isSet()) {
74+
String componentTypesList = context.getProperty(COMPONENT_TYPE).getValue();
75+
if (componentTypesList != null) {
76+
Arrays.stream(componentTypesList.split(","))
77+
.filter(StringUtils::isNotBlank)
78+
.map(String::trim)
79+
.forEach(componentTypes::add);
80+
}
81+
}
82+
83+
String componentType = context.getProperty(COMPONENT_TYPE).getValue();
84+
85+
if (component instanceof VersionedExtensionComponent versionedExtensionComponent) {
86+
87+
String encounteredComponentType = versionedExtensionComponent.getType();
88+
String encounteredSimpleComponentType = encounteredComponentType.substring(encounteredComponentType.lastIndexOf(".") + 1);
89+
90+
if (componentTypes.isEmpty() || componentTypes.contains(encounteredComponentType) || componentType.contains(encounteredSimpleComponentType)) {
91+
Set<PropertyDescriptor> propertyDescriptors = context.getProperties().keySet();
92+
// Loop over the properties for this component looking for an SSLContextService
93+
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
94+
Class<? extends ControllerService> definition = propertyDescriptor.getControllerServiceDefinition();
95+
if (definition == null) {
96+
continue;
97+
}
98+
try {
99+
// Is this an SSLContextService?
100+
definition.asSubclass(SSLContextProvider.class);
101+
102+
// If it is and the property value is not set, report a violation
103+
if (!context.getProperty(propertyDescriptor).isSet()) {
104+
ComponentAnalysisResult result = new ComponentAnalysisResult(
105+
component.getInstanceIdentifier(),
106+
"'" + componentType + "' is not allowed"
107+
);
108+
109+
results.add(result);
110+
}
111+
} catch (ClassCastException cce) {
112+
// Do nothing, this is not the property we're looking for
113+
continue;
114+
}
115+
}
116+
}
117+
}
118+
119+
return results;
120+
}
121+
}

nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/main/resources/META-INF/services/org.apache.nifi.flowanalysis.FlowAnalysisRule

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
# limitations under the License.
1515

1616
org.apache.nifi.flowanalysis.rules.DisallowComponentType
17+
org.apache.nifi.flowanalysis.rules.RequireSecureConnection
1718
org.apache.nifi.flowanalysis.rules.RestrictBackpressureSettings
1819
org.apache.nifi.flowanalysis.rules.RestrictFlowFileExpiration

nifi-extension-bundles/nifi-standard-bundle/nifi-standard-rules/src/test/java/org/apache/nifi/flowanalysis/rules/AbstractFlowAnalaysisRuleTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import org.apache.nifi.components.ValidationContext;
2424
import org.apache.nifi.controller.ConfigurationContext;
2525
import org.apache.nifi.flow.VersionedProcessGroup;
26+
import org.apache.nifi.flow.VersionedProcessor;
2627
import org.apache.nifi.flowanalysis.AbstractFlowAnalysisRule;
28+
import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
2729
import org.apache.nifi.flowanalysis.FlowAnalysisRuleContext;
2830
import org.apache.nifi.flowanalysis.GroupAnalysisResult;
2931
import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
@@ -39,6 +41,7 @@
3941
import java.util.List;
4042
import java.util.Map;
4143

44+
import static org.junit.jupiter.api.Assertions.assertEquals;
4245
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
4346
import static org.mockito.ArgumentMatchers.any;
4447

@@ -62,6 +65,7 @@ public abstract class AbstractFlowAnalaysisRuleTest<T extends AbstractFlowAnalys
6265

6366
@BeforeEach
6467
public void setup() {
68+
properties.clear();
6569
rule = initializeRule();
6670
Mockito.lenient().when(flowAnalysisRuleContext.getProperty(any())).thenAnswer(invocation -> {
6771
return properties.get(invocation.getArgument(0));
@@ -72,6 +76,9 @@ public void setup() {
7276
Mockito.lenient().when(validationContext.getProperty(any())).thenAnswer(invocation -> {
7377
return properties.get(invocation.getArgument(0));
7478
});
79+
Mockito.lenient().when(flowAnalysisRuleContext.getProperties()).thenAnswer(invocation -> {
80+
return properties;
81+
});
7582
}
7683

7784
protected void setProperty(PropertyDescriptor propertyDescriptor, String value) {
@@ -87,4 +94,22 @@ protected void testAnalyzeProcessGroup(String flowDefinition, List<String> expec
8794
final Collection<GroupAnalysisResult> actual = rule.analyzeProcessGroup(getProcessGroup(flowDefinition), flowAnalysisRuleContext);
8895
assertIterableEquals(expected, actual.stream().map(r -> r.getComponent().get().getInstanceIdentifier()).sorted().toList());
8996
}
97+
98+
99+
protected void testAnalyzeProcessors(String flowDefinition, List<ComponentAnalysisResult> expected) throws Exception {
100+
VersionedProcessGroup rootPG = getProcessGroup(flowDefinition);
101+
for (VersionedProcessor processor : rootPG.getProcessors()) {
102+
final Collection<ComponentAnalysisResult> actual = rule.analyzeComponent(processor, flowAnalysisRuleContext);
103+
final int expectedSize = (expected == null) ? 0 : expected.size();
104+
final int actualSize = (actual == null) ? 0 : actual.size();
105+
assertEquals(expectedSize, actualSize);
106+
if (actualSize > 0) {
107+
final ComponentAnalysisResult[] actualArray = actual.toArray(new ComponentAnalysisResult[actualSize]);
108+
109+
for (int i = 0; i < expectedSize; i++) {
110+
assertEquals(expected.get(i).getIssueId(), actualArray[i].getIssueId());
111+
}
112+
}
113+
}
114+
}
90115
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.flowanalysis.rules;
18+
19+
import org.apache.nifi.components.PropertyDescriptor;
20+
import org.apache.nifi.flowanalysis.ComponentAnalysisResult;
21+
import org.apache.nifi.ssl.SSLContextProvider;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
25+
import java.util.List;
26+
27+
public class RequireSecureConnectionTest extends AbstractFlowAnalaysisRuleTest<RequireSecureConnection> {
28+
29+
public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
30+
.name("SSL Context Service")
31+
.description("SSL Context Service enables support for HTTPS")
32+
.required(false)
33+
.identifiesControllerService(SSLContextProvider.class)
34+
.build();
35+
@Override
36+
protected RequireSecureConnection initializeRule() {
37+
return new RequireSecureConnection();
38+
}
39+
40+
@BeforeEach
41+
@Override
42+
public void setup() {
43+
super.setup();
44+
setProperty(RequireSecureConnection.COMPONENT_TYPE, "ListenHTTP");
45+
}
46+
@Test
47+
public void testNoViolations() throws Exception {
48+
setProperty(SSL_CONTEXT_SERVICE, "9c50e433-c2aa-3d19-aae6-20299f4ac38c");
49+
testAnalyzeProcessors(
50+
"src/test/resources/RequireSecureConnection/RequireSecureConnection_noViolation.json",
51+
List.of()
52+
);
53+
}
54+
55+
@Test
56+
public void testViolations() throws Exception {
57+
setProperty(SSL_CONTEXT_SERVICE, null);
58+
59+
ComponentAnalysisResult expectedResult = new ComponentAnalysisResult("b5734be4-0195-1000-0e75-bc0f150d06bd", "'ListenHTTP' is not allowed");
60+
testAnalyzeProcessors(
61+
"src/test/resources/RequireSecureConnection/RequireSecureConnection.json",
62+
List.of(
63+
expectedResult // processor ListenHttp with no SSLContextService set
64+
)
65+
);
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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","bundle":{"group":"org.apache.nifi","artifact":"nifi-standard-nar","version":"2.0.0.4.0.0.0-383"},"properties":{"authorized-issuer-dn-pattern":".*","multipart-request-max-size":"1 MB","record-writer":null,"Request Header Maximum Size":"8 KB","HTTP Protocols":"HTTP_1_1","HTTP Headers to receive as Attributes (Regex)":null,"health-check-port":null,"Authorized DN Pattern":".*","max-thread-pool-size":"200","Base Path":"contentListener","multipart-read-buffer-size":"512 KB","SSL Context Service":null,"Max Unconfirmed Flowfile Time":"60 secs","Max Data to Receive per Second":null,"client-authentication":"AUTO","Return Code":"200","record-reader":null,"Listening Port":"8080"},"propertyDescriptors":{"authorized-issuer-dn-pattern":{"name":"authorized-issuer-dn-pattern","displayName":"Authorized Issuer DN Pattern","identifiesControllerService":false,"sensitive":false,"dynamic":false},"multipart-request-max-size":{"name":"multipart-request-max-size","displayName":"Multipart Request Max Size","identifiesControllerService":false,"sensitive":false,"dynamic":false},"record-writer":{"name":"record-writer","displayName":"Record Writer","identifiesControllerService":true,"sensitive":false,"dynamic":false},"Request Header Maximum Size":{"name":"Request Header Maximum Size","displayName":"Request Header Maximum Size","identifiesControllerService":false,"sensitive":false,"dynamic":false},"HTTP Protocols":{"name":"HTTP Protocols","displayName":"HTTP Protocols","identifiesControllerService":false,"sensitive":false,"dynamic":false},"HTTP Headers to receive as Attributes (Regex)":{"name":"HTTP Headers to receive as Attributes (Regex)","displayName":"HTTP Headers to receive as Attributes (Regex)","identifiesControllerService":false,"sensitive":false,"dynamic":false},"health-check-port":{"name":"health-check-port","displayName":"Listening Port for Health Check Requests","identifiesControllerService":false,"sensitive":false,"dynamic":false},"Authorized DN Pattern":{"name":"Authorized DN Pattern","displayName":"Authorized Subject DN Pattern","identifiesControllerService":false,"sensitive":false,"dynamic":false},"max-thread-pool-size":{"name":"max-thread-pool-size","displayName":"Maximum Thread Pool Size","identifiesControllerService":false,"sensitive":false,"dynamic":false},"Base Path":{"name":"Base Path","displayName":"Base Path","identifiesControllerService":false,"sensitive":false,"dynamic":false},"multipart-read-buffer-size":{"name":"multipart-read-buffer-size","displayName":"Multipart Read Buffer Size","identifiesControllerService":false,"sensitive":false,"dynamic":false},"SSL Context Service":{"name":"SSL Context Service","displayName":"SSL Context Service","identifiesControllerService":true,"sensitive":false,"dynamic":false},"Max Unconfirmed Flowfile Time":{"name":"Max Unconfirmed Flowfile Time","displayName":"Max Unconfirmed Flowfile Time","identifiesControllerService":false,"sensitive":false,"dynamic":false},"Max Data to Receive per Second":{"name":"Max Data to Receive per Second","displayName":"Max Data to Receive per Second","identifiesControllerService":false,"sensitive":false,"dynamic":false},"client-authentication":{"name":"client-authentication","displayName":"Client Authentication","identifiesControllerService":false,"sensitive":false,"dynamic":false},"Return Code":{"name":"Return Code","displayName":"Return Code","identifiesControllerService":false,"sensitive":false,"dynamic":false},"record-reader":{"name":"record-reader","displayName":"Record Reader","identifiesControllerService":true,"sensitive":false,"dynamic":false},"Listening Port":{"name":"Listening Port","displayName":"Listening Port","identifiesControllerService":false,"sensitive":false,"dynamic":false}},"style":{},"schedulingPeriod":"0 sec","schedulingStrategy":"TIMER_DRIVEN","executionNode":"ALL","penaltyDuration":"30 sec","yieldDuration":"1 sec","bulletinLevel":"WARN","runDurationMillis":0,"concurrentlySchedulableTaskCount":1,"autoTerminatedRelationships":["success"],"scheduledState":"ENABLED","retryCount":10,"retriedRelationships":["success"],"backoffMechanism":"PENALIZE_FLOWFILE","maxBackoffPeriod":"10 mins","componentType":"PROCESSOR","groupIdentifier":"5d78473b-43d3-33b6-beed-83d2ae71a73d"}],"inputPorts":[],"outputPorts":[],"connections":[],"labels":[],"funnels":[],"controllerServices":[],"defaultFlowFileExpiration":"0 sec","defaultBackPressureObjectThreshold":10000,"defaultBackPressureDataSizeThreshold":"1 GB","scheduledState":"ENABLED","executionEngine":"INHERITED","maxConcurrentTasks":1,"statelessFlowTimeout":"1 min","componentType":"PROCESS_GROUP","flowFileConcurrency":"UNBOUNDED","flowFileOutboundPolicy":"STREAM_WHEN_AVAILABLE"},"externalControllerServices":{},"parameterContexts":{},"flowEncodingVersion":"1.0","parameterProviders":{},"latest":false}

0 commit comments

Comments
 (0)