From 3214fc7c287fbc53c2e43b00610738973ef61327 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 18 Jun 2019 20:13:37 -0700 Subject: [PATCH] Fix OS fingerprinting, add tests, remove dead code Fix cases where OS fingerprinting differed too much from the old recog-java matching. Added tests for OS fingerprinting. Added test for analyzing an image, including a fake alpine image which only contains the os-release file and installed packages file. --- fakealpine.tar | Bin 0 -> 119296 bytes .../analyzer/docker/os/Fingerprinter.java | 80 +++------- .../analyzer/docker/os/FingerprinterTest.java | 121 ++++++++++++++++ .../DockerImageAnalyzerServiceTest.java | 38 +++++ .../docker/test/ImageAnalyzerTester.java | 137 ------------------ .../docker/test/LayerDecompressor.java | 54 ------- .../analyzer/docker/os/alpine-release.txt | 1 + .../container/analyzer/docker/os/alpine.txt | 6 + .../container/analyzer/docker/os/debian.txt | 8 + .../container/analyzer/docker/os/oracle.txt | 16 ++ .../analyzer/docker/os/photon-release.txt | 2 + .../container/analyzer/docker/os/photon.txt | 8 + .../analyzer/docker/os/redhat-release.txt | 1 + .../container/analyzer/docker/os/ubuntu.txt | 11 ++ 14 files changed, 236 insertions(+), 247 deletions(-) create mode 100644 fakealpine.tar create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/os/FingerprinterTest.java create mode 100644 src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java delete mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java delete mode 100644 src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/alpine-release.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/alpine.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/debian.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/oracle.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/photon-release.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/photon.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/redhat-release.txt create mode 100644 src/test/resources/com/rapid7/container/analyzer/docker/os/ubuntu.txt diff --git a/fakealpine.tar b/fakealpine.tar new file mode 100644 index 0000000000000000000000000000000000000000..f690712f812096f62a269546db9dde27a0ffb9a6 GIT binary patch literal 119296 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 literal 0 HcmV?d00001 diff --git a/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java b/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java index b2ba262..b9e0c3b 100644 --- a/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java +++ b/src/main/java/com/rapid7/container/analyzer/docker/os/Fingerprinter.java @@ -1,8 +1,6 @@ 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 java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -20,6 +18,9 @@ public class Fingerprinter { private static final Pattern PATTERN = Pattern.compile("(?.*)=(?.*)"); + private static final Pattern PHOTON_RELEASE = Pattern.compile("^(?i:VMWare Photon(?:\\s?OS)?(?:/)?(?:\\s?Linux)?\\s?(?:v)?(\\d+?(?:\\.\\d+?)*?)?)$"); + private static final Pattern RHEL_RELEASE = Pattern.compile("^(?i:(?:Red Hat|RedHat|Red-Hat|RHEL)(?: Enterprise)?(?: Linux)?(?: Server)?(?: release)?(?: [a-z]+)?\\s?(\\d+?(?:\\.\\d+?)*?)?)(?:\\s?\\(.*\\))?$"); + private static final String OS_FAMILY = "Linux"; private static final Map OS_ID_TO_VENDOR = Arrays.stream(new String[][]{ {"alpine", "Alpine"}, {"amzn", "Amazon"}, @@ -35,10 +36,6 @@ public class Fingerprinter { {"ubuntu", "Ubuntu"}, }).collect(toMap(kv -> kv[0], kv -> kv[1])); - public Fingerprinter() { - - } - /** * Parses the contents of an os-release file to ascertain the fingerprint of an operating system. * @@ -97,37 +94,25 @@ private OperatingSystem parseOsRelease(InputStream input, String architecture) t } 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); + if (!vendor.equals("VMWare")) + product = OS_FAMILY; - return operatingSystem; + return fingerprintOperatingSystem(vendor, product, version, architecture); } } private OperatingSystem parseRhelFamilyRelease(InputStream input, String architecture) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + Matcher matcher = RHEL_RELEASE.matcher(""); + String version = ""; String line = null; - String description = ""; while ((line = reader.readLine()) != null) { - description += line; + matcher.reset(line); + if (matcher.matches()) + version = matcher.group(1); } - return fingerprintOperatingSystem(description, null, null, architecture); + return fingerprintOperatingSystem("Red Hat", OS_FAMILY, version, architecture); } } @@ -140,48 +125,31 @@ private OperatingSystem parseAlpineRelease(InputStream input, String architectur version = line; } - return fingerprintOperatingSystem("Alpine Linux", version, "Alpine", architecture); + return fingerprintOperatingSystem("Alpine", OS_FAMILY, version, architecture); } } private OperatingSystem parsePhotonRelease(InputStream input, String architecture) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + Matcher matcher = PHOTON_RELEASE.matcher(""); + String product = "Photon Linux"; + String version = ""; String line = null; - String description = null; while ((line = reader.readLine()) != null) { - if (line.startsWith("VMWare")) - description = line; + matcher.reset(line); + if (matcher.matches()) + version = matcher.group(1); } - return fingerprintOperatingSystem(description, null, "VMWare", architecture); + return fingerprintOperatingSystem("VMWare", product, version, architecture); } } - private String trimProductName(String productName) { - return productName.replaceAll("(?i:\\s+(?:Enterprise|(?:GNU/)?Linux).*)", ""); - } - - public Package fingerprintPackage(OperatingSystem operatingSystem, String pkg) { + private OperatingSystem fingerprintOperatingSystem(String vendor, String product, String version, String architecture) { + if (vendor.equals("VMWare")) + product = "Photon Linux"; - 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; + return new OperatingSystem(vendor, OS_FAMILY, product, architecture, version, vendor + " " + product + " " + version); } - 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; - - return operatingSystem; - } } diff --git a/src/test/java/com/rapid7/container/analyzer/docker/os/FingerprinterTest.java b/src/test/java/com/rapid7/container/analyzer/docker/os/FingerprinterTest.java new file mode 100644 index 0000000..342a401 --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/os/FingerprinterTest.java @@ -0,0 +1,121 @@ +package com.rapid7.container.analyzer.docker.os; + +import com.rapid7.container.analyzer.docker.model.image.OperatingSystem; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FingerprinterTest { + + @Test + void parseAlpine() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("alpine.txt"), "/etc/os-release", "x86_64"); + + // Then + assertEquals("Alpine", os.getVendor()); + assertEquals("3.8.0", os.getVersion()); + assertEquals("Alpine Linux 3.8.0", os.getDescription()); + } + + @Test + void parseAlpineRelease() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("alpine-release.txt"), "/etc/alpine-release", "x86_64"); + + // Then + assertEquals("Alpine", os.getVendor()); + assertEquals("3.8.0", os.getVersion()); + assertEquals("Alpine Linux 3.8.0", os.getDescription()); + } + + @Test + void parseDebian() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("debian.txt"), "/etc/os-release", "x86_64"); + + // Then + assertEquals("Debian", os.getVendor()); + assertEquals("8", os.getVersion()); + assertEquals("Debian Linux 8", os.getDescription()); + } + + @Test + void parseOracle() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("oracle.txt"), "/etc/os-release", "x86_64"); + + // Then + assertEquals("Oracle", os.getVendor()); + assertEquals("7.6", os.getVersion()); + assertEquals("Oracle Linux 7.6", os.getDescription()); + } + + @Test + void parsePhoton() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("photon.txt"), "/etc/os-release", "x86_64"); + + // Then + assertEquals("VMWare", os.getVendor()); + assertEquals("3.0", os.getVersion()); + assertEquals("VMWare Photon Linux 3.0", os.getDescription()); + } + + @Test + void parsePhotonRelease() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("photon-release.txt"), "/etc/photon-release", "x86_64"); + + // Then + assertEquals("VMWare", os.getVendor()); + assertEquals("3.0", os.getVersion()); + assertEquals("VMWare Photon Linux 3.0", os.getDescription()); + } + + @Test + void parseRedhatRelease() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("redhat-release.txt"), "/etc/redhat-release", "x86_64"); + + // Then + assertEquals("Red Hat", os.getVendor()); + assertEquals("7.6", os.getVersion()); + assertEquals("Red Hat Linux 7.6", os.getDescription()); + } + + @Test + void parseUbuntu() throws IOException { + // Given + Fingerprinter fp = new Fingerprinter(); + + // When + OperatingSystem os = fp.parse(FingerprinterTest.class.getResourceAsStream("ubuntu.txt"), "/etc/os-release", "x86_64"); + + // Then + assertEquals("Ubuntu", os.getVendor()); + assertEquals("16.04", os.getVersion()); + assertEquals("Ubuntu Linux 16.04", os.getDescription()); + } +} diff --git a/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java b/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java new file mode 100644 index 0000000..48b4da9 --- /dev/null +++ b/src/test/java/com/rapid7/container/analyzer/docker/service/DockerImageAnalyzerServiceTest.java @@ -0,0 +1,38 @@ +package com.rapid7.container.analyzer.docker.service; + +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.OperatingSystem; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DockerImageAnalyzerServiceTest { + + @Test + public void test() throws IOException { + // Given + File tarFile = new File("fakealpine.tar"); + ImageId expectedId = new ImageId("sha256:7be494284b1dea6cb2012a5ef99676b4ec22868d9ee235c60e48181542d70fd5"); + OperatingSystem expectedOs = new OperatingSystem("Alpine", "Linux", "Linux", "x86_64", "3.8.0", "Alpine Linux 3.8.0"); + long expectedSize = 119296; + long expectedLayers = 2; + long expectedPackages = 66; + + // When + DockerImageAnalyzerService analyzer = new DockerImageAnalyzerService(null); + Path tmpdir = Files.createTempDirectory("r7dia"); + Image image = analyzer.analyze(tarFile, tmpdir.toString()); + + // Then + assertEquals(expectedId, image.getId()); + assertEquals(expectedOs, image.getOperatingSystem()); + assertEquals(expectedSize, image.getSize()); + assertEquals(expectedLayers, image.getLayers().size()); + assertEquals(expectedPackages, image.getPackages().size()); + } + +} 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 deleted file mode 100644 index 3879d47..0000000 --- a/src/test/java/com/rapid7/container/analyzer/docker/test/ImageAnalyzerTester.java +++ /dev/null @@ -1,137 +0,0 @@ -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 final long MAX_SIZE = 2147483648L; - - /** - * Test image extraction and fingerprinting. Provide the full path to either a docker image - * tar file (from docker save) or a directory containing manifest, config, and layer files. - * - * @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"); - - ImageAnalyzerTester tester = new ImageAnalyzerTester(); - tester.analyzeImage(new File(args[1])); - } - - 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 deleted file mode 100644 index b9b5aa7..0000000 --- a/src/test/java/com/rapid7/container/analyzer/docker/test/LayerDecompressor.java +++ /dev/null @@ -1,54 +0,0 @@ -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/resources/com/rapid7/container/analyzer/docker/os/alpine-release.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/alpine-release.txt new file mode 100644 index 0000000..1981190 --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/alpine-release.txt @@ -0,0 +1 @@ +3.8.0 diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/alpine.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/alpine.txt new file mode 100644 index 0000000..0ebcbf4 --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/alpine.txt @@ -0,0 +1,6 @@ +NAME="Alpine Linux" +ID=alpine +VERSION_ID=3.8.0 +PRETTY_NAME="Alpine Linux v3.8" +HOME_URL="http://alpinelinux.org" +BUG_REPORT_URL="http://bugs.alpinelinux.org" diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/debian.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/debian.txt new file mode 100644 index 0000000..120c51b --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/debian.txt @@ -0,0 +1,8 @@ +PRETTY_NAME="Debian GNU/Linux 8 (jessie)" +NAME="Debian GNU/Linux" +VERSION_ID="8" +VERSION="8 (jessie)" +ID=debian +HOME_URL="http://www.debian.org/" +SUPPORT_URL="http://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/oracle.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/oracle.txt new file mode 100644 index 0000000..c49ccdc --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/oracle.txt @@ -0,0 +1,16 @@ +NAME="Oracle Linux Server" +VERSION="7.6" +ID="ol" +VARIANT="Server" +VARIANT_ID="server" +VERSION_ID="7.6" +PRETTY_NAME="Oracle Linux Server 7.6" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:oracle:linux:7:6:server" +HOME_URL="https://linux.oracle.com/" +BUG_REPORT_URL="https://bugzilla.oracle.com/" + +ORACLE_BUGZILLA_PRODUCT="Oracle Linux 7" +ORACLE_BUGZILLA_PRODUCT_VERSION=7.6 +ORACLE_SUPPORT_PRODUCT="Oracle Linux" +ORACLE_SUPPORT_PRODUCT_VERSION=7.6 diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/photon-release.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/photon-release.txt new file mode 100644 index 0000000..b5cfcaf --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/photon-release.txt @@ -0,0 +1,2 @@ +VMware Photon OS 3.0 +PHOTON_BUILD_NUMBER=49d932d diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/photon.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/photon.txt new file mode 100644 index 0000000..01f4b9a --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/photon.txt @@ -0,0 +1,8 @@ +NAME="VMware Photon OS" +VERSION="3.0" +ID=photon +VERSION_ID=3.0 +PRETTY_NAME="VMware Photon OS/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://vmware.github.io/photon/" +BUG_REPORT_URL="https://github.com/vmware/photon/issues" diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/redhat-release.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/redhat-release.txt new file mode 100644 index 0000000..1d50bfc --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/redhat-release.txt @@ -0,0 +1 @@ +Red Hat Enterprise Linux Server release 7.6 (Maipo) diff --git a/src/test/resources/com/rapid7/container/analyzer/docker/os/ubuntu.txt b/src/test/resources/com/rapid7/container/analyzer/docker/os/ubuntu.txt new file mode 100644 index 0000000..d748d55 --- /dev/null +++ b/src/test/resources/com/rapid7/container/analyzer/docker/os/ubuntu.txt @@ -0,0 +1,11 @@ +NAME="Ubuntu" +VERSION="16.04.6 LTS (Xenial Xerus)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 16.04.6 LTS" +VERSION_ID="16.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" +VERSION_CODENAME=xenial +UBUNTU_CODENAME=xenial