From 4f45d1ba524d83af13bdd3a4ce39122533521894 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Mon, 15 Oct 2018 15:17:29 -0700 Subject: [PATCH] Initial commit --- .checkstyle | 10 + .gitignore | 90 + README.md | 38 + google_checks.xml | 160 ++ pom.xml | 244 +++ .../docker/analyzer/ImageHandler.java | 9 + .../docker/analyzer/LayerExtractor.java | 9 + .../docker/analyzer/LayerFileHandler.java | 13 + .../fingerprinter/ApkgFingerprinter.java | 25 + .../fingerprinter/DpkgFingerprinter.java | 24 + .../fingerprinter/FileFingerprinter.java | 87 + .../fingerprinter/OsReleaseFingerprinter.java | 60 + .../fingerprinter/PacmanFingerprinter.java | 26 + .../fingerprinter/RpmFingerprinter.java | 116 + .../fingerprinter/WhiteoutImageHandler.java | 85 + .../analyzer/docker/model/Digest.java | 28 + .../analyzer/docker/model/HashId.java | 58 + .../analyzer/docker/model/HashType.java | 27 + .../analyzer/docker/model/image/File.java | 133 ++ .../analyzer/docker/model/image/FileId.java | 23 + .../docker/model/image/FileState.java | 17 + .../analyzer/docker/model/image/FileType.java | 30 + .../model/image/IdentifiablePackage.java | 28 + .../analyzer/docker/model/image/Image.java | 177 ++ .../analyzer/docker/model/image/ImageId.java | 25 + .../docker/model/image/ImageType.java | 27 + .../analyzer/docker/model/image/Layer.java | 199 ++ .../docker/model/image/LayerFile.java | 26 + .../analyzer/docker/model/image/LayerId.java | 39 + .../docker/model/image/OperatingSystem.java | 109 + .../analyzer/docker/model/image/Package.java | 191 ++ .../docker/model/image/PackageId.java | 23 + .../docker/model/image/PackageType.java | 27 + .../docker/model/image/TypeSafeId.java | 75 + .../docker/model/image/TypeSafeLongId.java | 37 + .../image/json/ImageModelObjectMapper.java | 20 + .../image/json/JacksonImageModelModule.java | 107 + .../json/mixin/IdentifiablePackageMixin.java | 113 + .../model/image/json/mixin/ImageIdMixin.java | 29 + .../model/image/json/mixin/ImageMixin.java | 76 + .../model/image/json/mixin/LayerIdMixin.java | 12 + .../model/image/json/mixin/LayerMixin.java | 124 ++ .../json/mixin/OperatingSystemMixin.java | 37 + .../model/image/json/mixin/PackageMixin.java | 112 + .../docker/model/json/Configuration.java | 15 + .../model/json/ConfigurationJsonV2.java | 91 + .../docker/model/json/ContainerConfig.java | 31 + .../docker/model/json/FileSystemJson.java | 41 + .../docker/model/json/FsLayerJson.java | 29 + .../docker/model/json/HistoryJson.java | 73 + .../analyzer/docker/model/json/LayerJson.java | 67 + .../docker/model/json/LayerManifestV1.java | 104 + .../docker/model/json/LayerReferenceJson.java | 51 + .../analyzer/docker/model/json/Manifest.java | 32 + .../docker/model/json/ManifestJsonV2S1.java | 191 ++ .../docker/model/json/ManifestJsonV2S2.java | 82 + .../docker/model/json/TarManifestJson.java | 90 + .../model/json/V1CompatibleHistory.java | 17 + .../docker/model/json/mixin/HashIdMixin.java | 29 + .../analyzer/docker/os/Fingerprinter.java | 211 ++ .../analyzer/docker/packages/ApkgParser.java | 54 + .../analyzer/docker/packages/DpkgParser.java | 54 + .../docker/packages/PackageParser.java | 163 ++ .../docker/packages/PacmanPackageParser.java | 97 + .../docker/packages/RpmPackageParser.java | 64 + .../analyzer/docker/recog/RecogClient.java | 62 + .../service/DockerImageAnalyzerService.java | 343 +++ .../util/InstantCustomDeserializer.java | 14 + .../analyzer/docker/util/InstantParser.java | 42 + .../docker/util/InstantParserModule.java | 14 + .../docker/packages/DpkgParserTest.java | 57 + .../docker/test/ImageAnalyzerMockConfig.java | 29 + .../docker/test/ImageAnalyzerTester.java | 145 ++ .../docker/test/LayerDecompressor.java | 54 + .../docker/test/RecogMatcherService.java | 50 + .../docker/util/InstantParserUnitTests.java | 127 ++ .../analyzer/docker/packages/dpkg-dash.info | 13 + .../docker/packages/dpkg-multiple.info | 35 + .../analyzer/docker/packages/dpkg.info | 1891 +++++++++++++++++ 79 files changed, 7387 insertions(+) create mode 100644 .checkstyle create mode 100644 .gitignore create mode 100644 README.md create mode 100644 google_checks.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/analyzer/ImageHandler.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerExtractor.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerFileHandler.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/ApkgFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DpkgFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/FileFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/OsReleaseFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/PacmanFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/RpmFingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/WhiteoutImageHandler.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/Digest.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/HashId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/HashType.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/File.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/FileId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/FileState.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/FileType.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/IdentifiablePackage.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/Image.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageType.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/Layer.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerFile.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/OperatingSystem.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/Package.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageType.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeLongId.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/ImageModelObjectMapper.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/JacksonImageModelModule.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/IdentifiablePackageMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageIdMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerIdMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/OperatingSystemMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/PackageMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/Configuration.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/ConfigurationJsonV2.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/ContainerConfig.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/FileSystemJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/FsLayerJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/HistoryJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerManifestV1.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerReferenceJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S1.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S2.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/TarManifestJson.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/V1CompatibleHistory.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/model/json/mixin/HashIdMixin.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/packages/ApkgParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/packages/DpkgParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/packages/PackageParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/packages/PacmanPackageParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/packages/RpmPackageParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/recog/RecogClient.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/util/InstantCustomDeserializer.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/util/InstantParser.java create mode 100644 src/main/java/com/rapid7/container/analyzer/docker/util/InstantParserModule.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/packages/DpkgParserTest.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerMockConfig.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/RecogMatcherService.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/util/InstantParserUnitTests.java create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-dash.info create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-multiple.info create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg.info diff --git a/.checkstyle b/.checkstyle new file mode 100644 index 0000000..66b3b31 --- /dev/null +++ b/.checkstyle @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84ba128 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +archaius-core/MySiteConfiguration/ + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log + +# OS generated files # +###################### +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db + +# Editor Files # +################ +*~ +*.swp + +# Gradle Files # +################ +.gradle + +# Build output directies +/target +*/target +/build +*/build + +# uncommitted resources +# temporarily commit recog until plugin resource assembly is fixed +# /src/main/resources/recog + +# IntelliJ specific files/directories +out +.idea +*.ipr +*.iws +*.iml +atlassian-ide-plugin.xml + +# Eclipse specific files/directories +.classpath +.project +.settings +.metadata +*/bin + +# NetBeans specific files/directories +.nbattrs + +# JavaScript +/node_modules +/bower_components + +# Maven +/.repostory/ + + +# PD Build Tools # +################## +.bundle +.vagrant +.builderator +Berksfile +Berksfile.lock +Vagrantfile +VERSION +.recog diff --git a/README.md b/README.md new file mode 100644 index 0000000..b403e4a --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +## Docker Image Analyzer + +Extracts, parses, and analyzes [Docker](https://www.docker.com) images into Java objects with JSON mappings. + +## Getting Started + +Add the dependency to your pom file: + +```xml + + com.rapid7.docker + docker-image-analyzer + 0.1.0 + +``` + +_TODO: Usage example(s)_ + +## Development + +Fork the repository and create a development branch in your fork. _Working from the master branch in your fork is not recommended._ + +1. Open your favorite IDE or text editor +2. Make some changes +3. Add some tests if needed +4. Run the tests +5. Push your changes +6. Open a pull request + +You can use `mvn clean install` to clean compile, run checkstyle, and run all tests. + +#### Code Style + +docker-image-analyzer uses a variation of the Google Java code style, enforced with Checkstyle. Please make sure your changes adhere to this style before submitting a pull request. + +## Testing + +Run `mvn test` or `mvn clean install`. \ No newline at end of file diff --git a/google_checks.xml b/google_checks.xml new file mode 100644 index 0000000..9477eab --- /dev/null +++ b/google_checks.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..dc7eba8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,244 @@ + + + 4.0.0 + + com.rapid7.docker + docker-image-analyzer + 0.1.0-SNAPSHOT + jar + https://www.rapid7.com + Extracts, parses, and analyzes Docker images into Java objects with JSON mappings. + + + scm:git:git@github.com:rapid7/docker-image-analyzer.git + scm:git:git@github.com:rapid7/docker-image-analyzer.git + https://github.com/rapid7/docker-image-analyzer + HEAD + + + + 1.0.5 + 1.8 + 1.8 + UTF-8 + + + + + + org.rapid7.recog + recog-java + ${r7.recog.java.version} + + + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.0 + + + + com.fasterxml.jackson.core + jackson-core + 2.9.5 + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.5 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.9.5 + + + + commons-codec + commons-codec + 1.11 + + + + commons-io + commons-io + 2.6 + + + + org.apache.commons + commons-compress + 1.13 + + + + org.hamcrest + hamcrest-core + 1.3 + test + + + + org.hamcrest + hamcrest-library + 1.3 + test + + + + org.junit.jupiter + junit-jupiter-api + 5.2.0-M1 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0-M1 + test + + + + org.mockito + mockito-core + 2.18.0 + test + + + + org.slf4j + slf4j-api + 1.7.25 + + + + org.slf4j + slf4j-simple + 1.7.25 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:none + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + + + + + org.junit.platform + junit-platform-surefire-provider + 1.2.0-M1 + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + ${basedir}/google_checks.xml + warning + + + + checkstyle + + check + + + ${basedir}/google_checks.xml + + + + + + com.puppycrawl.tools + checkstyle + 7.6 + compile + + + + + + diff --git a/src/main/java/com/rapid7/container/analyzer/docker/analyzer/ImageHandler.java b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/ImageHandler.java new file mode 100644 index 0000000..3a6a1b4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/ImageHandler.java @@ -0,0 +1,9 @@ +package com.rapid7.container.analyzer.docker.analyzer; + +import com.rapid7.container.analyzer.docker.model.image.Image; +import java.io.IOException; + +public interface ImageHandler { + + public void handle(Image image) throws IOException; +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerExtractor.java b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerExtractor.java new file mode 100644 index 0000000..d5775b3 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerExtractor.java @@ -0,0 +1,9 @@ +package com.rapid7.container.analyzer.docker.analyzer; + +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.io.File; +import java.io.IOException; + +public interface LayerExtractor { + public File getLayer(LayerId layerId) throws IOException; +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerFileHandler.java b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerFileHandler.java new file mode 100644 index 0000000..95da75f --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/analyzer/LayerFileHandler.java @@ -0,0 +1,13 @@ +package com.rapid7.container.analyzer.docker.analyzer; + +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import java.io.IOException; +import java.io.InputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; + +public interface LayerFileHandler { + + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException; +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/ApkgFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/ApkgFingerprinter.java new file mode 100644 index 0000000..d0097d8 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/ApkgFingerprinter.java @@ -0,0 +1,25 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.packages.ApkgParser; +import java.io.IOException; +import java.io.InputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; + + +public class ApkgFingerprinter implements LayerFileHandler { + private ApkgParser apkgParser; + + public ApkgFingerprinter(ApkgParser apkgParser) { + this.apkgParser = apkgParser; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + if (!entry.isSymbolicLink() && name.endsWith("lib/apk/db/installed")) + layer.addPackages(apkgParser.parse(contents, image.getOperatingSystem() == null ? layer.getOperatingSystem() : image.getOperatingSystem())); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DpkgFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DpkgFingerprinter.java new file mode 100644 index 0000000..e8559f3 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/DpkgFingerprinter.java @@ -0,0 +1,24 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.packages.DpkgParser; +import java.io.IOException; +import java.io.InputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; + +public class DpkgFingerprinter implements LayerFileHandler { + private DpkgParser dpkgParser; + + public DpkgFingerprinter(DpkgParser dpkgParser) { + this.dpkgParser = dpkgParser; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + if (!entry.isSymbolicLink() && name.endsWith("var/lib/dpkg/status")) + layer.addPackages(dpkgParser.parse(contents, image.getOperatingSystem() == null ? layer.getOperatingSystem() : image.getOperatingSystem())); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/FileFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/FileFingerprinter.java new file mode 100644 index 0000000..f7a495b --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/FileFingerprinter.java @@ -0,0 +1,87 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.HashId; +import com.rapid7.container.analyzer.docker.model.HashType; +import com.rapid7.container.analyzer.docker.model.image.File; +import com.rapid7.container.analyzer.docker.model.image.FileState; +import com.rapid7.container.analyzer.docker.model.image.FileType; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.image.LayerFile; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import static com.rapid7.container.analyzer.docker.model.image.FileType.BLOCK_DEVICE; +import static com.rapid7.container.analyzer.docker.model.image.FileType.CHARACTER_DEVICE; +import static com.rapid7.container.analyzer.docker.model.image.FileType.DIRECTORY; +import static com.rapid7.container.analyzer.docker.model.image.FileType.REGULAR_FILE; +import static com.rapid7.container.analyzer.docker.model.image.FileType.SYMBOLIC_LINK; +import static com.rapid7.container.analyzer.docker.model.image.FileType.WHITEOUT; +import static com.rapid7.container.analyzer.docker.model.image.FileType.WHITEOUT_OPAQUE; + + +public class FileFingerprinter implements LayerFileHandler { + + private static final Pattern WHITEOUT_AUFS_PREFIX = Pattern.compile("^(?.*/)?\\.wh\\.(?!.wh\\.)(?.*)$"); + private static final Pattern WHITEOUT_OPAQUE_AUFS_PREFIX = Pattern.compile("^(?.*/)?\\.wh\\.\\.wh\\.\\.opq$"); + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + + // prefix with / to indicate its an absolute path + name = "/" + name; + + // remove trailing slashes (for dir style) + if (name.endsWith("/")) + name = name.substring(0, name.length() - 1); + + FileType fileType = fileTypeOf(entry); + String linkTarget = null; + + // handle whiteout files + Matcher matcher = WHITEOUT_AUFS_PREFIX.matcher(name); + if (matcher.matches()) { + linkTarget = matcher.group("prefix") + matcher.group("suffix"); + fileType = WHITEOUT; + } + + // handle whiteout opaque files + matcher = WHITEOUT_OPAQUE_AUFS_PREFIX.matcher(name); + if (matcher.matches()) { + linkTarget = matcher.group("prefix"); + fileType = WHITEOUT_OPAQUE; + } + + File file = new File(fileType, name, entry.getSize()).setPermissions(entry.getMode()); + if (entry.isSymbolicLink()) + file.setLinkTarget(entry.getLinkName()); + else if (entry.isFile()) { + file.setLinkTarget(linkTarget); + file.setChecksum(checksum(contents)); + } + + layer.addFile(new LayerFile(file, FileState.ADDED)); + } + + private HashId checksum(InputStream contents) throws IOException { + return new HashId(DigestUtils.sha256Hex(contents), HashType.SHA256); + } + + private FileType fileTypeOf(TarArchiveEntry entry) { + if (entry.isDirectory()) + return DIRECTORY; + else if (entry.isSymbolicLink()) + return SYMBOLIC_LINK; + else if (entry.isCharacterDevice()) + return CHARACTER_DEVICE; + else if (entry.isBlockDevice()) + return BLOCK_DEVICE; + else + return REGULAR_FILE; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/OsReleaseFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/OsReleaseFingerprinter.java new file mode 100644 index 0000000..a84d566 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/OsReleaseFingerprinter.java @@ -0,0 +1,60 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.image.Image; +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.json.Configuration; +import com.rapid7.container.analyzer.docker.os.Fingerprinter; +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Pattern; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +public class OsReleaseFingerprinter implements LayerFileHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(OsReleaseFingerprinter.class); + private static final Pattern FILENAME_PATTERN = Pattern.compile("(?i:^((?:\\.)?(?:/)?etc/.*-release|(?:\\.)?(?:/)?usr/lib/os-release)$)"); + private Fingerprinter osReleaseParser; + + public OsReleaseFingerprinter(Fingerprinter osReleaseParser) { + this.osReleaseParser = osReleaseParser; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + if (!entry.isSymbolicLink() && FILENAME_PATTERN.matcher(name).matches()) + if (layer.getOperatingSystem() == null || name.endsWith("/os-release")) { + OperatingSystem os = osReleaseParser.parse(contents, name, convert(configuration.getArchitecture())); + if (os != null) { + MDC.put("operating_system", os.toString()); + try { + LOGGER.info("Operating system detected on layer."); + layer.setOperatingSystem(os); + + // given all packages detected on the layer this OS reference + layer.getPackages().forEach(pkg -> pkg.setOperatingSystem(os)); + } finally { + MDC.remove("operating_system"); + } + } + } + } + + // maps values to what content expects + private String convert(String architecture) { + switch (architecture) { + case "amd64": + return "x86_64"; + case "386": + return "x86"; + case "arm": + return "ARM"; + default: + return architecture; + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/PacmanFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/PacmanFingerprinter.java new file mode 100644 index 0000000..8636e0c --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/PacmanFingerprinter.java @@ -0,0 +1,26 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.packages.PacmanPackageParser; +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Pattern; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; + +public class PacmanFingerprinter implements LayerFileHandler { + private PacmanPackageParser pacmanPackageParser; + private static final Pattern PACMAN_DESC_PATTERN = Pattern.compile(".*/lib/pacman/local/(?.*)/desc"); + + public PacmanFingerprinter(PacmanPackageParser pacmanPackageParser) { + this.pacmanPackageParser = pacmanPackageParser; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + if (!entry.isSymbolicLink() && PACMAN_DESC_PATTERN.matcher(name).matches()) + layer.addPackages(pacmanPackageParser.parse(contents, image.getOperatingSystem() == null ? layer.getOperatingSystem() : image.getOperatingSystem())); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/RpmFingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/RpmFingerprinter.java new file mode 100644 index 0000000..6f7f3d2 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/RpmFingerprinter.java @@ -0,0 +1,116 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.LayerFileHandler; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.packages.RpmPackageParser; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.concurrent.TimeUnit; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.slf4j.helpers.MessageFormatter.format; + +public class RpmFingerprinter implements LayerFileHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RpmFingerprinter.class); + private static final String RPM_QUERY_FORMAT = "Package:%{NAME}\nSource:%{SOURCEPACKAGE}\nDescription:%{SUMMARY}\nInstalled-Size:%{SIZE}\nLicense:%{LICENSE}\nHomepage:%{URL}\nMaintainer:%{VENDOR}\nVersion:%{VERSION}\nEpoch:%{EPOCH}\nRelease:%{RELEASE}\n\n"; + private static final String DEFAULT_DOCKER_IMAGE = "centos"; + private RpmPackageParser rpmPackageParser; + private String rpmDockerImage; + + public RpmFingerprinter(RpmPackageParser rpmPackageParser, String rpmDockerImage) { + this.rpmPackageParser = rpmPackageParser; + this.rpmDockerImage = rpmDockerImage; + } + + @Override + public void handle(String name, TarArchiveEntry entry, InputStream contents, Image image, Configuration configuration, Layer layer) throws IOException { + if (!entry.isSymbolicLink() && name.endsWith("lib/rpm/Packages")) { + File directory = Files.createTempDirectory("rpm").toFile(); + try { + File packages = new File(directory, "Packages"); + + LOGGER.info("Extracting RPM Packages database to {}.", packages.getAbsolutePath()); + + // extract the Packages database + try (FileOutputStream output = new FileOutputStream(packages)) { + IOUtils.copy(contents, output); + } + + LOGGER.info("Executing RPM command against directory '{}'.", directory.getAbsolutePath()); + + if (commandExists("rpm")) { + // only exec one RPM command at a time on the host (to prevent timeouts and other concurrency issues on host) + synchronized (this) { + // the --root flag sets the path which all rpm commands use as base for relative paths. + // the --dbpath flag uses a path relative to the root path. we use "." (current directory) since the root path is the full rpmdb path. + Process process = new ProcessBuilder("/usr/bin/env", "rpm", "--root=" + directory.getAbsolutePath(),"--dbpath=.", "-qa", "--queryformat=" + RPM_QUERY_FORMAT).start(); + LOGGER.info(format("[Image: {}] Parsing RPM output.", image.getId()).getMessage()); + layer.addPackages(rpmPackageParser.parse(process.getInputStream(), image.getOperatingSystem() == null ? layer.getOperatingSystem() : image.getOperatingSystem())); + LOGGER.info(layer.getPackages().toString()); + if (!process.waitFor(5, TimeUnit.SECONDS)) + process.destroyForcibly(); + } + } else if (commandExists("docker")) { + // use a docker image to execute rpm command + String rpmImage = (rpmDockerImage == null || rpmDockerImage.isEmpty()) ? DEFAULT_DOCKER_IMAGE : rpmDockerImage; + LOGGER.info("Using docker image '{}' to execute RPM command.", rpmImage); + Process process = new ProcessBuilder("/usr/bin/env", "docker", "run", "--rm","--mount", "type=bind,source=" + directory.getAbsolutePath() + ",target=/rpm", + rpmImage, "rpm", "--root=/rpm", "--dbpath=.", "-qa", "--queryformat=" + RPM_QUERY_FORMAT).start(); + process.waitFor(300, TimeUnit.SECONDS); + if (process.exitValue() != 0) { + LOGGER.error("Docker exection failed with exit code {}.", process.exitValue()); + LOGGER.info("stdout: {}", IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8)); + LOGGER.info("stderr: {}", IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8)); + } else { + LOGGER.info(format("[Image: {}] Parsing RPM output.", image.getId()).getMessage()); + layer.addPackages(rpmPackageParser.parse(process.getInputStream(), image.getOperatingSystem() == null ? layer.getOperatingSystem() : image.getOperatingSystem())); + if (!process.waitFor(300, TimeUnit.SECONDS)) // TODO: user configurable? + process.destroyForcibly(); + LOGGER.info("Parsed {} packages from layer.", layer.getPackages().size()); + } + } else { + LOGGER.warn("No suitable commands available to fingerprint RPM packages."); + } + } catch (Exception exception) { + LOGGER.warn("Failed to fingerprint RPM packages.", exception); + } finally { + FileUtils.deleteDirectory(directory); + } + } + } + + private boolean commandExists(String command) { + String result = null; + try { + Process process = new ProcessBuilder("/usr/bin/env", "command", "-v", command).start(); + process.waitFor(5, TimeUnit.SECONDS); + if (process.exitValue() == 0) + result = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException | InterruptedException exception) { + LOGGER.warn("Failed to find '{}' with 'command -v'.", command, exception); + } + + try { + if (result == null || result.isEmpty()) { + Process process = new ProcessBuilder("/usr/bin/env", "which", command).start(); + process.waitFor(5, TimeUnit.SECONDS); + if (process.exitValue() == 0) + result = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); + } + } catch (IOException | InterruptedException exception) { + LOGGER.warn("Failed to find '{}' with 'which'.", command, exception); + } + + return result != null && !result.isEmpty(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/WhiteoutImageHandler.java b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/WhiteoutImageHandler.java new file mode 100644 index 0000000..aa987e4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/fingerprinter/WhiteoutImageHandler.java @@ -0,0 +1,85 @@ +package com.rapid7.container.analyzer.docker.fingerprinter; + +import com.rapid7.container.analyzer.docker.analyzer.ImageHandler; +import com.rapid7.container.analyzer.docker.model.image.File; +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.image.LayerFile; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import static com.rapid7.container.analyzer.docker.model.image.FileState.REMOVED; +import static com.rapid7.container.analyzer.docker.model.image.FileState.UPDATED; +import static com.rapid7.container.analyzer.docker.model.image.FileType.WHITEOUT; +import static com.rapid7.container.analyzer.docker.model.image.FileType.WHITEOUT_OPAQUE; + +public class WhiteoutImageHandler implements ImageHandler { + + @Override + public void handle(Image image) throws IOException { + + // the prefixes of files/directories that have been white-out'ed + Set whiteoutPrefixes = new HashSet<>(); + + List layers = new ArrayList<>(image.getLayers()); + if (layers.isEmpty()) + return; + + // process layers in reverse order to apply removes + Collections.reverse(layers); + for (Layer layer : layers) { + + Set whiteoutsInLayer = new HashSet<>(); + + // process layer files in reverse order + List layerFiles = new ArrayList<>(layer.getFiles()); + Collections.reverse(layerFiles); + + for (LayerFile layerFile : layerFiles) { + File file = layerFile.getFile(); + + // white-out file + if (file.getType() == WHITEOUT || file.getType() == WHITEOUT_OPAQUE) { + whiteoutsInLayer.add(file.getLinkTarget()); + } else { + // perform exact matching against any whiteouts in the same layer (this + // handles the opaque whiteout of a directory, when the parent directory + // is enumerated because of the presence of the whiteout file itself) + if (whiteoutsInLayer.contains(file.getName())) + layerFile.setState(REMOVED); + + // perform prefix matching on all whiteouts from any previous layers + for (String whiteoutPrefix : whiteoutPrefixes) { + if (file.getName().startsWith(whiteoutPrefix)) + layerFile.setState(REMOVED); + } + } + } + + // only when the layer is fully processed will white-outs take affect (on children layers) + whiteoutPrefixes.addAll(whiteoutsInLayer); + } + + // process layers in normal order to apply updates + Set files = new HashSet<>(); + for (Layer layer : image.getLayers()) { + for (LayerFile layerFile : layer.getFiles()) { + File file = layerFile.getFile(); + + if (layerFile.getState() == REMOVED) + files.remove(file.getName()); + else { + if (files.contains(file.getName())) + layerFile.setState(UPDATED); + + // track all normal non-whiteout files + if (file.getType() != WHITEOUT && file.getType() != WHITEOUT_OPAQUE) + files.add(file.getName()); + } + } + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/Digest.java b/src/main/java/com/rapid7/container/analyzer/docker/model/Digest.java new file mode 100644 index 0000000..6eb7d89 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/Digest.java @@ -0,0 +1,28 @@ +package com.rapid7.container.analyzer.docker.model; + +public class Digest extends HashId { + + public Digest(String id, HashType type) { + super(id, type); + } + + public Digest(String hashId) { + super(hashId); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof Digest) { + Digest other = (Digest)obj; + return super.equals(other); + } else + return false; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/HashId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/HashId.java new file mode 100644 index 0000000..f8acb04 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/HashId.java @@ -0,0 +1,58 @@ +package com.rapid7.container.analyzer.docker.model; + +import java.util.Objects; +import static java.util.Objects.requireNonNull; + +/** + * An identifier that is a hash value. + */ +public class HashId { + + protected HashType type; + protected String id; + + public HashId(String id, HashType type) { + this.id = requireNonNull(id, "id"); + this.type = requireNonNull(type, "type"); + } + + public HashId(String hashId) { + String[] components = requireNonNull(hashId, "id").split(":"); + type = components.length == 2 ? HashType.fromValue(components[0]) : HashType.SHA256; + id = components[components.length - 1]; + } + + public String getId() { + return id; + } + + public HashType getType() { + return type; + } + + public String getString() { + return type + ":" + id; + } + + @Override + public int hashCode() { + return Objects.hash(id, type); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof HashId) { + HashId other = (HashId)obj; + return Objects.equals(id, other.id) + && Objects.equals(type, other.type); + } else + return false; + } + + @Override + public String toString() { + return getString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/HashType.java b/src/main/java/com/rapid7/container/analyzer/docker/model/HashType.java new file mode 100644 index 0000000..421f865 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/HashType.java @@ -0,0 +1,27 @@ +package com.rapid7.container.analyzer.docker.model; + +public enum HashType { + + /** An MD5 based hash. */ + MD5, + /** A SHA-1 based hash. */ + SHA1, + /** A SHA-256 based hash. */ + SHA256, + /** An unknown hash type. */ + UNKNOWN; + + public static HashType fromValue(String type) { + if (type != null) + for (HashType value : values()) + if (value.toString().equalsIgnoreCase(type)) + return value; + + return UNKNOWN; + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/File.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/File.java new file mode 100644 index 0000000..9574df4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/File.java @@ -0,0 +1,133 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import com.rapid7.container.analyzer.docker.model.HashId; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static java.util.Objects.requireNonNull; + +public class File { + + private static final PosixFilePermission[] PERMISSIONS = { OTHERS_EXECUTE, OTHERS_WRITE, OTHERS_READ, GROUP_EXECUTE, GROUP_WRITE, GROUP_READ, OWNER_EXECUTE, OWNER_WRITE, OWNER_READ }; + + private FileType type; + private String name; + private long size; + private HashId checksum; + private String linkTarget; + private Set permissions; + + public File(FileType type, String name, long size) { + this.type = requireNonNull(type); + this.name = requireNonNull(name); + this.size = size; + permissions = new HashSet<>(); + } + + public FileType getType() { + return type; + } + + public File setType(FileType type) { + this.type = type; + return this; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public Set getPermissions() { + return permissions; + } + + public File setPermissions(Set permissions) { + this.permissions = requireNonNull(permissions); + return this; + } + + public File setPermissions(int mode) { + permissions = posixFilePermissions(mode); + return this; + } + + public HashId getChecksum() { + return checksum; + } + + public File setChecksum(HashId checksum) { + this.checksum = checksum; + return this; + } + + public String getLinkTarget() { + return linkTarget; + } + + public File setLinkTarget(String linkTarget) { + this.linkTarget = linkTarget; + return this; + } + + private static Set posixFilePermissions(int mode) { + int mask = 1; + Set perms = new HashSet<>(); + for (PosixFilePermission flag : PERMISSIONS) { + if (flag != null && (mask & mode) != 0) + perms.add(flag); + + mask = mask << 1; + } + return perms; + } + + @Override + public int hashCode() { + return Objects.hash(type, name, size, linkTarget, permissions, checksum); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + else if (!(obj instanceof File)) + return false; + else { + File other = (File)obj; + return Objects.equals(type, other.type) + && Objects.equals(name, other.name) + && size == other.size + && Objects.equals(linkTarget, other.linkTarget) + && Objects.equals(permissions, other.permissions) + && Objects.equals(checksum, other.checksum); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", File.class.getSimpleName() + "[", "]") + .add("Name=" + name) + .add("Size=" + size) + .add("Link Target=" + linkTarget) + .add("Type=" + type) + .add("Permissions=" + type.getCode() + PosixFilePermissions.toString(permissions)) + .add("Checksum=" + checksum) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileId.java new file mode 100644 index 0000000..ce6449d --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileId.java @@ -0,0 +1,23 @@ +package com.rapid7.container.analyzer.docker.model.image; + +public class FileId extends TypeSafeLongId { + + public FileId(long id) { + super(id); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof FileId) + return super.equals(obj); + else + return false; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileState.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileState.java new file mode 100644 index 0000000..3855d73 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileState.java @@ -0,0 +1,17 @@ +package com.rapid7.container.analyzer.docker.model.image; + +public enum FileState { + + ADDED, + REMOVED, + UPDATED, + UNKNOWN; + + public static FileState fromString(String value) { + for (FileState type : values()) + if (type.name().equalsIgnoreCase(value)) + return type; + + return UNKNOWN; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileType.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileType.java new file mode 100644 index 0000000..8e271ed --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/FileType.java @@ -0,0 +1,30 @@ +package com.rapid7.container.analyzer.docker.model.image; + +public enum FileType { + + REGULAR_FILE('-'), + DIRECTORY('d'), + SYMBOLIC_LINK('l'), + CHARACTER_DEVICE('c'), + BLOCK_DEVICE('b'), + WHITEOUT('w'), + WHITEOUT_OPAQUE('o'); + + private char code; + + private FileType(char code) { + this.code = code; + } + + public char getCode() { + return code; + } + + public static FileType fromString(String value) { + for (FileType type : values()) + if (type.name().equalsIgnoreCase(value)) + return type; + + return REGULAR_FILE; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/IdentifiablePackage.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/IdentifiablePackage.java new file mode 100644 index 0000000..9cb5688 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/IdentifiablePackage.java @@ -0,0 +1,28 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import static java.util.Objects.requireNonNull; + + +public class IdentifiablePackage extends Package { + + private PackageId id; + + public IdentifiablePackage(PackageId id, String source, PackageType type, OperatingSystem os, String name, String version, String description, Long size, String maintainer, String homepage, String license) { + super(source, type, os, name, version, description, size, maintainer, homepage, license); + this.id = requireNonNull(id); + } + + public IdentifiablePackage(PackageId id, String source, PackageType type, String osVendor, String osFamily, String osName, String osVersion, String osArchitecture, String name, String version, String description, Long size, String maintainer, String homepage, String license) { + super(source, type, osVendor, osFamily, osName, osVersion, osArchitecture, name, version, description, size, maintainer, homepage, license); + this.id = requireNonNull(id); + } + + public IdentifiablePackage(PackageId id, String source, PackageType type, String osVendor, String osFamily, String osName, String osVersion, String osArchitecture, String name, String version, String description, Long size, String maintainer, String homepage, String license, String epoch, String release) { + super(source, type, osVendor, osFamily, osName, osVersion, osArchitecture, name, version, description, size, maintainer, homepage, license, epoch, release); + this.id = requireNonNull(id); + } + + public PackageId getId() { + return id; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/Image.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Image.java new file mode 100644 index 0000000..3d693bb --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Image.java @@ -0,0 +1,177 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import com.rapid7.container.analyzer.docker.model.Digest; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; +import static java.util.Objects.requireNonNull; + +public class Image { + + protected ImageId id; + protected Set digests; + protected ImageType type; + protected Instant created; + protected Long size; + protected LinkedHashMap layers; + + private Image() { + digests = new HashSet<>(); + layers = new LinkedHashMap<>(); + } + + public Image(ImageId id, ImageType type) { + this(); + this.id = requireNonNull(id); + this.type = requireNonNull(type); + } + + public Image(ImageId id, ImageType type, Digest digest) { + this(id, type); + digests.add(digest); + } + + public Image(ImageId id, ImageType type, Long size, Instant created) { + this(id, type); + this.created = created; + this.size = size; + } + + public Image(ImageId id, ImageType type, Digest digest, Long size, Instant created) { + this(id, type, size, created); + addDigest(digest); + } + + public Image(ImageId id, ImageType type, Iterable digests, Long size, Instant created) { + this(id, type, size, created); + addDigests(digests); + } + + public Image addLayer(Layer layer) { + + if (layer != null) + layers.put(layer.getId(), layer); + + return this; + } + + public Image addLayers(Iterable layers) { + if (layers != null) + for (Layer layer : layers) + addLayer(layer); + + return this; + } + + public Layer getLayer(LayerId id) { + return layers.get(id); + } + + public Layer getPackageLayer() { + return layers.values().stream().filter(l -> !l.getPackages().isEmpty()).reduce((layer1, layer2) -> layer2).orElse(null); + } + + public ImageId getId() { + return id; + } + + public ImageType getType() { + return type; + } + + public Image addDigest(Digest digest) { + if (digest != null) + digests.add(digest); + + return this; + } + + public Image addDigests(Iterable digests) { + if (digests != null) + for (Digest digest : digests) + addDigest(digest); + + return this; + } + + public Set getDigests() { + return unmodifiableSet(digests); + } + + public Image setCreated(Instant created) { + this.created = created; + return this; + } + + public Instant getCreated() { + return created; + } + + public Long getSize() { + return size; + } + + public Image setSize(Long size) { + this.size = size; + return this; + } + + public List getFiles() { + Layer layer = getPackageLayer(); + return layer == null ? emptyList() : layer.getFiles(); + } + + public List getLayers() { + return unmodifiableList(new ArrayList<>(layers.values())); + } + + public Set getPackages() { + Layer layer = getPackageLayer(); + return layer == null ? emptySet() : layer.getPackages(); + } + + public OperatingSystem getOperatingSystem() { + return layers.values().stream().map(Layer::getOperatingSystem).filter(Objects::nonNull).findFirst().orElse(null); + } + + public boolean isAnalyzed() { + return !layers.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof Image) { + Image other = (Image)obj; + return Objects.equals(id, other.id); + } else + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return new StringJoiner(", ", Image.class.getSimpleName() + "[", "]") + .add("Id=" + id) + .add("Digests=" + digests) + .add("Type=" + type) + .add("Created=" + created) + .add("Size=" + size) + .add("Layers=" + layers) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageId.java new file mode 100644 index 0000000..41a9f7d --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageId.java @@ -0,0 +1,25 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import com.rapid7.container.analyzer.docker.model.HashId; +import com.rapid7.container.analyzer.docker.model.HashType; + +public class ImageId extends HashId { + + public ImageId(String id, HashType type) { + super(id, type); + } + + public ImageId(String idStr) { + super(idStr); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (!(obj instanceof ImageId)) + return false; + else + return super.equals(obj); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageType.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageType.java new file mode 100644 index 0000000..8cb7f0e --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/ImageType.java @@ -0,0 +1,27 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.image; + +public enum ImageType { + + /** Docker image. */ + DOCKER, + /** Unknown. */ + UNKNOWN; + + @Override + public String toString() { + return name(); + } + + public static ImageType fromValue(String type) { + if (type != null) + for (ImageType value : values()) + if (value.name().equalsIgnoreCase(type)) + return value; + + return UNKNOWN; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/Layer.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Layer.java new file mode 100644 index 0000000..ccfc801 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Layer.java @@ -0,0 +1,199 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; +import static java.util.Objects.requireNonNull; + +/** + * A layer within an image representing some state change within the image, which can comprise of + * file-system changes. Each layer tracks the commands that were used to construct it, and + * fingerprints of packages/etc. + */ +public class Layer { + + private LayerId id; + private Set packages; + private String command; + private String comment; + private String author; + private OperatingSystem operatingSystem; + private LayerId parentId; + private Instant created; + private long size; + private boolean empty; + private List files; + + protected Layer() { + // deserialization + packages = new HashSet<>(); + files = new ArrayList<>(); + } + + public Layer(LayerId id) { + this.id = requireNonNull(id); + packages = new HashSet<>(); + files = new ArrayList<>(); + } + + public LayerId getId() { + return id; + } + + public Layer setCommand(String command) { + this.command = command; + return this; + } + + public String getCommand() { + return command; + } + + public Layer setComment(String comment) { + this.comment = comment; + return this; + } + + public String getComment() { + return comment; + } + + public Layer setAuthor(String author) { + this.author = author; + return this; + } + + public String getAuthor() { + return author; + } + + public OperatingSystem getOperatingSystem() { + return operatingSystem; + } + + public Layer setOperatingSystem(OperatingSystem operatingSystem) { + this.operatingSystem = operatingSystem; + return this; + } + + public Layer addPackage(Package pkg) { + packages.add(pkg); + return this; + } + + public Layer addPackages(Iterable packages) { + if (packages != null) + packages.forEach(this::addPackage); + + return this; + } + + public Set getPackages() { + return unmodifiableSet(packages); + } + + public Package getPackage(PackageId id) { + for (Package pkg : packages) + if (pkg instanceof IdentifiablePackage) + if (((IdentifiablePackage) pkg).getId().equals(id)) + return pkg; + + return null; + } + + public Layer addFile(LayerFile pkg) { + files.add(pkg); + return this; + } + + public Layer addFiles(Iterable files) { + if (files != null) + files.forEach(this::addFile); + + return this; + } + + public List getFiles() { + return unmodifiableList(files); + } + + public LayerId getParentId() { + return parentId; + } + + public Layer setParentId(LayerId parentId) { + this.parentId = parentId; + return this; + } + + public Instant getCreated() { + return created; + } + + public Layer setCreated(Instant created) { + this.created = created; + return this; + } + + public long getSize() { + return size; + } + + public Layer setSize(long size) { + this.size = size; + return this; + } + + public boolean isPackageLayer() { + return !packages.isEmpty(); + } + + public boolean isEmpty() { + return empty; + } + + public Layer setEmpty(boolean empty) { + this.empty = empty; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + else if (!(obj instanceof Layer)) + return false; + else { + Layer other = (Layer) obj; + return Objects.equals(id, other.id); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", Layer.class.getSimpleName() + "[", "]") + .add("Id=" + id) + .add("Parent Id=" + parentId) + .add("Files=" + (files.size() > 3 ? files.size() : files)) + .add("Packages=" + (packages.size() > 3 ? packages.size() : packages)) + .add("Command=" + command) + .add("Comment=" + comment) + .add("Author=" + author) + .add("Operating System=" + operatingSystem) + .add("Created=" + created) + .add("Size=" + size) + .add("Empty=" + empty) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerFile.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerFile.java new file mode 100644 index 0000000..47cd0f3 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerFile.java @@ -0,0 +1,26 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import static java.util.Objects.requireNonNull; + +public class LayerFile { + + private FileState state; + private File file; + + public LayerFile(File file, FileState state) { + this.state = state; + this.file = file; + } + + public FileState getState() { + return state; + } + + public File getFile() { + return file; + } + + public void setState(FileState state) { + this.state = requireNonNull(state); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerId.java new file mode 100644 index 0000000..f73628f --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/LayerId.java @@ -0,0 +1,39 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.image; + +import com.rapid7.container.analyzer.docker.model.HashId; +import com.rapid7.container.analyzer.docker.model.HashType; + +public class LayerId extends HashId implements Comparable { + + public LayerId(String id, HashType type) { + super(id, type); + } + + public LayerId(String hashId) { + super(hashId); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (!(obj instanceof LayerId)) + return false; + else + return super.equals(obj); + } + + @Override + public int compareTo(LayerId other) { + return id.compareTo(other.id); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/OperatingSystem.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/OperatingSystem.java new file mode 100644 index 0000000..50783e6 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/OperatingSystem.java @@ -0,0 +1,109 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.image; + +import java.util.Objects; +import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; + +public class OperatingSystem { + + private String vendor; + private String family; + private String name; + private String architecture; + private String version; + private String description; + + protected OperatingSystem() { + // deserialization + } + + public OperatingSystem(String vendor, String family, String name, String architecture, String version, String description) { + this.vendor = sanitize(vendor); + this.family = sanitize(family); + this.name = sanitize(requireNonNull(name, "name")); + this.architecture = sanitize(architecture); + this.version = sanitize(version); + this.description = sanitize(description); + } + + public String getVendor() { + return vendor; + } + + public String getFamily() { + return family; + } + + public String getName() { + return name; + } + + public String getArchitecture() { + return architecture; + } + + public String getVersion() { + return version; + } + + public String getDescription() { + return description; + } + + static String sanitize(String value) { + if (value == null) + return value; + else if (value.isEmpty()) + return null; + // (none) a common value is replaced to null + else if ("(none)".equalsIgnoreCase(value)) + return null; + else { + // remove wrapping single and double quotes + value = value.replaceAll("^[\"']|[\"']$", ""); + + if (value.isEmpty()) + return null; + else + return value.trim(); + } + } + + @Override + public int hashCode() { + return Objects.hash(vendor, family, name, architecture, version, description); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + else if (!(obj instanceof OperatingSystem)) + return false; + else { + OperatingSystem other = (OperatingSystem)obj; + return Objects.equals(vendor, other.vendor) + && Objects.equals(family, other.family) + && Objects.equals(name, other.name) + && Objects.equals(architecture, other.architecture) + && Objects.equals(version, other.version) + && Objects.equals(description, other.description); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", OperatingSystem.class.getSimpleName() + "[", "]") + .add("Vendor=" + vendor) + .add("Family=" + family) + .add("Name=" + name) + .add("Architecture=" + architecture) + .add("Version=" + version) + .add("Description=" + description) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/Package.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Package.java new file mode 100644 index 0000000..7165eb8 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/Package.java @@ -0,0 +1,191 @@ +package com.rapid7.container.analyzer.docker.model.image; + +import java.util.Objects; +import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; + +public class Package implements Comparable { + + private PackageType type; + private String name; + private String osVendor; + private String osFamily; + private String osVersion; + private String osName; + private String osArchitecture; + private String source; + private String version; + private String description; + private Long size; + private String maintainer; + private String homepage; + private String license; + /** + * Exclusive to RPM package types, the epoch tag is a monotonically increasing integer where the largest epoch + * number means it's the newest version of a package. When multiple packages have the same epoch value, then the + * release field is compared to identify the newest package. + */ + private String epoch; + /** + * Exclusive to RPM package types, the release field is a build version representing patches not in the upstream + * version (major version number). Is used to identify a newer version of a package when multiple packages have the + * same epoch value. + */ + private String release; + + public Package(String source, PackageType type, OperatingSystem os, String name, String version, String description, Long size, String maintainer, String homepage, String license) { + this(source, type, os == null ? null : os.getVendor(), os == null ? null : os.getFamily(), os == null ? null : os.getName(), os == null ? null : os.getVersion(), os == null ? null : os.getArchitecture(), name, version, description, size, maintainer, homepage, license, null, null); + } + + public Package(String source, PackageType type, OperatingSystem os, String name, String version, String description, Long size, String maintainer, String homepage, String license, String epoch, String release) { + this(source, type, os == null ? null : os.getVendor(), os == null ? null : os.getFamily(), os == null ? null : os.getName(), os == null ? null : os.getVersion(), os == null ? null : os.getArchitecture(), name, version, description, size, maintainer, homepage, license, epoch, release); + } + + public Package(String source, PackageType type, String osVendor, String osFamily, String osName, String osVersion, String osArchitecture, String name, String version, String description, Long size, String maintainer, String homepage, String license) { + this(source, type, osVendor, osFamily, osName, osVersion, osArchitecture, name, version, description, size, maintainer, homepage, license, null, null); + } + + public Package(String source, PackageType type, String osVendor, String osFamily, String osName, String osVersion, String osArchitecture, String name, String version, String description, Long size, String maintainer, String homepage, String license, String epoch, String release) { + this.type = requireNonNull(type, "type"); + this.source = source; + this.osVendor = osVendor; + this.osFamily = osFamily; + this.osVersion = osVersion; + this.osArchitecture = osArchitecture; + this.osName = osName; + this.name = requireNonNull(name, "name"); + this.version = requireNonNull(version, "version"); + this.description = description; + this.maintainer = maintainer; + this.homepage = homepage; + this.size = size; + this.license = license; + this.epoch = epoch; + this.release = release; + } + + public PackageType getType() { + return type; + } + + public String getPackage() { + return name; + } + + public String getOsVendor() { + return osVendor; + } + + public String getOsFamily() { + return osFamily; + } + + public String getOsName() { + return osName; + } + + public String getOsVersion() { + return osVersion; + } + + public String getOsArchitecture() { + return osArchitecture; + } + + public String getSource() { + return source; + } + + public String getVersion() { + return version; + } + + public String getDescription() { + return description; + } + + public Long getSize() { + return size; + } + + public String getMaintainer() { + return maintainer; + } + + public String getHomepage() { + return homepage; + } + + public String getLicense() { + return license; + } + + public String getEpoch() { + return epoch; + } + + public String getRelease() { + return release; + } + + public Package setOperatingSystem(OperatingSystem operatingSystem) { + osVendor = operatingSystem.getVendor(); + osFamily = operatingSystem.getFamily(); + osVersion = operatingSystem.getVersion(); + osName = operatingSystem.getName(); + return this; + } + + @Override + public int hashCode() { + return Objects.hash(type, osVendor, osName, osVersion, osArchitecture, name, version, epoch, release, source); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof Package)) { + return false; + } else { + Package other = (Package) obj; + return Objects.equals(type, other.type) + && Objects.equals(osVendor, other.osVendor) + && Objects.equals(osFamily, other.osFamily) + && Objects.equals(osName, other.osName) + && Objects.equals(osVersion, other.osVersion) + && Objects.equals(osArchitecture, other.osArchitecture) + && Objects.equals(name, other.name) + && Objects.equals(version, other.version) + && Objects.equals(epoch, other.epoch) + && Objects.equals(release, other.release) + && Objects.equals(source, other.source); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", Package.class.getSimpleName() + "[", "]") + .add("Type=" + type) + .add("OS Vendor=" + osVendor) + .add("OS Family=" + osFamily) + .add("OS Name=" + osName) + .add("OS Version=" + osVersion) + .add("OS Architecture=" + osArchitecture) + .add("Package=" + name) + .add("Version=" + version) + .add("Source=" + source) + .add("Size=" + size) + .add("Maintainer=" + maintainer) + .add("Home Page=" + homepage) + .add("License=" + license) + .add("Epoch=" + epoch) + .add("Release=" + release) + .toString(); + } + + @Override + public int compareTo(Package other) { + return name.compareTo(other.name); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageId.java new file mode 100644 index 0000000..f4f1c72 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageId.java @@ -0,0 +1,23 @@ +package com.rapid7.container.analyzer.docker.model.image; + +public class PackageId extends TypeSafeLongId { + + public PackageId(long id) { + super(id); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof PackageId) + return super.equals(obj); + else + return false; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageType.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageType.java new file mode 100644 index 0000000..9b39a8b --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/PackageType.java @@ -0,0 +1,27 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.image; + +public enum PackageType { + + APGK, + DPGK, + PACMAN, + RPM, + UNKNOWN; + + public static PackageType fromString(String string) { + for (PackageType type : values()) + if (type.name().equalsIgnoreCase(string)) + return type; + + return UNKNOWN; + } + + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeId.java new file mode 100644 index 0000000..df056b5 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeId.java @@ -0,0 +1,75 @@ +package com.rapid7.container.analyzer.docker.model.image; + +/** + * Base abstraction for a type-safe identifier. Type-safe identifiers can be used to ensure that primitive identifiers + * for an object are type-safe and do not accidently get misused with other objects. Type-safe identifiers are immutable + * and cannot be changed once constructed. + *

