Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions dd-java-agent/agent-profiling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ excludedClassesCoverage += [
'com.datadog.profiling.agent.ProfilingAgent',
'com.datadog.profiling.agent.ProfilingAgent.ShutdownHook',
'com.datadog.profiling.agent.ProfilingAgent.DataDumper',
'com.datadog.profiling.agent.ProfilerFlare'
'com.datadog.profiling.agent.ProfilerFlare',
'com.datadog.profiling.agent.ScrubRecordingDataListener',
'com.datadog.profiling.agent.ScrubRecordingDataListener.ScrubbedRecordingData'
]

dependencies {
api libs.slf4j
api project(':internal-api')
implementation project(':internal-api')

api project(':dd-java-agent:agent-profiling:profiling-ddprof')
api project(':dd-java-agent:agent-profiling:profiling-uploader')
api project(':dd-java-agent:agent-profiling:profiling-controller')
implementation project(':dd-java-agent:agent-profiling:profiling-scrubber')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr:implementation')
api project(':dd-java-agent:agent-profiling:profiling-controller-ddprof')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,10 @@ && isEventEnabled(recordingSettings, "jdk.NativeMethodSample")) {
}

private static String getJfrRepositoryBase(ConfigProvider configProvider) {
String jfrRepoDefault = System.getProperty("java.io.tmpdir") + "/dd/jfr";
String legacy =
configProvider.getString(
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT);
if (!legacy.equals(ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT)) {
configProvider.getString(ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE, jfrRepoDefault);
if (!legacy.equals(jfrRepoDefault)) {
log.warn(
"The configuration key {} is deprecated. Please use {} instead.",
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ private String getProfilerConfig() {
"JFR Repository Base",
configProvider.getString(
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT),
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT);
System.getProperty("java.io.tmpdir") + "/dd/jfr"),
System.getProperty("java.io.tmpdir") + "/dd/jfr");
appendConfig(
sb,
"JFR Repository Max Size",
Expand Down Expand Up @@ -504,8 +504,8 @@ private String getProfilerConfig() {
sb,
"Temp Directory",
configProvider.getString(
ProfilingConfig.PROFILING_TEMP_DIR, ProfilingConfig.PROFILING_TEMP_DIR_DEFAULT),
ProfilingConfig.PROFILING_TEMP_DIR_DEFAULT);
ProfilingConfig.PROFILING_TEMP_DIR, System.getProperty("java.io.tmpdir")),
System.getProperty("java.io.tmpdir"));
appendConfig(
sb,
"Debug Dump Path",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.nio.file.Path;
import java.time.Instant;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

final class DatadogProfilerRecordingData extends RecordingData {
private final Path recordingFile;
Expand Down Expand Up @@ -36,4 +37,10 @@ public void release() {
public String getName() {
return "ddprof";
}

@Nullable
@Override
public Path getPath() {
return recordingFile;
}
}
19 changes: 19 additions & 0 deletions dd-java-agent/agent-profiling/profiling-scrubber/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apply from: "$rootDir/gradle/java.gradle"

minimumInstructionCoverage = 0.0
minimumBranchCoverage = 0.0

dependencies {
api libs.slf4j

implementation(libs.jafar.tools) {
// ASM is only used by jafar's CodeGenerator/Deserializer path which the scrubber does not use
exclude group: 'org.ow2.asm', module: 'asm'
// Agent has its own slf4j binding
exclude group: 'org.slf4j', module: 'slf4j-simple'
}

testImplementation libs.bundles.junit5
testImplementation libs.bundles.mockito
testImplementation libs.bundles.jmc
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.datadog.profiling.scrubber;

import io.jafar.tools.Scrubber;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Provides the default scrub definition targeting sensitive JFR event fields. */
public final class DefaultScrubDefinition {

private static final Map<String, Scrubber.ScrubField> DEFAULT_SCRUB_FIELDS;

static {
Map<String, Scrubber.ScrubField> fields = new HashMap<>();
// System properties may contain API keys, passwords
fields.put("jdk.InitialSystemProperty", new Scrubber.ScrubField(null, "value", (k, v) -> true));
// JVM args may contain credentials in -D flags
fields.put("jdk.JVMInformation", new Scrubber.ScrubField(null, "jvmArguments", (k, v) -> true));
// Env vars may contain secrets
fields.put(
"jdk.InitialEnvironmentVariable", new Scrubber.ScrubField(null, "value", (k, v) -> true));
// Process command lines may reveal infrastructure
fields.put("jdk.SystemProcess", new Scrubber.ScrubField(null, "commandLine", (k, v) -> true));
DEFAULT_SCRUB_FIELDS = Collections.unmodifiableMap(fields);
}

/**
* Creates a scrubber with the default scrub definition.
*
* @param excludeEventTypes list of event type names to exclude from scrubbing, or null for none
* @return a configured {@link JfrScrubber}
*/
public static JfrScrubber create(List<String> excludeEventTypes) {
Set<String> excludeSet =
excludeEventTypes != null
? new HashSet<>(excludeEventTypes)
: Collections.<String>emptySet();

return new JfrScrubber(
eventTypeName -> {
if (excludeSet.contains(eventTypeName)) {
return null;
}
return DEFAULT_SCRUB_FIELDS.get(eventTypeName);
});
}

private DefaultScrubDefinition() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.datadog.profiling.scrubber;

import io.jafar.tools.Scrubber;
import java.nio.file.Path;
import java.util.function.Function;

/**
* Thin wrapper around {@link Scrubber} from jafar-tools, hiding jafar types from consumers outside
* the profiling-scrubber module.
*/
public final class JfrScrubber {

private final Function<String, Scrubber.ScrubField> scrubDefinition;

JfrScrubber(Function<String, Scrubber.ScrubField> scrubDefinition) {
this.scrubDefinition = scrubDefinition;
}

/**
* Scrub the given file by replacing targeted field values with 'x' bytes.
*
* @param input the input file to scrub
* @param output the output file to write the scrubbed content to
* @throws Exception if an error occurs during parsing or writing
*/
public void scrubFile(Path input, Path output) throws Exception {
Scrubber.scrubFile(input, output, scrubDefinition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.datadog.profiling.scrubber;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit;

class JfrScrubberTest {

@TempDir Path tempDir;

private Path inputFile;

@BeforeEach
void setUp() throws IOException {
inputFile = tempDir.resolve("input.jfr");
try (InputStream is = getClass().getResourceAsStream("/test-recording.jfr")) {
if (is == null) {
throw new IllegalStateException("test-recording.jfr not found in test resources");
}
Files.copy(is, inputFile, StandardCopyOption.REPLACE_EXISTING);
}
}

@Test
void scrubInitialSystemPropertyValues() throws Exception {
JfrScrubber scrubber = DefaultScrubDefinition.create(null);
Path outputFile = tempDir.resolve("output.jfr");
scrubber.scrubFile(inputFile, outputFile);

assertTrue(Files.exists(outputFile));
assertTrue(Files.size(outputFile) > 0, "Scrubbed file should not be empty");

// Verify the scrubbed file is valid and parseable
JfrLoaderToolkit.loadEvents(outputFile.toFile());
}

@Test
void scrubWithNoMatchingEvents() throws Exception {
// Scrubber with all default events excluded — nothing matches
JfrScrubber scrubber = new JfrScrubber(name -> null);
Path outputFile = tempDir.resolve("output.jfr");
scrubber.scrubFile(inputFile, outputFile);

// Output should be identical to input when no events match
assertEquals(Files.size(inputFile), Files.size(outputFile));
}

@Test
void scrubWithExcludedEventType() throws Exception {
// Exclude jdk.InitialSystemProperty from scrubbing
JfrScrubber scrubber =
DefaultScrubDefinition.create(Collections.singletonList("jdk.InitialSystemProperty"));
Path outputFile = tempDir.resolve("output.jfr");
scrubber.scrubFile(inputFile, outputFile);

assertTrue(Files.exists(outputFile));
assertTrue(Files.size(outputFile) > 0);
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import static datadog.environment.JavaVirtualMachine.isJavaVersion;
import static datadog.environment.JavaVirtualMachine.isJavaVersionAtLeast;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_SCRUB_ENABLED;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_SCRUB_ENABLED_DEFAULT;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_SCRUB_FAIL_OPEN;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_SCRUB_FAIL_OPEN_DEFAULT;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_START_FORCE_FIRST;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_START_FORCE_FIRST_DEFAULT;
import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY;
Expand All @@ -14,6 +18,7 @@
import com.datadog.profiling.controller.ProfilingSystem;
import com.datadog.profiling.controller.UnsupportedEnvironmentException;
import com.datadog.profiling.controller.jfr.JFRAccess;
import com.datadog.profiling.scrubber.DefaultScrubDefinition;
import com.datadog.profiling.uploader.ProfileUploader;
import com.datadog.profiling.utils.Timestamper;
import datadog.trace.api.Config;
Expand All @@ -32,6 +37,7 @@
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -137,6 +143,27 @@ public static synchronized boolean run(final boolean earlyStart, Instrumentation

uploader = new ProfileUploader(config, configProvider);

RecordingDataListener listener = uploader::upload;
if (dumper != null) {
RecordingDataListener upload = listener;
listener =
(type, data, sync) -> {
dumper.onNewData(type, data, sync);
upload.onNewData(type, data, sync);
};
}
if (configProvider.getBoolean(PROFILING_SCRUB_ENABLED, PROFILING_SCRUB_ENABLED_DEFAULT)) {
List<String> excludeEventTypes =
configProvider.getList(ProfilingConfig.PROFILING_SCRUB_EXCLUDE_EVENTS);
boolean failOpen =
configProvider.getBoolean(
PROFILING_SCRUB_FAIL_OPEN, PROFILING_SCRUB_FAIL_OPEN_DEFAULT);

listener =
new ScrubRecordingDataListener(
listener, DefaultScrubDefinition.create(excludeEventTypes), failOpen);
}

final Duration startupDelay = Duration.ofSeconds(config.getProfilingStartDelay());
final Duration uploadPeriod = Duration.ofSeconds(config.getProfilingUploadPeriod());

Expand All @@ -149,12 +176,7 @@ public static synchronized boolean run(final boolean earlyStart, Instrumentation
configProvider,
controller,
context.snapshot(),
dumper == null
? uploader::upload
: (type, data, sync) -> {
dumper.onNewData(type, data, sync);
uploader.upload(type, data, sync);
},
listener,
startupDelay,
startupDelayRandomRange,
uploadPeriod,
Expand Down
Loading
Loading