From c27335630fcd9a0837fd5b7fbe45a64a4398b69c Mon Sep 17 00:00:00 2001 From: Jessie Wei Date: Sun, 6 Oct 2024 12:59:44 +1100 Subject: [PATCH] feat: Allows clone of a workspace (#144) * feat: Allows clone of a workspace * chore: Update the menu label wording * chore: Fix the typo --- .projen/tasks.json | 2 +- README.md | 57 +++++- .../workspaces/EditWorkspace/index.tsx | 22 ++- .../workspaces/WorkspaceSelector/index.tsx | 31 +++- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 52 ++++++ .../contexts/CrossWorkspaceContext/context.ts | 29 +++ .../contexts/CrossWorkspaceContext/index.tsx | 28 +++ .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../LocalStorageContextProvider/index.tsx | 2 +- .../WorkspaceContextAggregator/index.tsx | 9 +- .../src/contexts/WorkspacesContext/context.ts | 2 +- .../WorkspacesContext/useWorkspaces.ts | 4 +- .../workspaceExamples/workspaceExamples.ts | 2 + .../src/hooks/useCloneWorkspace/index.ts | 36 ++++ .../src/utils/setLocalStorageKey/index.ts | 20 ++ projenrc/monorepo.ts | 2 +- scripts/{packs => data}/buildPacks.ts | 85 +++++++-- scripts/data/injectData.ts | 174 ++++++++++++++++++ scripts/packs/injectPacks.ts | 117 ------------ 25 files changed, 533 insertions(+), 155 deletions(-) create mode 100644 packages/threat-composer/src/contexts/CrossWorkspaceContext/components/LocalStorageContextProvider/index.tsx create mode 100644 packages/threat-composer/src/contexts/CrossWorkspaceContext/context.ts create mode 100644 packages/threat-composer/src/contexts/CrossWorkspaceContext/index.tsx create mode 100644 packages/threat-composer/src/hooks/useCloneWorkspace/index.ts create mode 100644 packages/threat-composer/src/utils/setLocalStorageKey/index.ts rename scripts/{packs => data}/buildPacks.ts (54%) create mode 100644 scripts/data/injectData.ts delete mode 100644 scripts/packs/injectPacks.ts diff --git a/.projen/tasks.json b/.projen/tasks.json index ef4c2fce..987f1b51 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -16,7 +16,7 @@ "name": "build:packs", "steps": [ { - "exec": "npx ts-node ./scripts/packs/buildPacks.ts" + "exec": "npx ts-node ./scripts/data/buildPacks.ts ThreatPack && npx ts-node ./scripts/data/buildPacks.ts MitigationPack" } ] }, diff --git a/README.md b/README.md index 36c52879..1d18e5da 100644 --- a/README.md +++ b/README.md @@ -388,8 +388,19 @@ Reference or example threat models are available directly in the Workspace selec }, ] as WorkspaceExample[]; ``` -1. Build the project. +1. Build the project + +#### Dynamically inject example threat models in build time +1. Follow steps 1-2 above to author your example threat models +1. Store your example threat models within a folder in a seperate location or repository +1. Copy the file folder containing example threat models under the path `packages/threat-composer/src/data/workspaceExamples` in your build +1. Run the script below in your build from the project root to inject the example threat models entry to configuration file `packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts`: + + ``` + npx ts-node ./scripts/data/injectData.ts WorkspaceExample + ``` +1. Build the project ### Threat packs @@ -424,7 +435,27 @@ Threat packs allow you to quickly find and add bulk or selected threat statement GenAIChatbot, ] as ThreatPack[]; ``` -1. Build the project. +1. Build the project + +#### Dynamically inject example threat packs in build time + +1. Follow steps 1-2 above to author your threat packs +1. Store your threat packs within a folder in a seperate location or repository +1. Follow steps 4-8 above to author your threat pack metadata files +1. Store your threat pack metadata files within a folder in a seperate location or repository (can be the same folder of threat pack files) +1. Copy the file folder(s) containing threat pack files and metadata files under the path `packages/threat-composer/src/data/threatPacks` in your build +1. Run the script below in your build from the project root to build the threat packs + + ``` + npx ts-node ./scripts/data/buildPacks.ts ThreatPack + ``` + +1. Run the script below in your build from the project root to inject the generated threat packs entry to configuration file `packages/threat-composer/src/data/threatPacks/threatPacks.ts`: + + ``` + npx ts-node ./scripts/data/injectData.ts ThreatPack + ``` +1. Build the project ### Mitigation packs @@ -459,7 +490,27 @@ Mitigation packs allow you to quickly find and add bulk or selected mitigation c GenAIChatbot, ] as MitigationPack[]; ``` -1. Build the project. +1. Build the project + +#### Dynamically inject example mitigation packs in build time + +1. Follow steps 1-2 above to author your mitigation packs +1. Store your mitigation packs within a folder in a seperate location or repository +1. Follow steps 4-8 above to author your mitigation pack metadata files +1. Store your mitigation pack metadata files within a folder in a seperate location or repository (can be the same folder of mitigation pack files) +1. Copy the file folder(s) containing mitigation pack files and metadata files under the path `packages/threat-composer/src/data/mitigationPacks` in your build +1. Run the script below in your build from the project root to build the mitigation packs + + ``` + npx ts-node ./scripts/data/buildPacks.ts MitigationPack + ``` + +1. Run the script below in your build from the project root to inject the generated mitigation packs entry to configuration file `packages/threat-composer/src/data/mitigationPacks/mitigationPacks.ts`: + + ``` + npx ts-node ./scripts/data/injectData.ts MitigationPack + ``` +1. Build the project ### Threat examples diff --git a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx index 3d214df6..011aa167 100644 --- a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx +++ b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx @@ -29,17 +29,29 @@ export interface EditWorkspaceProps { setVisible: React.Dispatch>; onConfirm: (workspace: string) => Promise; value?: string; - editMode?: boolean; + editMode: 'add' | 'update' | 'clone'; currentWorkspace?: Workspace; workspaceList: Workspace[]; exampleWorkspaceList: Workspace[]; } +const BUTTON_LABEL = { + add: 'Add', + update: 'Update', + clone: 'Clone', +}; + +const HEADER_TEXT = { + add: 'Add new workspace', + update: 'Update workspace', + clone: 'Clone workspace', +}; + const EditWorkspace: FC = ({ visible, setVisible, onConfirm, - editMode = false, + editMode = 'add', workspaceList, exampleWorkspaceList, currentWorkspace, @@ -74,21 +86,21 @@ const EditWorkspace: FC = ({ ); }, [setVisible, handleConfirm, value, editMode]); return {editMode ? 'Update workspace' : 'Add new workspace'}} + header={
{HEADER_TEXT[editMode as 'add' | 'update' | 'clone'] || 'Add'}
} visible={visible} footer={footer} onDismiss={() => setVisible(false)} > } diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx index f01e0344..7b93d281 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx @@ -42,6 +42,7 @@ import { DataExchangeFormat, TemplateThreatStatement, } from '../../../customTypes'; +import useCloneWorkspace from '../../../hooks/useCloneWorkspace'; import useImportExport from '../../../hooks/useExportImport'; import useRemoveData from '../../../hooks/useRemoveData'; import getMobileMediaQuery from '../../../utils/getMobileMediaQuery'; @@ -76,6 +77,7 @@ export interface WorkspaceSelectorProps { onPreview?: (data: DataExchangeFormat) => void; onPreviewClose?: () => void; onImported?: () => void; + onClone?: () => Promise; } const WorkspaceSelector: FC> = ({ @@ -95,6 +97,8 @@ const WorkspaceSelector: FC> = ({ useState(false); const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false); + const [cloneWorkspaceModalVisible, setCloneWorkspaceModalVisible] = + useState(false); const [removeDataModalVisible, setRemoveDataModalVisible] = useState(false); const [removeWorkspaceModalVisible, setRemoveWorkspaceModalVisible] = useState(false); @@ -120,6 +124,8 @@ const WorkspaceSelector: FC> = ({ switchWorkspace, } = useWorkspacesContext(); + const cloneWorkspace = useCloneWorkspace(); + const workspacesOptions = useMemo(() => { const options: (SelectProps.Option | SelectProps.OptionGroup)[] = [ { @@ -188,6 +194,9 @@ const WorkspaceSelector: FC> = ({ case 'import': setFileImportModalVisible(true); break; + case 'clone': + setCloneWorkspaceModalVisible(true); + break; case 'exportAll': exportAll(); break; @@ -217,6 +226,7 @@ const WorkspaceSelector: FC> = ({ setRemoveDataModalVisible, setRemoveWorkspaceModalVisible, setAddWorkspaceModalVisible, + setCloneWorkspaceModalVisible, setEditWorkspaceModalVisible, handleSingletonPrimaryButtonClick, ], @@ -272,9 +282,13 @@ const WorkspaceSelector: FC> = ({ { id: 'add', text: 'Add new workspace' }, { id: 'import', - text: 'Import', + text: 'Import into current workspace', disabled: isWorkspaceExample(currentWorkspace?.id), }, + { + id: 'clone', + text: 'Clone current workspace', + }, { id: 'exportAll', text: embededMode @@ -390,6 +404,7 @@ const WorkspaceSelector: FC> = ({ )} {addWorkspaceModalVisible && ( { @@ -399,11 +414,23 @@ const WorkspaceSelector: FC> = ({ exampleWorkspaceList={workspaceExamples} /> )} + {cloneWorkspaceModalVisible && ( + { + await cloneWorkspace(workspaceName); + }} + workspaceList={workspaceList} + exampleWorkspaceList={workspaceExamples} + /> + )} {editWorkspaceModalVisible && currentWorkspace && ( renameWorkspace(currentWorkspace.id, newWorkspaceName) diff --git a/packages/threat-composer/src/contexts/ApplicationContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/ApplicationContext/components/LocalStorageContextProvider/index.tsx index d619f5db..dcc63656 100644 --- a/packages/threat-composer/src/contexts/ApplicationContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/ApplicationContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { INFO_DEFAULT_VALUE } from '../../../constants'; import { ApplicationInfoContext } from '../../context'; import { ApplicationContextProviderProps } from '../../types'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_APPLICATION_INFO}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/ArchitectureContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/ArchitectureContext/components/LocalStorageContextProvider/index.tsx index ab4f4175..2ff0ca19 100644 --- a/packages/threat-composer/src/contexts/ArchitectureContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/ArchitectureContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { INFO_DEFAULT_VALUE } from '../../../constants'; import { ArchitectureInfoContext } from '../../context'; import { ArchitectureContextProviderProps } from '../../types'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_ARCHIECTURE_INFO}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/AssumptionLinksContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/AssumptionLinksContext/components/LocalStorageContextProvider/index.tsx index baabd374..aaaa5bd6 100644 --- a/packages/threat-composer/src/contexts/AssumptionLinksContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/AssumptionLinksContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { AssumptionLinksContext } from '../../context'; import { AssumptionLinksContextProviderProps } from '../../types'; import useAssumptionLinks from '../../useAssumptionLinks'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_ASSUMPTION_LINK_LIST}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/AssumptionsContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/AssumptionsContext/components/LocalStorageContextProvider/index.tsx index 3535b455..23993a65 100644 --- a/packages/threat-composer/src/contexts/AssumptionsContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/AssumptionsContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { AssumptionsContext } from '../../context'; import { AssumptionsContextProviderProps } from '../../types'; import useAssumptions from '../../useAssumptions'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_ASSUMPTION_LIST}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/CrossWorkspaceContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/CrossWorkspaceContext/components/LocalStorageContextProvider/index.tsx new file mode 100644 index 00000000..8c311353 --- /dev/null +++ b/packages/threat-composer/src/contexts/CrossWorkspaceContext/components/LocalStorageContextProvider/index.tsx @@ -0,0 +1,52 @@ +/** ******************************************************************************************************************* + 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 { FC, PropsWithChildren, useCallback } from 'react'; +import { DataExchangeFormat } from '../../../../customTypes'; +import setLocalStorageKey from '../../../../utils/setLocalStorageKey'; +import { getLocalStorageKey as getApplicationInfoLocalStorageKey } from '../../../ApplicationContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getArchitectureInfoLocalStorageKey } from '../../../ArchitectureContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getAssumptionLinksLocalStorageKey } from '../../../AssumptionLinksContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getAssumptionsListLocalStorageKey } from '../../../AssumptionsContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getDataflowInfoLocalStorageKey } from '../../../DataflowContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getMitigationLinksLocalStorageKey } from '../../../MitigationLinksContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getMitigationsLocalStorageKey } from '../../../MitigationsContext/components/LocalStorageContextProvider'; +import { getLocalStorageKey as getThreatsLocalStorageKey } from '../../../ThreatsContext/components/LocalStorageContextProvider'; +import { CrossWorkspaceContext } from '../../context'; + + +const CrossWorkspaceLocalStorageContextProvider: FC> = ({ + children, +}) => { + const handleCloneWorkspaceData = useCallback(async (targetWorkspaceId: string, data: DataExchangeFormat) => { + data.applicationInfo && setLocalStorageKey(getApplicationInfoLocalStorageKey(targetWorkspaceId), data.applicationInfo); + data.architecture && setLocalStorageKey(getArchitectureInfoLocalStorageKey(targetWorkspaceId), data.architecture); + data.dataflow && setLocalStorageKey(getDataflowInfoLocalStorageKey(targetWorkspaceId), data.dataflow); + data.assumptions && setLocalStorageKey(getAssumptionsListLocalStorageKey(targetWorkspaceId), data.assumptions); + data.threats && setLocalStorageKey(getThreatsLocalStorageKey(targetWorkspaceId), data.threats); + data.mitigations && setLocalStorageKey(getMitigationsLocalStorageKey(targetWorkspaceId), data.mitigations); + data.assumptionLinks && setLocalStorageKey(getAssumptionLinksLocalStorageKey(targetWorkspaceId), data.assumptionLinks); + data.mitigationLinks && setLocalStorageKey(getMitigationLinksLocalStorageKey(targetWorkspaceId), data.mitigationLinks); + }, []); + + return ( + {children} + ); +}; + +export default CrossWorkspaceLocalStorageContextProvider; + diff --git a/packages/threat-composer/src/contexts/CrossWorkspaceContext/context.ts b/packages/threat-composer/src/contexts/CrossWorkspaceContext/context.ts new file mode 100644 index 00000000..724276d9 --- /dev/null +++ b/packages/threat-composer/src/contexts/CrossWorkspaceContext/context.ts @@ -0,0 +1,29 @@ +/** ******************************************************************************************************************* + 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 { useContext, createContext } from 'react'; +import { DataExchangeFormat } from '../../customTypes'; + +export interface CrossWorkspaceContextApi { + cloneWorkspaceData: (targetWorkspaceId: string, data: DataExchangeFormat) => Promise; +} + +const initialState: CrossWorkspaceContextApi = { + cloneWorkspaceData: () => Promise.resolve(), +}; + +export const CrossWorkspaceContext = createContext(initialState); + +export const useCrossWorkspaceContext = () => useContext(CrossWorkspaceContext); \ No newline at end of file diff --git a/packages/threat-composer/src/contexts/CrossWorkspaceContext/index.tsx b/packages/threat-composer/src/contexts/CrossWorkspaceContext/index.tsx new file mode 100644 index 00000000..36e4a460 --- /dev/null +++ b/packages/threat-composer/src/contexts/CrossWorkspaceContext/index.tsx @@ -0,0 +1,28 @@ +/** ******************************************************************************************************************* + 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 { FC, PropsWithChildren } from 'react'; +import CrossWorkspaceLocalStorageContextProvider from './components/LocalStorageContextProvider'; +import { useCrossWorkspaceContext } from './context'; + +const CrossWorkspaceContextProvider: FC> = (props) => { + return (); +}; + +export default CrossWorkspaceContextProvider; + +export { + useCrossWorkspaceContext, +}; diff --git a/packages/threat-composer/src/contexts/DataflowContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/DataflowContext/components/LocalStorageContextProvider/index.tsx index 47251855..8b70370c 100644 --- a/packages/threat-composer/src/contexts/DataflowContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/DataflowContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { INFO_DEFAULT_VALUE } from '../../../constants'; import { DataflowInfoContext, useDataflowInfoContext } from '../../context'; import { DataflowContextProviderProps } from '../../types'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_DATAFLOW_INFO}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/MitigationLinksContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/MitigationLinksContext/components/LocalStorageContextProvider/index.tsx index e14fa607..344261d7 100644 --- a/packages/threat-composer/src/contexts/MitigationLinksContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/MitigationLinksContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { MitigationLinksContext } from '../../context'; import { MitigationLinksContextProviderProps } from '../../types'; import useMitigationLinks from '../../useMitigationLinks'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_MITIGATION_LINK_LIST}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/MitigationsContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/MitigationsContext/components/LocalStorageContextProvider/index.tsx index 6a4703e2..d0dde9c7 100644 --- a/packages/threat-composer/src/contexts/MitigationsContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/MitigationsContext/components/LocalStorageContextProvider/index.tsx @@ -22,7 +22,7 @@ import { MitigationsContext } from '../../context'; import { MitigationsContextProviderProps } from '../../types'; import useMitigations from '../../useMitigations'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_MITIGATION_LIST}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx index f3717138..caf6a610 100644 --- a/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx @@ -24,7 +24,7 @@ import useThreatExamples from '../../hooks/useThreatExamples'; import useThreats from '../../hooks/useThreats'; import { ThreatsContextProviderProps } from '../../types'; -const getLocalStorageKey = (workspaceId: string | null) => { +export const getLocalStorageKey = (workspaceId: string | null) => { if (workspaceId) { return `${LOCAL_STORAGE_KEY_STATEMENT_LIST}_${workspaceId}`; } diff --git a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx index ce5fcda2..8d811ac7 100644 --- a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx @@ -20,6 +20,7 @@ import ApplicationInfoContextProvider from '../ApplicationContext'; import ArchitectureInfoContextProvider from '../ArchitectureContext'; import AssumptionLinksContextProvider from '../AssumptionLinksContext'; import AssumptionsContextProvider from '../AssumptionsContext'; +import CrossWorkspaceContextProvider from '../CrossWorkspaceContext'; import DataflowInfoContextProvider from '../DataflowContext'; import GlobalSetupContextProvider from '../GlobalSetupContext'; import MitigationLinksContextProvider from '../MitigationLinksContext'; @@ -54,9 +55,11 @@ const WorkspaceContextInnerAggregator: FC - - {children} - + + + {children} + + diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts index 07bbdade..dbff301f 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts +++ b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts @@ -21,7 +21,7 @@ export interface WorkspacesContextApi { workspaceList: Workspace[]; setWorkspaceList: (workspace: Workspace[]) => void; currentWorkspace: Workspace | null; - switchWorkspace: (workspaceId: string | null) => void; + switchWorkspace: (toBeSwitchedWorkspace: string | null | Workspace) => void; removeWorkspace: (id: string) => Promise; addWorkspace: (workspaceName: string, storageType?: Workspace['storageType'], metadata?: Workspace['metadata']) => Promise; renameWorkspace: (id: string, newWorkspaceName: string) => Promise; diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts b/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts index 049e3c05..f7173525 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts +++ b/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts @@ -46,8 +46,8 @@ const useWorkspaces = ( workspaceList, ]); - const handleSwitchWorkspace = useCallback((toBeSwitchedWorkspaceId: string | null) => { - const workspace = getWorkspace(toBeSwitchedWorkspaceId); + const handleSwitchWorkspace = useCallback((toBeSwitchedWorkspace: string | null | Workspace) => { + const workspace = typeof toBeSwitchedWorkspace === 'string' ? getWorkspace(toBeSwitchedWorkspace) : toBeSwitchedWorkspace; setCurrentWorkspace(workspace); onWorkspaceChanged?.(workspace?.name || DEFAULT_WORKSPACE_ID); }, [onWorkspaceChanged, getWorkspace]); diff --git a/packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts b/packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts index 283b4842..8dfcd0ac 100644 --- a/packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts +++ b/packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts @@ -16,6 +16,7 @@ import genAIChatbot from './GenAIChatbot.tc.json'; import threatComposer from './ThreatComposer.tc.json'; import { WorkspaceExample } from '../../customTypes'; +// {IMPORT_PLACEHOLDER} const workspaceExamples = [ { @@ -26,6 +27,7 @@ const workspaceExamples = [ name: 'GenAI Chatbot', value: genAIChatbot, }, + // {ENTRY_PLACEHOLDER} ] as WorkspaceExample[]; export default workspaceExamples; \ No newline at end of file diff --git a/packages/threat-composer/src/hooks/useCloneWorkspace/index.ts b/packages/threat-composer/src/hooks/useCloneWorkspace/index.ts new file mode 100644 index 00000000..46cf29ab --- /dev/null +++ b/packages/threat-composer/src/hooks/useCloneWorkspace/index.ts @@ -0,0 +1,36 @@ +/** ******************************************************************************************************************* + 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 { useCallback } from 'react'; +import { useWorkspacesContext } from '../../contexts'; +import { useCrossWorkspaceContext } from '../../contexts/CrossWorkspaceContext'; +import useImportExport from '../useExportImport'; + +const useCloneWorkspace = () => { + const { addWorkspace, switchWorkspace } = useWorkspacesContext(); + const { getWorkspaceData } = useImportExport(); + const { cloneWorkspaceData } = useCrossWorkspaceContext(); + + const cloneWorkspace= useCallback(async (newWorkspaceName: string) => { + const newWorkspace = await addWorkspace(newWorkspaceName); + const data = await getWorkspaceData(); + await cloneWorkspaceData(newWorkspace.id, data); + switchWorkspace(newWorkspace); + }, [addWorkspace, cloneWorkspaceData]); + + return cloneWorkspace; +}; + +export default useCloneWorkspace; \ No newline at end of file diff --git a/packages/threat-composer/src/utils/setLocalStorageKey/index.ts b/packages/threat-composer/src/utils/setLocalStorageKey/index.ts new file mode 100644 index 00000000..c0868019 --- /dev/null +++ b/packages/threat-composer/src/utils/setLocalStorageKey/index.ts @@ -0,0 +1,20 @@ +/** ******************************************************************************************************************* + 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. + ******************************************************************************************************************** */ +const setLocalStorageKey = (key: string, value: object) => { + window.localStorage.setItem(key, JSON.stringify(value)); +}; + +export default setLocalStorageKey; \ No newline at end of file diff --git a/projenrc/monorepo.ts b/projenrc/monorepo.ts index 8b002134..aa32455c 100644 --- a/projenrc/monorepo.ts +++ b/projenrc/monorepo.ts @@ -90,7 +90,7 @@ class ThreatComposerMonorepoProject extends MonorepoTsProject { }); this.addTask("build:packs", { - exec: "npx ts-node ./scripts/packs/buildPacks.ts", + exec: "npx ts-node ./scripts/data/buildPacks.ts ThreatPack && npx ts-node ./scripts/data/buildPacks.ts MitigationPack", }); this.buildTask.reset(); diff --git a/scripts/packs/buildPacks.ts b/scripts/data/buildPacks.ts similarity index 54% rename from scripts/packs/buildPacks.ts rename to scripts/data/buildPacks.ts index 54e6bdbd..39eaa25d 100644 --- a/scripts/packs/buildPacks.ts +++ b/scripts/data/buildPacks.ts @@ -1,6 +1,11 @@ import fs from "fs"; import path from "path"; +/** + * Build packs from pack metadata json files. + * Usage: npx ts-node ./scripts/data/buildPacks.ts + */ + const DATA_FOLDER = path.join( __dirname, "..", @@ -17,6 +22,11 @@ const MITIGATION_PACKS_FOLDER = path.join(DATA_FOLDER, "mitigationPacks"); const GENERATED_FILES_FOLDER_NAME = "generated"; +const PACK_FOLDER = { + ThreatPack: THREAT_PACKS_FOLDER, + MitigationPack: MITIGATION_PACKS_FOLDER, +}; + const THREAT_PACK_BASE = { schema: 1, namespace: "threat-composer", @@ -29,14 +39,14 @@ const MITIGATION_PACK_BASE = { type: "mitigation-pack", }; -type PackType = "ThreatPacks" | "MitigationPacks"; +type PackType = "ThreatPack" | "MitigationPack"; const getPackContent = ( packType: PackType, metadataContent: any, sourceContent: any ) => { - if (packType === "ThreatPacks") { + if (packType === "ThreatPack") { const threats = sourceContent.threats; const mitigationLinks = sourceContent.mitigationLinks.filter((x: any) => threats.map((t: any) => t.id).includes(x.linkedId) @@ -67,12 +77,13 @@ const getPackContent = ( const processFile = ( filePath: string, - packFolder: string, + sourceDir: string, + destDir: string, packType: PackType ) => { const fileContent = fs.readFileSync(filePath, "utf-8"); const jsonContent = JSON.parse(fileContent); - const sourceFilePath = path.join(packFolder, jsonContent.path); + const sourceFilePath = path.join(sourceDir, jsonContent.path); const sourceFileContent = fs.readFileSync(sourceFilePath, "utf-8"); const sourceContent = JSON.parse(sourceFileContent); @@ -80,27 +91,34 @@ const processFile = ( const packContent = getPackContent(packType, jsonContent, sourceContent); const generateFilePath = path.join( - packFolder, - GENERATED_FILES_FOLDER_NAME, + destDir, `${path.basename(filePath, ".metadata.json")}.json` ); + console.log(`Writing ${packType} file: ${generateFilePath}`); + fs.writeFileSync(generateFilePath, JSON.stringify(packContent, null, 2)); return generateFilePath; }; const generatePacksFromMetaDatafiles = ( - packFolder: string, + sourceDir: string, + destDir: string, packType: PackType ) => { - const files = fs.readdirSync(packFolder); + const files = fs.readdirSync(sourceDir); files.forEach((file) => { - const filePath = path.join(packFolder, file); + const filePath = path.join(sourceDir, file); if (file.endsWith("metadata.json")) { try { console.log(`Processing file ${filePath}`); - const generateFilePath = processFile(filePath, packFolder, packType); + const generateFilePath = processFile( + filePath, + sourceDir, + destDir, + packType + ); console.log(`Generated ${packType} file: ${generateFilePath}`); } catch (e) { console.log(`Error processing file ${filePath}`, e); @@ -109,5 +127,48 @@ const generatePacksFromMetaDatafiles = ( }); }; -generatePacksFromMetaDatafiles(THREAT_PACKS_FOLDER, "ThreatPacks"); -generatePacksFromMetaDatafiles(MITIGATION_PACKS_FOLDER, "MitigationPacks"); +const mkdirIfNotExist = (destDir: string) => { + if (!fs.existsSync(destDir)) { + console.log(`Creating folder for ${destDir}`); + fs.mkdirSync(destDir, { recursive: true }); + } +}; + +const main = () => { + const args = process.argv; + + console.log("Arguments", args); + + const lenArgs = args.length; + + if (lenArgs < 3) { + console.log( + "Usage: npx ts-node ./scripts/data/buildPacks.ts " + ); + return -1; + } + + const input = { + type: args[2], + sourceDir: lenArgs > 3 ? args[3] : ".", + destDir: lenArgs > 4 ? args[4] : GENERATED_FILES_FOLDER_NAME, + }; + + console.log("buildPacks params", input); + + const packFolder = PACK_FOLDER[input.type as PackType]; + + if (!packFolder) { + console.error(`Invalid data type ${input.type}`); + } + + const sourceDir = path.join(packFolder, input.sourceDir); + const destDir = path.join(packFolder, input.destDir); + + mkdirIfNotExist(destDir); + generatePacksFromMetaDatafiles(sourceDir, destDir, input.type as PackType); + + return 0; +}; + +main(); diff --git a/scripts/data/injectData.ts b/scripts/data/injectData.ts new file mode 100644 index 00000000..5882a8f9 --- /dev/null +++ b/scripts/data/injectData.ts @@ -0,0 +1,174 @@ +import fs from "fs"; +import path from "path"; + +/** + * Inject data into threat ThreatPack/MitigationPack/WorkspaceExample dynamically in build time. + * Usage: npx ts-node ./scripts/data/injectData.ts + */ + +const IMPORT_PLACEHOLDER = "// {IMPORT_PLACEHOLDER}"; +const ENTRY_PLACEHOLDER = "// {ENTRY_PLACEHOLDER}"; + +const DATA_FOLDER = path.join( + __dirname, + "..", + "..", + "packages", + "threat-composer", + "src", + "data" +); + +const THREAT_PACKS_FOLDER = path.join(DATA_FOLDER, "threatPacks"); + +const THREAT_PACKS_FILE = path.join(THREAT_PACKS_FOLDER, "threatPacks.ts"); + +const MITIGATION_PACKS_FOLDER = path.join(DATA_FOLDER, "mitigationPacks"); + +const MITIGATION_PACKS_FILE = path.join( + MITIGATION_PACKS_FOLDER, + "mitigationPacks.ts" +); + +const WORKSPACE_EXAMPLE_FOLDER = path.join(DATA_FOLDER, "workspaceExamples"); + +const WORKSPACE_EXAMPLE_FILE = path.join( + WORKSPACE_EXAMPLE_FOLDER, + "workspaceExamples.ts" +); + +type DataType = "ThreatPack" | "MitigationPack" | "WorkspaceExample"; + +const CONFIG_DATA_FOLDER = { + ThreatPack: THREAT_PACKS_FOLDER, + MitigationPack: MITIGATION_PACKS_FOLDER, + WorkspaceExample: WORKSPACE_EXAMPLE_FOLDER, +}; + +const CONFIG_FILE_PATH = { + ThreatPack: THREAT_PACKS_FILE, + MitigationPack: MITIGATION_PACKS_FILE, + WorkspaceExample: WORKSPACE_EXAMPLE_FILE, +}; + +const readFileContent = (filePath: string) => { + return fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); +}; + +const writeFileContent = (filePath: string, content: string) => { + return fs.writeFileSync(filePath, content); +}; + +const removeExt = (filePath: string) => { + let fileName = path.basename(filePath); + while (fileName.indexOf(".") >= 0) { + fileName = path.basename(fileName, path.extname(fileName)); + } + + return fileName; +}; + +const injectDataEntry = ( + dataType: DataType, + dataConfig: string, + filePaths: string[] +) => { + const fileNames: string[] = []; + const importFiles = filePaths + .map((filePath) => { + const fileName = removeExt(filePath).replace(/[^a-zA-Z0-9]+/gm, "_"); + fileNames.push(fileName); + const importFile = `import ${fileName} from "${filePath}";`; + return importFile; + }) + .join("\n"); + + let updatedDataConfig = dataConfig + .replace(IMPORT_PLACEHOLDER, `${importFiles}\n${IMPORT_PLACEHOLDER}`) + .replace( + ENTRY_PLACEHOLDER, + `${fileNames + .map((fn) => { + if (dataType === "WorkspaceExample") { + return `{ name: '${fn}', value: ${fn}, },`; + } + + return `${fn},`; + }) + .join("\n")}\n${ENTRY_PLACEHOLDER}` + ); + + return updatedDataConfig; +}; + +const listFilePaths = (dir: string) => { + const filePaths: string[] = []; + + fs.readdirSync(dir).forEach((x) => { + const filePath = path.join(dir, x); + if (fs.lstatSync(filePath).isDirectory()) { + filePaths.push(...listFilePaths(filePath)); + } + + if (x.endsWith(".json")) { + filePaths.push(filePath); + } + }); + + return filePaths; +}; + +const main = () => { + const args = process.argv; + + console.log("Arguments", args); + + const lenArgs = args.length; + + if (lenArgs !== 4) { + console.log( + "Usage: npx ts-node ./scripts/data/injectData.ts " + ); + return -1; + } + + const input = { + type: args[lenArgs - 2], + dir: args[lenArgs - 1], + }; + + const configFilePath = CONFIG_FILE_PATH[input.type as DataType]; + + if (!configFilePath) { + console.error(`Invalid data type ${input.type}`); + return -1; + } + + console.log(`Config file to be update: ${configFilePath}`); + const dataConfig = readFileContent(configFilePath); + + const configDataFolder = CONFIG_DATA_FOLDER[input.type as DataType]; + + if (!configDataFolder) { + console.error(`Invalid data type ${input.type}`); + return -1; + } + + const importDataFolder = path.join(configDataFolder, input.dir); + + console.log(`Imported data folder ${importDataFolder}`); + + const filePaths = listFilePaths(importDataFolder); + + const updatedDataConfig = injectDataEntry( + input.type as DataType, + dataConfig, + filePaths + ); + + writeFileContent(configFilePath, updatedDataConfig); + + return 0; +}; + +main(); diff --git a/scripts/packs/injectPacks.ts b/scripts/packs/injectPacks.ts deleted file mode 100644 index 44540997..00000000 --- a/scripts/packs/injectPacks.ts +++ /dev/null @@ -1,117 +0,0 @@ -import fs from "fs"; -import path from "path"; - -const IMPORT_PLACEHOLDER = "// {IMPORT_PLACEHOLDER}"; -const ENTRY_PLACEHOLDER = "// {ENTRY_PLACEHOLDER}"; - -const DATA_FOLDER = path.join( - __dirname, - "..", - "..", - "packages", - "threat-composer", - "src", - "data" -); - -const THREAT_PACKS_FOLDER = path.join(DATA_FOLDER, "threatPacks"); - -const THREAT_PACKS_FILE = path.join(THREAT_PACKS_FOLDER, "threatPacks.ts"); - -const MITIGATION_PACKS_FOLDER = path.join(DATA_FOLDER, "mitigationPacks"); - -const MITIGATION_PACKS_FILE = path.join( - MITIGATION_PACKS_FOLDER, - "mitigationPacks.ts" -); - -const readFileContent = (filePath: string) => { - return fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); -}; - -const writeFileContent = (filePath: string, content: string) => { - return fs.writeFileSync(filePath, content); -}; - -const injectPackEntry = (packDefinition: string, filePaths: string[]) => { - const fileNames: string[] = []; - const importFiles = filePaths - .map((filePath) => { - const fileName = path - .basename(filePath, path.extname(filePath)) - .replace(/[^a-zA-Z0-9]+/gm, "_"); - fileNames.push(fileName); - const importFile = `import ${fileName} from "${filePath}";`; - return importFile; - }) - .join("\n"); - - let updatedPackDefiniton = packDefinition - .replace(IMPORT_PLACEHOLDER, importFiles) - .replace( - ENTRY_PLACEHOLDER, - `${fileNames.map((fn) => `${fn},`).join("\n")}` - ); - - return updatedPackDefiniton; -}; - -const listFilePaths = (dir: string) => { - const filePaths: string[] = []; - - fs.readdirSync(dir).forEach((x) => { - const filePath = path.join(dir, x); - if (fs.lstatSync(filePath).isDirectory()) { - filePaths.push(...listFilePaths(filePath)); - } - - if (x.endsWith(".json")) { - filePaths.push(filePath); - } - }); - - return filePaths; -}; - -const main = () => { - const args = process.argv; - - console.log("Arguments", args); - - const lenArgs = args.length; - - if (lenArgs !== 4) { - console.log( - "Usage: npx ts-node ./scripts/packs/injectPacks.ts " - ); - return -1; - } - - const input = { - type: args[lenArgs - 2], - dir: args[lenArgs - 1], - }; - - const packFilePath = - input.type === "ThreatPack" ? THREAT_PACKS_FILE : MITIGATION_PACKS_FILE; - const packDefinition = readFileContent(packFilePath); - - console.log(`Pack file to be update: ${packFilePath}`); - - const packFileFolder = - input.type === "ThreatPack" ? THREAT_PACKS_FOLDER : MITIGATION_PACKS_FOLDER; - - const importPackFolder = path.join(packFileFolder, input.dir); - - console.log(`Imported pack folder ${importPackFolder}`); - - const filePaths = listFilePaths(importPackFolder); - - const updatedPackDefiniton = injectPackEntry(packDefinition, filePaths); - - writeFileContent(packFilePath, updatedPackDefiniton); - - return 0; -}; - -main();