Skip to content

Commit 75d0d15

Browse files
authored
feat(ThreatComposer): edit, view Threat Models aws#5109
Problem Threat models, represented as *.tc.json files, are rendered using the default JSON editor Solution Build a custom editor in VSCode to render *.tc.json files as a Threat Model, similar to how it would be displayed in a browser
1 parent 6ab41ba commit 75d0d15

24 files changed

+1511
-4
lines changed
615 KB
Loading

packages/core/package.json

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@
278278
"markdownDescription": "%AWS.configuration.description.amazonq.shareContentWithAWS%",
279279
"default": true,
280280
"scope": "application"
281+
},
282+
"aws.threatComposer.defaultEditor": {
283+
"type": "boolean",
284+
"default": true,
285+
"description": "%AWS.configuration.description.threatComposer.defaultEditor%"
281286
}
282287
}
283288
},
@@ -1174,6 +1179,10 @@
11741179
{
11751180
"command": "aws.toolkit.amazonq.extensionpage",
11761181
"when": "false"
1182+
},
1183+
{
1184+
"command": "aws.newThreatComposerFile",
1185+
"when": "false"
11771186
}
11781187
],
11791188
"editor/title": [
@@ -1224,7 +1233,7 @@
12241233
},
12251234
{
12261235
"command": "aws.openInApplicationComposer",
1227-
"when": "editorLangId == json || editorLangId == yaml || resourceFilename =~ /^.*\\.(template)$/",
1236+
"when": "(editorLangId == json && !(resourceFilename =~ /^.*\\.tc\\.json$/)) || editorLangId == yaml || resourceFilename =~ /^.*\\.(template)$/",
12281237
"group": "navigation"
12291238
}
12301239
],
@@ -1345,7 +1354,7 @@
13451354
},
13461355
{
13471356
"command": "aws.openInApplicationComposer",
1348-
"when": "isFileSystemResource && resourceFilename =~ /^.*\\.(json|yml|yaml|template)$/",
1357+
"when": "isFileSystemResource && !(resourceFilename =~ /^.*\\.tc\\.json$/) && resourceFilename =~ /^.*\\.(json|yml|yaml|template)$/",
13491358
"group": "z_aws@1"
13501359
}
13511360
],
@@ -2014,6 +2023,11 @@
20142023
"command": "aws.toolkit.viewLogs",
20152024
"group": "1_help@5"
20162025
}
2026+
],
2027+
"file/newFile": [
2028+
{
2029+
"command": "aws.newThreatComposerFile"
2030+
}
20172031
]
20182032
},
20192033
"commands": [
@@ -3482,6 +3496,26 @@
34823496
"category": "%AWS.title.cn%"
34833497
}
34843498
}
3499+
},
3500+
{
3501+
"command": "aws.createNewThreatComposer",
3502+
"title": "%AWS.command.threatComposer.createNew%",
3503+
"category": "%AWS.title%",
3504+
"cloud9": {
3505+
"cn": {
3506+
"category": "%AWS.title.cn%"
3507+
}
3508+
}
3509+
},
3510+
{
3511+
"command": "aws.newThreatComposerFile",
3512+
"title": "%AWS.command.threatComposer.newFile%",
3513+
"category": "%AWS.title%",
3514+
"cloud9": {
3515+
"cn": {
3516+
"category": "%AWS.title.cn%"
3517+
}
3518+
}
34853519
}
34863520
],
34873521
"jsonValidation": [
@@ -3937,7 +3971,23 @@
39373971
}
39383972
]
39393973
}
3940-
]
3974+
],
3975+
"customEditors": [
3976+
{
3977+
"viewType": "threatComposer.tc.json",
3978+
"displayName": "%AWS.threatComposer.title%",
3979+
"selector": [
3980+
{
3981+
"filenamePattern": "*.tc.json"
3982+
}
3983+
]
3984+
}
3985+
],
3986+
"configurationDefaults": {
3987+
"workbench.editorAssociations": {
3988+
"{git,gitlens,conflictResolution,vscode-local-history}:/**/*.tc.json": "default"
3989+
}
3990+
}
39413991
},
39423992
"scripts": {
39433993
"postinstall": "npm run generateTelemetry && npm run generateConfigurationAttributes && npm run compile",

packages/core/package.nls.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@
212212
"AWS.command.q.transform.showChanges": "Show Proposed Changes",
213213
"AWS.command.q.transform.showChangeSummary": "Show Transformation Summary",
214214
"AWS.command.q.transform.showTransformationPlan": "Show Transformation Plan",
215+
"AWS.command.threatComposer.createNew": "Create New Threat Composer File",
216+
"AWS.command.threatComposer.newFile": "Threat Composer File",
217+
"AWS.threatComposer.page.title": "{0} (Threat Composer)",
218+
"AWS.threatComposer.title": "Threat Composer",
219+
"AWS.configuration.description.threatComposer.defaultEditor": "Use Threat Composer as the default editor for *.tc.json files.",
215220
"AWS.command.accessanalyzer.iamPolicyChecks": "Open IAM Policy Checks",
216221
"AWS.lambda.explorerTitle": "Explorer",
217222
"AWS.developerTools.explorerTitle": "Developer Tools",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/* eslint-disable no-undef */
7+
/* eslint-disable no-case-declarations */
8+
9+
const autoSaveIntervalTimeout = 1000
10+
const checkThreatComposerAPITimeout = 50
11+
12+
const vscode = acquireVsCodeApi()
13+
const instanceMapper = {}
14+
let disableAutoSave = false
15+
let theme = 'unknown'
16+
17+
// Handle messages sent from the extension to the webview
18+
window.addEventListener('message', handleMessage)
19+
20+
function handleMessage(event) {
21+
const message = event.data // The json data that the extension sent
22+
23+
if (message.messageType === 'BROADCAST') {
24+
switch (message.command) {
25+
case 'FILE_CHANGED':
26+
const fileContents = message.fileContents
27+
28+
// Persist state information.
29+
// This state is returned in the call to `vscode.getState` below when a webview is reloaded.
30+
vscode.setState({
31+
fileName: message.fileName,
32+
filePath: message.filePath,
33+
fileContents: fileContents,
34+
})
35+
36+
// Update our webview's content
37+
updateContent(fileContents)
38+
break
39+
40+
case 'THEME_CHANGED':
41+
applyTheme(message.newTheme)
42+
break
43+
44+
case 'OVERWRITE_FILE':
45+
disableAutoSave = false
46+
break
47+
}
48+
} else if (message.messageType === 'RESPONSE') {
49+
switch (message.command) {
50+
case 'SAVE_FILE':
51+
if (message.isSuccess) {
52+
console.log('File Saved successfully')
53+
disableAutoSave = false
54+
} else {
55+
console.log('File Save unsuccessful')
56+
}
57+
break
58+
}
59+
}
60+
}
61+
62+
function updateContent(/** @type {string} */ text) {
63+
if (document.readyState === 'complete') {
64+
void render(text)
65+
} else {
66+
document.addEventListener('DOMContentLoaded', function () {
67+
void render(text)
68+
})
69+
}
70+
}
71+
72+
async function checkThreatComposerAPI() {
73+
while (!window.threatcomposer || !window.threatcomposer.setCurrentWorkspaceData) {
74+
await new Promise(r => setTimeout(r, checkThreatComposerAPITimeout))
75+
}
76+
}
77+
78+
/**
79+
* Render the document in the webview.
80+
*/
81+
async function render(/** @type {string} */ text) {
82+
await checkThreatComposerAPI()
83+
84+
vscode.postMessage({
85+
command: 'LOAD_STAGE',
86+
messageType: 'BROADCAST',
87+
loadStage: 'API_LOADED',
88+
})
89+
90+
try {
91+
let threatModel = text && JSON.parse(text)
92+
await window.threatcomposer.setCurrentWorkspaceData(threatModel)
93+
} catch (e) {
94+
disableAutoSave = true
95+
await window.threatcomposer.setCurrentWorkspaceData('')
96+
vscode.postMessage({
97+
command: 'LOG',
98+
messageType: 'BROADCAST',
99+
logMessage: e.message,
100+
logType: 'ERROR',
101+
showNotification: true,
102+
notificationType: 'INVALID_JSON',
103+
})
104+
}
105+
106+
let defaultTemplate = ''
107+
108+
if (text === '') {
109+
const initialState = await window.threatcomposer.getCurrentWorkspaceData()
110+
defaultTemplate = window.threatcomposer.stringifyWorkspaceData(initialState)
111+
}
112+
113+
window.threatcomposer.addEventListener('save', e => {
114+
const stringyfiedData = window.threatcomposer.stringifyWorkspaceData(e.detail)
115+
const currentState = vscode.getState()
116+
currentState.fileContents = stringyfiedData
117+
vscode.setState(currentState)
118+
disableAutoSave = true
119+
120+
vscode.postMessage({
121+
command: 'SAVE_FILE',
122+
messageType: 'REQUEST',
123+
fileContents: stringyfiedData,
124+
})
125+
})
126+
127+
autoSaveInterval = setInterval(async () => {
128+
if (disableAutoSave) {
129+
return
130+
}
131+
132+
const data = await window.threatcomposer.getCurrentWorkspaceData()
133+
const stringyfiedData = window.threatcomposer.stringifyWorkspaceData(data)
134+
135+
const currentState = vscode.getState()
136+
137+
if (stringyfiedData === defaultTemplate || stringyfiedData === currentState.fileContents) {
138+
return
139+
}
140+
141+
currentState.fileContents = stringyfiedData
142+
vscode.setState(currentState)
143+
144+
vscode.postMessage({
145+
command: 'AUTO_SAVE_FILE',
146+
messageType: 'REQUEST',
147+
fileContents: stringyfiedData,
148+
})
149+
}, autoSaveIntervalTimeout)
150+
151+
vscode.postMessage({
152+
command: 'LOAD_STAGE',
153+
messageType: 'BROADCAST',
154+
loadStage: 'RENDER_COMPLETE',
155+
})
156+
}
157+
158+
function applyTheme(newTheme) {
159+
if (!newTheme || theme === newTheme) {
160+
return
161+
}
162+
theme = newTheme
163+
164+
window.threatcomposer.applyTheme(newTheme)
165+
}
166+
167+
// Webviews are normally torn down when not visible and re-created when they become visible again.
168+
// State lets us save information across these re-loads
169+
const state = vscode.getState()
170+
if (state && state.fileContents) {
171+
vscode.postMessage({
172+
command: 'RELOAD',
173+
messageType: 'REQUEST',
174+
})
175+
} else {
176+
vscode.postMessage({
177+
command: 'INIT',
178+
messageType: 'REQUEST',
179+
})
180+
}

packages/core/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { ExtensionUse } from './auth/utils'
6060
import { ExtStartUpSources } from './shared/telemetry'
6161

6262
export { makeEndpointsProvider, registerGenericCommands } from './extensionShared'
63+
import { activate as activateThreatComposerEditor } from './threatComposer/activation'
6364

6465
let localize: nls.LocalizeFunc
6566

@@ -221,6 +222,7 @@ export async function activate(context: vscode.ExtensionContext) {
221222
}
222223
}
223224
await activateApplicationComposer(context)
225+
await activateThreatComposerEditor(context)
224226
}
225227

