Skip to content

Commit

Permalink
Merge pull request #211 from Spyderisk/178-link-from-system-modeller-…
Browse files Browse the repository at this point in the history
…ui-to-domain-model-docs

178 link from system modeller UI to domain model docs
  • Loading branch information
kenmeacham authored Jan 10, 2025
2 parents 96b8aaf + d06eb41 commit 771eb75
Show file tree
Hide file tree
Showing 20 changed files with 366 additions and 27 deletions.
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<String, String>> 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<String, String> row = rows.get(0);
return row.get(result);
}

return null;
}

// Assets /////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Get all system-specific assets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -467,6 +479,7 @@ public ResponseEntity<ModelDTO> getModel(@PathVariable String modelId, HttpServl
@RequestMapping(value = "/models/{modelId}/info", method = RequestMethod.GET)
public ResponseEntity<ModelDTO> 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);
Expand All @@ -483,6 +496,72 @@ public ResponseEntity<ModelDTO> 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<String, Object> 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)
Expand Down Expand Up @@ -1448,8 +1527,10 @@ public ResponseEntity<JobResponseDTO> calculateRecommendations(

final List<String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,28 @@ private Map<String, String> 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) {
Expand Down
25 changes: 25 additions & 0 deletions src/main/webapp/app/common/documentation/documentation.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
17 changes: 17 additions & 0 deletions src/main/webapp/app/modeller/actions/ModellerActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
24 changes: 24 additions & 0 deletions src/main/webapp/app/modeller/components/Modeller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
closeRecommendationsExplorer,
closeMisbehaviourExplorer,
closeReportDialog,
openDocsDialog,
closeDocsDialog,
getModel,
postAssertedAsset,
toggleThreatEditor,
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -360,6 +366,12 @@ class Modeller extends React.Component {
getAssetType={this.getAssetType}
/>

<ConfirmDocRedirectModal show={this.props.isDocsModalVisible}
onHide={this.closeDocsDialog}
model={this.props.model}
selectedDocEntity={this.props.selectedDocEntity}
/>

{this.props.loading.newFact.length > 0 && <div className="creation-overlay visible"><span
className="fa fa-refresh fa-spin fa-3x fa-fw"/><h1>Creating new {this.props.loading.newFact[0]}...</h1>
</div>}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 771eb75

Please sign in to comment.