diff --git a/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskLevelCount.java b/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskLevelCount.java index 56052540..81c20a83 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskLevelCount.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskLevelCount.java @@ -24,14 +24,15 @@ ///////////////////////////////////////////////////////////////////////// package uk.ac.soton.itinnovation.security.model.system; +import java.util.Objects; import uk.ac.soton.itinnovation.security.model.Level; -public class RiskLevelCount { - +public class RiskLevelCount implements Comparable { + private Level level; - + private int count; - + public RiskLevelCount() { } @@ -50,6 +51,30 @@ public int getCount() { public void setCount(int count) { this.count = count; } - - + + @Override + public int compareTo(RiskLevelCount other) { + // Compare levels first + int levelComparison = this.level.compareTo(other.level); + if (levelComparison != 0) { + return levelComparison; + } + + // If levels are equal, compare counts + return Integer.compare(this.count, other.count); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + RiskLevelCount that = (RiskLevelCount) obj; + return count == that.count && Objects.equals(level, that.level); + } + + @Override + public int hashCode() { + return Objects.hash(level, count); + } + } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskVector.java b/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskVector.java index d0dcf33f..79829810 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskVector.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/model/system/RiskVector.java @@ -25,55 +25,127 @@ package uk.ac.soton.itinnovation.security.model.system; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Objects; + import uk.ac.soton.itinnovation.security.model.Level; -public class RiskVector { - - private Map riskVector; - - public RiskVector(Collection riskLevels, Map riskLevelCounts) { - riskVector = new HashMap<>(); - - //For each defined risk level, get the count of misbehaviours at this level - for (Level riskLevel : riskLevels) { - RiskLevelCount riskLevelCount = new RiskLevelCount(); - riskLevelCount.setLevel(riskLevel); - Integer count = riskLevelCounts.get(riskLevel.getUri()); - riskLevelCount.setCount(count); - riskVector.put(riskLevel.getUri(), riskLevelCount); - } - } - - public Map getRiskVector() { - return riskVector; - } - - public void setRiskVector(Map riskVector) { - this.riskVector = riskVector; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - sb.append("("); - - Collection riskLevelCounts = riskVector.values(); - - for (RiskLevelCount riskLevelCount : riskLevelCounts) { - sb.append(riskLevelCount.getLevel().getLabel()); - sb.append(": "); - sb.append(riskLevelCount.getCount()); - sb.append(", "); - } - - sb.setLength(sb.length() -2); //remove last comma - - sb.append(")"); - - return sb.toString(); - } +public class RiskVector implements Comparable { + + private Map riskV; + private Map levelValueMap; // aux map for comparison + + public RiskVector(Collection riskLevels, Map riskLevelCounts) { + this.riskV = new HashMap<>(); + this.levelValueMap = new HashMap<>(); + + //For each defined risk level, get the count of misbehaviours at this level + for (Level riskLevel : riskLevels) { + RiskLevelCount riskLevelCount = new RiskLevelCount(); + riskLevelCount.setLevel(riskLevel); + Integer count = riskLevelCounts.get(riskLevel.getUri()); + riskLevelCount.setCount(count); + riskV.put(riskLevel.getUri(), riskLevelCount); + levelValueMap.put(riskLevel.getValue(), riskLevel.getUri()); + } + } + + public Map getRiskVector() { + return riskV; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("("); + + // put the items from riskLevelCounts in a list + List riskLevelCounts = new ArrayList<>(riskV.values()); + + // sort the riskLevelCounts entries by the RiskLevelCount object's default sort + Collections.sort(riskLevelCounts); + + for (RiskLevelCount riskLevelCount : riskLevelCounts) { + sb.append(riskLevelCount.getLevel().getLabel()); + sb.append(": "); + sb.append(riskLevelCount.getCount()); + sb.append(", "); + } + + sb.setLength(sb.length() - 2); //remove last comma + + sb.append(")"); + + return sb.toString(); + } + + public String getOverall() { + int overall = 0; + String uri = ""; + for (Map.Entry entry : riskV.entrySet()) { + String riskLevelUri = entry.getValue().getLevel().getUri(); + int riskLevelValue = entry.getValue().getLevel().getValue(); + int riskCount = entry.getValue().getCount(); + if (riskCount > 0 && riskLevelValue >= overall) { + overall = riskLevelValue; + uri = riskLevelUri; + } + } + return uri; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + RiskVector other = (RiskVector) obj; + return Objects.equals(riskV, other.riskV); + } + + @Override + public int hashCode() { + return Objects.hashCode(riskV); + } + + @Override + public int compareTo(RiskVector other) { + + List sortedKeys = new ArrayList<>(levelValueMap.keySet()); + Collections.sort(sortedKeys, Collections.reverseOrder()); + + // iterate based on the sorted keys + for (Integer key : sortedKeys) { + String riskLevelUri = levelValueMap.get(key); + RiskLevelCount thisRiskLevelCount = riskV.get(riskLevelUri); + RiskLevelCount otherRiskLevelCount = other.riskV.get(riskLevelUri); + + if (thisRiskLevelCount == null && otherRiskLevelCount == null) { + continue; // Both are missing + } + if (thisRiskLevelCount == null) { + return -1; // This object is considered "less" + } + if (otherRiskLevelCount == null) { + return 1; // This object is considered "greater" + } + + // Compare RiskLevelCount objects + int result = thisRiskLevelCount.compareTo(otherRiskLevelCount); + if (result != 0) { + return result; + } + } + // If all compared RiskLevelCount objects are equal, consider the RiskVectors equal + return 0; + } } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/IQuerierDB.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/IQuerierDB.java index 11ffabeb..8eee164b 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/IQuerierDB.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/IQuerierDB.java @@ -217,8 +217,11 @@ public interface IQuerierDB { */ void repairAssertedAssetPopulations(); void repairCardinalityConstraints(); + boolean updateAssertedLevel(LevelDB level, String twasURI, String model); boolean updateAssertedLevel(LevelDB level, TrustworthinessAttributeSetDB twas, String model); + boolean updateCoverageLevel(LevelDB level, String csURI, String model); boolean updateCoverageLevel(LevelDB level, ControlSetDB cs, String model); + boolean updateProposedStatus(Boolean status, String csURI, String model); boolean updateProposedStatus(Boolean status, ControlSetDB cs, String model); /** diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/JenaQuerierDB.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/JenaQuerierDB.java index 7dac93c7..3309673a 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/JenaQuerierDB.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/JenaQuerierDB.java @@ -158,6 +158,8 @@ public void initForRiskCalculation(){ } public void init(){ + logger.info("Initialising JenaQuerierDB"); + final long startTime = System.currentTimeMillis(); this.prefixMap = dataset.getNamedModel(stack.getGraph("core")).getNsPrefixMap(); @@ -3390,13 +3392,13 @@ private void fixCardinalityConstraintURI(Map cc } - /* Method to override the assumed TW level of a TWAS in a graph without creating the TWAS + /* Methods to override the assumed TW level of a TWAS in a graph without creating the TWAS * in the same graph. Needed to adjust user/client asserted levels which appear as single * triples in the asserted graph, but without the TWAS entity (which is added later by the * validator in the inferred graph). */ @Override - public boolean updateAssertedLevel(LevelDB level, TrustworthinessAttributeSetDB twas, String model){ + public boolean updateAssertedLevel(LevelDB level, String twasURI, String model){ String graphUri = stack.getGraph(model); if (graphUri == null) { return false; @@ -3404,27 +3406,54 @@ public boolean updateAssertedLevel(LevelDB level, TrustworthinessAttributeSetDB Model datasetModel = dataset.getNamedModel(graphUri); // Encode the population level as a single property of the asset resource - Resource resource = datasetModel.getResource(getLongName(twas.getUri())); + Resource resource = datasetModel.getResource(getLongName(twasURI)); Property property = ResourceFactory.createProperty(getLongName("core#hasAssertedLevel")); RDFNode object = ResourceFactory.createResource(getLongName(level.getUri())); // Now remove the old value and save the new value - dataset.begin(ReadWrite.WRITE); - resource.removeAll(property); - resource.addProperty(property, object); - dataset.commit(); - dataset.end(); + try { + dataset.begin(ReadWrite.WRITE); + resource.removeAll(property); + resource.addProperty(property, object); + dataset.commit(); + } + catch (Exception e) { + // Abort the changes and signal that there has been an error + dataset.abort(); + String message = String.format("Error occurred while updating assumed TW level for TWAS %s", twasURI); + logger.error(message, e); + throw new RuntimeException(message, e); + } + finally { + dataset.end(); + } + + if(cacheEnabled){ + // Make the same change in the cached object, if it exists + TrustworthinessAttributeSetDB twas = this.getTrustworthinessAttributeSet(twasURI, model); + if(twas != null) { + twas.setAssertedLevel(level.getUri()); + this.store(twas, model); + } + + // Note that the calling process must change ControlSetDB objects for other graphs } + } return true; } - /* Method to override the coverage level of a CS in a graph without creating the CS in the + @Override + public boolean updateAssertedLevel(LevelDB level, TrustworthinessAttributeSetDB twas, String model){ + return updateAssertedLevel(level, twas.getUri(), model); + } + + /* Methods to override the coverage level of a CS in a graph without creating the CS in the * same graph. Needed to adjust user/client supplied coverage levels which appear as single * triples in the asserted graph, but without the CS entity (which is added later by the * validator in the inferred graph). */ @Override - public boolean updateCoverageLevel(LevelDB level, ControlSetDB cs, String model){ + public boolean updateCoverageLevel(LevelDB level, String csURI, String model){ String graphUri = stack.getGraph(model); if (graphUri == null) { return false; @@ -3432,27 +3461,54 @@ public boolean updateCoverageLevel(LevelDB level, ControlSetDB cs, String model) Model datasetModel = dataset.getNamedModel(graphUri); // Encode the population level as a single property of the asset resource - Resource resource = datasetModel.getResource(getLongName(cs.getUri())); + Resource resource = datasetModel.getResource(getLongName(csURI)); Property property = ResourceFactory.createProperty(getLongName("core#hasCoverageLevel")); RDFNode object = ResourceFactory.createResource(getLongName(level.getUri())); // Now remove the old value and save the new value - dataset.begin(ReadWrite.WRITE); - resource.removeAll(property); - resource.addProperty(property, object); - dataset.commit(); - dataset.end(); + try { + dataset.begin(ReadWrite.WRITE); + resource.removeAll(property); + resource.addProperty(property, object); + dataset.commit(); + } + catch (Exception e) { + // Abort the changes and signal that there has been an error + dataset.abort(); + String message = String.format("Error occurred while updating control coverage level for CS %s", csURI); + logger.error(message, e); + throw new RuntimeException(message, e); + } + finally { + dataset.end(); + } + + if(cacheEnabled){ + // Make the same change in the cached object, if it exists + ControlSetDB cs = this.getControlSet(csURI, model); + if(cs != null) { + cs.setCoverageLevel(level.getUri()); + this.store(cs, model); + } + + // Note that the calling process must change ControlSetDB objects for other graphs } + } return true; } - /* Method to override the proposed status of a CS in a graph without creating the CS in the + @Override + public boolean updateCoverageLevel(LevelDB level, ControlSetDB cs, String model){ + return updateCoverageLevel(level, cs.getUri(), model); + } + + /* Methods to override the proposed status of a CS in a graph without creating the CS in the * same graph. Needed to adjust user/client supplied status flags which appear as single * triples in the asserted graph, but without the CS entity (which is added later by the * validator in the inferred graph). */ @Override - public boolean updateProposedStatus(Boolean status, ControlSetDB cs, String model){ + public boolean updateProposedStatus(Boolean status, String csURI, String model){ String graphUri = stack.getGraph(model); if (graphUri == null) { return false; @@ -3460,19 +3516,45 @@ public boolean updateProposedStatus(Boolean status, ControlSetDB cs, String mode Model datasetModel = dataset.getNamedModel(graphUri); // Encode the population level as a single property of the asset resource - Resource resource = datasetModel.getResource(getLongName(cs.getUri())); + Resource resource = datasetModel.getResource(getLongName(csURI)); Property property = ResourceFactory.createProperty(getLongName("core#isProposed")); // Now remove the old value and save the new value - dataset.begin(ReadWrite.WRITE); - resource.removeAll(property); - resource.addLiteral(property, status.booleanValue()); - dataset.commit(); - dataset.end(); + try { + dataset.begin(ReadWrite.WRITE); + resource.removeAll(property); + resource.addLiteral(property, status.booleanValue()); + dataset.commit(); + } + catch (Exception e) { + // Abort the changes and signal that there has been an error + dataset.abort(); + String message = String.format("Error occurred while updating control proposed status for CS %s", csURI); + logger.error(message, e); + throw new RuntimeException(message, e); + } + finally { + dataset.end(); + } + + if(cacheEnabled){ + // Make the same change in the cached object, if it exists + ControlSetDB cs = this.getControlSet(csURI, model); + if(cs != null) { + cs.setProposed(status); + this.store(cs, model); + } + + // Note that the calling process must change ControlSetDB objects for other graphs + } return true; } + @Override + public boolean updateProposedStatus(Boolean status, ControlSetDB cs, String model){ + return updateProposedStatus(status, cs.getUri(), model); + } /* Internal class passed to the Querier's GsonBuilder */ diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelUpdater.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelUpdater.java index 1bf92c97..23224bf5 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelUpdater.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelUpdater.java @@ -42,6 +42,7 @@ import uk.ac.soton.itinnovation.security.model.system.Relation; import uk.ac.soton.itinnovation.security.model.system.TrustworthinessAttributeSet; import uk.ac.soton.itinnovation.security.modelquerier.util.ModelStack; +import uk.ac.soton.itinnovation.security.modelquerier.util.QuerierUtils; import uk.ac.soton.itinnovation.security.semanticstore.AStoreWrapper; import uk.ac.soton.itinnovation.security.semanticstore.util.SparqlHelper; @@ -661,42 +662,6 @@ public Set updateControlSet(AStoreWrapper store, ControlSet cs) { return controlSets; } - private Set getExpandedControlSets(Set controlSets) { - Set expandedControlSets = new HashSet<>(); - - for (String cs : controlSets) { - Set expCs = getControlTriplet(cs); - expandedControlSets.addAll(expCs); - } - - return expandedControlSets; - } - - private Set getControlTriplet(String csuri) { - String[] uriFrags = csuri.split("#"); - String uriPrefix = uriFrags[0]; - String shortUri = uriFrags[1]; - - String [] shortUriFrags = shortUri.split("-"); - String control = shortUriFrags[0] + "-" + shortUriFrags[1]; - control = control.replace("_Min", "").replace("_Max", ""); - String assetId = shortUriFrags[2]; - - //logger.debug("control: {}", control); - //logger.debug("assetId: {}", assetId); - - String csAvg = uriPrefix + "#" + control + "-" + assetId; - String csMin = uriPrefix + "#" + control + "_Min" + "-" + assetId; - String csMax = uriPrefix + "#" + control + "_Max" + "-" + assetId; - - //logger.debug("csAvg: {}", csAvg); - //logger.debug("csMin: {}", csMin); - //logger.debug("csMax: {}", csMax); - - Set controlSets = new HashSet<>(Arrays.asList(csAvg, csMin, csMax)); - return controlSets; - } - /** * Toggle the proposed status of multiple control sets * @@ -713,7 +678,7 @@ public Set updateControlSets(AStoreWrapper store, Set controlSet throw new IllegalArgumentException("Controls cannot be work in progress but not proposed"); } - Set expandedControlSets = getExpandedControlSets(controlSets); + Set expandedControlSets = QuerierUtils.getExpandedControlSets(controlSets); for (String cs : expandedControlSets) { logger.debug("control set {}, proposed: {}", cs, proposed); diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ControlStrategyDB.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ControlStrategyDB.java index 09fb19ed..59be9563 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ControlStrategyDB.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ControlStrategyDB.java @@ -86,15 +86,39 @@ public ControlStrategyDB() { private String hasMin; // Pointer from CSG for an average likelihood Threat to the CSG for the lowest likelihood Threat private String hasMax; // Pointer from CSG for an average likelihood Threat to the CSG for the highest likelihood Threat + /** + * Returns true if this CSG is relevant in current risk calculations + */ public boolean isCurrentRisk() { // If the property doesn't exist, default to true return currentRisk != null ? currentRisk : true; } + public void setCurrentRisk(Boolean value){ + if(value == null || value) { + // If the property doesn't exist, it is equivalent to true + this.currentRisk = null; + } else { + // So it only needs to be stored if false + this.currentRisk = value; + } + } + /** + * Returns true if this CSG is relevant in future risk calculations + */ public boolean isFutureRisk() { // If the property doesn't exist, default to true return futureRisk != null ? futureRisk : true; } + public void setFutureRisk(Boolean value){ + if(value == null || value) { + // If the property doesn't exist, it is equivalent to true + this.currentRisk = null; + } else { + // So it only needs to be stored if false + this.currentRisk = value; + } + } public boolean isEnabled() { // If the property doesn't exist, default to false diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ThreatDB.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ThreatDB.java index 994c82d3..8b5e0cbb 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ThreatDB.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/dto/ThreatDB.java @@ -189,10 +189,13 @@ public boolean isCurrentRisk() { return currentRisk != null ? currentRisk : true; } public void setCurrentRisk(Boolean value){ - if(value == null || !value) + if(value == null || value) { + // If the property doesn't exist, it is equivalent to true this.currentRisk = null; - else + } else { + // So it only needs to be stored if false this.currentRisk = value; + } } /** @@ -203,10 +206,13 @@ public boolean isFutureRisk() { return futureRisk != null ? futureRisk : true; } public void setFutureRisk(Boolean value){ - if(value == null || !value) - this.futureRisk = null; - else - this.futureRisk = value; + if(value == null || value) { + // If the property doesn't exist, it is equivalent to true + this.currentRisk = null; + } else { + // So it only needs to be stored if false + this.currentRisk = value; + } } } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/util/QuerierUtils.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/util/QuerierUtils.java new file mode 100644 index 00000000..9ab94a31 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/util/QuerierUtils.java @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2024 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By : Ken Meacham +// Created Date : 18/01/2024 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.modelquerier.util; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class QuerierUtils { + + private QuerierUtils() { + throw new IllegalStateException("QuerierUtils is a Utility class"); + } + + /** + * Given a set of ControlSet URIs, return all related URIs (see getControlTriplet) + * @param controlSets set of ControlSet URIs + * @return expanded set of ControlSet URIs + */ + public static Set getExpandedControlSets(Set controlSets) { + Set expandedControlSets = new HashSet<>(); + + for (String cs : controlSets) { + Set expCs = getControlTriplet(cs); + expandedControlSets.addAll(expCs); + } + + return expandedControlSets; + } + + /** + * Given a ControlSet URI, return the set of related URIs: csAvg, csMin, csMax + * @param csuri ControlSet URI (could be avg, min or max) + * @return set of related URIs: csAvg, csMin, csMax + */ + public static Set getControlTriplet(String csuri) { + String[] uriFrags = csuri.split("#"); + String uriPrefix = uriFrags[0]; + String shortUri = uriFrags[1]; + + String [] shortUriFrags = shortUri.split("-"); + String control = shortUriFrags[0] + "-" + shortUriFrags[1]; + control = control.replace("_Min", "").replace("_Max", ""); + String assetId = shortUriFrags[2]; + + String csAvg = uriPrefix + "#" + control + "-" + assetId; + String csMin = uriPrefix + "#" + control + "_Min" + "-" + assetId; + String csMax = uriPrefix + "#" + control + "_Max" + "-" + assetId; + + return new HashSet<>(Arrays.asList(csAvg, csMin, csMax)); + } + + /** + * get domain Control URI given a CS URI + * @param CS URI + * @return control URI + */ + public static String getDomainControlUri(String csuri) { + Pattern pattern = Pattern.compile("system#CS-(.*?)-[0-9a-f]+"); + Matcher matcher = pattern.matcher(csuri); + + if (matcher.find()) { + String extractedPart = matcher.group(1); // "CS-DisabledProcess" + return "domain#" + extractedPart; + } + return ""; + } + +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/ModelValidator.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/ModelValidator.java index 07ea2966..d070dd85 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/ModelValidator.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/ModelValidator.java @@ -111,6 +111,7 @@ public RiskCalcResultsDB calculateRiskLevels(RiskCalculationMode mode, boolean s final long startTime = System.currentTimeMillis(); IQuerierDB querier = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model); + //TODO: check when this should be run, as it may also be done elseqhere querier.initForRiskCalculation(); RiskCalculator rc = new RiskCalculator(querier); boolean success = rc.calculateRiskLevels(mode, saveResults, progress); diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/Validator.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/Validator.java index ef6f63af..2ea9eaf0 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/Validator.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/Validator.java @@ -1065,7 +1065,9 @@ public void createThreats() { } else { systemThreatAvg.setFrequency(domainThreat.getFrequency()); systemThreatAvg.setSecondaryThreat(domainThreat.isSecondaryThreat()); - systemThreatAvg.setNormalOperation(domainThreat.isNormalOperation()); + systemThreatAvg.setNormalOperation(domainThreat.isNormalOperation()); + systemThreatAvg.setCurrentRisk(domainThreat.isCurrentRisk()); + systemThreatAvg.setFutureRisk(domainThreat.isFutureRisk()); } // Create the minimum likelihood threat, if the domain model has one and the system pattern is a non-singleton @@ -1090,7 +1092,9 @@ public void createThreats() { systemThreatMin.setFrequency(domainThreat.getFrequency()); systemThreatMin.setSecondaryThreat(domainThreat.isSecondaryThreat()); systemThreatMin.setNormalOperation(domainThreat.isNormalOperation()); - } + systemThreatMin.setCurrentRisk(domainThreat.isCurrentRisk()); + systemThreatMin.setFutureRisk(domainThreat.isFutureRisk()); + } systemThreatMin.setMinOf(systemThreatAvg.getUri()); systemThreatAvg.setHasMin(systemThreatMin.getUri()); } @@ -1116,7 +1120,9 @@ public void createThreats() { } else { systemThreatMax.setFrequency(domainThreat.getFrequency()); systemThreatMax.setSecondaryThreat(domainThreat.isSecondaryThreat()); - systemThreatMax.setNormalOperation(domainThreat.isNormalOperation()); + systemThreatMax.setNormalOperation(domainThreat.isNormalOperation()); + systemThreatMax.setCurrentRisk(domainThreat.isCurrentRisk()); + systemThreatMax.setFutureRisk(domainThreat.isFutureRisk()); } systemThreatMax.setMaxOf(systemThreatAvg.getUri()); systemThreatAvg.setHasMax(systemThreatMax.getUri()); @@ -1353,16 +1359,22 @@ public void createControlStrategies() { controlStrategyAvg = new ControlStrategyDB(); controlStrategyAvg.setParent(dcsg.getUri()); controlStrategyAvg.setDescription(generateDescription(dcsg.getDescription(), matchingPattern)); + controlStrategyAvg.setFutureRisk(dcsg.isFutureRisk()); + controlStrategyAvg.setCurrentRisk(dcsg.isCurrentRisk()); if(threatMax != null) { controlStrategyMax = new ControlStrategyDB(); controlStrategyMax.setParent(dcsg.getUri()); controlStrategyMax.setDescription(generateDescription(dcsg.getDescription(), matchingPattern)); + controlStrategyMax.setFutureRisk(dcsg.isFutureRisk()); + controlStrategyMax.setCurrentRisk(dcsg.isCurrentRisk()); } if(threatMin != null) { controlStrategyMin = new ControlStrategyDB(); controlStrategyMin.setParent(dcsg.getUri()); controlStrategyMin.setDescription(generateDescription(dcsg.getDescription(), matchingPattern)); - } + controlStrategyMin.setFutureRisk(dcsg.isFutureRisk()); + controlStrategyMin.setCurrentRisk(dcsg.isCurrentRisk()); + } // Assemble a complete list of domain CS to be found, with a deterministic ordering List allCS = new ArrayList<>(); diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackNode.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackNode.java index e0ca136f..aa7ca181 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackNode.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackNode.java @@ -41,6 +41,7 @@ import com.bpodgursky.jbool_expressions.Variable; public class AttackNode { + private static final Logger logger = LoggerFactory.getLogger(AttackNode.class); private AttackPathDataset apd; @@ -96,12 +97,26 @@ public class AttackNode { private static final String ATTACK_MITIGATION_CSG = "attack_mitigation_csg"; private class InnerResult { + Set loopbackNodeUris = new HashSet<>(); Set allCauseUris = new HashSet<>(); int minDistance = 0; int maxDistance = 0; Map data = new HashMap<>(); + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{{{"); + sb.append(loopbackNodeUris); + sb.append(", "); + sb.append(allCauseUris); + sb.append(", "); + sb.append("data:"); + sb.append(data); + sb.append("}}}"); + return sb.toString(); + } + public LogicalExpression getData(String key) { return this.data.get(key); } @@ -143,6 +158,13 @@ public int getMaxDistance() { } }; + /** + * Attack Node + * @param uri + * @param apd + * @param nodes + * @param id + */ public AttackNode(String uri, AttackPathDataset apd, AttackTree nodes, int id) { this.apd = apd; @@ -165,6 +187,10 @@ public AttackNode(String uri, AttackPathDataset apd, AttackTree nodes, int id) { this.uriSymbol = this.makeSymbol(uri); } + public LogicalExpression getAttackTreeMitigationCSG() { + return this.attackTreeMitigationCSG; + } + @Override public int hashCode() { return Objects.hash(this.uri); @@ -172,12 +198,19 @@ public int hashCode() { @Override public boolean equals(Object obj) { - AttackNode an = (AttackNode) obj; - if (this.uri.equals(an.getUri())) { + // Check for self-comparison + if (this == obj) { return true; - } else { + } + + // Use instanceof to check for null and ensure the correct type + if (!(obj instanceof AttackNode)) { return false; } + + AttackNode an = (AttackNode) obj; + + return java.util.Objects.equals(this.uri, an.getUri()); } public void setMaxDistanceFromTargetByTarget(String uri, int value) { @@ -205,7 +238,7 @@ public int getVisits() { } public String getVisitsStats() { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append(" Visits: " + this.visits); sb.append(" noCauseV: " + this.noCauseVisits); sb.append(" causeV: " + this.causeVisits); @@ -214,7 +247,7 @@ public String getVisitsStats() { } public String toString(String pad) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append(pad + " ID("); sb.append(this.id); sb.append(") --> "); @@ -226,7 +259,7 @@ public String toString(String pad) { } public String toString() { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append("\nNode ("); sb.append(this.id); sb.append(") URI: "); @@ -259,9 +292,7 @@ public LogicalExpression getControlStrategies() { csgSymbols.add(this.makeSymbol(csgUri)); } - LogicalExpression leCSG = new LogicalExpression(this.apd, csgSymbols, false); - - return leCSG; + return new LogicalExpression(csgSymbols, false); } public LogicalExpression getControls() { @@ -276,7 +307,6 @@ public LogicalExpression getControls() { * * So we will end up with something like: OR(AND(c1, c1), AND(c3), AND(c1, c4)) */ - Set csgUris = this.apd.getThreatControlStrategyUris(this.uri, this.nodes.getIsFutureRisk()); List leCSGs = new ArrayList<>(); @@ -287,9 +317,9 @@ public LogicalExpression getControls() { for (String csUri : csUris) { csSymbols.add(this.makeSymbol(csUri)); } - leCSGs.add(new LogicalExpression(this.apd, csSymbols, true)); + leCSGs.add(new LogicalExpression(csSymbols, true)); } - return new LogicalExpression(this.apd, new ArrayList(leCSGs), false); + return new LogicalExpression(new ArrayList(leCSGs), false); } public int getId() { @@ -324,18 +354,18 @@ public List getAllDirectCauseUris() { } private Expression makeSymbol(String uri) { - // TODO need to find equivalent of symbol->algebra.definition - return Variable.of(this.uri); + return Variable.of(uri); } /** * Performs a backtrace from the current AttackNode to its ancestors * - * @param cPath the current path to the AttackNode being traced + * @param cPath the current path to the AttackNode being traced * @param computeLogic compute the logical result of the backtrace * @return an object containing the results of the backtrace - * @throws TreeTraversalException if an error occurs during traversal of the AttackNode tree - * @throws Exception if an unexpected error occurs + * @throws TreeTraversalException if an error occurs during traversal of the + * AttackNode tree + * @throws Exception if an unexpected error occurs */ public InnerResult backtrace(Set cPath, boolean computeLogic) throws TreeTraversalException, Exception { @@ -346,9 +376,10 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre } currentPath.add(this.uri); - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " BACKTRACE for: " + this.uri.substring(7) + " (nodeID:" + this.id + ") "+ - // " current path length: " + (currentPath.size()-1)); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " BACKTRACE for: " + this.uri.substring(7) + " (nodeID:" + this.id + ") " + + " current path length: " + (currentPath.size() - 1) + + " all direct cause uris: " + this.allDirectCauseUris.size()); this.visits += 1; @@ -368,8 +399,8 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre intersection.retainAll(currentPath); if (intersection.size() == result.getLoopbackNodeUris().size()) { this.cacheHitVisits += 1; - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " Cache hit, no cause"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Cache hit, no cause"); throw new TreeTraversalException(result.getLoopbackNodeUris()); } } @@ -399,8 +430,8 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre continue; } else { // then in this case there is more to explore - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " Cache hit: node can be cause, but more to explore"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Cache hit: node can be cause, but more to explore"); useCache = false; break; } @@ -408,8 +439,8 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre if (useCache) { this.cacheHitVisits += 1; - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " Cache hit, node can be caused, cache can be used"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Cache hit, node can be caused, cache can be used"); return res; } } @@ -443,23 +474,20 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre boolean outerSuccess = true; // need this for try->except->ELSE* python equivalent try { - this.allDirectCauseUris = this.getAllDirectCauseUris(); - if (this.allDirectCauseUris.isEmpty()) { - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " No direct causes"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " No direct causes"); // This will be top of tree misbehaviours (normal-op, external // cause). Not root causes as they have parents in normal-ops. // TODO: can this just move to the end of the function? - tmpMinDistanceFromRoot = -1; tmpMaxDistanceFromRoot = -1; - List tmpObjList = new ArrayList(); + List tmpObjList = new ArrayList<>(); tmpObjList.add(this.makeSymbol(this.uri)); - tmpRootCause = new LogicalExpression(this.apd, tmpObjList, true); + tmpRootCause = new LogicalExpression(tmpObjList, true); if (this.isThreat()) { String err = "There should not be a threat with no parents: " + this.uri; @@ -469,7 +497,6 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre logger.error(err); throw new Exception(err); // TODO: put error in exception and choose a better Exception class } else { - attackMitigatedByCS = null; threatMitigatedByCS = null; attackMitigatedByCSG = null; @@ -482,25 +509,25 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre Set intersection = new HashSet<>(this.allDirectCauseUris); intersection.retainAll(currentPath); - if (intersection.size() > 0) { + if (!intersection.isEmpty()) { // For a threat we require all parents. // If even one is on the current path then the threat is triggered by its own consequence which is useless. List consequence = new ArrayList<>(); for (String item : intersection) { consequence.add(item.substring(7)); } - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " threat is dependent on its own consequence: " + consequence); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " threat is dependent on its own consequence: " + consequence); throw new TreeTraversalException(intersection); } List sortedCauses = new ArrayList<>(this.allDirectCauseUris); Collections.sort(sortedCauses); - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " " + sortedCauses.size() + " direct causes of threat"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " " + sortedCauses.size() + " direct causes of threat"); - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " └─>" + sortedCauses); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " └─>" + sortedCauses); for (String parentUri : sortedCauses) { AttackNode parent = this.nodes.getOrCreateNode(parentUri); @@ -522,12 +549,10 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre // We could collect all the p_results from the try // block and then iterate through them instead of // executing immediately. - validParentUris.add(parentUri); loopbackNodeUris.addAll(pResult.getLoopbackNodeUris()); allCauseUris.addAll(pResult.getAllCauseUris()); - // if (this.isNormalOp() == parent.isNormalOp()) { if (Objects.equals(this.isNormalOp(), parent.isNormalOp()) && !parent.isExternalCause()) { parentMinDistancesFromRoot.add(pResult.getMinDistance()); parentMaxDistancesFromRoot.add(pResult.getMaxDistance()); @@ -539,17 +564,14 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre parentThreatMitigationsCSG.add(pResult.getData(THREAT_MITIGATION_CSG)); // Entire path parentThreatTrees.add(pResult.getData(THREAT_TREE)); if (!parent.isNormalOp() && !parent.isExternalCause()) { - parentAttackMitigationsCS.add(pResult.getData(THREAT_MITIGATION_CS)); - parentAttackMitigationsCSG.add(pResult.getData(THREAT_MITIGATION_CSG)); + parentAttackMitigationsCS.add(pResult.getData(ATTACK_MITIGATION_CS)); + parentAttackMitigationsCSG.add(pResult.getData(ATTACK_MITIGATION_CSG)); parentAttackTrees.add(pResult.getData(ATTACK_TREE)); } } } } - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " Finished looking at threat causes (nodeID:" + this.id + ")"); - if (parentRootCauses.isEmpty()) { // then this is a root cause threat parentMinDistancesFromRoot = new ArrayList<>(); @@ -558,20 +580,23 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre parentMaxDistancesFromRoot = new ArrayList<>(); parentMaxDistancesFromRoot.add(-1); - List tmpObjList = new ArrayList(); + List tmpObjList = new ArrayList<>(); tmpObjList.add(this.makeSymbol(this.uri)); - parentRootCauses.add(new LogicalExpression(this.apd, tmpObjList, true)); + parentRootCauses.add(new LogicalExpression(tmpObjList, true)); } // The root cause of a threat is all (AND) of the rout // causes of its parents - tmpRootCause = new LogicalExpression(this.apd, parentRootCauses, true); + tmpRootCause = new LogicalExpression(parentRootCauses, true); // The distance from a root cause therefore is the maximum // of the parent distances +1 tmpMinDistanceFromRoot = Collections.max(parentMinDistancesFromRoot) + 1; tmpMaxDistanceFromRoot = Collections.max(parentMaxDistancesFromRoot) + 1; + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Finished looking at threat causes (nodeID:" + this.id + ")"); + if (computeLogic == true) { // The attack/threat tree is // AND( @@ -580,15 +605,15 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre // ) if (!this.isNormalOp()) { // if this threat (self) is on the attack path then - // it can inself bea mitigation on the attack_path + // it can inself be a mitigation on the attack_path parentAttackTrees.add(this.uriSymbol); } - bsAttackTree = new LogicalExpression(this.apd, parentAttackTrees, true); + bsAttackTree = new LogicalExpression(parentAttackTrees, true); // All threats are on the threat path parentThreatTrees.add(this.uriSymbol); - threatTree = new LogicalExpression(this.apd, parentThreatTrees, true); + threatTree = new LogicalExpression(parentThreatTrees, true); /* * A threat can be mitigated by OR( inactive control strategies located at itself mitigations of any of its parents ) @@ -605,13 +630,13 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre parentThreatMitigationsCS.add(this.controls); parentThreatMitigationsCSG.add(this.controlStrategies); - attackMitigatedByCS = new LogicalExpression(this.apd, + attackMitigatedByCS = new LogicalExpression( new ArrayList(parentAttackMitigationsCS), false); - threatMitigatedByCS = new LogicalExpression(this.apd, + threatMitigatedByCS = new LogicalExpression( new ArrayList(parentThreatMitigationsCS), false); - attackMitigatedByCSG = new LogicalExpression(this.apd, + attackMitigatedByCSG = new LogicalExpression( new ArrayList(parentAttackMitigationsCSG), false); - threatMitigatedByCSG = new LogicalExpression(this.apd, + threatMitigatedByCSG = new LogicalExpression( new ArrayList(parentThreatMitigationsCSG), false); } } else { @@ -623,11 +648,11 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre sortedCauses.removeAll(currentPath); Collections.sort(sortedCauses); - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " " + sortedCauses.size() + " direct causes of MS"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " " + sortedCauses.size() + " direct causes of MS"); - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " └─>" + sortedCauses); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " └─>" + sortedCauses); for (String parentUri : sortedCauses) { AttackNode parent = this.nodes.getOrCreateNode(parentUri); @@ -655,8 +680,8 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre parentThreatMitigationsCSG.add(pResult.getData(THREAT_MITIGATION_CSG)); // Entire path parentThreatTrees.add(pResult.getData(THREAT_TREE)); if (!parent.isNormalOp()) { - parentAttackMitigationsCS.add(pResult.getData(THREAT_MITIGATION_CS)); - parentAttackMitigationsCSG.add(pResult.getData(THREAT_MITIGATION_CSG)); + parentAttackMitigationsCS.add(pResult.getData(ATTACK_MITIGATION_CS)); + parentAttackMitigationsCSG.add(pResult.getData(ATTACK_MITIGATION_CSG)); parentAttackTrees.add(pResult.getData(ATTACK_TREE)); } } @@ -666,39 +691,38 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre if (validParentUris.isEmpty()) { // Then all parents have thrown exceptions or were on the // current path - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " misbehaviour with all parents invalid: " + this.uri + " (nodeID:" + this.id + ")"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " misbehaviour with all parents invalid: " + this.uri + " (nodeID:" + this.id + ")"); throw new TreeTraversalException(loopbackNodeUris); } // The rootCause of a misbehaviour is any (OR) of the root // cause of its parents - rootCause = new LogicalExpression(this.apd, parentRootCauses, false); + rootCause = new LogicalExpression(parentRootCauses, false); // The distance from a root cause is therefore the minimum of // the parent distances tmpMinDistanceFromRoot = Collections.min(parentMinDistancesFromRoot) + 1; tmpMaxDistanceFromRoot = Collections.min(parentMaxDistancesFromRoot) + 1; - // logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + - // " Finished looking at MS causes (nodeID:" + this.id + ") distance: " + - // tmpMinDistanceFromRoot + " " + tmpMaxDistanceFromRoot); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Finished looking at MS causes (nodeID:" + this.id + ") distance: " + + tmpMinDistanceFromRoot + " " + tmpMaxDistanceFromRoot); if (computeLogic) { - bsAttackTree = new LogicalExpression(this.apd, parentAttackTrees, false); - bsThreatTree = new LogicalExpression(this.apd, parentThreatTrees, false); - + bsAttackTree = new LogicalExpression(parentAttackTrees, false); + bsThreatTree = new LogicalExpression(parentThreatTrees, false); // Misbehaviours can be miticated by // AND( // mitigations of their parents // ) - attackMitigatedByCS = new LogicalExpression(this.apd, + attackMitigatedByCS = new LogicalExpression( new ArrayList(parentAttackMitigationsCS), true); - threatMitigatedByCS = new LogicalExpression(this.apd, + threatMitigatedByCS = new LogicalExpression( new ArrayList(parentThreatMitigationsCS), true); - attackMitigatedByCSG = new LogicalExpression(this.apd, + attackMitigatedByCSG = new LogicalExpression( new ArrayList(parentAttackMitigationsCSG), true); - threatMitigatedByCSG = new LogicalExpression(this.apd, + threatMitigatedByCSG = new LogicalExpression( new ArrayList(parentThreatMitigationsCSG), true); } } @@ -706,24 +730,23 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre } catch (TreeTraversalException error) { outerSuccess = false; - // logger.error(String.format("%1$"+ currentPath.size() +"s", "") + - // " Error " + this.uri + " (nodeID:" + this.id + ")"); - + //logger.debug(String.format("%1$"+ currentPath.size() +"s", "") + + // " Error " + this.uri + " (nodeID:" + this.id + ")"); loopbackNodeUris = error.getLoopbackNodeUris(); - Set loopbackNodeUrisOnPath = new HashSet(currentPath); + Set loopbackNodeUrisOnPath = new HashSet<>(currentPath); loopbackNodeUrisOnPath.retainAll(loopbackNodeUris); loopbackNodeUrisOnPath.remove(this.uri); InnerResult result = new InnerResult(); if (loopbackNodeUrisOnPath.isEmpty()) { this.cannotBeCaused = true; - // logger.error(String.format("%1$"+ currentPath.size() +"s", "") + - // " Error " + this.uri + " can never be caused (nodeID:" + this.id + ")"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Error " + this.uri + " can never be caused (nodeID:" + this.id + ")"); } else { result.setLoopbackNodeUris(loopbackNodeUrisOnPath); - // logger.error(String.format("%1$"+ currentPath.size() +"s", "") + - // " Error " + this.uri + " caused by node on path: (nodeID:" + this.id + ")"); + logger.debug(String.format("%1$" + currentPath.size() + "s", "") + + " Error " + this.uri + " caused by node on path: (nodeID:" + this.id + ")"); } this.noCauseResults.add(result); @@ -750,9 +773,8 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre * this node, but before that we need to merge the results with any others that have previously been found from other paths to this node. Interestingly, when * combining cause over different paths, the logic is reversed. */ - List tmpObjList = new ArrayList<>(Arrays.asList(this.rootCause, tmpRootCause)); - this.rootCause = new LogicalExpression(this.apd, tmpObjList, false); + this.rootCause = new LogicalExpression(tmpObjList, false); // Save the max and min distance from this root_cause // The max is useful to spread things out for display @@ -765,29 +787,28 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre // although tempting to calculate the distance from target here, we // can't because we don't know if the current tree is going to be // successful all the way back to the target. - if (computeLogic) { List aCsList = new ArrayList<>( Arrays.asList(this.attackTreeMitigationCS, attackMitigatedByCS)); - this.attackTreeMitigationCS = new LogicalExpression(this.apd, new ArrayList(aCsList), true); + this.attackTreeMitigationCS = new LogicalExpression(new ArrayList(aCsList), true); List tCsList = new ArrayList<>( Arrays.asList(this.threatTreeMitigationCS, threatMitigatedByCS)); - this.threatTreeMitigationCS = new LogicalExpression(this.apd, new ArrayList(tCsList), true); + this.threatTreeMitigationCS = new LogicalExpression(new ArrayList(tCsList), true); List aCsgList = new ArrayList<>( Arrays.asList(this.attackTreeMitigationCSG, attackMitigatedByCSG)); - this.attackTreeMitigationCSG = new LogicalExpression(this.apd, new ArrayList(aCsgList), true); + this.attackTreeMitigationCSG = new LogicalExpression(new ArrayList(aCsgList), true); List tCsgList = new ArrayList<>( Arrays.asList(this.threatTreeMitigationCSG, threatMitigatedByCSG)); - this.threatTreeMitigationCSG = new LogicalExpression(this.apd, new ArrayList(tCsgList), true); + this.threatTreeMitigationCSG = new LogicalExpression(new ArrayList(tCsgList), true); List atList = new ArrayList<>(Arrays.asList(this.attackTree, bsAttackTree)); - this.attackTree = new LogicalExpression(this.apd, new ArrayList(atList), false); + this.attackTree = new LogicalExpression(new ArrayList(atList), false); List ttList = new ArrayList<>(Arrays.asList(this.threatTree, bsThreatTree)); - this.threatTree = new LogicalExpression(this.apd, new ArrayList(ttList), false); + this.threatTree = new LogicalExpression(new ArrayList(ttList), false); } InnerResult iResult = new InnerResult(); @@ -804,7 +825,7 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre iResult.putData(THREAT_MITIGATION_CS, threatMitigatedByCS); iResult.putData(THREAT_MITIGATION_CSG, threatMitigatedByCSG); iResult.putData(ATTACK_TREE, bsAttackTree); - iResult.putData(ATTACK_TREE, bsThreatTree); + iResult.putData(THREAT_TREE, bsThreatTree); } this.causeResults.add(iResult); @@ -817,10 +838,18 @@ public InnerResult backtrace(Set cPath, boolean computeLogic) throws Tre return new InnerResult(); } + /** + * Get direct cause URIs + * @return + */ public Set getDirectCauseUris() { return this.directCauseUris; } + /** + * Add direct cause URIs to directCauseUris + * @param uris + */ public void addDirectCauseUris(Set uris) { this.directCauseUris.addAll(uris); for (String causeUri : uris) { diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathAlgorithm.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathAlgorithm.java index c274f5cc..6bbc6f17 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathAlgorithm.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathAlgorithm.java @@ -53,9 +53,7 @@ public AttackPathAlgorithm(IQuerierDB querier) { final long startTime = System.currentTimeMillis(); logger.debug("STARTING Shortest Path Attack algortithm ..."); - - // TODO might have to delay initialisation of the dataset until risk - // mode is checked. + apd = new AttackPathDataset(querier); final long endTime = System.currentTimeMillis(); @@ -90,38 +88,19 @@ public void checkRequestedRiskCalculationMode(String requestedRiskMode) { } public boolean checkTargetUris(List targetUris) { - boolean retVal = true; logger.debug("Checking submitted list of target URIs: {}", targetUris); - if (!apd.checkMisbehaviourList(targetUris)) { - logger.error("shortest path, target MS URI not valid"); - retVal = false; - } - return retVal; - } - - public AttackTree calculateAttack(List targetUris, boolean allPaths, boolean normalOperations) - throws RuntimeException { - - logger.debug("calculate attack tree with allPaths: {}, normalOperations: {}", allPaths, normalOperations); - logger.debug("target URIs: {}", targetUris); - - AttackTree attackTree; - - try { - final long startTime = System.currentTimeMillis(); - // calculate attack tree, allPath dictates one or two backtrace - // AttackTree is initialised with FUTURE risk mode enabled - attackTree = new AttackTree(targetUris, true, !allPaths, apd); - - final long endTime = System.currentTimeMillis(); - logger.info("AttackPathAlgorithm.calculateAttackTree: execution time {} ms", endTime - startTime); - - } catch (Exception e) { - throw new RuntimeException(e); + // Check if the list is null or empty + if (targetUris == null || targetUris.isEmpty()) { + logger.warn("The list of target URIs is null or empty."); + return false; } - return attackTree; + if (!apd.checkMisbehaviourList(targetUris)) { + logger.error("shortest path, target MS URI not valid"); + return false; + } + return true; } public TreeJsonDoc calculateAttackTreeDoc(List targetUris, String riskCalculationMode, boolean allPaths, diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathDataset.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathDataset.java index 3622b4f0..5fa6592a 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathDataset.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackPathDataset.java @@ -32,24 +32,36 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.soton.itinnovation.security.model.Level; import uk.ac.soton.itinnovation.security.model.system.RiskCalculationMode; +import uk.ac.soton.itinnovation.security.model.system.RiskVector; import uk.ac.soton.itinnovation.security.modelquerier.IQuerierDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.AssetDB; +import uk.ac.soton.itinnovation.security.modelquerier.dto.ControlDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.ControlSetDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.ControlStrategyDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.LevelDB; +import uk.ac.soton.itinnovation.security.modelquerier.dto.MisbehaviourDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.MisbehaviourSetDB; +import uk.ac.soton.itinnovation.security.modelquerier.dto.ModelDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.ThreatDB; import uk.ac.soton.itinnovation.security.modelquerier.dto.TrustworthinessAttributeSetDB; +import uk.ac.soton.itinnovation.security.modelquerier.util.QuerierUtils; +import uk.ac.soton.itinnovation.security.modelvalidator.Progress; +import uk.ac.soton.itinnovation.security.modelvalidator.RiskCalculator; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.AssetDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.ConsequenceDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.ControlDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.StateDTO; + public class AttackPathDataset { + private static final Logger logger = LoggerFactory.getLogger(AttackPathDataset.class); protected IQuerierDB querier; @@ -85,20 +97,30 @@ public AttackPathDataset(IQuerierDB querier) { // Save the querier reference for use in other methods this.querier = querier; - // Load domain model poulation, impact, trustworthiness, risk and likelihood scales as maps keyed on their URI + // Load domain model poulation, impact, trustworthiness, risk and likelihood scales as maps keyed on their short URI (e.g. "domain#RiskLevelMedium") poLevels = querier.getPopulationLevels(); imLevels = querier.getImpactLevels(); liLevels = querier.getLikelihoodLevels(); twLevels = querier.getTrustworthinessLevels(); riLevels = querier.getRiskLevels(); - // Load domain model impact, trustworthiness, risk, and likelihood scales as lists sorted by their level value + // Make a sorted list of the LevelDB objects by their risk level values riskLevels.addAll(riLevels.values()); riskLevels.sort(Comparator.comparingInt(LevelDB::getLevelValue)); // Load system model assets, matching patterns and nodes assets = querier.getAssets("system", "system-inf"); + updateDatasets(); + + final long endTime = System.currentTimeMillis(); + logger.info("AttackPathDataset.AttackPathDataset(IQuerierDB querier): execution time {} ms", + endTime - startTime); + + } + + private void updateDatasets() { + // Load system model trustworthiness attribute sets trustworthinessAttributeSets = querier.getTrustworthinessAttributeSets("system-inf"); @@ -114,12 +136,17 @@ public AttackPathDataset(IQuerierDB querier) { // Load system model control strategies and determine whether they are enabled controlStrategies = querier.getControlStrategies("system-inf"); - final long endTime = System.currentTimeMillis(); - logger.info("AttackPathDataset.AttackPathDataset(IQuerierDB querier): execution time {} ms", - endTime - startTime); + // Create likelihood maps + for (ThreatDB threat : threats.values()) { + likelihoods.put(threat.getUri(), threat.getPrior()); + } + for (MisbehaviourSetDB miss : misbehaviourSets.values()) { + likelihoods.put(miss.getUri(), miss.getPrior()); + } } + /* * Create maps required by the risk calculation to find TWAS, MS and their relationship to roles */ @@ -135,80 +162,30 @@ protected void createMaps() { likelihoods.put(miss.getUri(), miss.getPrior()); } + final long endTime = System.currentTimeMillis(); + logger.debug("*********CREATE MAPS*********"); logger.debug("AttackPathDataset threats: {}", threats.size()); logger.debug("AttackPathDataset MS: {}", misbehaviourSets.size()); logger.debug("AttackPathDataset likelihoods: {}", likelihoods.size()); logger.debug("*****************************"); - - final long endTime = System.currentTimeMillis(); logger.info("AttackPathDataset.CreateMaps(): execution time {} ms", endTime - startTime); } public boolean isFutureRisk(String input) { - RiskCalculationMode requestedMode; try { - requestedMode = RiskCalculationMode.valueOf(input); + RiskCalculationMode requestedMode = RiskCalculationMode.valueOf(input); return requestedMode == RiskCalculationMode.FUTURE; } catch (IllegalArgumentException e) { // TODO: throw an exception - logger.error("Found unexpected riskCalculationMode parameter value {}.", input); + logger.warn("Found unexpected riskCalculationMode parameter value {}.", input); return false; } } - public boolean calculateAttackPath() throws RuntimeException { - try { - createMaps(); - return true; - } catch (Exception e) { - logger.error("calculating attack path dataset failed", e); - throw new RuntimeException(e); - } - } - - private void printAttackPathDataset() { - logger.debug("*******************************************************"); - logger.debug("*******************************************************"); - logger.debug("Threat CSGs:"); - for (ThreatDB threat : threats.values()) { - int csgsSize = threat.getBlockedByCSG().size() + threat.getMitigatedByCSG().size(); - if (csgsSize > 0) { - logger.debug(" {}, blocked: {} mitigated: {}", threat.getUri(), threat.getBlockedByCSG().size(), - threat.getMitigatedByCSG().size()); - } - Collection csgsBlocked = threat.getBlockedByCSG(); - if (csgsBlocked.size() > 0) { - for (String csg : csgsBlocked) { - List css = this.controlStrategies.get(csg).getMandatoryCS(); - logger.debug(" CSG blocked: {}, cs: {}", csg, css.size()); - for (String cs : css) { - logger.debug(" cs: {}", cs); - } - } - } - } - - logger.debug("Control Strategies"); - for (ControlStrategyDB csg : controlStrategies.values()) { - logger.debug("CSG: {} cs: {}", csg.getUri(), csg.getMandatoryCS().size()); - for (String cs : csg.getMandatoryCS()) { - logger.debug(" cs: {}", cs); - } - } - - logger.debug("ContolSets:"); - for (ControlSetDB cs : controlSets.values()) { - logger.debug("ControlSet: {}, proposed {}", cs.getUri(), cs.isProposed()); - } - logger.debug("Misbehaviours"); - for (MisbehaviourSetDB ms : misbehaviourSets.values()) { - AssetDB asset = assets.get(ms.getLocatedAt()); - logger.debug(" MS {}, likelihood: {}, risk: {}, asset: {}", ms.getUri(), ms.getPrior(), ms.getRisk(), - asset.getLabel()); - } - logger.debug("*******************************************************"); - logger.debug("*******************************************************"); + public String getCSGDescription(String uri) { + ControlStrategyDB csg = controlStrategies.get(uri); + return csg.getDescription(); } public Map getLikelihoods() { @@ -225,13 +202,12 @@ public Set getNormalOps() { * @param uri * @return */ - // TODO MS will provide a direct call to get uris public List getMisbehaviourDirectCauseUris(String misbUri) throws RuntimeException { try { MisbehaviourSetDB ms = misbehaviourSets.get(misbUri); return new ArrayList<>(ms.getCausedBy()); } catch (Exception e) { - return new ArrayList(); + return new ArrayList<>(); } } @@ -246,65 +222,43 @@ public List getThreatDirectCauseUris(String threatUri) throws RuntimeExc ThreatDB threat = threats.get(threatUri); return new ArrayList<>(threat.getCausedBy()); } catch (Exception e) { - return new ArrayList(); + return new ArrayList<>(); } } /** - * check if CSG ends in -Runtime or -Implementation + * Check if a Control Strategy Group (CSG) is activated. * - * @param csgUri - * @return - */ - public boolean isCurrentRiskCSG(String csgUri) { - return this.checkImplementationRuntime(csgUri); - } - - private boolean checkImplementationRuntime(String csgUri) { - Pattern pattern = Pattern.compile("\\b-Implementation-Runtime\\b|\\b-Implementation\\b"); - Matcher matcher = pattern.matcher(csgUri); - if (matcher.find()) { - return true; - } else { - return false; - } - } - - /** - * check if CSG ends in -Implementation-Runtime or -Implementation + * This method evaluates whether all mandatory Control Sets (CS) associated + * with the given CSG are proposed. * - * @param csgUri - * @return + * @param csg The Control Strategy Group to be checked. + * @return {@code true} if all mandatory Control Sets are proposed, + * otherwise {@code false}. */ - public boolean isFutureRiskCSG(String csgUri) { - // TODO: REGEX is now changed!!! - return !(csgUri.endsWith("-Implementation-Runtime") || csgUri.endsWith("-Implementation")); + public boolean isCSGActivated(ControlStrategyDB csg) { + return csg.getMandatoryCS().stream().allMatch(cs -> controlSets.get(cs).isProposed()); } /** - * check if CSG has a contingency plan + * Check if control strategy plan exists and is activated need to have a + * different way checking for contingency plans * - * @param csgUri - * @return + * @param csg the control stragegy + * @return {@code true} if contingency plan exists and is activated, + * otherwise {@code false} */ - public boolean checkContingencyPlan(String csgUri) throws RuntimeException { + public boolean hasContingencyPlan(String csgUri) throws RuntimeException { try { String contingencyPlan; - if (this.checkImplementationRuntime(csgUri)) { + if (csgUri.contains("-Implementation")) { contingencyPlan = csgUri.replaceAll("-Implementation-Runtime|-Implementation", ""); } else { - return false; + return true; } if (controlStrategies.containsKey(contingencyPlan)) { - boolean activated = true; - for (String cs : controlStrategies.get(contingencyPlan).getMandatoryCS()) { - if (!controlSets.get(cs).isProposed()) { - activated = false; - break; - } - } - return activated; + return isCSGActivated(controlStrategies.get(contingencyPlan)); } return true; } catch (Exception e) { @@ -313,34 +267,54 @@ public boolean checkContingencyPlan(String csgUri) throws RuntimeException { } /** - * get threat CSGs + * return false when this CSG + * - has no effect in future risk calculations + * - has no effect in current risk calculations + * - cannot be changed at runtime + * @param csg + * @param future + * @return + */ + boolean considerCSG(ControlStrategyDB csg, boolean future) { + if (future) { + return csg.isFutureRisk(); + } else { + return csg.isCurrentRisk() && isRuntimeMalleable(csg); + } + } + + /** + * Check if CS is runtime malleable assume all -Implementation, + * -Implementation-Runtime CSGs have contingency plans activated. * - * @param threatUri - * @return + * @param csg + * @return boolean */ - public Set getThreatControlStrategyUris(String threatUri, boolean future) throws RuntimeException { - // Return list of control strategies (urirefs) that block a threat - // (uriref) + Boolean isRuntimeMalleable(ControlStrategyDB csg) { + if (csg.getUri().contains("-Implementation")) { + return true; + //return hasContingencyPlan(csg.getUri()); + } else if (csg.getUri().contains("-Runtime")) { + return true; + } + return false; + } - /* - * "blocks": means a CSG appropriate for current or future risk calc "mitigates": means a CSG appropriate for furture risk (often a contingency plan for a - * current risk CSG); excluded from likelihood calc in current risk - */ + public Set getThreatControlStrategyUris(String threatUri, boolean future) throws RuntimeException { + // Return list of control strategies (urirefs) that block a threat (uriref) - Set csgURIs = new HashSet(); Set csgToConsider = new HashSet<>(); ThreatDB threat = this.threats.get(threatUri); try { - csgURIs.addAll(threat.getBlockedByCSG()); - if (future) { - csgURIs.addAll(threat.getMitigatedByCSG()); - } - for (String csgURI : csgURIs) { + for (String csgURI : threat.getBlockedByCSG()) { ControlStrategyDB csg = querier.getControlStrategy(csgURI, "system-inf"); - if (csg.isCurrentRisk()) { + if (considerCSG(csg, future)) { csgToConsider.add(csgURI); + } else { + logger.debug("CSG {} is NOT considered", csgURI); } } + } catch (Exception e) { throw new RuntimeException(e); } @@ -373,9 +347,9 @@ public List getCsgControlSetsUris(String csgUri) throws RuntimeException */ public List getCsgControlSets(String csgUri) throws RuntimeException { try { - List csList = new ArrayList(); + List csList = new ArrayList<>(); for (String csUri : controlStrategies.get(csgUri).getMandatoryCS()) { - csList.add(controlSets.get(csgUri)); + csList.add(controlSets.get(csUri)); } return csList; } catch (Exception e) { @@ -389,24 +363,25 @@ public List getCsgControlSets(String csgUri) throws RuntimeExcepti * @param csgUri * @return */ - public List getCsgInactiveControlSets(String csgUri) throws RuntimeException { - + public List getCsgInactiveControlSets(String csgUri) { try { - List csList = new ArrayList<>(); - for (String csUri : this.controlStrategies.get(csgUri).getMandatoryCS()) { - // TODO needs revisiting, CS object should be accessed directly - for (ControlSetDB cs : controlSets.values()) { - if (cs.getUri().equals(csUri) && (!cs.isProposed())) { - csList.add(csUri); - } - } - } - return csList; + return controlSets.values().stream() + .filter(cs -> !cs.isProposed() && (isMandatoryCS(csgUri, cs) || isOptionalCS(csgUri, cs))) + .map(ControlSetDB::getUri) + .collect(Collectors.toList()); } catch (Exception e) { throw new RuntimeException(e); } } + private boolean isMandatoryCS(String csgUri, ControlSetDB cs) { + return controlStrategies.get(csgUri).getMandatoryCS().contains(cs.getUri()); + } + + private boolean isOptionalCS(String csgUri, ControlSetDB cs) { + return controlStrategies.get(csgUri).getOptionalCS().contains(cs.getUri()); + } + /** * get threat inactive CSGs * @@ -415,7 +390,7 @@ public List getCsgInactiveControlSets(String csgUri) throws RuntimeExcep */ public List getThreatInactiveCSGs(String threatUri, boolean future) throws RuntimeException { try { - List csgUriList = new ArrayList(); + List csgUriList = new ArrayList<>(); for (String csgUri : getThreatControlStrategyUris(threatUri, future)) { if (!getCsgInactiveControlSets(csgUri).isEmpty()) { csgUriList.add(csgUri); @@ -427,60 +402,38 @@ public List getThreatInactiveCSGs(String threatUri, boolean future) thro } } - // TODO filtering LevelValue should be a parameter - public List filterMisbehaviours() throws RuntimeException { - /* - * compare MS by risk then likelihood, and return MS with likelihood or risk >= MEDIUM - */ - - List msUris = new ArrayList<>(); - try { - logger.debug("filtering misbehaviour sets..."); - - List msSorted = new ArrayList<>(misbehaviourSets.values()); - - Comparator comparator = Comparator.comparing(MisbehaviourSetDB::getRisk) - .thenComparing(MisbehaviourSetDB::getPrior); + /** + * Return MS with risk level > acceptableRiskLevel + */ + public List filterMisbehavioursByRiskLevel(String acceptableRiskLevel) { - msSorted.sort(comparator); + List msUris = new ArrayList<>(); - List msFiltered = msSorted.stream() - .filter(ms -> riLevels.get(ms.getRisk()).getLevelValue() >= 3).collect(Collectors.toList()); + logger.debug("filtering misbehaviour sets..."); - for (MisbehaviourSetDB ms : msFiltered) { - AssetDB asset = assets.get(ms.getLocatedAt()); - logger.debug("filtered MS: {} \t-> risk {} prior {} at {}", ms.getUri().substring(7), - ms.getRisk().substring(7), ms.getPrior().substring(7), asset.getLabel()); + int acceptableThreshold = riLevels.get(acceptableRiskLevel).getLevelValue(); + for (MisbehaviourSetDB ms : misbehaviourSets.values()) { + if (riLevels.get(ms.getRisk()).getLevelValue() > acceptableThreshold) { msUris.add(ms.getUri()); } - - logger.debug("filtered MS sets size: {}/{}", msUris.size(), misbehaviourSets.size()); - - } catch (Exception e) { - logger.error("got an error filtering misbehaviours: {}", e.getMessage()); - throw new RuntimeException("got an error filtering misbehavours", e); } + logger.debug("filtered MS sets size: {}/{}", msUris.size(), misbehaviourSets.size()); + return msUris; } + public boolean isExternalCause(String uri) { - boolean retVal = false; - // TODO: no need to check MS for external causes any more? if (misbehaviourSets.containsKey(uri)) { MisbehaviourSetDB ms = querier.getMisbehaviourSet(uri, "system-inf"); - if (ms != null) { - retVal = ms.isExternalCause(); - } + return (ms != null) && ms.isExternalCause(); } else if (trustworthinessAttributeSets.containsKey(uri)) { TrustworthinessAttributeSetDB twa = trustworthinessAttributeSets.get(uri); - if (twa != null) { - retVal = twa.isExternalCause(); - } + return (twa != null) && twa.isExternalCause(); } - - return retVal; + return false; } /** @@ -490,67 +443,41 @@ public boolean isExternalCause(String uri) { * @rerutn boolean */ public boolean isNormalOp(String uri) { - boolean retVal = false; // check if we have to deal with a threat URI if (this.threats.containsKey(uri)) { ThreatDB threat = this.querier.getThreat(uri, "system-inf"); - if (threat != null) { - retVal = threat.isNormalOperation(); - } + return (threat != null) && threat.isNormalOperation(); } else if (misbehaviourSets.containsKey(uri)) { MisbehaviourSetDB ms = querier.getMisbehaviourSet(uri, "system-inf"); - if (ms != null) { - retVal = ms.isNormalOpEffect(); - } + return (ms != null) && ms.isNormalOpEffect(); } else if (trustworthinessAttributeSets.containsKey(uri)) { - retVal = false; + return false; } else { logger.warn("Not sure what is this: {}", uri); + return false; } - - return retVal; } // describes if the URI refers to an initial cause misbehaviour public boolean isInitialCause(String uri) { - if (this.threats.keySet().contains(uri)) { - return threats.get(uri).isInitialCause(); - } else { - return false; - } + return threats.containsKey(uri) && threats.get(uri).isInitialCause(); } public boolean isThreatSimple(String uri) { - if (this.threats.keySet().contains(uri)) { - return true; - } else { - return false; - } + return this.threats.keySet().contains(uri); } public boolean isMisbehaviourSet(String uri) { - if (this.misbehaviourSets.keySet().contains(uri)) { - return true; - } else { - return false; - } + return this.misbehaviourSets.keySet().contains(uri); } public boolean isTrustworthinessAttributeSets(String uri) { - if (this.trustworthinessAttributeSets.keySet().contains(uri)) { - return true; - } else { - return false; - } + return this.trustworthinessAttributeSets.keySet().contains(uri); } public boolean isThreat(String uri) { - if (this.threats.keySet().contains(uri)) { - return true; - } else { - return false; - } + return this.threats.keySet().contains(uri); } public ThreatDB getThreat(String uri) { @@ -564,10 +491,7 @@ public void printThreatUris() { } public boolean isSecondaryThreat(String uri) { - if (threats.keySet().contains(uri) && (threats.get(uri).getSecondaryEffectConditions().size() > 0)) { - return true; - } - return false; + return threats.containsKey(uri) && threats.get(uri).getSecondaryEffectConditions().size() > 0; } public boolean isRootCause(String uri) { @@ -587,29 +511,216 @@ public String getLikelihood(String uri) { return ""; } - // check MS list exists, no point going futher - public boolean checkMisbehaviourList(List misbehaviours) { - boolean retVal = true; + /** + * Check risk calculation mode is the same as the requested one + * @param input + * @return + */ + public boolean checkRiskCalculationMode(String input) { + ModelDB model = querier.getModelInfo("system"); + logger.info("Model info: {}", model); + + RiskCalculationMode modelRiskCalculationMode; + RiskCalculationMode requestedMode; + + try { + logger.info("riskCalculationMode: {}", model.getRiskCalculationMode()); + modelRiskCalculationMode = model.getRiskCalculationMode() != null ? RiskCalculationMode.valueOf(model.getRiskCalculationMode()) : null; + requestedMode = RiskCalculationMode.valueOf(input); + + return modelRiskCalculationMode == requestedMode; + + } catch (IllegalArgumentException e) { + return false; + } + } + + public boolean checkRiskLevelKey(String riskKey) { + return riLevels.containsKey(riskKey); + } - for (String misb : misbehaviours) { + /** Checks if all elements in the given list represent a valid misbehaviour + * set. + * + * This method iterates through the list of misbehaviour set identifiers + * and checks each one to determine if it corresponds to a valid + * misbehaviour set. + * + * @param misbehaviourSetList A list of misbehavour set short URIs as + * strings + * @return {@code true} if every identifier in the list corresponds to a valid + * misbehaviour set, otherwise {@code false}. + */ + public boolean checkMisbehaviourList(List misbehaviourSetList) { + + for (String misb : misbehaviourSetList) { if (!this.isMisbehaviourSet(misb)) { logger.warn("failed to identify MS: {}", misb); - retVal = false; - break; + return false; } } - return retVal; + return true; + } + + public AssetDTO fillAssetDTO(String assetUri) { + AssetDB asset = assets.get(assetUri); + AssetDTO assetDTO = new AssetDTO(); + assetDTO.setUri(asset.getUri()); + assetDTO.setType(asset.getType()); + assetDTO.setLabel(asset.getLabel()); + assetDTO.setIdentifier(asset.getId()); + return assetDTO; + } + + public ControlDTO fillControlDTO(String csUri) { + ControlDTO ctrl = new ControlDTO(); + ControlSetDB cs = controlSets.get(csUri); + ControlDB control = querier.getControl(cs.getControl(), "domain"); + + ctrl.setUri(csUri); + ctrl.setLabel(control.getLabel()); + ctrl.setDescription(control.getDescription()); + ctrl.setAsset(fillAssetDTO(cs.getLocatedAt())); + ctrl.setAction("Enable control"); + + return ctrl; + } + + public void changeCS(Set csSet, boolean proposed) { + logger.info("changeCS list ({} {}): {}", proposed ? "enabling" : "disabling", csSet.size(), csSet); + + for (String csURIa : csSet) { + + logger.debug(" └──> {}", csURIa); + + Set csTriplet = QuerierUtils.getControlTriplet(csURIa); + + for (String csURI : csTriplet) { + logger.debug(" Set triplet {}: proposed -> {}", csURI, proposed); + querier.updateProposedStatus(proposed, csURI, "system"); + } + + } + + } + + public RiskVector calculateRisk(String modelId, RiskCalculationMode riskMode) throws RuntimeException { + try { + + RiskCalculator rc = new RiskCalculator(querier); + rc.calculateRiskLevels(riskMode, false, new Progress(modelId)); + + updateDatasets(); + + return getRiskVector(); + } catch (Exception e) { + logger.error("Error calculating risks for APD", e); + throw new RuntimeException("Failed to calculate risk", e); + } + + } + + public RiskVector getRiskVector() { + + Map riskVector = new HashMap<>(); + Collection rvRiskLevels = new ArrayList<>(); + for (LevelDB level : riLevels.values()) { + riskVector.put(level.getUri(), 0); + Level l = new Level(); + l.setValue(Integer.valueOf(level.getLevelValue())); + l.setUri(level.getUri()); + rvRiskLevels.add(l); + } + + for (MisbehaviourSetDB ms : misbehaviourSets.values()) { + riskVector.put(ms.getRisk(), riskVector.get(ms.getRisk()) + 1); + } + + return new RiskVector(rvRiskLevels, riskVector); + } + + public String validateRiskLevel(String uri) { + return uri; } /** - * capitilise string - * - * @param str - * @return + * Compare two risk levels specified by URI fragments + */ + public int compareRiskLevelURIs(String overallRiskA, String overallRiskB) { + logger.debug("Overall Risk Comparison: riskA({}) ? riskB({})", overallRiskA, overallRiskB); + + int levelA = riLevels.get(overallRiskA).getLevelValue(); + int levelB = riLevels.get(overallRiskB).getLevelValue(); + + // Compare levelA and levelB and return -1, 0, or 1 + return Integer.compare(levelA, levelB); + } + + /* + * Compare the risk levels of a list of misbehaviour sets with another single level */ - private String capitaliseString(String str) { - return str.substring(0, 1).toUpperCase() + str.substring(1); + public int compareMSListRiskLevel(List targetMSURIs, String otherRiskURI) { + int targetRiskLevel = riLevels.get(otherRiskURI).getLevelValue(); + int maxRiskLevel = 0; + + for (String msURI : targetMSURIs) { + int riskLevel = riLevels.get(misbehaviourSets.get(msURI).getRisk()).getLevelValue(); + if (riskLevel > maxRiskLevel) { + maxRiskLevel = riskLevel; + } + } + + return Integer.compare(maxRiskLevel, targetRiskLevel); } + public StateDTO getState() { + // state is risk + list of consequences + + Map riskVector = new HashMap<>(); + Collection rvRiskLevels = new ArrayList<>(); + for (LevelDB level : riLevels.values()) { + riskVector.put(level.getUri(), 0); + Level l = new Level(); + l.setValue(Integer.valueOf(level.getLevelValue())); + l.setUri(level.getUri()); + rvRiskLevels.add(l); + } + + List consequences = new ArrayList<>(); + for (MisbehaviourSetDB ms : misbehaviourSets.values()) { + + riskVector.put(ms.getRisk(), riskVector.get(ms.getRisk()) + 1); + int threshold = riLevels.get("domain#RiskLevelMedium").getLevelValue(); + if (riLevels.get(ms.getRisk()).getLevelValue() >= threshold) { + MisbehaviourDB msdb = querier.getMisbehaviour(ms.getMisbehaviour(), "domain"); + ConsequenceDTO consequence = new ConsequenceDTO(); + consequence.setLabel(msdb.getLabel().replaceAll("(? getAllCS() { + Set css = new HashSet<>(); + for (ControlSetDB cs : controlSets.values()) { + css.add(cs.getUri()); + } + return css; + } } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackTree.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackTree.java index ec42477f..43ef5eea 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackTree.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/AttackTree.java @@ -29,6 +29,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -119,8 +120,10 @@ public AttackTree(List targetUris, boolean futureRisk, boolean shortestP this.backtrace(true); } else { /* - * If the shortest path is required then we get the URIRefs of the shortest path nodes from the first pass at the ThreatTree then discard all TreeNodes and - * create a new ThreatTree which is bounded by the shortest path URIRefs. + * If the shortest path is required then we get the URIRefs of the + * shortest path nodes from the first pass at the ThreatTree then + * discard all TreeNodes and create a new ThreatTree which is + * bounded by the shortest path URIRefs. */ logger.info("***********************"); logger.info("RUNNING FIRST backtrace"); @@ -128,7 +131,7 @@ public AttackTree(List targetUris, boolean futureRisk, boolean shortestP this.backtrace(false); - this.boundingUriRefs = new HashSet(); + this.boundingUriRefs = new HashSet<>(); for (AttackNode node : this.shortestPathNodes()) { this.boundingUriRefs.add(node.getUri()); } @@ -219,8 +222,6 @@ private Set shortestPathNodes() { * ones which have at least one child further away than the node, remove the others and iterate until no change. */ - // TODO: review this as it looks liek it's not quite working - Set spn = this.nodes().stream().collect(Collectors.toSet()); while (true) { Set goodNodes = new HashSet<>(); @@ -265,22 +266,10 @@ private Set nodes() { return filteredSet; } - /** - * Gets a list of all the AttackNodes in the AttackTree that are not in the error state, i.e. not not-a-cause. - * - * @return A list of all the AttackNodes in the AttackTree. - */ - private List excludedNodes() { - // Don't return the nodes that are the error state - List filteredList; - filteredList = this.nodeByUri.values().stream().filter(node -> node.getNotACause()) - .collect(Collectors.toList()); - return filteredList; - } private void addMaxDistanceFromTarget(String uriRef, List currentPath) { if (currentPath == null) { - currentPath = new ArrayList(); + currentPath = new ArrayList<>(); } List copyCP = new ArrayList<>(); @@ -314,25 +303,6 @@ private List uris() { return filteredList; } - private Set rootCauses() { - Set uriSet = new HashSet<>(); - for (AttackNode an : this.nodes()) { - if (an.isRootCause()) { - uriSet.add(an.getUri()); - } - } - return uriSet; - } - - private Set externalCauses() { - Set uriSet = new HashSet<>(); - for (AttackNode an : this.nodes()) { - if (an.isExternalCause()) { - uriSet.add(an.getUri()); - } - } - return uriSet; - } private Set normalOperations() { Set uriSet = new HashSet<>(); @@ -373,7 +343,7 @@ Set initialCauses() { private void followPath(String uri, List cPath, Map pathNodes) { if (cPath == null) { - cPath = new ArrayList(); + cPath = new ArrayList<>(); } cPath.add(uri); AttackNode cNode = this.nodeByUri.get(uri); @@ -413,7 +383,8 @@ public Graph createGraphDoc(Map fNodes, Set links) { List> treeLinks = new ArrayList<>(); // create nodes lists - for (String nodeUri : fNodes.keySet()) { + for (Iterator it = fNodes.keySet().iterator(); it.hasNext();) { + String nodeUri = it.next(); AttackNode node = this.nodeByUri.get(nodeUri); if (node.isThreat()) { threats.put(nodeUri, fNodes.get(nodeUri)); @@ -436,10 +407,8 @@ public Graph createGraphDoc(Map fNodes, Set links) { } } - Graph graph = new Graph(this.sortedMap(threats), this.sortedMap(misbehaviours), this.sortedMap(twas), + return new Graph(this.sortedMap(threats), this.sortedMap(misbehaviours), this.sortedMap(twas), treeLinks); - - return graph; } /** @@ -483,26 +452,17 @@ public TreeJsonDoc calculateTreeJsonDoc(boolean allPaths, boolean normalOp) { graphs.put(targetMS, graph); } - TreeJsonDoc treeJsonDoc = new TreeJsonDoc(graphs); - - return treeJsonDoc; - } - - private LogicalExpression attackMitigationCSG() { - List leList = new ArrayList<>(); - for (String uri : this.targetUris) { - leList.add(this.nodeByUri.get(uri).getControlStrategies()); - } - logger.debug("attackMitigationCSG LE size: {}", leList.size()); - return new LogicalExpression(this.apd, new ArrayList(leList), true); + return new TreeJsonDoc(graphs); } - private LogicalExpression attackMitigationCS() { + public LogicalExpression attackMitigationCSG() { List leList = new ArrayList<>(); for (String uri : this.targetUris) { - leList.add(this.nodeByUri.get(uri).getControls()); + leList.add(this.nodeByUri.get(uri).getAttackTreeMitigationCSG()); } - return new LogicalExpression(this.apd, new ArrayList(leList), true); + + logger.debug("attackMitigationCSG target uris: {}", this.targetUris); + return new LogicalExpression(new ArrayList(leList), true); } private Set createLinks(Set nodes) { @@ -534,9 +494,7 @@ private void setRank(String nodeUri, int rank) { this.rankByUri.put(nodeUri, ranks); } - if (ranks.contains(rank)) { - return; - } else { + if (!ranks.contains(rank)) { ranks.add(rank); for (String causeUri : this.nodeByUri.get(nodeUri).getDirectCauseUris()) { this.setRank(causeUri, rank + 1); @@ -571,6 +529,7 @@ public void stats() { logger.debug("CSGs...............: {}", csgs.size()); logger.debug("CS.................: {}", controls.size()); logger.info("#################################"); + } } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/CSGNode.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/CSGNode.java new file mode 100644 index 00000000..7a7050c9 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/CSGNode.java @@ -0,0 +1,90 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-07-25 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.modelvalidator.attackpath; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationDTO; + +public class CSGNode { + private List csgList; + private Set csList; + private List children; + private RecommendationDTO recommendation; + private int greaterEqualLess; + + public CSGNode() { + this(new ArrayList<>()); + } + + public CSGNode(List csgList) { + if (csgList == null) { + csgList = new ArrayList<>(); + } + this.csgList = csgList; + this.children = new ArrayList<>(); + this.recommendation = null; + this.csList = new HashSet<>(); + } + + public void addChild(CSGNode child) { + children.add(child); + } + + public Set getCsList() { + return this.csList; + } + + public List getCsgList() { + return this.csgList; + } + + public void setCsList(Set csList) { + this.csList = csList; + } + + public List getChildren() { + return this.children; + } + + public RecommendationDTO getRecommendation() { + return this.recommendation; + } + + public void setRecommendation(RecommendationDTO rec) { + this.recommendation = rec; + } + + public void setGreaterEqualLess(int val) { + greaterEqualLess = val; + } + + public int getGreaterEqualLess() { + return greaterEqualLess; + } +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/LogicalExpression.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/LogicalExpression.java index d0afb320..d49770b1 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/LogicalExpression.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/LogicalExpression.java @@ -36,35 +36,36 @@ import com.bpodgursky.jbool_expressions.And; import com.bpodgursky.jbool_expressions.Expression; import com.bpodgursky.jbool_expressions.Or; +import com.bpodgursky.jbool_expressions.Variable; import com.bpodgursky.jbool_expressions.rules.RuleSet; public class LogicalExpression { - // private static final Logger logger = LoggerFactory.getLogger(AttackNode.class); + private static final Logger logger = LoggerFactory.getLogger(LogicalExpression.class); + + private static int instanceCount = 0; // Static counter variable private boolean allRequired; private List> allCauses = new ArrayList<>(); private Expression cause; - public LogicalExpression(AttackPathDataset ds, List cList, boolean ar) { + public LogicalExpression(List cList, boolean ar) { + + instanceCount++; this.allRequired = ar; - List> allCausesAux = new ArrayList<>(); for (Object causeObj : cList) { if (causeObj instanceof LogicalExpression) { LogicalExpression leObj = (LogicalExpression) causeObj; - allCausesAux.add(leObj.getCause()); + if (leObj.getCause() != null) { + allCauses.add(leObj.getCause()); + } } else { Expression exprObj = (Expression) causeObj; - allCausesAux.add(exprObj); - } - } - - // all_causes = [cc for cc in all_causes if cc is not None] - for (Expression cc : allCausesAux) { - if (cc != null) { - allCauses.add(cc); + if (exprObj != null) { + allCauses.add(exprObj); + } } } @@ -81,12 +82,11 @@ public LogicalExpression(AttackPathDataset ds, List cList, boolean ar) { this.cause = RuleSet.simplify(ors); } } - } public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("{"); + sb.append("LE{{"); Set uris = this.uris(); Iterator it = uris.iterator(); while (it.hasNext()) { @@ -95,7 +95,7 @@ public String toString() { sb.append(", "); } } - sb.append("}"); + sb.append("}}"); return sb.toString(); } @@ -111,11 +111,17 @@ public Expression getCause() { } } + /** + * Apply DNF to logical expression + * @param maxComplexity + */ public void applyDNF(int maxComplexity) { // apply DNF if (this.cause == null) { return; } + //TODO throw an exception if complexity is too high + // and caclulate complexity correctly. int causeComplexity = this.cause.getChildren().size(); if (causeComplexity <= maxComplexity) { this.cause = RuleSet.toDNF(RuleSet.simplify(this.cause)); @@ -123,24 +129,106 @@ public void applyDNF(int maxComplexity) { } public Set uris() { - Set symbolSetUris = new HashSet(); + Set symbolSetUris = new HashSet<>(); if (this.cause != null) { for (Expression symbol : this.cause.getChildren()) { symbolSetUris.add(symbol.toString()); } if (symbolSetUris.isEmpty()) { + logger.debug("EMPTY URI"); symbolSetUris.add(this.cause.toString()); } } return symbolSetUris; } - public String getCsgComment(String dummyUri) { - if (!dummyUri.startsWith("system#")) { - dummyUri = "system#" + dummyUri; + + /** + * Get list of OR terms + * @return + */ + public List getListFromOr() { + List retVal = new ArrayList<>(); + if (this.cause == null) { + logger.warn("Logical Expression cause is none"); + } else if (this.cause instanceof Or) { + for (Expression expr : this.cause.getChildren()) { + retVal.add(expr); + } + } else if (this.cause instanceof And) { + logger.warn("Logical Expression cause is And when Or was expected"); + retVal.add(this.cause); + } else { + logger.warn("Logical Expression operator not supported: {}", this.cause); } - // MyControlStrategy myCSG = new MyControlStrategy("", "", ""); - return ""; + + return retVal; } + /** + * Extract AND terms from logical expression + * @param expression + * @return + */ + public static List getListFromAnd(Expression expression) { + List retVal = new ArrayList<>(); + + if (expression instanceof And) { + for (Object obj : expression.getChildren()) { + retVal.add((Variable)obj); + } + } else if (expression instanceof Variable) { + retVal.add((Variable)expression); + } else { + logger.warn("Logical Expression operator not supported: {}", expression); + } + return retVal; + } + + /** + * Display logical expression in terms of Variables + */ + public void displayExpression() { + logger.debug("CSG LogicalExpression has the following terms:"); + parse(this.cause, 0); + } + + /** + * Parse Expression terms + * @param expression + * @param depth + */ + private void parse(Expression expression, int depth) { + StringBuilder indent = new StringBuilder(); + for (int i = 0; i < depth; i++) { + indent.append(" "); + } + + if (expression instanceof And) { + // Handle the 'And' expression + And andExpression = (And) expression; + logger.debug("{} AND(#{}", indent.toString(), andExpression.getChildren().size()); + for (Expression subExpr : andExpression.getChildren()) { + parse(subExpr, depth + 1); // Recursive call + } + logger.debug("{} )", indent); + } else if (expression instanceof Or) { + // Handle the 'Or' expression + Or orExpression = (Or) expression; + logger.debug("{} OR(#{}", indent, orExpression.getChildren().size()); + for (Expression subExpr : orExpression.getChildren()) { + parse(subExpr, depth + 1); // Recursive call + } + logger.debug("{} )", indent); + } else if (expression instanceof Variable) { + // Handle the 'Variable' expression + Variable variableExpression = (Variable) expression; + // Display the variable, e.g., print it + logger.debug("{} {}", indent, variableExpression.getValue().substring(11)); + } else { + // Handle other types of expressions if any, we should not reach + // here!!! + logger.warn("LE PARSER: unkown expression {}", expression); + } + } } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithm.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithm.java new file mode 100644 index 00000000..1a58dcde --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithm.java @@ -0,0 +1,625 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-01-24 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.modelvalidator.attackpath; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import java.time.LocalDateTime; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.stereotype.Component; + +import uk.ac.soton.itinnovation.security.model.system.RiskCalculationMode; +import uk.ac.soton.itinnovation.security.model.system.RiskVector; +import uk.ac.soton.itinnovation.security.modelquerier.IQuerierDB; +import uk.ac.soton.itinnovation.security.modelquerier.dto.ModelDB; +import uk.ac.soton.itinnovation.security.modelvalidator.Progress; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.ControlDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.ControlStrategyDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationReportDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.StateDTO; + +import com.bpodgursky.jbool_expressions.Expression; +import com.bpodgursky.jbool_expressions.Variable; + +import uk.ac.soton.itinnovation.security.systemmodeller.mongodb.RecommendationRepository; +import uk.ac.soton.itinnovation.security.systemmodeller.attackpath.RecommendationsService.RecommendationJobState; +import uk.ac.soton.itinnovation.security.systemmodeller.model.RecommendationEntity; + +@Component +public class RecommendationsAlgorithm { + + private static final Logger logger = LoggerFactory.getLogger(RecommendationsAlgorithm.class); + + private AttackPathDataset apd; + private IQuerierDB querier; + private String modelId; + private int recCounter = 0; + private RecommendationReportDTO report; + private String riskMode = "CURRENT"; + private String acceptableRiskLevel; + private List targetMS; + private RiskVector initialRiskVector; + private boolean localSearch; + private boolean abortFlag = false; + private RecommendationRepository recRepository; + private String jobId; + private RecommendationJobState finalState; + + // allPaths flag for single or double backtrace + private boolean shortestPath = true; + + // used to implement timeout + private Integer maxSecs; + private long maxEndTime; + + public RecommendationsAlgorithm(RecommendationsAlgorithmConfig config, Integer maxSecs) { + this.querier = config.getQuerier(); + this.modelId = config.getModelId(); + this.riskMode = config.getRiskMode(); + this.acceptableRiskLevel = config.getAcceptableRiskLevel(); + this.targetMS = config.getTargetMS(); + this.report = new RecommendationReportDTO(); + this.localSearch = config.getLocalSearch(); + this.maxSecs = maxSecs; + + initializeAttackPathDataset(); + } + + private void initializeAttackPathDataset() { + logger.debug("Preparing datasets ..."); + + apd = new AttackPathDataset(querier); + } + + public void setRecRepository(RecommendationRepository recRepository, String job) { + this.recRepository = recRepository; + this.jobId = job; + } + + public void setAbortFlag() { + this.abortFlag = true; + } + + public RecommendationJobState getFinalState() { + return finalState; + } + + /** + * Check risk calculation mode is the same as the requested one + * @param input + * @return + */ + public boolean checkRiskCalculationMode(String input) { + ModelDB model = querier.getModelInfo("system"); + logger.info("Model info: {}", model); + + RiskCalculationMode modelRiskCalculationMode; + RiskCalculationMode requestedMode; + + try { + logger.info("riskCalculationMode: {}", model.getRiskCalculationMode()); + modelRiskCalculationMode = model.getRiskCalculationMode() != null ? RiskCalculationMode.valueOf(model.getRiskCalculationMode()) : null; + requestedMode = RiskCalculationMode.valueOf(input); + + return modelRiskCalculationMode == requestedMode; + + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * wrapper method for check existing risk calculation mode + * @param requestedRiskMode + */ + public void checkRequestedRiskCalculationMode(String requestedRiskMode) { + if (!checkRiskCalculationMode(requestedRiskMode)) { + logger.debug("mismatch between the stored risk calculation mode and the requested one"); + throw new RuntimeException("mismatch between the stored risk calculation mode and the requested one"); + } + } + + /** + * Calculate the attack tree + * @return the attack graph + */ + private AttackTree calculateAttackTree() { + if (!targetMS.isEmpty()) { + logger.debug("caclulate attack tree using MS list: {}", targetMS); + return calculateAttackTree(targetMS); + } else { + logger.debug("caclulate attack tree using acceptable risk level: {}", acceptableRiskLevel); + return calculateAttackTree(apd.filterMisbehavioursByRiskLevel(acceptableRiskLevel)); + } + } + + /** + * Calculate the attack tree + * @param targetUris + * @return the attack graph + * @throws RuntimeException + */ + private AttackTree calculateAttackTree(List targetUris) throws RuntimeException { + logger.debug("calculate attack tree with isFUTURE: {}, shortestPath: {}", riskMode, shortestPath); + logger.debug("target URIs: {}", targetUris); + + boolean isFutureRisk = apd.isFutureRisk(riskMode); + AttackTree attackTree = null; + + try { + final long startTime = System.currentTimeMillis(); + attackTree = new AttackTree(targetUris, isFutureRisk, shortestPath, apd); + attackTree.stats(); + final long endTime = System.currentTimeMillis(); + logger.info("AttackPathAlgorithm.calculateAttackTree: execution time {} ms", endTime - startTime); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return attackTree; + } + + private RiskVector processOption(List csgList, Set csSet, CSGNode childNode) { + + // Calculate risk, and create a potential recommendation + RiskVector riskResponse = null; + RecommendationDTO recommendation = null; + try { + riskResponse = apd.calculateRisk(modelId, RiskCalculationMode.valueOf(riskMode)); + logger.debug("Risk calculation response: {}", riskResponse); + logger.debug("Overall model risk: {}", riskResponse.getOverall()); + StateDTO state = apd.getState(); + + recommendation = createRecommendation(csgList, csSet, state); + + if (report.getRecommendations() == null) { + report.setRecommendations(new ArrayList<>()); + } + + // store recommendation to node + childNode.setRecommendation(recommendation); + + // flag this recommendation if there is risk reduction + childNode.setGreaterEqualLess(initialRiskVector.compareTo(riskResponse)); + + } catch (Exception e) { + logger.error("failed to get risk calculation, restore model"); + + // restore model ... + apd.changeCS(csSet, false); + + // raise exception since failed to run risk calculation + throw new RuntimeException(e); + } + + return riskResponse; + } + + private Set extractCS(List csgList) { + + Set csSet = new HashSet<>(); + for (String csg : csgList) { + for (String cs : apd.getCsgInactiveControlSets(csg)) { + csSet.add(cs); + } + } + + logger.debug("CS set for LE CSG_option {}", csgList); + logger.debug(" └──> {}", csSet); + + return csSet; + } + + private void updateJobState(RecommendationJobState newState) { + // get job status: + Optional optionalRec = recRepository.findById(jobId); + logger.debug("updating job status: {}", optionalRec); + optionalRec.ifPresent(rec -> { + rec.setState(newState); + rec.setModifiedAt(LocalDateTime.now()); + recRepository.save(rec); + }); + } + + private boolean checkJobAborted(){ + // get job status: + Optional jobState = recRepository.findById(jobId).map(RecommendationEntity::getState); + logger.debug("APPLY CSG: check task status: {}", jobState); + if (jobState.isPresent() && jobState.get() == RecommendationJobState.ABORTED) { + logger.debug("APPLY CSG: Got job status, cancelling this task"); + this.finalState = RecommendationJobState.ABORTED; + setAbortFlag(); + } + return abortFlag; + } + + private boolean checkJobTimedOut() { + boolean timedOut = System.currentTimeMillis() > this.maxEndTime; + if (timedOut) { + logger.warn("JOB TIMED OUT"); + this.finalState = RecommendationJobState.TIMED_OUT; + } + else { + logger.debug("JOB NOT YET TIMED OUT"); + } + return timedOut; + } + + private CSGNode applyCSGs(LogicalExpression le) { + CSGNode node = new CSGNode(); + return applyCSGs(le, node, "", apd.getRiskVector()); + } + + /** + * Build CSG recommendations tree. + * The method is recursive and will create a tree of CSG options. + * @param le + * @param myNode + * @param parentStep + * @param parentRiskVector + * @return + */ + private CSGNode applyCSGs(LogicalExpression le, CSGNode myNode, String parentStep, RiskVector parentRiskVector) { + logger.debug("applyCSGs() with parentStep: {}", parentStep); + + // convert LE to DNF + le.applyDNF(300); + + // convert from CSG logical expression to list of CSG options + List csgOptions = le.getListFromOr(); + + logger.debug("Derived DNF (OR) CSG expressions: {} (options).", csgOptions.size()); + for (Expression csgOption : csgOptions) { + logger.debug(" └──> {}", csgOption); + } + + // examine CSG options + int csgOptionCounter = 0; + for (Expression csgOption : csgOptions) { + + // avoid checking job state if jobId is not defined + if (jobId != null && !jobId.isEmpty()) { + // check if job is aborted or timed out: + if (checkJobAborted() || checkJobTimedOut()) { + break; + } else { + updateJobState(RecommendationJobState.RUNNING); + } + } + + csgOptionCounter += 1; + String myStep = String.format("%s%d/%d", parentStep.equals("") ? "" : parentStep + "-", csgOptionCounter, csgOptions.size()); + logger.debug("examining CSG LE option {}: {}", myStep, csgOption); + + List options = LogicalExpression.getListFromAnd(csgOption); + + List csgList = new ArrayList<>(); + + for (Variable va : options) { + csgList.add(va.toString()); + } + logger.debug("CSG flattened list ({}): {}", csgList.size(), csgList); + + CSGNode childNode = new CSGNode(csgList); + myNode.addChild(childNode); + + // get available CS + Set csSet = extractCS(csgList); + + // store CS set in the node to reconstruct the final CS list + // correctly in the Recommendation report for nested iterations. + childNode.setCsList(csSet); + + logger.debug("CS set for LE CSG_option {}", csgOption); + logger.debug(" └──> {}", csSet); + + // apply all CS in the CS_set + if (csSet.isEmpty()) { + logger.warn("EMPTY csSet is found, skipping this CSG option"); + continue; + } + apd.changeCS(csSet, true); + + // Re-calculate risk now and create a potential recommendation + RiskVector riskResponse = processOption(csgList, csSet, childNode); + + // Check for success + // Finish if the maximum risk is below or equal to the acceptable risk level + // If we are constrained to some target MS, then we should only check the + // risk levels of the targets (otherwise it is likely it will never finish) + + boolean globalRiskAcceptable = targetMS.isEmpty() && apd.compareRiskLevelURIs(riskResponse.getOverall(), acceptableRiskLevel) <= 0; + boolean targetedRiskAcceptable = !targetMS.isEmpty() && apd.compareMSListRiskLevel(targetMS, acceptableRiskLevel) <= 0; + + if (globalRiskAcceptable || targetedRiskAcceptable) { + logger.debug("Success termination condition reached for {}", myStep); + } else { + logger.debug("Risk is still higher than {}", acceptableRiskLevel); + + // Check if we should abort + // If doing localSearch then stop searching (fail) if the risk vector is higher than the parent + // In this way we do not let the risk vector increase. We could make this softer by comparing + // the "overall risk level", i.e. the highest risk level of the current and parent vector + + if (localSearch && (riskResponse.compareTo(parentRiskVector) > 0)) { + logger.debug("Risk level has increased. Abort branch {}", myStep); + } else { + + // Carry on searching by recursing into the next level + + logger.debug("Recalculate nested attack path tree"); + AttackTree nestedAttackTree = calculateAttackTree(); + LogicalExpression nestedLogicalExpression = nestedAttackTree.attackMitigationCSG(); + applyCSGs(nestedLogicalExpression, childNode, myStep, riskResponse); + } + } + + // undo CS changes in CS_set + logger.debug("Undo CS controls ({})", csSet.size()); + apd.changeCS(csSet, false); + logger.debug("Re-run risk calculation after CS changes have been revoked"); + // TODO: optimise this + // This does more work than is necessary as we are going to run the risk calculation again in the next iteration. + // The reason it is here is because of the side effect of the calculateRisk method which updates the various cached data. + apd.calculateRisk(modelId, RiskCalculationMode.valueOf(riskMode)); + + logger.debug("Finished examining CSG LE option {}: {}", myStep, csgOption); + } + + logger.debug("return from applyCSGs() iteration with parentStep: {}", parentStep); + + return myNode; + } + + /** + * Create control strategy DTO object + * @param csgUri + * @return + */ + private ControlStrategyDTO createControlStrategyDTO(String csgUri) { + ControlStrategyDTO csgDto = new ControlStrategyDTO(); + csgDto.setUri(csgUri); + csgDto.setDescription(apd.getCSGDescription(csgUri)); + csgDto.setCategory(csgUri.contains("-Runtime") ? "Applicable" : "Conditional"); + return csgDto; + } + + /** + * Crete CSG DTO object + * @param csgList + * @return + */ + private Set createCSGDTO(List csgList) { + Set recCSGSet = new HashSet<>(); + for (String csgUri : csgList) { + recCSGSet.add(createControlStrategyDTO(csgUri)); + } + return recCSGSet; + } + + /** + * create control DTO + * @param ctrlUri + * @return + */ + private ControlDTO createControlDTO(String ctrlUri) { + return apd.fillControlDTO(ctrlUri); + } + + /** + * Crete control set DTO + * @param csSet + * @return + */ + private Set createCSDTO(Set csSet) { + Set recControlSet = new HashSet<>(); + for (String ctrlUri : csSet) { + recControlSet.add(createControlDTO(ctrlUri)); + } + return recControlSet; + } + + /** + * Create recommendation DTO object + * @param csgList + * @param csSet + * @param state + * @return + */ + private RecommendationDTO createRecommendation(List csgList, Set csSet, StateDTO state) { + RecommendationDTO recommendation = new RecommendationDTO(); + recommendation.setIdentifier(recCounter++); + recommendation.setState(state); + logger.debug("Creating a potential recommendation ID: {}", recommendation.getIdentifier()); + + Set csgDTOs = createCSGDTO(csgList); + Set controlDTOs = createCSDTO(csSet); + + recommendation.setControlStrategies(csgDTOs); + recommendation.setControls(controlDTOs); + + // N.B. the list of CSG and CS will be updated later if the recommendation is nested. + return recommendation; + } + + /** + * Update recommendation DTO object + * @param recommendation + * @param csgList + * @param csSet + */ + private void updateRecommendation(RecommendationDTO recommendation, List csgList, Set csSet) { + Set csgDTOs = createCSGDTO(csgList); + Set controlDTOs = createCSDTO(csSet); + + recommendation.setControlStrategies(csgDTOs); + recommendation.setControls(controlDTOs); + } + + /** + * Parse the CSGNode tree and find recommendations + * @param node + */ + private void makeRecommendations(CSGNode node) { + List path = new ArrayList<>(); + makeRecommendations(node, path); + } + + /** + * Parse the CSGNode tree and find recommendations + * @param node + * @param path + */ + private void makeRecommendations(CSGNode node, List path) { + + // if path is undefined, initalise it as empty list + if (path == null) { + path = new ArrayList<>(); + } + + // Create a new instance of the path list for the current recursive call + List currentPath = new ArrayList<>(path); + currentPath.add(node); + + if (node.getChildren().isEmpty()) { + if ((node.getRecommendation() != null) & (node.getGreaterEqualLess() > 0)){ + Set csgSet = reconstructCSGs(currentPath); + Set csSet = reconstructCSs(currentPath); + updateRecommendation(node.getRecommendation(), new ArrayList<>(csgSet), csSet); + report.getRecommendations().add(node.getRecommendation()); + } else { + logger.debug("skipping recommendation: {}", node.getRecommendation()); + } + } else { + for (CSGNode child : node.getChildren()){ + makeRecommendations(child, currentPath); + } + } + } + + /** + * Reconstruct CSGs for nested recommendations + * @param nodeList + * @return + */ + private Set reconstructCSGs(List nodeList) { + Set csgSet = new HashSet<>(); + for (CSGNode node : nodeList) { + for (String csg : node.getCsgList()) { + csgSet.add(csg); + } + } + return csgSet; + } + + /** + * Reconstruct CS for nested recommendations + * @param nodeList + * @return + */ + private Set reconstructCSs(List nodeList) { + Set csSet = new HashSet<>(); + for (CSGNode node : nodeList) { + for (String cs : node.getCsList()) { + csSet.add(cs); + } + } + return csSet; + } + + /** + * Start recommendations algorithm + * @param progress + * @return + */ + public RecommendationReportDTO recommendations(Progress progress) { + + logger.info("Recommendations core part (risk mode: {})", riskMode); + logger.warn("Job timeout: {} secs", maxSecs); + + // Set start time for recommendations + long startTime = System.currentTimeMillis(); + + // Determine end time for recommendations (i.e. after which no further iterations will be completed) + if (maxSecs != null) { + this.maxEndTime = startTime + maxSecs * 1000; + } + else { + logger.warn("No recommendations.timeout.secs property set. Not setting timeout..."); + this.maxEndTime = Long.MAX_VALUE; + } + + try { + progress.updateProgress(0.1, "Getting initial risk state"); + // get initial risk state + initialRiskVector = apd.calculateRisk(modelId, RiskCalculationMode.valueOf(riskMode)); + + StateDTO state = apd.getState(); + report.setCurrent(state); + + progress.updateProgress(0.2, "Calculating attack tree"); + AttackTree threatTree = calculateAttackTree(); + + // step: attackMitigationCSG? + LogicalExpression attackMitigationCSG = threatTree.attackMitigationCSG(); + attackMitigationCSG.displayExpression(); + + // step: rootNode? + progress.updateProgress(0.3, "Trying different control strategy options"); + CSGNode rootNode = applyCSGs(attackMitigationCSG); + + // step: makeRecommendations on rootNode? + logger.debug("MAKE RECOMMENDATIONS"); + progress.updateProgress(0.8, "Making recommendations"); + makeRecommendations(rootNode); + + progress.updateProgress(0.9, "Preparing report"); + List recommendations = report.getRecommendations() != null ? report.getRecommendations() : Collections.emptyList(); + logger.info("The Recommendations Report has: {} recommendations", recommendations.size()); + for (RecommendationDTO rec : recommendations) { + logger.debug(" recommendation: {}", rec.getState().getRisk()); + for (ControlStrategyDTO csgDTO : rec.getControlStrategies()) { + logger.debug(" └──> csgs: {}", csgDTO.getUri().substring(7)); + } + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + + return report; + } + +} + diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithmConfig.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithmConfig.java new file mode 100644 index 00000000..a7c4b4da --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/RecommendationsAlgorithmConfig.java @@ -0,0 +1,100 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-07-25 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.modelvalidator.attackpath; + +import java.util.ArrayList; +import java.util.List; + +import uk.ac.soton.itinnovation.security.modelquerier.IQuerierDB; + +public class RecommendationsAlgorithmConfig { + private IQuerierDB querier; + private String modelId; + private String riskMode; + private String acceptableRiskLevel; + private List targetMS; + private boolean localSearch; + + public RecommendationsAlgorithmConfig(IQuerierDB querier, String modelId, String riskMode, boolean localSearch, String level, List targets) { + this.querier = querier; + this.modelId = modelId; + this.riskMode = riskMode; + this.acceptableRiskLevel = level; + if (targets == null) { + this.targetMS = new ArrayList<>(); + } else { + this.targetMS = targets; + } + this.localSearch = localSearch; + } + + public IQuerierDB getQuerier() { + return querier; + } + + public String getModelId() { + return modelId; + } + + public String getRiskMode() { + return riskMode; + } + + public void setQuerier(IQuerierDB querier) { + this.querier = querier; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + + public void setRiskMode(String riskMode) { + this.riskMode = riskMode; + } + + public String getAcceptableRiskLevel() { + return this.acceptableRiskLevel; + } + + public void setAcceptableRiskLevel(String level) { + acceptableRiskLevel = level; + } + + public List getTargetMS() { + return this.targetMS; + } + + public void setTargetMS(List targets) { + this.targetMS = targets; + } + + public Boolean getLocalSearch() { + return this.localSearch; + } + + public void setLocalSearch(Boolean flag) { + this.localSearch = flag; + } +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/RiskVectorDB.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/RiskVectorDB.java new file mode 100644 index 00000000..8b35eeaf --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/RiskVectorDB.java @@ -0,0 +1,122 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-09-01 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.modelvalidator.attackpath.dto; + +import java.util.List; +import java.util.Map; + +public class RiskVectorDB implements Comparable { + private int veryHigh; + private int high; + private int medium; + private int low; + private int veryLow; + + public RiskVectorDB() { + veryHigh = 0; + high = 0; + medium = 0; + low = 0; + veryLow = 0; + } + + public RiskVectorDB(int vHigh, int high, int med, int low, int vLow) { + this.veryHigh = vHigh; + this.high = high; + this.medium = med; + this.low = low; + this.veryLow = vLow; + } + + @Override + public int compareTo(RiskVectorDB other) { + if (this.equals(other)) { + return 0; + } else if (this.greaterThan(other)) { + return 1; + } else if (this.lessThan(other)) { + return -1; + } else { + // it should not happen? + return -2; + } + } + + + public boolean equals(RiskVectorDB other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + return low == other.low && + veryLow == other.veryLow && + medium == other.medium && + high == other.high && + veryHigh == other.veryHigh; + } + + public boolean greaterThan(RiskVectorDB other) { + if (veryHigh - other.veryHigh > 0) { + return true; + } else if (veryHigh - other.veryHigh < 0) { + return false; + } else if (high - other.high > 0) { + return true; + } else if (high - other.high < 0) { + return false; + } else if (medium - other.medium > 0) { + return true; + } else if (medium - other.medium < 0) { + return false; + } else if (low - other.low > 0) { + return true; + } else if (low - other.low < 0) { + return false; + } else { + return veryLow - other.veryLow > 0; + } + } + + public boolean lessThan(RiskVectorDB other) { + if (veryHigh - other.veryHigh < 0) { + return true; + } else if (veryHigh - other.veryHigh > 0) { + return false; + } else if (high - other.high < 0) { + return true; + } else if (high - other.high > 0) { + return false; + } else if (medium - other.medium < 0) { + return true; + } else if (medium - other.medium > 0) { + return false; + } else if (low - other.low < 0) { + return true; + } else if (low - other.low > 0) { + return false; + } else { + return veryLow - other.veryLow < 0; + } + } + +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/TreeJsonDoc.java b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/TreeJsonDoc.java index 3fefdd8e..7e6991d4 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/TreeJsonDoc.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelvalidator/attackpath/dto/TreeJsonDoc.java @@ -28,7 +28,7 @@ public class TreeJsonDoc { // TODO: should pass it as a parameter - private static final String uriPrefix = "http://it-innovation.soton.ac.uk/ontologies/trustworthiness/"; + private static final String URI_PREFIX = "http://it-innovation.soton.ac.uk/ontologies/trustworthiness/"; private Map graphs; public TreeJsonDoc(Map graphs) { @@ -36,7 +36,7 @@ public TreeJsonDoc(Map graphs) { } public String getUriPrefix() { - return uriPrefix; + return URI_PREFIX; } public Map getGraphs() { diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/SystemModellerApplication.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/SystemModellerApplication.java index 2e95627d..69ef0955 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/SystemModellerApplication.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/SystemModellerApplication.java @@ -34,11 +34,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; @Configuration @EnableAutoConfiguration @ComponentScan @SpringBootApplication +@EnableAsync public class SystemModellerApplication extends SpringBootServletInitializer{ @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/attackpath/RecommendationsService.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/attackpath/RecommendationsService.java new file mode 100644 index 00000000..b2f04abb --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/attackpath/RecommendationsService.java @@ -0,0 +1,158 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-01-24 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.attackpath; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.Optional; + +import uk.ac.soton.itinnovation.security.modelvalidator.Progress; +import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.RecommendationsAlgorithm; +import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.RecommendationsAlgorithmConfig; +import uk.ac.soton.itinnovation.security.systemmodeller.model.RecommendationEntity; +import uk.ac.soton.itinnovation.security.systemmodeller.mongodb.RecommendationRepository; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationReportDTO; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.RiskModeMismatchException; + +@Service +public class RecommendationsService { + + private static final Logger logger = LoggerFactory.getLogger(RecommendationsService.class); + + @Autowired + private RecommendationRepository recRepository; + + @Value("${recommendations.timeout.secs: 900}") + private Integer recommendationsTimeoutSecs; + + public void startRecommendationTask(String jobId, RecommendationsAlgorithmConfig config, Progress progress) { + + logger.debug("startRecommendationTask for {}", jobId); + logger.debug("recommendationsTimeoutSecs: {}", this.recommendationsTimeoutSecs); + + // create recEntry and save it to mongo db + RecommendationEntity recEntity = new RecommendationEntity(); + recEntity.setId(jobId); + recEntity.setModelId(config.getModelId()); + recEntity.setState(RecommendationJobState.STARTED); + recRepository.save(recEntity); + logger.debug("rec entity saved for {}", recEntity.getId()); + + try { + RecommendationsAlgorithm reca = new RecommendationsAlgorithm(config, recommendationsTimeoutSecs); + + if (!reca.checkRiskCalculationMode(config.getRiskMode())) { + throw new RiskModeMismatchException(); + } + + reca.setRecRepository(recRepository, jobId); + + RecommendationReportDTO report = reca.recommendations(progress); + + storeRecReport(jobId, report); + + RecommendationJobState finalState = reca.getFinalState() != null ? reca.getFinalState() : RecommendationJobState.FINISHED; + updateRecommendationJobState(jobId, finalState); + } catch (Exception e) { + updateRecommendationJobState(jobId, RecommendationJobState.FAILED); + } + } + + public void updateRecommendationJobState(String recId, RecommendationJobState newState, String msg) { + Optional optionalRec = recRepository.findById(recId); + optionalRec.ifPresent(rec -> { + rec.setState(newState); + rec.setMessage(msg); + rec.setModifiedAt(LocalDateTime.now()); + recRepository.save(rec); + }); + } + + public void updateRecommendationJobState(String recId, RecommendationJobState newState) { + Optional optionalRec = recRepository.findById(recId); + optionalRec.ifPresent(rec -> { + rec.setState(newState); + rec.setModifiedAt(LocalDateTime.now()); + recRepository.save(rec); + }); + } + + public void storeRecReport(String jobId, RecommendationReportDTO report) { + Optional optionalRec = recRepository.findById(jobId); + optionalRec.ifPresent(job -> { + job.setReport(report); + job.setState(RecommendationJobState.FINISHED); + job.setModifiedAt(LocalDateTime.now()); + recRepository.save(job); + }); + } + + public Optional getRecommendationJobState(String jobId) { + return recRepository.findById(jobId).map(RecommendationEntity::getState); + } + + public Optional getRecommendationJobMessage(String jobId) { + return recRepository.findById(jobId).map(RecommendationEntity::getMessage); + } + + + public Optional getRecReport(String jobId) { + return recRepository.findById(jobId).map(RecommendationEntity::getReport); + } + + public Optional getJobById(String jobId) { + return recRepository.findById(jobId); + } + + // TODO: the happy path for a job should be: created -> started-> running + // -> finished. + // For cancelled jobs the sequence should be: created -> started -> running + // -> aborted -> finished? + // + // Somehow the recommendation report should indicate what has happened to + // the job if it was cancelled, because cancelled jobs should still produce + // results. + // + // Then use the RecommendationEntity to store additional information, eg + // number of recommendations found. + // + public enum RecommendationJobState { + CREATED, + STARTED, + RUNNING, + FAILED, + FINISHED, + ABORTED, + TIMED_OUT, + UNKNOWN + } + +} + diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/model/RecommendationEntity.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/model/RecommendationEntity.java new file mode 100644 index 00000000..9dcbe5c8 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/model/RecommendationEntity.java @@ -0,0 +1,66 @@ +package uk.ac.soton.itinnovation.security.systemmodeller.model; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +import uk.ac.soton.itinnovation.security.systemmodeller.attackpath.RecommendationsService.RecommendationJobState; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationReportDTO; + +@Document(collection = "recommendations") +public class RecommendationEntity { + + @Id + private String id; + private RecommendationReportDTO report; + private RecommendationJobState state; + private String message; + private String modelId; + private int validRec = 0; + private int totalReci = 0; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime modifiedAt; + + // getters and setters + public void setState(RecommendationJobState state) { + this.state = state; + } + public RecommendationJobState getState() { + return this.state; + } + public String getId() { + return this.id; + } + public void setId(String id) { + this.id = id; + } + public void setReport(RecommendationReportDTO report) { + this.report = report; + } + public RecommendationReportDTO getReport() { + return this.report; + } + public void setModifiedAt(LocalDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + } + public void setMessage(String msg) { + this.message = msg; + } + public String getMessage() { + return this.message; + } + public void setModelId(String modelId) { + this.modelId = modelId; + } + public String getModelId() { + return this.modelId; + } +} + diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/mongodb/RecommendationRepository.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/mongodb/RecommendationRepository.java new file mode 100644 index 00000000..c756d4f5 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/mongodb/RecommendationRepository.java @@ -0,0 +1,10 @@ +package uk.ac.soton.itinnovation.security.systemmodeller.mongodb; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import uk.ac.soton.itinnovation.security.systemmodeller.model.RecommendationEntity; + +public interface RecommendationRepository extends MongoRepository { + public RecommendationEntity findOneById(String id); +} + diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/AssetController.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/AssetController.java index f125614f..5114b422 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/AssetController.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/AssetController.java @@ -51,8 +51,6 @@ import uk.ac.soton.itinnovation.security.model.system.Asset; import uk.ac.soton.itinnovation.security.model.system.AssetGroup; -import uk.ac.soton.itinnovation.security.model.system.ComplianceSet; -import uk.ac.soton.itinnovation.security.model.system.ComplianceThreat; import uk.ac.soton.itinnovation.security.model.system.ControlSet; import uk.ac.soton.itinnovation.security.model.system.MetadataPair; import uk.ac.soton.itinnovation.security.model.system.Relation; @@ -63,7 +61,6 @@ import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.ControlsAndThreatsResponse; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.CreateAssetResponse; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.DeleteAssetResponse; -import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.DeleteRelationResponse; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.UpdateAsset; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.UpdateAssetCardinality; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.UpdateAssetResponse; diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/EntityController.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/EntityController.java index 7b012b78..eb0b7ed3 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/EntityController.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/EntityController.java @@ -92,8 +92,6 @@ public ResponseEntity getEntitySystemThreat(@PathVariable String model AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -134,8 +132,6 @@ public ResponseEntity> getEntitySystemThreats(@PathVariabl AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -174,8 +170,6 @@ public ResponseEntity getEntitySystemMisbehaviourSet(@PathVar AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -218,8 +212,6 @@ public ResponseEntity> getEntitySystemMisbehaviou AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -267,8 +259,6 @@ public ResponseEntity getEntitySystemControlStrategy(@PathVar AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -309,8 +299,6 @@ public ResponseEntity> getEntitySystemControlStra AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -349,8 +337,6 @@ public ResponseEntity getEntitySystemControlSet(@PathVariable Stri AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -391,8 +377,6 @@ public ResponseEntity> getEntitySystemControlSets(@Pat AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -431,8 +415,6 @@ public ResponseEntity getEntitySystemAsset(@PathVariable String modelId AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -472,8 +454,6 @@ public ResponseEntity> getEntitySystemAssets(@PathVariable AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -512,8 +492,6 @@ public ResponseEntity getEntitySystemTWAS(@PathVa AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -554,8 +532,6 @@ public ResponseEntity> getEntitySyste AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -593,8 +569,6 @@ public ResponseEntity getEntityDomainTWA(@PathVariab AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -634,8 +608,6 @@ public ResponseEntity> getEntityDomainTW AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -673,8 +645,6 @@ public ResponseEntity getEntityDomainControl(@PathVariable String mod AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -714,8 +684,6 @@ public ResponseEntity> getEntityDomainControls(@PathVaria AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -754,8 +722,6 @@ public ResponseEntity getEntityDomainMisbehaviour(@PathVariable AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -796,8 +762,6 @@ public ResponseEntity> getEntityDomainMisbehaviours( AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -836,8 +800,6 @@ public ResponseEntity getEntityDomainLevel(@PathVariable String modelId AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -898,8 +860,6 @@ public ResponseEntity> getEntityDomainLevels(@PathVariable AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/ModelController.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/ModelController.java index f35377b2..f99f9ee9 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/ModelController.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/ModelController.java @@ -32,16 +32,18 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.rmi.UnexpectedException; import java.text.SimpleDateFormat; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; @@ -54,8 +56,6 @@ import javax.naming.SizeLimitExceededException; import javax.servlet.http.HttpServletRequest; -//import org.apache.jena.query.Dataset; - import org.keycloak.representations.idm.UserRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,6 +67,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -87,6 +89,9 @@ import uk.ac.soton.itinnovation.security.modelvalidator.ModelValidator; import uk.ac.soton.itinnovation.security.modelvalidator.Progress; import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.AttackPathAlgorithm; +import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.AttackPathDataset; +import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.RecommendationsAlgorithm; +import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.RecommendationsAlgorithmConfig; import uk.ac.soton.itinnovation.security.modelvalidator.attackpath.dto.TreeJsonDoc; import uk.ac.soton.itinnovation.security.semanticstore.AStoreWrapper; import uk.ac.soton.itinnovation.security.semanticstore.IStoreWrapper; @@ -97,23 +102,31 @@ import uk.ac.soton.itinnovation.security.systemmodeller.model.ModelFactory; import uk.ac.soton.itinnovation.security.systemmodeller.model.WebKeyRole; import uk.ac.soton.itinnovation.security.systemmodeller.mongodb.IModelRepository; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.JobResponseDTO; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.LoadingProgress; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.LoadingProgressResponse; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.ModelDTO; import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.UpdateModelResponse; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations.RecommendationReportDTO; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.BadRequestErrorException; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.BadRiskModeException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.InternalServerErrorException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.MisbehaviourSetInvalidException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.ModelException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.ModelInvalidException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.NotAcceptableErrorException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.NotFoundErrorException; +import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.RiskModeMismatchException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.UnprocessableEntityException; import uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions.UserForbiddenFromDomainException; import uk.ac.soton.itinnovation.security.systemmodeller.semantics.ModelObjectsHelper; import uk.ac.soton.itinnovation.security.systemmodeller.semantics.StoreModelManager; import uk.ac.soton.itinnovation.security.systemmodeller.util.ReportGenerator; import uk.ac.soton.itinnovation.security.systemmodeller.util.SecureUrlHelper; +import uk.ac.soton.itinnovation.security.systemmodeller.model.RecommendationEntity; +import uk.ac.soton.itinnovation.security.systemmodeller.mongodb.RecommendationRepository; +import uk.ac.soton.itinnovation.security.systemmodeller.attackpath.RecommendationsService; +import uk.ac.soton.itinnovation.security.systemmodeller.attackpath.RecommendationsService.RecommendationJobState; /** * Includes all operations of the Model Controller Service. @@ -141,12 +154,23 @@ public class ModelController { @Autowired private SecureUrlHelper secureUrlHelper; + @Autowired + private RecommendationsService recommendationsService; + + @Autowired + private RecommendationRepository recRepository; + @Value("${admin-role}") public String adminRole; @Value("${knowledgebases.install.folder}") private String kbInstallFolder; + private static final String VALIDATION = "Validation"; + private static final String RISK_CALCULATION = "Risk calculation"; + private static final String RECOMMENDATIONS = "Recommendations"; + private static final String STARTING = "starting"; + /** * Take the user IDs of the model owner, editor and modifier and look up the current username for them */ @@ -246,6 +270,14 @@ private Set getModelObjectsForUser(UserRepresentation user){ return models; } + private void warnIsValidating(String modelId, String modelWebkey) { + logger.warn("Model {} is currently validating - ignoring request {}", modelId, modelWebkey); + } + + private void warnIsCalculatingRisks(String modelId, String modelWebkey) { + logger.warn("Model {} is already calculating risks - ignoring request {}", modelId, modelWebkey); + } + /** * Returns a list of models for the current user. * @@ -633,17 +665,17 @@ public ResponseEntity validateModel(@PathVariable String modelWriteId) t String modelId = model.getId(); if (model.isValidating()) { - logger.warn("Model {} is already validating - ignoring request {}", modelId, modelWriteId); + warnIsValidating(modelId, modelWriteId); return new ResponseEntity<>(HttpStatus.ACCEPTED); } if (model.isCalculatingRisks()) { - logger.warn("Model {} is already calculating risks - ignoring request {}", modelId, modelWriteId); + warnIsCalculatingRisks(modelId, modelWriteId); return new ResponseEntity<>(HttpStatus.ACCEPTED); } Progress validationProgress = modelObjectsHelper.getValidationProgressOfModel(model); - validationProgress.updateProgress(0d, "Validation starting"); + validationProgress.updateProgress(0d, VALIDATION + " " + STARTING); logger.debug("Marking as validating model [{}] {}", modelId, model.getName()); model.markAsValidating(); @@ -677,7 +709,7 @@ public ResponseEntity validateModel(@PathVariable String modelWriteId) t return true; }, 0, TimeUnit.SECONDS); - modelObjectsHelper.registerValidationExecution(modelId, future); + modelObjectsHelper.registerTaskExecution(modelId, future); return new ResponseEntity<>(HttpStatus.ACCEPTED); } @@ -721,12 +753,12 @@ public ResponseEntity calculateRisks(@PathVariable String modelWriteId, String modelId = model.getId(); if (model.isValidating()) { - logger.warn("Model {} is currently validating - ignoring calc risks request {}", modelId, modelWriteId); + warnIsValidating(modelId, modelWriteId); return new ResponseEntity<>(HttpStatus.ACCEPTED); } if (model.isCalculatingRisks()) { - logger.warn("Model {} is already calculating risks - ignoring request {}", modelId, modelWriteId); + warnIsCalculatingRisks(modelId, modelWriteId); return new ResponseEntity<>(HttpStatus.ACCEPTED); } @@ -742,10 +774,8 @@ public ResponseEntity calculateRisks(@PathVariable String modelWriteId, RiskCalculationMode.values()); } - Progress validationProgress = modelObjectsHelper.getValidationProgressOfModel(model); - validationProgress.updateProgress(0d, "Risk calculation starting"); - - logger.debug("Marking as calculating risks [{}] {}", modelId, model.getName()); + Progress validationProgress = modelObjectsHelper.getTaskProgressOfModel(RISK_CALCULATION, model); + validationProgress.updateProgress(0d, RISK_CALCULATION + " " + STARTING); model.markAsCalculatingRisks(rcMode, true); ScheduledFuture future = Executors.newScheduledThreadPool(1).schedule(() -> { @@ -768,7 +798,7 @@ public ResponseEntity calculateRisks(@PathVariable String modelWriteId, return true; }, 0, TimeUnit.SECONDS); - modelObjectsHelper.registerValidationExecution(modelId, future); + modelObjectsHelper.registerTaskExecution(modelId, future); return new ResponseEntity<>(HttpStatus.ACCEPTED); } @@ -818,19 +848,17 @@ public ResponseEntity calculateRisksBlocking(@PathVariable St String modelId = model.getId(); if (model.isValidating()) { - logger.warn("Model {} is currently validating - ignoring calc risks request {}", modelId, modelWriteId); - return ResponseEntity.status(HttpStatus.OK).body(new RiskCalcResultsDB()); //TODO: may need to improve this + warnIsValidating(modelId, modelWriteId); + return ResponseEntity.status(HttpStatus.OK).body(new RiskCalcResultsDB()); } if (model.isCalculatingRisks()) { - logger.warn("Model {} is already calculating risks - ignoring request {}", modelId, modelWriteId); - return ResponseEntity.status(HttpStatus.OK).body(new RiskCalcResultsDB()); //TODO: may need to improve this + warnIsCalculatingRisks(modelId, modelWriteId); + return ResponseEntity.status(HttpStatus.OK).body(new RiskCalcResultsDB()); } - validationProgress = modelObjectsHelper.getValidationProgressOfModel(model); - validationProgress.updateProgress(0d, "Risk calculation starting"); - - logger.debug("Marking as calculating risks [{}] {}", modelId, model.getName()); + validationProgress = modelObjectsHelper.getTaskProgressOfModel(RISK_CALCULATION, model); + validationProgress.updateProgress(0d, RISK_CALCULATION + " " + STARTING); model.markAsCalculatingRisks(rcMode, save); } //synchronized block @@ -1241,7 +1269,26 @@ public ResponseEntity getRiskCalcProgress(@PathVariable String modelId synchronized(this) { final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); - return ResponseEntity.status(HttpStatus.OK).body(modelObjectsHelper.getValidationProgressOfModel(model)); + return ResponseEntity.status(HttpStatus.OK).body(modelObjectsHelper.getTaskProgressOfModel(RISK_CALCULATION, model)); + } + } + + /** + * Get an update on the progress of the recommendations operation, given the ID of the model. + * + * @param modelId + * @return recommendations progress + * @throws java.rmi.UnexpectedException + */ + @GetMapping(value = "/models/{modelId}/recommendationsprogress") + public ResponseEntity getRecommendationsProgress(@PathVariable String modelId) throws UnexpectedException { + logger.info("Called REST method to GET recommendations progress for model {}", modelId); + + synchronized(this) { + final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); + Progress progress = modelObjectsHelper.getTaskProgressOfModel(RECOMMENDATIONS, model); + logger.info("{}", progress); + return ResponseEntity.status(HttpStatus.OK).body(progress); } } @@ -1332,10 +1379,7 @@ public ResponseEntity calculateThreatGraph( try { RiskCalculationMode.valueOf(riskMode); } catch (IllegalArgumentException e) { - logger.error("Found unexpected riskCalculationMode parameter value {}, valid values are: {}.", - riskMode, RiskCalculationMode.values()); - throw new BadRequestErrorException("Invalid 'riskMode' parameter value " + riskMode + - ", valid values are: " + Arrays.toString(RiskCalculationMode.values())); + throw new BadRiskModeException(riskMode); } final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); @@ -1343,8 +1387,6 @@ public ResponseEntity calculateThreatGraph( AStoreWrapper store = storeModelManager.getStore(); try { - logger.info("Initialising JenaQuerierDB"); - JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), model.getModelStack(), false); @@ -1360,8 +1402,7 @@ public ResponseEntity calculateThreatGraph( } if (!apa.checkRiskCalculationMode(riskMode)) { - logger.error("mismatch in risk calculation mode found"); - throw new BadRequestErrorException("mismatch between the stored and requested risk calculation mode, please run the risk calculation"); + throw new RiskModeMismatchException(); } TreeJsonDoc treeDoc = apa.calculateAttackTreeDoc(targetURIs, riskMode, allPaths, normalOperations); @@ -1372,7 +1413,6 @@ public ResponseEntity calculateThreatGraph( logger.error("Threat graph calculation failed due to invalid misbehaviour set", e); throw e; } catch (BadRequestErrorException e) { - logger.error("mismatch between the stored and requested risk calculation mode, please run the risk calculation"); throw e; } catch (Exception e) { logger.error("Threat path failed due to an error", e); @@ -1381,4 +1421,174 @@ public ResponseEntity calculateThreatGraph( } } + /* + * This REST method generates a recommendation report, as an asynchronous call. + * Results may be downloaded once this task has completed. + * + * @param modelId the String representation of the model object to seacrh + * @param riskMode optional string indicating the prefered risk calculation mode (defaults to CURRENT) + * @param localSearch optional flag indicating whether to use local search (defaults to true) + * @param acceptableRiskLevel string indicating the acceptable risk level using domain model URI + * @param targetURIs optional list of target misbehaviour sets + * @return ACCEPTED status and jobId for the background task + * @throws InternalServerErrorException if an error occurs during report generation + */ + @GetMapping(value = "/models/{modelId}/recommendations") + public ResponseEntity calculateRecommendations( + @PathVariable String modelId, + @RequestParam(defaultValue = "CURRENT") String riskMode, + @RequestParam(defaultValue = "true") boolean localSearch, + @RequestParam String acceptableRiskLevel, + @RequestParam (required = false) List targetURIs) { + + // Check if targetURIs is null or empty and assign an empty list if it is + if (targetURIs == null) { + targetURIs = new ArrayList<>(); + } + + final List finalTargetURIs = targetURIs; + + logger.info("Calculating recommendations for model {}", modelId); + riskMode = riskMode.replaceAll("[\n\r]", "_"); + logger.info(" riskMode: {}",riskMode); + + RiskCalculationMode rcMode; + + try { + rcMode = RiskCalculationMode.valueOf(riskMode); + } catch (IllegalArgumentException e) { + throw new BadRiskModeException(riskMode); + } + + final String rm = riskMode; + + final Model model; + Progress progress; + + synchronized(this) { + model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); + String mId = model.getId(); + + if (model.isValidating()) { + warnIsValidating(mId, modelId); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } + + if (model.isCalculatingRisks()) { + warnIsCalculatingRisks(mId, modelId); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } + + progress = modelObjectsHelper.getTaskProgressOfModel(RECOMMENDATIONS, model); + progress.updateProgress(0d, RECOMMENDATIONS + " " + STARTING); + model.markAsCalculatingRisks(rcMode, false); + } //synchronized block + + AStoreWrapper store = storeModelManager.getStore(); + + logger.info("Creating async job for {}", modelId); + String jobId = UUID.randomUUID().toString(); + logger.info("Submitting async job with id: {}", jobId); + + ScheduledFuture future = Executors.newScheduledThreadPool(1).schedule(() -> { + boolean success = false; + + try { + JenaQuerierDB querierDB = new JenaQuerierDB(((JenaTDBStoreWrapper) store).getDataset(), + model.getModelStack(), true); + + querierDB.initForRiskCalculation(); + + logger.info("Calculating recommendations"); + + AttackPathDataset apd = new AttackPathDataset(querierDB); + + // validate targetURIs (if set) + if (!apd.checkMisbehaviourList(finalTargetURIs)) { + logger.error("Invalid target URIs set"); + throw new MisbehaviourSetInvalidException("Invalid misbehaviour set"); + } + + // validate acceptable risk level + if (!apd.checkRiskLevelKey(acceptableRiskLevel)) { + logger.error("Invalid acceptableRiskLevel: {}", acceptableRiskLevel); + throw new MisbehaviourSetInvalidException("Invalid acceptableRiskLevel value"); + } + + RecommendationsAlgorithmConfig recaConfig = new RecommendationsAlgorithmConfig(querierDB, model.getId(), rm, localSearch, acceptableRiskLevel, finalTargetURIs); + recommendationsService.startRecommendationTask(jobId, recaConfig, progress); + + success = true; + } catch (BadRequestErrorException e) { + throw e; + } catch (Exception e) { + logger.error("Recommendations failed due to an error", e); + throw new InternalServerErrorException( + "Finding recommendations failed. Please contact support for further assistance."); + } finally { + //always reset the flags even if the risk calculation crashes + model.finishedCalculatingRisks(success, rcMode, false); + progress.updateProgress(1.0, "Recommendations complete"); + } + return true; + }, 0, TimeUnit.SECONDS); + + modelObjectsHelper.registerTaskExecution(model.getId(), future); + + // Build the Location URI for the job status + URI locationUri = URI.create("/models/" + modelId + "/recommendations/status/" + jobId); + + // Return 202 Accepted with a Location header + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(locationUri); + + JobResponseDTO response = new JobResponseDTO(jobId, "CREATED"); + + return ResponseEntity.accepted().headers(headers).body(response); + } + + @PostMapping("/models/{modelId}/recommendations/{jobId}/cancel") + public ResponseEntity cancelRecJob( + @PathVariable String modelId, @PathVariable String jobId) { + + logger.info("Got request to cancel recommendation task for model: {}, jobId: {}", modelId, jobId); + + synchronized(this) { + final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.WRITE); + Progress progress = modelObjectsHelper.getTaskProgressOfModel(RECOMMENDATIONS, model); + progress.setMessage("Cancelling"); + recommendationsService.updateRecommendationJobState(jobId, RecommendationJobState.ABORTED, "job cancelled"); + } + + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping("/models/{modelId}/recommendations/{jobId}/status") + public ResponseEntity checkRecJobStatus( + @PathVariable String modelId, @PathVariable String jobId) { + + logger.info("Got request for jobId {} status", jobId); + + Optional optionalState = recommendationsService.getRecommendationJobState(jobId); + String stateAsString = optionalState.map(state -> state.toString()).orElse("UNKNOWN"); + + Optional optionalMessage = recommendationsService.getRecommendationJobMessage(jobId); + String message = optionalMessage.map(msg -> msg.toString()).orElse(""); + + JobResponseDTO response = new JobResponseDTO(jobId, stateAsString, message); + + return ResponseEntity.ok().body(response); + } + + @GetMapping("/models/{modelId}/recommendations/{jobId}/result") + public ResponseEntity downloadRecommendationsReport( + @PathVariable String modelId, @PathVariable String jobId) { + + logger.debug("Got download request for jobId: {}", jobId); + + return recommendationsService.getRecReport(jobId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + } diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/JobResponseDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/JobResponseDTO.java new file mode 100644 index 00000000..c9072b7f --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/JobResponseDTO.java @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-12-23 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto; + +public class JobResponseDTO { + + private String jobId; + private String state; + private String message; + + public JobResponseDTO(String jobId, String stateName) { + this.jobId = jobId; + this.state = stateName; + this.message = ""; + } + + public JobResponseDTO(String jobId, String stateName, String msg) { + this.jobId = jobId; + this.state = stateName; + this.message = msg; + } + + public String getJobId() { return this.jobId; } + + public void setJobId(String jobid) { this.jobId = jobid; } + + public String getMessage() { return this.message; } + + public void setMessage(String msg) { this.message = msg; } + + public String getState() { return this.state; } + + public void setState(String stateName) { this.state = stateName; } + + +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AdditionalPropertyDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AdditionalPropertyDTO.java new file mode 100644 index 00000000..6c34861b --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AdditionalPropertyDTO.java @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import lombok.Data; + +@Data +public class AdditionalPropertyDTO { + private String key = ""; + private String value = ""; +} + diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AssetDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AssetDTO.java new file mode 100644 index 00000000..db4d2a40 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/AssetDTO.java @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import java.util.List; + +import lombok.Data; + +@Data +public class AssetDTO { + private String label; + private String type; + private String uri; + private String identifier; + private List additionalProperties; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ConsequenceDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ConsequenceDTO.java new file mode 100644 index 00000000..7541ed9c --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ConsequenceDTO.java @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import lombok.Data; + +@Data +public class ConsequenceDTO { + private AssetDTO asset; + private String label; + private String description; + private String impact; + private String likelihood; + private String risk; + private String uri; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlDTO.java new file mode 100644 index 00000000..c2be5ed9 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlDTO.java @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import lombok.Data; + +@Data +public class ControlDTO { + private String label; + private String description; + private String uri; + private AssetDTO asset; + private String action; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlStrategyDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlStrategyDTO.java new file mode 100644 index 00000000..ad8bdfb7 --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/ControlStrategyDTO.java @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import lombok.Data; + +@Data +public class ControlStrategyDTO { + private String uri; + private String description; + private String category; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationDTO.java new file mode 100644 index 00000000..9e5690bc --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationDTO.java @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import java.util.Set; + +import lombok.Data; + +@Data +public class RecommendationDTO { + private int identifier; + private Set controlStrategies; + private Set controls; + private StateDTO state; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationReportDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationReportDTO.java new file mode 100644 index 00000000..cbba6fef --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/RecommendationReportDTO.java @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import java.util.List; + +import lombok.Data; + +@Data +public class RecommendationReportDTO { + private StateDTO current; + private List recommendations; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/StateDTO.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/StateDTO.java new file mode 100644 index 00000000..68c3faff --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/dto/recommendations/StateDTO.java @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2023 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By: Panos Melas +// Created Date: 2023-11-14 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.dto.recommendations; + +import java.util.List; +import java.util.Map; + +import lombok.Data; + +@Data +public class StateDTO { + private Map risk; + private List consequences; +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/BadRiskModeException.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/BadRiskModeException.java new file mode 100644 index 00000000..b123d1aa --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/BadRiskModeException.java @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2024 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By : Ken Meacham +// Created Date : 05/02/2024 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions; + +import java.util.Arrays; + +import uk.ac.soton.itinnovation.security.model.system.RiskCalculationMode; + +/** + * BAD_REQUEST error indicating invalid riskMode + * Will present as an HTTP response. + */ + +public class BadRiskModeException extends BadRequestErrorException { + public BadRiskModeException(String riskMode) { + super(createMessage(riskMode)); + } + + private static String createMessage(String riskMode) { + return(String.format("Invalid 'riskMode' parameter value: %s. Valid values are: %s", + riskMode, Arrays.toString(RiskCalculationMode.values()))); + } +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/RiskModeMismatchException.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/RiskModeMismatchException.java new file mode 100644 index 00000000..98a760ae --- /dev/null +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/rest/exceptions/RiskModeMismatchException.java @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////// +// +// © University of Southampton IT Innovation Centre, 2024 +// +// Copyright in this software belongs to University of Southampton +// IT Innovation Centre of Gamma House, Enterprise Road, +// Chilworth Science Park, Southampton, SO16 7NS, UK. +// +// This software may not be used, sold, licensed, transferred, copied +// or reproduced in whole or in part in any manner or form or in or +// on any media by any person other than in accordance with the terms +// of the Licence Agreement supplied with the software, or otherwise +// without the prior written consent of the copyright owners. +// +// This software is distributed WITHOUT ANY WARRANTY, without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE, except where stated in the Licence Agreement supplied with +// the software. +// +// Created By : Ken Meacham +// Created Date : 05/02/2024 +// Created for Project : Cyberkit4SME +// +///////////////////////////////////////////////////////////////////////// +package uk.ac.soton.itinnovation.security.systemmodeller.rest.exceptions; + +/** + * BAD_REQUEST error indicating msimatch between stored and requested risk calculation modes + * Will present as an HTTP response. + */ + +public class RiskModeMismatchException extends BadRequestErrorException { + public RiskModeMismatchException() { + super("Mismatch between the stored and requested risk calculation mode, please run the risk calculation"); + } +} diff --git a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/semantics/ModelObjectsHelper.java b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/semantics/ModelObjectsHelper.java index fb4bdccd..f03a51ee 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/semantics/ModelObjectsHelper.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/systemmodeller/semantics/ModelObjectsHelper.java @@ -102,8 +102,8 @@ public class ModelObjectsHelper { private Map> modelAssetUris; private Map> modelThreats; - private Map modelValidationProgress; - private Map> validationFutures; + private Map taskProgress; + private Map> taskFutures; private HashMap> loadingFutures; private Map modelLoadingProgress; @@ -115,6 +115,11 @@ public class ModelObjectsHelper { private List defaultUserDomainModels; //TODO: persist in TDB instead + private static final String FAILED = "failed"; + private static final String COMPLETED = "completed"; + private static final String CANCELLED = "cancelled"; + + /** * Initialises this component. */ @@ -124,8 +129,8 @@ public void init() throws IOException { modelAssetIDs = new HashMap<>(); modelAssetUris = new HashMap<>(); modelThreats = new HashMap<>(); - modelValidationProgress = new HashMap<>(); - validationFutures = new HashMap<>(); + taskProgress = new HashMap<>(); + taskFutures = new HashMap<>(); loadingFutures = new HashMap<>(); modelLoadingProgress = new HashMap<>(); modelLocks = new HashMap<>(); @@ -325,22 +330,22 @@ public void deleteAssetFromCache(Asset asset, Model model) { assetIDs.remove(uri); assetUris.remove(id); } - - public boolean registerValidationExecution(String modelId, ScheduledFuture future) { - if (validationFutures.containsKey(modelId)){ - ScheduledFuture validationExecution = validationFutures.get(modelId); - if (validationExecution.isDone()) { - logger.debug("Clearing previous validation execution"); + public boolean registerTaskExecution(String modelId, ScheduledFuture future) { + + if (taskFutures.containsKey(modelId)){ + ScheduledFuture taskExecution = taskFutures.get(modelId); + if (taskExecution.isDone()) { + logger.debug("Clearing previous task execution"); //TODO: tidy up previous execution } else { - logger.warn("Validation execution already registered (still running)"); + logger.warn("Task execution already registered (still running)"); return false; } } - logger.debug("Registering validation execution for model: {}", modelId); - validationFutures.put(modelId, future); + logger.debug("Registering task execution for model: {}", modelId); + taskFutures.put(modelId, future); return true; } @@ -352,7 +357,7 @@ public boolean registerLoadingExecution(String modelId, ScheduledFuture futur logger.debug("Clearing previous loading execution"); //TODO: tidy up previous execution } else { - logger.warn("Validation execution already registered (still running)"); + logger.warn("Loading execution already registered (still running)"); return false; } } @@ -362,61 +367,76 @@ public boolean registerLoadingExecution(String modelId, ScheduledFuture futur return true; } - public Progress getValidationProgressOfModel(Model model){ + //Default method retuns validation progress + public Progress getValidationProgressOfModel(Model model) { + return getTaskProgressOfModel("Validation", model); + } + + public Progress getTaskProgressOfModel(String name, Model model) { String modelId = model.getId(); - Progress validationProgress; - if (modelValidationProgress.containsKey(modelId)){ - validationProgress = modelValidationProgress.get(modelId); - } else { - validationProgress = new Progress(modelId); - modelValidationProgress.put(modelId, validationProgress); - } - //logger.info("Validation progress status: {}", validationProgress.getStatus()); - + Progress progress = getOrCreateTaskProgress(modelId); + // No need to check execution if not yet running - if (! "running".equals(validationProgress.getStatus())) { - logger.info("Validation not running - not checking execution status"); - return validationProgress; + if (! "running".equals(progress.getStatus())) { + logger.info("{} not running - not checking execution status", name); + return progress; } - if (validationFutures.containsKey(modelId)) { - ScheduledFuture validationExecution = validationFutures.get(modelId); - if (validationExecution.isDone()) { - Object result; - - try { - result = validationExecution.get(); - logger.debug("Validation result: {}", result != null ? result.toString() : "null"); - if ( (result == null) || (result.equals(false)) ) { - validationProgress.updateProgress(1.0, "Validation failed", "failed", "Unknown error"); - } - else { - validationProgress.updateProgress(1.0, "Validation complete", "completed"); - } - } catch (InterruptedException ex) { - logger.error("Could not get validation progress", ex); - validationProgress.updateProgress(1.0, "Validation cancelled", "cancelled"); - } catch (ExecutionException ex) { - logger.error("Could not get validation progress", ex); - validationProgress.updateProgress(1.0, "Validation failed", "failed", ex.getMessage()); - } - - // Finally, remove the execution from the list - logger.debug("Unregistering validation execution for model: {}", modelId); - validationFutures.remove(modelId); - //KEM - don't remove the progress object here, as others requests still need access to this - //(e.g. another user may monitor validation progress) - //modelValidationProgress.remove(modelId); - } + if (taskFutures.containsKey(modelId)) { + ScheduledFuture taskExecution = taskFutures.get(modelId); + updateProgressWithTaskResult(taskExecution, name, modelId, progress); } else { - logger.warn("No registered execution for model validation: {}", modelId); + String lowerName = name.toLowerCase(); + logger.warn("No registered execution for model {}: {}", lowerName, modelId); } - //logger.info("Validation progress: {}", validationProgress); - return validationProgress; + return progress; } + + private Progress getOrCreateTaskProgress(String modelId) { + Progress progress; + + if (taskProgress.containsKey(modelId)){ + progress = taskProgress.get(modelId); + } else { + progress = new Progress(modelId); + taskProgress.put(modelId, progress); + } + + return progress; + } + + private void updateProgressWithTaskResult(ScheduledFuture taskExecution, String name, String modelId, Progress progress) { + if (taskExecution.isDone()) { + Object result; + + try { + result = taskExecution.get(); + logger.debug("{} result: {}", name, result != null ? result.toString() : "null"); + if ( (result == null) || (result.equals(false)) ) { + progress.updateProgress(1.0, name + " failed", FAILED, "Unknown error"); + } + else { + progress.updateProgress(1.0, name + " complete", COMPLETED); + } + } catch (InterruptedException ex) { + logger.error("Could not get task progress", ex); + progress.updateProgress(1.0, name + " cancelled", CANCELLED); + } catch (ExecutionException ex) { + logger.error("Could not get task progress", ex); + progress.updateProgress(1.0, name + " failed", FAILED, ex.getMessage()); + } + + // Finally, remove the execution from the list + logger.info("Unregistering task execution for model: {}", modelId); + taskFutures.remove(modelId); + //KEM - don't remove the progress object here, as others requests still need access to this + //(e.g. another user may monitor validation progress) + //modelValidationProgress.remove(modelId); + } +} public LoadingProgress createLoadingProgressOfModel(Model model, String loadingProgressID){ @@ -454,24 +474,24 @@ public LoadingProgress getLoadingProgressOfModel(String loadingProgressID){ result = loadingExecution.get(); logger.debug("Loading result: {}", result != null ? result.toString() : "null"); if ( (result == null) || (! (result instanceof Model)) ) { - loadingProgress.updateProgress(1.0, "Loading failed", "failed", "Unknown error", null); + loadingProgress.updateProgress(1.0, "Loading failed", FAILED, "Unknown error", null); } else { - loadingProgress.updateProgress(1.0, "Loading complete", "completed", "", (Model)result); + loadingProgress.updateProgress(1.0, "Loading complete", COMPLETED, "", (Model)result); } } catch (InterruptedException ex) { logger.error("Could not get loading progress", ex); - loadingProgress.updateProgress(1.0, "Loading cancelled", "cancelled"); + loadingProgress.updateProgress(1.0, "Loading cancelled", CANCELLED); } catch (ExecutionException ex) { Throwable cause = ex.getCause(); if (cause instanceof ModelException) { ModelException me = (ModelException)cause; logger.error("Model exception", me); - loadingProgress.updateProgress(1.0, "Model error", "failed", me.getMessage(), me.getModel()); + loadingProgress.updateProgress(1.0, "Model error", FAILED, me.getMessage(), me.getModel()); } else { logger.error("Could not get loading progress", ex); - loadingProgress.updateProgress(1.0, "Loading failed", "failed", ex.getMessage(), null); + loadingProgress.updateProgress(1.0, "Loading failed", FAILED, ex.getMessage(), null); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f092c31b..7d8e84d7 100755 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -101,3 +101,6 @@ admin-role=admin # Make SpringDoc format the JSON nicely at /system-modeller/v3/api-docs springdoc.writer-with-default-pretty-printer=true + +# Timeout (in secs) for recommendations calculation +recommendations.timeout.secs=900 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 6d4e159e..7c74b108 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -33,11 +33,14 @@ - + - - - + + + + + + diff --git a/src/main/webapp/app/common/constants.js b/src/main/webapp/app/common/constants.js index ab71632c..19934dd7 100644 --- a/src/main/webapp/app/common/constants.js +++ b/src/main/webapp/app/common/constants.js @@ -3,8 +3,11 @@ export const CTRL_TOOLTIP_DELAY = 1000; export const MAX_ASSET_NAME_LENGTH = 50; export const ASSET_MIN_CARDINALITY = 1; //default min cardinality for new asset export const ASSET_MAX_CARDINALITY = 1; //default max cardinality for new asset +export const URI_PREFIX = "http://it-innovation.soton.ac.uk/ontologies/trustworthiness/" export const ASSET_DEFAULT_POPULATION = "http://it-innovation.soton.ac.uk/ontologies/trustworthiness/domain#PopLevelSingleton" export const MODELLING_ERRORS_URI = "http://it-innovation.soton.ac.uk/ontologies/trustworthiness/domain#Anomalies"; +export const ACCEPTABLE_RISK_LEVEL = "domain#RiskLevelMedium"; +export const MAX_RECOMMENDATIONS = 10; //max number of recommendations to display export const MODEL_NAME_LIMIT = 50; export const MODEL_DESCRIPTION_LIMIT = 500; export const DISABLE_GROUPING = false; //disable add group feature on Canvas diff --git a/src/main/webapp/app/dashboard/components/modelItem/ModelItem.js b/src/main/webapp/app/dashboard/components/modelItem/ModelItem.js index cc61dd9d..75c2b2d7 100644 --- a/src/main/webapp/app/dashboard/components/modelItem/ModelItem.js +++ b/src/main/webapp/app/dashboard/components/modelItem/ModelItem.js @@ -380,6 +380,9 @@ class ModelItem extends Component { } formatRiskCalcMode(mode) { + console.log("mode:", mode); + if (!mode) + return "unknown" //Capitalise first char, lower case the rest return mode.charAt(0).toUpperCase() + mode.slice(1).toLowerCase(); } diff --git a/src/main/webapp/app/dashboard/components/modelList/ModelList.js b/src/main/webapp/app/dashboard/components/modelList/ModelList.js index 1bb44a98..ecde703d 100644 --- a/src/main/webapp/app/dashboard/components/modelList/ModelList.js +++ b/src/main/webapp/app/dashboard/components/modelList/ModelList.js @@ -11,12 +11,20 @@ class ModelList extends Component { render() { let {models, filter, dispatch, ontologies, user} = this.props; + + models.map(model => { + model.name = model.name ? model.name : "null"; + return model; + }); + let domainToFilter = [] + for (let i = 0; i < filter.domainModelFilters.length; i++) { if(filter.domainModelFilters[i].checked){ domainToFilter[i] = filter.domainModelFilters[i].name.toLowerCase(); } } + return (
diff --git a/src/main/webapp/app/dashboard/components/popups/EditModelModal.js b/src/main/webapp/app/dashboard/components/popups/EditModelModal.js index 659c5ec4..eb7f90eb 100644 --- a/src/main/webapp/app/dashboard/components/popups/EditModelModal.js +++ b/src/main/webapp/app/dashboard/components/popups/EditModelModal.js @@ -17,7 +17,7 @@ class EditModelModal extends Component { componentWillMount() { this.setState({ ...this.state, - draftName: this.props.model.name, + draftName: this.props.model.name ? this.props.model.name : "null", draftDescription: this.props.model.description ? this.props.model.description : "" }) } diff --git a/src/main/webapp/app/modeller/actions/ModellerActions.js b/src/main/webapp/app/modeller/actions/ModellerActions.js index 14770c07..34886532 100644 --- a/src/main/webapp/app/modeller/actions/ModellerActions.js +++ b/src/main/webapp/app/modeller/actions/ModellerActions.js @@ -1,4 +1,5 @@ import * as instr from "../modellerConstants"; +import * as Constants from "../../common/constants.js"; import {polyfill} from "es6-promise"; import {axiosInstance} from "../../common/rest/rest"; import {addAsset, addRelation} from "./InterActions" @@ -146,7 +147,6 @@ export function getAuthz(modelId, username) { export function pollForValidationProgress(modelId) { - //console.log("pollForValidationProgress"); return function (dispatch) { dispatch({ type: instr.UPDATE_VALIDATION_PROGRESS, @@ -160,7 +160,6 @@ export function pollForValidationProgress(modelId) { .then((response) => { dispatch({ type: instr.UPDATE_VALIDATION_PROGRESS, - //payload: response.data payload: { status: response.data["status"], progress: response.data["progress"], @@ -174,7 +173,6 @@ export function pollForValidationProgress(modelId) { } export function pollForRiskCalcProgress(modelId) { - //console.log("pollForRiskCalcProgress"); return function (dispatch) { dispatch({ type: instr.UPDATE_RISK_CALC_PROGRESS, @@ -188,7 +186,32 @@ export function pollForRiskCalcProgress(modelId) { .then((response) => { dispatch({ type: instr.UPDATE_RISK_CALC_PROGRESS, - //payload: response.data + payload: { + status: response.data["status"], + progress: response.data["progress"], + message: response.data["message"], + error: response.data["error"], + waitingForUpdate: false + } + }); + }); + }; +} + +export function pollForRecommendationsProgress(modelId) { + return function (dispatch) { + dispatch({ + type: instr.UPDATE_RECOMMENDATIONS_PROGRESS, + payload: { + waitingForUpdate: true, + } + }); + + axiosInstance + .get("/models/" + modelId + "/recommendationsprogress") + .then((response) => { + dispatch({ + type: instr.UPDATE_RECOMMENDATIONS_PROGRESS, payload: { status: response.data["status"], progress: response.data["progress"], @@ -421,6 +444,53 @@ export function riskCalcFailed(modelId) { }; } +export function recommendationsCompleted(modelId, jobId) { + console.log("recommendationsCompleted"); + return function (dispatch) { + dispatch({ + type: instr.IS_NOT_CALCULATING_RECOMMENDATIONS, + }); + axiosInstance + .get("/models/" + modelId + "/recommendations/" + jobId + "/result") + .then((response) => { + dispatch({ + type: instr.RECOMMENDATIONS_RESULTS, + payload: response.data + }); + dispatch({ + type: instr.OPEN_WINDOW, + payload: "recommendationsExplorer" + }); + }) + .catch((error) => { + console.log("Error:", error); + }); + }; +} + +export function recommendationsFailed(modelId) { + console.log("recommendationsFailed"); + return function (dispatch) { + dispatch({ + type: instr.RECOMMENDATIONS_FAILED + }); + }; +} + +export function abortRecommendations(modelId, jobId) { + console.log("abortRecommendations for model: ", modelId, jobId); + return function (dispatch) { + axiosInstance + .post("/models/" + modelId + "/recommendations/" + jobId + "/cancel") + .then((response) => { + console.log("Cancel sent"); + }) + .catch((error) => { + console.log("Error:", error); + }); + }; +} + export function loadingCompleted(modelId) { } @@ -1261,6 +1331,22 @@ export function closeControlStrategyExplorer() { }; } +export function openRecommendationsExplorer(csg, context) { + return function (dispatch) { + dispatch({ + type: instr.OPEN_RECOMMENDATIONS_EXPLORER, + }); + }; +} + +export function closeRecommendationsExplorer() { + return function (dispatch) { + dispatch({ + type: instr.CLOSE_RECOMMENDATIONS_EXPLORER + }); + }; +} + export function openReportDialog(reportType) { return function (dispatch) { dispatch({ @@ -1363,16 +1449,16 @@ export function revertControlCoverageOnAsset(modelId, assetId, controlSet) { //Reset controls (i.e. "Reset All" button) export function resetControls(modelId, controls) { - return updateControls(modelId, controls, false, true); + return updateControls(modelId, controls, false, false, true); } //Update multiple controls with new proposed value -export function updateControls(modelId, controls, proposed, controlsReset) { +export function updateControls(modelId, controls, proposed, workInProgress, controlsReset) { console.log("updateControls: controlsReset = " + controlsReset); let controlsUpdateRequest = { controls: controls, proposed: proposed, - workInProgress: false //this should be set to false, irrespective of whether proposed is true or false + workInProgress: workInProgress }; return function (dispatch) { @@ -1601,7 +1687,6 @@ export function getThreatGraph(modelId, riskMode, msUri) { payload: true }); let shortUri = msUri.split("#")[1]; - //let uri = '/models/' + modelId + "/threatgraph?longPath=true&normalOperations=false&targetURIs=system%23" + shortUri; let uri = '/models/' + modelId + "/threatgraph?riskMode=" + riskMode + "&allPath=false&normalOperations=false&targetURIs=system%23" + shortUri; axiosInstance.get(uri).then(response => { dispatch({ @@ -1622,7 +1707,6 @@ export function getThreatGraph(modelId, riskMode, msUri) { export function getShortestPathPlot(modelId, riskMode) { return function(dispatch) { - console.log("getShortestPathPlot with modelId: " + modelId); axiosInstance.get("/models/" + modelId + "/authz").then(response => { console.log("DATA: ", response.data.readUrl); let readUrl = response.data.readUrl; @@ -1636,6 +1720,72 @@ export function getShortestPathPlot(modelId, riskMode) { }; } +export function getRecommendationsBlocking(modelId, riskMode) { + return function(dispatch) { + dispatch({ + type: instr.IS_CALCULATING_RECOMMENDATIONS + }); + + axiosInstance + .get("/models/" + modelId + "/recommendations", {params: {riskMode: riskMode}}) + .then((response) => { + dispatch({ + type: instr.IS_NOT_CALCULATING_RECOMMENDATIONS, + }); + dispatch({ + type: instr.RECOMMENDATIONS_RESULTS, + payload: response.data + }); + dispatch({ + type: instr.OPEN_WINDOW, + payload: "recommendationsExplorer" + }); + }) + .catch((error) => { + console.log("Error:", error); + dispatch({ + type: instr.IS_NOT_CALCULATING_RECOMMENDATIONS //fix + }); + }); + }; +} + +export function getRecommendations(modelId, riskMode, acceptableRiskLevel, msUri, localSearch) { + console.log("Called getRecommendations"); + + let shortUri = msUri ? msUri.replace(Constants.URI_PREFIX, "") : null; + + console.log("riskMode = ", riskMode); + console.log("acceptableRiskLevel = ", acceptableRiskLevel); + console.log("msUri = ", msUri); + console.log("shortUri = ", shortUri); + console.log("localSearch = ", localSearch); + + return function(dispatch) { + dispatch({ + type: instr.IS_CALCULATING_RECOMMENDATIONS + }); + + axiosInstance + .get("/models/" + modelId + "/recommendations", {params: {riskMode: riskMode, + acceptableRiskLevel: acceptableRiskLevel, + targetURIs: shortUri, + localSearch: localSearch, + }}) + .then((response) => { + dispatch({ + type: instr.RECOMMENDATIONS_JOB_STARTED, + payload: response.data + }); + }) + .catch((error) => { + console.log("Error:", error); + dispatch({ + type: instr.IS_NOT_CALCULATING_RECOMMENDATIONS //fix + }); + }); + }; +} /* export function hoverThreat (show, threat) { diff --git a/src/main/webapp/app/modeller/actions/ViewActions.js b/src/main/webapp/app/modeller/actions/ViewActions.js index e2d82442..3d98abb7 100644 --- a/src/main/webapp/app/modeller/actions/ViewActions.js +++ b/src/main/webapp/app/modeller/actions/ViewActions.js @@ -1,8 +1,6 @@ import * as instr from "../modellerConstants"; - export function bringToFrontWindow(windowName) { - //console.log("bringToFrontWindow: ", windowName); return function (dispatch) { dispatch({ type: instr.OPEN_WINDOW, diff --git a/src/main/webapp/app/modeller/components/Modeller.js b/src/main/webapp/app/modeller/components/Modeller.js index 2d80d9f7..4ce6790e 100644 --- a/src/main/webapp/app/modeller/components/Modeller.js +++ b/src/main/webapp/app/modeller/components/Modeller.js @@ -7,6 +7,7 @@ import { closeComplianceExplorer, closeControlExplorer, closeControlStrategyExplorer, + closeRecommendationsExplorer, closeMisbehaviourExplorer, closeReportDialog, getModel, @@ -25,6 +26,7 @@ import RootCausesEditor from "./panes/details/popups/RootCausesEditor"; import ComplianceExplorer from "./panes/compliance/ComplianceExplorer"; import ControlExplorer from "./panes/controlExplorer/ControlExplorer"; import ControlStrategyExplorer from "./panes/csgExplorer/ControlStrategyExplorer"; +import RecommendationsExplorer from "./panes/recommendationsExplorer/RecommendationsExplorer"; import ControlPane from "./panes/controls/controlPane/ControlPane"; import OverviewPane from "./panes/controls/overviewPane/OverviewPane"; import Canvas from "./canvas/Canvas"; @@ -67,6 +69,7 @@ class Modeller extends React.Component { this.closeComplianceExplorer = this.closeComplianceExplorer.bind(this); this.closeControlExplorer = this.closeControlExplorer.bind(this); this.closeControlStrategyExplorer = this.closeControlStrategyExplorer.bind(this); + this.closeRecommendationsExplorer = this.closeRecommendationsExplorer.bind(this); this.closeReportDialog = this.closeReportDialog.bind(this); this.populateThreatMisbehaviours = this.populateThreatMisbehaviours.bind(this); this.getSystemThreats = this.getSystemThreats.bind(this); @@ -177,7 +180,6 @@ class Modeller extends React.Component { } let threats = this.getSystemThreats(); - //console.log("Modeller render: threats:", threats); let complianceSetsData = this.getComplianceSetsData(); let hasModellingErrors = this.getHasModellingErrors(); @@ -190,6 +192,8 @@ class Modeller extends React.Component { isValid={this.props.model.valid} validationProgress={this.props.validationProgress} hasModellingErrors={hasModellingErrors} isCalculatingRisks={this.props.model.calculatingRisks} + isCalculatingRecommendations={this.props.model.calculatingRecommendations} + recommendationsJobId={this.props.recommendationsJobId} isDroppingInferredGraph={this.props.isDroppingInferredGraph} isLoading={this.props.loading.model} loadingProgress={this.props.loadingProgress} dispatch={this.props.dispatch}/> @@ -246,15 +250,14 @@ class Modeller extends React.Component { @@ -275,6 +278,20 @@ class Modeller extends React.Component { authz={this.props.authz} /> + + { + if (e.stopPropagation) e.stopPropagation(); + if (e.preventDefault) e.preventDefault(); + e.cancelBubble = true; + e.returnValue = false; + }} + onDrag={(e) => { + if (e.stopPropagation) e.stopPropagation(); + if (e.preventDefault) e.preventDefault(); + e.cancelBubble = true; + e.returnValue = false; + }} + className={!this.props.show ? "hidden" : null}> +
+
{ + this.props.dispatch(bringToFrontWindow(this.props.windowName)); + }} + > +
+

+
+
+ {this.props.title} +
+
+

+ this.props.onHide()} + /> + openDocumentation(e, this.props.documentationLink)} + /> +
+
+ {this.props.renderContent()} +
+ + ); + } + + render() { + if (!this.props.show) { + return null; + } + + return (this.renderRnd()) + } +} + +Explorer.propTypes = { + title: PropTypes.string, + windowName: PropTypes.string, + documentationLink: PropTypes.string, + rndParams: PropTypes.object, + //isActive: PropTypes.bool, // is in front of other panels + show: PropTypes.bool, + onHide: PropTypes.func, + loading: PropTypes.object, + dispatch: PropTypes.func, + renderContent: PropTypes.func, + windowOrder: PropTypes.number, +}; + +export default Explorer; diff --git a/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlAccordion.js b/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlAccordion.js index ef0aaf71..80875905 100644 --- a/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlAccordion.js +++ b/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlAccordion.js @@ -284,7 +284,7 @@ class ControlAccordion extends React.Component { if (controlsToUpdate.length > 0) { this.updateControlsState(controlsToUpdate, flag); - this.props.dispatch(updateControls(this.props.model.id, controlsToUpdate, flag)); + this.props.dispatch(updateControls(this.props.model.id, controlsToUpdate, flag, false)); } else { //console.log("No controls to update"); diff --git a/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js b/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js index 41b126b2..01a6af00 100644 --- a/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js +++ b/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js @@ -1,157 +1,91 @@ import React from "react"; import PropTypes from 'prop-types'; import ControlAccordion from "./ControlAccordion"; -import {Rnd} from "../../../../../node_modules/react-rnd/lib/index"; -import {bringToFrontWindow, closeWindow} from "../../../actions/ViewActions"; import {connect} from "react-redux"; -import {openDocumentation} from "../../../../common/documentation/documentation" +import Explorer from "../common/Explorer" class ControlExplorer extends React.Component { constructor(props) { super(props); - this.rnd = null; + this.renderContent = this.renderContent.bind(this); } - shouldComponentUpdate(nextProps, nextState) { - let shouldComponentUpdate = true; - - if ((!this.props.show) && (!nextProps.show)) { - return false; - } - - if (nextProps.loading.model) { - return false; - } - - if (this.props.isActive != nextProps.isActive) { - return true; - } - - if(this.props.selectedAsset != nextProps.selectedAsset) { - return true; + render() { + if (!this.props.show) { + return null; } - return shouldComponentUpdate; + return ( + + ) } - render() { + renderContent() { const {model, hoverThreat, dispatch, loading, ...modalProps} = this.props; - var controlUri = this.props.selectedAsset.selectedControl; - var controlSet = Array.from(this.props.model.controlSets).find(c => c["control"] == controlUri); - var label = (controlSet!=null ? controlSet.label : ""); - var description = (controlSet!=null ? controlSet.description : ""); let controlSetsMap = {}; this.props.model.controlSets.forEach(cs => controlSetsMap[cs.uri] = cs); - if (!this.props.show) { - return null; - } + var controlUri = this.props.selectedAsset.selectedControl; + var controlSet = Array.from(this.props.model.controlSets).find(c => c["control"] == controlUri); + var label = (controlSet!=null ? controlSet.label : ""); + var description = (controlSet!=null ? controlSet.description : ""); return ( - {this.rnd = c;} } - bounds={ '#view-boundary' } - default={{ - x: window.outerWidth * 0.15, - y: (100 / window.innerHeight) * window.devicePixelRatio, - width: 360, - height: 600, - }} - style={{ zIndex: this.props.windowOrder }} - minWidth={150} - minHeight={200} - cancel={".content, .text-primary, strong, span"} - onResize={(e) => { - if (e.stopPropagation) e.stopPropagation(); - if (e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - e.returnValue = false; - }} - // onDragStart={(e) => { - // let elName = $(e.target)[0].localName; - // //console.log("onDragStart: element: " + elName); - // - // /* - // if (elName === "input") { - // return false; - // } - // */ - // if (this.props.isActive) { - // return; - // } - // - // this.bringWindowToFront(true); - // this.props.dispatch(activateControlExplorer(this.props.selectedControl)); - // }} - onDrag={(e) => { - if (e.stopPropagation) e.stopPropagation(); - if (e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - e.returnValue = false; - }} - className={!this.props.show ? "hidden" : null}> -
- -
{ - this.props.dispatch(bringToFrontWindow("controlExplorer")); - }}> -

