View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.oscal.lib.profile.resolver.selection;
28  
29  import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
30  import gov.nist.secauto.oscal.lib.model.Matching;
31  import gov.nist.secauto.oscal.lib.model.control.catalog.IControl;
32  import gov.nist.secauto.oscal.lib.model.control.profile.IProfileSelectControlById;
33  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionEvaluationException;
34  
35  import org.apache.commons.lang3.tuple.Pair;
36  import org.apache.logging.log4j.LogManager;
37  import org.apache.logging.log4j.Logger;
38  
39  import java.util.Collections;
40  import java.util.List;
41  import java.util.Objects;
42  import java.util.Set;
43  import java.util.regex.Pattern;
44  import java.util.stream.Collectors;
45  
46  import edu.umd.cs.findbugs.annotations.NonNull;
47  
48  public class DefaultControlSelectionFilter implements IControlSelectionFilter {
49    private static final Logger LOGGER = LogManager.getLogger(DefaultControlSelectionFilter.class);
50  
51    @NonNull
52    private final List<Selection> selections;
53  
54    /**
55     * Construct a new selection filter based on the provided list of select
56     * criteria.
57     *
58     * @param selections
59     *          a list of select criteria
60     */
61    @SuppressWarnings("null")
62    public DefaultControlSelectionFilter(@NonNull List<? extends IProfileSelectControlById> selections) {
63      this.selections = selections.stream()
64          // ignore null entries
65          .filter(Objects::nonNull)
66          // create a selection object for the selection
67          .map(selection -> new Selection(selection))
68          .collect(Collectors.toUnmodifiableList());
69    }
70  
71    @NonNull
72    @Override
73    public Pair<Boolean, Boolean> apply(IControl control) {
74      String id = control.getId();
75      if (id == null) {
76        throw new ProfileResolutionEvaluationException("control is missing an identifier");
77      }
78      return match(id);
79    }
80  
81    /**
82     * Checks if the provided control identifier matches the criteria defined by
83     * this object.
84     *
85     * @param id
86     *          the control identifier to match
87     * @return a {@link Pair} whose first member is {@code true} for a match or
88     *         {@code false} otherwise, and whose second member is {@code true} if
89     *         the match applies to any child controls or {@code false} otherwise
90     */
91    @SuppressWarnings("null")
92    @NonNull
93    protected Pair<Boolean, Boolean> match(String id) {
94      return selections.parallelStream()
95          .map(selection -> selection.match(id))
96          // filter out non-matches
97          .filter(pair -> pair.getLeft())
98          // aggregate matches
99          .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 }