diff --git a/java/code/src/com/redhat/rhn/common/conf/ConfigDefaults.java b/java/code/src/com/redhat/rhn/common/conf/ConfigDefaults.java index f41794c02b59..1ed0442aa8e6 100644 --- a/java/code/src/com/redhat/rhn/common/conf/ConfigDefaults.java +++ b/java/code/src/com/redhat/rhn/common/conf/ConfigDefaults.java @@ -235,6 +235,8 @@ public class ConfigDefaults { public static final String MESSAGE_QUEUE_THREAD_POOL_SIZE = "java.message_queue_thread_pool_size"; + public static final String CVE_AUDIT_ENABLE_OVAL_METADATA = "java.cve_audit.enable_oval_metadata"; + /** * Token lifetime in seconds */ @@ -1188,4 +1190,13 @@ public int getRebootDelay() { return rebootDelay; } + + /** + * Check if the usage of OVAL metadata is permitted in scanning systems for CVE vulnerabilities. + * + * @return {@code true} if OVAL usage is permitted and {@code false} otherwise. + * */ + public boolean isOvalEnabledForCveAudit() { + return Config.get().getBoolean(CVE_AUDIT_ENABLE_OVAL_METADATA, false); + } } diff --git a/java/code/src/com/redhat/rhn/common/db/datasource/xml/oval_queries.xml b/java/code/src/com/redhat/rhn/common/db/datasource/xml/oval_queries.xml index 0b9e12e368b4..29eccb64c4f0 100755 --- a/java/code/src/com/redhat/rhn/common/db/datasource/xml/oval_queries.xml +++ b/java/code/src/com/redhat/rhn/common/db/datasource/xml/oval_queries.xml @@ -29,4 +29,27 @@ AND cve.name = :cve_name; + + + + SELECT 1 FROM suseOVALPlatform plat WHERE starts_with(:cpe, plat.cpe); + + + + + + SELECT 1 + FROM suseCVEServerChannel, + rhnChannelErrata + WHERE suseCVEServerChannel.channel_id = rhnChannelErrata.channel_id + AND server_id = :server_id + + + + + + DELETE FROM suseOVALPlatformVulnerablePackage pvp WHERE pvp.platform_id = (SELECT id FROM suseOVALPlatform plat WHERE plat.cpe = :cpe); + DELETE FROM suseOVALPlatform plat where plat.cpe = :cpe; + + diff --git a/java/code/src/com/redhat/rhn/domain/server/Server.java b/java/code/src/com/redhat/rhn/domain/server/Server.java index 5f2723187e9d..a70dee1e7972 100644 --- a/java/code/src/com/redhat/rhn/domain/server/Server.java +++ b/java/code/src/com/redhat/rhn/domain/server/Server.java @@ -2431,6 +2431,13 @@ public boolean doesOsSupportPtf() { return ServerConstants.SLES.equals(getOs()); } + boolean isSLES() { + return ServerConstants.SLES.equalsIgnoreCase(getOs()); + } + boolean isSLED() { + return ServerConstants.SLED.equalsIgnoreCase(getOs()); + } + /** * Return true if OS supports Confidential Computing Attestation * @@ -2476,6 +2483,10 @@ boolean isSLES15() { return ServerConstants.SLES.equals(getOs()) && getRelease().startsWith("15"); } + boolean isLeap() { + return ServerConstants.LEAP.equalsIgnoreCase(getOs()); + } + boolean isLeap15() { return ServerConstants.LEAP.equalsIgnoreCase(getOs()) && getRelease().startsWith("15"); } @@ -2494,6 +2505,10 @@ boolean isopenSUSEMicroOS() { return ServerConstants.OPENSUSEMICROOS.equals(getOs()); } + boolean isUbuntu() { + return ServerConstants.UBUNTU.equalsIgnoreCase(getOs()); + } + boolean isUbuntu1804() { return ServerConstants.UBUNTU.equals(getOs()) && getRelease().equals("18.04"); } @@ -2506,6 +2521,10 @@ boolean isUbuntu2204() { return ServerConstants.UBUNTU.equals(getOs()) && getRelease().equals("22.04"); } + boolean isDebian() { + return ServerConstants.DEBIAN.equalsIgnoreCase(getOs()); + } + boolean isDebian12() { return ServerConstants.DEBIAN.equals(getOs()) && getRelease().equals("12"); } @@ -2518,6 +2537,10 @@ boolean isDebian10() { return ServerConstants.DEBIAN.equals(getOs()) && getRelease().equals("10"); } + boolean isRHEL() { + return ServerConstants.RHEL.equals(getOs()); + } + /** * This is supposed to cover all RedHat flavors (incl. RHEL, RES and CentOS Linux) */ diff --git a/java/code/src/com/redhat/rhn/domain/server/ServerConstants.java b/java/code/src/com/redhat/rhn/domain/server/ServerConstants.java index 1887dc4d4002..f8a10d5d06d6 100644 --- a/java/code/src/com/redhat/rhn/domain/server/ServerConstants.java +++ b/java/code/src/com/redhat/rhn/domain/server/ServerConstants.java @@ -38,6 +38,8 @@ public class ServerConstants { public static final String ALMA = "AlmaLinux"; public static final String AMAZON = "Amazon Linux"; public static final String ROCKY = "Rocky"; + public static final String SLED = "SLED"; + public static final String RHEL = "Red Hat Enterprise Linux"; private ServerConstants() { diff --git a/java/code/src/com/redhat/rhn/domain/server/ServerFactory.java b/java/code/src/com/redhat/rhn/domain/server/ServerFactory.java index 71a509cfdf7d..767c19f0272f 100644 --- a/java/code/src/com/redhat/rhn/domain/server/ServerFactory.java +++ b/java/code/src/com/redhat/rhn/domain/server/ServerFactory.java @@ -42,6 +42,7 @@ import com.redhat.rhn.frontend.dto.SystemOverview; import com.redhat.rhn.frontend.xmlrpc.ChannelSubscriptionException; import com.redhat.rhn.frontend.xmlrpc.ServerNotInGroupException; +import com.redhat.rhn.manager.audit.OsReleasePair; import com.redhat.rhn.manager.entitlement.EntitlementManager; import com.redhat.rhn.manager.rhnset.RhnSetDecl; import com.redhat.rhn.manager.system.SystemManager; @@ -66,6 +67,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -249,6 +251,19 @@ public static Optional lookupProxyServer(String name) { } } + /** + * List the unique set of pairs of os and release versions used by servers + * + * @return the set of unique pairs of os and release version used by servers + * */ + public static Set listAllServersOsAndRelease() { + List result = SINGLETON.listObjectsByNamedQuery("Server.listAllServersOsAndRelease", + Collections.emptyMap()); + + return result.stream().map(row -> new OsReleasePair((String) row[0], (String) row[1])) + .collect(Collectors.toSet()); + } + /** * Return a map from Salt minion IDs to System IDs. * Map entries are limited to systems that are visible by the specified user. diff --git a/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml b/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml index 03aee52ae5fb..977545c6b6a0 100644 --- a/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml +++ b/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml @@ -361,6 +361,12 @@ PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" WHERE USP.user_id = :user_id AND USP.server_id = s.id) ]]> + + + + CVE Server Channels + + Sync OVAL Data + Refresh mgr-sync data @@ -8833,6 +8836,9 @@ Alternatively, you will want to download <strong>Incremental Channel Conte Generates data required for performing CVE audit queries + + Generate OVAL data required to increase the accuracy of CVE audit queries + Refreshes data about channels, products and subscriptions diff --git a/java/code/src/com/redhat/rhn/frontend/xmlrpc/audit/CVEAuditHandler.java b/java/code/src/com/redhat/rhn/frontend/xmlrpc/audit/CVEAuditHandler.java index f8e3f26a08bf..39732ef89f7b 100644 --- a/java/code/src/com/redhat/rhn/frontend/xmlrpc/audit/CVEAuditHandler.java +++ b/java/code/src/com/redhat/rhn/frontend/xmlrpc/audit/CVEAuditHandler.java @@ -20,7 +20,7 @@ import com.redhat.rhn.frontend.xmlrpc.MethodInvalidParamException; import com.redhat.rhn.frontend.xmlrpc.UnknownCVEIdentifierFaultException; import com.redhat.rhn.manager.audit.CVEAuditImage; -import com.redhat.rhn.manager.audit.CVEAuditManager; +import com.redhat.rhn.manager.audit.CVEAuditManagerOVAL; import com.redhat.rhn.manager.audit.CVEAuditServer; import com.redhat.rhn.manager.audit.PatchStatus; import com.redhat.rhn.manager.audit.UnknownCVEIdentifierException; @@ -117,8 +117,7 @@ public List listSystemsByPatchStatus(User loggedInUser, } try { - // TODO: Use CVEAuditManagerOVAL once it's ready - List result = CVEAuditManager.listSystemsByPatchStatus( + List result = CVEAuditManagerOVAL.listSystemsByPatchStatus( loggedInUser, cveIdentifier, patchStatuses); result.sort(Comparator.comparingInt(s -> s.getPatchStatus().getRank())); @@ -210,8 +209,7 @@ public List listImagesByPatchStatus(User loggedInUser, } try { - // TODO: Use CVEAuditManagerOVAL once it's ready - List result = CVEAuditManager.listImagesByPatchStatus( + List result = CVEAuditManagerOVAL.listImagesByPatchStatus( loggedInUser, cveIdentifier, patchStatuses); result.sort(Comparator.comparingInt(i -> i.getPatchStatus().getRank())); diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditImage.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditImage.java index 87b2840e60f1..c4295e254b0b 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditImage.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditImage.java @@ -94,5 +94,9 @@ public Set getErratas() { return erratas; } + @Override + public Set getScanDataSources() { + return Set.of(); + } } diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManager.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManager.java index aa03c2e8fcd1..eb462b12b96d 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManager.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManager.java @@ -877,7 +877,8 @@ public static List listSystemsByPatchStatus(User user, system.getSystemName(), system.getPatchStatus(), system.getChannels(), - system.getErratas() + system.getErratas(), + Set.of(ScanDataSource.CHANNELS) )).collect(Collectors.toList()); } diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManagerOVAL.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManagerOVAL.java index 91d07e944849..4a177730a5ef 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManagerOVAL.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditManagerOVAL.java @@ -18,19 +18,31 @@ import static com.redhat.rhn.manager.audit.CVEAuditManager.SUCCESSOR_PRODUCT_RANK_BOUNDARY; +import com.redhat.rhn.common.conf.ConfigDefaults; import com.redhat.rhn.domain.rhnpackage.PackageEvr; import com.redhat.rhn.domain.server.Server; +import com.redhat.rhn.domain.server.ServerFactory; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.manager.rhnpackage.PackageManager; import com.suse.oval.OVALCachingFactory; +import com.suse.oval.OVALCleaner; +import com.suse.oval.OsFamily; +import com.suse.oval.OvalParser; import com.suse.oval.ShallowSystemPackage; +import com.suse.oval.config.OVALConfigLoader; +import com.suse.oval.ovaldownloader.OVALDownloadResult; +import com.suse.oval.ovaldownloader.OVALDownloader; +import com.suse.oval.ovaltypes.OvalRootType; import com.suse.oval.vulnerablepkgextractor.VulnerablePackage; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -82,30 +94,77 @@ public static List listSystemsByPatchStatus(User user, String cv Set clients = user.getServers(); for (Server clientServer : clients) { - CVEAuditSystemBuilder systemAuditResult; - // We need this initially to be able to get errata and audit channels information for the OVAL - // implementation. - CVEAuditSystemBuilder auditWithChannelsResult = - CVEAuditManager.doAuditSystem(clientServer.getId(), resultsBySystem.get(clientServer.getId())); - - systemAuditResult = doAuditSystem(cveIdentifier, resultsBySystem.get(clientServer.getId()), - clientServer); - systemAuditResult.setChannels(auditWithChannelsResult.getChannels()); - systemAuditResult.setErratas(auditWithChannelsResult.getErratas()); - - if (patchStatuses.contains(systemAuditResult.getPatchStatus())) { + CVEAuditSystemBuilder auditWithChannelsResult = null; + CVEAuditSystemBuilder auditWithOVALResult = null; + + if (ConfigDefaults.get().isOvalEnabledForCveAudit() && checkOVALAvailability(clientServer)) { + auditWithOVALResult = + doAuditSystem(cveIdentifier, resultsBySystem.get(clientServer.getId()), clientServer); + } + + if (checkChannelsErrataAvailability(clientServer)) { + auditWithChannelsResult = + CVEAuditManager.doAuditSystem(clientServer.getId(), resultsBySystem.get(clientServer.getId())); + } + + CVEAuditSystemBuilder auditResult; + if (auditWithOVALResult != null && auditWithChannelsResult != null) { + auditWithOVALResult.setChannels(auditWithChannelsResult.getChannels()); + auditWithOVALResult.setErratas(auditWithChannelsResult.getErratas()); + auditWithOVALResult.setScanDataSources(ScanDataSource.OVAL, ScanDataSource.CHANNELS); + auditResult = auditWithOVALResult; + } + else if (auditWithOVALResult != null) { + auditWithOVALResult.setChannels(Collections.emptySet()); + auditWithOVALResult.setErratas(Collections.emptySet()); + auditWithOVALResult.setScanDataSources(ScanDataSource.OVAL); + auditResult = auditWithOVALResult; + } + else if (auditWithChannelsResult != null) { + auditWithChannelsResult.setScanDataSources(ScanDataSource.CHANNELS); + auditResult = auditWithChannelsResult; + } + else { + auditResult = new CVEAuditSystemBuilder(clientServer.getId()); + auditResult.setPatchStatus(PatchStatus.UNKNOWN); + auditResult.setSystemID(clientServer.getId()); + auditResult.setSystemName(clientServer.getName()); + } + + if (patchStatuses.contains(auditResult.getPatchStatus())) { result.add(new CVEAuditServer( - systemAuditResult.getId(), - systemAuditResult.getSystemName(), - systemAuditResult.getPatchStatus(), - systemAuditResult.getChannels(), - systemAuditResult.getErratas())); + auditResult.getId(), + auditResult.getSystemName(), + auditResult.getPatchStatus(), + auditResult.getChannels(), + auditResult.getErratas(), + auditResult.getScanDataSources())); } } return result; } + /** + * Check if we have any OVAL vulnerability records for the given client OS in the database. + * + * @param clientServer the server to check + * @return {@code True} + * */ + public static boolean checkOVALAvailability(Server clientServer) { + return OVALCachingFactory.checkOVALAvailability(clientServer.getCpe()); + } + + /** + * Check if we have any erratas assigned to the client's CVE channels. + * + * @param clientServer the server to check + * @return {@code True} + * */ + public static boolean checkChannelsErrataAvailability(Server clientServer) { + return OVALCachingFactory.checkChannelsErrataAvailability(clientServer.getId()); + } + private static boolean isCVEIdentifierUnknown(String cveIdentifier) { return !OVALCachingFactory.canAuditCVE(cveIdentifier) && CVEAuditManager.isCVEIdentifierUnknown(cveIdentifier); } @@ -298,4 +357,125 @@ public static List listImagesByPatchStatus(User user, public static void populateCVEChannels() { CVEAuditManager.populateCVEChannels(); } + + /** + * Launches the OVAL synchronization process + * */ + public static void syncOVAL() { + Set productsToSync = getProductsToSync(); + + LOG.debug("Detected {} products eligible for OVAL synchronization: {}", productsToSync.size(), productsToSync); + + OVALDownloader ovalDownloader = new OVALDownloader(OVALConfigLoader.loadDefaultConfig()); + for (OVALProduct product : productsToSync) { + try { + syncOVALForProduct(product, ovalDownloader); + } + catch (Exception e) { + LOG.error("Failed to sync OVAL for product '{} {}'", + product.getOsFamily().fullname(), product.getOsVersion(), e); + } + } + } + + private static void syncOVALForProduct(OVALProduct product, OVALDownloader ovalDownloader) { + LOG.debug("Downloading OVAL for {} {}", product.getOsFamily(), product.getOsVersion()); + OVALDownloadResult downloadResult; + try { + downloadResult = ovalDownloader.download(product.getOsFamily(), product.getOsVersion()); + } + catch (IOException e) { + throw new RuntimeException("Failed to download OVAL data", e); + } + LOG.debug("Downloading finished"); + + LOG.debug("OVAL vulnerability file: {}", + downloadResult.getVulnerabilityFile().map(File::getAbsoluteFile).orElse(null)); + LOG.debug("OVAL patch file: {}", downloadResult.getPatchFile().map(File::getAbsoluteFile).orElse(null)); + + downloadResult.getVulnerabilityFile().ifPresent(ovalVulnerabilityFile -> { + extractAndSaveOVALData(product, ovalVulnerabilityFile); + LOG.debug("Saving Vulnerability OVAL for {} {}", product.getOsFamily(), product.getOsVersion()); + }); + + downloadResult.getPatchFile().ifPresent(patchFile -> { + extractAndSaveOVALData(product, patchFile); + LOG.debug("Saving Patch OVAL for {} {}", product.getOsFamily(), product.getOsVersion()); + }); + + LOG.debug("Saving OVAL finished"); + } + + /** + * Extracts OVAL metadata from the given {@code ovalFile}, clean it and save it to the database. + * */ + private static void extractAndSaveOVALData(OVALProduct product, File ovalFile) { + OvalRootType ovalRoot = new OvalParser().parse(ovalFile); + OVALCleaner.cleanup(ovalRoot, product.getOsFamily(), product.getOsVersion()); + OVALCachingFactory.savePlatformsVulnerablePackages(ovalRoot); + } + + /** + * Identifies the OS products to synchronize OVAL data for. + * */ + private static Set getProductsToSync() { + return ServerFactory.listAllServersOsAndRelease() + .stream() + .map(OsReleasePair::toOVALProduct) + .filter(Optional::isPresent) + .map(Optional::get).collect(Collectors.toSet()); + } + + public static class OVALProduct { + private OsFamily osFamily; + private String osVersion; + + /** + * Default constructor + * @param osFamilyIn the os family + * @param osVersionIn the os version + * */ + public OVALProduct(OsFamily osFamilyIn, String osVersionIn) { + this.osFamily = osFamilyIn; + this.osVersion = osVersionIn; + } + + public OsFamily getOsFamily() { + return osFamily; + } + + public void setOsFamily(OsFamily osFamilyIn) { + this.osFamily = osFamilyIn; + } + + public String getOsVersion() { + return osVersion; + } + + public void setOsVersion(String osVersionIn) { + this.osVersion = osVersionIn; + } + + @Override + public boolean equals(Object oIn) { + if (this == oIn) { + return true; + } + if (oIn == null || getClass() != oIn.getClass()) { + return false; + } + OVALProduct that = (OVALProduct) oIn; + return osFamily == that.osFamily && Objects.equals(osVersion, that.osVersion); + } + + @Override + public int hashCode() { + return Objects.hash(osFamily, osVersion); + } + + @Override + public String toString() { + return osFamily.fullname() + " " + osVersion; + } + } } diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditServer.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditServer.java index 07f861d7c276..c4099529ae9d 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditServer.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditServer.java @@ -29,6 +29,7 @@ public class CVEAuditServer extends SelectableAdapter implements CVEAuditSystem private long id; private String name; private PatchStatus patchStatus; + private Set scanDataSources; // LinkedHashSet is used to preserve insertion order when iterating private Set channels; @@ -36,20 +37,23 @@ public class CVEAuditServer extends SelectableAdapter implements CVEAuditSystem /** * Constructor - * @param idIn id - * @param nameIn name - * @param statusIn status - * @param channelsIn channels - * @param erratasIn errata + * + * @param idIn id + * @param nameIn name + * @param statusIn status + * @param channelsIn channels + * @param erratasIn errata + * @param scanDataSourcesIn scan data sources */ public CVEAuditServer(long idIn, String nameIn, PatchStatus statusIn, Set channelsIn, - Set erratasIn) { + Set erratasIn, Set scanDataSourcesIn) { this.id = idIn; this.name = nameIn; this.patchStatus = statusIn; this.channels = channelsIn; this.erratas = erratasIn; + this.scanDataSources = scanDataSourcesIn; } /** @@ -110,5 +114,11 @@ public Set getErratas() { return erratas; } - + /** + * @inherit + * */ + @Override + public Set getScanDataSources() { + return scanDataSources; + } } diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystem.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystem.java index b0d7ac3c9bd8..1da8edd8d029 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystem.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystem.java @@ -52,6 +52,12 @@ public interface CVEAuditSystem { */ Set getErratas(); + /** + * Return the data sources used to scan the system. + * @return the set of data sources + * */ + Set getScanDataSources(); + /** * Return the closest channel as {@link String} for CSV file download. * @return closest channel name diff --git a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystemBuilder.java b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystemBuilder.java index 4f14391c4f2b..de32087d773c 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystemBuilder.java +++ b/java/code/src/com/redhat/rhn/manager/audit/CVEAuditSystemBuilder.java @@ -14,6 +14,8 @@ */ package com.redhat.rhn.manager.audit; +import java.util.Arrays; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; @@ -30,6 +32,7 @@ public class CVEAuditSystemBuilder { private long systemID; private String systemName; private PatchStatus patchStatus; + private Set scanDataSources = new HashSet<>(); // LinkedHashSet is used to preserve insertion order when iterating private Set channels = @@ -181,4 +184,18 @@ public String getPatchAdvisory() { public Long getId() { return systemID; } + + /** + * Returns the list of data sources used to audit the system. + * + * @return list of data sources + * */ + public Set getScanDataSources() { + return scanDataSources; + } + + + public void setScanDataSources(ScanDataSource... dataSourcesIn) { + scanDataSources = new HashSet<>(Arrays.asList(dataSourcesIn)); + } } diff --git a/java/code/src/com/redhat/rhn/manager/audit/OsReleasePair.java b/java/code/src/com/redhat/rhn/manager/audit/OsReleasePair.java new file mode 100644 index 000000000000..d991894658ff --- /dev/null +++ b/java/code/src/com/redhat/rhn/manager/audit/OsReleasePair.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.redhat.rhn.manager.audit; + +import com.suse.oval.OsFamily; + +import java.util.Optional; + +public class OsReleasePair { + private final String os; + private final String osRelease; + + /** + * Default Constructor + * @param osIn the name of the OS + * @param osReleaseIn the OS release version + * */ + public OsReleasePair(String osIn, String osReleaseIn) { + os = osIn; + osRelease = osReleaseIn; + } + + public String getOs() { + return os; + } + + public String getOsRelease() { + return osRelease; + } + + /** + * Derives an {@link com.redhat.rhn.manager.audit.CVEAuditManagerOVAL.OVALProduct} object based on the server's + * information, including the operating system name and release version. Used by the OVAL synchronization process + * to identify the installed product and synchronize OVAL data for it. + * + * @return the information of the installed product to synchronize OVAL data for if the product is eligible for + * OVAL synchronization and {@code Optional.empty} otherwise + * (for example product maintainers don't produce OVAL data) + * */ + public Optional toOVALProduct() { + return OsFamily.fromOsName(os).flatMap(serverOsFamily -> { + String serverOsRelease = getOsRelease(); + if (serverOsFamily == OsFamily.REDHAT_ENTERPRISE_LINUX || + serverOsFamily == OsFamily.SUSE_LINUX_ENTERPRISE_SERVER || + serverOsFamily == OsFamily.SUSE_LINUX_ENTERPRISE_DESKTOP) { + // Removing the minor version part: 15.6 --> 15 + serverOsRelease = serverOsRelease.replaceFirst("\\..*$", ""); + } + + CVEAuditManagerOVAL.OVALProduct ovalProduct = null; + if (serverOsFamily.isSupportedRelease(serverOsRelease)) { + ovalProduct = new CVEAuditManagerOVAL.OVALProduct(serverOsFamily, serverOsRelease); + } + + return Optional.ofNullable(ovalProduct); + }); + } + + +} diff --git a/java/code/src/com/redhat/rhn/manager/audit/PatchStatus.java b/java/code/src/com/redhat/rhn/manager/audit/PatchStatus.java index c3d07ae33d4b..2ad2dd78e3f0 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/PatchStatus.java +++ b/java/code/src/com/redhat/rhn/manager/audit/PatchStatus.java @@ -28,7 +28,8 @@ public enum PatchStatus { AFFECTED_FULL_PATCH_APPLICABLE("Affected, full patch available in assigned channel", 4), NOT_AFFECTED("Not affected", 5), PATCHED("Patched", 6), - AFFECTED_PATCH_INAPPLICABLE_SUCCESSOR_PRODUCT("Affected, patch available in a Product Migration target", 7); + AFFECTED_PATCH_INAPPLICABLE_SUCCESSOR_PRODUCT("Affected, patch available in a Product Migration target", 7), + UNKNOWN("Unknown, CVE metadata not available", 8); /** * The lower the more severe diff --git a/java/code/src/com/redhat/rhn/manager/audit/ScanDataSource.java b/java/code/src/com/redhat/rhn/manager/audit/ScanDataSource.java new file mode 100644 index 000000000000..fa0c419bf2e5 --- /dev/null +++ b/java/code/src/com/redhat/rhn/manager/audit/ScanDataSource.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.redhat.rhn.manager.audit; + +public enum ScanDataSource { + OVAL("Oval"), CHANNELS("Channels"); + + private final String displayName; + + ScanDataSource(String displayNameIn) { + this.displayName = displayNameIn; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/java/code/src/com/redhat/rhn/manager/audit/test/CVEAuditManagerOVALTest.java b/java/code/src/com/redhat/rhn/manager/audit/test/CVEAuditManagerOVALTest.java index a2e31c0b49e8..1a686d6fbf04 100644 --- a/java/code/src/com/redhat/rhn/manager/audit/test/CVEAuditManagerOVALTest.java +++ b/java/code/src/com/redhat/rhn/manager/audit/test/CVEAuditManagerOVALTest.java @@ -61,6 +61,7 @@ import java.util.Set; import java.util.stream.Collectors; +// TODO: Test for when patch status is unknown public class CVEAuditManagerOVALTest extends RhnBaseTestCase { public static final String CPE_OPENSUSE_LEAP_15_4 = "cpe:/o:opensuse:leap:15.4"; private OvalParser ovalParser = new OvalParser(); diff --git a/java/code/src/com/redhat/rhn/taskomatic/task/OVALDataSync.java b/java/code/src/com/redhat/rhn/taskomatic/task/OVALDataSync.java new file mode 100644 index 000000000000..931b38219414 --- /dev/null +++ b/java/code/src/com/redhat/rhn/taskomatic/task/OVALDataSync.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.redhat.rhn.taskomatic.task; + +import com.redhat.rhn.manager.audit.CVEAuditManagerOVAL; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Updates OVAL data to power the CVE auditing feature. + * */ +public class OVALDataSync extends RhnJavaJob { + @Override + public String getConfigNamespace() { + return "oval_data_sync"; + } + + @Override + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + log.info("Syncing OVAL data"); + + CVEAuditManagerOVAL.syncOVAL(); + + log.info("Done syncing OVAL data"); + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/CVEAuditController.java b/java/code/src/com/suse/manager/webui/controllers/CVEAuditController.java index 7dcce74c7d48..8ecf54abafce 100644 --- a/java/code/src/com/suse/manager/webui/controllers/CVEAuditController.java +++ b/java/code/src/com/suse/manager/webui/controllers/CVEAuditController.java @@ -25,7 +25,7 @@ import com.redhat.rhn.common.localization.LocalizationService; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.manager.audit.CVEAuditImage; -import com.redhat.rhn.manager.audit.CVEAuditManager; +import com.redhat.rhn.manager.audit.CVEAuditManagerOVAL; import com.redhat.rhn.manager.audit.CVEAuditServer; import com.redhat.rhn.manager.audit.CVEAuditSystem; import com.redhat.rhn.manager.audit.PatchStatus; @@ -142,15 +142,13 @@ public static Object cveAudit(Request req, Response res, User user) { switch (cveAuditRequest.getTarget()) { case SERVER: Set systemSet = RhnSetDecl.SYSTEMS.get(user).getElementValues(); - // TODO: Use CVEAuditManagerOVAL once it's ready - List cveAuditServers = CVEAuditManager + List cveAuditServers = CVEAuditManagerOVAL .listSystemsByPatchStatus(user, cveAuditRequest.cveIdentifier, cveAuditRequest.statuses); cveAuditServers.forEach(serv -> serv.setSelected(systemSet.contains(serv.getId()))); return result(res, ResultJson.success(cveAuditServers), new TypeToken<>() { }); case IMAGE: - // TODO: Use CVEAuditManagerOVAL once it's ready - List cveAuditImages = CVEAuditManager + List cveAuditImages = CVEAuditManagerOVAL .listImagesByPatchStatus(user, cveAuditRequest.cveIdentifier, cveAuditRequest.statuses); return result(res, ResultJson.success(cveAuditImages), new TypeToken<>() { }); @@ -166,15 +164,13 @@ private static List handleRequest(CVEAuditRequest request, User throws UnknownCVEIdentifierException { switch (request.getTarget()) { case SERVER: - // TODO: Use CVEAuditManagerOVAL once it's ready - List cveAuditServers = CVEAuditManager + List cveAuditServers = CVEAuditManagerOVAL .listSystemsByPatchStatus(user, request.cveIdentifier, request.statuses); return cveAuditServers.stream().map(x -> (CVEAuditSystem)x) .collect(Collectors.toList()); case IMAGE: - // TODO: Use CVEAuditManagerOVAL once it's ready - List cveAuditImages = CVEAuditManager + List cveAuditImages = CVEAuditManagerOVAL .listImagesByPatchStatus(user, request.cveIdentifier, request.statuses); return cveAuditImages.stream().map(x -> (CVEAuditSystem)x) diff --git a/java/code/src/com/suse/oval/OVALCachingFactory.java b/java/code/src/com/suse/oval/OVALCachingFactory.java index a34fd82b9b9d..9e395733a600 100644 --- a/java/code/src/com/suse/oval/OVALCachingFactory.java +++ b/java/code/src/com/suse/oval/OVALCachingFactory.java @@ -15,11 +15,14 @@ package com.suse.oval; +import static java.util.stream.Collectors.groupingBy; + import com.redhat.rhn.common.db.datasource.CallableMode; import com.redhat.rhn.common.db.datasource.DataResult; import com.redhat.rhn.common.db.datasource.ModeFactory; import com.redhat.rhn.common.db.datasource.Row; import com.redhat.rhn.common.db.datasource.SelectMode; +import com.redhat.rhn.common.db.datasource.WriteMode; import com.redhat.rhn.common.hibernate.HibernateFactory; import com.suse.oval.manager.OVALLookupHelper; @@ -47,6 +50,11 @@ private OVALCachingFactory() { // Left empty on purpose } + private static void clearOVALMetadataByPlatform(String platformCpe) { + WriteMode mode = ModeFactory.getWriteMode("oval_queries", "clear_oval_metadata_by_platform"); + mode.executeUpdate(Map.of("cpe", platformCpe)); + } + /** * Extracts and save the list of vulnerable packages from {@code rootType} * @@ -57,33 +65,40 @@ public static void savePlatformsVulnerablePackages(OvalRootType rootType) { OVALLookupHelper ovalLookupHelper = new OVALLookupHelper(rootType); - DataResult> batch = new DataResult<>(new ArrayList<>(1000)); - + List productVulnerablePackages = new ArrayList<>(); for (DefinitionType definition : rootType.getDefinitions()) { VulnerablePackagesExtractor vulnerablePackagesExtractor = VulnerablePackagesExtractors.create(definition, rootType.getOsFamily(), ovalLookupHelper); - List extractionResult = vulnerablePackagesExtractor.extract(); - for (ProductVulnerablePackages productVulnerablePackages : extractionResult) { - for (String cve : productVulnerablePackages.getCves()) { - for (VulnerablePackage vulnerablePackage : productVulnerablePackages.getVulnerablePackages()) { - Map params = new HashMap<>(); - params.put("product_name", productVulnerablePackages.getProductCpe()); - params.put("cve_name", cve); - params.put("package_name", vulnerablePackage.getName()); - params.put("fix_version", vulnerablePackage.getFixVersion().orElse(null)); - - batch.add(params); - - if (batch.size() % 1000 == 0) { - mode.getQuery().executeBatchUpdates(batch); - batch.clear(); - commitTransaction(); - - Session session = getSession(); - if (!inTransaction()) { - session.beginTransaction(); - } + productVulnerablePackages.addAll(vulnerablePackagesExtractor.extract()); + } + + // Clear previous OVAL metadata + productVulnerablePackages.stream() + .collect(groupingBy(ProductVulnerablePackages::getProductCpe)) + .keySet().forEach(OVALCachingFactory::clearOVALMetadataByPlatform); + + // Write OVAL metadata in batches + DataResult> batch = new DataResult<>(new ArrayList<>(1000)); + for (ProductVulnerablePackages pvp : productVulnerablePackages) { + for (String cve : pvp.getCves()) { + for (VulnerablePackage vulnerablePackage : pvp.getVulnerablePackages()) { + Map params = new HashMap<>(); + params.put("product_name", pvp.getProductCpe()); + params.put("cve_name", cve); + params.put("package_name", vulnerablePackage.getName()); + params.put("fix_version", vulnerablePackage.getFixVersion().orElse(null)); + + batch.add(params); + + if (batch.size() % 1000 == 0) { + mode.getQuery().executeBatchUpdates(batch); + batch.clear(); + commitTransaction(); + + Session session = getSession(); + if (!inTransaction()) { + session.beginTransaction(); } } } @@ -134,6 +149,38 @@ public static boolean canAuditCVE(String cve) { return !result.isEmpty(); } + /** + * Check if we have any OVAL vulnerability records for the given client OS in the database. + * + * @param cpe the cpe representing of the OS of servers to check for + * @return {@code True} if OVAL is available for servers with {@code cpe} and {@code False} otherwise. + */ + public static boolean checkOVALAvailability(String cpe) { + SelectMode m = ModeFactory.getMode("oval_queries", "check_oval_availability"); + Map params = new HashMap<>(); + params.put("cpe", cpe); + + DataResult result = m.execute(params); + + return !result.isEmpty(); + } + + /** + * Check if we have any erratas assigned to the client's CVE channels. + * + * @param serverId the id of the client to check for + * @return {@code True} if the CVE channels of the server contains erratas. and {@code False} otherwise. + * */ + public static boolean checkChannelsErrataAvailability(Long serverId) { + SelectMode m = ModeFactory.getMode("oval_queries", "check_errata_availability"); + Map params = new HashMap<>(); + params.put("server_id", serverId); + + DataResult result = m.execute(params); + + return !result.isEmpty(); + } + @Override protected Logger getLogger() { return LOG; diff --git a/java/code/src/com/suse/oval/OVALCleaner.java b/java/code/src/com/suse/oval/OVALCleaner.java index 0ff91e4096b5..9fba5d739b99 100644 --- a/java/code/src/com/suse/oval/OVALCleaner.java +++ b/java/code/src/com/suse/oval/OVALCleaner.java @@ -33,6 +33,8 @@ import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -40,6 +42,7 @@ * OVAL data from multiple sources and make changes to it to have a more predictable format. */ public class OVALCleaner { + private OVALCleaner() { } @@ -58,9 +61,8 @@ public static void cleanup(OvalRootType root, OsFamily osFamily, String osVersio root.getDefinitions().removeIf(def -> def.getId().contains("unaffected")); } - if (osFamily == OsFamily.DEBIAN || osFamily == OsFamily.SUSE_LINUX_ENTERPRISE_SERVER || - osFamily == OsFamily.SUSE_LINUX_ENTERPRISE_DESKTOP || osFamily == OsFamily.LEAP) { - // For the above OS families, we only need OVAL vulnerability definitions + // Debian OVAL files could contain patch definitions, but we're only interested in vulnerability definitions + if (osFamily == OsFamily.DEBIAN) { root.getDefinitions().removeIf(def -> def.getDefinitionClass() != DefinitionClassEnum.VULNERABILITY); } @@ -83,16 +85,28 @@ private static void doCleanupDefinition(DefinitionType definition, OsFamily osFa } } + private static final Pattern EXTRACT_CVE_REGEX = Pattern.compile(".{0,30}(CVE-\\d{4}-\\d+).{0,30}"); + private static void fillCves(DefinitionType definition, OsFamily osFamily) { switch (osFamily) { case REDHAT_ENTERPRISE_LINUX: case LEAP: + case LEAP_MICRO: case SUSE_LINUX_ENTERPRISE_SERVER: case SUSE_LINUX_ENTERPRISE_DESKTOP: + case SUSE_LINUX_ENTERPRISE_MICRO: List cves = definition.getMetadata().getAdvisory().map(Advisory::getCveList) .orElse(Collections.emptyList()) - .stream().map(AdvisoryCveType::getCve).collect(Collectors.toList()); + .stream().map(AdvisoryCveType::getCve) + .map(cve -> { + Matcher matcher = EXTRACT_CVE_REGEX.matcher(cve); + if (matcher.find()) { + return matcher.group(1); + } + + return ""; + }).filter(StringUtils::isNotBlank).collect(Collectors.toList()); definition.setCves(cves); break; case DEBIAN: @@ -141,10 +155,11 @@ private static void doCleanupObject(ObjectType object, OsFamily osFamily, String } /** - * Debian Ids are not unique among different versions, so it's possible to have OVAL constructs that have the - * same id but different content for different versions of Debian. - *

- * To work around this, we insert the codename of the version into the id string + * Debian Ids are not unique among different versions. For example, it is possible to find an OVAL test with + * {@code id = "1"} in the OVAL file of debian buster and debian bullseye. This would create a conflict between + * the two tests. + * To work around this and to have globally unique IDs for OVAL tests, we insert the codename of the version into + * the id string. */ private static void convertDebianTestRefs(BaseCriteria root, String osVersion) { if (root instanceof CriteriaType) { diff --git a/java/code/src/com/suse/oval/OsFamily.java b/java/code/src/com/suse/oval/OsFamily.java index db4b4282ea7c..91438d4ae3f2 100644 --- a/java/code/src/com/suse/oval/OsFamily.java +++ b/java/code/src/com/suse/oval/OsFamily.java @@ -15,25 +15,86 @@ package com.suse.oval; +import com.redhat.rhn.domain.server.Server; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * This enum defines the operating system families for which we can retrieve OVAL data. + * */ public enum OsFamily { - LEAP("openSUSE Leap", "leap", "opensuse"), - SUSE_LINUX_ENTERPRISE_SERVER("SUSE Linux Enterprise Server", "sles", "suse"), - SUSE_LINUX_ENTERPRISE_DESKTOP("SUSE Linux Enterprise Desktop", "sled", "suse"), - REDHAT_ENTERPRISE_LINUX("Red Hat Enterprise Linux", "enterprise_linux", "redhat"), - UBUNTU("Ubuntu", "ubuntu", "canonical"), - DEBIAN("Debian", "debian", "debian"); + LEAP("openSUSE Leap", "Leap", "opensuse", + oneOf("15.2", "15.3", "15.4", "15.5", "15.6")), + LEAP_MICRO("openSUSELeap Micro", "openSUE Leap Micro", "opensuse", + oneOf("5.2", "5.3", "5.4", "5.5", "6.0")), + SUSE_LINUX_ENTERPRISE_SERVER("SUSE Linux Enterprise Server", "SLES", "suse", + oneOf("11", "12", "15")), + SUSE_LINUX_ENTERPRISE_DESKTOP("SUSE Linux Enterprise Desktop", "SLED", "suse", + oneOf("10", "11", "12", "15")), + SUSE_LINUX_ENTERPRISE_MICRO("SUSE Linux Enterprise Micro", "SLE Micro", "suse", + oneOf("5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "6.0")), + REDHAT_ENTERPRISE_LINUX("Red Hat Enterprise Linux", "Red Hat Enterprise Linux", "redhat", + withPrefix("7.", "8.", "9.")), + DEBIAN("Debian", "Debian", "debian", oneOf("10", "11", "12")); private final String vendor; private final String fullname; - // Should consist of all lower case characters - private final String shortname; + /** + * Should consist of the same values as {@link Server#getOs()} + * */ + private final String os; + private final Pattern legalReleasePattern; - OsFamily(String fullnameIn, String shortnameIn, String vendorIn) { + OsFamily(String fullnameIn, String osIn, String vendorIn, String legalReleaseRegex) { this.fullname = fullnameIn; - this.shortname = shortnameIn; + this.os = osIn; this.vendor = vendorIn; + legalReleasePattern = Pattern.compile(legalReleaseRegex); + } + + /** + * Returns the full name of the OS family. + * + * @return OS family fullname + * */ + public String fullname() { + return fullname; } - OsFamily(String fullnameIn, String vendorIn) { - this(fullnameIn, fullnameIn.toLowerCase(), vendorIn); + + /** + * Checks if the given OS release version is valid for this OS family. + * + * @param release the release version to check + * @return weather the given OS release version is valid for this OS family. + * */ + public boolean isSupportedRelease(String release) { + return legalReleasePattern.matcher(release).matches(); + } + + private static String oneOf(String ... legalReleases) { + return "(" + String.join("|", escapePeriods(legalReleases)) + ")"; + } + + private static String withPrefix(String ... legalReleasePrefixes) { + return "(" + Arrays.stream(escapePeriods(legalReleasePrefixes)) + .map(prefix -> prefix + ".*") + .collect(Collectors.joining("|")) + ")"; + } + + private static String[] escapePeriods(String ... strings) { + return Arrays.stream(strings).map(str -> str.replace(".", "\\.")).toArray(String[]::new); + } + + /** + * Creates an {@code OsFamily} object from the given os name. + * + * @param osName the os name to convert (it should consist of the same values as {@link Server#getOs()}) + * @return the os family that correspond to the given os name. + * */ + public static Optional fromOsName(String osName) { + return Arrays.stream(values()).filter(osFamily -> osFamily.os.equalsIgnoreCase(osName)).findFirst(); } } diff --git a/java/code/src/com/suse/oval/config/OVALConfig.java b/java/code/src/com/suse/oval/config/OVALConfig.java new file mode 100644 index 000000000000..f030188eaf74 --- /dev/null +++ b/java/code/src/com/suse/oval/config/OVALConfig.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.config; + +import com.suse.oval.OsFamily; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; +import java.util.Optional; + +public class OVALConfig { + @SerializedName("sources") + private Map sources; + + public Map getSources() { + return sources; + } + + /** + * Finds the location or URL of the OVAL vulnerability and/or patch files for the given OVAL product + * identified by the given os family and version. + * + * @param osFamily the os family of the OS to find OVAL data for + * @param version the version of the OS to find OVAL data for + * @return the locations of OVAL vulnerability and/or patch files for the given OS + * */ + public Optional lookupSourceInfo(OsFamily osFamily, String version) { + OVALDistributionSourceInfo distributionSources = sources.get(osFamily); + if (distributionSources != null) { + return distributionSources.getVersionSourceInfo(version); + } + return Optional.empty(); + } + + @Override + public String toString() { + return "OVALConfig{" + + "sources=" + sources + + '}'; + } +} diff --git a/java/code/src/com/suse/oval/config/OVALConfigLoader.java b/java/code/src/com/suse/oval/config/OVALConfigLoader.java new file mode 100644 index 000000000000..c4e195bfe261 --- /dev/null +++ b/java/code/src/com/suse/oval/config/OVALConfigLoader.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.config; + +import static com.suse.utils.Json.GSON; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Objects; + +public class OVALConfigLoader { + private static final String DEFAULT_CONFIG_PATH = "/usr/share/susemanager/oval/oval.config.json"; + private final String configPath; + + /** + * Default constructor + * @param configPathIn the path of oval.config.json + * */ + public OVALConfigLoader(String configPathIn) { + Objects.requireNonNull(configPathIn); + + this.configPath = configPathIn; + } + + /** + * Empty constructor + * */ + public OVALConfigLoader() { + this(DEFAULT_CONFIG_PATH); + } + + /** + * Reads {@code oval.config.json} from the given path, parses it and return it as a {@link OVALConfig} object. + * + * @return A configuration object that corresponds to {@code oval.config.json} + * */ + public OVALConfig load() { + File jsonConfigFile; + try { + jsonConfigFile = new File(configPath); + return GSON.fromJson(new FileReader(jsonConfigFile), OVALConfig.class); + } + catch (IOException e) { + throw new RuntimeException("Failed to load OVAL config file", e); + } + } + + /** + * Loads the OVAL configuration file from the default path. + * + * @return the default OVAL config object. + * */ + public static OVALConfig loadDefaultConfig() { + return new OVALConfigLoader().load(); + } +} diff --git a/java/code/src/com/suse/oval/config/OVALDistributionSourceInfo.java b/java/code/src/com/suse/oval/config/OVALDistributionSourceInfo.java new file mode 100644 index 000000000000..666c47473d6e --- /dev/null +++ b/java/code/src/com/suse/oval/config/OVALDistributionSourceInfo.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.config; + +import com.google.gson.annotations.SerializedName; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +/** + * Encapsulates information about the supported Linux distributions and their corresponding sources of OVAL data. + * */ +public class OVALDistributionSourceInfo { + @SerializedName("content") + private Map content; + + public Map getContent() { + return content == null ? Collections.emptyMap() : content; + } + + /** + * Returns the version source info + * + * @param version the OS version + * + * @return version source info + * */ + public Optional getVersionSourceInfo(String version) { + return Optional.ofNullable(getContent().get(version)); + } + + @Override + public String toString() { + return "OVALDistributionSourceInfo{" + + "content=" + content + + '}'; + } +} diff --git a/java/code/src/com/suse/oval/config/OVALSourceInfo.java b/java/code/src/com/suse/oval/config/OVALSourceInfo.java new file mode 100644 index 000000000000..214a76285a30 --- /dev/null +++ b/java/code/src/com/suse/oval/config/OVALSourceInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.config; + +import com.google.gson.annotations.SerializedName; + +/** + * OVALSourceInfo + * */ +public class OVALSourceInfo { + @SerializedName("vulnerability") + private String vulnerabilitiesInfoSource; + @SerializedName("patch") + private String patchInfoSource; + + public String getVulnerabilitiesInfoSource() { + return vulnerabilitiesInfoSource; + } + + public String getPatchInfoSource() { + return patchInfoSource; + } + + @Override + public String toString() { + return "OVALSourceInfo{" + + "vulnerabilitiesInfoSource='" + vulnerabilitiesInfoSource + '\'' + + ", patchInfoSource='" + patchInfoSource + '\'' + + '}'; + } +} diff --git a/java/code/src/com/suse/oval/config/test/OVALConfigTest.java b/java/code/src/com/suse/oval/config/test/OVALConfigTest.java new file mode 100644 index 000000000000..443aeef33893 --- /dev/null +++ b/java/code/src/com/suse/oval/config/test/OVALConfigTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.config.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.redhat.rhn.testing.TestUtils; + +import com.suse.oval.OsFamily; +import com.suse.oval.config.OVALConfig; +import com.suse.oval.config.OVALConfigLoader; +import com.suse.oval.config.OVALDistributionSourceInfo; +import com.suse.oval.config.OVALSourceInfo; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import wiremock.com.google.common.io.Files; + +public class OVALConfigTest { + + private static OVALConfig config; + + + @BeforeAll + public static void setUp() throws IOException, ClassNotFoundException { + File tempDir = Files.createTempDir(); + File configFile = tempDir.toPath().resolve("oval.config.json").toFile(); + FileUtils.copyURLToFile(TestUtils.findTestData("oval.config.json"), + configFile); + + config = new OVALConfigLoader(configFile.getAbsolutePath()).load(); + } + + @Test + public void testAllSources() throws IOException { + Map sources = config.getSources(); + + List allSources = sources.keySet().stream().flatMap(osFamily -> { + OVALDistributionSourceInfo ovalDistributionSourceInfo = sources.get(osFamily); + return ovalDistributionSourceInfo.getContent().keySet().stream() + .flatMap(version -> { + Optional ovalSourceInfoOpt = + ovalDistributionSourceInfo.getVersionSourceInfo(version); + + OVALSourceInfo ovalSourceInfo = ovalSourceInfoOpt.get(); + + String ovalVulnerabilitiesUrl = ovalSourceInfo.getVulnerabilitiesInfoSource(); + String ovalPatchesUrl = ovalSourceInfo.getPatchInfoSource(); + + List urls = new ArrayList<>(); + if (StringUtils.isNotBlank(ovalVulnerabilitiesUrl)) { + urls.add(ovalVulnerabilitiesUrl); + } + + if (StringUtils.isNotBlank(ovalPatchesUrl)) { + urls.add(ovalPatchesUrl); + } + + return urls.stream(); + }); + }).collect(Collectors.toList()); + + assertFalse(allSources.isEmpty()); + + for (String sourceURL : allSources) { + HttpURLConnection huc = (HttpURLConnection) new URL(sourceURL).openConnection(); + + huc.setRequestMethod("HEAD"); + + int responseCode = huc.getResponseCode(); + + assertEquals(HttpURLConnection.HTTP_OK, responseCode, () -> "Can't connect to URL: " + sourceURL); + } + } + + public void testOsFamilies() { + // TODO: Test that all os families in the config file are in OsFamily enum + } +} diff --git a/java/code/src/com/suse/oval/config/test/oval.config.json b/java/code/src/com/suse/oval/config/test/oval.config.json new file mode 100644 index 000000000000..1dd42f1a7756 --- /dev/null +++ b/java/code/src/com/suse/oval/config/test/oval.config.json @@ -0,0 +1,152 @@ +{ + "sources": { + "LEAP": { + "content": { + "15.2": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.15.2-affected.xml.gz", + "patch": "" + }, + "15.3": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.15.3-affected.xml.gz", + "patch": "" + }, + "15.4": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.15.4-affected.xml.gz", + "patch": "" + }, + "15.5": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.15.5-affected.xml.gz", + "patch": "" + }, + "15.6": { + "vulnerability": "", + "patch": "" + } + } + }, + "LEAP_MICRO": { + "content": { + "5.2": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.micro.5.2-affected.xml.gz", + "patch": "" + }, + "5.3": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.micro.5.3-affected.xml.gz", + "patch": "" + }, + "5.4": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.micro.5.4-affected.xml.gz", + "patch": "" + }, + "5.5": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.micro.5.5-affected.xml.gz", + "patch": "" + }, + "6.0": { + "vulnerability": "", + "patch": "" + } + } + }, + "SUSE_LINUX_ENTERPRISE_SERVER": { + "content": { + "11": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.server.11-affected.xml.gz", + "patch": "" + }, + "12": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.server.12-affected.xml.gz", + "patch": "" + }, + "15": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.server.15-affected.xml.gz", + "patch": "" + } + } + }, + "SUSE_LINUX_ENTERPRISE_DESKTOP": { + "content": { + "10": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.desktop.10.xml.gz", + "patch": "" + }, + "11": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.desktop.11.xml.gz", + "patch": "" + }, + "12": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.desktop.12-affected.xml.gz", + "patch": "" + }, + "15": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.desktop.15-affected.xml.gz", + "patch": "" + } + } + }, + "SUSE_LINUX_ENTERPRISE_MICRO": { + "content": { + "5.0": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.0-affected.xml.gz", + "patch": "" + }, + "5.1": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.1-affected.xml.gz", + "patch": "" + }, + "5.2": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.2-affected.xml.gz", + "patch": "" + }, + "5.3": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.3-affected.xml.gz", + "patch": "" + }, + "5.4": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.4-affected.xml.gz", + "patch": "" + }, + "5.5": { + "vulnerability": "https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.micro.5.5-affected.xml.gz", + "patch": "" + }, + "6.0": { + "vulnerability": "", + "patch": "" + } + } + }, + "REDHAT_ENTERPRISE_LINUX": { + "content": { + "7": { + "vulnerability": "https://www.redhat.com/security/data/oval/v2/RHEL7/rhel-7-including-unpatched.oval.xml.bz2", + "patch": "" + }, + "8": { + "vulnerability": "https://www.redhat.com/security/data/oval/v2/RHEL8/rhel-8-including-unpatched.oval.xml.bz2", + "patch": "" + }, + "9": { + "vulnerability": "https://www.redhat.com/security/data/oval/v2/RHEL9/rhel-9-including-unpatched.oval.xml.bz2", + "patch": "" + } + } + }, + "DEBIAN": { + "content": { + "10": { + "vulnerability": "https://www.debian.org/security/oval/oval-definitions-buster.xml.bz2", + "patch": "" + }, + "11": { + "vulnerability": "https://www.debian.org/security/oval/oval-definitions-bullseye.xml.bz2", + "patch": "" + }, + "12": { + "vulnerability": "https://www.debian.org/security/oval/oval-definitions-bookworm.xml.bz2", + "patch": "" + } + } + } + } +} \ No newline at end of file diff --git a/java/code/src/com/suse/oval/config/test/update_oval_config_json.sh b/java/code/src/com/suse/oval/config/test/update_oval_config_json.sh new file mode 100755 index 000000000000..ac538de523fa --- /dev/null +++ b/java/code/src/com/suse/oval/config/test/update_oval_config_json.sh @@ -0,0 +1,18 @@ +# +# Copyright (c) 2024 SUSE LLC +# +# This software is licensed to you under the GNU General Public License, +# version 2 (GPLv2). There is NO WARRANTY for this software, express or +# implied, including the implied warranties of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 +# along with this software; if not, see +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. +# +# Red Hat trademarks are not licensed under GPLv2. No permission is +# granted to use or replicate Red Hat trademarks that are incorporated +# in this software or its documentation. +# + +pushd $(dirname $0) +cp ../../../../../../../../susemanager-sync-data/oval.config.json . +popd \ No newline at end of file diff --git a/java/code/src/com/suse/oval/ovaldownloader/OVALDownloadResult.java b/java/code/src/com/suse/oval/ovaldownloader/OVALDownloadResult.java new file mode 100644 index 000000000000..b9a3349df198 --- /dev/null +++ b/java/code/src/com/suse/oval/ovaldownloader/OVALDownloadResult.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.ovaldownloader; + +import java.io.File; +import java.util.Optional; + +/** + * This class encapsulates the OVAL (XML) files produced by the {@link OVALDownloader} class. + * Usually, OVAL data of Linux distributions is organized in a repository that contains vulnerability and patch + * definitions. The vulnerability definitions are grouped together in the same OVAL (XML) file, and the same thing + * for patch definitions. + * */ +public class OVALDownloadResult { + private File vulnerabilityFile; + private File patchFile; + + + public Optional getVulnerabilityFile() { + return Optional.ofNullable(vulnerabilityFile); + } + + public Optional getPatchFile() { + return Optional.ofNullable(patchFile); + } + + public void setVulnerabilityFile(File vulnerabilityFileIn) { + this.vulnerabilityFile = vulnerabilityFileIn; + } + + public void setPatchFile(File patchFileIn) { + this.patchFile = patchFileIn; + } +} diff --git a/java/code/src/com/suse/oval/ovaldownloader/OVALDownloader.java b/java/code/src/com/suse/oval/ovaldownloader/OVALDownloader.java new file mode 100644 index 000000000000..01942678a4c7 --- /dev/null +++ b/java/code/src/com/suse/oval/ovaldownloader/OVALDownloader.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.suse.oval.ovaldownloader; + +import com.redhat.rhn.common.conf.Config; +import com.redhat.rhn.common.conf.ConfigDefaults; + +import com.suse.oval.OsFamily; +import com.suse.oval.config.OVALConfig; +import com.suse.oval.config.OVALSourceInfo; + +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.zip.GZIPInputStream; +/** + * The OVAL Downloader is responsible for finding OVAL data online, downloading them, and caching them for easy + * access. Each supported distribution maintains an online server containing their OVAL data organized into XML files, + * known as an OVAL repository. The links in these repositories can be adjusted by editing the {@code oval.config.json} + * file. By default, {@code oval.config.json} directs to the official repositories supplied by the distribution + * maintainer to ensure the integrity and consistency of the consumed OVAL data. + * */ +public class OVALDownloader { + /** + * The path where downloaded OVAL files will be stored. + * */ + private final String ovalCacheDir; + /** + * A configuration object that corresponds to {@code oval.config.json} + * */ + private final OVALConfig config; + + /** + * Default constructor + * + * @param configIn A configuration object that corresponds to {@code oval.config.json} + * */ + public OVALDownloader(OVALConfig configIn) { + this.config = configIn; + String mountPoint = Config.get().getString(ConfigDefaults.REPOMD_CACHE_MOUNT_POINT, "/var/cache"); + Path ovalCacheDirPath = Path.of(mountPoint, "rhn", "ovals").toAbsolutePath(); + + try { + Files.createDirectories(ovalCacheDirPath); + ovalCacheDir = ovalCacheDirPath.toAbsolutePath().toString(); + } + catch (IOException eIn) { + throw new RuntimeException("Couldn't create OVAL cache directory", eIn); + } + + } + + /** + * Download the OVAL data that correspond to the given {@code osFamily} and {@code osVersion} and cache + * them for future access. + * + * @param osFamily the family of the Operating system (OS) to download OVAL data for. + * @param osVersion the version of the Operating system (OS) to download OVAL data for. + * @return {@link OVALDownloadResult} + * */ + public OVALDownloadResult download(OsFamily osFamily, String osVersion) throws IOException { + Optional sourceInfoOpt = config.lookupSourceInfo(osFamily, osVersion); + if (sourceInfoOpt.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "OVAL sources for '%s' '%s' is not configured. Please ensure OVAL is configured " + + "correctly oval.config.json", + osFamily, osVersion)); + } + + return doDownload(sourceInfoOpt.get()); + } + + /** + * A helper method for download OVAL data. + * */ + private OVALDownloadResult doDownload(OVALSourceInfo sourceInfo) + throws IOException { + OVALDownloadResult result = new OVALDownloadResult(); + + String vulnerabilityInfoSource = sourceInfo.getVulnerabilitiesInfoSource(); + String patchInfoSource = sourceInfo.getPatchInfoSource(); + + if (StringUtils.isNotBlank(vulnerabilityInfoSource)) { + File vulnerabilityFile = downloadOVALFile(vulnerabilityInfoSource); + result.setVulnerabilityFile(vulnerabilityFile); + } + + if (StringUtils.isNotBlank(patchInfoSource)) { + File patchFile = downloadOVALFile(patchInfoSource); + result.setPatchFile(patchFile); + } + + return result; + } + + private File downloadOVALFile(String vulnerabilityInfoSource) throws IOException { + URL vulnerabilityInfoURL = new URL(vulnerabilityInfoSource); + String vulnerabilityInfoOVALFilename = FilenameUtils.getName(vulnerabilityInfoURL.getPath()); + File vulnerabilityFile = Path.of(ovalCacheDir, vulnerabilityInfoOVALFilename).toFile(); + // Start downloading + FileUtils.copyURLToFile(vulnerabilityInfoURL, vulnerabilityFile, 15_000, 15_000); + + return decompressIfNeeded(vulnerabilityFile); + } + + private File decompressIfNeeded(File file) { + String filename = file.getName(); + + File uncompressedOVALFile; + if (filename.endsWith(".bz2")) { + uncompressedOVALFile = new File(FilenameUtils.removeExtension(file.getPath())); + decompressBzip2(file, uncompressedOVALFile); + } + else if (filename.endsWith(".gz")) { + uncompressedOVALFile = new File(FilenameUtils.removeExtension(file.getPath())); + decompressGzip(file, uncompressedOVALFile); + } + else if (filename.endsWith(".xml")) { + uncompressedOVALFile = file; + } + else { + throw new IllegalStateException("Unable to decompress file: " + file.getPath()); + } + + return uncompressedOVALFile; + } + + /** + * Decompress the GZIP {@code archive} into {@code target} file. + */ + private static void decompressGzip(File archive, File target) { + try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream(archive))) { + FileUtils.copyToFile(gis, target); + } + catch (IOException e) { + throw new RuntimeException("Failed to decompress GZIP archive", e); + } + } + + /** + * Decompress the BZIP2 {@code archive} into {@code target} file. + */ + private static void decompressBzip2(File archive, File target) { + try (InputStream inputStream = new BZip2CompressorInputStream(new FileInputStream(archive))) { + FileUtils.copyToFile(inputStream, target); + } + catch (IOException e) { + throw new RuntimeException("Failed to decompress BZIP2 archive", e); + } + } +} diff --git a/java/code/src/com/suse/oval/vulnerablepkgextractor/SUSEVulnerablePackageExtractor.java b/java/code/src/com/suse/oval/vulnerablepkgextractor/SUSEVulnerablePackageExtractor.java index 2f6d9c015d5a..757043c39c70 100644 --- a/java/code/src/com/suse/oval/vulnerablepkgextractor/SUSEVulnerablePackageExtractor.java +++ b/java/code/src/com/suse/oval/vulnerablepkgextractor/SUSEVulnerablePackageExtractor.java @@ -42,7 +42,7 @@ */ public class SUSEVulnerablePackageExtractor extends CriteriaTreeBasedExtractor { private static final Pattern RELEASE_PACKAGE_REGEX = Pattern.compile( - "^\\s*(?[-a-zA-Z_0-9]+) is\\s*==\\s*(?[0-9.]+)\\s*$"); + "^\\s*(?[-a-zA-Z_0-9]+) is\\s*(==|>=)\\s*(?[0-9.]+)\\s*$"); private final OVALLookupHelper ovalLookupHelper; /** @@ -173,11 +173,33 @@ private Cpe deriveCpe(TestType productTest) { if (osProduct == OsFamily.LEAP) { return deriveOpenSUSELeapCpe(); } + else if (osProduct == OsFamily.LEAP_MICRO) { + return deriveOpenSUSELeapMicroCpe(); + } + else if (osProduct == OsFamily.SUSE_LINUX_ENTERPRISE_MICRO) { + return deriveSUSEMicroCpe(); + } else { - return deriveSUSEProductCpe(productTest); + return deriveFromProductOVALTest(productTest); } } + private Cpe deriveOpenSUSELeapMicroCpe() { + return new CpeBuilder() + .withVendor("opensuse") + .withProduct("leap-micro") + .withVersion(definition.getOsVersion()) + .build(); + } + + private Cpe deriveSUSEMicroCpe() { + return new CpeBuilder() + .withVendor("suse") + .withProduct("sle-micro") + .withVersion(definition.getOsVersion()) + .build(); + } + private Cpe deriveOpenSUSELeapCpe() { return new CpeBuilder() .withVendor("opensuse") @@ -186,7 +208,10 @@ private Cpe deriveOpenSUSELeapCpe() { .build(); } - private Cpe deriveSUSEProductCpe(TestType productTest) { + private Cpe deriveFromProductOVALTest(TestType productTest) { + // Example of the content of an OVAL product test: + // { }); }; + getPatchStatusAccuracyWarning = (row) => { + const dataSources: string[] = row.scanDataSources; + if (!dataSources) { + Loggerhead.error("CVE audit data sources were not supplied by server."); + return t("Error, see console"); + } + + if (dataSources.length === 0) { + return t("Unknown patch status"); + } else if (dataSources.indexOf("OVAL") === -1) { + return t("OVAL data out of sync. Potential missed vulnerabilities"); + } else if (dataSources.indexOf("CHANNELS") === -1) { + return t("Server product channels out of sync; no patch information available"); + } + + Loggerhead.error(`Invalid scan data sources: ${dataSources}`); + + return t("Error, see console"); + }; + render() { return ( @@ -346,6 +375,12 @@ class CVEAudit extends React.Component { className={"fa fa-big " + PATCH_STATUS_LABEL[row.patchStatus].className} title={PATCH_STATUS_LABEL[row.patchStatus].description} /> + {row.patchStatus !== UNKNOWN && row.scanDataSources.length < 2 && ( + + )} )} /> @@ -378,7 +413,8 @@ class CVEAudit extends React.Component { row.patchStatus === PATCHED || row.patchStatus === AFFECTED_PATCH_UNAVAILABLE || row.patchStatus === AFFECTED_PATCH_UNAVAILABLE_IN_UYUNI || - row.patchStatus === AFFECTED_PATCH_UNAVAILABLE + row.patchStatus === AFFECTED_PATCH_UNAVAILABLE || + row.patchStatus === UNKNOWN ) { return t("No action required"); } else if (row.patchStatus === AFFECTED_FULL_PATCH_APPLICABLE) { diff --git a/web/spacewalk-web.changes.HoussemNasri.oval-downloader b/web/spacewalk-web.changes.HoussemNasri.oval-downloader new file mode 100644 index 000000000000..0847510d51d8 --- /dev/null +++ b/web/spacewalk-web.changes.HoussemNasri.oval-downloader @@ -0,0 +1,2 @@ +- Add a column to the CVE auditing result table to show the data + source (OVAL or channels) used for auditing the system \ No newline at end of file