-
-
- {"Control Explorer"} -
-
-

- { - this.props.dispatch(closeWindow("controlExplorer")); - this.props.onHide(); - }}> - - openDocumentation(e, "redirect/control-explorer")} /> -
- -
-
-
-

- {label} -

-

- {description} -

-
-
- - -
-
- -
- ); +
+
+
+

+ {label} +

+

+ {description} +

+
+
+ + +
+ ) } } ControlExplorer.propTypes = { selectedAsset: PropTypes.object, - isActive: PropTypes.bool, // is in front of other panels + //isActive: PropTypes.bool, // is in front of other panels model: PropTypes.object, + show: PropTypes.bool, onHide: PropTypes.func, - loading: PropTypes.object, hoverThreat: PropTypes.func, getAssetType: PropTypes.func, + loading: PropTypes.object, + dispatch: PropTypes.func, authz: PropTypes.object, + windowOrder: PropTypes.number, }; let mapStateToProps = function (state) { diff --git a/src/main/webapp/app/modeller/components/panes/controls/controlPane/ControlPane.js b/src/main/webapp/app/modeller/components/panes/controls/controlPane/ControlPane.js index 1a2433f0..8db8ab62 100644 --- a/src/main/webapp/app/modeller/components/panes/controls/controlPane/ControlPane.js +++ b/src/main/webapp/app/modeller/components/panes/controls/controlPane/ControlPane.js @@ -12,6 +12,7 @@ import * as Constants from "../../../../../common/constants.js"; import OptionsModal from "../options/OptionsModal"; import { getShortestPathPlot, + getRecommendations, getValidatedModel, calculateRisks, calculateRisksBlocking, @@ -145,7 +146,7 @@ class ControlPane extends React.Component { let riskLevelsValid = valid && this.props.model["riskLevelsValid"]; //if model is invalid, riskLevelsValid must also be false let saved = this.props.model["saved"]; let riskModeCurrent = false; - //console.log("saved = " + saved); + let acceptableRiskLevel = Constants.ACCEPTABLE_RISK_LEVEL; return (
Show attack path (current risk) + { + this.props.dispatch(getRecommendations(this.props.model["id"], "FUTURE", acceptableRiskLevel)); + }}> + Recommendations (future risk) + + { + this.props.dispatch(getRecommendations(this.props.model["id"], "CURRENT", acceptableRiskLevel)); + }}> + Recommendations (current risk) + diff --git a/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js b/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js index 2d8cc4fd..821dc4ca 100644 --- a/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js +++ b/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js @@ -1,54 +1,28 @@ import React from "react"; import PropTypes from 'prop-types'; -//import ControlStrategyAccordion from "./ControlStrategyAccordion"; -import {Rnd} from "../../../../../node_modules/react-rnd/lib/index"; -import {bringToFrontWindow, closeWindow} from "../../../actions/ViewActions"; -import renderControlStrategy from "./ControlStrategyRenderer"; +import {renderControlStrategy} from "./ControlStrategyRenderer"; import { updateControlOnAsset, updateThreat } from "../../../../modeller/actions/ModellerActions"; import {connect} from "react-redux"; -import {openDocumentation} from "../../../../common/documentation/documentation" +import Explorer from "../common/Explorer" class ControlStrategyExplorer extends React.Component { constructor(props) { super(props); + this.renderContent = this.renderContent.bind(this); this.updateThreat = this.updateThreat.bind(this); this.getCsgControlSets = this.getCsgControlSets.bind(this); - this.rnd = null; - this.state = { updatingControlSets: {}, controlSets: this.getCsgControlSets(props) } } - shouldComponentUpdate(nextProps, nextState) { - let shouldComponentUpdate = true; - - if ((!this.props.show) && (!nextProps.show)) { - return false; - } - - if (nextProps.loading.model) { - return false; - } - - if (this.props.isActive != nextProps.isActive) { - return true; - } - - if(this.props.selectedAsset != nextProps.selectedAsset) { - return true; - } - - return shouldComponentUpdate; - } - componentWillReceiveProps(nextProps) { this.setState({ updatingControlSets: {}, @@ -146,91 +120,26 @@ class ControlStrategyExplorer extends React.Component { } render() { - const {model, hoverThreat, dispatch, loading, ...modalProps} = this.props; - - //TODO: for now we take the overall CSG label/desc from the first element in the CSG array, - //however the desc will be specific for a particular CSG (threat at asset) - var csg = this.props.selectedControlStrategy.length > 0 ? this.props.selectedControlStrategy[0] : null; - var label = (csg != null ? csg.label : ""); - let context = this.props.csgExplorerContext; - if (!this.props.show) { return null; } return ( - {this.rnd = c;} } - bounds={ '#view-boundary' } - default={{ - x: window.outerWidth * 0.20, - y: (100 / window.innerHeight) * window.devicePixelRatio, - width: 500, - height: 600, - }} - style={{ zIndex: this.props.windowOrder }} - minWidth={150} - minHeight={200} - cancel={".content, .text-primary, strong, span"} - onResize={(e) => { - if (e.stopPropagation) e.stopPropagation(); - if (e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - e.returnValue = false; - }} - onDrag={(e) => { - if (e.stopPropagation) e.stopPropagation(); - if (e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - e.returnValue = false; - }} - className={!this.props.show ? "hidden" : null}> -
- -
{ - this.props.dispatch(bringToFrontWindow("controlStrategyExplorer")); - }}> -