226228
await activateStepFunctions(context, globals.awsContext, globals.outputChannel)

packages/core/src/feedback/vue/submitFeedback.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class FeedbackWebview extends VueWebview {
6767
}
6868
}
6969

70-
type FeedbackId = 'AWS Toolkit' | 'Amazon Q' | 'Application Composer'
70+
type FeedbackId = 'AWS Toolkit' | 'Amazon Q' | 'Application Composer' | 'Threat Composer'
7171

7272
let _submitFeedback: RegisteredCommand<(_: VsCodeCommandArg, id: FeedbackId) => Promise<void>> | undefined
7373
export function submitFeedback(_: VsCodeCommandArg, id: FeedbackId) {

packages/core/src/shared/telemetry/vscodeTelemetry.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,12 @@
332332
"name": "awsRegion",
333333
"type": "string",
334334
"description": "An AWS region."
335+
},
336+
{
337+
"name": "saveType",
338+
"type": "string",
339+
"allowedValues": ["MANUAL_SAVE", "AUTO_SAVE"],
340+
"description": "Type of save executed"
335341
}
336342
],
337343
"metrics": [
@@ -1243,6 +1249,60 @@
12431249
"required": true
12441250
}
12451251
]
1252+
},
1253+
{
1254+
"name": "threatComposer_created",
1255+
"description": "Called after a new threat composer file is created using command pallet or New File option",
1256+
"metadata": [
1257+
{
1258+
"type": "id",
1259+
"required": true
1260+
}
1261+
]
1262+
},
1263+
{
1264+
"name": "threatComposer_opened",
1265+
"description": "Called after opening a threat composer file",
1266+
"metadata": [
1267+
{
1268+
"type": "id",
1269+
"required": true
1270+
}
1271+
]
1272+
},
1273+
{
1274+
"name": "threatComposer_fileSaved",
1275+
"description": "Called after a threat composer file has been saved",
1276+
"metadata": [
1277+
{
1278+
"type": "id",
1279+
"required": true
1280+
},
1281+
{
1282+
"type": "saveType",
1283+
"required": true
1284+
}
1285+
]
1286+
},
1287+
{
1288+
"name": "threatComposer_closed",
1289+
"description": "Called after closing a threat composer file",
1290+
"metadata": [
1291+
{
1292+
"type": "id",
1293+
"required": true
1294+
}
1295+
]
1296+
},
1297+
{
1298+
"name": "threatComposer_error",
1299+
"description": "Called after an error is thrown from the threat composer view",
1300+
"metadata": [
1301+
{
1302+
"type": "id",
1303+
"required": true
1304+
}
1305+
]
12461306
}
12471307
]
12481308
}

0 commit comments

Comments
 (0)