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.decima.core.assessment.result;
28  
29  import gov.nist.secauto.decima.core.assessment.Assessment;
30  import gov.nist.secauto.decima.core.assessment.util.LoggingHandler;
31  import gov.nist.secauto.decima.core.assessment.util.NoOpLoggingHandler;
32  import gov.nist.secauto.decima.core.document.Document;
33  import gov.nist.secauto.decima.core.requirement.BaseRequirement;
34  import gov.nist.secauto.decima.core.requirement.DerivedRequirement;
35  import gov.nist.secauto.decima.core.requirement.RequirementsManager;
36  import gov.nist.secauto.decima.core.util.ObjectUtil;
37  
38  import org.apache.logging.log4j.LogManager;
39  import org.apache.logging.log4j.Logger;
40  
41  import java.time.Clock;
42  import java.time.ZonedDateTime;
43  import java.util.Collection;
44  import java.util.Collections;
45  import java.util.HashMap;
46  import java.util.LinkedHashMap;
47  import java.util.LinkedList;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Objects;
51  
52  public class DefaultAssessmentResultBuilder implements AssessmentResultBuilder {
53    private static final Logger log = LogManager.getLogger(DefaultAssessmentResultBuilder.class);
54    private static final ResultStatusBehavior DEFAULT_RESULT_STATUS_BEHAVIOR = new DefaultResultStatusBehavior();
55    private final ResultStatusBehavior resultStatusBehavior;
56    private final Map<String, Document> systemIdToAssessedDocumentMap;
57    private final Map<String, List<TestResult>> derivedRequirementToTestResultsMap;
58    private final Map<String, TestState> derivedRequirementsTestStatusMap;
59    private final Map<String, String> assessmentProperties;
60  
61    private ZonedDateTime startDateTime;
62    private ZonedDateTime endDateTime;
63    private LoggingHandler loggingHandler = NoOpLoggingHandler.instance();
64  
65    public DefaultAssessmentResultBuilder() {
66      this(DEFAULT_RESULT_STATUS_BEHAVIOR);
67    }
68  
69    /**
70     * Construct a new assessment result builder using the provided result status behavior
71     * Implementation.
72     * 
73     * @param resultStatusBehavior
74     *          the behavior to use
75     */
76    public DefaultAssessmentResultBuilder(ResultStatusBehavior resultStatusBehavior) {
77      Objects.requireNonNull(resultStatusBehavior, "resultStatusBehavior");
78      this.resultStatusBehavior = resultStatusBehavior;
79      this.systemIdToAssessedDocumentMap = new HashMap<>();
80      this.derivedRequirementToTestResultsMap = new HashMap<>(50);
81      this.derivedRequirementsTestStatusMap = new HashMap<>(50);
82      this.assessmentProperties = new LinkedHashMap<>();
83    }
84  
85    /**
86     * Get the time for when the assessment started.
87     * 
88     * @return the time the assessment was started
89     */
90    public synchronized ZonedDateTime getStartDateTime() {
91      return startDateTime;
92    }
93  
94    /**
95     * This can be used by child classes to manipulate the start time of the assessment.
96     * 
97     * @param startDateTime
98     *          the new start time for the assessment
99     */
100   protected synchronized void setStartDateTime(ZonedDateTime startDateTime) {
101     this.startDateTime = startDateTime;
102   }
103 
104   /**
105    * Get the time for when the assessment ended.
106    * 
107    * @return the time the assessment ended
108    */
109   public synchronized ZonedDateTime getEndDateTime() {
110     return endDateTime;
111   }
112 
113   /**
114    * This can be used by child classes to manipulate the end time of the assessment.
115    * 
116    * @param endDateTime
117    *          the new end time for the assessment
118    */
119   protected synchronized void setEndDateTime(ZonedDateTime endDateTime) {
120     this.endDateTime = endDateTime;
121   }
122 
123   @Override
124   public synchronized Map<String, TestState> getTestStateByDerivedRequirementId() {
125     return Collections.unmodifiableMap(derivedRequirementsTestStatusMap);
126   }
127 
128   public LoggingHandler getLoggingHandler() {
129     return loggingHandler;
130   }
131 
132   public void setLoggingHandler(LoggingHandler loggingHandler) {
133     Objects.requireNonNull(loggingHandler, "loggingHandler");
134     this.loggingHandler = loggingHandler;
135   }
136 
137   @Override
138   public synchronized AssessmentResultBuilder start() {
139     synchronized (this) {
140       if (getStartDateTime() == null) {
141         setStartDateTime(ZonedDateTime.now(Clock.systemDefaultZone()));
142         getLoggingHandler().validationStarted();
143       }
144     }
145     return this;
146   }
147 
148   @Override
149   public synchronized AssessmentResultBuilder end() {
150     synchronized (this) {
151       if (getStartDateTime() == null) {
152         throw new IllegalStateException("The builder was not started. Please call start() first.");
153       }
154       if (getEndDateTime() == null) {
155         setEndDateTime(ZonedDateTime.now(Clock.systemDefaultZone()));
156         getLoggingHandler().validationEnded(this);
157       }
158     }
159     return this;
160   }
161 
162   @Override
163   public AssessmentResultBuilder addAssessmentTarget(Document document) {
164     String systemId = document.getSystemId();
165     if (!systemIdToAssessedDocumentMap.containsKey(systemId)) {
166       systemIdToAssessedDocumentMap.put(systemId, document);
167     } else {
168       Document other = systemIdToAssessedDocumentMap.get(systemId);
169       if (!other.equals(document) && log.isDebugEnabled()) {
170         log.debug("Duplicate systemId {} found for documents {} and {}", systemId, document.toString(),
171             other.toString());
172       }
173     }
174     return this;
175   }
176 
177   @Override
178   public <DOC extends Document> AssessmentResultBuilder addTestResult(Assessment<? extends DOC> assessment,
179       DOC document, String derivedRequirementId, TestResult result) {
180     ObjectUtil.requireNonEmpty(derivedRequirementId);
181     Objects.requireNonNull(result);
182 
183     synchronized (this) {
184       start();
185       List<TestResult> resultList = derivedRequirementToTestResultsMap.get(derivedRequirementId);
186       if (resultList == null) {
187         resultList = new LinkedList<>();
188         derivedRequirementToTestResultsMap.put(derivedRequirementId, resultList);
189       }
190       resultList.add(result);
191       assignTestStatus(assessment, document, derivedRequirementId, TestState.TESTED);
192     }
193 
194     LoggingHandler loggingHandler = getLoggingHandler();
195     if (loggingHandler != null) {
196       loggingHandler.addTestResult(assessment, document, derivedRequirementId, result);
197     }
198     return this;
199   }
200 
201   @Override
202   public AssessmentResults build(RequirementsManager requirementsManager) {
203     getLoggingHandler().producingResults(this, requirementsManager);
204 
205     log.info("Compiling assessment results");
206     DefaultAssessmentResults retval;
207 
208     synchronized (this) {
209       if (startDateTime == null) {
210         throw new IllegalStateException("The builder was not started. Please call start() first.");
211       }
212 
213       if (endDateTime == null) {
214         throw new IllegalStateException("The builder has not been stopped. Please call end() first.");
215       }
216 
217       retval = new DefaultAssessmentResults(requirementsManager, getStartDateTime(), getEndDateTime());
218 
219       for (Map.Entry<String, String> entry : assessmentProperties.entrySet()) {
220         retval.setProperty(entry.getKey(), entry.getValue());
221       }
222 
223       for (Map.Entry<String, Document> entry : systemIdToAssessedDocumentMap.entrySet()) {
224         retval.addAssessmentSubject(entry.getValue());
225       }
226       for (BaseRequirement base : requirementsManager.getBaseRequirements()) {
227         boolean inScope = resultStatusBehavior.isInScope(base);
228         DefaultBaseRequirementResult baseResult;
229         if (inScope) {
230           baseResult = buildBaseRequirementResult(base);
231         } else {
232           baseResult = new DefaultBaseRequirementResult(base, ResultStatus.NOT_IN_SCOPE);
233           for (DerivedRequirement derived : base.getDerivedRequirements()) {
234             baseResult
235                 .addDerivedRequirementResult(new DefaultDerivedRequirementResult(derived, ResultStatus.NOT_IN_SCOPE));
236           }
237         }
238         retval.addValidationResult(baseResult);
239       }
240     }
241 
242     getLoggingHandler().completedResults(this, requirementsManager, retval);
243     return retval;
244   }
245 
246   private DefaultBaseRequirementResult buildBaseRequirementResult(BaseRequirement base) {
247     DefaultBaseRequirementResult retval;
248 
249     Collection<DerivedRequirement> derivedRequirements = base.getDerivedRequirements();
250     retval = new DefaultBaseRequirementResult(base, ResultStatus.NOT_TESTED);
251     if (!derivedRequirements.isEmpty()) {
252       for (DerivedRequirement derived : base.getDerivedRequirements()) {
253         DefaultDerivedRequirementResult result = buildDerivedRequirementResult(derived);
254         retval.addDerivedRequirementResult(result);
255       }
256     }
257     return retval;
258   }
259 
260   private DefaultDerivedRequirementResult buildDerivedRequirementResult(DerivedRequirement derived) {
261 
262     DefaultDerivedRequirementResult derivedResult;
263 
264     boolean inScope = resultStatusBehavior.isInScope(derived);
265     if (!inScope) {
266       derivedResult = new DefaultDerivedRequirementResult(derived, ResultStatus.NOT_IN_SCOPE);
267     } else {
268       derivedResult = new DefaultDerivedRequirementResult(derived, ResultStatus.NOT_TESTED);
269 
270       // Build from the ground up
271       // First add assertion results to the derived result
272       List<TestResult> assertionResults = getAssertionResultsByDerivedRequirementId(derived.getId());
273       if (!assertionResults.isEmpty()) {
274         derivedResult.addTestResults(assertionResults);
275       } else {
276         // No test results means that all the tests passed, the test was
277         // not applicable, or the test was not implemented
278         TestState testStatus = derivedRequirementsTestStatusMap.get(derived.getId());
279         if (testStatus == null) {
280           testStatus = TestState.NOT_TESTED;
281         }
282 
283         switch (testStatus) {
284         case NOT_APPLICABLE:
285           derivedResult.setStatus(ResultStatus.NOT_APPLICABLE);
286           break;
287         case TESTED:
288           // the test was implemented and was tested
289           if (Severity.INFO.equals(derived.getType().getSeverity())) {
290             derivedResult.setStatus(ResultStatus.INFORMATIONAL);
291             // } else {
292             // derivedResult.setStatus(ResultStatus.PASS);
293           } else {
294             derivedResult.setStatus(ResultStatus.PASS);
295           }
296           break;
297         case NOT_TESTED:
298           // do nothing;
299           break;
300         default:
301           throw new UnsupportedOperationException(testStatus.toString());
302         }
303       }
304     }
305     return derivedResult;
306   }
307 
308   @Override
309   public <DOC extends Document> AssessmentResultBuilder assignTestStatus(Assessment<? extends DOC> assessment,
310       DOC document, String derivedRequirementId, TestState state) {
311     ObjectUtil.requireNonEmpty(derivedRequirementId, "derivedRequirementId");
312     Objects.requireNonNull(state, "state");
313 
314     synchronized (this) {
315       start();
316       TestState oldStatus = derivedRequirementsTestStatusMap.get(derivedRequirementId);
317       if (oldStatus == null || oldStatus.ordinal() < state.ordinal()) {
318         derivedRequirementsTestStatusMap.put(derivedRequirementId, state);
319       }
320     }
321 
322     LoggingHandler loggingHandler = getLoggingHandler();
323     if (loggingHandler != null) {
324       loggingHandler.assignTestStatus(assessment, document, derivedRequirementId, state);
325     }
326     return this;
327   }
328 
329   /**
330    * Allows the retrieval of all reported test results for a given derived requirement.
331    * 
332    * @param derivedRequirementId
333    *          the derived requirement to retrieve test results for
334    * @return a non-null list of test results
335    */
336   public List<TestResult> getAssertionResultsByDerivedRequirementId(String derivedRequirementId) {
337     ObjectUtil.requireNonEmpty(derivedRequirementId);
338 
339     List<TestResult> retval;
340 
341     synchronized (this) {
342       retval = derivedRequirementToTestResultsMap.get(derivedRequirementId);
343     }
344 
345     if (retval == null) {
346       retval = Collections.emptyList();
347     } else {
348       retval = Collections.unmodifiableList(retval);
349     }
350     return retval;
351   }
352 
353   /**
354    * Allows arbitrary assessment properties to be associated with the results. This can be used to
355    * include meta information in the assessment results produced.
356    * 
357    * @param key
358    *          the property key
359    * @param value
360    *          the property value
361    * @return this builder
362    */
363   public AssessmentResultBuilder assignProperty(String key, String value) {
364     Objects.requireNonNull(key, "key");
365     Objects.requireNonNull(value, "value");
366     this.assessmentProperties.put(key, value);
367     return this;
368   }
369 }