diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/FileNameAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/FileNameAnalyzer.java index 2d0c37bd244..be054509931 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/FileNameAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/FileNameAnalyzer.java @@ -59,6 +59,7 @@ public class FileNameAnalyzer extends AbstractAnalyzer { "Package.swift", "classes.jar", "build.gradle", + "pom.xml", "CMakeLists.txt"}, IOCase.INSENSITIVE); //CSON: WhitespaceAfter diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzer.java new file mode 100644 index 00000000000..4369171c19e --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzer.java @@ -0,0 +1,339 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed 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. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.analyzer; + +import org.apache.commons.lang3.StringUtils; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.exception.InitializationException; +import org.owasp.dependencycheck.processing.MvnListProcessor; +import org.owasp.dependencycheck.utils.FileFilterBuilder; +import org.owasp.dependencycheck.utils.Settings; +import org.owasp.dependencycheck.utils.processing.ProcessReader; +import org.owasp.dependencycheck.utils.processing.Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.concurrent.ThreadSafe; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * An analyzer that By finding the pom.xml file in the Maven source code directory and executing + * the 'mvn dependency:list' command to obtain the dependency information by parsing the result. + * Maven should be installed, and it is recommended to execute the Maven build command in the Maven + * source code directory before scanning and analyzing. + * This helps reduce network access requests during scanning and makes the scanning time shorter. + *

