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) => {