Skip to content

Commit

Permalink
feat(ThreatModelView): Add next step suggestions (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessieweiyi authored Aug 15, 2023
1 parent d031d92 commit 81ed842
Show file tree
Hide file tree
Showing 15 changed files with 192 additions and 44 deletions.
22 changes: 22 additions & 0 deletions packages/threat-composer-app/public/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting to Threat Composer</title>
<script type="text/javascript">
var pathSegmentsToKeep = 0;

var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);

</script>
</head>
<body>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ const Full: FC = () => {
navigate(generateUrl(ROUTE_WORKSPACE_HOME, searchParms, newWorkspaceId));
}, [navigate, workspaceId, searchParms]);

const handleNavigationView = useCallback((route: string) => {
navigate(generateUrl(route, searchParms, workspaceId));
}, [navigate]);

const handleThreatListView = useCallback((filter?: ThreatStatementListFilter) => {
navigate(generateUrl(ROUTE_THREAT_LIST, searchParms, workspaceId), {
state: filter ? {
Expand Down Expand Up @@ -143,6 +147,11 @@ const Full: FC = () => {
<ContextAggregator
composerMode='Full'
onWorkspaceChanged={handleWorkspaceChanged}
onApplicationInfoView={() => handleNavigationView(ROUTE_APPLICATION_INFO)}
onArchitectureView={() => handleNavigationView(ROUTE_ARCHITECTURE_INFO)}
onDataflowView={() => handleNavigationView(ROUTE_DATAFLOW_INFO)}
onAssumptionListView={() => handleNavigationView(ROUTE_ASSUMPTION_LIST)}
onMitigationListView={() => handleNavigationView(ROUTE_MITIGATION_LIST)}
onThreatListView={handleThreatListView}
onThreatEditorView={handleThreatEditorView}
onPreview={handlePreview}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
limitations under the License.
******************************************************************************************************************** */
/** @jsxImportSource @emotion/react */
import { SpaceBetween } from '@cloudscape-design/components';
import Box from '@cloudscape-design/components/box';
import Button from '@cloudscape-design/components/button';
import Header from '@cloudscape-design/components/header';
import Popover from '@cloudscape-design/components/popover';
import SpaceBetween from '@cloudscape-design/components/space-between';
import Spinner from '@cloudscape-design/components/spinner';
import StatusIndicator from '@cloudscape-design/components/status-indicator';
import { FC, useEffect, useCallback, useState } from 'react';
import { DataExchangeFormat } from '../../../../../customTypes';
import * as awsui from '@cloudscape-design/design-tokens';
import { css } from '@emotion/react';
import { FC, useEffect, useCallback, useState, ReactNode } from 'react';
import { DataExchangeFormat, HasContentDetails, ViewNavigationEvent } from '../../../../../customTypes';
import printStyles from '../../../../../styles/print';
import sanitizeHtml from '../../../../../utils/sanitizeHtml';
import MarkdownViewer from '../../../../generic/MarkdownViewer';
Expand All @@ -33,43 +37,89 @@ import { getDataflowContent } from '../../utils/getDataFlow';
import { getMitigationsContent } from '../../utils/getMitigations';
import { getThreatsContent } from '../../utils/getThreats';

export interface ThreatModelViewProps {
const styles = {
text: css({
display: 'flex',
alignItems: 'center',
height: '100%',
}),
nextStepsContainer: css({
borderTop: `2px solid ${awsui.colorBorderDividerDefault}`,
paddingTop: awsui.spaceScaledS,
}),
noData: css({
textAlign: 'center',
width: '100%',
}),
};

export interface ThreatModelViewProps extends ViewNavigationEvent {
composerMode: string;
data: DataExchangeFormat;
onPrintButtonClick?: () => void;
hasContentDetails?: HasContentDetails;
}

const ThreatModelView: FC<ThreatModelViewProps> = ({
data,
composerMode,
onPrintButtonClick,
hasContentDetails,
...props
}) => {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);

useEffect(() => {
const updateContent = async () => {
setLoading(true);
const sanitizedData = sanitizeHtml(data);
const processedContent = (composerMode === 'Full' ? [
await getApplicationName(sanitizedData),
await getApplicationInfoContent(sanitizedData),
await getArchitectureContent(sanitizedData),
await getDataflowContent(sanitizedData),
await getAssumptionsContent(sanitizedData),
await getThreatsContent(sanitizedData),
await getMitigationsContent(sanitizedData),
await getAssetsContent(sanitizedData),
hasContentDetails?.applicationName && await getApplicationName(sanitizedData),
hasContentDetails?.applicationInfo && await getApplicationInfoContent(sanitizedData),
hasContentDetails?.architecture && await getArchitectureContent(sanitizedData),
hasContentDetails?.dataflow && await getDataflowContent(sanitizedData),
hasContentDetails?.assumptions && await getAssumptionsContent(sanitizedData),
hasContentDetails?.threats && await getThreatsContent(sanitizedData),
hasContentDetails?.mitigations && await getMitigationsContent(sanitizedData),
hasContentDetails?.threats && await getAssetsContent(sanitizedData),
] : [await getThreatsContent(sanitizedData, true)]).filter(x => !!x).join('\n');

setContent(processedContent);
setLoading(false);
};

updateContent().catch(err => console.log('Error', err));
}, [data, composerMode]);
}, [data, composerMode, hasContentDetails]);

const handleCopyMarkdown = useCallback(async () => {
await navigator.clipboard.writeText(content);
}, [content]);

const getNextStepButtons = useCallback(() => {
const buttons: ReactNode[] = [];
if (!hasContentDetails?.applicationInfo) {
buttons.push(<Button onClick={props.onApplicationInfoView}>Add Application Info</Button>);
}
if (!hasContentDetails?.architecture) {
buttons.push(<Button onClick={props.onArchitectureView}>Add Architecture</Button>);
}
if (!hasContentDetails?.dataflow) {
buttons.push(<Button onClick={props.onDataflowView}>Add Dataflow</Button>);
}
if (!hasContentDetails?.assumptions) {
buttons.push(<Button onClick={props.onAssumptionListView}>Add Assumptions</Button>);
}
if (!hasContentDetails?.threats) {
buttons.push(<Button onClick={() => props.onThreatListView?.()}>Add Threats</Button>);
}
if (!hasContentDetails?.threats) {
buttons.push(<Button onClick={props.onMitigationListView}>Add Mitigations</Button>);
}
const len = buttons.length;
return buttons.flatMap((b, index) => index === len - 1 ? <Box>{b}</Box> : [b, <Box fontWeight="bold" css={styles.text}>or</Box>]);
}, [hasContentDetails, props]);

return (<div>
<SpaceBetween direction='vertical' size='s'>
<div css={printStyles.hiddenPrint}><Header
Expand All @@ -96,7 +146,18 @@ const ThreatModelView: FC<ThreatModelViewProps> = ({
}
>
</Header></div>
<MarkdownViewer allowHtml>{content}</MarkdownViewer>
{content ?
(<MarkdownViewer allowHtml>{content}</MarkdownViewer>) :
(<Box fontSize='body-m' margin='xxl' fontWeight="bold" css={styles.noData}>{loading ? <Spinner /> : 'No data available'}</Box>)
}
{composerMode === 'Full' && hasContentDetails && Object.values(hasContentDetails).some(x => !x) && <div css={printStyles.hiddenPrint}>
<Box css={styles.nextStepsContainer}>
<SpaceBetween direction="horizontal" size="xs">
<Box fontWeight="bold" css={styles.text}>Suggested next steps: </Box>
{getNextStepButtons()}
</SpaceBetween>
</Box>
</div>}
</SpaceBetween>
</div>);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
******************************************************************************************************************** */
import { FC } from 'react';
import ThreatModelView from './components/ThreatModelView';
import { useGlobalSetupContext } from '../../../contexts';
import { useGlobalSetupContext, useWorkspacesContext } from '../../../contexts';
import useImportExport from '../../../hooks/useExportImport';
import useHasContent from '../../../hooks/useHasContent';

export interface ThreatModelProps {
onPrintButtonClick?: () => void;
Expand All @@ -27,10 +28,26 @@ const ThreatModel: FC<ThreatModelProps> = ({
}) => {
const { getWorkspaceData } = useImportExport();
const { composerMode } = useGlobalSetupContext();
const [_, hasContentDetails] = useHasContent();
const {
onApplicationInfoView,
onArchitectureView,
onDataflowView,
onAssumptionListView,
onThreatListView,
onMitigationListView,
} = useWorkspacesContext();
return <ThreatModelView
onPrintButtonClick={onPrintButtonClick}
composerMode={composerMode}
data={getWorkspaceData()}
hasContentDetails={hasContentDetails}
onApplicationInfoView={onApplicationInfoView}
onArchitectureView={onArchitectureView}
onDataflowView={onDataflowView}
onAssumptionListView={onAssumptionListView}
onThreatListView={onThreatListView}
onMitigationListView={onMitigationListView}
/>;
};

Expand Down
17 changes: 7 additions & 10 deletions packages/threat-composer/src/contexts/ContextAggregator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
limitations under the License.
******************************************************************************************************************** */
import { FC, PropsWithChildren } from 'react';
import { ComposerMode, DataExchangeFormat } from '../../customTypes';
import { ComposerMode, DataExchangeFormat, ViewNavigationEvent } from '../../customTypes';
import GlobalSetupContextProvider from '../GlobalSetupContext';
import WorkspaceContextAggregator, { WorkspaceContextAggregatorProps } from '../WorkspaceContextAggregator';
import WorkspaceContextAggregator from '../WorkspaceContextAggregator';
import WorkspacesContextProvider, { WorkspacesContextProviderProps } from '../WorkspacesContext';

export interface ContextAggregatorProps {
export interface ContextAggregatorProps extends ViewNavigationEvent {
composerMode?: ComposerMode;
onWorkspaceChanged?: WorkspacesContextProviderProps['onWorkspaceChanged'];
onThreatEditorView?: WorkspaceContextAggregatorProps['onThreatEditorView'];
onThreatListView?: WorkspaceContextAggregatorProps['onThreatListView'];
onPreview?: (content: DataExchangeFormat) => void;
onPreviewClose?: () => void;
onImported?: () => void;
Expand All @@ -34,12 +32,11 @@ const ContextAggregator: FC<PropsWithChildren<ContextAggregatorProps>> = ({
children,
onWorkspaceChanged,
composerMode = 'ThreatsOnly',
onThreatEditorView,
onThreatListView,
onPreview,
onPreviewClose,
onImported,
onDefineWorkload,
...props
}) => {
return (
<GlobalSetupContextProvider
Expand All @@ -48,12 +45,12 @@ const ContextAggregator: FC<PropsWithChildren<ContextAggregatorProps>> = ({
onImported={onImported}
onDefineWorkload={onDefineWorkload}
composerMode={composerMode}>
<WorkspacesContextProvider onWorkspaceChanged={onWorkspaceChanged}>
<WorkspacesContextProvider onWorkspaceChanged={onWorkspaceChanged} {...props}>
{(workspaceId) => (<WorkspaceContextAggregator
workspaceId={workspaceId}
requiredGlobalSetupContext={false}
onThreatEditorView={onThreatEditorView}
onThreatListView={onThreatListView}
onThreatEditorView={props.onThreatEditorView}
onThreatListView={props.onThreatListView}
>
{children}
</WorkspaceContextAggregator>)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import useLocalStorageState from 'use-local-storage-state';
import { v4 as uuidV4 } from 'uuid';
import { PerFieldExamplesType, ThreatsContext, DEFAULT_PER_FIELD_EXAMPLES, useThreatsContext } from './context';
import { LOCAL_STORAGE_KEY_STATEMENT_LIST, LOCAL_STORAGE_KEY_EDITING_STATEMENT } from '../../configs/localStorageKeys';
import { PerFieldExample, TemplateThreatStatement, ThreatStatementListFilter } from '../../customTypes';
import { PerFieldExample, TemplateThreatStatement, ViewNavigationEvent } from '../../customTypes';
import threatStatementExamplesData from '../../data/threatStatementExamples.json';
import ThreatsMigration from '../../migrations/ThreatsMigration';
import removeLocalStorageKey from '../../utils/removeLocalStorageKey';
Expand All @@ -29,8 +29,8 @@ export type View = 'list' | 'editor';

export interface ThreatsContextProviderProps {
workspaceId: string | null;
onThreatListView?: (filter?: ThreatStatementListFilter) => void;
onThreatEditorView?: (threatId: string) => void;
onThreatListView?: ViewNavigationEvent['onThreatListView'];
onThreatEditorView?: ViewNavigationEvent['onThreatEditorView'];
}

const getLocalStorageKey = (workspaceId: string | null) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
******************************************************************************************************************** */
import { FC, PropsWithChildren } from 'react';
import { ComposerMode, DataExchangeFormat } from '../../customTypes';
import { ComposerMode, DataExchangeFormat, ViewNavigationEvent } from '../../customTypes';
import ApplicationInfoContextProvider from '../ApplicationContext';
import ArchitectureInfoContextProvider from '../ArchitectureContext';
import AssumptionLinksContextProvider from '../AssumptionLinksContext';
Expand All @@ -23,14 +23,12 @@ import DataflowInfoContextProvider from '../DataflowContext';
import GlobalSetupContextProvider from '../GlobalSetupContext';
import MitigationLinksContextProvider from '../MitigationLinksContext';
import MitigationsContextProvider from '../MitigationsContext';
import ThreatsContextProvider, { ThreatsContextProviderProps } from '../ThreatsContext';
import ThreatsContextProvider from '../ThreatsContext';

export interface WorkspaceContextAggregatorProps {
export interface WorkspaceContextAggregatorProps extends ViewNavigationEvent {
workspaceId: string | null;
composerMode?: ComposerMode;
requiredGlobalSetupContext?: boolean;
onThreatEditorView?: ThreatsContextProviderProps['onThreatEditorView'];
onThreatListView?: ThreatsContextProviderProps['onThreatListView'];
onPreview?: (content: DataExchangeFormat) => void;
onPreviewClose?: () => void;
onImported?: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
limitations under the License.
******************************************************************************************************************** */
import { useContext, createContext } from 'react';
import { Workspace } from '../../customTypes';
import { ViewNavigationEvent, Workspace } from '../../customTypes';

export interface WorkspacesContextApi {
export interface WorkspacesContextApi extends ViewNavigationEvent {
workspaceList: Workspace[];
setWorkspaceList: (workspace: Workspace[]) => void;
currentWorkspace: Workspace | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import { v4 as uuidv4 } from 'uuid';
import { WorkspacesContext, useWorkspacesContext } from './context';
import { DEFAULT_WORKSPACE_ID } from '../../configs/constants';
import { LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, LOCAL_STORAGE_KEY_WORKSPACE_LIST } from '../../configs/localStorageKeys';
import { Workspace } from '../../customTypes';
import { ViewNavigationEvent, Workspace } from '../../customTypes';
import WorkspacesMigration from '../../migrations/WorkspacesMigration';

export interface WorkspacesContextProviderProps {
export interface WorkspacesContextProviderProps extends ViewNavigationEvent {
workspaceId?: string;
onWorkspaceChanged?: (workspaceId: string) => void;
children: (workspace: string | null) => ReactElement<{ workspaceId: string | null }>;
Expand All @@ -32,6 +32,7 @@ const WorkspacesContextProvider: FC<WorkspacesContextProviderProps> = ({
children,
workspaceId,
onWorkspaceChanged,
...props
}) => {
const [currentWorkspace, setCurrentWorkspace] = useLocalStorageState<Workspace | null>(LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, {
defaultValue: null,
Expand Down Expand Up @@ -99,6 +100,7 @@ const WorkspacesContextProvider: FC<WorkspacesContextProviderProps> = ({
addWorkspace: handleAddWorkspace,
removeWorkspace: handleRemoveWorkspace,
renameWorkspace: handleRenameWorkspace,
...props,
}}>
<WorkspacesMigration>
{children(currentWorkspace?.id || null)}
Expand Down
12 changes: 11 additions & 1 deletion packages/threat-composer/src/customTypes/dataExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ export const DataExchangeFormatSchema = z.object({
threats: TemplateThreatStatementSchema.array().optional(),
}).strict();

export type DataExchangeFormat = z.infer<typeof DataExchangeFormatSchema>;
export type DataExchangeFormat = z.infer<typeof DataExchangeFormatSchema>;

export interface HasContentDetails {
applicationName: boolean;
applicationInfo: boolean;
architecture: boolean;
dataflow: boolean;
assumptions: boolean;
mitigations: boolean;
threats: boolean;
}
Loading

0 comments on commit 81ed842

Please sign in to comment.