diff --git a/.env.template b/.env.template index 59d15f00..1df5d71a 100644 --- a/.env.template +++ b/.env.template @@ -27,6 +27,9 @@ # Location of the Spyderisk System Modeller documentation to be used in the web application: #DOCUMENTATION_URL=https://spyderisk.org/documentation/modeller/latest/ +# Location of knowledgebase query (script) +KNOWLEDGEBASE_DOCS_QUERY_URL=https://docs.spyderisk.org/knowledgebase/q + # Set this if the realm name is different to the default value of 'ssm-realm'. #KEYCLOAK_REALM=ssm-realm diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 2204e5a1..99677c12 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -16,6 +16,7 @@ services: # Use the DOCKER_GATEWAY_HOST value (in `.env` file) if it is set (used in Linux), fall back on "host.docker.internal" # See https://sthasantosh.com.np/2020/05/23/docker-tip-how-to-use-the-hosts-ip-address-inside-a-docker-container-on-macos-windows-and-linux/ KEYCLOAK_AUTH_SERVER_URL: http://${DOCKER_GATEWAY_HOST:-host.docker.internal}:${KEYCLOAK_PORT:-8080}/auth/ + KNOWLEDGEBASE_DOCS_QUERY_URL: ${KNOWLEDGEBASE_DOCS_QUERY_URL:-https://docs.spyderisk.org/knowledgebase/q} volumes: # Volumes of type "bind" mount a folder from the host machine. diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d3791530..d40ad817 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -3,8 +3,6 @@ # This docker-compose file is used by the CI pipeline to execute the test. # The tests can be executed using e.g. `docker compose exec -T ssm sh -c 'cd /system-modeller && gradle test'` -version: '3.7' - services: ssm: @@ -15,3 +13,5 @@ services: # Port 8080 is the one exposed by keycloak by default and not related to any port-mapping. # Environment variables are not available at build time (only at runtime). KEYCLOAK_AUTH_SERVER_URL: http://keycloak:8080/auth/ + # The following value is a placeholder, as it is not currently used in tests, but needs to be defined + KNOWLEDGEBASE_DOCS_QUERY_URL: https://docs.spyderisk.org/knowledgebase/q diff --git a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelQuerier.java b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelQuerier.java index 166c171d..dda4ce0e 100644 --- a/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelQuerier.java +++ b/src/main/java/uk/ac/soton/itinnovation/security/modelquerier/SystemModelQuerier.java @@ -167,6 +167,53 @@ public Model getModelInfo(AStoreWrapper store) { return m; } + /** + * Gets the domain model type of a given system model entity + * + * @param store the store to query + * @param uri the entity uri to query + * @return type of the entity + */ + public String getSystemEntityType(AStoreWrapper store, String uri) { + String subQuery = "?uri core:parent ?parent ."; + return getEntityType(store, "system-inf", subQuery, uri, "parent"); + } + + /** + * Gets the domain model type of a given domain model entity + * + * @param store the store to query + * @param uri the entity uri to query + * @return type of the entity + */ + public String getDomainEntityType(AStoreWrapper store, String uri) { + String subQuery = "?uri rdf:type ?type ."; + return getEntityType(store, "domain", subQuery, uri, "type"); + } + + private String getEntityType(AStoreWrapper store, String graph, String subQuery, String uri, String result) { + String query = String.format("\r\nSELECT DISTINCT * WHERE {\r\n" + + " GRAPH <%s> {\r\n" + + (uri != null ? " BIND (<" + SparqlHelper.escapeURI(uri) + "> as ?uri) .\n" : "") + + " " + subQuery + "\r\n" + + " }\r\n" + + "}", model.getGraph(graph)); + + List> rows = store.translateSelectResult(store.querySelect(query, + model.getGraph(graph) + )); + + if (rows.size() > 1) { + throw new RuntimeException("Duplicate entries found for uri: " + uri); + } + else if (rows.size() == 1) { + Map row = rows.get(0); + return row.get(result); + } + + return null; + } + // Assets ///////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Get all system-specific assets 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 f99f9ee9..64355319 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,7 +32,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -78,6 +81,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.ModelAndView; import com.fasterxml.jackson.databind.ObjectMapper; @@ -90,7 +94,6 @@ 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; @@ -123,7 +126,6 @@ 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; @@ -166,11 +168,21 @@ public class ModelController { @Value("${knowledgebases.install.folder}") private String kbInstallFolder; + @Value("${knowledgebase.docs.query.url}") + private String kbDocsQueryUrl; + 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"; + //Regex for fixing security vulnnerability + private static final String PARAM_REGEX = "[\n\r]"; + + private String encodeValue(String value) throws UnsupportedEncodingException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } + /** * Take the user IDs of the model owner, editor and modifier and look up the current username for them */ @@ -467,6 +479,7 @@ public ResponseEntity getModel(@PathVariable String modelId, HttpServl @RequestMapping(value = "/models/{modelId}/info", method = RequestMethod.GET) public ResponseEntity getModelInfo(@PathVariable String modelId, HttpServletRequest servletRequest) throws UnexpectedException { + modelId = modelId.replaceAll(PARAM_REGEX, "_"); logger.info("Called REST method to GET model info {}", modelId); final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); @@ -483,6 +496,72 @@ public ResponseEntity getModelInfo(@PathVariable String modelId, HttpS return ResponseEntity.status(HttpStatus.OK).body(responseModel); } + /** + * Redirects to the domain model documentation page for a given entity URI + * + * @param modelId Webkey of the model + * @param entity the domain model entity URI + * @param servletRequest + * @return the domain model webpage + */ + @GetMapping(value = "/models/{modelId}/docs") + public ModelAndView getModelDocs(@PathVariable String modelId, @RequestParam() String entity, HttpServletRequest servletRequest) { + + modelId = modelId.replaceAll(PARAM_REGEX, "_"); + entity = entity.replaceAll(PARAM_REGEX, "_"); + logger.info("Called REST method to GET model docs {}", modelId); + + final Model model = secureUrlHelper.getModelFromUrlThrowingException(modelId, WebKeyRole.READ); + + //Get basic model details only + model.loadModelInfo(modelObjectsHelper); + + String domainGraph = model.getDomainGraph(); + logger.debug("domainGraph: {}", domainGraph); + + Map domainModel = storeModelManager.getDomainModel(domainGraph); + String domainModelName = ((String)domainModel.get("label")).toLowerCase(); + logger.debug("domainModelName: {}", domainModelName); + + String domainModelVersion = model.getDomainVersion(); + logger.debug("domainModelVersion: {}", domainModelVersion); + + String validatedDomainModelVersion = model.getValidatedDomainVersion(); + logger.debug("validatedDomainModelVersion: {}", validatedDomainModelVersion); + + String docHome = kbDocsQueryUrl; + logger.debug("docHome: {}", docHome); + + String domainEntityUri; + + if (entity.contains("system#")) { + //First get the domain type for this system entity + logger.debug("system entity: {}", entity); + domainEntityUri = this.modelObjectsHelper.getSystemEntityType(model, entity); + } + else { //assume domain# + domainEntityUri = entity; + } + + logger.debug("domain entity: {}", domainEntityUri); + String typeUri = this.modelObjectsHelper.getDomainEntityType(model, domainEntityUri); + + logger.debug("typeUri: {}", typeUri); + + if (typeUri != null) { + try { + String docURL = docHome + "?domain=" + domainModelName + "&version=" + validatedDomainModelVersion + + "&type=" + encodeValue(typeUri) + "&entity=" + encodeValue(domainEntityUri); + logger.info("Redirecting to: {}", docURL); + return new ModelAndView("redirect:" + docURL); + } catch (UnsupportedEncodingException e) { + logger.error("Could not encode URI", e); + throw new NotFoundErrorException("Could not encode URI"); + } + } + + return null; + } /** * Gets the basic model details and risks data (only) @@ -1448,8 +1527,10 @@ public ResponseEntity calculateRecommendations( final List finalTargetURIs = targetURIs; + modelId = modelId.replaceAll(PARAM_REGEX, "_"); + riskMode = riskMode.replaceAll(PARAM_REGEX, "_"); + logger.info("Calculating recommendations for model {}", modelId); - riskMode = riskMode.replaceAll("[\n\r]", "_"); logger.info(" riskMode: {}",riskMode); RiskCalculationMode rcMode; 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 9e002b23..2e15c74f 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 @@ -615,6 +615,28 @@ private Map loadQueries() throws IOException { // Get from store ///////////////////////////////////////////////////////////////////////////////////////////////// // These should be called from the REST controllers to get things from the store + /** + * Gets the domain model type of a given system model entity + * + * @param model + * @param entity + * @return entity type + */ + public String getSystemEntityType(Model model, String entity) { + return model.getQuerier().getSystemEntityType(storeManager.getStore(), entity); + } + + /** + * Gets the domain model type of a given domain model entity + * + * @param model + * @param entity + * @return entity type + */ + public String getDomainEntityType(Model model, String entity) { + return model.getQuerier().getDomainEntityType(storeManager.getStore(), entity); + } + public Asset getAssetById(String assetId, Model model, boolean fullDetails) { if (fullDetails) { diff --git a/src/main/webapp/app/common/documentation/documentation.js b/src/main/webapp/app/common/documentation/documentation.js index 09555049..635f16ec 100644 --- a/src/main/webapp/app/common/documentation/documentation.js +++ b/src/main/webapp/app/common/documentation/documentation.js @@ -1,8 +1,33 @@ +import {openDocsDialog} from "../../modeller/actions/ModellerActions"; + export function openDocumentation(e, link) { e.stopPropagation(); window.open("/documentation/" + link, "system-modeller-docs", "noopener"); } +export function openDomainDocEvent(e, model, entity, dispatch) { + e.stopPropagation(); + + let modelId = model.id; + let domainVersion = model.domainVersion; + let validatedDomainVersion = model.validatedDomainVersion; + + let versionMismatch = (validatedDomainVersion !== domainVersion); + + if (versionMismatch) { + dispatch(openDocsDialog(entity)); + } + else { + openDomainDoc(modelId, entity); + } +} + +export function openDomainDoc(modelId, entity) { + let docUrl = "/system-modeller/models/" + modelId + "/docs?entity=" + encodeURIComponent(entity); + window.open(docUrl, + "domain-model-docs", "noopener"); +} + export function openApiDocs(e) { e.stopPropagation(); window.open("/system-modeller/swagger-ui.html", "openapi-docs", "noopener"); diff --git a/src/main/webapp/app/modeller/actions/ModellerActions.js b/src/main/webapp/app/modeller/actions/ModellerActions.js index 9d903023..6f894e0c 100644 --- a/src/main/webapp/app/modeller/actions/ModellerActions.js +++ b/src/main/webapp/app/modeller/actions/ModellerActions.js @@ -1364,6 +1364,23 @@ export function closeReportDialog() { }; } +export function openDocsDialog(entity) { + return function (dispatch) { + dispatch({ + type: instr.OPEN_DOCS_DIALOG, + payload: {entity: entity} + }); + }; +} + +export function closeDocsDialog() { + return function (dispatch) { + dispatch({ + type: instr.CLOSE_DOCS_DIALOG + }); + }; +} + export function updateControlOnAsset(modelId, assetId, updatedControl) { //build request body for updated CS let updatedCS = { diff --git a/src/main/webapp/app/modeller/components/Modeller.js b/src/main/webapp/app/modeller/components/Modeller.js index 56ff9d6c..a7d738a2 100644 --- a/src/main/webapp/app/modeller/components/Modeller.js +++ b/src/main/webapp/app/modeller/components/Modeller.js @@ -10,6 +10,8 @@ import { closeRecommendationsExplorer, closeMisbehaviourExplorer, closeReportDialog, + openDocsDialog, + closeDocsDialog, getModel, postAssertedAsset, toggleThreatEditor, @@ -31,6 +33,8 @@ import ControlPane from "./panes/controls/controlPane/ControlPane"; import OverviewPane from "./panes/controls/overviewPane/OverviewPane"; import Canvas from "./canvas/Canvas"; import ReportDialog from "./panes/reports/ReportDialog"; +import ConfirmDocRedirectModal from "./panes/common/popups/ConfirmDocRedirectModal"; + import * as Constants from "../../common/constants.js" import "../index.scss"; import { axiosInstance } from "../../common/rest/rest"; @@ -71,6 +75,8 @@ class Modeller extends React.Component { this.closeControlStrategyExplorer = this.closeControlStrategyExplorer.bind(this); this.closeRecommendationsExplorer = this.closeRecommendationsExplorer.bind(this); this.closeReportDialog = this.closeReportDialog.bind(this); + this.openDocsDialog = this.openDocsDialog.bind(this); + this.closeDocsDialog = this.closeDocsDialog.bind(this); this.populateThreatMisbehaviours = this.populateThreatMisbehaviours.bind(this); this.getSystemThreats = this.getSystemThreats.bind(this); this.getComplianceSetsData = this.getComplianceSetsData.bind(this); @@ -360,6 +366,12 @@ class Modeller extends React.Component { getAssetType={this.getAssetType} /> + + {this.props.loading.newFact.length > 0 &&