+ * In order to create a type-safe identifier, this class must be extended and that type used in place of the traditional + * identifier. + *

+ * + * @param The type of the identifier. + */ +public abstract class TypeSafeId { + + /** + * The identifier, this is not final as child classes may have mutable IDs and being final is not compatible with + * JAXB marshaling and unmarshaling. + */ + protected T id; + + /** + * Returns the identifier. + * + * @return The identifier. Will never be {@code null}. + */ + public T getId() { + return id; + } + + /** + * Constructs an identifier with the given value. + * + * @param id The identifier. Must not be {@code null}. + */ + protected TypeSafeId(T id) { + if (id == null) + throw new IllegalArgumentException("The identifier must not be null."); + + this.id = id; + } + + /** + * Default constructor, allowing reflective construction. + */ + protected TypeSafeId() { + + } + + ///////////////////////////////////////////////////////////////////////// + // Object overrides + ///////////////////////////////////////////////////////////////////////// + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + + if (!(obj instanceof TypeSafeId)) + return false; + + Object objId = ((TypeSafeId)obj).getId(); + return objId != null && objId.equals(getId()); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return id.toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeLongId.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeLongId.java new file mode 100644 index 0000000..02dfe6e --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/TypeSafeLongId.java @@ -0,0 +1,37 @@ +package com.rapid7.container.analyzer.docker.model.image; + +/** + * A type-safe ID with a {@link Long} value. Implementations should override the {@link TypeSafeLongId#equals(Object)} + * method to ensure that identifiers of different types are not equivalent, even if the underlying values are + * the same. + */ +public abstract class TypeSafeLongId extends TypeSafeId { + + public TypeSafeLongId() { + + } + + public TypeSafeLongId(Long id) { + super(id); + } + + ///////////////////////////////////////////////////////////////////////// + // Object overrides + ///////////////////////////////////////////////////////////////////////// + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + else if (obj instanceof TypeSafeLongId) { + TypeSafeLongId other = (TypeSafeLongId)obj; + return super.equals(other); + } else + return false; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/ImageModelObjectMapper.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/ImageModelObjectMapper.java new file mode 100644 index 0000000..a8c4a17 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/ImageModelObjectMapper.java @@ -0,0 +1,20 @@ +package com.rapid7.container.analyzer.docker.model.image.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +public class ImageModelObjectMapper extends ObjectMapper { + + public ImageModelObjectMapper() { + enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + registerModule(new JavaTimeModule()); + registerModule(new JacksonImageModelModule()); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/JacksonImageModelModule.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/JacksonImageModelModule.java new file mode 100644 index 0000000..8875f26 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/JacksonImageModelModule.java @@ -0,0 +1,107 @@ +package com.rapid7.container.analyzer.docker.model.image.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.rapid7.container.analyzer.docker.model.HashId; +import com.rapid7.container.analyzer.docker.model.image.IdentifiablePackage; +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.LayerId; +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.PackageId; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.IdentifiablePackageMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.ImageIdMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.ImageMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.LayerIdMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.LayerMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.OperatingSystemMixin; +import com.rapid7.container.analyzer.docker.model.image.json.mixin.PackageMixin; +import com.rapid7.container.analyzer.docker.model.json.mixin.HashIdMixin; +import java.io.IOException; + +public class JacksonImageModelModule extends SimpleModule { + + public JacksonImageModelModule() { + this("r7-docker-image-model-module", new Version(1, 0, 0, null, "www.rapid.com", "docker-image-model")); + } + + public JacksonImageModelModule(String name, Version version) { + super(name, version); + configure(this); + } + + public static T configure(T module) { + + module.setMixInAnnotation(HashId.class, HashIdMixin.class); + module.setMixInAnnotation(ImageId.class, ImageIdMixin.class); + module.setMixInAnnotation(LayerId.class, LayerIdMixin.class); + module.setMixInAnnotation(Package.class, PackageMixin.class); + module.setMixInAnnotation(IdentifiablePackage.class, IdentifiablePackageMixin.class); + module.setMixInAnnotation(Layer.class, LayerMixin.class); + module.setMixInAnnotation(Image.class, ImageMixin.class); + module.setMixInAnnotation(OperatingSystem.class, OperatingSystemMixin.class); + module.setSerializerModifier(new BeanSerializerModifier() { + + @SuppressWarnings("unchecked") + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { + + if (HashId.class.isAssignableFrom(beanDesc.getBeanClass())) + return new HashIdSerializer((JsonSerializer) serializer); + + return serializer; + } + }); + + module.addKeyDeserializer(PackageId.class, new KeyDeserializer() { + + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + + return new PackageId(Long.valueOf(key)); + } + }); + + return module; + } + + public static class HashIdSerializer extends StdSerializer { + + private JsonSerializer defaultSerializer; + + public HashIdSerializer() { + super(HashId.class); + } + + public HashIdSerializer(JsonSerializer defaultSerializer) { + super(HashId.class); + this.defaultSerializer = defaultSerializer; + } + + @Override + public void serialize(HashId value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { + gen.writeString(value.getString()); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + visitor.expectStringFormat(typeHint); + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/IdentifiablePackageMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/IdentifiablePackageMixin.java new file mode 100644 index 0000000..9d47f44 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/IdentifiablePackageMixin.java @@ -0,0 +1,113 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.rapid7.container.analyzer.docker.model.image.IdentifiablePackage; +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.PackageId; +import com.rapid7.container.analyzer.docker.model.image.PackageType; + +@JsonTypeName("id-package") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type") +public abstract class IdentifiablePackageMixin extends IdentifiablePackage { + + @JsonCreator + public IdentifiablePackageMixin( + @JsonProperty("id") PackageId id, + @JsonProperty("source") String source, + @JsonProperty("type") PackageType type, + @JsonProperty("osVendor") String osVendor, + @JsonProperty("osFamily") String osFamily, + @JsonProperty("osName") String osName, + @JsonProperty("osVersion") String osVersion, + @JsonProperty("osArchitecture") String osArchitecture, + @JsonProperty("name") String name, + @JsonProperty("version") String version, + @JsonProperty("description") String description, + @JsonProperty("size") Long size, + @JsonProperty("maintainer") String maintainer, + @JsonProperty("homePage") String homepage, + @JsonProperty("license") String license, + @JsonProperty("epoch") String epoch, + @JsonProperty("release") String release) { + super(id, source, type, osVendor, osFamily, osName, osVersion, osArchitecture, name, version, description, size, maintainer, homepage, license, epoch, release); + } + + @Override + @JsonProperty("id") + public abstract PackageId getId(); + + @Override + @JsonProperty("type") + public abstract PackageType getType(); + + @Override + @JsonProperty("osVendor") + public abstract String getOsVendor(); + + @Override + @JsonProperty("osFamily") + public abstract String getOsFamily(); + + @Override + @JsonProperty("osName") + public abstract String getOsName(); + + @Override + @JsonProperty("osVersion") + public abstract String getOsVersion(); + + @Override + @JsonProperty("osArchitecture") + public abstract String getOsArchitecture(); + + @Override + @JsonProperty("source") + public abstract String getSource(); + + @Override + @JsonProperty("name") + public abstract String getPackage(); + + @Override + @JsonProperty("version") + public abstract String getVersion(); + + @Override + @JsonProperty("description") + public abstract String getDescription(); + + @Override + @JsonProperty("maintainer") + public abstract String getMaintainer(); + + @Override + @JsonProperty("homePage") + public abstract String getHomepage(); + + @Override + @JsonProperty("size") + public abstract Long getSize(); + + @Override + @JsonProperty("license") + public abstract String getLicense(); + + @Override + @JsonProperty("epoch") + public abstract String getEpoch(); + + @Override + @JsonProperty("release") + public abstract String getRelease(); + + // ignored + + @Override + @JsonIgnore + public abstract Package setOperatingSystem(OperatingSystem operatingSystem); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageIdMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageIdMixin.java new file mode 100644 index 0000000..66755d4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageIdMixin.java @@ -0,0 +1,29 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.HashType; +import com.rapid7.container.analyzer.docker.model.image.ImageId; + +public abstract class ImageIdMixin extends ImageId { + + @JsonCreator + public ImageIdMixin(String idStr) { + super(idStr); + } + + @Override + @JsonProperty("id") + public abstract String getString(); + + // ignored + + @Override + @JsonIgnore + public abstract String getId(); + + @Override + @JsonIgnore + public abstract HashType getType(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageMixin.java new file mode 100644 index 0000000..e8d5777 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/ImageMixin.java @@ -0,0 +1,76 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +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.ImageType; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.image.LayerFile; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; +import com.rapid7.container.analyzer.docker.model.image.Package; +import java.time.Instant; +import java.util.List; +import java.util.Set; + +public abstract class ImageMixin extends Image { + + @JsonCreator + public ImageMixin( + @JsonProperty("id") ImageId id, + @JsonProperty("type") ImageType type, + @JsonProperty("size") Long size, + @JsonProperty("created") Instant created) { + super(id, type, size, created); + } + + @Override + @JsonProperty("id") + public abstract ImageId getId(); + + @Override + @JsonProperty("type") + public abstract ImageType getType(); + + @Override + @JsonProperty("size") + public abstract Long getSize(); + + @Override + @JsonProperty("created") + public abstract Instant getCreated(); + + @Override + @JsonProperty("layers") + public abstract List getLayers(); + + @Override + @JsonProperty("layers") + public abstract Image addLayers(Iterable layers); + + @Override + @JsonIgnore + public abstract Set getPackages(); + + @Override + @JsonIgnore + public abstract OperatingSystem getOperatingSystem(); + + @Override + @JsonIgnore + public abstract boolean isAnalyzed(); + + @Override + @JsonIgnore + public abstract List getFiles(); + + @Override + @JsonIgnore + public abstract Layer getPackageLayer(); + + @Override + @JsonIgnore + public abstract Layer getLayer(LayerId id); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerIdMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerIdMixin.java new file mode 100644 index 0000000..631644d --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerIdMixin.java @@ -0,0 +1,12 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.rapid7.container.analyzer.docker.model.image.LayerId; + +public abstract class LayerIdMixin extends LayerId { + + @JsonCreator + public LayerIdMixin(String idStr) { + super(idStr); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerMixin.java new file mode 100644 index 0000000..1a566e1 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/LayerMixin.java @@ -0,0 +1,124 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.image.LayerFile; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +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.PackageId; +import java.time.Instant; +import java.util.List; +import java.util.Set; + +public abstract class LayerMixin extends Layer { + + @JsonCreator + public LayerMixin(@JsonProperty("id") LayerId id) { + super(id); + } + + @Override + @JsonProperty("id") + public abstract LayerId getId(); + + @Override + @JsonProperty("command") + public abstract String getCommand(); + + @Override + @JsonProperty("command") + public abstract Layer setCommand(String command); + + @Override + @JsonProperty("comment") + public abstract String getComment(); + + @Override + @JsonProperty("comment") + public abstract Layer setComment(String comment); + + @Override + @JsonProperty("author") + public abstract String getAuthor(); + + @Override + @JsonProperty("author") + public abstract Layer setAuthor(String author); + + @Override + @JsonProperty("os") + public abstract OperatingSystem getOperatingSystem(); + + @Override + @JsonProperty("os") + public abstract Layer setOperatingSystem(OperatingSystem operatingSystem); + + @Override + @JsonProperty("packages") + public abstract Set getPackages(); + + @Override + @JsonProperty("packages") + public abstract Layer addPackages(Iterable packages); + + @Override + @JsonProperty("parentId") + public abstract LayerId getParentId(); + + @Override + @JsonProperty("parentId") + public abstract Layer setParentId(LayerId parentId); + + @Override + @JsonProperty("created") + public abstract Instant getCreated(); + + @Override + @JsonProperty("created") + public abstract Layer setCreated(Instant created); + + @Override + @JsonProperty("size") + public abstract long getSize(); + + @Override + @JsonProperty("size") + public abstract Layer setSize(long size); + + @Override + @JsonProperty("empty") + public abstract boolean isEmpty(); + + @Override + @JsonProperty("empty") + public abstract Layer setEmpty(boolean empty); + + // ignored + + @Override + @JsonIgnore + public abstract Layer addPackage(Package pkg); + + @Override + @JsonIgnore + public abstract Package getPackage(PackageId id); + + @Override + @JsonIgnore + public abstract Layer addFile(LayerFile pkg); + + @Override + @JsonIgnore + public abstract boolean isPackageLayer(); + + @Override + @JsonIgnore + public abstract Layer addFiles(Iterable files); + + @Override + @JsonIgnore + public abstract List getFiles(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/OperatingSystemMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/OperatingSystemMixin.java new file mode 100644 index 0000000..f56a013 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/OperatingSystemMixin.java @@ -0,0 +1,37 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; + +public abstract class OperatingSystemMixin extends OperatingSystem { + + @JsonCreator + public OperatingSystemMixin(@JsonProperty("vendor") String vendor, @JsonProperty("family") String family, @JsonProperty("name") String name, @JsonProperty("architecture") String architecture, @JsonProperty("version") String version, @JsonProperty("description") String description) { + super(vendor, family, name, architecture, version, description); + } + + @Override + @JsonProperty("vendor") + public abstract String getVendor(); + + @Override + @JsonProperty("family") + public abstract String getFamily(); + + @Override + @JsonProperty("name") + public abstract String getName(); + + @Override + @JsonProperty("architecture") + public abstract String getArchitecture(); + + @Override + @JsonProperty("version") + public abstract String getVersion(); + + @Override + @JsonProperty("description") + public abstract String getDescription(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/PackageMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/PackageMixin.java new file mode 100644 index 0000000..3ea022e --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/image/json/mixin/PackageMixin.java @@ -0,0 +1,112 @@ +package com.rapid7.container.analyzer.docker.model.image.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.rapid7.container.analyzer.docker.model.image.IdentifiablePackage; +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; + +@JsonTypeName("package") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = Package.class, name = "package"), + @JsonSubTypes.Type(value = IdentifiablePackage.class, name = "id-package") +}) +public abstract class PackageMixin extends Package { + + @JsonCreator + public PackageMixin( + @JsonProperty("source") String source, + @JsonProperty("type") PackageType type, + @JsonProperty("osVendor") String osVendor, + @JsonProperty("osFamily") String osFamily, + @JsonProperty("osName") String osName, + @JsonProperty("osVersion") String osVersion, + @JsonProperty("osArchitecture") String osArchitecture, + @JsonProperty("name") String name, + @JsonProperty("version") String version, + @JsonProperty("description") String description, + @JsonProperty("size") Long size, + @JsonProperty("maintainer") String maintainer, + @JsonProperty("homePage") String homepage, + @JsonProperty("license") String license, + @JsonProperty("epoch") String epoch, + @JsonProperty("release") String release) { + super(source, type, osVendor, osFamily, osName, osVersion, osArchitecture, name, version, description, size, maintainer, homepage, license, epoch, release); + } + + @Override + @JsonProperty("type") + public abstract PackageType getType(); + + @Override + @JsonProperty("osVendor") + public abstract String getOsVendor(); + + @Override + @JsonProperty("osFamily") + public abstract String getOsFamily(); + + @Override + @JsonProperty("osName") + public abstract String getOsName(); + + @Override + @JsonProperty("osVersion") + public abstract String getOsVersion(); + + @Override + @JsonProperty("osArchitecture") + public abstract String getOsArchitecture(); + + @Override + @JsonProperty("source") + public abstract String getSource(); + + @Override + @JsonProperty("name") + public abstract String getPackage(); + + @Override + @JsonProperty("version") + public abstract String getVersion(); + + @Override + @JsonProperty("description") + public abstract String getDescription(); + + @Override + @JsonProperty("maintainer") + public abstract String getMaintainer(); + + @Override + @JsonProperty("homePage") + public abstract String getHomepage(); + + @Override + @JsonProperty("size") + public abstract Long getSize(); + + @Override + @JsonProperty("license") + public abstract String getLicense(); + + @Override + @JsonProperty("epoch") + public abstract String getEpoch(); + + @Override + @JsonProperty("release") + public abstract String getRelease(); + + // ignored + + @Override + @JsonIgnore + public abstract Package setOperatingSystem(OperatingSystem operatingSystem); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/Configuration.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/Configuration.java new file mode 100644 index 0000000..504c5d2 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/Configuration.java @@ -0,0 +1,15 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import java.time.Instant; +import java.util.List; + +public interface Configuration { + + public List getHistory(); + + public Instant getCreated(); + + public String getOperatingSystem(); + + public String getArchitecture(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/ConfigurationJsonV2.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ConfigurationJsonV2.java new file mode 100644 index 0000000..c5167cb --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ConfigurationJsonV2.java @@ -0,0 +1,91 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.List; +import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; + +public class ConfigurationJsonV2 implements Configuration { + + private String architecture; + private Instant created; + private String dockerVersion; + private List history; + private String operatingSystem; + private FileSystemJson fileSystem; + + @Override + @JsonProperty("architecture") + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = requireNonNull(architecture, "architecture"); + } + + @Override + @JsonProperty("created") + public Instant getCreated() { + return created; + } + + public void setCreated(Instant created) { + this.created = requireNonNull(created, "created"); + } + + @JsonProperty("docker_version") + public String getDockerVersion() { + return dockerVersion; + } + + public void setDockerVersion(String version) { + dockerVersion = requireNonNull(version, "version"); + } + + @Override + @JsonProperty("history") + public List getHistory() { + return history; + } + + public void setHistory(List history) { + this.history = requireNonNull(history, "history"); + } + + @Override + @JsonProperty("os") + public String getOperatingSystem() { + return operatingSystem; + } + + public void setOperatingSystem(String operatingSystem) { + this.operatingSystem = operatingSystem; + } + + @JsonProperty("rootfs") + public FileSystemJson getFileSystem() { + return fileSystem; + } + + public void setFileSysetm(FileSystemJson fileSystem) { + this.fileSystem = fileSystem; + } + + @Override + public String toString() { + return new StringJoiner(", ", ConfigurationJsonV2.class.getSimpleName() + "[", "]") + .add("Architecture=" + architecture) + .add("Created=" + created) + .add("Version=" + dockerVersion) + .add("History=" + history) + .add("Operating System=" + operatingSystem) + .add("File System=" + fileSystem) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/ContainerConfig.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ContainerConfig.java new file mode 100644 index 0000000..050bb20 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ContainerConfig.java @@ -0,0 +1,31 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Arrays; +import java.util.StringJoiner; + +public class ContainerConfig { + + private String[] commands; + + @JsonProperty("Cmd") + public String[] getCommands() { + return commands; + } + + public ContainerConfig setCommands(String[] commands) { + this.commands = commands; + return this; + } + + @Override + public String toString() { + return new StringJoiner(", ", ContainerConfig.class.getSimpleName() + "[", "]") + .add("Commands=" + Arrays.toString(commands)) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/FileSystemJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/FileSystemJson.java new file mode 100644 index 0000000..9952117 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/FileSystemJson.java @@ -0,0 +1,41 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.StringJoiner; + +public class FileSystemJson { + + private String type; + private List layers; + + @JsonProperty("type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @JsonProperty("diff_ids") + public List getLayers() { + return layers; + } + + public void setLayers(List layers) { + this.layers = layers; + } + + @Override + public String toString() { + return new StringJoiner(", ", FileSystemJson.class.getSimpleName() + "[", "]") + .add("Type=" + type) + .add("Layers=" + layers) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/FsLayerJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/FsLayerJson.java new file mode 100644 index 0000000..62f3b09 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/FsLayerJson.java @@ -0,0 +1,29 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; + +public class FsLayerJson { + + private String blobSum; + + @JsonProperty("blobSum") + public String getBlobSum() { + return blobSum; + } + + public void setBlogSum(String blobSum) { + this.blobSum = blobSum; + } + + @Override + public String toString() { + return new StringJoiner(", ", FsLayerJson.class.getSimpleName() + "[", "]") + .add("Blob Sum=" + blobSum) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/HistoryJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/HistoryJson.java new file mode 100644 index 0000000..6f70bb4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/HistoryJson.java @@ -0,0 +1,73 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; + +public class HistoryJson { + + private String command; + private String created; + private String comment; + private String author; + private boolean empty; + + @JsonProperty("created_by") + public String getCommand() { + return command; + } + + public void setCommands(String command) { + this.command = command; + } + + @JsonProperty("created") + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + @JsonProperty("comment") + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + @JsonProperty("author") + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @JsonProperty("empty_layer") + public boolean isEmpty() { + return empty; + } + + public void setEmpty(boolean empty) { + this.empty = empty; + } + + @Override + public String toString() { + return new StringJoiner(", ", HistoryJson.class.getSimpleName() + "[", "]") + .add("Created=" + created) + .add("Command=" + command) + .add("Comment=" + comment) + .add("Author=" + author) + .add("Empty=" + empty) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerJson.java new file mode 100644 index 0000000..7a14806 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerJson.java @@ -0,0 +1,67 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.time.Instant; +import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; + +public class LayerJson { + + private LayerId id; + private LayerId parentId; + private Instant created; + + @JsonProperty("id") + public LayerId getId() { + return id; + } + + public LayerJson setId(LayerId id) { + this.id = requireNonNull(id); + return this; + } + + @JsonProperty("parentId") + public LayerId getParentId() { + return parentId; + } + + public LayerJson setParentId(LayerId parentId) { + this.parentId = requireNonNull(parentId); + return this; + } + + @JsonProperty("parent") + public LayerId getParent() { + return parentId; + } + + public LayerJson setParent(LayerId parentId) { + this.parentId = requireNonNull(parentId); + return this; + } + + + public Instant getCreated() { + return created; + } + + public LayerJson setCreated(Instant created) { + this.created = requireNonNull(created); + return this; + } + + @Override + public String toString() { + return new StringJoiner(", ", LayerJson.class.getSimpleName() + "[", "]") + .add("Id=" + id) + .add("Paren tId=" + parentId) + .add("Created=" + created) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerManifestV1.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerManifestV1.java new file mode 100644 index 0000000..afd1c30 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerManifestV1.java @@ -0,0 +1,104 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.util.Arrays; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +public class LayerManifestV1 { + + private ContainerConfig containerConfig; + private LayerId id; + private LayerId parentId; + private String created; + private String architecture; + private String os; + private long size; + private boolean throwaway; + + @JsonProperty("id") + public LayerId getId() { + return id; + } + + public void setId(LayerId id) { + this.id = requireNonNull(id); + } + + @JsonProperty("parentId") + public LayerId getParentId() { + return parentId; + } + + public void setParentId(LayerId parentId) { + this.parentId = requireNonNull(parentId); + } + + @JsonProperty("parent") + public LayerId getParent() { + return parentId; + } + + public void setParent(LayerId parentId) { + this.parentId = requireNonNull(parentId); + } + + @JsonProperty("created") + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = requireNonNull(created); + } + + @JsonProperty("architecture") + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = requireNonNull(architecture); + } + + @JsonProperty("os") + public String getOs() { + return os; + } + + public void setOs(String os) { + this.os = requireNonNull(os); + } + + @JsonProperty("Size") + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = requireNonNull(size); + } + + @JsonProperty("throwaway") + public boolean getThrowaway() { + return throwaway; + } + + public void setThrowaway(boolean throwaway) { + this.throwaway = throwaway; + } + + @JsonProperty("container_config") + public ContainerConfig getContainerConfig() { + return containerConfig; + } + + public void setContainerConfig(ContainerConfig containerConfig) { + this.containerConfig = containerConfig; + } + + public String getCommands() { + return containerConfig == null || containerConfig.getCommands() == null ? null : Arrays.stream(containerConfig.getCommands()).collect(joining(" ")); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerReferenceJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerReferenceJson.java new file mode 100644 index 0000000..ce52be4 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/LayerReferenceJson.java @@ -0,0 +1,51 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; + +public class LayerReferenceJson { + + private String mediaType; + private long size; + private String digest; + + @JsonProperty("mediaType") + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + @JsonProperty("size") + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + @JsonProperty("digest") + public String getDigest() { + return digest; + } + + public void setDigest(String digest) { + this.digest = digest; + } + + @Override + public String toString() { + return new StringJoiner(", ", LayerReferenceJson.class.getSimpleName() + "[", "]") + .add("Media Type=" + mediaType) + .add("Size=" + size) + .add("Digest=" + digest) + .toString(); + } +} 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 new file mode 100644 index 0000000..f10cfd3 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/Manifest.java @@ -0,0 +1,32 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.rapid7.container.analyzer.docker.model.image.ImageId; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.util.List; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + defaultImpl = TarManifestJson.class, + property = "schemaVersion") +@JsonSubTypes({ + @JsonSubTypes.Type(value = TarManifestJson.class), + @JsonSubTypes.Type(value = ManifestJsonV2S1.class, name = "1"), + @JsonSubTypes.Type(value = ManifestJsonV2S2.class, name = "2") +}) +public interface Manifest { + + public List getLayers(); + + public default List getLayerBlobIds() { + return getLayers(); + } + + public ImageId getImageId(); + + public long getSize(); + + public String getType(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S1.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S1.java new file mode 100644 index 0000000..1f7639f --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S1.java @@ -0,0 +1,191 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.rapid7.container.analyzer.docker.model.image.ImageId; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; +import static java.util.stream.Collectors.summingLong; +import static java.util.stream.Collectors.toList; + +/** + * Models an image manifest, schema version 2, schema 1. + * + * @see Docker Image Manifest v2 Schema 1 + */ +public class ManifestJsonV2S1 implements Manifest, Configuration { + + private static final LayerId EMPTY_LAYER_ID = new LayerId("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"); + private String name; + private String tag; + private String architecture; + private List fsLayers; + private List v1history; + private static ObjectMapper objectMapper; + private List layers; + private List history; + private List layerManifests; + + static { + objectMapper = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .registerModule(new JavaTimeModule()); + } + + public ManifestJsonV2S1() { + fsLayers = new ArrayList<>(); + v1history = new ArrayList<>(); + history = new ArrayList<>(); + layers = new ArrayList<>(); + layerManifests = new ArrayList<>(); + } + + @JsonProperty("name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonProperty("tag") + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + @Override + @JsonProperty("architecture") + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + @JsonProperty("fsLayers") + public List getFsLayers() { + return fsLayers; + } + + public void setFsLayers(List fsLayers) { + this.fsLayers = fsLayers; + } + + @JsonProperty("history") + public List getV1History() { + return v1history; + } + + public void setV1History(List v1history) { + this.v1history = v1history; + + try { + for (V1CompatibleHistory v1History : this.v1history) { + + LayerManifestV1 layerManifest = objectMapper.readValue(v1History.getV1Compatibility(), LayerManifestV1.class); + + HistoryJson layerHistory = new HistoryJson(); + layerHistory.setCreated(layerManifest.getCreated()); + layerHistory.setCommands(layerManifest.getCommands()); + layerHistory.setEmpty(layerManifest.getThrowaway()); + + if (!layerManifest.getThrowaway()) + layers.add(layerManifest.getId()); + + history.add(layerHistory); + } + + // the histories are in last-first order (top-most to bottom-most) + Collections.reverse(history); + + // the layer are in last-first order (top-most to bottom-most) + Collections.reverse(layers); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + @Override + public List getLayerBlobIds() { + // some very old instances of the manifest have no "throwaway" (empty layer) declarations, in this + // case empty layers are not filtered out from the fs layer blob ids + boolean filterEmpty = layers.size() != history.size(); + + // in normal situations, just filter out all layer blob IDs for empty layers (the layer ID of + // an empty layer is consistently hashed) + List retrievalIds = new ArrayList<>(); + for (FsLayerJson fsLayer : fsLayers) { + LayerId layerId = new LayerId(fsLayer.getBlobSum()); + if (!filterEmpty || !layerId.equals(EMPTY_LAYER_ID)) + retrievalIds.add(layerId); + } + + // there are also cases where the number of layer histories subtract the number of throwaway layers + // does not equal the number of layer blob IDs; if the number of non-empty layers is not equivalent + // to the number of non-empty layer blobs, then treat them as one-to-one with no filtering + if (retrievalIds.size() != layers.size()) + retrievalIds = fsLayers.stream().map(fsLayer -> new LayerId(fsLayer.getBlobSum())).collect(toList()); + + // retrieval blob ids are in reverse order (top-most to bottom-most) + Collections.reverse(retrievalIds); + + return retrievalIds; + } + + @Override + public List getLayers() { + return layers; + } + + @Override + public ImageId getImageId() { + // does not have an ID + return null; + } + + @Override + public long getSize() { + return layerManifests.stream().collect(summingLong(LayerManifestV1::getSize)); + } + + @Override + public List getHistory() { + return history; + } + + @Override + public Instant getCreated() { + return null; + } + + @Override + public String getOperatingSystem() { + return null; + } + + @Override + public String getType() { + return "v2s1"; + } + + @Override + public String toString() { + return new StringJoiner(", ", ManifestJsonV2S1.class.getSimpleName() + "[", "]") + .add("Name=" + name) + .add("Tag=" + tag) + .add("Architecture=" + architecture) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S2.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S2.java new file mode 100644 index 0000000..1305275 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/ManifestJsonV2S2.java @@ -0,0 +1,82 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.ImageId; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import static java.util.stream.Collectors.summingLong; +import static java.util.stream.Collectors.toList; + +/** + * Models an image manifest, schema version 2. + * + * @see Docker Image Manifest v2 + */ +public class ManifestJsonV2S2 implements Manifest { + + private String mediaType; + private LayerReferenceJson config; + private List layers; + + public ManifestJsonV2S2() { + layers = new ArrayList<>(); + } + + @JsonProperty("mediaType") + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + @JsonProperty("config") + public LayerReferenceJson getConfig() { + return config; + } + + public void setConfig(LayerReferenceJson config) { + this.config = config; + } + + @JsonProperty("layers") + public List getLayerReferences() { + return layers; + } + + public void setLayerReferences(List layers) { + this.layers = layers; + } + + @Override + public List getLayers() { + return getLayerReferences().stream().map(reference -> reference.getDigest()).map(LayerId::new).collect(toList()); + } + + @Override + public ImageId getImageId() { + return config == null ? null : config.getDigest() == null ? null : new ImageId(config.getDigest()); + } + + @Override + public long getSize() { + return layers.stream().collect(summingLong(LayerReferenceJson::getSize)); + } + + @Override + public String getType() { + return "v2s2"; + } + + @Override + public String toString() { + return new StringJoiner(", ", ManifestJsonV2S2.class.getSimpleName() + "[", "]") + .add("Media Type=" + mediaType) + .add("Config=" + config) + .add("Layers=" + layers) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/TarManifestJson.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/TarManifestJson.java new file mode 100644 index 0000000..e3f5e72 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/TarManifestJson.java @@ -0,0 +1,90 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.image.ImageId; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +public class TarManifestJson implements Manifest { + + private String config; + private List repoTags; + private List layers; + private long size; + + public TarManifestJson() { + size = -1L; + repoTags = new ArrayList<>(); + layers = new ArrayList<>(); + } + + @JsonProperty("Config") + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = requireNonNull(config, "config"); + } + + @JsonProperty("RepoTags") + public List getRepoTags() { + return repoTags; + } + + public void setRepoTags(List repoTags) { + this.repoTags = repoTags; + } + + @JsonProperty("Layers") + public List getLayerIds() { + return layers; + } + + public void setLayerIds(List layers) { + this.layers = requireNonNull(layers, "layers"); + } + + @Override + public List getLayers() { + return getLayerIds().stream().map(layer -> layer.replaceAll("/layer.tar", "")).map(LayerId::new).collect(toList()); + } + + @Override + public ImageId getImageId() { + return new ImageId(getConfig().replaceAll(".json", "")); + } + + @Override + public long getSize() { + return size; + } + + public TarManifestJson setSize(long size) { + this.size = size; + return this; + } + + @Override + public String getType() { + return "docker save tar"; + } + + @Override + public String toString() { + return new StringJoiner(", ", TarManifestJson.class.getSimpleName() + "[", "]") + .add("Config=" + config) + .add("Repo Tags=" + repoTags) + .add("Layers=" + layers) + .add("Size=" + size) + .toString(); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/V1CompatibleHistory.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/V1CompatibleHistory.java new file mode 100644 index 0000000..55ef5af --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/V1CompatibleHistory.java @@ -0,0 +1,17 @@ +package com.rapid7.container.analyzer.docker.model.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class V1CompatibleHistory { + + String v1Compatibility; + + @JsonProperty("v1Compatibility") + public String getV1Compatibility() { + return v1Compatibility; + } + + public void setV1Compatibility(String v1Compatibility) { + this.v1Compatibility = v1Compatibility; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/model/json/mixin/HashIdMixin.java b/src/main/java/com/rapid7/container/analyzer/docker/model/json/mixin/HashIdMixin.java new file mode 100644 index 0000000..743327e --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/model/json/mixin/HashIdMixin.java @@ -0,0 +1,29 @@ +package com.rapid7.container.analyzer.docker.model.json.mixin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rapid7.container.analyzer.docker.model.HashId; +import com.rapid7.container.analyzer.docker.model.HashType; + +public abstract class HashIdMixin extends HashId { + + @JsonCreator + public HashIdMixin(String idStr) { + super(idStr); + } + + @Override + @JsonProperty("id") + public abstract String getString(); + + // ignored + + @Override + @JsonIgnore + public abstract String getId(); + + @Override + @JsonIgnore + public abstract HashType 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 new file mode 100644 index 0000000..e90aff1 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java @@ -0,0 +1,211 @@ + +/** + * + */ +package com.rapid7.container.analyzer.docker.os; + +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.recog.Recog; +import com.rapid7.recog.RecogMatchResult; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +/** + * OS release file parser that parses the contents of "os-release" to fingerprint the host system. + */ +public class Fingerprinter { + + private static final Pattern PATTERN = Pattern.compile("(?.*)=(?.*)"); + private static final Map OS_ID_TO_VENDOR = Arrays.stream(new String[][]{ + {"alpine", "Alpine"}, + {"amzn", "Amazon"}, + {"arch", "Arch"}, + {"centos", "CentOS"}, + {"debian", "Debian"}, + {"fedora", "Fedora"}, + {"gentoo", "Gentoo"}, + {"ol", "Oracle"}, + {"opensuse", "OpenSUSE"}, + {"photon", "VMWare"}, + {"rhel", "Red Hat"}, + {"ubuntu", "Ubuntu"}, + }).collect(toMap(kv -> kv[0], kv -> kv[1])); + + private Recog recog; + + public Fingerprinter(Recog recogClient) { + recog = requireNonNull(recogClient, "recog client"); + } + + /** + * Parses the contents of an os-release file to ascertain the fingerprint of an operating system. + * + * @param input The os-release file to parse. Must not be {@code null}. + * @return The fingerprint of the operating system, or {@code null} if one could not be detected. + */ + public OperatingSystem parse(InputStream input, String fileName, String architecture) throws IOException { + if (fileName.endsWith("/os-release") || fileName.endsWith("/lsb-release")) + return parseOsRelease(input, architecture); + else if (fileName.endsWith("/alpine-release")) + return parseAlpineRelease(input, architecture); + else if (fileName.endsWith("/photon-release")) + return parsePhotonRelease(input, architecture); + else // catch-all for rhel-like release files and anything else we missed + return parseRhelFamilyRelease(input, architecture); + } + + /** + * Parse os-release and lsb-release formats. + */ + private OperatingSystem parseOsRelease(InputStream input, String architecture) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String id = null; + String product = null; + String description = null; + String version = null; + while ((line = reader.readLine()) != null) { + Matcher matcher = PATTERN.matcher(line); + if (matcher.matches()) { + String name = matcher.group("name"); + String value = matcher.group("value"); + switch (name) { + case "ID": + id = value.replaceAll("\"", ""); + break; + case "NAME": + case "DISTRIB_ID": + product = value.replaceAll("\"", ""); + break; + case "VERSION_ID": + case "DISTRIB_RELEASE": + version = value.replaceAll("(?:^v|\\\")", ""); + break; + case "PRETTY_NAME": + case "DISTRIB_DESCRIPTION": + description = value.replaceAll("\"", ""); + break; + default: + break; + } + } + } + + String vendor = OS_ID_TO_VENDOR.get(id); + OperatingSystem operatingSystem = null; + + // attempt to run recog against the name and version, which should be accurate already + if (product != null && !product.isEmpty() && version != null && !version.isEmpty()) + operatingSystem = fingerprintOperatingSystem(product + " " + version, version, vendor, architecture); + + // try with description if first try didn't match + if (operatingSystem == null && description != null) + operatingSystem = fingerprintOperatingSystem(description, version, vendor, architecture); + + // try with product name if the description was null or didn't match + if (operatingSystem == null && product != null) + operatingSystem = fingerprintOperatingSystem(product, version, vendor, architecture); + + // default to building a fingerprint with the distribution ID and release version + if (operatingSystem == null && product != null && version != null) + operatingSystem = new OperatingSystem(vendor == null ? trimProductName(product) : vendor, "Linux", "Linux", architecture, version, description); + + return operatingSystem; + } + } + + private OperatingSystem parseRhelFamilyRelease(InputStream input, String architecture) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String description = ""; + while ((line = reader.readLine()) != null) { + description += line; + } + + return fingerprintOperatingSystem(description, null, null, architecture); + } + } + + private OperatingSystem parseAlpineRelease(InputStream input, String architecture) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String version = null; + while ((line = reader.readLine()) != null) { + if (version == null && line.matches("(?:\\d+(?:\\.)?)+")) + version = line; + } + + return fingerprintOperatingSystem("Alpine Linux", version, "Alpine", architecture); + } + } + + private OperatingSystem parsePhotonRelease(InputStream input, String architecture) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String description = null; + while ((line = reader.readLine()) != null) { + if (line.startsWith("VMWare")) + description = line; + } + + return fingerprintOperatingSystem(description, null, "VMWare", architecture); + } + } + + private String trimProductName(String productName) { + return productName.replaceAll("(?i:\\s+(?:Enterprise|(?:GNU/)?Linux).*)", ""); + } + + public Package fingerprintPackage(OperatingSystem operatingSystem, String pkg) { + + Pattern pattern = Pattern.compile("(?.*) (?.*).*"); + Matcher matcher = pattern.matcher(pkg); + if (matcher.matches()) { + String name = matcher.group("name"); + String version = matcher.group("version"); + return new Package("linux", PackageType.UNKNOWN, operatingSystem, name, version, pkg, 0L, null, null, null); + } else + return null; + } + + public OperatingSystem fingerprintOperatingSystem(String productDescription, String productArchitecture) { + return fingerprintOperatingSystem(productDescription, null, null, productArchitecture); + } + + public OperatingSystem fingerprintOperatingSystem(String productDescription, String productVersion, String productVendor, String productArchitecture) { + OperatingSystem operatingSystem = null; + if (productDescription == null || productDescription.isEmpty()) + return null; + + List matchResults = recog.fingerprint(productDescription); + if (!matchResults.isEmpty()) { + RecogMatchResult matchResult = matchResults.get(0); + Map matches = matchResult.getMatches(); + String vendor = (matches.get("os.vendor") == null ? "" : matches.get("os.vendor")); + if (productVendor != null && !productVendor.isEmpty() && !vendor.toLowerCase().contains(productVendor.toLowerCase())) + return operatingSystem; // probably a bad match from recog + + String description = Stream.of(matches.get("os.vendor"), matches.get("os.product"), matches.get("os.version") != null ? matches.get("os.version") : productVersion).filter(Objects::nonNull).collect(joining(" ")); + String architecture = matches.get("os.arch") == null ? productArchitecture : matches.get("os.arch"); + if (matches.get("os.product") != null) + operatingSystem = new OperatingSystem(matches.get("os.vendor"), matches.get("os.family"), matches.get("os.product"), architecture, matches.get("os.version") != null ? matches.get("os.version") : productVersion, description); + } + + return operatingSystem; + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/ApkgParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/ApkgParser.java new file mode 100644 index 0000000..9cd8baa --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/ApkgParser.java @@ -0,0 +1,54 @@ +package com.rapid7.container.analyzer.docker.packages; + +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import java.util.regex.Pattern; + +public class ApkgParser extends PackageParser { + + private static final Pattern APKG_PATTERN = Pattern.compile("(?.*):(?.*)"); + + public ApkgParser() { + super(APKG_PATTERN, PackageType.APGK, new PackageKeys() { + + @Override + public String getPackageKey() { + return "P"; + } + + @Override + public String getSourceKey() { + return "o"; + } + + @Override + public String getVersionKey() { + return "V"; + } + + @Override + public String getLicenseKey() { + return "L"; + } + + @Override + public String getDescriptionKey() { + return "T"; + } + + @Override + public String getMaintainerKey() { + return "m"; + } + + @Override + public String getHomePageKey() { + return "U"; + } + + @Override + public String getSizeKey() { + return "I"; + } + }); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/DpkgParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/DpkgParser.java new file mode 100644 index 0000000..ff12337 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/DpkgParser.java @@ -0,0 +1,54 @@ +package com.rapid7.container.analyzer.docker.packages; + +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import java.util.regex.Pattern; + +public class DpkgParser extends PackageParser { + + private static final Pattern DPKG_PATTERN = Pattern.compile("(?.*): (?.*)"); + + public DpkgParser() { + super(DPKG_PATTERN, PackageType.DPGK, new PackageKeys() { + + @Override + public String getPackageKey() { + return "Package"; + } + + @Override + public String getSourceKey() { + return "Source"; + } + + @Override + public String getVersionKey() { + return "Version"; + } + + @Override + public String getDescriptionKey() { + return "Description"; + } + + @Override + public String getMaintainerKey() { + return "Maintainer"; + } + + @Override + public String getHomePageKey() { + return "Homepage"; + } + + @Override + public String getSizeKey() { + return "Installed-Size"; + } + + @Override + public int getSizeScale() { + return 1000; + } + }); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/PackageParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/PackageParser.java new file mode 100644 index 0000000..e43523d --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/PackageParser.java @@ -0,0 +1,163 @@ +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.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; + +public abstract class PackageParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(PackageParser.class); + private static final Set statusBlacklist = Stream.of("deinstall ok config-files", "purge ok not-installed").collect(toSet()); + private Pattern pattern; + private PackageType type; + private PackageKeys keys; + + public PackageParser(Pattern pattern, PackageType type, PackageKeys keys) { + this.pattern = requireNonNull(pattern, "pattern"); + this.type = requireNonNull(type, "type"); + this.keys = requireNonNull(keys, "keys"); + } + + public Set parse(InputStream input, OperatingSystem operatingSystem) throws FileNotFoundException, IOException { + + LOGGER.info("Parsing packages using {} with operating system {}.", getClass().getSimpleName(), operatingSystem); + + Set packages = new HashSet<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String pkg = null; + String source = null; + String version = null; + Long installedSize = null; + String maintainer = null; + String homepage = null; + String description = null; + String license = null; + String epoch = null; + String release = null; + String status = null; + while ((line = reader.readLine()) != null) { + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + String name = matcher.group("name"); + String value = matcher.group("value"); + + if (name.equals(keys.getPackageKey())) { + if (pkg != null) { + if (!isBlacklisted(status)) + packages.add(new Package(source, type, operatingSystem, pkg, version, description, installedSize, maintainer, homepage, license, epoch, release)); + pkg = null; + source = null; + version = null; + installedSize = null; + maintainer = null; + homepage = null; + description = null; + license = null; + epoch = null; + release = null; + status = null; + } + + pkg = value; + } else if (name.equals(keys.getSourceKey())) + source = value; + else if (name.equals(keys.getVersionKey())) + version = value; + else if (name.equals(keys.getLicenseKey())) + license = value; + else if (name.equals(keys.getDescriptionKey())) + description = value; + else if (name.equals(keys.getMaintainerKey())) + maintainer = value; + else if (name.equals(keys.getHomePageKey())) + homepage = value; + else if (name.equals(keys.getEpochKey())) + epoch = value; + else if (name.equals(keys.getReleaseKey())) + release = value; + else if (name.equals(keys.getSizeKey())) + installedSize = Long.parseLong(value) * keys.getSizeScale(); + else if (name.equals(keys.getStatusKey())) + status = value; + } else { + // TODO: wrapped content from previous section + } + } + + if (pkg != null) { + if (!isBlacklisted(status)) + packages.add(new Package(source, type, operatingSystem, pkg, version, description, installedSize, maintainer, homepage, license, epoch, release)); + + pkg = null; + source = null; + version = null; + installedSize = null; + maintainer = null; + homepage = null; + description = null; + license = null; + epoch = null; + release = null; + } + } + + return packages; + } + + private boolean isBlacklisted(String value) { + return value != null && statusBlacklist.contains(value.toLowerCase()); + } + + public static interface PackageKeys { + + public String getPackageKey(); + + public String getSourceKey(); + + public String getVersionKey(); + + public String getDescriptionKey(); + + public String getMaintainerKey(); + + public String getHomePageKey(); + + public String getSizeKey(); + + public default String getLicenseKey() { + return "License"; + } + + public default String getEpochKey() { + return "Epoch"; + } + + public default String getReleaseKey() { + return "Release"; + } + + public default int getSizeScale() { + return 1; + } + + public default String getStatusKey() { + return "Status"; + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/PacmanPackageParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/PacmanPackageParser.java new file mode 100644 index 0000000..958d37b --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/PacmanPackageParser.java @@ -0,0 +1,97 @@ +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.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; + +public class PacmanPackageParser { + + public Set parse(InputStream input, OperatingSystem operatingSystem) throws FileNotFoundException, IOException { + Set packages = new HashSet<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line = null; + String pkg = null; + String source = null; + String version = null; + long installedSize = -1L; + String maintainer = null; + String homepage = null; + String description = null; + String license = null; + String token = "name"; + while ((line = reader.readLine()) != null) { + + if (line.startsWith("%")) + token = line; + else if (line != null && !line.isEmpty()) { + switch (token) { + case "%NAME%": + if (pkg != null) + packages.add(new Package(source, PackageType.PACMAN, operatingSystem, pkg, version, description, installedSize, maintainer, homepage, license)); + + pkg = line; + break; + case "%VERSION%": + version = line; + break; + case "%DESC%": + description = line; + break; + case "%URL%": + homepage = line; + break; + case "%PACKAGER%": + maintainer = line; + break; + case "%SIZE%": + installedSize = Long.parseLong(line); + break; + case "%LICENSE%": + license = line; + break; + default: + break; + } + } + } + + if (pkg != null) + packages.add(new Package(source, PackageType.PACMAN, operatingSystem, pkg, version, description, installedSize, maintainer, homepage, license)); + } + + return packages; + } + + public static interface PackageKeys { + + public String getPackageKey(); + + public String getSourceKey(); + + public String getVersionKey(); + + public String getDescriptionKey(); + + public String getMaintainerKey(); + + public String getHomePageKey(); + + public String getSizeKey(); + + public default String getLicenseKey() { + return "License"; + } + + public default int getSizeScale() { + return 1; + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/packages/RpmPackageParser.java b/src/main/java/com/rapid7/container/analyzer/docker/packages/RpmPackageParser.java new file mode 100644 index 0000000..316b529 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/packages/RpmPackageParser.java @@ -0,0 +1,64 @@ +package com.rapid7.container.analyzer.docker.packages; + +import com.rapid7.container.analyzer.docker.model.image.PackageType; +import java.util.regex.Pattern; + +public class RpmPackageParser extends PackageParser { + + private static final Pattern RPM_PATTERN = Pattern.compile("(?.*):(?.*)"); + + public RpmPackageParser() { + super(RPM_PATTERN, PackageType.RPM, new PackageKeys() { + + @Override + public String getPackageKey() { + return "Package"; + } + + @Override + public String getSourceKey() { + return "Source"; + } + + @Override + public String getVersionKey() { + return "Version"; + } + + @Override + public String getDescriptionKey() { + return "Description"; + } + + @Override + public String getMaintainerKey() { + return "Maintainer"; + } + + @Override + public String getHomePageKey() { + return "Homepage"; + } + + @Override + public String getSizeKey() { + return "Installed-Size"; + } + + @Override + public String getLicenseKey() { + return "License"; + } + + @Override + public String getEpochKey() { + return "Epoch"; + } + + @Override + public String getReleaseKey() { + return "Release"; + } + }); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/recog/RecogClient.java b/src/main/java/com/rapid7/container/analyzer/docker/recog/RecogClient.java new file mode 100644 index 0000000..6c5940a --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/recog/RecogClient.java @@ -0,0 +1,62 @@ +package com.rapid7.container.analyzer.docker.recog; + +import com.rapid7.recog.Recog; +import com.rapid7.recog.RecogMatch; +import com.rapid7.recog.RecogMatchResult; +import com.rapid7.recog.RecogMatcher; +import com.rapid7.recog.RecogMatchers; +import com.rapid7.recog.RecogVersion; +import com.rapid7.recog.provider.RecogMatchersProvider; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import static com.rapid7.recog.RecogType.BUILTIN; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +public class RecogClient implements Recog { + + private RecogMatchersProvider provider; + + public RecogClient(File matchersDirectory) { + this.provider = new RecogMatchersProvider(BUILTIN, matchersDirectory); + } + + @Override + public List fingerprint(String description) { + List matches = new ArrayList<>(); + matches.addAll(fingerprint(description, getMatchers())); + + // sort the results by the matcher preference, then OS certainty + return matches.stream() + .sorted( + comparing((RecogMatchResult match) -> match.getPreference()) + .thenComparing((RecogMatchResult match) -> match.getMatches().containsKey("os.certainty") ? Float.valueOf(match.getMatches().get("os.certainty")) : 1) + .reversed() + ).collect(toList()); + } + + private Collection fingerprint(String description, Collection recogMatchers) { + + Collection matches = new ArrayList<>(); + + for (RecogMatchers matchers : recogMatchers) { + for (RecogMatch match : matchers.getMatches(description)) { + RecogMatcher matcher = match.getMatcher(); + matches.add(new RecogMatchResult(matchers.getKey(), matchers.getType(), matchers.getProtocol(), matchers.getPreference(), match.getMatcher().getDescription(), matcher.getPattern(), matcher.getExamples(), match.getParameters())); + } + } + + return matches; + } + + @Override + public RecogVersion refreshContent() { + throw new UnsupportedOperationException("Local recog content cannot be refreshed"); + } + + protected Collection getMatchers() { + return provider.getMatchers(BUILTIN); + } +} 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 new file mode 100644 index 0000000..77c838a --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerService.java @@ -0,0 +1,343 @@ +package com.rapid7.container.analyzer.docker.service; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rapid7.container.analyzer.docker.analyzer.ImageHandler; +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.DpkgFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.FileFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.OsReleaseFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.PacmanFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.RpmFingerprinter; +import com.rapid7.container.analyzer.docker.fingerprinter.WhiteoutImageHandler; +import com.rapid7.container.analyzer.docker.model.Digest; +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.ImageType; +import com.rapid7.container.analyzer.docker.model.image.Layer; +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import com.rapid7.container.analyzer.docker.model.image.json.ImageModelObjectMapper; +import com.rapid7.container.analyzer.docker.model.json.Configuration; +import com.rapid7.container.analyzer.docker.model.json.ConfigurationJsonV2; +import com.rapid7.container.analyzer.docker.model.json.HistoryJson; +import com.rapid7.container.analyzer.docker.model.json.LayerJson; +import com.rapid7.container.analyzer.docker.model.json.Manifest; +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.DpkgParser; +import com.rapid7.container.analyzer.docker.packages.PacmanPackageParser; +import com.rapid7.container.analyzer.docker.packages.RpmPackageParser; +import com.rapid7.container.analyzer.docker.recog.RecogClient; +import com.rapid7.container.analyzer.docker.util.InstantParser; +import com.rapid7.container.analyzer.docker.util.InstantParserModule; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.FilenameUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import static java.util.stream.Collectors.summingLong; +import static org.apache.commons.io.FileUtils.deleteQuietly; +import static org.slf4j.helpers.MessageFormatter.arrayFormat; +import static org.slf4j.helpers.MessageFormatter.format; + +public class DockerImageAnalyzerService { + + private static final Logger LOGGER = LoggerFactory.getLogger(DockerImageAnalyzerService.class); + private static final String WHITEOUT_AUFS_PREFIX = ".wh."; + private ObjectMapper objectMapper; + private List layerHandlers; + private List imageHandlers; + + public DockerImageAnalyzerService(String rpmDockerImage, File recogMatchersDirectory) { + objectMapper = new ObjectMapper(); + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.registerModule(new InstantParserModule()); + imageHandlers = new ArrayList<>(1); + imageHandlers.add(new WhiteoutImageHandler()); + layerHandlers = new ArrayList<>(6); + layerHandlers.add(new OsReleaseFingerprinter(new Fingerprinter(new RecogClient(recogMatchersDirectory)))); + layerHandlers.add(new RpmFingerprinter(new RpmPackageParser(), rpmDockerImage)); + layerHandlers.add(new DpkgFingerprinter(new DpkgParser())); + layerHandlers.add(new ApkgFingerprinter(new ApkgParser())); + layerHandlers.add(new PacmanFingerprinter(new PacmanPackageParser())); + layerHandlers.add(new FileFingerprinter()); + + initialize(); + } + + private void initialize() { + layerHandlers.forEach(handler -> LOGGER.info("Handler " + handler.getClass().getSimpleName())); + } + + public ImageId getId(File imageTar) throws IOException { + + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new BufferedInputStream(new FileInputStream(imageTar)))) { + TarArchiveEntry entry = null; + while ((entry = tarIn.getNextTarEntry()) != null) { + String name = entry.getName(); + + if (name.equals("manifest.json")) { + + List manifests = objectMapper.readValue(new NonClosingInputStream(tarIn), objectMapper.getTypeFactory().constructCollectionType(List.class, TarManifestJson.class)); + TarManifestJson manifest = manifests.get(0); + + return new ImageId(manifest.getConfig().replaceAll(".json", "")); + } + } + } + + return null; + } + + public void writeJson(Image image, File destination) throws IOException { + ObjectMapper imageMapper = new ImageModelObjectMapper(); + imageMapper.writeValue(destination, image); + } + + public Image analyze(File imageTar, String tempFilePath) throws IOException { + + File outputDirectory = Files.createTempDirectory(Paths.get(tempFilePath), FilenameUtils.removeExtension(imageTar.getName())).toFile(); + try { + LOGGER.info("Extracting image to {}.", outputDirectory.getAbsolutePath()); + // extract the tar + untar(imageTar, outputDirectory); + LOGGER.info("Extracted image {}.", outputDirectory.getAbsolutePath()); + TarManifestJson manifest = parseTarManifest(imageTar.length(), new File(outputDirectory, "manifest.json")); + Image image = analyze( + outputDirectory, + manifest.getImageId(), + null, + manifest, + parseConfiguration(new File(outputDirectory, manifest.getConfig())), + layerId -> new File(outputDirectory, layerId.getId() + "/layer.tar")); + + LOGGER.info("Completed analyzing image {}", image.getId()); + return image; + } finally { + deleteQuietly(outputDirectory); + } + } + + public Image analyze(File outputDirectory, ImageId id, Digest digest, M manifest, Configuration configuration, LayerExtractor layerSupplier) throws IOException { + + try { + // parse the image configuration + List layerHistories = configuration.getHistory(); + + // the ordered identifiers of non-empty layers (which are presumed to have file-system changes requiring a blob) + List layers = manifest.getLayers(); + + // the ordered identifiers of the blobs to retrieve for each non-empty layer + List layerBlobIds = manifest.getLayerBlobIds(); + + // TODO: not pulling out the repository in the digest reference + Image image = new Image(id, ImageType.DOCKER, digest, manifest.getSize(), configuration.getCreated()); + + int layerIndex = 0; + int emptyLayerIdSuffix = 0; + Layer previousLayer = null; + for (HistoryJson layerHistory : layerHistories) { + + Instant start = Instant.now(); + boolean empty = layerHistory.isEmpty(); + LayerId layerId = null; + if (!empty) { + layerId = layers.get(layerIndex); + } else + layerId = new LayerId(id.getString() + "_empty_" + emptyLayerIdSuffix++); + + MDC.put("layer_id", layerId.getString()); + LOGGER.info("Processing layer."); + try { + Layer layer = new Layer(layerId); + layer.setCommand(layerHistory.getCommand()); + layer.setComment(layerHistory.getComment()); + layer.setAuthor(layerHistory.getAuthor()); + layer.setCreated(InstantParser.parse(layerHistory.getCreated())); + layer.setEmpty(empty); + + // if the layer is non-empty, process it + if (!empty) { + + // locate the layer to process + LayerId layerRetrievalId = layerBlobIds.get(layerIndex); + File layerTar = layerSupplier.getLayer(layerRetrievalId); + layer.setSize(layerTar.length()); + + MDC.put("layer_blob_id", layerRetrievalId.getString()); + try { + LOGGER.info("Processing layer tar."); + + // extract layer + processLayer(image, configuration, layer, layerTar); + + // attach additional layer information + File layerInfoFile = new File(layerTar.getParentFile(), "json"); + if (layerInfoFile.exists()) { + try { + LayerJson layerInfo = parseLayerConfiguration(layerInfoFile); + layer.setCreated(layerInfo.getCreated()); + layer.setParentId(layerInfo.getParentId()); + } catch (Exception exception) { + LOGGER.warn("Failed to parse layer configuration json. Skipping setting of created data and parent identifier.", exception); + } + } else if (previousLayer != null) { + layer.setParentId(previousLayer.getId()); + } + } finally { + MDC.remove("layer_blob_id"); + } + + layerIndex++; + previousLayer = layer; + } else if (previousLayer != null) { + layer.setParentId(previousLayer.getId()); + } + + image.addLayer(layer); + + if (layer.getOperatingSystem() != null) + MDC.put("operating_system", layer.getOperatingSystem().toString()); + + MDC.put("packages", Integer.toString(layer.getPackages().size())); + try { + Duration duration = Duration.between(start, Instant.now()); + MDC.put("payload_process_time_ms", String.valueOf(duration.toMillis())); + LOGGER.info("Layer processed."); + } finally { + MDC.remove("operating_system"); + MDC.remove("packages"); + MDC.remove("payload_process_time_ms"); + } + } finally { + MDC.remove("layer_id"); + } + } + + // post-process image handlers + for (ImageHandler handler : imageHandlers) + handler.handle(image); + + // if the size on the image isn't known (e.g. from a manifest that does not support it), sum up layers as a last resort + if (image.getSize() == null || image.getSize() <= 0L) + image.setSize(image.getLayers().stream().collect(summingLong(Layer::getSize))); + + // if the created date on an image isn't known, use the last created date from the last layer + if (image.getCreated() == null && !image.getLayers().isEmpty()) + image.setCreated(image.getLayers().get(image.getLayers().size() - 1).getCreated()); + + return image; + } catch (IOException exception) { + LOGGER.error("Failed to analyze image.", exception); + throw exception; + } + } + + public Manifest parseManifest(File file) throws JsonParseException, JsonMappingException, IOException { + return objectMapper.readValue(file, Manifest.class); // TODO: polymorphic + } + + public Configuration parseConfiguration(File file) throws JsonParseException, JsonMappingException, IOException { + return objectMapper.readValue(file, ConfigurationJsonV2.class); + } + + public TarManifestJson parseTarManifest(long tarSize, File file) throws JsonParseException, JsonMappingException, IOException { + List manifests = objectMapper.readValue(file, objectMapper.getTypeFactory().constructCollectionType(List.class, TarManifestJson.class)); + return manifests.get(0).setSize(tarSize); + } + + public LayerJson parseLayerConfiguration(File file) throws JsonParseException, JsonMappingException, IOException { + return objectMapper.readValue(file, LayerJson.class); + } + + public void untar(File tar, File destination) throws FileNotFoundException, IOException { + + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new BufferedInputStream(new FileInputStream(tar)))) { + TarArchiveEntry entry = null; + while ((entry = tarIn.getNextTarEntry()) != null) { + String name = entry.getName(); + File file = new File(destination, name); + try { + if (!file.toPath().toAbsolutePath().normalize().startsWith(destination.toPath().toAbsolutePath().normalize())) { + LOGGER.debug(format("[Image: {}] Skipping extraction of {} due to directory traversal.", tar.getName(), name).getMessage()); + continue; + } + } catch (InvalidPathException exception) { + LOGGER.debug(format("[Image: {}] Skipping extraction of {} due to invalid filename.", tar.getName(), name).getMessage()); + continue; + } + + LOGGER.debug(arrayFormat("[Image: {}] Extracting {} {}.", new Object[]{tar.getName(), entry.isDirectory() ? "directory" : "file", file.getAbsolutePath()}).getMessage()); + + long usableSpace = file.getParentFile().getFreeSpace(); + long objectSize = entry.getRealSize(); + if (objectSize * 3 > usableSpace) { + throw new IOException("Insufficient disk space to extract resource. " + usableSpace + " available, " + objectSize * 3 + " required."); + } + + try { + if (entry.isDirectory()) { + file.mkdirs(); + } else if (!name.contains(WHITEOUT_AUFS_PREFIX)) { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + IOUtils.copy(tarIn, outputStream); + } + } else { + LOGGER.debug(format("[Image: {}] Skipping whiteout file {}.", tar.getName(), file.getAbsolutePath()).getMessage()); + } + } catch (FileNotFoundException exception) { + // can occur if there is case sensitivity collision, etc + LOGGER.warn(format("[Image: {}] File failed to extract {}.", tar.getName(), file.getAbsolutePath()).getMessage()); + } + } + } + } + + private void processLayer(Image image, Configuration configuration, Layer layer, File tar) throws FileNotFoundException, IOException { + + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new BufferedInputStream(new FileInputStream(tar)))) { + TarArchiveEntry entry = null; + while ((entry = tarIn.getNextTarEntry()) != null) { + String name = entry.getName(); + + LOGGER.trace(arrayFormat("[Image: {}] Processing {} {}.", new Object[]{tar.getName(), entry.isDirectory() ? "directory" : "file", name}).getMessage()); + + for (LayerFileHandler handler : layerHandlers) { + handler.handle(name, entry, new NonClosingInputStream(tarIn), image, configuration, layer); + } + } + } + } + + private static class NonClosingInputStream extends FilterInputStream { + NonClosingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + } + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/util/InstantCustomDeserializer.java b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantCustomDeserializer.java new file mode 100644 index 0000000..8ee80d8 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantCustomDeserializer.java @@ -0,0 +1,14 @@ +package com.rapid7.container.analyzer.docker.util; + +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +public class InstantCustomDeserializer extends InstantDeserializer { + + public static final InstantCustomDeserializer INSTANT = new InstantCustomDeserializer(Instant.class, InstantParser.INSTANT_FORMATTER); + + protected InstantCustomDeserializer(Class supportedType, DateTimeFormatter formatter) { + super(supportedType, formatter, Instant::from, a -> Instant.ofEpochMilli(a.value), a -> Instant.ofEpochSecond(a.integer, a.fraction), null, true); + } +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParser.java b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParser.java new file mode 100644 index 0000000..e6e93a5 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParser.java @@ -0,0 +1,42 @@ +package com.rapid7.container.analyzer.docker.util; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + +/** + * Parses {@link Instant} objects from string representations using a flexible ISO 8601 based + * format. This parser allows for any of the following "extended" formats: + * - YYYY-MM-DD'T'hh:mm:ss[.nnn]Z UTC date and time + * - YYYY-MM-DD'T'hh:mm:ss[.nnn][+|-]hh:mm local date time w/ zone-offset + * - YYYY-MM-DD'T'hh:mm:ss[.nnn][+|-]hh:mm[zone-id] + * When an input format does not have a time specified, the default is 12 am. + */ +public class InstantParser { + + public static Instant parse(CharSequence text) { + return INSTANT_FORMATTER.parse(text, Instant::from); + } + + public static final DateTimeFormatter INSTANT_FORMATTER = + new DateTimeFormatterBuilder().parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral('T') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .appendOffsetId() + .optionalStart() + .appendLiteral('[') + .parseCaseSensitive() + .appendZoneRegionId() + .appendLiteral(']') + .optionalEnd() + .optionalEnd() + .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) + .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) + .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) + .parseDefaulting(ChronoField.NANO_OF_SECOND, 0) + .parseDefaulting(ChronoField.OFFSET_SECONDS, 0) + .toFormatter(); +} diff --git a/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParserModule.java b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParserModule.java new file mode 100644 index 0000000..8b927a8 --- /dev/null +++ b/src/main/java/com/rapid7/container/analyzer/docker/util/InstantParserModule.java @@ -0,0 +1,14 @@ +package com.rapid7.container.analyzer.docker.util; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import java.time.Instant; + +public class InstantParserModule extends SimpleModule { + + public InstantParserModule() { + super(PackageVersion.VERSION); + addDeserializer(Instant.class, InstantCustomDeserializer.INSTANT); + } + +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/packages/DpkgParserTest.java b/src/test/java/com/rapid7/container/analyzer/docker/packages/DpkgParserTest.java new file mode 100644 index 0000000..869188d --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/packages/DpkgParserTest.java @@ -0,0 +1,57 @@ +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.FileNotFoundException; +import java.io.IOException; +import java.util.Set; +import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.samePropertyValuesAs; + +public class DpkgParserTest { + + private static final OperatingSystem OS = new OperatingSystem("Debian", "Linux", "Linux", "x86_64", "9", "Debian Linux 9"); + private static final Package PACKAGE = new Package(null, PackageType.DPGK, OS, "dash", "0.5.8-2.4", "POSIX-compliant shell", 204000L, "Gerrit Pape ", "http://gondor.apana.org.au/~herbert/dash/", null); + + @Test + public void singleParse() throws FileNotFoundException, IOException { + // given + DpkgParser parser = new DpkgParser(); + + // when + Set packages = parser.parse(DpkgParserTest.class.getResourceAsStream("dpkg-dash.info"), OS); + + // then + assertThat(packages, hasItem(samePropertyValuesAs(PACKAGE))); + } + + @Test + public void multipleParse() throws FileNotFoundException, IOException { + // given + DpkgParser parser = new DpkgParser(); + + // when + Set packages = parser.parse(DpkgParserTest.class.getResourceAsStream("dpkg-multiple.info"), OS); + + // then + assertThat(packages, hasItem(samePropertyValuesAs(PACKAGE))); + } + + @Test + public void fullParse() throws FileNotFoundException, IOException { + // given + DpkgParser parser = new DpkgParser(); + + // when + Set packages = parser.parse(DpkgParserTest.class.getResourceAsStream("dpkg.info"), OS); + + // then + assertThat(packages.size(), is(equalTo(83))); + assertThat(packages, hasItem(samePropertyValuesAs(PACKAGE))); + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerMockConfig.java b/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerMockConfig.java new file mode 100644 index 0000000..9803e5b --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerMockConfig.java @@ -0,0 +1,29 @@ +package com.rapid7.container.analyzer.docker.test; + +import com.rapid7.recog.Recog; +import com.rapid7.recog.RecogMatchResult; +import java.util.List; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ImageAnalyzerMockConfig { + + public RecogMatcherService recogMatcherService; + + public Recog recogClient() { + Recog client = mock(Recog.class); + + when(client.fingerprint(anyString())).then(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return recogMatcherService.fingerprint((String) args[0]); + } + }); + + return client; + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java b/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java new file mode 100644 index 0000000..8c864e7 --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java @@ -0,0 +1,145 @@ +package com.rapid7.container.analyzer.docker.test; + +import com.rapid7.container.analyzer.docker.model.image.Image; +import com.rapid7.container.analyzer.docker.model.json.Manifest; +import com.rapid7.container.analyzer.docker.model.json.TarManifestJson; +import com.rapid7.container.analyzer.docker.service.DockerImageAnalyzerService; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.apache.commons.io.FileUtils.deleteQuietly; + +public class ImageAnalyzerTester { + + private static final Logger LOGGER = LoggerFactory.getLogger(ImageAnalyzerTester.class); + private DockerImageAnalyzerService analyzer; + private static File recogContentDirectory; + private static final long MAX_SIZE = 2147483648L; + + /** + * Test image extraction and fingerprinting. Provide the full path to a local checkout of + * github.com/rapid7/recog and the full path to either a docker image tar file (from docker save) + * or a directory containing manifest, config, and layer files (from image puller lambda - S3 + * bucket). + * + * @param args Path to Recog, Path to Image file(s) + */ + public static void main(String[] args) throws IOException { + if (args.length < 2) + throw new IllegalArgumentException("Required arguments: recog path, image file path"); + + recogContentDirectory = new File(args[0]); + ImageAnalyzerTester tester = new ImageAnalyzerTester(); + tester.analyzeImage(new File(args[1])); + } + + public File recogContent() { + return recogContentDirectory; + } + + public void analyzeImage(File imagePath) throws IOException { + if (!imagePath.exists()) + throw new FileNotFoundException("File does not exist: " + imagePath); + + File outputDirectory = Files.createTempDirectory(null).toFile(); + outputDirectory.deleteOnExit(); + try { + if (imagePath.isFile()) { + LOGGER.info("Working with single tar file at {}", imagePath); + File unzippedTar = new File(outputDirectory, imagePath.getName().replaceAll("\\.gz$", "")); + long available = unzippedTar.getParentFile().getUsableSpace(); + try (FileOutputStream output = new FileOutputStream(unzippedTar)) { + try (GZIPInputStream input = new GZIPInputStream(new FileInputStream(imagePath))) { + byte[] buf = new byte[8192]; + int len; + long total = 0; + while ((len = input.read(buf)) > 0) { + total += len; + if (total > MAX_SIZE) { + LOGGER.warn("GZip decompression of {} exceeded threshold of {} bytes.", unzippedTar, MAX_SIZE); + throw new IOException("GZip file decompression exceeded maximum permitted size."); + } else if (total * 3 > available) { + LOGGER.warn("GZip decompression of {} exceeded safe available disk space of {} bytes.", unzippedTar, available - (total * 3)); + throw new IOException("GZip file decompression exceeded safe available disk space."); + } + output.write(buf, 0, len); + } + LOGGER.info("Decompressed gzipped file to {}", unzippedTar); + } catch (ZipException zipException) { + unzippedTar = imagePath; + LOGGER.warn("Failed to decompress gzip {}.", imagePath, zipException); + } + } catch (IOException ioException) { + LOGGER.warn("Failed to decompress image tar file to {}.", unzippedTar, ioException); + throw ioException; + } + LOGGER.info("Untarring {}", unzippedTar); + analyzer.untar(unzippedTar, outputDirectory); + } else { + LOGGER.info("Working with collection of layers at {}", imagePath); + } + + File manifestFile = null; + if (imagePath.isDirectory()) + for (File file : imagePath.listFiles()) + if (file.getName().startsWith("manifest-") && file.getName().endsWith(".json")) + manifestFile = file; + + Image image = null; + if (manifestFile == null) { + TarManifestJson manifest = analyzer.parseTarManifest(imagePath.length(), new File(outputDirectory, "manifest.json")); + image = analyzer.analyze( + // place output in this directory + outputDirectory, + // image id + manifest.getImageId(), + // image digest (unknown) + null, + // parse the manifest file + manifest, + // parse the configuration file + analyzer.parseConfiguration(new File(outputDirectory, manifest.getConfig())), + // locate each referenced layer + layerId -> new File(outputDirectory, layerId.getId() + "/layer.tar")); + } else { + Manifest manifest = analyzer.parseManifest(manifestFile); + image = analyzer.analyze( + // place output in this directory + outputDirectory, + // image id + manifest.getImageId(), + // image digest (unknown) + null, + // parse the manifest file + manifest, + // parse the configuration file + analyzer.parseConfiguration(new File(imagePath, "config-" + manifest.getImageId().getId() + ".json")), + // locate each referenced layer + layerId -> download(imagePath.getAbsolutePath() + "/" + layerId.getId() + ".tar.gz", new LayerDecompressor(outputDirectory, layerId))); + } + + System.out.println(image.toString()); + System.out.println(image.getOperatingSystem()); + } finally { + deleteQuietly(outputDirectory); + } + } + + @FunctionalInterface + public interface DownloadCallback { + T onDownload(File file) throws IOException; + } + + public T download(String file, DownloadCallback callback) throws IOException { + T value = callback.onDownload(new File(file)); + return value; + + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java b/src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java new file mode 100644 index 0000000..b9b5aa7 --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java @@ -0,0 +1,54 @@ +package com.rapid7.container.analyzer.docker.test; + +import com.rapid7.container.analyzer.docker.model.image.LayerId; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class LayerDecompressor implements com.rapid7.container.analyzer.docker.test.ImageAnalyzerTester.DownloadCallback { + + private static final Logger LOGGER = LoggerFactory.getLogger(LayerDecompressor.class); + private File outputDirectory; + private LayerId layerId; + private static final long MAX_SIZE = 2147483648L; + + public LayerDecompressor(File outputDirectory, LayerId layerId) { + this.outputDirectory = outputDirectory; + this.layerId = layerId; + } + + @Override + public File onDownload(File file) throws IOException { + + File layerTar = new File(outputDirectory, "layer-" + layerId.getId() + ".tar"); + long available = layerTar.getParentFile().getUsableSpace(); + LOGGER.info("Decompressing {} to {}.", file.getAbsolutePath(), layerTar.getAbsolutePath()); + LOGGER.info("Total space: {} - Free space: {} - Required space: {}", layerTar.getParentFile().getTotalSpace(), layerTar.getParentFile().getFreeSpace(), file.length()); + + try (FileOutputStream output = new FileOutputStream(layerTar)) { + try (GZIPInputStream input = new GZIPInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int len; + long total = 0; + while ((len = input.read(buf)) > 0) { + total += len; + if (total > MAX_SIZE) { + throw new IOException("GZip file decompression exceeded maximum permitted size."); + } else if (total * 3 > available) { + throw new IOException("GZip file decompression exceeded safe available disk space."); + } + output.write(buf, 0, len); + } + } + } catch (IOException ioException) { + LOGGER.warn("Failed to analyze layer {}.", layerId, ioException); + throw ioException; + } + + return layerTar; + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/test/RecogMatcherService.java b/src/test/java/com/rapid7/container/analyzer/docker/test/RecogMatcherService.java new file mode 100644 index 0000000..9d1c6c5 --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/test/RecogMatcherService.java @@ -0,0 +1,50 @@ +package com.rapid7.container.analyzer.docker.test; + +import com.rapid7.recog.RecogMatch; +import com.rapid7.recog.RecogMatchResult; +import com.rapid7.recog.RecogMatcher; +import com.rapid7.recog.RecogMatchers; +import com.rapid7.recog.RecogType; +import com.rapid7.recog.provider.RecogMatchersProvider; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +public class RecogMatcherService { + + private RecogMatchersProvider provider; + + public RecogMatcherService(File recogContentDirectory) { + provider = new RecogMatchersProvider(RecogType.BUILTIN, recogContentDirectory); + } + + public List fingerprint(String description) { + List matches = new ArrayList<>(); + matches.addAll(fingerprint(description, provider.getMatchers(RecogType.BUILTIN))); + + // sort the results by the matcher preference, then OS certainty + return matches.stream() + .sorted( + comparing((RecogMatchResult match) -> match.getPreference()) + .thenComparing((RecogMatchResult match) -> match.getMatches().containsKey("os.certainty") ? Float.valueOf(match.getMatches().get("os.certainty")) : 1) + .reversed() + ).collect(toList()); + } + + private Collection fingerprint(String description, Collection recogMatchers) { + + Collection matches = new ArrayList<>(); + + for (RecogMatchers matchers : recogMatchers) { + for (RecogMatch match : matchers.getMatches(description)) { + RecogMatcher matcher = match.getMatcher(); + matches.add(new RecogMatchResult(matchers.getKey(), matchers.getType(), matchers.getProtocol(), matchers.getPreference(), match.getMatcher().getDescription(), matcher.getPattern(), matcher.getExamples(), match.getParameters())); + } + } + + return matches; + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/util/InstantParserUnitTests.java b/src/test/java/com/rapid7/container/analyzer/docker/util/InstantParserUnitTests.java new file mode 100644 index 0000000..26ab88f --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/util/InstantParserUnitTests.java @@ -0,0 +1,127 @@ +package com.rapid7.container.analyzer.docker.util; + +import java.time.Instant; +import java.time.format.DateTimeParseException; +import org.junit.jupiter.api.Test; +import static com.rapid7.container.analyzer.docker.util.InstantParser.parse; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class InstantParserUnitTests { + + @Test + public void parseNegativeZoneOffset() { + assertThat(parse("2018-05-22T11:43:10.661876938-07:00"), equalTo(instant("2018-05-22T18:43:10.661876938Z"))); + } + + @Test + public void parseShortNegativeOffset() { + assertThat(parse("2018-05-22T11:43:10-07:00"), equalTo(instant("2018-05-22T18:43:10Z"))); + } + + @Test + public void parsePositiveOffset() { + assertThat(parse("2018-05-22T11:43:10.661876938+05:00"), equalTo(instant("2018-05-22T06:43:10.661876938Z"))); + } + + @Test + public void parseShortPositiveOffset() { + assertThat(parse("2018-05-22T11:43:10+05:00"), equalTo(instant("2018-05-22T06:43:10Z"))); + } + + @Test + public void parseUtc() { + assertThat(parse("2018-05-22T18:43:10.661876938"), equalTo(instant("2018-05-22T18:43:10.661876938Z"))); + } + + @Test + public void parseShortUtc() { + assertThat(parse("2018-05-22T18:43:10"), equalTo(instant("2018-05-22T18:43:10Z"))); + } + + @Test + public void parseZoneOffset() { + assertThat(parse("2018-05-22T18:43:10.661876938Z"), equalTo(instant("2018-05-22T18:43:10.661876938Z"))); + } + + @Test + public void parseTimeWithoutNanosecondValue() { + assertThat(parse("2018-05-22T18:43:10Z"), instanceOf(Instant.class)); + } + + @Test + public void parseTimeShortNanosecondValue() { + assertThat(parse("2018-05-22T11:43:10.2Z"), instanceOf(Instant.class)); + } + + @Test + public void parseTimeWithoutSecondValue() { + assertThat(parse("2018-05-22T11:43"), instanceOf(Instant.class)); + } + + @Test + public void parseTimeHourValueOnly() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11")); + } + + @Test + public void parseInvalidMinuteValue() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:4")); + } + + @Test + public void parseInvalidNegativeTimeOffset() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:43:10.661876938-18:01")); + } + + @Test + public void parseInvalidPositiveTimeOffset() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:43:10.661876938+19:00")); + } + + @Test + public void parseInvalidValueNegativeTimeOffset() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:43:10.661876938-10:71")); + } + + @Test + public void parseInvalidValuePositiveTimeOffset() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:43:10.661876938+04:90")); + } + + @Test + public void parseInvalidFormatZoneTimeOffset() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T11:43:10.661876938+-04:20")); + } + + @Test + public void parseOutOfOrderCharacters() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T:11-43:10.66")); + } + + @Test + public void parseUnknownCharacters() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22T!11:43:10.661876938+04:20")); + } + + @Test + public void parseDateOnly() { + assertThrows(DateTimeParseException.class, () -> parse("2018-05-22")); + } + + @Test + public void parseEmptyString() { + assertThrows(DateTimeParseException.class, () -> parse("")); + } + + @Test + public void parseNull() { + assertThrows(NullPointerException.class, () -> parse(null)); + } + + private Instant instant(String string) { + return Instant.parse(string); + } +} diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-dash.info b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-dash.info new file mode 100644 index 0000000..6a81dfc --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-dash.info @@ -0,0 +1,13 @@ +Package: dash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 204 +Maintainer: Gerrit Pape +Architecture: amd64 +Version: 0.5.8-2.4 +Depends: debianutils (>= 2.15), dpkg (>= 1.15.0) +Pre-Depends: libc6 (>= 2.14) +Description: POSIX-compliant shell +Homepage: http://gondor.apana.org.au/~herbert/dash/ \ No newline at end of file diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-multiple.info b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-multiple.info new file mode 100644 index 0000000..672f60e --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg-multiple.info @@ -0,0 +1,35 @@ +Package: gcc-6-base +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 209 +Maintainer: Debian GCC Maintainers +Architecture: amd64 +Multi-Arch: same +Source: gcc-6 +Version: 6.3.0-18+deb9u1 +Breaks: gcc-4.4-base (<< 4.4.7), gcc-4.7-base (<< 4.7.3), gcj-4.4-base (<< 4.4.6-9~), gcj-4.6-base (<< 4.6.1-4~), gnat-4.4-base (<< 4.4.6-3~), gnat-4.6 (<< 4.6.1-5~) +Description: GCC, the GNU Compiler Collection (base package) + This package contains files common to all languages and libraries + contained in the GNU Compiler Collection (GCC). +Homepage: http://gcc.gnu.org/ + +Package: dash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 204 +Maintainer: Gerrit Pape +Architecture: amd64 +Version: 0.5.8-2.4 +Depends: debianutils (>= 2.15), dpkg (>= 1.15.0) +Pre-Depends: libc6 (>= 2.14) +Description: POSIX-compliant shell + The Debian Almquist Shell (dash) is a POSIX-compliant shell derived + from ash. + . + Since it executes scripts faster than bash, and has fewer library + dependencies (making it more robust against software or hardware + failures), it is used as the default system shell on Debian systems. +Homepage: http://gondor.apana.org.au/~herbert/dash/ \ No newline at end of file diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg.info b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg.info new file mode 100644 index 0000000..a88273c --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/packages/dpkg.info @@ -0,0 +1,1891 @@ +Package: iputils-ping +Status: install ok installed +Priority: important +Section: net +Installed-Size: 111 +Maintainer: Noah Meyerhans +Architecture: amd64 +Source: iputils +Version: 3:20161105-1 +Provides: ping +Depends: libc6 (>= 2.14), libcap2 (>= 1:2.10), libidn11 (>= 1.13), libnettle6 +Recommends: libcap2-bin +Description: Tools to test the reachability of network hosts + The ping command sends ICMP ECHO_REQUEST packets to a host in order to + test if the host is reachable via the network. + . + This package includes a ping6 utility which supports IPv6 network + connections. + +Package: libpam-runtime +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 1016 +Maintainer: Steve Langasek +Architecture: all +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.6 +Replaces: libpam0g-dev, libpam0g-util +Depends: debconf (>= 0.5) | debconf-2.0, debconf (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6) +Conflicts: libpam0g-util +Conffiles: + /etc/pam.conf 87fc76f18e98ee7d3848f6b81b3391e5 + /etc/pam.d/other 31aa7f2181889ffb00b87df4126d1701 +Description: Runtime support for the PAM library + Contains configuration files and directories required for + authentication to work on Debian systems. This package is required + on almost all installations. +Homepage: http://www.linux-pam.org/ + +Package: libapt-pkg5.0 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 3056 +Maintainer: APT Development Team +Architecture: amd64 +Multi-Arch: same +Source: apt +Version: 1.4.8 +Provides: libapt-pkg (= 1.4.8) +Depends: libbz2-1.0, libc6 (>= 2.15), libgcc1 (>= 1:3.0), liblz4-1 (>= 0.0~r127), liblzma5 (>= 5.1.1alpha+20120614), libstdc++6 (>= 5.2), zlib1g (>= 1:1.2.2.3) +Recommends: apt (>= 1.4.8) +Breaks: appstream (<< 0.9.0-3~), apt (<< 1.1~exp14), libapt-inst1.5 (<< 0.9.9~) +Description: package management runtime library + This library provides the common functionality for searching and + managing packages as well as information about packages. + Higher-level package managers can depend upon this library. + . + This includes: + * retrieval of information about packages from multiple sources + * retrieval of packages and all dependent packages + needed to satisfy a request either through an internal + solver or by interfacing with an external one + * authenticating the sources and validating the retrieved data + * installation and removal of packages in the system + * providing different transports to retrieve data over cdrom, ftp, + http, rsh as well as an interface to add more transports like + https (apt-transport-https) and debtorrent (apt-transport-debtorrent). + +Package: libaudit1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 150 +Maintainer: Laurent Bigonville +Architecture: amd64 +Multi-Arch: same +Source: audit +Version: 1:2.6.7-2 +Depends: libaudit-common (>= 1:2.6.7-2), libc6 (>= 2.14), libcap-ng0 +Description: Dynamic library for security auditing + The audit-libs package contains the dynamic libraries needed for + applications to use the audit framework. It is used to monitor systems for + security related events. +Homepage: https://people.redhat.com/sgrubb/audit/ + +Package: libtinfo5 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 478 +Maintainer: Craig Small +Architecture: amd64 +Multi-Arch: same +Source: ncurses +Version: 6.0+20161126-1+deb9u2 +Replaces: libncurses5 (<< 5.9-3) +Depends: libc6 (>= 2.16) +Breaks: dialog (<< 1.2-20130523) +Description: shared low-level terminfo library for terminal handling + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the shared low-level terminfo library. +Homepage: http://invisible-island.net/ncurses/ + +Package: perl-base +Essential: yes +Status: install ok installed +Priority: required +Section: perl +Installed-Size: 7551 +Maintainer: Niko Tyni +Architecture: amd64 +Source: perl +Version: 5.24.1-3+deb9u4 +Replaces: libfile-path-perl (<< 2.12.01), libfile-temp-perl (<< 0.2304), libio-socket-ip-perl (<< 0.37), libscalar-list-utils-perl (<< 1:1.42.02), libsocket-perl (<< 2.020.03), libxsloader-perl (<< 0.22), perl (<< 5.10.1-12), perl-modules (<< 5.20.1-3) +Provides: libfile-path-perl, libfile-temp-perl, libio-socket-ip-perl, libscalar-list-utils-perl, libsocket-perl, libxsloader-perl, perlapi-5.24.1 +Pre-Depends: libc6 (>= 2.23), dpkg (>= 1.17.17) +Suggests: perl +Breaks: amanda-common (<< 1:3.3.9-2), autoconf2.13 (<< 2.13-45), backuppc (<< 3.3.1-2), dh-haskell (<< 0.3), libalien-wxwidgets-perl (<< 0.65+dfsg-2), libanyevent-perl (<< 7.070-2), libcommon-sense-perl (<< 3.72-2~), libexception-class-perl (<< 1.42), libfile-path-perl (<< 2.12.01), libfile-spec-perl (<< 3.6301), libfile-temp-perl (<< 0.2304), libgtk2-perl-doc (<< 2:1.2491-4), libio-socket-ip-perl (<< 0.37), libjcode-perl (<< 2.13-3), libmarc-charset-perl (<< 1.2), libsbuild-perl (<< 0.67.0-1), libscalar-list-utils-perl (<< 1:1.42.02), libsocket-perl (<< 2.020.03), libxsloader-perl (<< 0.22), mailagent (<< 1:3.1-81-2), pdl (<< 1:2.007-4), perl (<< 5.24.1~), perl-modules (<< 5.24.1~), texinfo (<< 6.1.0.dfsg.1-8) +Conflicts: defoma (<< 0.11.12), doc-base (<< 0.10.3), mono-gac (<< 2.10.8.1-3), safe-rm (<< 0.8), update-inetd (<< 4.41) +Description: minimal Perl system + Perl is a scripting language used in many system scripts and utilities. + . + This package provides a Perl interpreter and the small subset of the + standard run-time library required to perform basic tasks. For a full + Perl installation, install "perl" (and its dependencies, "perl-modules-5.24" + and "perl-doc"). +Homepage: http://dev.perl.org/perl5/ + +Package: libudev1 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 223 +Maintainer: Debian systemd Maintainers +Architecture: amd64 +Multi-Arch: same +Source: systemd +Version: 232-25+deb9u4 +Depends: libc6 (>= 2.16) +Description: libudev shared library + This library provides access to udev device information. +Homepage: https://www.freedesktop.org/wiki/Software/systemd + +Package: libnettle6 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 358 +Maintainer: Magnus Holmgren +Architecture: amd64 +Multi-Arch: same +Source: nettle (3.3-1) +Version: 3.3-1+b2 +Depends: libc6 (>= 2.14) +Description: low level cryptographic library (symmetric and one-way cryptos) + Nettle is a cryptographic library that is designed to fit easily in more or + less any context: In crypto toolkits for object-oriented languages (C++, + Python, Pike, ...), in applications like LSH or GNUPG, or even in kernel + space. + . + It tries to solve a problem of providing a common set of cryptographic + algorithms for higher-level applications by implementing a + context-independent set of cryptographic algorithms. In that light, Nettle + doesn't do any memory allocation or I/O, it simply provides the + cryptographic algorithms for the application to use in any environment and + in any way it needs. + . + This package contains the symmetric and one-way cryptographic + algorithms. To avoid having this package depend on libgmp, the + asymmetric cryptos reside in a separate library, libhogweed. +Homepage: http://www.lysator.liu.se/~nisse/nettle/ + +Package: libattr1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 42 +Maintainer: Anibal Monsalve Salazar +Architecture: amd64 +Multi-Arch: same +Source: attr (1:2.4.47-2) +Version: 1:2.4.47-2+b2 +Depends: libc6 (>= 2.4) +Conflicts: attr (<< 2.0.0) +Description: Extended attribute shared library + Contains the runtime environment required by programs that make use + of extended attributes. +Homepage: http://savannah.nongnu.org/projects/attr/ + +Package: libss2 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 95 +Maintainer: Theodore Y. Ts'o +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.43.4-2 +Replaces: e2fsprogs (<< 1.34-1) +Depends: libcomerr2, libc6 (>= 2.17) +Description: command-line interface parsing library + libss provides a simple command-line interface parser which will + accept input from the user, parse the command into an argv argument + vector, and then dispatch it to a handler function. + . + It was originally inspired by the Multics SubSystem library. +Homepage: http://e2fsprogs.sourceforge.net + +Package: liblzma5 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 339 +Maintainer: Jonathan Nieder +Architecture: amd64 +Multi-Arch: same +Source: xz-utils (5.2.2-1.2) +Version: 5.2.2-1.2+b1 +Depends: libc6 (>= 2.17) +Description: XZ-format compression library + XZ is the successor to the Lempel-Ziv/Markov-chain Algorithm + compression format, which provides memory-hungry but powerful + compression (often better than bzip2) and fast, easy decompression. + . + The native format of liblzma is XZ; it also supports raw (headerless) + streams and the older LZMA format used by lzma. (For 7-Zip's related + format, use the p7zip package instead.) +Homepage: http://tukaani.org/xz/ + +Package: libpam-modules-bin +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 220 +Maintainer: Steve Langasek +Architecture: amd64 +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.6 +Replaces: libpam-modules (<< 1.1.3-8) +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libselinux1 (>= 1.32) +Description: Pluggable Authentication Modules for PAM - helper binaries + This package contains helper binaries used by the standard set of PAM + modules in the libpam-modules package. +Homepage: http://www.linux-pam.org/ + +Package: grep +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 1131 +Maintainer: Anibal Monsalve Salazar +Architecture: amd64 +Multi-Arch: foreign +Version: 2.27-2 +Provides: rgrep +Depends: dpkg (>= 1.15.4) | install-info +Pre-Depends: libc6 (>= 2.14), libpcre3 +Suggests: libpcre3 (>= 7.7) +Conflicts: rgrep +Description: GNU grep, egrep and fgrep + 'grep' is a utility to search for text in files; it can be used from the + command line or in scripts. Even if you don't want to use it, other packages + on your system probably will. + . + The GNU family of grep utilities may be the "fastest grep in the west". + GNU grep is based on a fast lazy-state deterministic matcher (about + twice as fast as stock Unix egrep) hybridized with a Boyer-Moore-Gosper + search for a fixed string that eliminates impossible text from being + considered by the full regexp matcher without necessarily having to + look at every character. The result is typically many times faster + than Unix grep or egrep. (Regular expressions containing backreferencing + will run more slowly, however.) +Homepage: http://www.gnu.org/software/grep/ + +Package: base-passwd +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 229 +Maintainer: Colin Watson +Architecture: amd64 +Multi-Arch: foreign +Version: 3.5.43 +Replaces: base +Depends: libc6 (>= 2.8), libdebconfclient0 (>= 0.145) +Recommends: debconf (>= 0.5) | debconf-2.0 +Description: Debian base system master password and group files + These are the canonical master copies of the user database files + (/etc/passwd and /etc/group), containing the Debian-allocated user and + group IDs. The update-passwd tool is provided to keep the system databases + synchronized with these master files. + +Package: e2fslibs +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 449 +Maintainer: Theodore Y. Ts'o +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.43.4-2 +Replaces: e2fsprogs (<< 1.34-1) +Provides: libe2p2, libext2fs2 +Depends: libc6 (>= 2.17) +Description: ext2/ext3/ext4 file system libraries + The ext2, ext3 and ext4 file systems are successors of the original ext + ("extended") file system. They are the main file system types used for + hard disks on Debian and other Linux systems. + . + This package provides the ext2fs and e2p libraries, for userspace software + that directly accesses extended file systems. Programs that use libext2fs + include e2fsck, mke2fs, and tune2fs. Programs that use libe2p include + dumpe2fs, chattr, and lsattr. +Homepage: http://e2fsprogs.sourceforge.net + +Package: liblz4-1 +Status: install ok installed +Priority: extra +Section: libs +Installed-Size: 93 +Maintainer: Nobuhiro Iwamatsu +Architecture: amd64 +Multi-Arch: same +Source: lz4 (0.0~r131-2) +Version: 0.0~r131-2+b1 +Depends: libc6 (>= 2.14) +Description: Fast LZ compression algorithm library - runtime + LZ4 is a very fast lossless compression algorithm, providing compression speed + at 400 MB/s per core, scalable with multi-cores CPU. It also features an + extremely fast decoder, with speed in multiple GB/s per core, typically + reaching RAM speed limits on multi-core systems. + . + This package includes the shared library. +Homepage: https://github.com/Cyan4973/lz4 + +Package: debianutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 213 +Maintainer: Clint Adams +Architecture: amd64 +Multi-Arch: foreign +Version: 4.8.1.1 +Replaces: manpages-pl (<< 1:0.5) +Depends: sensible-utils +Pre-Depends: libc6 (>= 2.15) +Description: Miscellaneous utilities specific to Debian + This package provides a number of small utilities which are used + primarily by the installation scripts of Debian packages, although + you may use them directly. + . + The specific utilities included are: + add-shell installkernel ischroot remove-shell run-parts savelog + tempfile which + +Package: libgcrypt20 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1266 +Maintainer: Debian GnuTLS Maintainers +Architecture: amd64 +Multi-Arch: same +Version: 1.7.6-2+deb9u3 +Depends: libc6 (>= 2.15), libgpg-error0 (>= 1.14) +Suggests: rng-tools +Description: LGPL Crypto library - runtime library + libgcrypt contains cryptographic functions. Many important free + ciphers, hash algorithms and public key signing algorithms have been + implemented: + . + Arcfour, Blowfish, CAST5, DES, AES, Twofish, Serpent, rfc2268 (rc2), SEED, + Poly1305, Camellia, ChaCha20, IDEA, Salsa, CRC, MD2, MD4, MD5, RIPE-MD160, + SHA-1, SHA-256, SHA-512, SHA3-224, SHA3-256, SHA3-384, SHA3-512, SHAKE128, + SHAKE256 Tiger, Whirlpool, DSA, DSA2, ElGamal, RSA, ECC (Curve25519, + sec256k1, GOST R 34.10-2001 and GOST R 34.10-2012, etc.) +Homepage: http://directory.fsf.org/project/libgcrypt/ + +Package: libncursesw5 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 347 +Maintainer: Craig Small +Architecture: amd64 +Multi-Arch: same +Source: ncurses +Version: 6.0+20161126-1+deb9u2 +Depends: libtinfo5 (= 6.0+20161126-1+deb9u2), libc6 (>= 2.14) +Recommends: libgpm2 +Description: shared libraries for terminal handling (wide character support) + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the shared libraries necessary to run programs + compiled with ncursesw, which includes support for wide characters. +Homepage: http://invisible-island.net/ncurses/ + +Package: bash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 5798 +Maintainer: Matthias Klose +Architecture: amd64 +Multi-Arch: foreign +Version: 4.4-5 +Replaces: bash-completion (<< 20060301-0), bash-doc (<= 2.05-1) +Depends: base-files (>= 2.1.12), debianutils (>= 2.15) +Pre-Depends: dash (>= 0.5.5.1-2.2), libc6 (>= 2.15), libtinfo5 (>= 6) +Recommends: bash-completion (>= 20060301-0) +Suggests: bash-doc +Conflicts: bash-completion (<< 20060301-0) +Conffiles: + /etc/bash.bashrc 87b895cef45b8090d628a1d9a0f4bfb8 + /etc/skel/.bash_logout 22bfb8c1dd94b5f3813a2b25da67463f + /etc/skel/.bashrc ee35a240758f374832e809ae0ea4883a + /etc/skel/.profile ecb6d3479ac3823f1da7f314d871989b +Description: GNU Bourne Again SHell + Bash is an sh-compatible command language interpreter that executes + commands read from the standard input or from a file. Bash also + incorporates useful features from the Korn and C shells (ksh and csh). + . + Bash is ultimately intended to be a conformant implementation of the + IEEE POSIX Shell and Tools specification (IEEE Working Group 1003.2). + . + The Programmable Completion Code, by Ian Macdonald, is now found in + the bash-completion package. +Homepage: http://tiswww.case.edu/php/chet/bash/bashtop.html + +Package: libuuid1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 107 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.29.2-1+deb9u1 +Replaces: e2fsprogs (<< 1.34-1) +Depends: passwd, libc6 (>= 2.4) +Recommends: uuid-runtime +Description: Universally Unique ID library + The libuuid library generates and parses 128-bit Universally Unique + IDs (UUIDs). A UUID is an identifier that is unique within the space + of all such identifiers across both space and time. It can be used for + multiple purposes, from tagging objects with an extremely short lifetime + to reliably identifying very persistent objects across a network. + . + See RFC 4122 for more information. + +Package: libdb5.3 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1814 +Maintainer: Debian Berkeley DB Group +Architecture: amd64 +Multi-Arch: same +Source: db5.3 +Version: 5.3.28-12+deb9u1 +Depends: libc6 (>= 2.17) +Description: Berkeley v5.3 Database Libraries [runtime] + This is the runtime package for programs that use the v5.3 Berkeley + database library. +Homepage: http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/overview/index.html + +Package: debconf +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 558 +Maintainer: Debconf Developers +Architecture: all +Multi-Arch: foreign +Version: 1.5.61 +Replaces: debconf-tiny +Provides: debconf-2.0 +Pre-Depends: perl-base (>= 5.20.1-3~) +Recommends: apt-utils (>= 0.5.1), debconf-i18n +Suggests: debconf-doc, debconf-utils, whiptail | dialog, libterm-readline-gnu-perl, libgtk2-perl (>= 1:1.130), libnet-ldap-perl, perl, libqtgui4-perl, libqtcore4-perl +Conflicts: apt (<< 0.3.12.1), cdebconf (<< 0.96), debconf-tiny, debconf-utils (<< 1.3.22), dialog (<< 0.9b-20020814-1), menu (<= 2.1.3-1), whiptail (<< 0.51.4-11), whiptail-utf8 (<= 0.50.17-13) +Conffiles: + /etc/apt/apt.conf.d/70debconf 7e9d09d5801a42b4926b736b8eeabb73 + /etc/debconf.conf 8c0619be413824f1fc7698cee0f23811 +Description: Debian configuration management system + Debconf is a configuration management system for debian packages. Packages + use Debconf to ask questions when they are installed. + +Package: zlib1g +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 156 +Maintainer: Mark Brown +Architecture: amd64 +Multi-Arch: same +Source: zlib +Version: 1:1.2.8.dfsg-5 +Provides: libz1 +Depends: libc6 (>= 2.14) +Breaks: libxml2 (<< 2.7.6.dfsg-2), texlive-binaries (<< 2009-12) +Conflicts: zlib1 (<= 1:1.0.4-7) +Description: compression library - runtime + zlib is a library implementing the deflate compression method found + in gzip and PKZIP. This package includes the shared library. +Homepage: http://zlib.net/ + +Package: hostname +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 47 +Maintainer: Debian Hostname Team +Architecture: amd64 +Source: hostname (3.18) +Version: 3.18+b1 +Replaces: nis (<< 3.17-30) +Pre-Depends: libc6 (>= 2.4) +Breaks: nis (<< 3.17-30) +Description: utility to set/show the host name or domain name + This package provides commands which can be used to display the system's + DNS name, and to display or set its hostname or NIS domain name. + +Package: multiarch-support +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 220 +Maintainer: GNU Libc Maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: glibc +Version: 2.24-11+deb9u3 +Depends: libc6 (>= 2.3.6-2) +Description: Transitional package to ensure multiarch compatibility + This is a transitional package used to ensure multiarch support is present + in ld.so before unpacking libraries to the multiarch directories. It can + be removed once nothing on the system depends on it. +Homepage: http://www.gnu.org/software/libc/libc.html + +Package: tzdata +Status: install ok installed +Priority: required +Section: localization +Installed-Size: 3013 +Maintainer: GNU Libc Maintainers +Architecture: all +Multi-Arch: foreign +Version: 2018e-0+deb9u1 +Replaces: libc0.1, libc0.3, libc6, libc6.1 +Provides: tzdata-stretch +Depends: debconf (>= 0.5) | debconf-2.0 +Description: time zone and daylight-saving time data + This package contains data required for the implementation of + standard local time for many representative locations around the + globe. It is updated periodically to reflect changes made by + political bodies to time zone boundaries, UTC offsets, and + daylight-saving rules. +Homepage: http://www.iana.org/time-zones + +Package: mawk +Status: install ok installed +Priority: required +Section: interpreters +Installed-Size: 183 +Maintainer: Steve Langasek +Architecture: amd64 +Multi-Arch: foreign +Source: mawk (1.3.3-17) +Version: 1.3.3-17+b3 +Provides: awk +Pre-Depends: libc6 (>= 2.14) +Description: a pattern scanning and text processing language + Mawk is an interpreter for the AWK Programming Language. The AWK + language is useful for manipulation of data files, text retrieval and + processing, and for prototyping and experimenting with algorithms. Mawk + is a new awk meaning it implements the AWK language as defined in Aho, + Kernighan and Weinberger, The AWK Programming Language, Addison-Wesley + Publishing, 1988. (Hereafter referred to as the AWK book.) Mawk conforms + to the POSIX 1003.2 (draft 11.3) definition of the AWK language + which contains a few features not described in the AWK book, and mawk + provides a small number of extensions. + . + Mawk is smaller and much faster than gawk. It has some compile-time + limits such as NF = 32767 and sprintf buffer = 1020. + +Package: gzip +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 231 +Maintainer: Bdale Garbee +Architecture: amd64 +Source: gzip (1.6-5) +Version: 1.6-5+b1 +Depends: dpkg (>= 1.15.4) | install-info +Pre-Depends: libc6 (>= 2.17) +Suggests: less +Description: GNU compression utilities + This package provides the standard GNU file compression utilities, which + are also the default compression tools for Debian. They typically operate + on files with names ending in '.gz', but can also decompress files ending + in '.Z' created with 'compress'. + +Package: libelf1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 934 +Maintainer: Kurt Roeckx +Architecture: amd64 +Multi-Arch: same +Source: elfutils +Version: 0.168-1 +Depends: libc6 (>= 2.14), zlib1g (>= 1:1.1.4) +Description: library to read and write ELF files + The libelf1 package provides a shared library which allows reading and + writing ELF files on a high level. Third party programs depend on + this package to read internals of ELF files. The programs of the + elfutils package use it also to generate new ELF files. + . + This library is part of elfutils. +Homepage: https://sourceware.org/elfutils/ + +Package: gpgv +Status: install ok installed +Priority: important +Section: utils +Installed-Size: 721 +Maintainer: Debian GnuPG Maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: gnupg2 +Version: 2.1.18-8~deb9u2 +Replaces: gnupg2 (<< 2.0.21-2), gpgv2 (<< 2.1.11-7+exp1) +Depends: libbz2-1.0, libc6 (>= 2.14), libgcrypt20 (>= 1.7.0), libgpg-error0 (>= 1.14), zlib1g (>= 1:1.1.4) +Suggests: gnupg +Breaks: gnupg2 (<< 2.0.21-2), gpgv2 (<< 2.1.11-7+exp1), python-debian (<< 0.1.29) +Description: GNU privacy guard - signature verification tool + GnuPG is GNU's tool for secure communication and data storage. + . + gpgv is actually a stripped-down version of gpg which is only able + to check signatures. It is somewhat smaller than the fully-blown gpg + and uses a different (and simpler) way to check that the public keys + used to make the signature are valid. There are no configuration + files and only a few options are implemented. +Homepage: https://www.gnupg.org/ + +Package: libcap2 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 47 +Maintainer: Christian Kastner +Architecture: amd64 +Multi-Arch: same +Version: 1:2.25-1 +Depends: libc6 (>= 2.14) +Description: POSIX 1003.1e capabilities (library) + Libcap implements the user-space interfaces to the POSIX 1003.1e capabilities + available in Linux kernels. These capabilities are a partitioning of the all + powerful root privilege into a set of distinct privileges. + . + This package contains the shared library. +Homepage: http://sites.google.com/site/fullycapable/ + +Package: bsdutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 238 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: util-linux (2.29.2-1+deb9u1) +Version: 1:2.29.2-1+deb9u1 +Replaces: bash-completion (<< 1:2.1-4.1~) +Pre-Depends: libc6 (>= 2.16), libsystemd0 +Recommends: bsdmainutils +Breaks: bash-completion (<< 1:2.1-4.1~) +Description: basic utilities from 4.4BSD-Lite + This package contains the bare minimum of BSD utilities needed for a + Debian system: logger, renice, script, scriptreplay, and wall. The + remaining standard BSD utilities are provided by bsdmainutils. + +Package: dash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 204 +Maintainer: Gerrit Pape +Architecture: amd64 +Version: 0.5.8-2.4 +Depends: debianutils (>= 2.15), dpkg (>= 1.15.0) +Pre-Depends: libc6 (>= 2.14) +Description: POSIX-compliant shell + The Debian Almquist Shell (dash) is a POSIX-compliant shell derived + from ash. + . + Since it executes scripts faster than bash, and has fewer library + dependencies (making it more robust against software or hardware + failures), it is used as the default system shell on Debian systems. +Homepage: http://gondor.apana.org.au/~herbert/dash/ + +Package: gcc-6-base +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 209 +Maintainer: Debian GCC Maintainers +Architecture: amd64 +Multi-Arch: same +Source: gcc-6 +Version: 6.3.0-18+deb9u1 +Breaks: gcc-4.4-base (<< 4.4.7), gcc-4.7-base (<< 4.7.3), gcj-4.4-base (<< 4.4.6-9~), gcj-4.6-base (<< 4.6.1-4~), gnat-4.4-base (<< 4.4.6-3~), gnat-4.6 (<< 4.6.1-5~) +Description: GCC, the GNU Compiler Collection (base package) + This package contains files common to all languages and libraries + contained in the GNU Compiler Collection (GCC). +Homepage: http://gcc.gnu.org/ + +Package: mount +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 444 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: util-linux +Version: 2.29.2-1+deb9u1 +Replaces: bash-completion (<< 1:2.1-4.3~) +Pre-Depends: libblkid1 (>= 2.17.2), libc6 (>= 2.17), libmount1 (>= 2.25), libselinux1 (>= 2.6-3~), libsmartcols1 (>= 2.28~rc1), libudev1 (>= 183) +Suggests: nfs-common (>= 1:1.1.0-13) +Breaks: bash-completion (<< 1:2.1-4.3~) +Description: tools for mounting and manipulating filesystems + This package provides the mount(8), umount(8), swapon(8), + swapoff(8), and losetup(8) commands. + +Package: libsystemd0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 653 +Maintainer: Debian systemd Maintainers +Architecture: amd64 +Multi-Arch: same +Source: systemd +Version: 232-25+deb9u4 +Pre-Depends: libc6 (>= 2.17), libgcrypt20 (>= 1.7.0), liblz4-1 (>= 0.0~r113), liblzma5 (>= 5.1.1alpha+20120614), libselinux1 (>= 2.1.9) +Description: systemd utility library + The libsystemd0 library provides interfaces to various systemd components. +Homepage: https://www.freedesktop.org/wiki/Software/systemd + +Package: libc6 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 10684 +Maintainer: GNU Libc Maintainers +Architecture: amd64 +Multi-Arch: same +Source: glibc +Version: 2.24-11+deb9u3 +Replaces: libc6-amd64 +Depends: libgcc1 +Suggests: glibc-doc, debconf | debconf-2.0, libc-l10n, locales +Breaks: hurd (<< 1:0.5.git20140203-1), libtirpc1 (<< 0.2.3), locales (<< 2.24), locales-all (<< 2.24), lsb-core (<= 3.2-27), nscd (<< 2.24) +Conffiles: + /etc/ld.so.conf.d/x86_64-linux-gnu.conf 593ad12389ab2b6f952e7ede67b8fbbf +Description: GNU C Library: Shared libraries + Contains the standard libraries that are used by nearly all programs on + the system. This package includes shared versions of the standard C library + and the standard math library, as well as many others. +Homepage: http://www.gnu.org/software/libc/libc.html + +Package: libfdisk1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 469 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.29.2-1+deb9u1 +Depends: libblkid1 (>= 2.24.2), libc6 (>= 2.17), libuuid1 (>= 2.16) +Description: fdisk partitioning library + The libfdisk library is used for manipulating partition tables. It is + the core of the fdisk, cfdisk, and sfdisk tools. + +Package: libpcre3 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 668 +Maintainer: Matthew Vernon +Architecture: amd64 +Multi-Arch: same +Source: pcre3 +Version: 2:8.39-3 +Depends: libc6 (>= 2.14) +Pre-Depends: multiarch-support +Breaks: approx (<< 4.4-1~), cduce (<< 0.5.3-2~), cmigrep (<< 1.5-7~), galax (<< 1.1-7~), libpcre-ocaml (<< 6.0.1~), liquidsoap (<< 0.9.2-3~), ocsigen (<< 1.3.3-1~) +Conflicts: libpcre3-dev (<= 4.3-3) +Description: Old Perl 5 Compatible Regular Expression Library - runtime files + This is a library of functions to support regular expressions whose syntax + and semantics are as close as possible to those of the Perl 5 language. + . + New packages should use the newer pcre2 packages, and existing + packages should migrate to pcre2. + . + This package contains the runtime libraries. + +Package: coreutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 15103 +Maintainer: Michael Stone +Architecture: amd64 +Multi-Arch: foreign +Version: 8.26-3 +Replaces: mktemp, realpath, timeout +Pre-Depends: libacl1 (>= 2.2.51-8), libattr1 (>= 1:2.4.46-8), libc6 (>= 2.17), libselinux1 (>= 2.1.13) +Conflicts: timeout +Description: GNU core utilities + This package contains the basic file, shell and text manipulation + utilities which are expected to exist on every operating system. + . + Specifically, this package includes: + arch base64 basename cat chcon chgrp chmod chown chroot cksum comm cp + csplit cut date dd df dir dircolors dirname du echo env expand expr + factor false flock fmt fold groups head hostid id install join link ln + logname ls md5sum mkdir mkfifo mknod mktemp mv nice nl nohup nproc numfmt + od paste pathchk pinky pr printenv printf ptx pwd readlink realpath rm + rmdir runcon sha*sum seq shred sleep sort split stat stty sum sync tac + tail tee test timeout touch tr true truncate tsort tty uname unexpand + uniq unlink users vdir wc who whoami yes +Homepage: http://gnu.org/software/coreutils + +Package: iproute2 +Status: install ok installed +Priority: important +Section: net +Installed-Size: 1757 +Maintainer: Debian iproute2 Maintainers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.9.0-1+deb9u1 +Replaces: iproute +Provides: arpd +Depends: libc6 (>= 2.14), libdb5.3, libelf1 (>= 0.131), libmnl0 (>= 1.0.3-4~), libselinux1 (>= 2.0.15) +Recommends: libatm1 (>= 2.4.1-17~), libxtables12 (>= 1.6.0+snapshot20161117) +Suggests: iproute2-doc +Conflicts: arpd, iproute (<< 20130000-1) +Conffiles: + /etc/iproute2/bpf_pinning fd070252e6e9996bd04d9d59e4ce21eb + /etc/iproute2/ematch_map b91e7f9b26918449bade9573f8871d61 + /etc/iproute2/group 3aea2c0e0dd75e13a5f8f48f2936915f + /etc/iproute2/nl_protos c0fc5315e2dd3c6b50f19da3678bce80 + /etc/iproute2/rt_dsfield 4c80d267a84d350d89d88774efe48a0f + /etc/iproute2/rt_protos a4f97323f29caf9faf49596aea11d91a + /etc/iproute2/rt_realms 7137bdf40e8d58c87ac7e3bba503767f + /etc/iproute2/rt_scopes 6298b8df09e9bda23ea7da49021ca457 + /etc/iproute2/rt_tables a1313318d6778fe6b8c680248ef5a463 + /etc/iproute2/rt_tables.d/README 16d42be21c00b9edfae1fd4c39c15afc +Description: networking and traffic control tools + The iproute2 suite is a collection of utilities for networking and + traffic control. + . + These tools communicate with the Linux kernel via the (rt)netlink + interface, providing advanced features not available through the + legacy net-tools commands 'ifconfig' and 'route'. +Homepage: https://wiki.linuxfoundation.org/networking/iproute2 + +Package: e2fsprogs +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 4022 +Maintainer: Theodore Y. Ts'o +Architecture: amd64 +Multi-Arch: foreign +Version: 1.43.4-2 +Replaces: hurd (<= 20040301-1), libblkid1 (<< 1.38+1.39-WIP-2005.12.10-2), libuuid1 (<< 1.38+1.39-WIP-2005.12.10-2) +Pre-Depends: e2fslibs (= 1.43.4-2), libblkid1 (>= 2.17.2), libc6 (>= 2.14), libcomerr2 (>= 1.42~WIP-2011-10-05-1), libss2 (>= 1.34-1), libuuid1 (>= 2.16), util-linux (>= 2.15~rc1-1) +Suggests: gpart, parted, fuse2fs, e2fsck-static +Conflicts: dump (<< 0.4b4-4), initscripts (<< 2.85-4), quota (<< 1.55-8.1), sysvinit (<< 2.85-4) +Conffiles: + /etc/mke2fs.conf 22503ecadd27f788ab3e612aded4f5b0 +Description: ext2/ext3/ext4 file system utilities + The ext2, ext3 and ext4 file systems are successors of the original ext + ("extended") file system. They are the main file system types used for + hard disks on Debian and other Linux systems. + . + This package contains programs for creating, checking, and maintaining + ext2/3/4-based file systems. It also includes the "badblocks" program, + which can be used to scan for bad blocks on a disk or other storage device. +Homepage: http://e2fsprogs.sourceforge.net + +Package: tar +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 2770 +Maintainer: Bdale Garbee +Architecture: amd64 +Multi-Arch: foreign +Version: 1.29b-1.1 +Replaces: cpio (<< 2.4.2-39) +Pre-Depends: libacl1 (>= 2.2.51-8), libc6 (>= 2.17), libselinux1 (>= 1.32) +Suggests: bzip2, ncompress, xz-utils, tar-scripts +Breaks: dpkg-dev (<< 1.14.26) +Conflicts: cpio (<= 2.4.2-38) +Conffiles: + /etc/rmt 3c58b7cd13da1085eff0acc6a00f43c7 +Description: GNU version of the tar archiving utility + Tar is a program for packaging a set of files as a single archive in tar + format. The function it performs is conceptually similar to cpio, and to + things like PKZIP in the DOS world. It is heavily used by the Debian package + management system, and is useful for performing system backups and exchanging + sets of files with others. + +Package: libbz2-1.0 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 96 +Maintainer: Anibal Monsalve Salazar +Architecture: amd64 +Multi-Arch: same +Source: bzip2 +Version: 1.0.6-8.1 +Depends: libc6 (>= 2.4) +Description: high-quality block-sorting file compressor library - runtime + This package contains libbzip2 which is used by the bzip2 compressor. + . + bzip2 is a freely available, patent free, high-quality data compressor. + It typically compresses files to within 10% to 15% of the best available + techniques, whilst being around twice as fast at compression and six + times faster at decompression. + . + bzip2 compresses files using the Burrows-Wheeler block-sorting text + compression algorithm, and Huffman coding. Compression is generally + considerably better than that achieved by more conventional + LZ77/LZ78-based compressors, and approaches the performance of the PPM + family of statistical compressors. + . + The archive file format of bzip2 (.bz2) is incompatible with that of its + predecessor, bzip (.bz). +Homepage: http://www.bzip.org/ + +Package: libblkid1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 367 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.29.2-1+deb9u1 +Depends: libc6 (>= 2.17), libuuid1 (>= 2.16) +Description: block device ID library + The blkid library allows system programs such as fsck and mount to + quickly and easily find block devices by filesystem UUID or label. + This allows system administrators to avoid specifying filesystems by + hard-coded device names and use a logical naming system instead. + +Package: lsb-base +Status: install ok installed +Priority: required +Section: misc +Installed-Size: 49 +Maintainer: Debian LSB Team +Architecture: all +Multi-Arch: foreign +Source: lsb +Version: 9.20161125 +Description: Linux Standard Base init script functionality + The Linux Standard Base (http://www.linuxbase.org/) is a standard + core system that third-party applications written for Linux can + depend upon. + . + This package only includes the init-functions shell library, which + may be used by other packages' initialization scripts for console + logging and other purposes. +Homepage: http://www.linuxfoundation.org/collaborate/workgroups/lsb + +Package: libgpg-error0 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 572 +Maintainer: Debian GnuPG Maintainers +Architecture: amd64 +Multi-Arch: same +Source: libgpg-error +Version: 1.26-2 +Depends: libc6 (>= 2.15) +Description: library for common error values and messages in GnuPG components + Library that defines common error values for all GnuPG + components. Among these are GPG, GPGSM, GPGME, GPG-Agent, libgcrypt, + pinentry, SmartCard Daemon and possibly more in the future. +Homepage: https://www.gnupg.org/related_software/libgpg-error/ + +Package: base-files +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 333 +Maintainer: Santiago Vila +Architecture: amd64 +Multi-Arch: foreign +Version: 9.9+deb9u5 +Replaces: base, dpkg (<= 1.15.0), miscutils +Provides: base +Pre-Depends: awk +Breaks: initscripts (<< 2.88dsf-13.3), sendfile (<< 2.1b.20080616-5.2~) +Conffiles: + /etc/debian_version 6aaf9da96067c818ca7a682984fc1579 + /etc/dpkg/origins/debian 731423fa8ba067262f8ef37882d1e742 + /etc/host.conf 4eb63731c9f5e30903ac4fc07a7fe3d6 + /etc/issue 62dc6d46260360d4299e3a931afbdce5 + /etc/issue.net 6589b08348fd877fd34398489f62caf0 + /etc/update-motd.d/10-uname 9e1b832b7b06f566156e7c9e0548247b +Description: Debian base system miscellaneous files + This package contains the basic filesystem hierarchy of a Debian system, and + several important miscellaneous files, such as /etc/debian_version, + /etc/host.conf, /etc/issue, /etc/motd, /etc/profile, and others, + and the text of several common licenses in use on Debian systems. + +Package: sensible-utils +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 62 +Maintainer: Anibal Monsalve Salazar +Architecture: all +Multi-Arch: foreign +Version: 0.0.9+deb9u1 +Replaces: debianutils (<= 2.32.3), manpages-pl (<= 20060617-3~) +Description: Utilities for sensible alternative selection + This package provides a number of small utilities which are used + by programs to sensibly select and spawn an appropriate browser, + editor, or pager. + . + The specific utilities included are: sensible-browser sensible-editor + sensible-pager + +Package: passwd +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 2478 +Maintainer: Shadow package maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: shadow +Version: 1:4.4-4.1 +Replaces: manpages-tr (<< 1.0.5), manpages-zh (<< 1.5.1-1) +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libselinux1 (>= 1.32), libsemanage1 (>= 2.0.3), libpam-modules +Conffiles: + /etc/cron.daily/passwd db990990933b6f56322725223f13c2bc + /etc/default/useradd cc9f9a7713ab62a32cd38363d958f396 + /etc/pam.d/chfn 4d466e00a348ba426130664d795e8afa + /etc/pam.d/chpasswd 9900720564cb4ee98b7da29e2d183cb2 + /etc/pam.d/chsh a6e9b589e90009334ffd030d819290a6 + /etc/pam.d/newusers 1454e29bfa9f2a10836563e76936cea5 + /etc/pam.d/passwd eaf2ad85b5ccd06cceb19a3e75f40c63 +Description: change and administer password and group data + This package includes passwd, chsh, chfn, and many other programs to + maintain password and group data. + . + Shadow passwords are supported. See /usr/share/doc/passwd/README.Debian +Homepage: https://github.com/shadow-maint/shadow + +Package: init-system-helpers +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 131 +Maintainer: Debian systemd Maintainers +Architecture: all +Multi-Arch: foreign +Version: 1.48 +Replaces: sysv-rc (<< 2.88dsf-59.3~), sysvinit-utils (<< 2.88dsf-59.3) +Depends: perl-base (>= 5.20.1-3) +Breaks: systemd (<< 44-12), sysvinit-utils (<< 2.88dsf-59.3~) +Conflicts: file-rc (<< 0.8.17~), openrc (<= 0.18.3-1) +Description: helper tools for all init systems + This package contains helper tools that are necessary for switching between + the various init systems that Debian contains (e. g. sysvinit or + systemd). An example is deb-systemd-helper, a script that enables systemd unit + files without depending on a running systemd. + . + It also includes the "service", "invoke-rc.d", and "update-rc.d" scripts which + provide an abstraction for enabling, disabling, starting, and stopping + services for all supported Debian init systems as specified by the policy. + . + While this package is maintained by pkg-systemd-maintainers, it is NOT + specific to systemd at all. Maintainers of other init systems are welcome to + include their helpers in this package. + +Package: ncurses-base +Essential: yes +Status: install ok installed +Priority: required +Section: misc +Installed-Size: 340 +Maintainer: Craig Small +Architecture: all +Multi-Arch: foreign +Source: ncurses +Version: 6.0+20161126-1+deb9u2 +Provides: ncurses-runtime +Breaks: ncurses-term (<< 5.7+20100313-3) +Conffiles: + /etc/terminfo/README 45b6df19fb5e21f55717482fa7a30171 +Description: basic terminal type definitions + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains terminfo data files to support the most common types of + terminal, including ansi, dumb, linux, rxvt, screen, sun, vt100, vt102, vt220, + vt52, and xterm. +Homepage: http://invisible-island.net/ncurses/ + +Package: libc-bin +Essential: yes +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 3365 +Maintainer: GNU Libc Maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: glibc +Version: 2.24-11+deb9u3 +Depends: libc6 (>> 2.24), libc6 (<< 2.25) +Recommends: manpages +Conffiles: + /etc/bindresvport.blacklist 4c09213317e4e3dd3c71d74404e503c5 + /etc/default/nss d6d5d6f621fb3ead2548076ce81e309c + /etc/gai.conf 28fa76ff5a9e0566eaa1e11f1ce51f09 + /etc/ld.so.conf 4317c6de8564b68d628c21efa96b37e4 + /etc/ld.so.conf.d/libc.conf d4d833fd095fb7b90e1bb4a547f16de6 +Description: GNU C Library: Binaries + This package contains utility programs related to the GNU C Library. + . + * catchsegv: catch segmentation faults in programs + * getconf: query system configuration variables + * getent: get entries from administrative databases + * iconv, iconvconfig: convert between character encodings + * ldd, ldconfig: print/configure shared library dependencies + * locale, localedef: show/generate locale definitions + * tzselect, zdump, zic: select/dump/compile time zones +Homepage: http://www.gnu.org/software/libc/libc.html + +Package: libsemanage1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 291 +Maintainer: Debian SELinux maintainers +Architecture: amd64 +Multi-Arch: same +Source: libsemanage +Version: 2.6-2 +Depends: libsemanage-common (= 2.6-2), libaudit1 (>= 1:2.2.1), libbz2-1.0, libc6 (>= 2.14), libselinux1 (>= 2.6), libsepol1 (>= 2.6), libustr-1.0-1 (>= 1.0.4) +Breaks: policycoreutils (<< 2.4), selinux-policy-default (<< 2:2.20140421-10~), selinux-policy-mls (<< 2:2.20140421-10~) +Description: SELinux policy management library + This package provides the shared libraries for SELinux policy management. + It uses libsepol for binary policy manipulation and libselinux for + interacting with the SELinux system. It also exec's helper programs + for loading policy and for checking whether the file_contexts + configuration is valid (load_policy and setfiles from + policycoreutils) presently, although this may change at least for the + bootstrapping case + . + Security-enhanced Linux is a patch of the Linux kernel and a + number of utilities with enhanced security functionality designed to + add mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type Enforcement, Role-based Access + Control, and Multi-level Security. +Homepage: http://userspace.selinuxproject.org/ + +Package: sysvinit-utils +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 110 +Maintainer: Debian sysvinit maintainers +Architecture: amd64 +Multi-Arch: foreign +Source: sysvinit +Version: 2.88dsf-59.9 +Replaces: initscripts (<< 2.88dsf-59.5) +Depends: libc6 (>= 2.14), init-system-helpers (>= 1.25~), util-linux (>> 2.28-2~) +Breaks: systemd (<< 215) +Description: System-V-like utilities + This package contains the important System-V-like utilities. + . + Specifically, this package includes: + killall5, pidof +Homepage: http://savannah.nongnu.org/projects/sysvinit + +Package: debian-archive-keyring +Status: install ok installed +Priority: important +Section: misc +Installed-Size: 118 +Maintainer: Debian Release Team +Architecture: all +Multi-Arch: foreign +Version: 2017.5 +Depends: gpgv +Recommends: gnupg +Breaks: apt (<< 0.7.25.1) +Conffiles: + /etc/apt/trusted.gpg.d/debian-archive-jessie-automatic.gpg 548a861c55fdf06fb9b0419c30cd33ec + /etc/apt/trusted.gpg.d/debian-archive-jessie-security-automatic.gpg 28ffc0824518a9d42fb40e5caad7c5ef + /etc/apt/trusted.gpg.d/debian-archive-jessie-stable.gpg 3cf5c69a2532cc3a3cba6c5bcf02cb7b + /etc/apt/trusted.gpg.d/debian-archive-stretch-automatic.gpg c82e16869fb2d6234aff8b63fa00fb65 + /etc/apt/trusted.gpg.d/debian-archive-stretch-security-automatic.gpg 9fcd290147d1ed6b6768c1d1163f1383 + /etc/apt/trusted.gpg.d/debian-archive-stretch-stable.gpg 829333ce438185f23bd9c122a19ef25d + /etc/apt/trusted.gpg.d/debian-archive-wheezy-automatic.gpg e5d3c1f7461bc12685f9b331c60beadd + /etc/apt/trusted.gpg.d/debian-archive-wheezy-stable.gpg 722258fff5af68c002429c4433bd228b +Description: GnuPG archive keys of the Debian archive + The Debian project digitally signs its Release files. This package + contains the archive keys used for that. + +Package: libsemanage-common +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 39 +Maintainer: Debian SELinux maintainers +Architecture: all +Multi-Arch: foreign +Source: libsemanage +Version: 2.6-2 +Replaces: libsemanage1 (<= 2.0.41-1), libsemanage1-dev (<< 2.1.6-3~) +Breaks: libsemanage1 (<= 2.0.41-1), libsemanage1-dev (<< 2.1.6-3~) +Conffiles: + /etc/selinux/semanage.conf f6f9b97af233c90ca127f406fb6f0932 +Description: Common files for SELinux policy management libraries + This package provides the common files used by the shared libraries + for SELinux policy management. + . + Security-enhanced Linux is a patch of the Linux kernel and a + number of utilities with enhanced security functionality designed to + add mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type Enforcement, Role-based Access + Control, and Multi-level Security. +Homepage: http://userspace.selinuxproject.org/ + +Package: libdebconfclient0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 67 +Maintainer: Debian Install System Team +Architecture: amd64 +Multi-Arch: same +Source: cdebconf +Version: 0.227 +Depends: libc6 (>= 2.4) +Description: Debian Configuration Management System (C-implementation library) + Debconf is a configuration management system for Debian packages. It is + used by some packages to prompt you for information before they are + installed. cdebconf is a reimplementation of the original debconf in C. + . + This library allows C programs to interface with cdebconf. + +Package: libselinux1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 209 +Maintainer: Debian SELinux maintainers +Architecture: amd64 +Multi-Arch: same +Source: libselinux (2.6-3) +Version: 2.6-3+b3 +Depends: libc6 (>= 2.14), libpcre3 +Description: SELinux runtime shared libraries + This package provides the shared libraries for Security-enhanced + Linux that provides interfaces (e.g. library functions for the + SELinux kernel APIs like getcon(), other support functions like + getseuserbyname()) to SELinux-aware applications. Security-enhanced + Linux is a patch of the Linux kernel and a number of utilities with + enhanced security functionality designed to add mandatory access + controls to Linux. The Security-enhanced Linux kernel contains new + architectural components originally developed to improve the security + of the Flask operating system. These architectural components provide + general support for the enforcement of many kinds of mandatory access + control policies, including those based on the concepts of Type + Enforcement, Role-based Access Control, and Multi-level Security. + . + libselinux1 provides an API for SELinux applications to get and set + process and file security contexts and to obtain security policy + decisions. Required for any applications that use the SELinux + API. libselinux may use the shared libsepol to manipulate the binary + policy if necessary (e.g. to downgrade the policy format to an older + version supported by the kernel) when loading policy. +Homepage: http://userspace.selinuxproject.org/ + +Package: dpkg +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 6778 +Origin: debian +Maintainer: Dpkg Developers +Bugs: debbugs://bugs.debian.org +Architecture: amd64 +Multi-Arch: foreign +Version: 1.18.25 +Replaces: manpages-it (<< 2.80-4) +Depends: tar (>= 1.28-1) +Pre-Depends: libbz2-1.0, libc6 (>= 2.14), liblzma5 (>= 5.2.2), libselinux1 (>= 2.3), zlib1g (>= 1:1.1.4) +Suggests: apt, debsig-verify +Breaks: acidbase (<= 1.4.5-4), amule (<< 2.3.1+git1a369e47-3), beep (<< 1.3-4), im (<< 1:151-4), libdpkg-perl (<< 1.18.11), netselect (<< 0.3.ds1-27), pconsole (<< 1.0-12), phpgacl (<< 3.3.7-7.3), pure-ftpd (<< 1.0.43-1), systemtap (<< 2.8-1), terminatorx (<< 4.0.1-1), xvt (<= 2.1-20.1) +Conffiles: + /etc/alternatives/README 69c4ba7f08363e998e0f2e244a04f881 + /etc/cron.daily/dpkg b8065b6bfc248caba501c3f5bb508e66 + /etc/dpkg/dpkg.cfg f4413ffb515f8f753624ae3bb365b81b + /etc/logrotate.d/dpkg 782ea5ae536f67ff51dc8c3e2eeb4cf9 +Description: Debian package management system + This package provides the low-level infrastructure for handling the + installation and removal of Debian software packages. + . + For Debian package development tools, install dpkg-dev. +Homepage: https://wiki.debian.org/Teams/Dpkg + +Package: apt +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 3539 +Maintainer: APT Development Team +Architecture: amd64 +Version: 1.4.8 +Replaces: apt-utils (<< 1.3~exp2~) +Depends: adduser, gpgv | gpgv2 | gpgv1, debian-archive-keyring, init-system-helpers (>= 1.18~), libapt-pkg5.0 (>= 1.3~rc2), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libstdc++6 (>= 5.2) +Recommends: gnupg | gnupg2 | gnupg1 +Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), powermgmt-base, python-apt +Breaks: apt-utils (<< 1.3~exp2~) +Conffiles: + /etc/apt/apt.conf.d/01autoremove 0b1391c01d75f95fa4ea5ac01219b515 + /etc/cron.daily/apt-compat bc4a71cbcaeed4179f25d798257fa980 + /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a + /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3 +Description: commandline package manager + This package provides commandline tools for searching and + managing as well as querying information about packages + as a low-level access to all features of the libapt-pkg library. + . + These include: + * apt-get for retrieval of packages and information about them + from authenticated sources and for installation, upgrade and + removal of packages together with their dependencies + * apt-cache for querying available information about installed + as well as installable packages + * apt-cdrom to use removable media as a source for packages + * apt-config as an interface to the configuration settings + * apt-key as an interface to manage authentication keys + +Package: diffutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 1327 +Maintainer: Santiago Vila +Architecture: amd64 +Version: 1:3.5-3 +Replaces: diff +Pre-Depends: libc6 (>= 2.17) +Suggests: diffutils-doc, wdiff +Description: File comparison utilities + The diffutils package provides the diff, diff3, sdiff, and cmp programs. + . + `diff' shows differences between two files, or each corresponding file + in two directories. `cmp' shows the offsets and line numbers where + two files differ. `cmp' can also show all the characters that + differ between the two files, side by side. `diff3' shows differences + among three files. `sdiff' merges two files interactively. + . + The set of differences produced by `diff' can be used to distribute + updates to text files (such as program source code) to other people. + This method is especially useful when the differences are small compared + to the complete files. Given `diff' output, the `patch' program can + update, or "patch", a copy of the file. +Homepage: http://www.gnu.org/software/diffutils/ + +Package: libpam-modules +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 874 +Maintainer: Steve Langasek +Architecture: amd64 +Multi-Arch: same +Source: pam +Version: 1.1.8-3.6 +Replaces: libpam-umask, libpam0g-util +Provides: libpam-mkhomedir, libpam-motd, libpam-umask +Pre-Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.15), libdb5.3, libpam0g (>= 1.1.3-2), libselinux1 (>= 2.1.9), debconf (>= 0.5) | debconf-2.0, libpam-modules-bin (= 1.1.8-3.6) +Conflicts: libpam-mkhomedir, libpam-motd, libpam-umask +Conffiles: + /etc/security/access.conf 13ec4d189f0ed9acf3433977a53d446b + /etc/security/group.conf f1e26e8db6f7abd2d697d7dad3422c36 + /etc/security/limits.conf 11c27ba00b7bd6a255f33126f75c5005 + /etc/security/namespace.conf 6424c99a62ddf4b7d3ca713bb06ded89 + /etc/security/namespace.init d9e6a7c85e966427ef23a04ec6c7000f + /etc/security/pam_env.conf ddee4a931170dc21b4e0b9bb28e02a7b + /etc/security/sepermit.conf d41c74654734a5c069a37bfc02f0a6d4 + /etc/security/time.conf 06e05c6079e839c8833ac7c3abfde192 +Description: Pluggable Authentication Modules for PAM + This package completes the set of modules for PAM. It includes the + pam_unix.so module as well as some specialty modules. +Homepage: http://www.linux-pam.org/ + +Package: libstdc++6 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 1998 +Maintainer: Debian GCC Maintainers +Architecture: amd64 +Multi-Arch: same +Source: gcc-6 +Version: 6.3.0-18+deb9u1 +Replaces: libstdc++6-6-dbg (<< 4.9.0-3) +Depends: gcc-6-base (= 6.3.0-18+deb9u1), libc6 (>= 2.18), libgcc1 (>= 1:4.2) +Breaks: blockattack (<= 1.4.1+ds1-2.1+b2), boo (<= 0.9.5~git20110729.r1.202a430-2), c++-annotations (<= 10.2.0-1), clustalx (<= 2.1+lgpl-3), dff (<= 1.3.0+dfsg.1-4.1+b3), digikam-private-libs (<= 4:4.4.0-1.1+b2), emscripten (<= 1.22.1-1), ergo (<= 3.4.0-1), fceux (<= 2.2.2+dfsg0-1), fiona (<= 1.5.1-2), flush (<= 0.9.12-3.1), freeorion (<= 0.4.4+git20150327-2), fslview (<= 4.0.1-4), fwbuilder (<= 5.1.0-4), gcc-4.3 (<< 4.3.6-1), gcc-4.4 (<< 4.4.6-4), gcc-4.5 (<< 4.5.3-2), gnote (<= 3.16.2-1), gnudatalanguage (<= 0.9.5-2+b2), innoextract (<= 1.4-1+b1), libantlr-dev (<= 2.7.7+dfsg-6), libapache2-mod-passenger (<= 5.0.7-1), libaqsis1 (<= 1.8.2-1), libassimp3 (<= 3.0~dfsg-4), libboost-date-time1.54.0, libboost-date-time1.55.0, libchemps2-1 (<= 1.5-1), libcpprest2.4 (<= 2.4.0-2), libdap17 (<= 3.14.0-2), libdapclient6 (<= 3.14.0-2), libdapserver7 (<= 3.14.0-2), libdavix0 (<= 0.4.0-1+b1), libdballe6 (<= 6.8-1), libdiet-admin2.8 (<= 2.8.0-1+b3), libdiet-client2.8 (<= 2.8.0-1+b3), libdiet-sed2.8 (<= 2.8.0-1+b3), libfreefem++ (<= 3.37.1-1), libgazebo5 (<= 5.0.1+dfsg-2.1), libgetfem4++ (<= 4.2.1~beta1~svn4635~dfsg-3+b1), libgmsh2 (<= 2.9.3+dfsg1-1), libinsighttoolkit4.7 (<= 4.7.2-2), libkolabxml1 (<= 1.1.0-3), libmarisa0 (<= 0.2.4-8), libogre-1.8.0 (<= 1.8.0+dfsg1-7+b1), libogre-1.9.0 (<= 1.9.0+dfsg1-4), libopencv-core2.4, libopenmpi1.6, libopenwalnut1 (<= 1.4.0~rc1+hg3a3147463ee2-1+b1), libpqxx-4.0 (<= 4.0.1+dfsg-3), libreoffice-core (<= 1:4.4.5-2), librime1 (<= 1.2+dfsg-2), libsigc++-2.0-0c2a (<= 2.4.1-1+b1), libwibble-dev (<= 1.1-1), libwreport2 (<= 2.14-1), libxmltooling6 (<= 1.5.3-2.1), lightspark (<= 0.7.2+git20150512-2+b1), mira-assembler (<= 4.9.5-1), mongodb (<= 1:2.4.14-2), mongodb-server (<= 1:2.4.14-2), ncbi-blast+ (<= 2.2.30-4), openscad (<= 2014.03+dfsg-1+b1), passepartout (<= 0.7.1-1.1), pdf2djvu (<= 0.7.21-2), photoprint (<= 0.4.2~pre2-2.3+b2), plastimatch (<= 1.6.2+dfsg-1), plee-the-bear (<= 0.6.0-3.1), povray (<= 1:3.7.0.0-8), powertop (<= 2.6.1-1), printer-driver-brlaser (<= 3-3), psi4 (<= 4.0~beta5+dfsg-2+b1), python-fiona (<= 1.5.1-2), python-guiqwt (<= 2.3.1-1), python-healpy (<= 1.8.1-1+b1), python-htseq (<= 0.5.4p3-2), python-imposm (<= 2.5.0-3+b2), python-pysph (<= 0~20150606.gitfa26de9-5), python-rasterio (<= 0.24.0-1), python-scipy (<= 0.14.1-1), python-sfml (<= 2.2~git20150611.196c88+dfsg-1+b1), python3-fiona (<= 1.5.1-2), python3-scipy (<= 0.14.1-1), python3-sfml (<= 2.2~git20150611.196c88+dfsg-1+b1), python3-taglib (<= 0.3.6+dfsg-2+b2), realtimebattle (<= 1.0.8-14), ruby-passenger (<= 5.0.7-1), schroot (<= 1.6.10-1+b1), sqlitebrowser (<= 3.5.1-3), tecnoballz (<= 0.93.1-6), wesnoth-1.12-core (<= 1:1.12.4-1), widelands (<= 1:18-3+b1), xflr5 (<= 6.09.06-2) +Conflicts: scim (<< 1.4.2-1) +Description: GNU Standard C++ Library v3 + This package contains an additional runtime library for C++ programs + built with the GNU compiler. + . + libstdc++-v3 is a complete rewrite from the previous libstdc++-v2, which + was included up to g++-2.95. The first version of libstdc++-v3 appeared + in g++-3.0. +Homepage: http://gcc.gnu.org/ + +Package: libaudit-common +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 30 +Maintainer: Laurent Bigonville +Architecture: all +Multi-Arch: foreign +Source: audit +Version: 1:2.6.7-2 +Replaces: libaudit0, libaudit1 (<< 1:2.2.1-2) +Breaks: libaudit0, libaudit1 (<< 1:2.2.1-2) +Conffiles: + /etc/libaudit.conf cdc703f9d27f0d980271a9e95d0f18b2 +Description: Dynamic library for security auditing - common files + The audit-libs package contains the dynamic libraries needed for + applications to use the audit framework. It is used to monitor systems for + security related events. + . + This package contains the libaudit.conf configuration file and the associated + manpage. +Homepage: https://people.redhat.com/sgrubb/audit/ + +Package: libcomerr2 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 83 +Maintainer: Theodore Y. Ts'o +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.43.4-2 +Replaces: e2fsprogs (<< 1.34-1) +Provides: libcomerr-kth-compat +Depends: libc6 (>= 2.17) +Description: common error description library + libcomerr is an attempt to present a common error-handling mechanism to + manipulate the most common form of error code in a fashion that does not + have the problems identified with mechanisms commonly in use. +Homepage: http://e2fsprogs.sourceforge.net + +Package: findutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 1854 +Maintainer: Andreas Metzler +Architecture: amd64 +Multi-Arch: foreign +Version: 4.6.0+git+20161106-2 +Pre-Depends: libc6 (>= 2.17), libselinux1 (>= 1.32) +Suggests: mlocate | locate +Breaks: binstats (<< 1.08-8.1), debhelper (<< 9.20130504), guilt (<< 0.36-0.2), kernel-package (<< 13.000), libpython3.4-minimal (<< 3.4.4-2), libpython3.5-minimal (<< 3.5.1-3), lsat (<< 0.9.7.1-2.1), mc (<< 3:4.8.11-1), sendmail (<< 8.14.4-5), switchconf (<< 0.0.9-2.1) +Conflicts: debconf (<< 1.5.50) +Description: utilities for finding files--find, xargs + GNU findutils provides utilities to find files meeting specified + criteria and perform various actions on the files which are found. + This package contains 'find' and 'xargs'; however, 'locate' has + been split off into a separate package. +Homepage: http://savannah.gnu.org/projects/findutils/ + +Package: libpam0g +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 229 +Maintainer: Steve Langasek +Architecture: amd64 +Multi-Arch: same +Source: pam +Version: 1.1.8-3.6 +Replaces: libpam0g-util +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), debconf (>= 0.5) | debconf-2.0 +Suggests: libpam-doc +Description: Pluggable Authentication Modules library + Contains the shared library for Linux-PAM, a library that enables the + local system administrator to choose how applications authenticate users. + In other words, without rewriting or recompiling a PAM-aware application, + it is possible to switch between the authentication mechanism(s) it uses. + One may entirely upgrade the local authentication system without touching + the applications themselves. +Homepage: http://www.linux-pam.org/ + +Package: libcap-ng0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 43 +Maintainer: Pierre Chifflier +Architecture: amd64 +Multi-Arch: same +Source: libcap-ng (0.7.7-3) +Version: 0.7.7-3+b1 +Depends: libc6 (>= 2.8) +Description: An alternate POSIX capabilities library + This library implements the user-space interfaces to the POSIX + 1003.1e capabilities available in Linux kernels. These capabilities are + a partitioning of the all powerful root privilege into a set of distinct + privileges. + . + The libcap-ng library is intended to make programming with POSIX + capabilities much easier than the traditional libcap library. + . + This package contains header files and libraries for libcap-ng. +Homepage: http://people.redhat.com/sgrubb/libcap-ng + +Package: libmount1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 403 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.29.2-1+deb9u1 +Depends: libblkid1 (>= 2.17.2), libc6 (>= 2.17), libselinux1 (>= 2.6-3~) +Description: device mounting library + This device mounting library is used by mount and umount helpers. + +Package: login +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 2747 +Maintainer: Shadow package maintainers +Architecture: amd64 +Source: shadow +Version: 1:4.4-4.1 +Replaces: manpages-de (<< 0.5-3), manpages-tr (<< 1.0.5), manpages-zh (<< 1.5.1-1) +Pre-Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libpam-runtime, libpam-modules (>= 1.1.8-1) +Conflicts: amavisd-new (<< 2.3.3-8), backupninja (<< 0.9.3-5), echolot (<< 2.1.8-4), gnunet (<< 0.7.0c-2), python-4suite (<< 0.99cvs20060405-1) +Conffiles: + /etc/login.defs c92ea31d5ac50ae3e5d33d24f1e70983 + /etc/pam.d/login 1fd6cb4d4267a68148ee9973510a9d3e + /etc/pam.d/su ce6dcfda3b190a27a455bb38a45ff34a + /etc/securetty a7fc5292d9ae16f24d45f94fe88e001b +Description: system login tools + These tools are required to be able to login and use your system. The + login program invokes your user shell and enables command execution. The + newgrp program is used to change your effective group ID (useful for + workgroup type situations). The su program allows changing your effective + user ID (useful being able to execute commands as another user). +Homepage: https://github.com/shadow-maint/shadow + +Package: adduser +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 849 +Maintainer: Debian Adduser Developers +Architecture: all +Multi-Arch: foreign +Version: 3.115 +Depends: passwd, debconf (>= 0.5) | debconf-2.0 +Suggests: liblocale-gettext-perl, perl +Conffiles: + /etc/deluser.conf 773fb95e98a27947de4a95abb3d3f2a2 +Description: add and remove users and groups + This package includes the 'adduser' and 'deluser' commands for creating + and removing users. + . + - 'adduser' creates new users and groups and adds existing users to + existing groups; + - 'deluser' removes users and groups and removes users from a given + group. + . + Adding users with 'adduser' is much easier than adding them manually. + Adduser will choose appropriate UID and GID values, create a home + directory, copy skeletal user configuration, and automate setting + initial values for the user's password, real name and so on. + . + Deluser can back up and remove users' home directories + and mail spool or all the files they own on the system. + . + A custom script can be executed after each of the commands. + . + Development mailing list: + http://lists.alioth.debian.org/mailman/listinfo/adduser-devel/ +Homepage: http://alioth.debian.org/projects/adduser/ + +Package: libacl1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 62 +Maintainer: Anibal Monsalve Salazar +Architecture: amd64 +Multi-Arch: same +Source: acl (2.2.52-3) +Version: 2.2.52-3+b1 +Depends: libattr1 (>= 1:2.4.46-8), libc6 (>= 2.14) +Conflicts: acl (<< 2.0.0), libacl1-kerberos4kth +Description: Access control list shared library + This package contains the libacl.so dynamic library containing + the POSIX 1003.1e draft standard 17 functions for manipulating + access control lists. +Homepage: http://savannah.nongnu.org/projects/acl/ + +Package: ncurses-bin +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 536 +Maintainer: Craig Small +Architecture: amd64 +Multi-Arch: foreign +Source: ncurses +Version: 6.0+20161126-1+deb9u2 +Pre-Depends: libc6 (>= 2.14), libtinfo5 (>= 6.0+20151017), libtinfo5 (<< 6.1~) +Description: terminal-related programs and man pages + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the programs used for manipulating the terminfo + database and individual terminfo entries, as well as some programs for + resetting terminals and such. +Homepage: http://invisible-island.net/ncurses/ + +Package: libmnl0 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 46 +Maintainer: Anibal Monsalve Salazar +Architecture: amd64 +Multi-Arch: same +Source: libmnl +Version: 1.0.4-2 +Depends: libc6 (>= 2.14) +Description: minimalistic Netlink communication library + libmnl is a minimalistic user-space library oriented to Netlink developers. + There are a lot of common tasks in parsing, validating, constructing of + both the Netlink header and TLVs that are repetitive and easy to get wrong. + This library aims to provide simple helpers that allows you to re-use code + and to avoid re-inventing the wheel. + . + The main features of this library are: + . + Small: the shared library requires around 30KB for an x86-based computer. + . + Simple: this library avoids complexity and elaborated abstractions that + tend to hide Netlink details. + . + Easy to use: the library simplifies the work for Netlink-wise developers. + It provides functions to make socket handling, message building, + validating, parsing and sequence tracking, easier. + . + Easy to re-use: you can use the library to build your own abstraction + layer on top of this library. + . + Decoupling: the interdependency of the main bricks that compose the + library is reduced, i.e. the library provides many helpers, but the + programmer is not forced to use them. + . + This package contains the shared libraries needed to run programs that use + the minimalistic Netlink communication library. +Homepage: http://netfilter.org/projects/libmnl/ + +Package: libsepol1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 653 +Maintainer: Debian SELinux maintainers +Architecture: amd64 +Multi-Arch: same +Source: libsepol +Version: 2.6-2 +Depends: libc6 (>= 2.14) +Description: SELinux library for manipulating binary security policies + Security-enhanced Linux is a patch of the Linux kernel and a number + of utilities with enhanced security functionality designed to add + mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type EnforcementĀ®, Role-based Access + Control, and Multi-level Security. + . + libsepol provides an API for the manipulation of SELinux binary policies. + It is used by checkpolicy (the policy compiler) and similar tools, as well + as by programs like load_policy that need to perform specific transformations + on binary policies such as customizing policy boolean settings. +Homepage: http://userspace.selinuxproject.org/ + +Package: libidn11 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 306 +Maintainer: Debian Libidn Team +Architecture: amd64 +Multi-Arch: same +Source: libidn +Version: 1.33-1 +Replaces: libidn11-dev +Depends: libc6 (>= 2.14) +Conflicts: libidn9-dev +Description: GNU Libidn library, implementation of IETF IDN specifications + GNU Libidn is a fully documented implementation of the Stringprep, + Punycode and IDNA specifications. Libidn's purpose is to encode and + decode internationalized domain names. The Nameprep, XMPP, SASLprep, + and iSCSI profiles are supported. + . + This package contains the shared library. +Homepage: https://www.gnu.org/software/libidn/ + +Package: libgcc1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 108 +Maintainer: Debian GCC Maintainers +Architecture: amd64 +Multi-Arch: same +Source: gcc-6 (6.3.0-18+deb9u1) +Version: 1:6.3.0-18+deb9u1 +Depends: gcc-6-base (= 6.3.0-18+deb9u1), libc6 (>= 2.14) +Breaks: gcc-4.3 (<< 4.3.6-1), gcc-4.4 (<< 4.4.6-4), gcc-4.5 (<< 4.5.3-2) +Description: GCC support library + Shared version of the support library, a library of internal subroutines + that GCC uses to overcome shortcomings of particular machines, or + special needs for some languages. +Homepage: http://gcc.gnu.org/ + +Package: util-linux +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 3558 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: foreign +Version: 2.29.2-1+deb9u1 +Replaces: bash-completion (<< 1:2.1-4.1~), initscripts (<< 2.88dsf-59.2~), mount (= 2.26.2-3), mount (= 2.26.2-3ubuntu1), sysvinit-utils (<< 2.88dsf-59.1~) +Pre-Depends: libblkid1 (>= 2.25), libc6 (>= 2.15), libfdisk1 (>= 2.29~rc2), libmount1 (>= 2.25), libncursesw5 (>= 6), libpam0g (>= 0.99.7.1), libselinux1 (>= 2.6-3~), libsmartcols1 (>= 2.28~rc1), libsystemd0, libtinfo5 (>= 6), libudev1 (>= 183), libuuid1 (>= 2.16), zlib1g (>= 1:1.1.4) +Suggests: dosfstools, kbd | console-tools, util-linux-locales +Breaks: bash-completion (<< 1:2.1-4.1~), cloud-utils (<< 0.27-1~), grml-debootstrap (<< 0.68), mount (= 2.26.2-3), mount (= 2.26.2-3ubuntu1), sysvinit-utils (<< 2.88dsf-59.4~) +Conffiles: + /etc/default/hwclock 3916544450533eca69131f894db0ca12 + /etc/init.d/hwclock.sh 1ca5c0743fa797ffa364db95bb8d8d8e + /etc/pam.d/runuser b8b44b045259525e0fae9e38fdb2aeeb + /etc/pam.d/runuser-l 2106ea05877e8913f34b2c77fa02be45 +Description: miscellaneous system utilities + This package contains a number of important utilities, most of which + are oriented towards maintenance of your system. Some of the more + important utilities included in this package allow you to partition + your hard disk, view kernel messages, and create new filesystems. + +Package: libustr-1.0-1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 258 +Maintainer: Vaclav Ovsik +Architecture: amd64 +Multi-Arch: same +Source: ustr +Version: 1.0.4-6 +Depends: libc6 (>= 2.14) +Description: Micro string library: shared library + ustr (Micro string library) is a string API for C. It has tiny overhead over + just plain strdup(), is much safer, is easier to use, is faster for many + operations, can be used with read-only or automatically allocated data. You + don't even need to link to the library to use it (so there are no + dependencies). + . + This package contains the shared library. +Homepage: http://www.and.org/ustr/ + +Package: sed +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 799 +Maintainer: Clint Adams +Architecture: amd64 +Multi-Arch: foreign +Version: 4.4-1 +Pre-Depends: libc6 (>= 2.14), libselinux1 (>= 1.32) +Description: GNU stream editor for filtering/transforming text + sed reads the specified files or the standard input if no + files are specified, makes editing changes according to a + list of commands, and writes the results to the standard + output. +Homepage: https://www.gnu.org/software/sed/ + +Package: libsmartcols1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 257 +Maintainer: Debian util-linux Maintainers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.29.2-1+deb9u1 +Depends: libc6 (>= 2.17) +Description: smart column output alignment library + This smart column output alignment library is used by fdisk utilities.