diff --git a/pom.xml b/pom.xml index a6dc868..cfbe269 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,6 @@ 1.12 1.19 2.6 - 2.11.4 1.7.30 5.3.2 @@ -69,7 +68,6 @@ - com.fasterxml.jackson.core diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DotNetFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DotNetFingerprinter.java new file mode 100644 index 0000000..b9d4260 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DotNetFingerprinter.java @@ -0,0 +1,33 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.LayerPath; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.packages.DotNetParser; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; + +public class DotNetFingerprinter implements LayerFileHandler { + + private final DotNetParser parser; + + public DotNetFingerprinter(DotNetParser parser) { + this.parser = parser; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, LayerPath layerPath) throws IOException { + if (parser.supports(name, entry)) { + File tmpFile = Paths.get(layerPath.getPath(), name).toFile(); + if (tmpFile.isFile()) { + layerPath.getLayer().addPackages(parser.parse(tmpFile, image.getOperatingSystem() == null + ? layerPath.getLayer().getOperatingSystem() : image.getOperatingSystem())); + } + } + } + +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java index f10cfd3..c450835 100644 --- a/src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java @@ -1,5 +1,6 @@ package com.rapid7.container.analyzer.docker.model.json; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.rapid7.container.analyzer.docker.model.image.ImageId; @@ -18,15 +19,15 @@ }) public interface Manifest { - public List getLayers(); + List getLayers(); - public default List getLayerBlobIds() { + default List getLayerBlobIds() { return getLayers(); } - public ImageId getImageId(); + ImageId getImageId(); - public long getSize(); + long getSize(); - public String getType(); + String getType(); } diff --git a/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java index 2b80c50..ed8bc32 100644 --- a/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java +++ b/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java @@ -1,5 +1,6 @@ package com.rapid7.container.analyzer.docker.os; +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/DotNetParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/DotNetParser.java new file mode 100644 index 0000000..357446c --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/DotNetParser.java @@ -0,0 +1,73 @@ +package com.rapid7.container.analyzer.docker.packages; + +import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; +import com.rapid7.container.analyzer.docker.model.image.Package; +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class DotNetParser implements PackageParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(DotNetParser.class); + private static final Pattern DOT_NET_PATTERN = Pattern.compile(".*(?i)(\\.nuspec)$"); + + @Override + public boolean supports(String name, TarArchiveEntry entry) { + return !entry.isSymbolicLink() && DOT_NET_PATTERN.matcher(name).matches(); + } + + @Override + public Set parse(File input, OperatingSystem operatingSystem) throws IOException { + + Set packages = new HashSet<>(); + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbFactory.newDocumentBuilder(); + Document document = db.parse(input); + document.getDocumentElement().normalize(); + + NodeList nodeList = document.getElementsByTagName("package"); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + + String source = input.getName(); + String name = getValueForAttribute(element, "id"); + String version = getValueForAttribute(element, "version"); + String description = getValueForAttribute(element, "description"); + packages.add(new Package(source, PackageType.DOTNET, operatingSystem, name, version, description, null, null, null, null)); + } + } + } catch (ParserConfigurationException | SAXException exception) { + LOGGER.error("Could not parse .nuspec file", exception); + } + + return packages; + } + + + private String getValueForAttribute(Element element, String attribute) { + NodeList nodeList = element.getElementsByTagName(attribute); + if (nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent(); + } else { + return null; + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/settings/CustomParserSettingsBuilder.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/settings/CustomParserSettingsBuilder.java new file mode 100644 index 0000000..6687305 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/settings/CustomParserSettingsBuilder.java @@ -0,0 +1,49 @@ +package com.rapid7.container.analyzer.docker.packages.settings; + +import com.google.common.collect.ImmutableMap; +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.fingerprinter.DotNetFingerprinter; +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import com.rapid7.container.analyzer.docker.packages.DotNetParser; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class CustomParserSettingsBuilder { + + // Mappings for customer parsers + private static final ImmutableMap FINGERPRINTER_MAPPINGS = ImmutableMap.of( + PackageType.DOTNET, new DotNetFingerprinter(new DotNetParser()) + ); + + public static final CustomParserSettingsBuilder ALL = CustomParserSettingsBuilder.builder() + .addFingerprinters(FINGERPRINTER_MAPPINGS.keySet()); + + private final Set enabledFingerprinters = new HashSet<>(); + + private CustomParserSettingsBuilder() { + } + + public static CustomParserSettingsBuilder builder() { + return new CustomParserSettingsBuilder(); + } + + public CustomParserSettingsBuilder addFingerprinter(PackageType packageType) { + LayerFileHandler handler = FINGERPRINTER_MAPPINGS.get(packageType); + if (handler != null) { + enabledFingerprinters.add(handler); + } + return this; + } + + public CustomParserSettingsBuilder addFingerprinters(Collection packageTypes) { + for (PackageType packageType : packageTypes) { + addFingerprinter(packageType); + } + return this; + } + + public Set getFingerprinters() { + return enabledFingerprinters; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java b/src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java index 6f46578..30a3621 100644 --- a/src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java +++ b/src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java @@ -8,6 +8,7 @@ import com.rapid7.container.analyzer.docker.analyzer.LayerExtractor; import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; import com.rapid7.container.analyzer.docker.fingerprinter.ApkgFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.DotNetFingerprinter; import com.rapid7.container.analyzer.docker.fingerprinter.DpkgFingerprinter; import com.rapid7.container.analyzer.docker.fingerprinter.FileFingerprinter; import com.rapid7.container.analyzer.docker.fingerprinter.OsReleaseFingerprinter; @@ -31,10 +32,12 @@ import com.rapid7.container.analyzer.docker.model.json.TarManifestJson; import com.rapid7.container.analyzer.docker.os.Fingerprinter; import com.rapid7.container.analyzer.docker.packages.ApkgParser; +import com.rapid7.container.analyzer.docker.packages.DotNetParser; import com.rapid7.container.analyzer.docker.packages.DpkgParser; import com.rapid7.container.analyzer.docker.packages.OwaspDependencyParser; import com.rapid7.container.analyzer.docker.packages.PacmanPackageParser; import com.rapid7.container.analyzer.docker.packages.RpmPackageParser; +import com.rapid7.container.analyzer.docker.packages.settings.CustomParserSettingsBuilder; import com.rapid7.container.analyzer.docker.packages.settings.OwaspDependencyParserSettingsBuilder; import com.rapid7.container.analyzer.docker.util.InstantParser; import com.rapid7.container.analyzer.docker.util.InstantParserModule; @@ -57,8 +60,10 @@ import java.util.Objects; import java.util.zip.GZIPInputStream; import java.util.zip.ZipException; +import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; @@ -80,7 +85,15 @@ public DockerImageAnalyzerService(String rpmDockerImage) { this(rpmDockerImage, OwaspDependencyParserSettingsBuilder.EXPERIMENTAL); } + public DockerImageAnalyzerService(String rpmDockerImage, CustomParserSettingsBuilder customBuilder) { + this(rpmDockerImage, OwaspDependencyParserSettingsBuilder.EXPERIMENTAL, customBuilder); + } + public DockerImageAnalyzerService(String rpmDockerImage, OwaspDependencyParserSettingsBuilder builder) { + this(rpmDockerImage, builder, CustomParserSettingsBuilder.builder()); + } + + public DockerImageAnalyzerService(String rpmDockerImage, OwaspDependencyParserSettingsBuilder owaspBuilder, CustomParserSettingsBuilder customBuilder) { objectMapper = new ObjectMapper(); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.registerModule(new InstantParserModule()); @@ -92,7 +105,8 @@ public DockerImageAnalyzerService(String rpmDockerImage, OwaspDependencyParserSe layerHandlers.add(new DpkgFingerprinter(new DpkgParser())); layerHandlers.add(new ApkgFingerprinter(new ApkgParser())); layerHandlers.add(new PacmanFingerprinter(new PacmanPackageParser())); - layerHandlers.add(new OwaspDependencyFingerprinter(new OwaspDependencyParser(builder))); + layerHandlers.add(new OwaspDependencyFingerprinter(new OwaspDependencyParser(owaspBuilder))); + layerHandlers.addAll(customBuilder.getFingerprinters()); } public void addFileHandler(LayerFileHandler handler) { @@ -250,7 +264,12 @@ else if (previousLayer != null && previousLayer.getCreated() != null) } public Manifest parseManifest(File file) throws JsonParseException, JsonMappingException, IOException { - return objectMapper.readValue(file, Manifest.class); // TODO: polymorphic + // TODO: polymorphic + try (GZIPInputStream stream = new GZIPInputStream(new FileInputStream(file))) { + return objectMapper.readValue(stream, Manifest.class); + } catch (ZipException exception) { + return objectMapper.readValue(file, Manifest.class); + } } public Configuration parseConfiguration(File file) throws JsonParseException, JsonMappingException, IOException { @@ -315,11 +334,13 @@ private void processLayer(Image image, Configuration configuration, Layer layer, if (tar.length() < 100) return; - try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(tar), 65536))) { + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(tar), 65536)))) { processLayerTar(image, configuration, layer, tar, tarIn); - } catch (ZipException ze) { - try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new BufferedInputStream(new FileInputStream(tar), 65536))) { - processLayerTar(image, configuration, layer, tar, tarIn); + } catch (IOException exception) { + if (exception.getMessage().equals("Input is not in the .gz format") || exception.getCause() instanceof ZipException) { + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new BufferedInputStream(new FileInputStream(tar), 65536))) { + processLayerTar(image, configuration, layer, tar, tarIn); + } } } } diff --git a/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java b/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java index e489252..58547ef 100644 --- a/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java +++ b/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java @@ -2,12 +2,23 @@ import com.rapid7.container.analyzer.docker.model.image.Image; import com.rapid7.container.analyzer.docker.model.image.ImageId; +import com.rapid7.container.analyzer.docker.model.image.Layer; import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; +import com.rapid7.container.analyzer.docker.model.image.Package; +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import com.rapid7.container.analyzer.docker.packages.settings.CustomParserSettingsBuilder; +import com.rapid7.container.analyzer.docker.packages.settings.OwaspDependencyParserSettingsBuilder; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; class DockerImageAnalyzerServiceTest { @@ -57,4 +68,28 @@ public void testCentos() throws IOException { assertEquals(expectedPackages, image.getPackages().size()); assertEquals("A set of system configuration and setup files", image.getPackages().stream().findFirst().get().getDescription()); } + + + @Test + public void parseDotnet() throws FileNotFoundException, IOException { + // given + File tarFile = new File(getClass().getClassLoader().getResource("containers/dotnet-packages.tar").getFile()); + + // when + DockerImageAnalyzerService analyzer = new DockerImageAnalyzerService(null, OwaspDependencyParserSettingsBuilder.builder(), + CustomParserSettingsBuilder.builder().addFingerprinter(PackageType.DOTNET)); + Path tmpdir = Files.createTempDirectory("r7dia"); + Image image = analyzer.analyze(tarFile, tmpdir.toString()); + + // then + List layersWithDotnetPackages = image.getLayers().stream() + .filter(layer -> layer.getPackages().stream().anyMatch(pkg -> pkg.getType() == PackageType.DOTNET)) + .collect(Collectors.toList()); + Set dotnetPackages = image.getPackages().stream() + .filter(pkg -> pkg.getType() == PackageType.DOTNET) + .collect(Collectors.toSet()); + + assertThat(layersWithDotnetPackages.size(), is(1)); + assertThat(dotnetPackages.size(), is(5)); + } } diff --git a/src/test/resources/containers/dotnet-packages.tar b/src/test/resources/containers/dotnet-packages.tar new file mode 100644 index 0000000..188e801 Binary files /dev/null and b/src/test/resources/containers/dotnet-packages.tar differ