-
-
- {"Control Strategy Explorer"} -
-
-

- { - this.props.dispatch(closeWindow("controlStrategyExplorer")); - this.props.onHide(); - }}> - - openDocumentation(e, "redirect/control-strategy-explorer")} /> -
- -
-
-
- {this.renderDescription(csg, context)} -
-
- - {/* TODO: remove this if we are sure we don't need expandable panels */} - {/* */} - - {this.renderCsgs()} -
-
- -
- ); + + ) } renderCsgs() { @@ -250,19 +159,44 @@ class ControlStrategyExplorer extends React.Component { let threat = null; //list all threats return renderControlStrategy(csgName, csg, csgKey, threat, this.state, this.props, this, "csg-explorer"); } + + renderContent() { + const {model, hoverThreat, dispatch, loading, ...modalProps} = this.props; + + //TODO: for now we take the overall CSG label/desc from the first element in the CSG array, + //however the desc will be specific for a particular CSG (threat at asset) + var csg = this.props.selectedControlStrategy.length > 0 ? this.props.selectedControlStrategy[0] : null; + var label = (csg != null ? csg.label : ""); + let context = this.props.csgExplorerContext; + + return ( +
+
+
+ {this.renderDescription(csg, context)} +
+
+ + {this.renderCsgs()} +
+ ) + } } ControlStrategyExplorer.propTypes = { selectedAsset: PropTypes.object, - selectedControlStrategy: PropTypes.object, + selectedControlStrategy: PropTypes.array, isActive: PropTypes.bool, // is in front of other panels model: PropTypes.object, controlSets: PropTypes.object, + show: PropTypes.bool, onHide: PropTypes.func, loading: PropTypes.object, hoverThreat: PropTypes.func, getAssetType: PropTypes.func, + dispatch: PropTypes.func, authz: PropTypes.object, + windowOrder: PropTypes.number, }; let mapStateToProps = function (state) { diff --git a/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyRenderer.js b/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyRenderer.js index 5022987f..b95f0f71 100644 --- a/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyRenderer.js +++ b/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyRenderer.js @@ -7,7 +7,7 @@ import { } from "../../../actions/ModellerActions"; import {bringToFrontWindow} from "../../../actions/ViewActions"; -export default function renderControlStrategy(csgName, controlStrategy, csgKey, threat, state, props, ths, context) { +export function renderControlStrategy(csgName, controlStrategy, csgKey, threat, state, props, ths, context) { const csgDescription = controlStrategy.description ? controlStrategy.description : csgName; let blockingEffect = controlStrategy.blockingEffect; let controlStrategyControlSetUris = controlStrategy.mandatoryControlSets.concat(controlStrategy.optionalControlSets); @@ -53,125 +53,16 @@ export default function renderControlStrategy(csgName, controlStrategy, csgKey,

{csgDescription}

{controlStrategyControlSets.sort((a, b) => a["label"].localeCompare(b["label"])).map((control, index) => { - let spinnerActive = state.updatingControlSets[control.uri] === true; - let optionalControl = controlStrategy.optionalControlSets.includes(control.uri); - let errorFlag = control["error"]; - let errorMsg = "Request failed. Please try again"; - - let errorFlagOverlay = ""; - if (errorFlag) { - let errorFlagOverlayProps = { - delayShow: Constants.TOOLTIP_DELAY, placement: "right", - overlay: - {errorMsg} - - }; - errorFlagOverlay = -
-   - - {/* show icon according to error / busy etc */} - - -
-
; - } else if (spinnerActive) { - let errorFlagOverlayProps = { - delayShow: Constants.TOOLTIP_DELAY, placement: "right", - overlay: - - Processing - - - }; - errorFlagOverlay = -
-   - -
-
; - } + control.optional = controlStrategy.optionalControlSets.includes(control.uri); let threatensAsset = false; if (!threat || (threat && (_.find(assets, ['uri', threat.threatensAssets]))) ) { threatensAsset = true; } - let cursor = (!threatensAsset || !control["assertable"]) ? 'not-allowed' : 'pointer'; + let assetName = filterNodeName(control["assetId"], control["assetUri"], threat, assets); - if (props.authz.userEdit == false) { - cursor = "default" - } - - return ( -
- - - - {"Control not implemented"} - - }> - props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, false) : undefined}> - - - - - - {"Control to be implemented"} - - }> - props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, true, true) : undefined}> - - - - - - {"Control is implemented"} - - }> - props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, true) : undefined}> - - - - - - - {control.description !== null - ? control["description"] - : "No description available"} - - }> - - {control["label"]} - {' at "'} - {filterNodeName(control["assetId"], control["assetUri"], threat, assets)} - {'" '} - - {optionalControl ? ' (optional) ' : ''} - - - - {errorFlagOverlay} -
- ) + return renderControlSet(control, index, controlStrategy, threatensAsset, assetName, props, state, ths); })} {(context === "csg-explorer") &&
Related Threats
} @@ -195,6 +86,123 @@ export default function renderControlStrategy(csgName, controlStrategy, csgKey, ) } +export function renderControlSet(control, index, controlStrategy, threatensAsset, assetName, props, state, ths) { + let spinnerActive = state.updatingControlSets[control.uri] === true; + let optionalControl = control.optional; + let errorFlag = control["error"]; + let errorMsg = "Request failed. Please try again"; + + let errorFlagOverlay = ""; + if (errorFlag) { + let errorFlagOverlayProps = { + delayShow: Constants.TOOLTIP_DELAY, placement: "right", + overlay: + {errorMsg} + + }; + errorFlagOverlay = +
+   + + {/* show icon according to error / busy etc */} + + +
+
; + } else if (spinnerActive) { + let errorFlagOverlayProps = { + delayShow: Constants.TOOLTIP_DELAY, placement: "right", + overlay: + + Processing + + + }; + errorFlagOverlay = +
+   + +
+
; + } + + let cursor = (!threatensAsset || !control["assertable"]) ? 'not-allowed' : 'pointer'; + + if (props.authz.userEdit == false) { + cursor = "default" + } + + return ( +
+ + + + {"Control not implemented"} + + }> + props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, false) : undefined}> + + + + + + {"Control to be implemented"} + + }> + props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, true, true) : undefined}> + + + + + + {"Control is implemented"} + + }> + props.authz.userEdit ? toggleControlProposedState(control, controlStrategy, ths, true) : undefined}> + + + + + + + {control.description !== null + ? control["description"] + : "No description available"} + + }> + + {control["label"]} + {' at "'} + {assetName} + {'" '} + + {optionalControl ? ' (optional) ' : ''} + + + + {errorFlagOverlay} +
+ ) +} + function openCsgExplorer(csgs, context, props) { if (csgs.length > 0) { props.dispatch(openControlStrategyExplorer(csgs, context)); @@ -210,7 +218,7 @@ function toggleControlProposedState(control, controlStrategy, ths, proposed = tr ...control, proposed: proposed, workInProgress: workInProgress, - controlStrategy: controlStrategy["id"] + controlStrategy: controlStrategy ? controlStrategy["id"] : null }, }); diff --git a/src/main/webapp/app/modeller/components/panes/details/accordion/panels/ControlStrategiesPanel.js b/src/main/webapp/app/modeller/components/panes/details/accordion/panels/ControlStrategiesPanel.js index 2ef60126..0a786956 100644 --- a/src/main/webapp/app/modeller/components/panes/details/accordion/panels/ControlStrategiesPanel.js +++ b/src/main/webapp/app/modeller/components/panes/details/accordion/panels/ControlStrategiesPanel.js @@ -20,7 +20,11 @@ class ControlStrategiesPanel extends React.Component { {csgs.length > 0 ? csgs.map((csgEntry, index) => { let name = csgEntry[0]; let csg = csgEntry[1]; - let context = {"selection": "csg", "asset": this.props.asset}; + let asset = csg.asset ? csg.asset : this.props.asset; + if (this.props.displayAssetName) { + name += " at \"" + asset.label + "\""; + } + let context = {"selection": "csg", "asset": asset}; let csgOverlayProps = { delayShow: Constants.TOOLTIP_DELAY, placement: "left", @@ -65,6 +69,7 @@ class ControlStrategiesPanel extends React.Component { ControlStrategiesPanel.propTypes = { modelId: PropTypes.string, asset: PropTypes.object, + displayAssetName: PropTypes.bool, assetCsgs: PropTypes.array, dispatch: PropTypes.func, authz: PropTypes.object diff --git a/src/main/webapp/app/modeller/components/panes/misbehaviours/accordion/MisbehaviourAccordion.js b/src/main/webapp/app/modeller/components/panes/misbehaviours/accordion/MisbehaviourAccordion.js index 5185d097..7eade5b3 100644 --- a/src/main/webapp/app/modeller/components/panes/misbehaviours/accordion/MisbehaviourAccordion.js +++ b/src/main/webapp/app/modeller/components/panes/misbehaviours/accordion/MisbehaviourAccordion.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import {OverlayTrigger, Panel, Tooltip, Button, ButtonToolbar} from "react-bootstrap"; import ThreatsPanel from "../../details/accordion/panels/ThreatsPanel"; import * as Constants from "../../../../../common/constants.js"; -import {getThreatGraph} from "../../../../actions/ModellerActions"; +import {getThreatGraph, getRecommendations} from "../../../../actions/ModellerActions"; class MisbehaviourAccordion extends React.Component { @@ -100,6 +100,17 @@ class MisbehaviourAccordion extends React.Component { this.props.selectedMisbehaviour.misbehaviour.uri)); }; + let acceptableRiskLevel = Constants.ACCEPTABLE_RISK_LEVEL; + + const handleRecommendationsButtonClick = () => { + this.props.dispatch(getRecommendations( + this.props.model.id, + this.props.model.riskCalculationMode, + acceptableRiskLevel, + this.props.selectedMisbehaviour.misbehaviour.uri, + false)); + }; + return (
@@ -203,7 +214,7 @@ class MisbehaviourAccordion extends React.Component { - {this.props.model.riskCalculationMode ? "Calculate attack path" : "Run risk calculation first!"} + {this.props.model.riskLevelsValid ? "Calculate attack path" : "To use this, first run the risk calculation"} } > @@ -211,11 +222,34 @@ class MisbehaviourAccordion extends React.Component { className="btn btn-primary btn-xs" disabled={ attackPathThreats.length > 0 || - !this.props.model.riskCalculationMode + !this.props.model.riskLevelsValid } onClick={handleThreatGraphButtonClick} > - Calculate Attack Path + Get Attack Path + + + + {this.props.model.riskLevelsValid ? "Calculate recommendations" : "To use this, first run the risk calculation"} + + } + > + {loadingAttackPath ? : null} diff --git a/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/AbortRecommendationsModal.js b/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/AbortRecommendationsModal.js new file mode 100644 index 00000000..e99e38d8 --- /dev/null +++ b/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/AbortRecommendationsModal.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from "react"; +import { Button, Modal } from "react-bootstrap"; + +class AbortRecommendationsModal extends Component { + + render() { + const {modelId, jobId, abortRecommendations, ...modalProps} = this.props; + + return ( + + + Abort Recommendations + + +

Abort current recommendations calculation?

+
+ + + + +
+ ); + } +} + +AbortRecommendationsModal.propTypes = { + modelId: PropTypes.string, + jobId: PropTypes.string, + abortRecommendations: PropTypes.func, + onHide: PropTypes.func, +}; + +export default AbortRecommendationsModal; diff --git a/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/RecommendationsExplorer.js b/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/RecommendationsExplorer.js new file mode 100644 index 00000000..fa5d420c --- /dev/null +++ b/src/main/webapp/app/modeller/components/panes/recommendationsExplorer/RecommendationsExplorer.js @@ -0,0 +1,357 @@ +import React from "react"; +import PropTypes from 'prop-types'; +import {Panel, Button, OverlayTrigger, Tooltip} from "react-bootstrap"; +import {connect} from "react-redux"; +import {JsonView, defaultStyles} from 'react-json-view-lite'; +import 'react-json-view-lite/dist/index.css'; +import Explorer from "../common/Explorer"; +import ControlStrategiesPanel from "../details/accordion/panels/ControlStrategiesPanel"; +import * as Constants from "../../../../common/constants.js"; +import {renderControlSet} from "../csgExplorer/ControlStrategyRenderer"; +import { + updateControlOnAsset, + updateControls, +} from "../../../../modeller/actions/ModellerActions"; + +class RecommendationsExplorer extends React.Component { + + constructor(props) { + super(props); + + this.renderContent = this.renderContent.bind(this); + this.renderJson = this.renderJson.bind(this); + this.renderRecommendations = this.renderRecommendations.bind(this); + this.renderNoRecommendations = this.renderNoRecommendations.bind(this); + this.renderControlSets = this.renderControlSets.bind(this); + this.getControlSets = this.getControlSets.bind(this); + this.getRiskVector = this.getRiskVector.bind(this); + this.getHighestRiskLevel = this.getHighestRiskLevel.bind(this); + this.getAssetByUri = this.getAssetByUri.bind(this); + this.getRiskVectorString = this.getRiskVectorString.bind(this); + this.compareRiskVectors = this.compareRiskVectors.bind(this); + this.updateThreat = this.updateThreat.bind(this); + this.applyRecommendation = this.applyRecommendation.bind(this); + + this.state = { + updatingControlSets: {} + } + + } + + componentWillReceiveProps(nextProps) { + this.setState({...this.state, + updatingControlSets: {} + }); + } + + render() { + if (!this.props.show) { + return null; + } + + return ( + + ) + } + + renderContent() { + let renderRecommentations = true; + let recommendations = this.props.recommendations; + + if (renderRecommentations) { + return this.renderRecommendations(recommendations); + } + else { + return this.renderJson(recommendations); + } + } + + renderJson(recommendations) { + return ( +
+ {recommendations && } +
+ ) + } + + renderRecommendations(report) { + if (jQuery.isEmptyObject(report)) { + return null; + } + + let max_recommendations = Constants.MAX_RECOMMENDATIONS; //max number to display + let recommendations = report.recommendations || []; + let selected_recommendations = []; + + recommendations.forEach(rec => { + rec.state.riskVector = this.getRiskVector(rec.state.risk); + }); + + recommendations.sort((a, b) => this.compareRiskVectors(a.state.riskVector, b.state.riskVector)); //sort by ascending risk vector + + //Select recommendations from the top of the list, up to the max number + selected_recommendations = recommendations.slice(0, max_recommendations); + + let csgAssets = this.props.csgAssets; + + return ( +
+
+

N.B. The recommendations feature is a work in progress + and will not always give the most sensible recommendation(s). + It may also take a very long time to run, in particular for Future risk recommendations. + A list of known issues can be found on GitHub.

+ {recommendations.length &&

Returned {recommendations.length} recommendations + {recommendations.length > max_recommendations ? " (Displaying top " + max_recommendations + ")" : ""}

} +
+ {!recommendations.length ? this.renderNoRecommendations() : +
+ {selected_recommendations.map((rec, index) => { + let id = rec.identifier; + let reccsgs = rec.controlStrategies; + let riskVectorString = this.getRiskVectorString(rec.state.riskVector); + let riskLevel = this.getHighestRiskLevel(rec.state.riskVector); + let csgsByName = new Map(); + + reccsgs.forEach((reccsg) => { + let csguri = Constants.URI_PREFIX + reccsg.uri; + let csg = this.props.model.controlStrategies[csguri]; + let name = csg.label; + let assetUri = csgAssets[csguri]; + let asset = assetUri ? this.getAssetByUri(assetUri) : {label: "Unknown"} + csg.asset = asset; + csgsByName.set(name, csg); + }); + + csgsByName = new Map([...csgsByName.entries()].sort((a, b) => a[0].localeCompare(b[0]))); + let csgsArray = Array.from(csgsByName); + let applyButtonTooltipText = "Enable all recommended controls"; + + return ( + + + + Recommendation {index + 1} + + + + +

Residual risk: {riskLevel.label} ({riskVectorString})

+

Control Strategies to enable

+ +

Controls to enable

+ {this.renderControlSets(rec.controls)} +

+ + {applyButtonTooltipText}}> + + +

+
+
+
+ ); + })} +
} +
+ ) + } + + renderNoRecommendations() { + return ( +
+

There are no current recommendations for reducing the system model risk any further.

+
+ ); + } + + renderControlSets(controls) { + let controlSets = this.getControlSets(controls); + controlSets.sort((a, b) => a["label"].localeCompare(b["label"])); + + return ( +
+ {controlSets.map((control, index) => { + control.optional = false; //assume recommendation does not suggest optional controls + let asset = control["assetUri"] ? this.getAssetByUri(control["assetUri"]) : {label: "Unknown"} + let assetName = asset.label; + return renderControlSet(control, index, null, true, assetName, this.props, this.state, this); + })} +
+ ); + } + + getControlSets(controls) { + let modelControlSets = this.props.controlSets; + let controlSets = controls.map(control => { + let csuri = Constants.URI_PREFIX + control.uri; + let cs = modelControlSets[csuri]; + return cs; + }); + + return controlSets; + } + + getRiskVector(reportedRisk) { + let shortUris = Object.keys(reportedRisk); + let riskLevels = this.props.model.levels["RiskLevel"]; + + let riskLevelsMap = {} + riskLevels.forEach(level => { + let levelUri = level.uri; + riskLevelsMap[levelUri] = level; + }); + + let riskVector = shortUris.map(shorturi => { + let uri = Constants.URI_PREFIX + shorturi; + let riskLevel = riskLevelsMap[uri]; + let riskLevelCount = {level: riskLevel, count: reportedRisk[shorturi]} + return riskLevelCount; + }); + + //Finally sort the risk vector by level value + riskVector.sort((a, b) => { + if (a.level.value < b.level.value) { + return -1; + } + else if (a.level.value > b.level.value) { + return 1; + } + else { + return 0; + } + }); + + return riskVector; + } + + //e.g. "Very Low: 695, Low: 0, Medium: 1, High: 0, Very High: 0" + getRiskVectorString(riskVector) { + let strArr = riskVector.map(riskLevelCount => { + let level = riskLevelCount.level; + return [level.label, riskLevelCount.count].join(": "); + }); + + return strArr.join(", "); + } + + //Compare risk vectors (assumes arrays are pre-sorted) + compareRiskVectors(rva, rvb) { + let compare = 0; + for (let i = rva.length -1; i >= 0; i--) { + compare = rva[i].count - rvb[i].count; + if (compare !== 0) { + return compare; + } + } + return compare; + } + + //Get highest risk level from given risk vector + //i.e. which is the highest risk level that has >0 misbehaviours + //TODO: could this be moved to a utility function? + getHighestRiskLevel(riskVector) { + let overall = 0; + let hishestLevel = null; + riskVector.forEach(riskLevelCount => { + let level = riskLevelCount.level; + let count = riskLevelCount.count; + if (count > 0 && level.value >= overall) { + overall = level.value; + hishestLevel = level; + } + }); + + return hishestLevel; + } + + //TODO: this should be a utility function on the model + getAssetByUri(assetUri) { + let asset = this.props.model.assets.find((asset) => { + return (asset.uri === assetUri); + }); + return asset; + } + + updateThreat(arg) { + //this is to enable a single control in a control strategy + if (arg.hasOwnProperty("control")) { + //Here we still want to keep the currently selected asset, not change to the asset referred to in the updatedControl + this.props.dispatch(updateControlOnAsset(this.props.model.id, arg.control.assetId, arg.control)); + } + } + + applyRecommendation(recid) { + let proposed = true; + let report = this.props.recommendations; //get recommendations report + let rec = report.recommendations.find((rec) => rec["identifier"] === recid); + + if (rec) { + let controlsToUpdate = rec.controls.map(control => { + return Constants.URI_PREFIX + control.uri; + }); + + let updatingControlSets = {...this.state.updatingControlSets}; + controlsToUpdate.forEach(controlUri => { + updatingControlSets[controlUri] = true; + }); + + this.setState({ + updatingControlSets: updatingControlSets, + }); + + this.props.dispatch(updateControls(this.props.model.id, controlsToUpdate, proposed, proposed)); //set WIP flag only if proposed is true + } + else { + console.warn("Could not locate recommendation: ", recid); + } + } + +} + +function shouldExpandRecommendationsNode(level) { + return level <= 1; +} + +RecommendationsExplorer.propTypes = { + model: PropTypes.object, + controlSets: PropTypes.object, + csgAssets: PropTypes.object, + selectedAsset: PropTypes.object, + isActive: PropTypes.bool, // is in front of other panels + recommendations: PropTypes.object, + show: PropTypes.bool, + onHide: PropTypes.func, + loading: PropTypes.object, + dispatch: PropTypes.func, + windowOrder: PropTypes.number, + authz: PropTypes.object, +}; + +let mapStateToProps = function (state) { + return { + windowOrder: state.view["recommendationsExplorer"] + } +}; + +export default connect(mapStateToProps)(RecommendationsExplorer); diff --git a/src/main/webapp/app/modeller/components/panes/threats/accordion/panels/ControlStrategiesPanel.js b/src/main/webapp/app/modeller/components/panes/threats/accordion/panels/ControlStrategiesPanel.js index 8dd058fc..acfcb62c 100644 --- a/src/main/webapp/app/modeller/components/panes/threats/accordion/panels/ControlStrategiesPanel.js +++ b/src/main/webapp/app/modeller/components/panes/threats/accordion/panels/ControlStrategiesPanel.js @@ -1,7 +1,7 @@ import React, {Fragment} from "react"; import PropTypes from 'prop-types'; import {Button, Checkbox, FormControl, FormGroup} from "react-bootstrap"; -import renderControlStrategy from "../../../csgExplorer/ControlStrategyRenderer"; +import {renderControlStrategy} from "../../../csgExplorer/ControlStrategyRenderer"; import {connect} from "react-redux"; class ControlStrategiesPanel extends React.Component { diff --git a/src/main/webapp/app/modeller/components/util/LoadingOverlay.js b/src/main/webapp/app/modeller/components/util/LoadingOverlay.js index 569db6de..a5ab2c04 100644 --- a/src/main/webapp/app/modeller/components/util/LoadingOverlay.js +++ b/src/main/webapp/app/modeller/components/util/LoadingOverlay.js @@ -1,11 +1,12 @@ import React from "react"; import PropTypes from "prop-types"; import {Modal, Button, ProgressBar} from "react-bootstrap"; +import AbortRecommendationsModal from "../panes/recommendationsExplorer/AbortRecommendationsModal"; import { - loadingCompleted, pollForLoadingProgress, pollForValidationProgress, pollForRiskCalcProgress, + pollForLoadingProgress, pollForValidationProgress, pollForRiskCalcProgress, pollForRecommendationsProgress, validationCompleted, validationFailed, - riskCalcCompleted, riskCalcFailed, changeSelectedAsset - //resetValidation + riskCalcCompleted, riskCalcFailed, changeSelectedAsset, + recommendationsCompleted, recommendationsFailed, abortRecommendations, } from "../../actions/ModellerActions"; class LoadingOverlay extends React.Component { @@ -33,26 +34,43 @@ class LoadingOverlay extends React.Component { } }, progress: 0, + abortRecommendationsModal: false, }; this.checkProgress = this.checkProgress.bind(this); this.pollValidationProgress = this.pollValidationProgress.bind(this); this.pollRiskCalcProgress = this.pollRiskCalcProgress.bind(this); + this.pollRecommendationsProgress = this.pollRecommendationsProgress.bind(this); this.pollLoadingProgress = this.pollLoadingProgress.bind(this); this.pollDroppingInfGraphProgress = this.pollDroppingInfGraphProgress.bind(this); this.getValidationTimeout = this.getValidationTimeout.bind(this); this.getRiskCalcTimeout = this.getRiskCalcTimeout.bind(this); + this.getRecommendationsTimeout = this.getRecommendationsTimeout.bind(this); this.getLoadingTimeout = this.getLoadingTimeout.bind(this); this.getTimeout = this.getTimeout.bind(this); + this.getHeaderText = this.getHeaderText.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + this.abortRecommendations = this.abortRecommendations.bind(this); + } + onKeyPress(event){ + //Check for Escape key + if (event.keyCode === 27) { + if (this.props.isCalculatingRecommendations) { + this.setState({...this.state, abortRecommendationsModal: true}); + } + } } componentDidMount() { - //console.log("LoadingOverlay timeout settings: ", this.state); + document.addEventListener("keydown", this.onKeyPress, false); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyPress, false); } componentWillReceiveProps(nextProps) { - //console.log("LoadingOverlay: componentWillReceiveProps", this.props.isValidating, nextProps.isValidating); let showModal = this.state.showModal; let timeout = this.state.timeout; let progress = this.state.progress; @@ -69,12 +87,18 @@ class LoadingOverlay extends React.Component { // If risk calc has completed, show modal if (this.props.isCalculatingRisks && !nextProps.isCalculatingRisks) { - //console.log("LoadingOverlay: setting showModal true"); stage = "Risk calculation"; if (nextProps.validationProgress.status !== "inactive") showModal = true; stateChanged = true; } + // If recommendations has completed, show modal + if (this.props.isCalculatingRecommendations && !nextProps.isCalculatingRecommendations) { + stage = "Recommendations"; + if (nextProps.validationProgress.status !== "inactive") showModal = true; + stateChanged = true; + } + // Show modal (error dialog) if loading has failed if (this.props.loadingProgress.status !== "failed" && nextProps.loadingProgress.status === "failed") { showModal = true; @@ -89,7 +113,6 @@ class LoadingOverlay extends React.Component { } if (this.props.isDroppingInferredGraph && !nextProps.isDroppingInferredGraph) { - //console.log("Dropped inferred graph - progress complete"); stage = "DroppingInferredGraph"; progress = 1.0; stateChanged = true; @@ -97,25 +120,28 @@ class LoadingOverlay extends React.Component { // If loading has started, start polling if (!this.props.isLoading && nextProps.isLoading) { - //console.log("LoadingOverlay: loading started. Initialising timeout"); stage = "Loading"; timeout = 0; stateChanged = true; } // If validation has started, start polling else if (!this.props.isValidating && nextProps.isValidating) { - //console.log("LoadingOverlay: validation started. Initialising timeout"); stage = "Validation"; timeout = 0; stateChanged = true; } // If risk calc has started, start polling else if (!this.props.isCalculatingRisks && nextProps.isCalculatingRisks) { - console.log("LoadingOverlay: risk calc started. Initialising timeout"); stage = "Risk calculation"; timeout = 0; stateChanged = true; } + // If recommendations has started, start polling + else if (!this.props.isCalculatingRecommendations && nextProps.isCalculatingRecommendations) { + stage = "Recommendations"; + timeout = 0; + stateChanged = true; + } if (stateChanged) { //here, we set both changes of state at the same time, otherwise initial change may be ignored @@ -130,9 +156,6 @@ class LoadingOverlay extends React.Component { } componentDidUpdate(prevProps, prevState) { - //console.log("LoadingOverlay: componentDidUpdate: state: ", prevState, this.state); - //console.log("LoadingOverlay: showModal = " + this.state.showModal); - // If loading has started, start polling if (!prevProps.isLoading && this.props.isLoading) { if (this.props.loadingId) { @@ -148,13 +171,18 @@ class LoadingOverlay extends React.Component { this.checkProgress(); } else if (!prevProps.isValidating && this.props.isValidating) { - //console.log("LoadingOverlay: validation started. Start polling"); this.checkProgress(); } else if (!prevProps.isCalculatingRisks && this.props.isCalculatingRisks) { - console.log("LoadingOverlay: risk calc started. Start polling"); this.checkProgress(); } + else if (!prevProps.isCalculatingRecommendations && this.props.isCalculatingRecommendations) { + this.checkProgress(); + } + else if (prevProps.isCalculatingRecommendations && !this.props.isCalculatingRecommendations) { + console.log("Recommendations finished - closing abort dialog..."); + this.setState({...this.state, abortRecommendationsModal: false}); + } else if (prevProps.loadingProgress.waitingForUpdate && !this.props.loadingProgress.waitingForUpdate) { this.checkProgress(); } @@ -164,9 +192,6 @@ class LoadingOverlay extends React.Component { else if (this.props.isDroppingInferredGraph) { this.checkProgress(); } - else { - //console.log("LoadingOverlay: polling componentDidUpdate (nothing to do)"); - } } getValidationTimeout() { @@ -183,6 +208,10 @@ class LoadingOverlay extends React.Component { return this.getTimeout(min, max, progress); } + getRecommendationsTimeout() { + return this.getRiskCalcTimeout(); + } + getLoadingTimeout() { let min = this.state.bounds.loading.min; let max = this.state.bounds.loading.max; @@ -194,12 +223,6 @@ class LoadingOverlay extends React.Component { let timeout = this.state.timeout; let increment = this.state.increment; - //console.log("current timeout: ", timeout); - //console.log("min: ", min); - //console.log("max: ", max); - //console.log("increment: ", increment); - //console.log("progress: ", progress); - // increment timeout initially, then decrement towards end if (progress < 0.4) { timeout += increment; @@ -208,13 +231,9 @@ class LoadingOverlay extends React.Component { timeout -= increment; } - //console.log("provisional timeout (before bounds check): ", timeout); - // check within min/max bounds timeout = (timeout < min) ? min : (timeout > max) ? max : timeout; - //console.log("LoadingOverlay: setting timeout: ", timeout); - //update state this.setState({...this.state, timeout: timeout}); @@ -224,7 +243,6 @@ class LoadingOverlay extends React.Component { checkProgress() { if (this.props.isValidating) { - //console.log("LoadingOverlay: validation progress: ", Math.round(this.props.validationProgress.progress * 100)); if (this.props.validationProgress.progress >= 1.0) { if (this.props.validationProgress.status === "completed") { console.log("LoadingOverlay: validation progress completed"); @@ -239,8 +257,6 @@ class LoadingOverlay extends React.Component { } } else if (this.props.isCalculatingRisks) { - console.log(this.props); - console.log("LoadingOverlay: risk calc progress: ", Math.round(this.props.validationProgress.progress * 100)); if (this.props.validationProgress.status === "inactive") { console.log("WARNING: isCalculatingRisks is true, but status is inactive"); this.props.dispatch(riskCalcFailed(this.props.modelId)); @@ -263,13 +279,34 @@ class LoadingOverlay extends React.Component { setTimeout(this.pollRiskCalcProgress, this.getRiskCalcTimeout()); } } + else if (this.props.isCalculatingRecommendations) { + if (this.props.validationProgress.status === "inactive") { + console.log("WARNING: isCalculatingRecommendations is true, but status is inactive"); + this.props.dispatch(recommendationsFailed(this.props.modelId)); + return; + } + else if (this.props.validationProgress.progress >= 1.0) { + if (this.props.validationProgress.status === "completed") { + console.log("LoadingOverlay: recommendations progress completed"); + this.props.dispatch(recommendationsCompleted(this.props.modelId, this.props.recommendationsJobId)); + } + else if (this.props.validationProgress.status === "failed") { + console.warn("LoadingOverlay: recommendations progress failed"); + this.props.dispatch(recommendationsFailed(this.props.modelId)); + } + else { + //This should not be necessary, but if server has not yet set completed state.. + setTimeout(this.pollRecommendationsProgress, this.getRecommendationsTimeout()); + } + } else { + setTimeout(this.pollRecommendationsProgress, this.getRecommendationsTimeout()); + } + } else if (this.props.isLoading) { - //console.log("LoadingOverlay: loading progress: ", Math.round(this.props.loadingProgress.progress * 100)); if (this.props.loadingProgress.progress >= 1.0) { //console.log("LoadingOverlay: loading progress completed"); } else { let timeout = this.getLoadingTimeout(); - //console.log("Calling setTimeout: " + timeout); setTimeout(this.pollLoadingProgress, timeout); } } @@ -295,8 +332,18 @@ class LoadingOverlay extends React.Component { } } + pollRecommendationsProgress() { + // While synchronous recommendations is running, the isCalculatingRecommendations flag is true, so poll for progress + // Once the recommendations call returns, the flag is set to false, so we avoid an unnecessary progress request below + if (this.props.isCalculatingRecommendations) { + this.props.dispatch(pollForRecommendationsProgress(this.props.modelId)); + } + else { + console.log("Recommendations complete. Cancel polling for progress"); + } + } + pollLoadingProgress() { - //console.log("LoadingOverlay: pollLoadingProgress: this.props.loadingId = " + this.props.loadingId); if (this.props.loadingId) { this.props.dispatch(pollForLoadingProgress($("meta[name='_model']").attr("content"), this.props.loadingId)); } @@ -314,19 +361,41 @@ class LoadingOverlay extends React.Component { } } + abortRecommendations(modelId, jobId) { + this.setState({...this.state, abortRecommendationsModal: false}); + this.props.dispatch(abortRecommendations(modelId, jobId)); + this.pollRecommendationsProgress(); + } + + getHeaderText() { + let header = ""; + + if (this.props.isValidating) { + header = "The model is currently validating"; + } + else if (this.props.isCalculatingRisks) { + header = "Calculating risks"; + } + else if (this.props.isCalculatingRecommendations) { + header = "Calculating recommendations (Esc to cancel)"; + } + + return header; + } + render() { let isCalculatingRisks = this.props.isCalculatingRisks && this.props.validationProgress.status !== "inactive"; - //console.log("isCalculatingRisks:", isCalculatingRisks); + let isCalculatingRecommendations = this.props.isCalculatingRecommendations && this.props.validationProgress.status !== "inactive" && !this.state.abortRecommendationsModal; let isDroppingInferredGraph = this.props.isDroppingInferredGraph; - //console.log("isDroppingInferredGraph", this.props.isDroppingInferredGraph); - var clazz = "loading-overlay " + (this.props.isValidating || isCalculatingRisks || isDroppingInferredGraph || this.props.isLoading ? "visible" : "invisible"); + let clazz = "loading-overlay " + (this.props.isValidating || isCalculatingRisks || isCalculatingRecommendations || isDroppingInferredGraph || this.props.isLoading ? "visible" : "invisible"); let stage = this.state.stage; + let headerText = this.getHeaderText(); return (
- {(this.props.isValidating || isCalculatingRisks) && + {(this.props.isValidating || isCalculatingRisks || isCalculatingRecommendations) &&
-

{this.props.isValidating ? "The model is currently validating" : "Calculating risks"}...

+

{headerText}...

{this.props.validationProgress.message}...

@@ -417,6 +486,9 @@ class LoadingOverlay extends React.Component { + + this.setState({...this.state, abortRecommendationsModal: false})}/>
) } @@ -426,10 +498,12 @@ LoadingOverlay.propTypes = { modelId: PropTypes.string, loadingId: PropTypes.string, isValidating: PropTypes.bool, + isCalculatingRisks: PropTypes.bool, + isCalculatingRecommendations: PropTypes.bool, + recommendationsJobId: PropTypes.string, isValid: PropTypes.bool, hasModellingErrors: PropTypes.bool, validationProgress: PropTypes.object, - isCalculatingRisks: PropTypes.bool, isLoading: PropTypes.bool, isDroppingInferredGraph: PropTypes.bool, loadingProgress: PropTypes.object, diff --git a/src/main/webapp/app/modeller/index.scss b/src/main/webapp/app/modeller/index.scss index e9b179fe..598ad427 100644 --- a/src/main/webapp/app/modeller/index.scss +++ b/src/main/webapp/app/modeller/index.scss @@ -255,10 +255,11 @@ body { .misbehaviour-explorer, .control-explorer, .control-strategy-explorer, +.recommendations-explorer, .compliance-explorer, +.explorer, .report-dialog { border: 1px inset $BLUE; - // border-radius: 0.3em; box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.3); background-color: $PANEL_BACKGROUND_COLOUR; z-index: 1065; @@ -266,6 +267,16 @@ body { flex-direction: column; height: 100%; width: 100%; + button.header { + border-bottom-width: 0px; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; + padding-top: 0px; + padding-bottom: 0px; + padding-right: 0px; + padding-left: 0px; + } .header { cursor: all-scroll; width: 100%; @@ -273,12 +284,15 @@ body { background: $DARK_BLUE; color: $PANEL_BACKGROUND_COLOUR; padding: 0.3em 0em; + .title { + padding-top: 4px; + } h1 { margin: 0 0.3em; font-size: 14px; line-height: 20px; font-weight: 600; - display: inline-block; + float: left; } .menu-close { cursor: pointer; @@ -286,6 +300,27 @@ body { margin: 1px 0.3em 0 0.3em; float: right; } + .button { + cursor: pointer; + font-size: 18px; + margin: 0px; + float: right; + } + button { + border-top-width: 0px; + border-right-width: 0px; + border-left-width: 0px; + border-bottom-width: 0px; + padding-top: 0px; + padding-bottom: 0px; + background-color: transparent + } + button, i { + color: $PANEL_BACKGROUND_COLOUR; + } + } + .header-no-padding { + padding: 0; } .content { cursor: default; @@ -319,30 +354,6 @@ body { border: 1px ridge #bcdbe4; margin: 0; overflow: hidden; - .control { - width: 100%; - background-color: #1962c0; - border: 1px ridge #1962c0; - color: #fff; - margin: 0; - overflow: hidden; - padding: 2px; - display: inline-flex; - align-items: center; - - .control-description { - margin-left: 5px; - font-weight: 300; - color: white; - } - - &[disabled] { - .control-description { - color: darken(#fff, 30); - cursor: not-allowed; - } - } - } .optional { background-color: #3d8ed4; } @@ -399,6 +410,31 @@ body { } } +.control { + width: 100%; + background-color: #1962c0; + border: 1px ridge #1962c0; + color: #fff; + margin: 0; + overflow: hidden; + padding: 2px; + display: inline-flex; + align-items: center; + + .control-description { + margin-left: 5px; + font-weight: 300; + color: white; + } + + &[disabled] { + .control-description { + color: darken(#fff, 30); + cursor: not-allowed; + } + } +} + .traffic-lights { align-items: center; @@ -2017,3 +2053,7 @@ div.panel-title { margin: 2px 0; padding: 2px 0; } + +.recommendations .bare-list { + font-size: 14px; +} diff --git a/src/main/webapp/app/modeller/modellerConstants.js b/src/main/webapp/app/modeller/modellerConstants.js index c3f164ae..865356e9 100644 --- a/src/main/webapp/app/modeller/modellerConstants.js +++ b/src/main/webapp/app/modeller/modellerConstants.js @@ -52,9 +52,11 @@ export const CLOSE_MISBEHAVIOUR_EXPLORER = "CLOSE_MISBEHAVIOUR_EXPLORER"; export const OPEN_COMPLIANCE_EXPLORER = "OPEN_COMPLIANCE_EXPLORER"; export const OPEN_CONTROL_EXPLORER = "OPEN_CONTROL_EXPLORER"; export const OPEN_CONTROL_STRATEGY_EXPLORER = "OPEN_CONTROL_STRATEGY_EXPLORER"; +export const OPEN_RECOMMENDATIONS_EXPLORER = "OPEN_RECOMMENDATIONS_EXPLORER"; export const CLOSE_COMPLIANCE_EXPLORER = "CLOSE_COMPLIANCE_EXPLORER"; export const CLOSE_CONTROL_EXPLORER = "CLOSE_CONTROL_EXPLORER"; export const CLOSE_CONTROL_STRATEGY_EXPLORER = "CLOSE_CONTROL_STRATEGY_EXPLORER"; +export const CLOSE_RECOMMENDATIONS_EXPLORER = "CLOSE_RECOMMENDATIONS_EXPLORER"; export const SUPPRESS_CANVAS_REFRESH = "SUPPRESS_CANVAS_REFRESH"; export const REDRAW_RELATIONS = "REDRAW_RELATIONS"; export const HOVER_THREAT = "HOVER_THREAT"; @@ -66,15 +68,21 @@ export const IS_VALIDATING = "IS_VALIDATING"; export const IS_NOT_VALIDATING = "IS_NOT_VALIDATING"; export const IS_CALCULATING_RISKS = "IS_CALCULATING_RISKS"; export const IS_NOT_CALCULATING_RISKS = "IS_NOT_CALCULATING_RISKS"; +export const IS_CALCULATING_RECOMMENDATIONS = "IS_CALCULATING_RECOMMENDATIONS"; +export const IS_NOT_CALCULATING_RECOMMENDATIONS = "IS_NOT_CALCULATING_RECOMMENDATIONS"; export const IS_DROPPING_INFERRED_GRAPH = "IS_DROPPING_INFERRED_GRAPH"; export const IS_NOT_DROPPING_INFERRED_GRAPH = "IS_NOT_DROPPING_INFERRED_GRAPH"; export const RISK_CALC_RESULTS = "RISK_CALC_RESULTS"; +export const RECOMMENDATIONS_JOB_STARTED = "RECOMMENDATIONS_JOB_STARTED"; +export const RECOMMENDATIONS_RESULTS = "RECOMMENDATIONS_RESULTS"; export const VALIDATION_FAILED = "VALIDATION_FAILED"; export const RISK_CALC_FAILED = "RISK_CALC_FAILED"; +export const RECOMMENDATIONS_FAILED = "RECOMMENDATIONS_FAILED"; export const RESOLVE_RELATION_ISSUE = "RESOLVE_RELATION_ISSUE"; export const GET_ISSUES = "GET_ISSUES"; export const UPDATE_VALIDATION_PROGRESS = "UPDATE_VALIDATION_PROGRESS"; export const UPDATE_RISK_CALC_PROGRESS = "UPDATE_RISK_CALC_PROGRESS"; +export const UPDATE_RECOMMENDATIONS_PROGRESS = "UPDATE_RECOMMENDATIONS_PROGRESS"; export const UPDATE_LOADING_PROGRESS = "UPDATE_LOADING_PROGRESS"; export const UPDATE_THREAT_LOADING = "UPDATE_THREAT_LOADING"; export const UPDATE_DETAILS_LOADING = "UPDATE_DETAILS_LOADING"; diff --git a/src/main/webapp/app/modeller/reducers/modeller.js b/src/main/webapp/app/modeller/reducers/modeller.js index a6a6bc81..52edb740 100644 --- a/src/main/webapp/app/modeller/reducers/modeller.js +++ b/src/main/webapp/app/modeller/reducers/modeller.js @@ -26,12 +26,15 @@ const modelState = { //riskLevelsValid: true, //don't set this initially (button will be coloured blue) saved: true, calculatingRisks: false, + calculatingRecommendations: false, controlsReset: false, canBeEdited: true, canBeShared: true, risksValid: false, riskCalculationMode: "" }, + recommendationsJobId: null, + recommendations: {}, // Rayna: TODO - when the backend for groups is implemented, put this array in the model above. groups: [], grouping: { @@ -81,6 +84,7 @@ const modelState = { } }, misbehaviourTwas: {}, + csgAssets: {}, isMisbehaviourExplorerVisible: false, isMisbehaviourExplorerActive: false, isComplianceExplorerVisible: false, @@ -89,6 +93,8 @@ const modelState = { isControlExplorerActive: false, isControlStrategyExplorerVisible: false, isControlStrategyExplorerActive: false, + isRecommendationsExplorerVisible: false, + isRecommendationsExplorerActive: false, isReportDialogVisible: false, isReportDialogActive: false, isDroppingInferredGraph: false, @@ -204,6 +210,7 @@ export default function modeller(state = modelState, action) { let model = action.payload; model.saved = true; //must be true if reloaded + model.calculatingRecommendations = false; //flag is not currently returned in model let groups = model.groups; @@ -238,6 +245,9 @@ export default function modeller(state = modelState, action) { model.threats.forEach(threat => setThreatTriggeredStatus(threat, model.controlStrategies)); } + //Get map of CSGs to asset + let csgAssets = getCsgAssets(model.threats); + return { ...state, model: model, @@ -250,7 +260,8 @@ export default function modeller(state = modelState, action) { ...state.selectedMisbehaviour, misbehaviour: misbehaviour, }, - misbehaviourTwas: misbehaviourTwas + misbehaviourTwas: misbehaviourTwas, + csgAssets: csgAssets, }; } @@ -315,7 +326,6 @@ export default function modeller(state = modelState, action) { if (action.type === instr.EDIT_MODEL) { let updatedModel = action.payload; - //console.log("EDIT_MODEL:", updatedModel); return { ...state, model: { @@ -327,86 +337,19 @@ export default function modeller(state = modelState, action) { } if (action.type === instr.UPDATE_VALIDATION_PROGRESS) { - if (action.payload.waitingForUpdate) { - //console.log("poll: UPDATE_VALIDATION_PROGRESS: (waiting for progress)"); - return { - ...state, validationProgress: { - ...state.validationProgress, - waitingForUpdate: action.payload.waitingForUpdate - } - }; - } - - let status = "running"; - - if (action.payload.status) { - status = action.payload.status; - } - else if (action.payload.message.indexOf("failed") != -1) { - console.log("Validation failed (detected from message)"); - status = "failed"; - } - else if (action.payload.message.indexOf("complete") != -1) { - console.log("Validation completed (detected from message)"); - status = "completed"; - } - - let error = action.payload.error != null ? action.payload.error : ""; - - return { - ...state, validationProgress: { - status: status, - progress: action.payload.progress, - message: action.payload.message, - error: error, - waitingForUpdate: action.payload.waitingForUpdate - } - }; + return updateProgress("Validation", state, action); } if (action.type === instr.UPDATE_RISK_CALC_PROGRESS) { - //console.log("UPDATE_RISK_CALC_PROGRESS", action.payload); - if (action.payload.waitingForUpdate) { - //console.log("poll: UPDATE_RISK_CALC_PROGRESS: (waiting for progress)"); - return { - ...state, validationProgress: { - ...state.validationProgress, - waitingForUpdate: action.payload.waitingForUpdate - } - }; - } - - let status = "running"; - - if (action.payload.status) { - status = action.payload.status; - } - else if (action.payload.message.indexOf("failed") != -1) { - console.log("Risk calc failed (detected from message)"); - status = "failed"; - } - else if (action.payload.message.indexOf("complete") != -1) { - console.log("Risk calc completed (detected from message)"); - status = "completed"; - } - - let error = action.payload.error != null ? action.payload.error : ""; + return updateProgress("Risk calc", state, action); + } - return { - ...state, validationProgress: { - status: status, - progress: action.payload.progress, - message: action.payload.message, - error: error, - waitingForUpdate: action.payload.waitingForUpdate - } - }; + if (action.type === instr.UPDATE_RECOMMENDATIONS_PROGRESS) { + return updateProgress("Recommendations", state, action); } if (action.type === instr.UPDATE_LOADING_PROGRESS) { - //console.log("UPDATE_LOADING_PROGRESS:", action.payload); if (action.payload.waitingForUpdate) { - //console.log("poll: UPDATE_LOADING_PROGRESS: (waiting for progress)"); return { ...state, loadingProgress: { ...state.loadingProgress, @@ -855,9 +798,6 @@ export default function modeller(state = modelState, action) { if (action.type === instr.IS_VALIDATING) { - - console.log("modellerReducer: model is validating"); - return { ...state, model: { @@ -877,9 +817,6 @@ export default function modeller(state = modelState, action) { } if (action.type === instr.IS_NOT_VALIDATING) { - - console.log("modellerReducer: model is not validating"); - return { ...state, model: { @@ -897,7 +834,6 @@ export default function modeller(state = modelState, action) { } if (action.type === instr.VALIDATION_FAILED) { - console.log("modellerReducer: validation failed"); return { @@ -910,7 +846,6 @@ export default function modeller(state = modelState, action) { } if (action.type === instr.RISK_CALC_FAILED) { - console.log("modellerReducer: risk calc failed"); return { @@ -922,10 +857,19 @@ export default function modeller(state = modelState, action) { }; } - if (action.type === instr.IS_CALCULATING_RISKS) { + if (action.type === instr.RECOMMENDATIONS_FAILED) { + console.log("modellerReducer: recommendationsc failed"); - console.log("modellerReducer: calculating risks for model"); + return { + ...state, + model: { + ...state.model, + calculatingRecommendations: false + }, + }; + } + if (action.type === instr.IS_CALCULATING_RISKS) { return { ...state, model: { @@ -944,9 +888,6 @@ export default function modeller(state = modelState, action) { } if (action.type === instr.IS_NOT_CALCULATING_RISKS) { - - console.log("modellerReducer: not calculating risks for model"); - return { ...state, model: { @@ -1133,6 +1074,60 @@ export default function modeller(state = modelState, action) { } + if (action.type === instr.IS_CALCULATING_RECOMMENDATIONS) { + return { + ...state, + model: { + ...state.model, + calculatingRecommendations: true + }, + recommendations: null, //clear previous results + validationProgress: { + status: "starting", + progress: 0.0, + message: "Starting calculation", + error: "", + waitingForUpdate: false + } + }; + } + + if (action.type === instr.IS_NOT_CALCULATING_RECOMMENDATIONS) { + return { + ...state, + model: { + ...state.model, + calculatingRecommendations: false + }, + validationProgress: { + status: "inactive", + progress: 0.0, + message: "", + error: "", + waitingForUpdate: false + }, + }; + } + + if (action.type === instr.RECOMMENDATIONS_JOB_STARTED) { + console.log("Recommendations job started: ", action.payload); + let jobId = action.payload.jobId; + return { + ...state, + recommendationsJobId: jobId + }; + } + + if (action.type === instr.RECOMMENDATIONS_RESULTS) { + let recommendations = action.payload; + return { + ...state, + recommendations: recommendations, + isRecommendationsExplorerVisible: true, + isRecommendationsExplorerActive: true, + }; + } + if (action.type === instr.IS_DROPPING_INFERRED_GRAPH) { console.log("modellerReducer: dropping inferred graph for model"); @@ -1651,6 +1646,22 @@ export default function modeller(state = modelState, action) { }; } + if (action.type === instr.OPEN_RECOMMENDATIONS_EXPLORER) { + return { + ...state, + isRecommendationsExplorerVisible: true, + isRecommendationsExplorerActive: true, + }; + } + + if (action.type === instr.CLOSE_RECOMMENDATIONS_EXPLORER) { + return { + ...state, + isRecommendationsExplorerVisible: false, + isRecommendationsExplorerActive: false, + }; + } + if (action.type === instr.OPEN_REPORT_DIALOG) { console.log("OPEN_REPORT_DIALOG"); return { @@ -2303,6 +2314,43 @@ export default function modeller(state = modelState, action) { return state; } +function updateProgress(task, state, action) { + if (action.payload.waitingForUpdate) { + return { + ...state, validationProgress: { + ...state.validationProgress, + waitingForUpdate: action.payload.waitingForUpdate + } + }; + } + + let status = "running"; + + if (action.payload.status) { + status = action.payload.status; + } + else if (action.payload.message.indexOf("failed") != -1) { + console.log(task + " failed (detected from message)"); + status = "failed"; + } + else if (action.payload.message.indexOf("complete") != -1) { + console.log(task + " completed (detected from message)"); + status = "completed"; + } + + let error = action.payload.error != null ? action.payload.error : ""; + + return { + ...state, validationProgress: { + status: status, + progress: action.payload.progress, + message: action.payload.message, + error: error, + waitingForUpdate: action.payload.waitingForUpdate + } + }; +} + function getAttackPathThreatRefs(attackPathData) { const prefix = attackPathData.prefix; const sortedAttackThreats = Array.from(new Map(Object.entries(attackPathData.threats))) @@ -2311,6 +2359,21 @@ function getAttackPathThreatRefs(attackPathData) { return sortedAttackThreats; } +//Generate map of Control Strategies to their associated asset +function getCsgAssets(threats) { + let csgAssets = {}; + + threats.forEach(threat => { + let assetUri = threat.threatensAssets; + let threatCsgs = Object.keys(threat.controlStrategies); + threatCsgs.forEach(threatCsg => { + csgAssets[threatCsg] = assetUri; + }); + }); + + return csgAssets; +} + function updateControlStrategies(threats, controlStrategies, controlSets) { let controlSetsMap = {}; controlSets.forEach(cs => { diff --git a/src/main/webapp/app/modeller/reducers/view.js b/src/main/webapp/app/modeller/reducers/view.js index 016eae22..b0f7c543 100644 --- a/src/main/webapp/app/modeller/reducers/view.js +++ b/src/main/webapp/app/modeller/reducers/view.js @@ -1,11 +1,12 @@ import * as instr from "../modellerConstants"; -var _ = require('lodash'); +let _ = require('lodash'); let defaultState = { windowOrder: [ { 'name': 'controlExplorer', order: 1065 }, { 'name': 'controlStrategyExplorer', order: 1065 }, + { 'name': 'recommendationsExplorer', order: 1065 }, { 'name': 'misbehaviourExplorer', order: 1065 }, { 'name': 'complianceExplorer', order: 1065 }, { 'name': 'reportDialog', order: 1065 }, @@ -17,14 +18,13 @@ let defaultState = { 'misbehaviourExplorer': 1065, 'controlExplorer': 1065, 'controlStrategyExplorer': 1065, + 'recommendationsExplorer': 1065, }; -var highestWindowOrder = 1074; -var hiddenWindowOrder = 1065; - +let highestWindowOrder = 1074; +let hiddenWindowOrder = 1065; export default function view(state=defaultState, action) { - //console.log("view:", state, action); if (action.type === instr.OPEN_WINDOW) { let newWindowOrder = []; let newWindowObjects = {}; diff --git a/src/main/webapp/build.gradle b/src/main/webapp/build.gradle index b5949db1..423b588c 100644 --- a/src/main/webapp/build.gradle +++ b/src/main/webapp/build.gradle @@ -12,7 +12,7 @@ version '0.0.1' buildDir = 'dist' node { - version = '12.13.0' + version = '14.21.3' download = true yarnVersion = '1.21.1' } diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 00e61b6d..5ef1e96c 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -24,6 +24,7 @@ "react-contextmenu": "2.11.0", "react-dom": "16.13.1", "react-hot-loader": "4.13.1", + "react-json-view-lite": "^1.2.1", "react-portal": "4.1.5", "react-redux": "5.0.7", "react-rnd": "^10.3.4", diff --git a/src/main/webapp/webpack/webpack.dev.config.js b/src/main/webapp/webpack/webpack.dev.config.js index 748edee5..e14e2565 100644 --- a/src/main/webapp/webpack/webpack.dev.config.js +++ b/src/main/webapp/webpack/webpack.dev.config.js @@ -1,11 +1,12 @@ -var webpack = require("webpack"); -var HtmlWebpackPlugin = require("html-webpack-plugin"); -var path = require("path"); -var autoprefixer = require("autoprefixer"); +const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const path = require("path"); +const autoprefixer = require("autoprefixer"); -var ROOT = path.resolve(__dirname, "../"); -var SRC = path.resolve(ROOT, "app"); -var BUILD = path.join(ROOT, "dist"); +const ROOT = path.resolve(__dirname, "../"); +const SRC = path.resolve(ROOT, "app"); +const NODE_MODULES = path.resolve(ROOT, "node_modules"); +const BUILD = path.join(ROOT, "dist"); module.exports = { mode: 'development', @@ -40,7 +41,7 @@ module.exports = { ] }, { - test: /\.(sc|c)ss$/, + test: /\.scss$/, include: SRC, exclude: /node_modules/, use: [ @@ -63,6 +64,21 @@ module.exports = { loader: "sass-loader" // compiles SASS to CSS } ], + }, + { + test: /\.css$/, + include: [SRC, NODE_MODULES], + use: [ + { + loader: 'style-loader', // creates