This is an automated email from the ASF dual-hosted git repository.
dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git
The following commit(s) were added to refs/heads/master by this push:
new 0d31fbeb9c [INLONG-12143][Manager][Agent] Fix Agent path traversal via
unvalidated file source path (#12146)
0d31fbeb9c is described below
commit 0d31fbeb9c9ba9a8cbb26298bcc173f9e627ccb4
Author: spirit_sx <[email protected]>
AuthorDate: Wed Jul 8 16:35:16 2026 +0800
[INLONG-12143][Manager][Agent] Fix Agent path traversal via unvalidated
file source path (#12146)
---
.../inlong/agent/constant/AgentConstants.java | 4 +
.../inlong/agent/constant/TaskConstants.java | 1 +
.../inlong/agent/utils/EventReportUtils.java | 7 +-
.../agent/plugin/fetcher/ManagerFetcher.java | 77 +++++-
.../plugin/task/logcollection/local/FileTask.java | 32 ++-
.../agent/plugin/fetcher/ManagerFetcherTest.java | 291 +++++++++++++++++++++
.../apache/inlong/common/constant/Constants.java | 14 +
.../inlong/common/util/PathValidationUtils.java | 88 +++++++
.../common/util/PathValidationUtilsTest.java | 176 +++++++++++++
.../service/source/file/FileSourceOperator.java | 14 +
.../source/file/FileSourceOperatorTest.java | 127 +++++++++
11 files changed, 826 insertions(+), 5 deletions(-)
diff --git
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/AgentConstants.java
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/AgentConstants.java
index d9880800e6..8b2bf4d67f 100755
---
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/AgentConstants.java
+++
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/AgentConstants.java
@@ -143,4 +143,8 @@ public class AgentConstants {
public static final String DEFAULT_KAFKA_SINK_SEND_VALUE_SERIALIZER =
"org.apache.kafka.common.serialization.ByteArraySerializer";
+
+ // file security: comma-separated list of directories the file agent is
allowed to access
+ public static final String AGENT_FILE_ALLOWED_DIRS =
"agent.file.allowed.dirs";
+ public static final String DEFAULT_AGENT_FILE_ALLOWED_DIRS = "";
}
diff --git
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/TaskConstants.java
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/TaskConstants.java
index da786b7cf6..237e79006d 100755
---
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/TaskConstants.java
+++
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/constant/TaskConstants.java
@@ -237,4 +237,5 @@ public class TaskConstants extends CommonConstants {
*/
public static final int SYNC_SEND_OPEN = 1;
+ public static final String DATA_CONFIG_PATTERN_KEY = "pattern";
}
diff --git
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/utils/EventReportUtils.java
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/utils/EventReportUtils.java
index 3c3863fc1f..ca96938848 100644
---
a/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/utils/EventReportUtils.java
+++
b/inlong-agent/agent-common/src/main/java/org/apache/inlong/agent/utils/EventReportUtils.java
@@ -39,6 +39,8 @@ import static
org.apache.inlong.agent.metrics.AgentEventMetricItem.KEY_INLONG_EV
import static
org.apache.inlong.agent.metrics.AgentEventMetricItem.KEY_INLONG_EXT;
import static
org.apache.inlong.agent.metrics.AgentEventMetricItem.KEY_INLONG_GROUP_ID;
import static
org.apache.inlong.agent.metrics.AgentEventMetricItem.KEY_INLONG_STREAM_ID;
+import static org.apache.inlong.common.constant.Constants.HYPHEN;
+import static org.apache.inlong.common.constant.Constants.PIPE;
/**
* DiagUtils
@@ -53,7 +55,8 @@ public class EventReportUtils {
CONFIG_INVALID_RET_CODE(3, "config invalid ret code"),
CONFIG_INVALID_RESULT(4, "config invalid result maybe visit manager
failed"),
TASK_ADD(5, "task add"),
- TASK_DELETE(6, "task delete");
+ TASK_DELETE(6, "task delete"),
+ TASK_VALIDATION_FAILED(7, "task validation failed");
private final int code;
private final String message;
@@ -101,7 +104,7 @@ public class EventReportUtils {
dims.put(KEY_INLONG_EVENT_TYPE, eventType);
dims.put(KEY_INLONG_EVENT_LEVEL, eventLevel);
dims.put(KEY_INLONG_EVENT_CODE, String.valueOf(evenCode.getCode()));
- dims.put(KEY_INLONG_EXT, ext.replaceAll("\\|", "-"));
+ dims.put(KEY_INLONG_EXT, ext.replace(PIPE, HYPHEN));
dims.put(KEY_INLONG_EVENT_DESC, desc);
metricItemSet.findMetricItem(dims).count.addAndGet(1);
}
diff --git
a/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcher.java
b/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcher.java
index da2315e480..353734c4ee 100644
---
a/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcher.java
+++
b/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcher.java
@@ -37,21 +37,29 @@ import
org.apache.inlong.common.pojo.agent.AgentResponseCode;
import org.apache.inlong.common.pojo.agent.DataConfig;
import org.apache.inlong.common.pojo.agent.TaskRequest;
import org.apache.inlong.common.pojo.agent.TaskResult;
+import org.apache.inlong.common.util.PathValidationUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static
org.apache.inlong.agent.constant.AgentConstants.AGENT_CLUSTER_NAME;
import static
org.apache.inlong.agent.constant.AgentConstants.AGENT_CLUSTER_TAG;
+import static
org.apache.inlong.agent.constant.AgentConstants.AGENT_FILE_ALLOWED_DIRS;
import static org.apache.inlong.agent.constant.AgentConstants.AGENT_LOCAL_IP;
import static org.apache.inlong.agent.constant.AgentConstants.AGENT_UNIQ_ID;
+import static
org.apache.inlong.agent.constant.AgentConstants.DEFAULT_AGENT_FILE_ALLOWED_DIRS;
import static
org.apache.inlong.agent.constant.AgentConstants.DEFAULT_AGENT_UNIQ_ID;
import static
org.apache.inlong.agent.constant.FetcherConstants.AGENT_FETCHER_INTERVAL;
import static
org.apache.inlong.agent.constant.FetcherConstants.AGENT_MANAGER_ADDR;
@@ -61,8 +69,10 @@ import static
org.apache.inlong.agent.constant.FetcherConstants.AGENT_MANAGER_TA
import static
org.apache.inlong.agent.constant.FetcherConstants.DEFAULT_AGENT_FETCHER_INTERVAL;
import static
org.apache.inlong.agent.constant.FetcherConstants.DEFAULT_AGENT_MANAGER_CONFIG_HTTP_PATH;
import static
org.apache.inlong.agent.constant.FetcherConstants.DEFAULT_AGENT_MANAGER_EXIST_TASK_HTTP_PATH;
+import static
org.apache.inlong.agent.constant.TaskConstants.DATA_CONFIG_PATTERN_KEY;
import static
org.apache.inlong.agent.plugin.fetcher.ManagerResultFormatter.getResultData;
import static org.apache.inlong.agent.utils.AgentUtils.fetchLocalUuid;
+import static org.apache.inlong.common.constant.Constants.COMMA;
/**
* Fetch command from Inlong-Manager
@@ -84,6 +94,7 @@ public class ManagerFetcher extends AbstractDaemon implements
ProfileFetcher {
private String uuid;
private String clusterTag;
private String clusterName;
+ private final Set<String> allowedDirs;
public ManagerFetcher(AgentManager agentManager) {
this.agentManager = agentManager;
@@ -96,6 +107,7 @@ public class ManagerFetcher extends AbstractDaemon
implements ProfileFetcher {
uniqId = conf.get(AGENT_UNIQ_ID, DEFAULT_AGENT_UNIQ_ID);
clusterTag = conf.get(AGENT_CLUSTER_TAG);
clusterName = conf.get(AGENT_CLUSTER_NAME);
+ allowedDirs = parseAllowedDirs(conf.get(AGENT_FILE_ALLOWED_DIRS,
DEFAULT_AGENT_FILE_ALLOWED_DIRS));
} else {
throw new RuntimeException("init manager error, cannot find
required key");
}
@@ -203,8 +215,23 @@ public class ManagerFetcher extends AbstractDaemon
implements ProfileFetcher {
EvenCodeEnum.CONFIG_UPDATE_SUC.getMessage());
List<TaskProfile> taskProfiles = new
ArrayList<>();
taskResult.getDataConfigs().forEach((config)
-> {
- TaskProfile profile =
TaskProfile.convertToTaskProfile(config);
- taskProfiles.add(profile);
+ try {
+ validateFilePattern(config,
allowedDirs);
+ TaskProfile profile =
TaskProfile.convertToTaskProfile(config);
+ taskProfiles.add(profile);
+ } catch (Exception e) {
+ LOGGER.error("skip invalid task config
taskId={}: {}",
+ config.getTaskId(),
e.getMessage());
+ EventReportUtils.report(
+ config.getInlongGroupId(),
+ config.getInlongStreamId(),
+ AgentUtils.getCurrentTime(),
+
EventReportUtils.EVENT_TYPE_CONFIG_UPDATE,
+
EventReportUtils.EVENT_LEVEL_ERROR,
+
EvenCodeEnum.TASK_VALIDATION_FAILED,
+ "taskId=" + config.getTaskId()
+ " reason=" + e.getMessage(),
+ "Agent rejected task config: "
+ e.getMessage());
+ }
});
agentManager.getTaskManager().submitTaskProfiles(taskProfiles);
agentManager.getTaskManager().setTaskResultMd5(taskResult.getMd5());
@@ -291,6 +318,52 @@ public class ManagerFetcher extends AbstractDaemon
implements ProfileFetcher {
return dataConfig;
}
+ /**
+ * Validate FILE task pattern to prevent path traversal attack
(defense-in-depth).
+ * Also enforces allowed-directory whitelist when configured.
+ * Rejects patterns containing ".." as a complete path segment,
+ * or pointing outside the configured allowed directories.
+ */
+ static void validateFilePattern(DataConfig config, Set<String>
allowedDirs) {
+ if (TaskTypeEnum.FILE.getType() != (config.getTaskType())) {
+ return;
+ }
+ String extParams = config.getExtParams();
+ if (StringUtils.isEmpty(extParams)) {
+ return;
+ }
+ JsonObject json = GSON.fromJson(extParams, JsonObject.class);
+ if (json == null || !json.has(DATA_CONFIG_PATTERN_KEY)) {
+ return;
+ }
+ String pattern = json.get(DATA_CONFIG_PATTERN_KEY).getAsString();
+ if (StringUtils.isEmpty(pattern)) {
+ throw new IllegalArgumentException("Empty file pattern");
+ }
+ if (PathValidationUtils.containsPathTraversal(pattern)) {
+ throw new IllegalArgumentException(
+ "Path traversal detected in file pattern: " + pattern);
+ }
+ if (!PathValidationUtils.isWithinAllowedDirs(pattern, allowedDirs)) {
+ throw new IllegalArgumentException(
+ "File pattern not in allowed directories: " + pattern);
+ }
+ }
+
+ /**
+ * Parse comma-separated allowed directories string into a Set.
+ * Returns an empty set when the input is null or blank (meaning no
restriction).
+ */
+ static Set<String> parseAllowedDirs(String allowedDirsStr) {
+ if (StringUtils.isBlank(allowedDirsStr)) {
+ return Collections.emptySet();
+ }
+ return Stream.of(allowedDirsStr.split(COMMA))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+ }
+
@Override
public void start() throws Exception {
// when agent start, check local ip and fetch manager ip list;
diff --git
a/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/task/logcollection/local/FileTask.java
b/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/task/logcollection/local/FileTask.java
index 82fca57109..387b703fbe 100644
---
a/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/task/logcollection/local/FileTask.java
+++
b/inlong-agent/agent-plugins/src/main/java/org/apache/inlong/agent/plugin/task/logcollection/local/FileTask.java
@@ -17,7 +17,9 @@
package org.apache.inlong.agent.plugin.task.logcollection.local;
+import org.apache.inlong.agent.conf.AgentConfiguration;
import org.apache.inlong.agent.conf.TaskProfile;
+import org.apache.inlong.agent.constant.AgentConstants;
import org.apache.inlong.agent.constant.CycleUnitType;
import org.apache.inlong.agent.constant.TaskConstants;
import org.apache.inlong.agent.plugin.task.logcollection.LogAbstractTask;
@@ -28,7 +30,9 @@ import org.apache.inlong.agent.plugin.utils.regex.PatternUtil;
import org.apache.inlong.agent.utils.AgentUtils;
import org.apache.inlong.agent.utils.DateTransUtils;
import org.apache.inlong.agent.utils.file.FileUtils;
+import org.apache.inlong.common.util.PathValidationUtils;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,6 +57,8 @@ import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import static org.apache.inlong.common.constant.Constants.COMMA;
+
/**
* Watch directory, if new valid files are created, create instance
correspondingly.
*/
@@ -78,7 +84,7 @@ public class FileTask extends LogAbstractTask {
super.initTask();
timeOffset = taskProfile.get(TaskConstants.TASK_FILE_TIME_OFFSET, "");
retry = taskProfile.isRetry();
- originPatterns =
Stream.of(taskProfile.get(TaskConstants.FILE_DIR_FILTER_PATTERNS).split(","))
+ originPatterns =
Stream.of(taskProfile.get(TaskConstants.FILE_DIR_FILTER_PATTERNS).split(COMMA))
.collect(Collectors.toSet());
if
(taskProfile.getCycleUnit().compareToIgnoreCase(CycleUnitType.REAL_TIME) == 0) {
realTime = true;
@@ -155,6 +161,13 @@ public class FileTask extends LogAbstractTask {
ArrayList<String> directories =
PatternUtil.cutDirectoryByWildcardAndDateExpression(originPattern);
String basicStaticPath = directories.get(0);
LOGGER.info("dataName {} watchPath {}", new Object[]{originPattern,
basicStaticPath});
+
+ // Defense-in-depth: validate against allowed directories configured
at agent level
+ if (!isWithinAllowedDirs(basicStaticPath)) {
+ LOGGER.error("Path not in allowed dirs, rejecting: {}",
basicStaticPath);
+ return;
+ }
+
/* Remember the failed watcher creations. */
if (!new File(basicStaticPath).exists()) {
LOGGER.warn("DIRECTORY_NOT_FOUND_ERROR" + basicStaticPath);
@@ -398,4 +411,21 @@ public class FileTask extends LogAbstractTask {
}
}
}
+
+ /**
+ * Check whether the given directory path is within the agent-level
allowed directories.
+ * When no allowed dirs are configured (empty), always returns true.
+ */
+ private boolean isWithinAllowedDirs(String dirPath) {
+ String allowedDirsStr = AgentConfiguration.getAgentConf()
+ .get(AgentConstants.AGENT_FILE_ALLOWED_DIRS,
AgentConstants.DEFAULT_AGENT_FILE_ALLOWED_DIRS);
+ if (StringUtils.isBlank(allowedDirsStr)) {
+ return true;
+ }
+ Set<String> allowedDirs = Stream.of(allowedDirsStr.split(COMMA))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+ return PathValidationUtils.isWithinAllowedDirs(dirPath, allowedDirs);
+ }
}
diff --git
a/inlong-agent/agent-plugins/src/test/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcherTest.java
b/inlong-agent/agent-plugins/src/test/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcherTest.java
new file mode 100644
index 0000000000..b4fae8a578
--- /dev/null
+++
b/inlong-agent/agent-plugins/src/test/java/org/apache/inlong/agent/plugin/fetcher/ManagerFetcherTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.inlong.agent.plugin.fetcher;
+
+import org.apache.inlong.agent.pojo.FileTask.FileTaskConfig;
+import org.apache.inlong.agent.utils.EventReportUtils;
+import org.apache.inlong.common.enums.TaskTypeEnum;
+import org.apache.inlong.common.pojo.agent.DataConfig;
+
+import com.google.gson.Gson;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit test for ManagerFetcher#validateFilePattern path traversal defense and
allowed-dirs enforcement.
+ */
+public class ManagerFetcherTest {
+
+ private static final Gson GSON = new Gson();
+ private static final Set<String> NO_RESTRICTION = Collections.emptySet();
+
+ private DataConfig buildFileDataConfig(String pattern) {
+ DataConfig dataConfig = new DataConfig();
+ dataConfig.setTaskType(TaskTypeEnum.FILE.getType());
+ dataConfig.setTaskId(1);
+ FileTaskConfig fileTaskConfig = new FileTaskConfig();
+ fileTaskConfig.setPattern(pattern);
+ dataConfig.setExtParams(GSON.toJson(fileTaskConfig));
+ return dataConfig;
+ }
+
+ // ==================== path traversal tests (no dir restriction)
====================
+
+ @Test
+ public void testNormalPatternShouldPass() {
+ DataConfig config = buildFileDataConfig("/data/logs/app.*.log");
+ // Should not throw
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ @Test
+ public void testSimpleTraversalShouldReject() {
+ DataConfig config = buildFileDataConfig("../../../../etc/passwd");
+ try {
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Path traversal
detected"));
+ }
+ }
+
+ @Test
+ public void testMixedPathWithTraversalShouldReject() {
+ DataConfig config = buildFileDataConfig("/data/logs/../../etc/passwd");
+ try {
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Path traversal
detected"));
+ }
+ }
+
+ @Test
+ public void testBackslashTraversalShouldReject() {
+ DataConfig config = buildFileDataConfig("..\\..\\etc\\passwd");
+ try {
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Path traversal
detected"));
+ }
+ }
+
+ @Test
+ public void testNonFileTaskShouldSkip() {
+ DataConfig config = new DataConfig();
+ config.setTaskType(TaskTypeEnum.BINLOG.getType());
+ config.setTaskId(1);
+ // Should not throw for non-FILE task
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ @Test
+ public void testEmptyPatternShouldFail() {
+ DataConfig config = buildFileDataConfig("");
+ // Should throw IllegalArgumentException
+ try {
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ Assert.fail("Expected IllegalArgumentException");
+
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Empty file pattern"));
+ }
+ }
+
+ @Test
+ public void testNullPatternShouldPass() {
+ DataConfig config = buildFileDataConfig(null);
+ // Should not throw
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ @Test
+ public void testDatePatternShouldPass() {
+ DataConfig config = buildFileDataConfig("/data/YYYYMMDD/app.*.log");
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ @Test
+ public void testDotInFilenameShouldPass() {
+ DataConfig config = buildFileDataConfig("/data/logs/app.log");
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ @Test
+ public void testDeepNormalPathShouldPass() {
+ DataConfig config = buildFileDataConfig("/a/b/c/d/e/f/g/h/app.log");
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ }
+
+ // ==================== allowed-dirs tests ====================
+
+ @Test
+ public void testWithinAllowedDirShouldPass() {
+ Set<String> allowed = allowedDirs("/data/logs");
+ DataConfig config = buildFileDataConfig("/data/logs/app.log");
+ ManagerFetcher.validateFilePattern(config, allowed);
+ }
+
+ @Test
+ public void testOutsideAllowedDirShouldReject() {
+ Set<String> allowed = allowedDirs("/data/logs");
+ DataConfig config = buildFileDataConfig("/var/log/app.log");
+ try {
+ ManagerFetcher.validateFilePattern(config, allowed);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("not in allowed
directories"));
+ }
+ }
+
+ @Test
+ public void testDatePatternWithinAllowedDirShouldPass() {
+ Set<String> allowed = allowedDirs("/data/logs");
+ DataConfig config =
buildFileDataConfig("/data/logs/YYYYMMDD/app.*.log");
+ ManagerFetcher.validateFilePattern(config, allowed);
+ }
+
+ @Test
+ public void testDatePatternOutsideAllowedDirShouldReject() {
+ Set<String> allowed = allowedDirs("/data/logs");
+ DataConfig config = buildFileDataConfig("/var/log/YYYYMMDD/app.*.log");
+ try {
+ ManagerFetcher.validateFilePattern(config, allowed);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("not in allowed
directories"));
+ }
+ }
+
+ @Test
+ public void testMultipleAllowedDirsMatchShouldPass() {
+ Set<String> allowed = allowedDirs("/data/logs", "/var/log/app");
+ DataConfig config = buildFileDataConfig("/var/log/app/service.log");
+ ManagerFetcher.validateFilePattern(config, allowed);
+ }
+
+ @Test
+ public void testTraversalRejectedBeforeAllowedDirsCheck() {
+ // Path traversal should be caught first, before allowed-dirs check
+ Set<String> allowed = allowedDirs("/");
+ DataConfig config = buildFileDataConfig("/data/logs/../../etc/passwd");
+ try {
+ ManagerFetcher.validateFilePattern(config, allowed);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Path traversal
detected"));
+ }
+ }
+
+ // ==================== parseAllowedDirs tests ====================
+
+ @Test
+ public void testParseNullAllowedDirs() {
+ Assert.assertEquals(Collections.emptySet(),
ManagerFetcher.parseAllowedDirs(null));
+ }
+
+ @Test
+ public void testParseEmptyAllowedDirs() {
+ Assert.assertEquals(Collections.emptySet(),
ManagerFetcher.parseAllowedDirs(""));
+ }
+
+ @Test
+ public void testParseBlankAllowedDirs() {
+ Assert.assertEquals(Collections.emptySet(),
ManagerFetcher.parseAllowedDirs(" "));
+ }
+
+ @Test
+ public void testParseSingleAllowedDir() {
+ Set<String> result = ManagerFetcher.parseAllowedDirs("/data/logs");
+ Assert.assertEquals(1, result.size());
+ Assert.assertTrue(result.contains("/data/logs"));
+ }
+
+ @Test
+ public void testParseMultipleAllowedDirs() {
+ Set<String> result = ManagerFetcher.parseAllowedDirs("/data/logs,
/var/log, /opt/data");
+ Assert.assertEquals(3, result.size());
+ Assert.assertTrue(result.contains("/data/logs"));
+ Assert.assertTrue(result.contains("/var/log"));
+ Assert.assertTrue(result.contains("/opt/data"));
+ }
+
+ @Test
+ public void testParseAllowedDirsWithExtraCommas() {
+ // trailing comma and double comma should produce empty entries that
are filtered
+ Set<String> result =
ManagerFetcher.parseAllowedDirs("/data/logs,/var/log,, ,");
+ Assert.assertEquals(2, result.size());
+ Assert.assertTrue(result.contains("/data/logs"));
+ Assert.assertTrue(result.contains("/var/log"));
+ }
+
+ // ==================== event reporting integration tests
====================
+
+ @Test
+ public void testTaskValidationFailedEventCode() {
+ EventReportUtils.EvenCodeEnum code =
EventReportUtils.EvenCodeEnum.TASK_VALIDATION_FAILED;
+ Assert.assertEquals(7, code.getCode());
+ Assert.assertEquals("task validation failed", code.getMessage());
+ }
+
+ @Test
+ public void testRejectedTaskRetainsGroupAndStreamForReporting() {
+ // Verify that a rejected DataConfig still carries groupId/streamId,
+ // so the catch block can pass them to EventReportUtils.report().
+ DataConfig config = buildFileDataConfig("../etc/passwd");
+ config.setInlongGroupId("test_reject_group");
+ config.setInlongStreamId("test_reject_stream");
+ try {
+ ManagerFetcher.validateFilePattern(config, NO_RESTRICTION);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("Path traversal
detected"));
+ Assert.assertTrue(e.getMessage().contains("../etc/passwd"));
+ // groupId/streamId survive the exception for event reporting
+ Assert.assertEquals("test_reject_group",
config.getInlongGroupId());
+ Assert.assertEquals("test_reject_stream",
config.getInlongStreamId());
+ }
+ }
+
+ @Test
+ public void testAllowedDirRejectionRetainsGroupAndStreamForReporting() {
+ Set<String> allowed = allowedDirs("/data/logs");
+ DataConfig config = buildFileDataConfig("/var/outside/app.log");
+ config.setInlongGroupId("test_outside_group");
+ config.setInlongStreamId("test_outside_stream");
+ try {
+ ManagerFetcher.validateFilePattern(config, allowed);
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(e.getMessage().contains("not in allowed
directories"));
+ Assert.assertTrue(e.getMessage().contains("/var/outside/app.log"));
+ Assert.assertEquals("test_outside_group",
config.getInlongGroupId());
+ Assert.assertEquals("test_outside_stream",
config.getInlongStreamId());
+ }
+ }
+
+ private static Set<String> allowedDirs(String... dirs) {
+ return new HashSet<>(Arrays.asList(dirs));
+ }
+}
\ No newline at end of file
diff --git
a/inlong-common/src/main/java/org/apache/inlong/common/constant/Constants.java
b/inlong-common/src/main/java/org/apache/inlong/common/constant/Constants.java
index 766e3c4a44..6e08489532 100644
---
a/inlong-common/src/main/java/org/apache/inlong/common/constant/Constants.java
+++
b/inlong-common/src/main/java/org/apache/inlong/common/constant/Constants.java
@@ -33,4 +33,18 @@ public class Constants {
// Default audit version is -1
public static final long DEFAULT_AUDIT_VERSION = -1;
+ public static final String SLASH = "/";
+
+ public static final char SLASH_CHAR = '/';
+
+ public static final char BACK_SLASH_CHAR = '\\';
+
+ public static final String COMMA = ",";
+
+ public static final String DOUBLE_DOT = "..";
+
+ public static final String PIPE = "\\|";
+
+ public static final String HYPHEN = "-";
+
}
diff --git
a/inlong-common/src/main/java/org/apache/inlong/common/util/PathValidationUtils.java
b/inlong-common/src/main/java/org/apache/inlong/common/util/PathValidationUtils.java
new file mode 100644
index 0000000000..d9817e907e
--- /dev/null
+++
b/inlong-common/src/main/java/org/apache/inlong/common/util/PathValidationUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.inlong.common.util;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Set;
+
+import static org.apache.inlong.common.constant.Constants.BACK_SLASH_CHAR;
+import static org.apache.inlong.common.constant.Constants.DOUBLE_DOT;
+import static org.apache.inlong.common.constant.Constants.SLASH;
+import static org.apache.inlong.common.constant.Constants.SLASH_CHAR;
+
+/**
+ * Utility class for validating file path patterns, providing protection
+ * against path traversal attacks and allowed-directory enforcement.
+ */
+public class PathValidationUtils {
+
+ /**
+ * Check whether the given path contains path traversal patterns.
+ * A traversal is detected when ".." appears as a complete path segment.
+ *
+ * @param path the path string to validate, may be null or empty
+ * @return true if the path contains a traversal segment, false otherwise
+ */
+ public static boolean containsPathTraversal(String path) {
+ if (StringUtils.isEmpty(path)) {
+ return false;
+ }
+ // Normalize backslashes to forward slashes and split into segments
+ String normalized = path.replace(BACK_SLASH_CHAR, SLASH_CHAR);
+ for (String segment : normalized.split(SLASH)) {
+ if (DOUBLE_DOT.equals(segment)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the given path starts with any of the allowed directory
prefixes.
+ * When allowedDirs is null or empty, all paths are considered allowed (no
restriction).
+ *
+ * @param path the path to validate, may be null or empty
+ * @param allowedDirs the set of allowed directory prefixes (normalized);
+ * if null or empty, the check passes for any path
+ * @return true if the path is within an allowed directory or no
restrictions exist
+ */
+ public static boolean isWithinAllowedDirs(String path, Set<String>
allowedDirs) {
+ if (StringUtils.isBlank(path)) {
+ return false;
+ }
+ if (CollectionUtils.isEmpty(allowedDirs)) {
+ return true;
+ }
+ String normalizedPath = path.replace(BACK_SLASH_CHAR, SLASH_CHAR);
+ for (String allowedDir : allowedDirs) {
+ if (StringUtils.isEmpty(allowedDir)) {
+ continue;
+ }
+ String normalizedAllowedDir = allowedDir.replace(BACK_SLASH_CHAR,
SLASH_CHAR);
+ if (!normalizedAllowedDir.endsWith(SLASH)) {
+ normalizedAllowedDir += SLASH;
+ }
+ if ((normalizedPath + SLASH).startsWith(normalizedAllowedDir)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git
a/inlong-common/src/test/java/org/apache/inlong/common/util/PathValidationUtilsTest.java
b/inlong-common/src/test/java/org/apache/inlong/common/util/PathValidationUtilsTest.java
new file mode 100644
index 0000000000..7074c649cc
--- /dev/null
+++
b/inlong-common/src/test/java/org/apache/inlong/common/util/PathValidationUtilsTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.inlong.common.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit test for {@link PathValidationUtils#isWithinAllowedDirs}.
+ */
+public class PathValidationUtilsTest {
+
+ // ==================== null / empty allowedDirs (no restriction)
====================
+
+ @Test
+ public void testNullAllowedDirsShouldPass() {
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/etc/passwd", null));
+ }
+
+ @Test
+ public void testEmptyAllowedDirsShouldPass() {
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/etc/passwd",
Collections.emptySet()));
+ }
+
+ @Test
+ public void testNullPathWithRestrictedDirsShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+ Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs(null,
allowed));
+ }
+
+ @Test
+ public void testEmptyPathWithRestrictedDirsShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+ Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("",
allowed));
+ }
+
+ // ==================== exact directory match ====================
+
+ @Test
+ public void testExactDirMatchShouldPass() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs",
allowed));
+ }
+
+ @Test
+ public void testFileUnderAllowedDirShouldPass() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ @Test
+ public void testDeepFileUnderAllowedDirShouldPass() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/2025/01/app.log",
allowed));
+ }
+
+ @Test
+ public void testSiblingDirShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("/data/other/app.log",
allowed));
+ }
+
+ @Test
+ public void testParentDirShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+ Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("/data",
allowed));
+ }
+
+ @Test
+ public void testUnrelatedDirShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("/etc/passwd",
allowed));
+ }
+
+ // ==================== prefix overlap guard ====================
+
+ @Test
+ public void testPrefixOverlapShouldNotMatch() {
+ // "/data" must NOT match "/data_other/logs" (prefix but not a
directory boundary)
+ Set<String> allowed = setOf("/data");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs",
allowed));
+ // Note: "/data_other" is NOT a subdirectory of "/data" — startsWith
alone would false-match
+ }
+
+ // ==================== multiple allowed dirs ====================
+
+ @Test
+ public void testMatchAgainstSecondAllowedDir() {
+ Set<String> allowed = setOf("/data/logs", "/var/log/app");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/var/log/app/service.log",
allowed));
+ }
+
+ @Test
+ public void testNoMatchWithMultipleAllowedDirs() {
+ Set<String> allowed = setOf("/data/logs", "/var/log/app");
+
Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("/home/user/file.txt",
allowed));
+ }
+
+ // ==================== trailing slash variations ====================
+
+ @Test
+ public void testAllowedDirWithTrailingSlash() {
+ Set<String> allowed = setOf("/data/logs/");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ @Test
+ public void testAllowedDirWithoutTrailingSlash() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ // ==================== backslash normalization ====================
+
+ @Test
+ public void testBackslashPathShouldNormalize() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("\\data\\logs\\app.log",
allowed));
+ }
+
+ @Test
+ public void testBackslashInAllowedDirShouldNormalize() {
+ Set<String> allowed = setOf("\\data\\logs");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ // ==================== edge cases ====================
+
+ @Test
+ public void testRelativePathShouldFail() {
+ Set<String> allowed = setOf("/data/logs");
+
Assert.assertFalse(PathValidationUtils.isWithinAllowedDirs("./data/logs",
allowed));
+ }
+
+ @Test
+ public void testRootDirShouldPass() {
+ Set<String> allowed = setOf("/");
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/anything/here",
allowed));
+ }
+
+ @Test
+ public void testNullEntryInAllowedDirsShouldBeIgnored() {
+ Set<String> allowed = new HashSet<>(Arrays.asList(null, "/data/logs"));
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ @Test
+ public void testEmptyEntryInAllowedDirsShouldBeIgnored() {
+ Set<String> allowed = new HashSet<>(Arrays.asList("", "/data/logs"));
+
Assert.assertTrue(PathValidationUtils.isWithinAllowedDirs("/data/logs/app.log",
allowed));
+ }
+
+ private static Set<String> setOf(String... values) {
+ return new HashSet<>(Arrays.asList(values));
+ }
+}
\ No newline at end of file
diff --git
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/source/file/FileSourceOperator.java
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/source/file/FileSourceOperator.java
index 90a0212aa1..d4903f9cba 100644
---
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/source/file/FileSourceOperator.java
+++
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/source/file/FileSourceOperator.java
@@ -18,6 +18,7 @@
package org.apache.inlong.manager.service.source.file;
import org.apache.inlong.common.pojo.agent.DataConfig;
+import org.apache.inlong.common.util.PathValidationUtils;
import org.apache.inlong.manager.common.consts.SourceType;
import org.apache.inlong.manager.common.enums.ErrorCodeEnum;
import org.apache.inlong.manager.common.exceptions.BusinessException;
@@ -77,6 +78,8 @@ public class FileSourceOperator extends
AbstractSourceOperator {
protected void setTargetEntity(SourceRequest request, StreamSourceEntity
targetEntity) {
FileSourceRequest sourceRequest = (FileSourceRequest) request;
try {
+ // Validate file path pattern to prevent path traversal attack
+ validatePathPattern(sourceRequest.getPattern());
CommonBeanUtils.copyProperties(sourceRequest, targetEntity, true);
FileSourceDTO dto = FileSourceDTO.getFromRequest(sourceRequest,
targetEntity.getExtParams());
targetEntity.setExtParams(objectMapper.writeValueAsString(dto));
@@ -86,6 +89,17 @@ public class FileSourceOperator extends
AbstractSourceOperator {
}
}
+ /**
+ * Validate file path pattern to prevent path traversal attacks.
+ * Rejects patterns containing ".." as a complete path segment.
+ */
+ private void validatePathPattern(String pattern) {
+ if (PathValidationUtils.containsPathTraversal(pattern)) {
+ throw new BusinessException(ErrorCodeEnum.SOURCE_INFO_INCORRECT,
+ "Path traversal detected in file pattern: " + pattern);
+ }
+ }
+
@Override
public StreamSource getFromEntity(StreamSourceEntity entity) {
FileSource source = new FileSource();
diff --git
a/inlong-manager/manager-service/src/test/java/org/apache/inlong/manager/service/source/file/FileSourceOperatorTest.java
b/inlong-manager/manager-service/src/test/java/org/apache/inlong/manager/service/source/file/FileSourceOperatorTest.java
new file mode 100644
index 0000000000..50bd04a7ba
--- /dev/null
+++
b/inlong-manager/manager-service/src/test/java/org/apache/inlong/manager/service/source/file/FileSourceOperatorTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.inlong.manager.service.source.file;
+
+import org.apache.inlong.manager.common.consts.SourceType;
+import org.apache.inlong.manager.common.exceptions.BusinessException;
+import org.apache.inlong.manager.pojo.source.file.FileSourceRequest;
+import org.apache.inlong.manager.service.ServiceBaseTest;
+import org.apache.inlong.manager.service.core.impl.InlongStreamServiceTest;
+import org.apache.inlong.manager.service.source.StreamSourceService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Unit test for FileSourceOperator path traversal validation.
+ */
+public class FileSourceOperatorTest extends ServiceBaseTest {
+
+ private static final String TEST_GROUP = "file_source_test_group";
+ private static final String TEST_STREAM = "file_source_test_stream";
+
+ @Autowired
+ private StreamSourceService sourceService;
+ @Autowired
+ private InlongStreamServiceTest streamServiceTest;
+
+ @BeforeEach
+ void setUp() {
+ streamServiceTest.saveInlongStream(TEST_GROUP, TEST_STREAM,
GLOBAL_OPERATOR);
+ }
+
+ @Test
+ void testNormalPatternShouldPass() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("normal-file-source");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("/data/logs/app.*.log");
+
+ Assertions.assertDoesNotThrow(() -> sourceService.save(request,
GLOBAL_OPERATOR));
+ }
+
+ @Test
+ void testSimpleTraversalShouldReject() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("traversal-source-1");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("../../../../etc/passwd");
+
+ BusinessException ex = Assertions.assertThrows(BusinessException.class,
+ () -> sourceService.save(request, GLOBAL_OPERATOR));
+ Assertions.assertTrue(ex.getMessage().contains("Path traversal
detected"));
+ }
+
+ @Test
+ void testMixedPathWithTraversalShouldReject() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("traversal-source-2");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("/data/logs/../../etc/passwd");
+
+ BusinessException ex = Assertions.assertThrows(BusinessException.class,
+ () -> sourceService.save(request, GLOBAL_OPERATOR));
+ Assertions.assertTrue(ex.getMessage().contains("Path traversal
detected"));
+ }
+
+ @Test
+ void testBackslashTraversalShouldReject() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("traversal-source-3");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("..\\..\\etc\\passwd");
+
+ BusinessException ex = Assertions.assertThrows(BusinessException.class,
+ () -> sourceService.save(request, GLOBAL_OPERATOR));
+ Assertions.assertTrue(ex.getMessage().contains("Path traversal
detected"));
+ }
+
+ @Test
+ void testDotInFilenameShouldPass() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("dotfile-source");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("/data/logs/app.log");
+
+ Assertions.assertDoesNotThrow(() -> sourceService.save(request,
GLOBAL_OPERATOR));
+ }
+
+ @Test
+ void testDatePatternShouldPass() {
+ FileSourceRequest request = new FileSourceRequest();
+ request.setInlongGroupId(TEST_GROUP);
+ request.setInlongStreamId(TEST_STREAM);
+ request.setSourceName("date-pattern-source");
+ request.setSourceType(SourceType.FILE);
+ request.setPattern("/data/YYYYMMDD/app.*.log");
+
+ Assertions.assertDoesNotThrow(() -> sourceService.save(request,
GLOBAL_OPERATOR));
+ }
+}
\ No newline at end of file