Improved extensibility by using ServiceLoader

This commit is contained in:
Jeremy Long
2012-09-11 22:35:09 -04:00
parent 391a410e5b
commit 0fbf2238af
11 changed files with 366 additions and 114 deletions

View File

@@ -0,0 +1,48 @@
package org.codesecure.dependencycheck.scanner;
/*
* This file is part of DependencyCheck.
*
* DependencyCheck is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DependencyCheck is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DependencyCheck. If not, see http://www.gnu.org/licenses/.
*
* Copyright (c) 2012 Jeremy Long. All Rights Reserved.
*/
import java.util.HashSet;
import java.util.Set;
/**
*
* @author Jeremy Long (jeremy.long@gmail.com)
*/
public abstract class AbstractAnalyzer implements Analyzer {
/**
* Utility method to help in the creation of the extensions set. This constructs
* a new Set that can be used in a final static declaration.<br/><br/>
*
* This implementation was copied from http://stackoverflow.com/questions/2041778/initialize-java-hashset-values-by-construction
*
* @param strings a list of strings to add to the set.
* @return a Set of strings.
*/
protected static Set<String> newHashSet(String... strings) {
Set<String> set = new HashSet<String>();
//yes, in Java7 we could use Array.toList(...) - but I'm trying to keep this Java 6 compliant.
for (String s : strings) {
set.add(s);
}
return set;
}
}

View File

@@ -20,6 +20,7 @@ package org.codesecure.dependencycheck.scanner;
import java.io.File;
import java.io.IOException;
import java.util.Set;
/**
* An interface that defines an Analyzer that is used to identify Dependencies.
@@ -38,4 +39,26 @@ public interface Analyzer {
* @throws IOException is thrown if there is an error reading the dependency file
*/
Dependency insepct(File file) throws IOException;
/**
* Returns a list of supported file extensions. An example would be an analyzer
* that inpected java jar files. The getSupportedExtensions function would return
* a set with a single element "jar".
*
* @return The file extensions supported by this analyzer.
*/
Set<String> getSupportedExtensions();
/**
* Returns the name of the analyzer.
* @return the name of the analyzer.
*/
String getName();
/**
* Returns whether or not this analyzer can process the given extension.
* @param extension the file extension to test for support.
* @return whether or not the specified file extension is supported by tihs analyzer.
*/
boolean supportsExtension(String extension);
}

View File

@@ -0,0 +1,57 @@
package org.codesecure.dependencycheck.scanner;
/*
* This file is part of DependencyCheck.
*
* DependencyCheck is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DependencyCheck is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DependencyCheck. If not, see http://www.gnu.org/licenses/.
*
* Copyright (c) 2012 Jeremy Long. All Rights Reserved.
*/
import java.util.Iterator;
import java.util.ServiceLoader;
/**
*
* @author Jeremy Long (jeremy.long@gmail.com)
*/
public class AnalyzerService {
private static AnalyzerService service;
private ServiceLoader<Analyzer> loader;
/**
* Creates a new instance of AnalyzerService
*/
private AnalyzerService() {
loader = ServiceLoader.load(Analyzer.class);
}
/**
* Retrieve the singleton instance of AnalyzerService.
* @return a singleton AnalyzerService.
*/
public static synchronized AnalyzerService getInstance() {
if (service == null) {
service = new AnalyzerService();
}
return service;
}
/**
* Returns an Iterator for all instances of the Analyzer interface.
* @return an iterator of Analyzers.
*/
public Iterator<Analyzer> getAnalyzers() {
return loader.iterator();
}
}

View File

