diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java index 851ed70ba..db54d1ab6 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java @@ -19,12 +19,14 @@ package org.owasp.dependencycheck.analyzer; import java.io.File; import java.io.FileFilter; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -42,6 +44,7 @@ import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.zip.ZipEntry; + import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; @@ -53,11 +56,11 @@ import org.owasp.dependencycheck.dependency.Dependency; import org.owasp.dependencycheck.dependency.EvidenceCollection; import org.owasp.dependencycheck.exception.InitializationException; import org.owasp.dependencycheck.utils.FileFilterBuilder; -import org.owasp.dependencycheck.xml.pom.License; -import org.owasp.dependencycheck.xml.pom.PomUtils; -import org.owasp.dependencycheck.xml.pom.Model; import org.owasp.dependencycheck.utils.FileUtils; import org.owasp.dependencycheck.utils.Settings; +import org.owasp.dependencycheck.xml.pom.License; +import org.owasp.dependencycheck.xml.pom.Model; +import org.owasp.dependencycheck.xml.pom.PomUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -168,6 +171,21 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer { */ private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build(); + /** + * The expected first bytes when reading a zip file. + */ + private static final byte[] ZIP_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x03, 0x04}; + + /** + * The expected first bytes when reading an empty zip file. + */ + private static final byte[] ZIP_EMPTY_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x05, 0x06}; + + /** + * The expected first bytes when reading a spanned zip file. + */ + private static final byte[] ZIP_SPANNED_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x07, 0x08}; + // // /** @@ -230,8 +248,11 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer { && (fileName.endsWith("-sources.jar") || fileName.endsWith("-javadoc.jar") || fileName.endsWith("-src.jar") - || fileName.endsWith("-doc.jar"))) { + || fileName.endsWith("-doc.jar") + || isMacOSMetaDataFile(dependency, engine)) + || !isZipFile(dependency)) { engine.getDependencies().remove(dependency); + return; } final boolean hasManifest = parseManifest(dependency, classNames); final boolean hasPOM = analyzePOM(dependency, classNames, engine); @@ -242,6 +263,62 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer { } } + /** + * Checks if the given dependency appears to be a macOS metadata file, returning true if its filename starts with a + * ._ prefix and if there is another dependency with the same filename minus the ._ prefix, otherwise it returns + * false. + * + * @param dependency the dependency to check if it's a macOS metadata file + * @param engine the engine that is scanning the dependencies + * @return whether or not the given dependency appears to be a macOS metadata file + */ + private boolean isMacOSMetaDataFile(final Dependency dependency, final Engine engine) { + final String fileName = Paths.get(dependency.getActualFilePath()).getFileName().toString(); + return fileName.startsWith("._") && hasDependencyWithFilename(engine.getDependencies(), fileName.substring(2)); + } + + /** + * Iterates through the given list of dependencies and returns true when it finds a dependency with a filename + * matching the given filename, otherwise returns false. + * + * @param dependencies the dependencies to search within + * @param fileName the filename to search for + * @return whether or not the given dependencies contain a dependency with the given filename + */ + private boolean hasDependencyWithFilename(final List dependencies, final String fileName) { + for (final Dependency dependency : dependencies) { + if (Paths.get(dependency.getActualFilePath()).getFileName().toString().toLowerCase() + .equals(fileName.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Attempts to read the first bytes of the given dependency (using its actual file path) and returns true if they + * match the expected first bytes of a zip file, which may be empty or spanned. If they don't match, or if the file + * could not be read, then it returns false. + * + * @param dependency the dependency to check if it's a zip file + * @return whether or not the given dependency appears to be a zip file from its first bytes + */ + private boolean isZipFile(final Dependency dependency) { + final byte[] buffer = new byte[4]; + try (final FileInputStream fileInputStream = new FileInputStream(dependency.getActualFilePath())) { + fileInputStream.read(buffer); + if (Arrays.equals(buffer, ZIP_FIRST_BYTES) || Arrays.equals(buffer, ZIP_EMPTY_FIRST_BYTES) || + Arrays.equals(buffer, ZIP_SPANNED_FIRST_BYTES)) { + return true; + } + } + catch (Exception e) { + LOGGER.warn("Unable to check if '{}' is a zip file", dependency.getActualFilePath()); + LOGGER.trace("", e); + } + return false; + } + /** * Attempts to find a pom.xml within the JAR file. If found it extracts * information and adds it to the evidence. This will attempt to interpolate diff --git a/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/JarAnalyzerTest.java b/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/JarAnalyzerTest.java index df9931548..4d9684f3d 100644 --- a/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/JarAnalyzerTest.java +++ b/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/JarAnalyzerTest.java @@ -17,24 +17,23 @@ */ package org.owasp.dependencycheck.analyzer; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; +import org.apache.commons.io.FileUtils; import org.junit.Test; import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.Engine; import org.owasp.dependencycheck.dependency.Dependency; import org.owasp.dependencycheck.dependency.Evidence; import org.owasp.dependencycheck.utils.Settings; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author Jeremy Long */ @@ -176,4 +175,31 @@ public class JarAnalyzerTest extends BaseTest { List results = instance.getPackageStructure(); assertEquals(expected, results); } + + @Test + public void testAnalyzeDependency_SkipsMacOSMetaDataFile() throws Exception { + JarAnalyzer instance = new JarAnalyzer(); + Dependency macOSMetaDataFile = new Dependency(); + macOSMetaDataFile + .setActualFilePath(FileUtils.getFile("src", "test", "resources", "._avro-ipc-1.5.0.jar").getAbsolutePath()); + macOSMetaDataFile.setFileName("._avro-ipc-1.5.0.jar"); + Dependency actualJarFile = new Dependency(); + actualJarFile.setActualFilePath(BaseTest.getResourceAsFile(this, "avro-ipc-1.5.0.jar").getAbsolutePath()); + actualJarFile.setFileName("avro-ipc-1.5.0.jar"); + Engine engine = new Engine(); + engine.setDependencies(Arrays.asList(macOSMetaDataFile, actualJarFile)); + instance.analyzeDependency(macOSMetaDataFile, engine); + } + + @Test + public void testAnalyseDependency_SkipsNonZipFile() throws Exception { + JarAnalyzer instance = new JarAnalyzer(); + Dependency textFileWithJarExtension = new Dependency(); + textFileWithJarExtension + .setActualFilePath(BaseTest.getResourceAsFile(this, "textFileWithJarExtension.jar").getAbsolutePath()); + textFileWithJarExtension.setFileName("textFileWithJarExtension.jar"); + Engine engine = new Engine(); + engine.setDependencies(Collections.singletonList(textFileWithJarExtension)); + instance.analyzeDependency(textFileWithJarExtension, engine); + } } diff --git a/dependency-check-core/src/test/resources/._avro-ipc-1.5.0.jar b/dependency-check-core/src/test/resources/._avro-ipc-1.5.0.jar new file mode 100644 index 000000000..1765527a0 Binary files /dev/null and b/dependency-check-core/src/test/resources/._avro-ipc-1.5.0.jar differ diff --git a/dependency-check-core/src/test/resources/avro-ipc-1.5.0.jar b/dependency-check-core/src/test/resources/avro-ipc-1.5.0.jar new file mode 100644 index 000000000..72fd9df60 Binary files /dev/null and b/dependency-check-core/src/test/resources/avro-ipc-1.5.0.jar differ diff --git a/dependency-check-core/src/test/resources/textFileWithJarExtension.jar b/dependency-check-core/src/test/resources/textFileWithJarExtension.jar new file mode 100644 index 000000000..9e15767b1 --- /dev/null +++ b/dependency-check-core/src/test/resources/textFileWithJarExtension.jar @@ -0,0 +1 @@ +text file with jar extension \ No newline at end of file