|
| 1 | +package com.datadog.appsec.config; |
| 2 | + |
| 3 | +import java.lang.instrument.ClassFileTransformer; |
| 4 | +import java.lang.instrument.Instrumentation; |
| 5 | +import java.util.ArrayList; |
| 6 | +import java.util.HashSet; |
| 7 | +import java.util.List; |
| 8 | +import java.util.Set; |
| 9 | +import java.util.concurrent.locks.Lock; |
| 10 | +import java.util.concurrent.locks.ReentrantLock; |
| 11 | +import org.slf4j.Logger; |
| 12 | +import org.slf4j.LoggerFactory; |
| 13 | + |
| 14 | +/** |
| 15 | + * Handles dynamic instrumentation updates for Supply Chain Analysis (SCA) vulnerability detection. |
| 16 | + * |
| 17 | + * <p>This class receives SCA configuration updates from Remote Config and triggers |
| 18 | + * retransformation of classes that match the instrumentation targets. |
| 19 | + * |
| 20 | + * <p>Thread-safe: Multiple threads can call {@link #onConfigUpdate(AppSecSCAConfig)} concurrently. |
| 21 | + */ |
| 22 | +public class AppSecSCAInstrumentationUpdater { |
| 23 | + |
| 24 | + private static final Logger log = LoggerFactory.getLogger(AppSecSCAInstrumentationUpdater.class); |
| 25 | + |
| 26 | + private final Instrumentation instrumentation; |
| 27 | + private final Lock updateLock = new ReentrantLock(); |
| 28 | + |
| 29 | + private volatile AppSecSCAConfig currentConfig; |
| 30 | + private ClassFileTransformer currentTransformer; |
| 31 | + |
| 32 | + public AppSecSCAInstrumentationUpdater(Instrumentation instrumentation) { |
| 33 | + if (instrumentation == null) { |
| 34 | + throw new IllegalArgumentException("Instrumentation cannot be null"); |
| 35 | + } |
| 36 | + if (!instrumentation.isRetransformClassesSupported()) { |
| 37 | + throw new IllegalStateException( |
| 38 | + "SCA requires retransformation support, but it's not available in this JVM"); |
| 39 | + } |
| 40 | + this.instrumentation = instrumentation; |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * Called when SCA configuration is updated via Remote Config. |
| 45 | + * |
| 46 | + * @param newConfig the new SCA configuration, or null if config was removed |
| 47 | + */ |
| 48 | + public void onConfigUpdate(AppSecSCAConfig newConfig) { |
| 49 | + updateLock.lock(); |
| 50 | + try { |
| 51 | + if (newConfig == null) { |
| 52 | + log.debug("SCA config removed, disabling instrumentation"); |
| 53 | + removeInstrumentation(); |
| 54 | + currentConfig = null; |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + if (!isEnabled(newConfig)) { |
| 59 | + log.debug("SCA config disabled, removing instrumentation"); |
| 60 | + removeInstrumentation(); |
| 61 | + currentConfig = newConfig; |
| 62 | + return; |
| 63 | + } |
| 64 | + |
| 65 | + if (newConfig.instrumentationTargets == null || newConfig.instrumentationTargets.isEmpty()) { |
| 66 | + log.debug("SCA config has no instrumentation targets"); |
| 67 | + removeInstrumentation(); |
| 68 | + currentConfig = newConfig; |
| 69 | + return; |
| 70 | + } |
| 71 | + |
| 72 | + log.info( |
| 73 | + "Applying SCA instrumentation for {} targets", |
| 74 | + newConfig.instrumentationTargets.size()); |
| 75 | + |
| 76 | + AppSecSCAConfig oldConfig = currentConfig; |
| 77 | + currentConfig = newConfig; |
| 78 | + |
| 79 | + applyInstrumentation(oldConfig, newConfig); |
| 80 | + } finally { |
| 81 | + updateLock.unlock(); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + private boolean isEnabled(AppSecSCAConfig config) { |
| 86 | + return config.enabled != null && config.enabled; |
| 87 | + } |
| 88 | + |
| 89 | + private void applyInstrumentation(AppSecSCAConfig oldConfig, AppSecSCAConfig newConfig) { |
| 90 | + // Determine which classes need to be retransformed |
| 91 | + Set<String> targetClassNames = extractTargetClassNames(newConfig); |
| 92 | + |
| 93 | + if (targetClassNames.isEmpty()) { |
| 94 | + log.debug("No valid target class names found"); |
| 95 | + return; |
| 96 | + } |
| 97 | + |
| 98 | + // Remove old transformer if exists |
| 99 | + if (currentTransformer != null) { |
| 100 | + log.debug("Removing previous SCA transformer"); |
| 101 | + instrumentation.removeTransformer(currentTransformer); |
| 102 | + currentTransformer = null; |
| 103 | + } |
| 104 | + |
| 105 | + // Install new transformer |
| 106 | + // TODO: Create AppSecSCATransformer |
| 107 | + log.debug("Installing new SCA transformer for targets: {}", targetClassNames); |
| 108 | + // currentTransformer = new AppSecSCATransformer(newConfig); |
| 109 | + // instrumentation.addTransformer(currentTransformer, true); |
| 110 | + |
| 111 | + // Find loaded classes that match targets |
| 112 | + List<Class<?>> classesToRetransform = findLoadedClasses(targetClassNames); |
| 113 | + |
| 114 | + if (classesToRetransform.isEmpty()) { |
| 115 | + log.debug("No loaded classes match SCA targets (they may load later)"); |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + // Trigger retransformation |
| 120 | + log.info("Retransforming {} classes for SCA instrumentation", classesToRetransform.size()); |
| 121 | + retransformClasses(classesToRetransform); |
| 122 | + } |
| 123 | + |
| 124 | + private Set<String> extractTargetClassNames(AppSecSCAConfig config) { |
| 125 | + Set<String> classNames = new HashSet<>(); |
| 126 | + |
| 127 | + for (AppSecSCAConfig.InstrumentationTarget target : config.instrumentationTargets) { |
| 128 | + if (target.className == null || target.className.isEmpty()) { |
| 129 | + log.warn("Skipping target with null or empty className"); |
| 130 | + continue; |
| 131 | + } |
| 132 | + |
| 133 | + // Convert internal format (org/foo/Bar) to binary name (org.foo.Bar) |
| 134 | + String binaryName = target.className.replace('/', '.'); |
| 135 | + classNames.add(binaryName); |
| 136 | + } |
| 137 | + |
| 138 | + return classNames; |
| 139 | + } |
| 140 | + |
| 141 | + private List<Class<?>> findLoadedClasses(Set<String> targetClassNames) { |
| 142 | + List<Class<?>> matchedClasses = new ArrayList<>(); |
| 143 | + |
| 144 | + Class<?>[] loadedClasses = instrumentation.getAllLoadedClasses(); |
| 145 | + log.debug("Scanning {} loaded classes for SCA targets", loadedClasses.length); |
| 146 | + |
| 147 | + for (Class<?> clazz : loadedClasses) { |
| 148 | + if (targetClassNames.contains(clazz.getName())) { |
| 149 | + if (!instrumentation.isModifiableClass(clazz)) { |
| 150 | + log.debug("Class {} matches target but is not modifiable", clazz.getName()); |
| 151 | + continue; |
| 152 | + } |
| 153 | + matchedClasses.add(clazz); |
| 154 | + log.debug("Found loaded class matching SCA target: {}", clazz.getName()); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + return matchedClasses; |
| 159 | + } |
| 160 | + |
| 161 | + private void retransformClasses(List<Class<?>> classes) { |
| 162 | + for (Class<?> clazz : classes) { |
| 163 | + try { |
| 164 | + log.debug("Retransforming class: {}", clazz.getName()); |
| 165 | + instrumentation.retransformClasses(clazz); |
| 166 | + } catch (Exception e) { |
| 167 | + log.error("Failed to retransform class: {}", clazz.getName(), e); |
| 168 | + } catch (Throwable t) { |
| 169 | + log.error("Throwable during retransformation of class: {}", clazz.getName(), t); |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + private void removeInstrumentation() { |
| 175 | + if (currentTransformer != null) { |
| 176 | + log.debug("Removing SCA transformer"); |
| 177 | + instrumentation.removeTransformer(currentTransformer); |
| 178 | + currentTransformer = null; |
| 179 | + } |
| 180 | + |
| 181 | + // TODO: Optionally retransform classes to remove instrumentation |
| 182 | + // For now, instrumentation stays until JVM restart |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Gets the current SCA configuration. |
| 187 | + * |
| 188 | + * @return the current config, or null if none is active |
| 189 | + */ |
| 190 | + public AppSecSCAConfig getCurrentConfig() { |
| 191 | + return currentConfig; |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * For testing: checks if a transformer is currently installed. |
| 196 | + */ |
| 197 | + boolean hasTransformer() { |
| 198 | + return currentTransformer != null; |
| 199 | + } |
| 200 | +} |
0 commit comments