001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.oscal.lib.profile.resolver.selection; 028 029import gov.nist.secauto.metaschema.model.common.util.ObjectUtils; 030import gov.nist.secauto.oscal.lib.model.Matching; 031import gov.nist.secauto.oscal.lib.model.control.catalog.IControl; 032import gov.nist.secauto.oscal.lib.model.control.profile.IProfileSelectControlById; 033import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException; 034 035import org.apache.commons.lang3.tuple.Pair; 036import org.apache.logging.log4j.LogManager; 037import org.apache.logging.log4j.Logger; 038 039import java.util.Collections; 040import java.util.List; 041import java.util.Objects; 042import java.util.Set; 043import java.util.regex.Pattern; 044import java.util.stream.Collectors; 045 046import edu.umd.cs.findbugs.annotations.NonNull; 047 048public class DefaultControlSelectionFilter implements IControlSelectionFilter { 049 private static final Logger LOGGER = LogManager.getLogger(DefaultControlSelectionFilter.class); 050 051 @NonNull 052 private final List<Selection> selections; 053 054 /** 055 * Construct a new selection filter based on the provided list of select 056 * criteria. 057 * 058 * @param selections 059 * a list of select criteria 060 */ 061 @SuppressWarnings("null") 062 public DefaultControlSelectionFilter(@NonNull List<? extends IProfileSelectControlById> selections) { 063 this.selections = selections.stream() 064 // ignore null entries 065 .filter(Objects::nonNull) 066 // create a selection object for the selection 067 .map(selection -> new Selection(selection)) 068 .collect(Collectors.toUnmodifiableList()); 069 } 070 071 @NonNull 072 @Override 073 public Pair<Boolean, Boolean> apply(IControl control) { 074 String id = control.getId(); 075 if (id == null) { 076 throw new ProfileResolutionEvaluationException("control is missing an identifier"); 077 } 078 return match(id); 079 } 080 081 /** 082 * Checks if the provided control identifier matches the criteria defined by 083 * this object. 084 * 085 * @param id 086 * the control identifier to match 087 * @return a {@link Pair} whose first member is {@code true} for a match or 088 * {@code false} otherwise, and whose second member is {@code true} if 089 * the match applies to any child controls or {@code false} otherwise 090 */ 091 @SuppressWarnings("null") 092 @NonNull 093 protected Pair<Boolean, Boolean> match(String id) { 094 return selections.parallelStream() 095 .map(selection -> selection.match(id)) 096 // filter out non-matches 097 .filter(pair -> pair.getLeft()) 098 // aggregate matches 099 .reduce((first, second) -> { 100 Pair<Boolean, Boolean> result; 101 if (first.getLeft() || second.getLeft()) { 102 // at least one matches 103 boolean withChild = first.getLeft() && first.getRight() || second.getLeft() && second.getRight(); 104 result = Pair.of(true, withChild); 105 } else { 106 result = IControlSelectionFilter.NON_MATCH; 107 } 108 return result; 109 }) 110 .orElse(NON_MATCH); 111 } 112 113 @SuppressWarnings("PMD.ImplicitSwitchFallThrough") 114 private static Pattern toPattern(@NonNull Matching matching) { 115 String pattern = ObjectUtils.requireNonNull(matching.getPattern()); 116 String regex = pattern.chars().boxed().map(ch -> (char) ch.intValue()).map(ch -> { 117 118 String value; 119 switch (ch) { 120 case '*': 121 value = ".*"; 122 break; 123 case '?': 124 value = "."; 125 break; 126 case '.': 127 case '+': 128 case '\\': 129 case '[': 130 case ']': 131 case '{': 132 case '}': 133 case '(': 134 case ')': 135 case '^': 136 case '$': 137 value = "\\" + ch; 138 break; 139 default: 140 value = String.valueOf(ch); 141 } 142 return value; 143 }).collect(Collectors.joining("", "^", "$")); 144 145 if (LOGGER.isTraceEnabled()) { 146 LOGGER.atTrace().log("regex: {}", regex); 147 } 148 return Pattern.compile(regex); 149 } 150 151 private static class Selection { 152 153 private final boolean withChildControls; 154 private final Set<String> identifiers; 155 private final List<Pattern> patterns; 156 157 public Selection(IProfileSelectControlById selection) { 158 // process with-child-controls 159 // default is "no" 160 this.withChildControls = "yes".equals(selection.getWithChildControls()); 161 162 // process with-ids 163 List<String> ids = selection.getWithIds(); 164 if (ids == null) { 165 ids = Collections.emptyList(); 166 } 167 this.identifiers = ids.stream() 168 .filter(Objects::nonNull) 169 .collect(Collectors.toUnmodifiableSet()); 170 171 // process with-ids 172 List<Matching> matching = selection.getMatching(); 173 if (matching == null) { 174 matching = Collections.emptyList(); 175 } 176 this.patterns = matching.stream() 177 .filter(Objects::nonNull) 178 .map(DefaultControlSelectionFilter::toPattern) 179 .collect(Collectors.toUnmodifiableList()); 180 } 181 182 public boolean isWithChildControls() { 183 return withChildControls; 184 } 185 186 @NonNull 187 protected Pair<Boolean, Boolean> match(String id) { 188 // first check for direct match 189 boolean result = identifiers.stream().anyMatch(controlIdentifier -> controlIdentifier.equals(id)); 190 if (!result) { 191 // next check for pattern match 192 result = patterns.stream().anyMatch(pattern -> pattern.asMatchPredicate().test(id)); 193 } 194 return ObjectUtils.notNull(Pair.of(result, isWithChildControls())); 195 } 196 } 197}