+ * createTime: 2023/10/29 15:01
+ * + * @author regedit0726 + */ +@ThreadSafe +public class MavenSourceAnalyzer extends AbstractFileTypeAnalyzer { + + /** + * A descriptor for the type of dependencies processed or added by this + * analyzer. + */ + public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVA; + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(MavenSourceAnalyzer.class); + + /** + * The name of the analyzer. + */ + private static final String ANALYZER_NAME = "Maven source Analyzer"; + + /** + * The phase that this analyzer is intended to run in. + */ + private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INITIAL; + + /** + * The config filen ame for maven source; + */ + private static final String POM_XML = "pom.xml"; + + /** + * Match any files that named pom.xml. + */ + private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames(POM_XML).build(); + + private String[] array; + + private static final String MVN_VERSION = "mvn -version"; + + private static final String MVN_SETTINGS = "mvn help:effective-settings"; + + private static final String MVN_LIST = "mvn dependency:list -f %s"; + + /** + * set to save the analyzed module name + */ + private Set analyzedModule = new HashSet<>(); + + /** + * The capture group #1 is the verion of maven. e.g.:"Apache Maven 3.6.3" + */ + private static final Pattern MAVEN_VERSION = Pattern.compile("Apache Maven ((\\d+\\.?)+)"); + + /** + * The capture group #2 is the local repository of maven. e.g.:"/repository" + */ + private static final Pattern LOCAL_REPO = Pattern.compile(".*((.*)).*"); + + /** + * the directory path of maven local repository, if maven not installed ,then null + */ + private String mavenLocalRepository; + + /** + * parse result from 'mvn dependency:list -f pomPath' to get dependencies + * @param dependency the dependency to analyze + * @param engine the engine that is scanning the dependencies + * @throws AnalysisException is thrown if there is an error analyzing dependency + */ + @Override + protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { + try(MvnListProcessor processor = new MvnListProcessor(dependency, engine, this)) { + runProcessor(launchMvnList(dependency), "dependency:list", processor, null); + } catch (Exception e) { + throw new AnalysisException("error occurs when analyzer maven source", e); + } + + } + + /** + * Returns the key used in the properties file to reference the analyzer's + * enabled property. + * + * @return the analyzer's enabled property setting key + */ + @Override + protected String getAnalyzerEnabledSettingKey() { + return Settings.KEYS.ANALYZER_MAVENSOURCE_ENABLED; + } + + /** + * Returns the FileFilter. + * + * @return the FileFilter + */ + @Override + protected FileFilter getFileFilter() { + return FILTER; + } + + /** + * Initializes the JarAnalyzer. + * + * @param engine a reference to the dependency-check engine + * @throws InitializationException is thrown if there is an exception + * check the maven version installed and get maven local repository path. + */ + @Override + protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException { + LOGGER.debug("Initializing Maven source analyzer"); + LOGGER.debug("Maven source analyzer enabled: {}", isEnabled()); + if (isEnabled()) { + try { + String os = System.getenv("os"); + this.array = (os != null && os.toLowerCase(Locale.ROOT).startsWith("windows")) + ? new String[]{"cmd", "/c"} + : new String[]{"/bin/sh", "-c"}; + checkMavenVersion(); + initMavenLocalRepositoryPath(); + } catch (Exception ex) { + setEnabled(false); + throw new InitializationException("pom analyzer can not work : " + ex.getMessage(), ex); + } + } + } + + /** + * parse result from "mvn help:effective-settings" to get the maven local repository + * @throws AnalysisException + */ + private void initMavenLocalRepositoryPath() throws AnalysisException { + runProcessor(launchMvnSettings(), "mvn-setttings", null, output -> { + Matcher matcher = LOCAL_REPO.matcher(output); + if(matcher.find()) { + mavenLocalRepository = matcher.group(2); + } + }); + } + + /** + * parse result from "mvn -version" to get the maven local repository + * @throws AnalysisException + */ + private void checkMavenVersion() throws AnalysisException { + runProcessor(launchMvnVersion(), "mvn-version", null, output -> { + Matcher matcher = MAVEN_VERSION.matcher(output); + if(matcher.find()) { + String version = matcher.group(1); + // fixme version3 is supported, version2 unknown, version4 not supported + if(version.charAt(0) != '3') { + throw new RuntimeException("unsupported version of maven(version 3 supported):" + version); + } + } + }); + } + + private void runProcessor(Process process, String name, Processor processor, Consumer consumer) throws AnalysisException { + try(ProcessReader processReader = new ProcessReader(process, processor)) { + processReader.readAll(); + final int exitValue = process.exitValue(); + if (exitValue < 0 || exitValue > 1) { + final String error = processReader.getError(); + if (StringUtils.isNoneBlank(error)) { + LOGGER.warn("Warnings from {} {}", name, error); + } + final String msg = String.format("Unexpected exit code from {} " + + "process; exit code: %s", name, exitValue); + throw new AnalysisException(msg); + } + final String output = processReader.getOutput(); + if (StringUtils.isNoneBlank(output)) { + LOGGER.debug("Warnings from {} {}", name, output); + if(consumer != null) { + // parse output + consumer.accept(output); + } + } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new AnalysisException(name + " process interrupted", ie); + } catch (IOException ioe) { + LOGGER.warn("mvn-setttings failure", ioe); + throw new AnalysisException(name + " error: " + ioe.getMessage(), ioe); + } + } + + /** + * launch "mavn -version" + */ + private Process launchMvnVersion() throws AnalysisException { + return startProcess(MVN_VERSION); + } + + /** + * launch "mavn help:effective-settings" + */ + private Process launchMvnSettings() throws AnalysisException { + return startProcess(MVN_SETTINGS); + } + + /** + * launch "mavn dependency:list" + */ + private Process launchMvnList(Dependency dependency) throws AnalysisException { + return startProcess(String.format(MVN_LIST, dependency.getActualFilePath())); + } + + /** + * start a Proccess + * @param command + * @return Proccess + * @throws AnalysisException + */ + private Process startProcess(String command) throws AnalysisException { + try { + final List args = new ArrayList<>(); + args.addAll(Arrays.asList(array)); + args.add(command); + final ProcessBuilder builder = new ProcessBuilder(args); + return builder.start(); + } catch (IOException e) { + throw new AnalysisException(command + " error occurs", e); + } + } + + /** + * Whether the analyzer is configured to support parallel processing. + * + * @return true if configured to support parallel processing; otherwise + * false + */ + @Override + public boolean supportsParallelProcessing() { + return true; + } + + /** + * Returns the name of the analyzer. + * + * @return the name of the analyzer. + */ + @Override + public String getName() { + return ANALYZER_NAME; + } + + /** + * Returns the phase that the analyzer is intended to run in. + * + * @return the phase that the analyzer is intended to run in. + */ + @Override + public AnalysisPhase getAnalysisPhase() { + return ANALYSIS_PHASE; + } + + /** + * Add module to analyzedModule set if the analyzedModule set doesn't contain the module. + * + * @return true if module is added to the analyzedModule set. + */ + public boolean addAnalyzedModule(String module) { + if(!analyzedModule.contains(module)) { + synchronized (analyzedModule) { + if(!analyzedModule.contains(module)) { + analyzedModule.add(module); + return true; + } + } + } + return false; + } + + /** + * Return mavenLocalRepositoryPath. + * + * @return mavenLocalRepositoryPath. + */ + public String getMavenLocalRepository() { + return mavenLocalRepository; + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/processing/MvnListProcessor.java b/core/src/main/java/org/owasp/dependencycheck/processing/MvnListProcessor.java new file mode 100644 index 00000000000..8d3a85a057f --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/processing/MvnListProcessor.java @@ -0,0 +1,260 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed 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. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.processing; + +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.MavenSourceAnalyzer; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.utils.processing.Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * description:
+ * createTime: 2023/11/9 16:11
+ * + * @author regedit0726 + */ +public class MvnListProcessor extends Processor { + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(MvnListProcessor.class); + /** + * Reference to the gem lock dependency. + */ + private final Dependency pomDependency; + /** + * Reference to the dependency-check engine. + */ + private final Engine engine; + + private final MavenSourceAnalyzer mavenSourceAnalyzer; + + /** + * the directory path of maven local repository + */ + private String mavenLocalRepositoryPath; + + /** + * Temporary storage for an exception if it occurs during the processing. + */ + private IOException ioException; + + /** + * a parsing state to search for moduleName(groupId, artifactId and version) + * searching for info e.g.: + * [INFO] -------------------< org.owasp:dependency-check-ant >------------------- + * [INFO] Building Dependency-Check Ant Task 8.4.3-SNAPSHOT [5/7] + * [INFO] --------------------------------[ jar ]--------------------------------- + */ + State parseModuleName = new State() { + /** + * The capture group #2 is the groupId in a pom. The capture group #3 is the artifactId in a pom. + * e.g.:"[INFO] ------------------< org.owasp:dependency-check-core >-------------------" + */ + Pattern pattern = Pattern.compile("<\\s*(([^\\s>]+?):([^\\s>]+?))\\s*>"); + + /** + * a flag, true when parsing for groupId and artifactId. false when parsing for version + */ + boolean findModuleName = true; + + /** + * groupId in pom + */ + String groupId; + + /** + * artfactId in pom + */ + String artfactId; + + /** + * version in pom + */ + String version; + @Override + public void parse(String line) { + if(findModuleName) { + Matcher matcher = pattern.matcher(line); + if(matcher.find()) { + LOGGER.debug("find module:{}", line); + groupId = matcher.group(2); + artfactId = matcher.group(3); + findModuleName = false; + } else { + LOGGER.debug("not match:{}", line); + } + } else { + String[] arr = line.split("\\s+"); + if(arr.length > 3) { + version = arr[3]; + String module = String.format("%s:%s:%s", groupId, artfactId, version); + if(mavenSourceAnalyzer.addAnalyzedModule(module)) { + LOGGER.debug("switch to findDependency state"); + currentState = findDependency; + } else { + LOGGER.debug("module:{} is analyzed", module); + } + }else { + LOGGER.warn("____________________________Version not match: {}", line); + } + reset(); + } + } + + /** + * reset parseModuleName state + */ + private void reset() { + findModuleName = true; + groupId = null; + artfactId = null; + version = null; + } + }; + + /** + * a state to search for start of the dependency list + * e.g.: + * [INFO] The following files have been resolved: + */ + State findDependency = new State() { + @Override + public void parse(String line) { + if(line.endsWith("resolved:")) { + LOGGER.debug("switch to parseDependency state"); + currentState = parseDependency; + } + } + }; + + /** + * a state to get out the dependency from list + * e.g.: + * [INFO] org.anarres.jdiagnostics:jdiagnostics:jar:1.0.7:compile + */ + State parseDependency = new State() { + @Override + public void parse(String line) { + if(!line.contains(":")) { + LOGGER.debug("switch to parseModuleName state"); + currentState = parseModuleName; + } else { + String[] array = line.split("\\s+"); + String[] split = array[1].split(":"); + if(split.length < 4) { + return; + } + String groupId = split[0]; + String artifactId = split[1]; + String type = split[2]; + String version = split[3]; + String scope = split[4]; + LOGGER.debug("groupId:{}, \tartifactId:{}, \ttype:{}\tversion:{}, \tscope:{}", groupId, artifactId, type, version, scope); + if("test".equals(scope) || "provided".equals(scope)) { + return; + } + + // add dependency + String jarFilePath = getFilePath(groupId, artifactId, version, ".jar"); + File jarFile = new File(jarFilePath); + if(jarFile.exists()) { + engine.addDependency(new Dependency(jarFile)); + } + } + } + + private String getFilePath(String groupId, String artifactId, String version, String fileSuffix) { + return String.format("%s%s%s%s", mavenLocalRepositoryPath, + getPath(groupId, artifactId, version, File.separator), + getFileName(artifactId, version, "-"), fileSuffix); + } + + private String getPath(String groupId, String artifactId, String version, String separator) { + String replaceMent = "\\".equals(separator) ? "\\\\" : separator; + return getPath(separator, "", groupId.replaceAll("\\.", replaceMent), artifactId, version, ""); + } + + private String getPath(String separator, String... args) { + return String.join(separator, args); + } + + private String getFileName(String artifactId, String version, String separator) { + return String.join(separator, artifactId, version); + } + }; + + State currentState = parseModuleName; + + /** + * Constructs a new processor to consume the output of `bundler-audit`. + * + * @param pomDependency a reference to `gem.lock` dependency + * @param engine a reference to the dependency-check engine + */ + public MvnListProcessor(Dependency pomDependency, Engine engine, MavenSourceAnalyzer pomAnalyzer) { + this.pomDependency = pomDependency; + this.engine = engine; + this.mavenSourceAnalyzer = pomAnalyzer; + this.mavenLocalRepositoryPath = pomAnalyzer.getMavenLocalRepository(); + } + + /** + * Throws any exceptions that occurred during processing. + * + * @throws IOException thrown if an IO Exception occurred + * @throws Exception thrown if a CPE validation exception + * occurred + */ + @Override + public void close() throws Exception { + if (ioException != null) { + addSuppressedExceptions(ioException); + throw ioException; + } + } + + @Override + public void run() { + LOGGER.debug("MvnListProcessor run"); + try (InputStreamReader ir = new InputStreamReader(getInput(), StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(ir)) { + String nextLine; + while ((nextLine = br.readLine()) != null) { + currentState.parse(nextLine); + } + } catch (IOException ex) { + this.ioException = ex; + } + } + + public interface State { + void parse(String line); + } +} diff --git a/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer b/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer index 83cf9dfadad..5363182da43 100644 --- a/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer +++ b/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer @@ -1,3 +1,4 @@ +org.owasp.dependencycheck.analyzer.MavenSourceAnalyzer org.owasp.dependencycheck.analyzer.ArchiveAnalyzer org.owasp.dependencycheck.analyzer.FileNameAnalyzer org.owasp.dependencycheck.analyzer.PEAnalyzer diff --git a/core/src/main/resources/dependencycheck.properties b/core/src/main/resources/dependencycheck.properties index 64fbdbebddc..c10609263ce 100644 --- a/core/src/main/resources/dependencycheck.properties +++ b/core/src/main/resources/dependencycheck.properties @@ -112,7 +112,7 @@ junit.fail.on.cvss=0 # defines if the experimental and retired analyzers can be enabled analyzer.experimental.enabled=false analyzer.retired.enabled=false - +analyzer.mavensource.enabled=true analyzer.jar.enabled=true analyzer.knownexploited.enabled=true analyzer.archive.enabled=true @@ -179,4 +179,4 @@ hosted.suppressions.validforhours=2 ## The following controls the max query limit used in the CPE searches for each ecosystem odc.ecosystem.maxquerylimit.native=1000 -odc.ecosystem.maxquerylimit.default=100 \ No newline at end of file +odc.ecosystem.maxquerylimit.default=100 diff --git a/core/src/test/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzerTest.java b/core/src/test/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzerTest.java new file mode 100644 index 00000000000..3069fcd5640 --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/analyzer/MavenSourceAnalyzerTest.java @@ -0,0 +1,100 @@ +package org.owasp.dependencycheck.analyzer; + +import org.junit.Test; +import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.utils.Settings; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * createTime: 2023/10/30 17:40
+ * + * @author regedit0726 + */ +public class MavenSourceAnalyzerTest extends BaseTest { + + @Test + public void testAnalyze() throws Exception { + + //File file = BaseTest.getResourceAsFile(this, "core/pom.xml"); + File file = new File("pom.xml"); + Dependency result = new Dependency(file); + MavenSourceAnalyzer instance = new MavenSourceAnalyzer(); + instance.initialize(getSettings()); + instance.prepareFileTypeAnalyzer(null); + Engine engine = new Engine(getSettings()); + instance.analyze(result, engine); + assert engine.getDependencies().length > 0; + } + + /** + * Test of getSupportedExtensions method, of class PomAnalyzer. + */ + @Test + public void testAcceptSupportedExtensions() throws Exception { + MavenSourceAnalyzer instance = new MavenSourceAnalyzer(); + instance.initialize(getSettings()); + instance.prepare(null); + instance.setEnabled(true); + String[] files4True = {"pom.xml"}; + for (String name : files4True) { + assertTrue(name, instance.accept(new File(name))); + } + + String[] files4False = {"test.jar", "test.war"}; + for (String name : files4False) { + assertFalse(name, instance.accept(new File(name))); + } + } + + /** + * Test of getName method, of class PomAnalyzer. + */ + @Test + public void testGetName() { + MavenSourceAnalyzer instance = new MavenSourceAnalyzer(); + String expResult = "Maven source Analyzer"; + String result = instance.getName(); + assertEquals(expResult, result); + } + + /** + * Test of getAnalysisPhase method, of class PomAnalyzer. + */ + @Test + public void testGetAnalysisPhase() { + MavenSourceAnalyzer instance = new MavenSourceAnalyzer(); + AnalysisPhase expResult = AnalysisPhase.INFORMATION_COLLECTION; + AnalysisPhase result = instance.getAnalysisPhase(); + assertEquals(expResult, result); + } + + /** + * Test of getAnalyzerEnabledSettingKey method, of class PomAnalyzer. + */ + @Test + public void testGetAnalyzerEnabledSettingKey() { + MavenSourceAnalyzer instance = new MavenSourceAnalyzer(); + String expResult = Settings.KEYS.ANALYZER_MAVENSOURCE_ENABLED; + String result = instance.getAnalyzerEnabledSettingKey(); + assertEquals(expResult, result); + } + + + @Test + public void testClassInformation() { + JarAnalyzer.ClassNameInformation instance = new JarAnalyzer.ClassNameInformation("org/owasp/dependencycheck/analyzer/MavenSourceAnalyzer"); + assertEquals("org/owasp/dependencycheck/analyzer/MavenSourceAnalyzer", instance.getName()); + List expected = Arrays.asList("owasp", "dependencycheck", "analyzer", "mavensourceanalyzer"); + List results = instance.getPackageStructure(); + assertEquals(expected, results); + } +} diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java index ca024ec84e7..97f91e60b85 100644 --- a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java @@ -324,6 +324,11 @@ public static final class KEYS { */ public static final String JUNIT_FAIL_ON_CVSS = "junit.fail.on.cvss"; + /** + * The properties key for whether the pom Analyzer is enabled. + */ + public static final String ANALYZER_MAVENSOURCE_ENABLED = "analyzer.mavensource.enabled"; + /** * The properties key for whether the Jar Analyzer is enabled. */ @@ -626,6 +631,7 @@ public static final class KEYS { * The properties key for the Central search query. */ public static final String ANALYZER_CENTRAL_QUERY = "analyzer.central.query"; + /** * The properties key for whether Central search results will be cached. */