Creating new {this.props.loading.newFact[0]}...

} @@ -719,6 +731,14 @@ class Modeller extends React.Component { this.props.dispatch(closeReportDialog()); } + openDocsDialog() { + this.props.dispatch(openDocsDialog()); + } + + closeDocsDialog() { + this.props.dispatch(closeDocsDialog()); + } + /** * AssertedAsset Creation: Called through PaletteAsset -> AssetList -> AssetPanel -> Modeller (dispatched) * @param assetTypeId The asset type to use in the creation. @@ -1052,6 +1072,8 @@ var mapStateToProps = function (state) { isRecommendationsExplorerActive: state.modeller.isRecommendationsExplorerActive, isReportDialogVisible: state.modeller.isReportDialogVisible, isReportDialogActive: state.modeller.isReportDialogActive, + isDocsModalVisible: state.modeller.isDocsModalVisible, + selectedDocEntity: state.modeller.selectedDocEntity, threatFiltersActive: state.modeller.threatFiltersActive, isAcceptancePanelActive: state.modeller.isAcceptancePanelActive, loading: state.modeller.loading, @@ -1106,6 +1128,8 @@ Modeller.propTypes = { isMisbehaviourExplorerActive: PropTypes.bool, isReportDialogVisible: PropTypes.bool, isReportDialogActive: PropTypes.bool, + isDocsModalVisible: PropTypes.bool, + selectedDocEntity: PropTypes.string, threatFiltersActive: PropTypes.object, isAcceptancePanelActive: PropTypes.bool, reportType: PropTypes.string, diff --git a/src/main/webapp/app/modeller/components/panes/common/popups/ConfirmDocRedirectModal.js b/src/main/webapp/app/modeller/components/panes/common/popups/ConfirmDocRedirectModal.js new file mode 100644 index 00000000..380df059 --- /dev/null +++ b/src/main/webapp/app/modeller/components/panes/common/popups/ConfirmDocRedirectModal.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React, { Component } from "react"; +import { Button, Modal } from "react-bootstrap"; +import {openDomainDoc} from "../../../../../common/documentation/documentation"; + +class ConfirmDocRedirectModal extends Component { + + constructor(props) { + super(props); + } + + render() { + const {model, selectedDocEntity, ...modalProps} = this.props; + + let domainVersion = this.props.model.domainVersion; + let validatedDomainVersion = this.props.model.validatedDomainVersion; + let versionWarningText = "Requested knowledgebase version (" + validatedDomainVersion + ") used in this system model does not match current knowledgebase (" + domainVersion + "). Continue to docs anyway?"; + + return ( + + + Versions Mismatch! + + +

{versionWarningText}

+
+ + + + +
+ ); + } +} + +ConfirmDocRedirectModal.propTypes = { + model: PropTypes.object, + selectedDocEntity: PropTypes.string, + show: PropTypes.bool, + onHide: PropTypes.func, +}; + +export default ConfirmDocRedirectModal; 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 01a6af00..0a75915c 100644 --- a/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js +++ b/src/main/webapp/app/modeller/components/panes/controlExplorer/ControlExplorer.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ControlAccordion from "./ControlAccordion"; import {connect} from "react-redux"; import Explorer from "../common/Explorer" +import {openDomainDocEvent} from "../../../../common/documentation/documentation"; class ControlExplorer extends React.Component { @@ -52,6 +53,7 @@ class ControlExplorer extends React.Component {

{label} +

{description} 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 821dc4ca..145434ee 100644 --- a/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js +++ b/src/main/webapp/app/modeller/components/panes/csgExplorer/ControlStrategyExplorer.js @@ -7,6 +7,7 @@ import { } from "../../../../modeller/actions/ModellerActions"; import {connect} from "react-redux"; import Explorer from "../common/Explorer" +import {openDomainDocEvent} from "../../../../common/documentation/documentation"; class ControlStrategyExplorer extends React.Component { @@ -89,6 +90,12 @@ class ControlStrategyExplorer extends React.Component { return controlSets; } + renderDocButton(csg) { + return ( + + ) + } + renderDescription(csg, context) { if (!context) return null; @@ -97,15 +104,15 @@ class ControlStrategyExplorer extends React.Component { if (selection === "csg") { let asset = context.asset; - return (

{csg.label} at "{asset.label}"

); + return (

{csg.label} at "{asset.label}" {this.renderDocButton(csg)}

); } else if (selection === "csgType") { - return (

{csg.label} (all occurrences)

); + return (

{csg.label} (all occurrences) {this.renderDocButton(csg)}

); } else if (selection === "controlSet") { let cs = context.controlSet; let asset = this.getAssetByUri(cs.assetUri); - return (

Control Strategies including: {cs.label} at "{asset.label}"

); + return (

Control Strategies including: {cs.label} at "{asset.label}" {this.renderDocButton(csg)}

); } else { return null; diff --git a/src/main/webapp/app/modeller/components/panes/details/DetailPane.js b/src/main/webapp/app/modeller/components/panes/details/DetailPane.js index 6a119347..dae023c8 100644 --- a/src/main/webapp/app/modeller/components/panes/details/DetailPane.js +++ b/src/main/webapp/app/modeller/components/panes/details/DetailPane.js @@ -16,6 +16,7 @@ import { putAssertedAssetType } from "../../../actions/ModellerActions"; import {renderPopulationLevel} from "../../util/Levels"; +import {openDomainDocEvent} from "../../../../common/documentation/documentation"; class DetailPane extends React.Component { @@ -161,11 +162,11 @@ class DetailPane extends React.Component { */ render() { let asset = this.state.asset; - var assetType = this.props.getAssetType(asset["type"]); + let assetType = this.props.getAssetType(asset["type"]); //Check assetType. If this is null, display default icon (this can happen if we are using the wrong palette, for example) - var defaultIcon = "fallback.svg"; - var icon = assetType ? assetType["icon"] : defaultIcon; + let defaultIcon = "fallback.svg"; + let icon = assetType ? assetType["icon"] : defaultIcon; const icon_path = process.env.config.API_END_POINT + "/images/" + icon; if (assetType === null) { @@ -173,7 +174,7 @@ class DetailPane extends React.Component { alert("ERROR: No palette icon for asset: " + asset["type"]); } - var iconStyles = { + let iconStyles = { backgroundImage: "url(" + icon_path + ")", backgroundSize: "contain", backgroundRepeat: "no-repeat", @@ -225,6 +226,7 @@ class DetailPane extends React.Component { } } +

{assetType["description"] !== "" ? "Description: " : ""}{assetType["description"] === null ? "None" : assetType["description"]} diff --git a/src/main/webapp/app/modeller/components/panes/details/popups/RootCausesEditor.js b/src/main/webapp/app/modeller/components/panes/details/popups/RootCausesEditor.js index dc9e3020..eab6fac6 100644 --- a/src/main/webapp/app/modeller/components/panes/details/popups/RootCausesEditor.js +++ b/src/main/webapp/app/modeller/components/panes/details/popups/RootCausesEditor.js @@ -8,7 +8,7 @@ import MisbehaviourAccordion from "../../misbehaviours/accordion/MisbehaviourAcc import {Rnd} from "react-rnd"; import {connect} from "react-redux"; import * as Constants from "../../../../../common/constants.js"; -import {openDocumentation} from "../../../../../common/documentation/documentation"; +import {openDocumentation, openDomainDocEvent} from "../../../../../common/documentation/documentation"; var _ = require('lodash'); @@ -237,6 +237,7 @@ class RootCausesEditor extends React.Component { {consequenceLabelHeading} {" at "} {assetLabelHeading} + {this.props.developerMode &&

{misbehaviour.uri}

}

