diff --git a/packages/threat-composer-app/public/404.html b/packages/threat-composer-app/public/404.html
new file mode 100644
index 00000000..a28fb2e4
--- /dev/null
+++ b/packages/threat-composer-app/public/404.html
@@ -0,0 +1,22 @@
+
+
+
+
+ Redirecting to Threat Composer
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/threat-composer-app/src/containers/App/components/Full/index.tsx b/packages/threat-composer-app/src/containers/App/components/Full/index.tsx
index f6e29ec6..8bcc7bc1 100644
--- a/packages/threat-composer-app/src/containers/App/components/Full/index.tsx
+++ b/packages/threat-composer-app/src/containers/App/components/Full/index.tsx
@@ -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 ? {
@@ -143,6 +147,11 @@ const Full: FC = () => {
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}
diff --git a/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx b/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx
index 60212f34..855a9dab 100644
--- a/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx
+++ b/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx
@@ -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';
@@ -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 = ({
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();
+ }
+ if (!hasContentDetails?.architecture) {
+ buttons.push();
+ }
+ if (!hasContentDetails?.dataflow) {
+ buttons.push();
+ }
+ if (!hasContentDetails?.assumptions) {
+ buttons.push();
+ }
+ if (!hasContentDetails?.threats) {
+ buttons.push();
+ }
+ if (!hasContentDetails?.threats) {
+ buttons.push();
+ }
+ const len = buttons.length;
+ return buttons.flatMap((b, index) => index === len - 1 ? {b} : [b, or]);
+ }, [hasContentDetails, props]);
+
return (
- {content}
+ {content ?
+ ({content}) :
+ ({loading ? : 'No data available'})
+ }
+ {composerMode === 'Full' && hasContentDetails && Object.values(hasContentDetails).some(x => !x) &&
+
+
+ Suggested next steps:
+ {getNextStepButtons()}
+
+
+
}
);
};
diff --git a/packages/threat-composer/src/components/report/ThreatModel/index.tsx b/packages/threat-composer/src/components/report/ThreatModel/index.tsx
index df45871c..5684c34e 100644
--- a/packages/threat-composer/src/components/report/ThreatModel/index.tsx
+++ b/packages/threat-composer/src/components/report/ThreatModel/index.tsx
@@ -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;
@@ -27,10 +28,26 @@ const ThreatModel: FC = ({
}) => {
const { getWorkspaceData } = useImportExport();
const { composerMode } = useGlobalSetupContext();
+ const [_, hasContentDetails] = useHasContent();
+ const {
+ onApplicationInfoView,
+ onArchitectureView,
+ onDataflowView,
+ onAssumptionListView,
+ onThreatListView,
+ onMitigationListView,
+ } = useWorkspacesContext();
return ;
};
diff --git a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx
index 3eb91b9d..50c84fb8 100644
--- a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx
+++ b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx
@@ -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;
@@ -34,12 +32,11 @@ const ContextAggregator: FC> = ({
children,
onWorkspaceChanged,
composerMode = 'ThreatsOnly',
- onThreatEditorView,
- onThreatListView,
onPreview,
onPreviewClose,
onImported,
onDefineWorkload,
+ ...props
}) => {
return (
> = ({
onImported={onImported}
onDefineWorkload={onDefineWorkload}
composerMode={composerMode}>
-
+
{(workspaceId) => (
{children}
)}
diff --git a/packages/threat-composer/src/contexts/ThreatsContext/index.tsx b/packages/threat-composer/src/contexts/ThreatsContext/index.tsx
index 0ce02fca..d678d271 100644
--- a/packages/threat-composer/src/contexts/ThreatsContext/index.tsx
+++ b/packages/threat-composer/src/contexts/ThreatsContext/index.tsx
@@ -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';
@@ -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) => {
diff --git a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx
index 08d04e6f..b1858dd6 100644
--- a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx
+++ b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx
@@ -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';
@@ -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;
diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts
index b06bce16..cbcc62ba 100644
--- a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts
+++ b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts
@@ -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;
diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx
index 83d7ea02..3307aa98 100644
--- a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx
+++ b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx
@@ -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 }>;
@@ -32,6 +32,7 @@ const WorkspacesContextProvider: FC = ({
children,
workspaceId,
onWorkspaceChanged,
+ ...props
}) => {
const [currentWorkspace, setCurrentWorkspace] = useLocalStorageState(LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, {
defaultValue: null,
@@ -99,6 +100,7 @@ const WorkspacesContextProvider: FC = ({
addWorkspace: handleAddWorkspace,
removeWorkspace: handleRemoveWorkspace,
renameWorkspace: handleRenameWorkspace,
+ ...props,
}}>
{children(currentWorkspace?.id || null)}
diff --git a/packages/threat-composer/src/customTypes/dataExchange.ts b/packages/threat-composer/src/customTypes/dataExchange.ts
index 68f614c8..057eccd2 100644
--- a/packages/threat-composer/src/customTypes/dataExchange.ts
+++ b/packages/threat-composer/src/customTypes/dataExchange.ts
@@ -35,4 +35,14 @@ export const DataExchangeFormatSchema = z.object({
threats: TemplateThreatStatementSchema.array().optional(),
}).strict();
-export type DataExchangeFormat = z.infer;
\ No newline at end of file
+export type DataExchangeFormat = z.infer;
+
+export interface HasContentDetails {
+ applicationName: boolean;
+ applicationInfo: boolean;
+ architecture: boolean;
+ dataflow: boolean;
+ assumptions: boolean;
+ mitigations: boolean;
+ threats: boolean;
+}
\ No newline at end of file
diff --git a/packages/threat-composer/src/customTypes/events.ts b/packages/threat-composer/src/customTypes/events.ts
new file mode 100644
index 00000000..da7fdba3
--- /dev/null
+++ b/packages/threat-composer/src/customTypes/events.ts
@@ -0,0 +1,26 @@
+/** *******************************************************************************************************************
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ ******************************************************************************************************************** */
+import { ThreatStatementListFilter } from './threats';
+
+export interface ViewNavigationEvent {
+ onApplicationInfoView?: () => void;
+ onArchitectureView?: () => void;
+ onDataflowView?: () => void;
+ onAssumptionListView?: () => void;
+ onMitigationListView?: () => void;
+ onThreatListView?: (filter?: ThreatStatementListFilter) => void;
+ onThreatEditorView?: (threatId: string) => void;
+}
\ No newline at end of file
diff --git a/packages/threat-composer/src/customTypes/index.ts b/packages/threat-composer/src/customTypes/index.ts
index 6c53525e..233e2af6 100644
--- a/packages/threat-composer/src/customTypes/index.ts
+++ b/packages/threat-composer/src/customTypes/index.ts
@@ -23,4 +23,5 @@ export * from './composerMode';
export * from './application';
export * from './architecture';
export * from './dataflow';
-export * from './dataExchange';
\ No newline at end of file
+export * from './dataExchange';
+export * from './events';
\ No newline at end of file
diff --git a/packages/threat-composer/src/hooks/useExportImport/index.ts b/packages/threat-composer/src/hooks/useExportImport/index.ts
index 69a78e88..e9f52caf 100644
--- a/packages/threat-composer/src/hooks/useExportImport/index.ts
+++ b/packages/threat-composer/src/hooks/useExportImport/index.ts
@@ -33,8 +33,7 @@ import validateData from '../../utils/validateData';
const SCHEMA_VERSION = 1.0;
const getExportFileName = (composerMode: ComposerMode, filtered: boolean, currentWorkspace: Workspace | null) => {
- const date = new Date().toISOString().split('T')[0];
- const exportFileName = `${EXPORT_FILE_NAME}_Workspace_${currentWorkspace ? currentWorkspace.name.replace(' ', '-') : 'Default'}${composerMode !== 'Full' ? '_ThreatsOnly' : ''}${filtered ? '_Filtered' : ''}_${date}`;
+ const exportFileName = `${EXPORT_FILE_NAME}_Workspace_${currentWorkspace ? currentWorkspace.name.replace(' ', '-') : 'Default'}${composerMode !== 'Full' ? '_ThreatsOnly' : ''}${filtered ? '_Filtered' : ''}`;
return exportFileName;
};
diff --git a/packages/threat-composer/src/hooks/useHasContent/index.tsx b/packages/threat-composer/src/hooks/useHasContent/index.tsx
index 48e374b6..d9190074 100644
--- a/packages/threat-composer/src/hooks/useHasContent/index.tsx
+++ b/packages/threat-composer/src/hooks/useHasContent/index.tsx
@@ -20,7 +20,8 @@ import { useAssumptionsContext } from '../../contexts/AssumptionsContext';
import { useDataflowInfoContext } from '../../contexts/DataflowContext';
import { useMitigationsContext } from '../../contexts/MitigationsContext';
import { useThreatsContext } from '../../contexts/ThreatsContext';
-import { hasApplicationInfo, hasArchitectureInfo, hasAssumptions, hasDataflowInfo, hasMitigations, hasThreats } from '../../utils/hasContent';
+import { HasContentDetails } from '../../customTypes';
+import { hasApplicationName, hasApplicationInfo, hasArchitectureInfo, hasAssumptions, hasDataflowInfo, hasMitigations, hasThreats } from '../../utils/hasContent';
const useHasContent = () => {
const { applicationInfo } = useApplicationInfoContext();
@@ -30,8 +31,9 @@ const useHasContent = () => {
const { mitigationList } = useMitigationsContext();
const { statementList } = useThreatsContext();
- const hasContent = useMemo(() => {
+ const hasContent: [ boolean, HasContentDetails] = useMemo(() => {
const details = {
+ applicationName: hasApplicationName(applicationInfo),
applicationInfo: hasApplicationInfo(applicationInfo),
architecture: hasArchitectureInfo(architectureInfo),
dataflow: hasDataflowInfo(dataflowInfo),
diff --git a/packages/threat-composer/src/utils/hasContent/index.ts b/packages/threat-composer/src/utils/hasContent/index.ts
index 18c6a3f7..e4b2db2c 100644
--- a/packages/threat-composer/src/utils/hasContent/index.ts
+++ b/packages/threat-composer/src/utils/hasContent/index.ts
@@ -15,8 +15,12 @@
******************************************************************************************************************** */
import { ApplicationInfo, ArchitectureInfo, Assumption, DataflowInfo, Mitigation, TemplateThreatStatement } from '../../customTypes';
+export const hasApplicationName = (applicationInfo: ApplicationInfo) => {
+ return !!(applicationInfo.name);
+};
+
export const hasApplicationInfo = (applicationInfo: ApplicationInfo) => {
- return !!(applicationInfo.name || applicationInfo.description);
+ return !!(applicationInfo.description);
};
export const hasArchitectureInfo = (archInfo: ArchitectureInfo) => {