Skip to content

Commit e32cd15

Browse files
mferretticlaude
andcommitted
feat(security): file permission validation at startup (TASK-036)
Adds FilePermissionValidator to core, called during execute command startup after job config is parsed: - Config files: warn if readable by group or others (chmod 640 advice) - Seed files (type: file): fail fast with SecurityException if readable by group or others (chmod 600 required) - Windows: all checks silently skipped (no POSIX permissions) - 9 unit tests (1 skipped on non-Windows), SpotBugs clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7f83254 commit e32cd15

File tree

6 files changed

+252
-2
lines changed

6 files changed

+252
-2
lines changed

cli/src/main/java/com/datagenerator/cli/ExecuteCommand.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import ch.qos.logback.classic.Level;
2020
import ch.qos.logback.classic.Logger;
2121
import com.datagenerator.core.engine.GenerationEngine;
22+
import com.datagenerator.core.security.FilePermissionValidator;
2223
import com.datagenerator.core.seed.SeedConfig;
2324
import com.datagenerator.core.seed.SeedResolver;
2425
import com.datagenerator.core.structure.StructureLoader;
@@ -379,6 +380,13 @@ public Integer call() throws Exception {
379380
JobConfig jobConfig = jobParser.parse(jobFile);
380381
log.info("Loaded job config: source={}, type={}", jobConfig.getSource(), jobConfig.getType());
381382

383+
// 1a. Validate file permissions
384+
FilePermissionValidator permValidator = new FilePermissionValidator();
385+
permValidator.validateConfigFile(jobFile);
386+
if (jobConfig.getSeed() instanceof SeedConfig.FileSeed fileSeed) {
387+
permValidator.validateSeedFile(Path.of(fileSeed.getPath()));
388+
}
389+
382390
// 2. Resolve seed
383391
long seed = resolveSeed(jobConfig);
384392
log.info("Using seed: {}", seed);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2026 Marco Ferretti
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.datagenerator.core.security;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.attribute.PosixFilePermission;
23+
import java.util.Locale;
24+
import java.util.Set;
25+
import lombok.extern.slf4j.Slf4j;
26+
27+
/**
28+
* Validates file permissions for configuration and seed files.
29+
*
30+
* <p>On Unix-like systems, warns when configuration files are readable by group or others, and
31+
* fails fast when seed files have the same permissive permissions (seed files may contain sensitive
32+
* values and should be owner-only: {@code chmod 600}).
33+
*
34+
* <p>On Windows, POSIX permissions are not available — all checks are silently skipped.
35+
*/
36+
@Slf4j
37+
public class FilePermissionValidator {
38+
39+
private static final boolean IS_POSIX =
40+
!System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win");
41+
42+
/**
43+
* Warns if the given configuration file is readable by group or others.
44+
*
45+
* @param configFile path to the configuration file
46+
*/
47+
public void validateConfigFile(Path configFile) {
48+
if (!IS_POSIX || !Files.exists(configFile)) {
49+
return;
50+
}
51+
try {
52+
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(configFile);
53+
if (perms.contains(PosixFilePermission.GROUP_READ)
54+
|| perms.contains(PosixFilePermission.OTHERS_READ)) {
55+
log.warn(
56+
"Configuration file {} has permissive permissions (readable by group or others). "
57+
+ "Consider restricting to owner-only: chmod 640 {}",
58+
configFile,
59+
configFile);
60+
}
61+
} catch (IOException e) {
62+
log.debug("Could not read permissions for config file {}: {}", configFile, e.getMessage());
63+
}
64+
}
65+
66+
/**
67+
* Fails fast if the given seed file is readable by group or others.
68+
*
69+
* <p>Seed files may contain sensitive values and must be restricted to owner read/write only
70+
* ({@code chmod 600}).
71+
*
72+
* @param seedFile path to the seed file
73+
* @throws SecurityException if the seed file has insecure permissions
74+
*/
75+
public void validateSeedFile(Path seedFile) {
76+
if (!IS_POSIX || !Files.exists(seedFile)) {
77+
return;
78+
}
79+
try {
80+
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(seedFile);
81+
if (perms.contains(PosixFilePermission.GROUP_READ)
82+
|| perms.contains(PosixFilePermission.OTHERS_READ)) {
83+
throw new SecurityException(
84+
"Seed file has insecure permissions (readable by group or others). "
85+
+ "Restrict to owner-only: chmod 600 "
86+
+ seedFile);
87+
}
88+
} catch (SecurityException e) {
89+
throw e;
90+
} catch (IOException e) {
91+
log.debug("Could not read permissions for seed file {}: {}", seedFile, e.getMessage());
92+
}
93+
}
94+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2026 Marco Ferretti
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** Security utilities for file permission validation and startup checks. */
18+
package com.datagenerator.core.security;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2026 Marco Ferretti
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.datagenerator.core.security;
18+
19+
import static org.assertj.core.api.Assertions.*;
20+
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.attribute.PosixFilePermission;
25+
import java.nio.file.attribute.PosixFilePermissions;
26+
import java.util.Set;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.condition.DisabledOnOs;
30+
import org.junit.jupiter.api.condition.EnabledOnOs;
31+
import org.junit.jupiter.api.condition.OS;
32+
import org.junit.jupiter.api.io.TempDir;
33+
34+
class FilePermissionValidatorTest {
35+
36+
@TempDir Path tempDir;
37+
38+
private FilePermissionValidator validator;
39+
40+
@BeforeEach
41+
void setUp() {
42+
validator = new FilePermissionValidator();
43+
}
44+
45+
// ── Config file tests (Unix only) ────────────────────────────────────────
46+
47+
@Test
48+
@DisabledOnOs(OS.WINDOWS)
49+
void shouldNotWarnWhenConfigFileIsOwnerOnly() throws IOException {
50+
Path file = createFileWithPermissions("config.yaml", "rw-------");
51+
// No exception expected — just a log warn check (hard to assert without log capture)
52+
assertThatNoException().isThrownBy(() -> validator.validateConfigFile(file));
53+
}
54+
55+
@Test
56+
@DisabledOnOs(OS.WINDOWS)
57+
void shouldNotThrowWhenConfigFileIsGroupReadable() throws IOException {
58+
Path file = createFileWithPermissions("config.yaml", "rw-r-----");
59+
// warn only — no exception
60+
assertThatNoException().isThrownBy(() -> validator.validateConfigFile(file));
61+
}
62+
63+
@Test
64+
@DisabledOnOs(OS.WINDOWS)
65+
void shouldNotThrowWhenConfigFileIsWorldReadable() throws IOException {
66+
Path file = createFileWithPermissions("config.yaml", "rw-r--r--");
67+
// warn only — no exception
68+
assertThatNoException().isThrownBy(() -> validator.validateConfigFile(file));
69+
}
70+
71+
@Test
72+
void shouldSilentlySkipConfigFileWhenItDoesNotExist() {
73+
Path missing = tempDir.resolve("nonexistent.yaml");
74+
assertThatNoException().isThrownBy(() -> validator.validateConfigFile(missing));
75+
}
76+
77+
// ── Seed file tests (Unix only) ──────────────────────────────────────────
78+
79+
@Test
80+
@DisabledOnOs(OS.WINDOWS)
81+
void shouldPassWhenSeedFileIsOwnerOnly() throws IOException {
82+
Path file = createFileWithPermissions("seed.txt", "rw-------");
83+
assertThatNoException().isThrownBy(() -> validator.validateSeedFile(file));
84+
}
85+
86+
@Test
87+
@DisabledOnOs(OS.WINDOWS)
88+
void shouldFailWhenSeedFileIsGroupReadable() throws IOException {
89+
Path file = createFileWithPermissions("seed.txt", "rw-r-----");
90+
assertThatThrownBy(() -> validator.validateSeedFile(file))
91+
.isInstanceOf(SecurityException.class)
92+
.hasMessageContaining("insecure permissions")
93+
.hasMessageContaining("chmod 600");
94+
}
95+
96+
@Test
97+
@DisabledOnOs(OS.WINDOWS)
98+
void shouldFailWhenSeedFileIsWorldReadable() throws IOException {
99+
Path file = createFileWithPermissions("seed.txt", "rw-r--r--");
100+
assertThatThrownBy(() -> validator.validateSeedFile(file))
101+
.isInstanceOf(SecurityException.class)
102+
.hasMessageContaining("insecure permissions");
103+
}
104+
105+
@Test
106+
void shouldSilentlySkipSeedFileWhenItDoesNotExist() {
107+
Path missing = tempDir.resolve("nonexistent.seed");
108+
assertThatNoException().isThrownBy(() -> validator.validateSeedFile(missing));
109+
}
110+
111+
// ── Windows: all checks silently skipped ─────────────────────────────────
112+
113+
@Test
114+
@EnabledOnOs(OS.WINDOWS)
115+
void shouldSkipAllChecksOnWindows() throws IOException {
116+
Path file = Files.createTempFile(tempDir, "test", ".yaml");
117+
assertThatNoException().isThrownBy(() -> validator.validateConfigFile(file));
118+
assertThatNoException().isThrownBy(() -> validator.validateSeedFile(file));
119+
}
120+
121+
// ── helpers ──────────────────────────────────────────────────────────────
122+
123+
private Path createFileWithPermissions(String name, String posixString) throws IOException {
124+
Set<PosixFilePermission> perms = PosixFilePermissions.fromString(posixString);
125+
Path file =
126+
Files.createFile(tempDir.resolve(name), PosixFilePermissions.asFileAttribute(perms));
127+
return file;
128+
}
129+
}

docs/internal/tasks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Each task lists dependencies on other tasks. Always complete dependencies before
9595
### Phase 9: Security & Compliance (🔄 In Progress)
9696
- [TASK-034: Security - Secret Management](TASK-034-security-secrets.md) ⏸️
9797
- [TASK-035: Security - Dependency Vulnerability Scanning](TASK-035-security-dependencies.md)
98-
- [TASK-036: Security - File Permission Checks](TASK-036-security-permissions.md) ⏸️
98+
- [TASK-036: Security - File Permission Checks](TASK-036-security-permissions.md)
9999
- [TASK-044: Extras Directory — External JARs and Custom Datafaker Providers](TASK-044-extras-directory-plugin-loading.md)
100100

101101
### Phase 10: Future Enhancements (🔄 In Progress)

docs/internal/tasks/TASK-036-security-permissions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# TASK-036: Security - File Permission Checks
22

3-
**Status**: ⏸️ Not Started
3+
**Status**: ✅ Complete
4+
**Completion Date**: March 15, 2026
45
**Priority**: P2 (Medium)
56
**Phase**: 9 - Security & Compliance
67
**Dependencies**: TASK-016 (File Destination)

0 commit comments

Comments
 (0)