{consequenceDesc}

@@ -354,6 +355,7 @@ RootCausesEditor.propTypes = { loadingCausesAndEffects: PropTypes.bool, hoverThreat: PropTypes.func, developerMode: PropTypes.bool, + dispatch: PropTypes.func, }; let mapStateToProps = function (state) { diff --git a/src/main/webapp/app/modeller/components/panes/threats/ThreatEditor.js b/src/main/webapp/app/modeller/components/panes/threats/ThreatEditor.js index 6fba72a1..89821e2d 100644 --- a/src/main/webapp/app/modeller/components/panes/threats/ThreatEditor.js +++ b/src/main/webapp/app/modeller/components/panes/threats/ThreatEditor.js @@ -10,7 +10,7 @@ import {changeSelectedAsset} from "../../../actions/ModellerActions"; import {getRenderedLevelText} from "../../util/Levels"; import {bringToFrontWindow, closeWindow} from "../../../actions/ViewActions"; import {connect} from "react-redux"; -import {openDocumentation} from "../../../../common/documentation/documentation" +import {openDocumentation, openDomainDocEvent} from "../../../../common/documentation/documentation" import {getThreatStatus} from "../../util/ThreatUtils.js"; class ThreatEditor extends React.Component { @@ -53,7 +53,14 @@ class ThreatEditor extends React.Component { return shouldComponentUpdate; } + renderDocButton(threat) { + return ( + + ) + } + render() { + console.log("modelId:", this.props.model.id); let threat = this.props.threat; let asset; @@ -235,6 +242,7 @@ class ThreatEditor extends React.Component { {threatType} {threat.isModellingError ? " Error at " : " Threat to "} {assetLabelHeading} + {this.renderDocButton(threat)}
{this.props.developerMode &&

{threat.uri}

} @@ -274,7 +282,7 @@ class ThreatEditor extends React.Component { relations={this.props.model["relations"]} controlStrategies={this.props.model["controlStrategies"]} controlSets={this.props.model["controlSets"]} - modelId={this.props.model["id"]} + model={this.props.model} threat={threat} threatStatus={status} triggeredStatus={triggeredStatus} @@ -322,6 +330,7 @@ ThreatEditor.propTypes = { renderTrustworthinessAttributes: PropTypes.func, authz: PropTypes.object, developerMode: PropTypes.bool, + dispatch: PropTypes.func, }; let mapStateToProps = function (state) { diff --git a/src/main/webapp/app/modeller/components/panes/threats/accordion/ThreatAccordion.js b/src/main/webapp/app/modeller/components/panes/threats/accordion/ThreatAccordion.js index 26853e9a..26fe8582 100644 --- a/src/main/webapp/app/modeller/components/panes/threats/accordion/ThreatAccordion.js +++ b/src/main/webapp/app/modeller/components/panes/threats/accordion/ThreatAccordion.js @@ -62,6 +62,8 @@ class ThreatAccordion extends React.Component { return null; } + console.log("modelId:", this.props.model.id); + let isComplianceThreat = this.props.threat.isComplianceThreat; //N.B. The causes, effects, secondaryEffects are set in populateThreatMisbehaviours() @@ -89,7 +91,7 @@ class ThreatAccordion extends React.Component { -
@@ -70,7 +75,7 @@ class PatternPanel extends React.Component {
- {threatLabel} + {threatLabel}

@@ -83,7 +88,7 @@ class PatternPanel extends React.Component { onMouseEnter={() => this.hoverPattern(true)} onMouseLeave={() => this.hoverPattern(false)} > - {this.props.threat.pattern.label} + {patternLabel}
@@ -501,7 +506,7 @@ class PatternPanel extends React.Component { console.log("Deleting relation: " + relation["id"]); this.props.dispatch(suppressCanvasRefresh(false)); var typeSuffix = relation["type"].split("#")[1] - this.props.dispatch(deleteAssertedRelation(this.props.modelId, relation["relationId"], relation["fromId"], typeSuffix, relation["toId"])); + this.props.dispatch(deleteAssertedRelation(this.props.model.id, relation["relationId"], relation["fromId"], typeSuffix, relation["toId"])); } else { alert("ERROR: Could not locate relation"); @@ -510,7 +515,7 @@ class PatternPanel extends React.Component { } PatternPanel.propTypes = { - modelId: PropTypes.string, + model: PropTypes.object, threat: PropTypes.object, asset: PropTypes.object, assets: PropTypes.array, diff --git a/src/main/webapp/app/modeller/index.scss b/src/main/webapp/app/modeller/index.scss index c651596f..ecf8d396 100644 --- a/src/main/webapp/app/modeller/index.scss +++ b/src/main/webapp/app/modeller/index.scss @@ -2000,6 +2000,18 @@ div.panel-title { border: rgba(0, 0, 0, 0); } +h4 .doc-help-button { + display: inline-block; +} + +p .doc-help-button { + display: inline-block; +} + +span .doc-help-button { + display: inline-block; +} + .panel-body .asset-list { font-size: 11px; } diff --git a/src/main/webapp/app/modeller/modellerConstants.js b/src/main/webapp/app/modeller/modellerConstants.js index 865356e9..8e5d3234 100644 --- a/src/main/webapp/app/modeller/modellerConstants.js +++ b/src/main/webapp/app/modeller/modellerConstants.js @@ -48,6 +48,8 @@ export const SIDE_PANEL_ACTIVATED = "SIDE_PANEL_ACTIVATED"; export const SIDE_PANEL_DEACTIVATED = "SIDE_PANEL_DEACTIVATED"; export const OPEN_REPORT_DIALOG = "OPEN_REPORT_DIALOG"; export const CLOSE_REPORT_DIALOG = "CLOSE_REPORT_DIALOG"; +export const OPEN_DOCS_DIALOG = "OPEN_DOCS_DIALOG"; +export const CLOSE_DOCS_DIALOG = "CLOSE_DOCS_DIALOG"; export const CLOSE_MISBEHAVIOUR_EXPLORER = "CLOSE_MISBEHAVIOUR_EXPLORER"; export const OPEN_COMPLIANCE_EXPLORER = "OPEN_COMPLIANCE_EXPLORER"; export const OPEN_CONTROL_EXPLORER = "OPEN_CONTROL_EXPLORER"; diff --git a/src/main/webapp/app/modeller/reducers/modeller.js b/src/main/webapp/app/modeller/reducers/modeller.js index 5c793c07..4621d48f 100644 --- a/src/main/webapp/app/modeller/reducers/modeller.js +++ b/src/main/webapp/app/modeller/reducers/modeller.js @@ -97,6 +97,8 @@ const modelState = { isRecommendationsExplorerActive: false, isReportDialogVisible: false, isReportDialogActive: false, + isDocsModalVisible: false, + selectedDocEntity: "", isDroppingInferredGraph: false, threatFiltersActive: { "asset-threats": false, @@ -1681,6 +1683,23 @@ export default function modeller(state = modelState, action) { }; } + if (action.type === instr.OPEN_DOCS_DIALOG) { + let entity = action.payload["entity"]; + return { + ...state, + isDocsModalVisible: true, + selectedDocEntity: entity + }; + } + + if (action.type === instr.CLOSE_DOCS_DIALOG) { + return { + ...state, + isDocsModalVisible: false, + selectedDocEntity: "" + }; + } + if (action.type === instr.TOGGLE_THREAT_EDITOR) { let isThreatEditorVisible = action.payload["toggle"]; //console.log(state.selectedAsset);