@@ -23,9 +23,12 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -47,22 +50,29 @@ import org.codesecure.dependencycheck.utils.Checksum;
*
* @author Jeremy Long (jeremy.long@gmail.com)
*/
public class JarAnalyzer implements Analyzer {
public class JarAnalyzer extends AbstractAnalyzer {
private static List<String> IGNORE_LIST;
public JarAnalyzer() {
IGNORE_LIST = new ArrayList<String>();
IGNORE_LIST.add("built-by");
IGNORE_LIST.add("created-by");
IGNORE_LIST.add("license");
IGNORE_LIST.add("build-jdk");
IGNORE_LIST.add("ant-version");
IGNORE_LIST.add("import-package");
IGNORE_LIST.add("export-package");
IGNORE_LIST.add("sealed");
IGNORE_LIST.add("manifest-version");
}
private final static String ANALYZER_NAME = "Jar Analyzer";
/**
* A list of elements in the manifest to ignore.
*/
private final static Set<String> IGNORE_LIST = newHashSet(
"built-by",
"created-by",
"license",
"build-jdk",
"ant-version",
"import-package",
"export-package",
"sealed",
"manifest-version",
"archiver-version",
"classpath",
"bundle-manifestversion");
private final static Set<String> extensions = newHashSet("jar");
/**
* item in some manifest, should be considered medium confidence.
*/
@@ -80,6 +90,31 @@ public class JarAnalyzer implements Analyzer {
*/
private static final String BUNDLE_VENDOR = "Bundle-Vendor"; //: Apache Software Foundation
/**
* Returns a list of file extensions supported by this analyzer.
* @return a list of file extensions supported by this analyzer.
*/
public Set<String> getSupportedExtensions() {
return extensions;
}
/**
* Returns the name of the analyzer.
* @return the name of the analyzer.
*/
public String getName() {
return ANALYZER_NAME;
}
/**
* Returns whether or not this analyzer can process the given extension.
* @param extension the file extension to test for support.
* @return whether or not the specified file extension is supported by tihs analyzer.
*/
public boolean supportsExtension(String extension) {
return extensions.contains(extension);
}
/**
* An enumeration to keep track of the characters in a string as it is being
* read in one character at a time.
@@ -331,7 +366,7 @@ public class JarAnalyzer implements Analyzer {
}
/**
* <p>Reads the manifest from the JAR file and collects the:</p>
* <p>Reads the manifest from the JAR file and collects the entries. Some key entries are:</p>
* <ul><li>Implementation Title</li>
* <li>Implementation Version</li>
* <li>Implementation Vendor</li>
@@ -342,7 +377,8 @@ public class JarAnalyzer implements Analyzer {
* <li>Bundle Description</li>
* <li>Main Class</li>
* </ul>
*
* However, all but a handful of specific entries are read in.
*
* @param dependency A reference to the dependency.
* @throws IOException if there is an issue reading the JAR file.
*/
@@ -382,7 +418,7 @@ public class JarAnalyzer implements Analyzer {
} else {
key = key.toLowerCase();
if (!IGNORE_LIST.contains(key)) {
if (!IGNORE_LIST.contains(key) && !key.contains("license") && !key.endsWith("jdk")) {
if (key.contains("version")) {
versionEvidence.addEvidence(source, key, value, Evidence.Confidence.MEDIUM);
} else if (key.contains("title")) {

View File

@@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
@@ -44,9 +45,9 @@ public class Scanner {
*/
protected List<Dependency> dependencies = new ArrayList<Dependency>();
/**
* A Map of analyzers - the key is the file extension.
* A List of analyzers.
*/
protected Map<String, Analyzer> analyzers = new HashMap<String, Analyzer>();
protected List<Analyzer> analyzers = new ArrayList<Analyzer>();
/**
* Creates a new Scanner.
@@ -59,56 +60,20 @@ public class Scanner {
* Loads the analyzers specified in the configuration file (or system properties).
*/
private void loadAnalyzers() {
Map<String, String> associations = Settings.getPropertiesByPrefix(KEYS.FILE_EXTENSION_ANALYZER_ASSOCIATION_PREFIX);
for (Map.Entry<String, String> entry : associations.entrySet()) {
addAnalyzer(entry.getKey(), entry.getValue());
AnalyzerService service = AnalyzerService.getInstance();
Iterator<Analyzer> iterator = service.getAnalyzers();
while(iterator.hasNext()) {
Analyzer a = iterator.next();
analyzers.add(a);
}
}
/**
* Adds an Analyzer to the collection of analyzers and associates the
* analyzer with a file extension.
*
* If the specified class does not implement 'org.codesecure.dependencycheck.detect.Analyzer'
* the load will fail mostly silently - only writting the failure to the log file.
*
* @param extension the file extension that this analyzer can analyze.
* @param className the fully qualified classname of the Analyzer.
*/
public final void addAnalyzer(String extension, String className) {
ClassLoader loader = this.getClass().getClassLoader();
try {
Class analyzer = loader.loadClass(className);
boolean implmnts = false;
for (Class p : analyzer.getInterfaces()) {
if (org.codesecure.dependencycheck.scanner.Analyzer.class.isAssignableFrom(p)) {
implmnts = true;
break;
}
}
if (implmnts) {
this.analyzers.put(extension, (Analyzer) analyzer.newInstance());
} else {
String msg = String.format("Class '%s' does not implement org.codesecure.dependencycheck.scanner.Analyzer and cannot be loaded as an Analyzer for extension '%s'.", className, extension);
Logger.getLogger(Scanner.class.getName()).log(Level.WARNING, msg);
}
} catch (ClassNotFoundException ex) {
Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* Get the Map of the analyzers.
* Get the List of the analyzers.
*
* @return the analyzers loaded
*/
public Map<String, Analyzer> getAnalyzers() {
public List<Analyzer> getAnalyzers() {
return analyzers;
}
@@ -170,24 +135,24 @@ public class Scanner {
String fileName = file.getName();
String extension = getFileExtension(fileName);
if (extension != null) {
if (analyzers.containsKey(extension)) {
Analyzer a = analyzers.get(extension);
try {
Dependency dependency = a.insepct(file);
dependencies.add(dependency);
} catch (IOException ex) {
String msg = String.format("IOException occured while scanning the file '%s'.", file.toString());
Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, msg, ex);
for (Analyzer a : analyzers) {
if (a.supportsExtension(extension)) {
try {
Dependency dependency = a.insepct(file);
if (dependency != null) {
dependencies.add(dependency);
break;
}
} catch (IOException ex) {
String msg = String.format("IOException occured while scanning the file '%s'.", file.toString());
Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, msg, ex);
}
}
} else {
String msg = String.format("No analyzer is configured for files of type '%s'. The file, '%s', was not analyzed.", extension, file.toString());
Logger.getLogger(Scanner.class.getName()).log(Level.WARNING, msg);
}
} else {
String msg = String.format("No files extension found on file '%s'. The file was not analyzed.", file.toString());
Logger.getLogger(Scanner.class.getName()).log(Level.WARNING, msg);
}
}
/**

View File

@@ -64,8 +64,17 @@ public class Settings {
* The properties key prefix for the analyzer assocations.
*/
public static final String FILE_EXTENSION_ANALYZER_ASSOCIATION_PREFIX = "file.extension.analyzer.association.";
/**
* The properties key for the proxy url.
*/
public static final String PROXY_URL = "proxy.url";
/**
* The properties key for the proxy port - this must be an integer value.
*/
public static final String PROXY_PORT = "proxy.port";
/**
* The properties key for the connection timeout.
*/
public static final String CONNECTION_TIMEOUT = "connection.timeout";
}
private static final String PROPERTIES_FILE = "dependencycheck.properties";
@@ -124,44 +133,7 @@ public class Settings {
public static String getString(String key) {
return System.getProperty(key, instance.props.getProperty(key));
}
/**
* Returns a map of properties selected by a given prefix. For isntance
* if you have five properties that started off with "org.codesecure.name"
* you could get a collection of those properties by calling this method.
*
* NOTE: The prefix is removed from the given properties when returned.
*
* @param prefix the prefix used to search the property collections for.
* @return a Map of properties found.
*/
public static Map<String, String> getPropertiesByPrefix(String prefix) {
Map<String, String> ret = new HashMap<String, String>();
Properties properties = instance.props;
for (Enumeration<Object> e = properties.keys(); e.hasMoreElements(); ) {
Object o = e.nextElement();
if (o instanceof String) {
String key = (String) o;
if (key.startsWith(prefix)) {
String ext = key.substring(prefix.length());
ret.put(ext, properties.getProperty(key));
}
}
}
properties = System.getProperties();
for (Enumeration<Object> e = properties.keys(); e.hasMoreElements(); ) {
Object o = e.nextElement();
if (o instanceof String) {
String key = (String) o;
if (key.startsWith(prefix)) {
String ext = key.substring(prefix.length() + 1);
ret.put(ext, properties.getProperty(key));
}
}
}
return ret;
}
/**
* Returns a integer value from the properties file. If the value was specified as a
* system property or passed in via the -Dprop=value argument - this method

View File

@@ -7,4 +7,3 @@ cpe.downloadfrequency=1
cve=store/cve
osvdb=store/osvdb
file.extension.analyzer.association.jar=org.codesecure.dependencycheck.scanner.JarAnalyzer

View File

@@ -0,0 +1 @@
org.codesecure.dependencycheck.scanner.JarAnalyzer

View File

@@ -0,0 +1,51 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.codesecure.dependencycheck.scanner;
import java.util.Set;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
/**
*
* @author Jeremy Long (jeremy.long@gmail.com)
*/
public class AbstractAnalyzerTest {
public AbstractAnalyzerTest() {
}
@BeforeClass
public static void setUpClass() throws Exception {
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Before
public void setUp() {
}
@After
public void tearDown() {
}
/**
* Test of newHashSet method, of class AbstractAnalyzer.
*/
@Test
public void testNewHashSet() {
System.out.println("newHashSet");
Set result = AbstractAnalyzer.newHashSet("one","two");
assertEquals(2, result.size());
assertTrue(result.contains("one"));
assertTrue(result.contains("two"));
}
}

View File

@@ -0,0 +1,60 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.codesecure.dependencycheck.scanner;
import java.util.Set;
import java.util.Iterator;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
/**
*
* @author Jeremy Long (jeremy.long@gmail.com)
*/
public class AnalyzerServiceTest {
public AnalyzerServiceTest() {
}
@BeforeClass
public static void setUpClass() throws Exception {
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Before
public void setUp() {
}
@After
public void tearDown() {
}
/**
* Test of getAnalyzers method, of class AnalyzerService.
*/
@Test
public void testGetAnalyzers() {
System.out.println("getAnalyzers");
AnalyzerService instance = AnalyzerService.getInstance();
Iterator<Analyzer> result = instance.getAnalyzers();
boolean found = false;
while (result.hasNext()) {
Analyzer a = result.next();
Set<String> e = a.getSupportedExtensions();
if (e.contains("jar")) {
found = true;
}
}
assertTrue("JarAnalyzer loaded", found);
}
}

View File

@@ -4,7 +4,9 @@
*/
package org.codesecure.dependencycheck.scanner;
import java.util.HashSet;
import java.io.File;
import java.util.Set;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -52,4 +54,42 @@ public class JarAnalyzerTest {
assertTrue(result.getVendorEvidence().toString().toLowerCase().contains("apache"));
assertTrue(result.getVendorEvidence().getWeighting().contains("apache"));
}
/**
* Test of getSupportedExtensions method, of class JarAnalyzer.
*/
@Test
public void testGetSupportedExtensions() {
System.out.println("getSupportedExtensions");
JarAnalyzer instance = new JarAnalyzer();
Set expResult = new HashSet();
expResult.add("jar");
Set result = instance.getSupportedExtensions();
assertEquals(expResult, result);
}
/**
* Test of getName method, of class JarAnalyzer.
*/
@Test
public void testGetName() {
System.out.println("getName");
JarAnalyzer instance = new JarAnalyzer();
String expResult = "Jar Analyzer";
String result = instance.getName();
assertEquals(expResult, result);
}
/**
* Test of supportsExtension method, of class JarAnalyzer.
*/
@Test
public void testSupportsExtension() {
System.out.println("supportsExtension");
String extension = "jar";
JarAnalyzer instance = new JarAnalyzer();
boolean expResult = true;
boolean result = instance.supportsExtension(extension);
assertEquals(expResult, result);
}
}