From 8a0c342c708603c656c99f237aa2d99aa517af09 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 4 Feb 2025 16:15:39 +0100 Subject: [PATCH] CM-44179 - Add proper support for disabled modules --- CHANGELOG.md | 3 + package.json | 16 ++-- src/commands/common.ts | 2 +- src/commands/run-all-scans-command.ts | 21 ++++- src/extension.ts | 7 +- src/listeners/on-did-save-text-document.ts | 10 ++- src/listeners/on-project-open.ts | 2 +- src/services/cli-service.ts | 25 ++++-- src/services/state-service.ts | 96 ++++++++++++++++++---- src/ui/panels/violation/js.ts | 2 +- src/ui/views/activity-bar.ts | 34 +++++++- src/ui/views/cycode-view.ts | 21 ++++- src/ui/views/scan/content.ts | 18 +++- src/ui/views/scan/scan-view.ts | 12 +++ 14 files changed, 212 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dbebfe..0d8528c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +- Add proper support for disabled modules +- Fix auth required state of status bar + ## [v1.14.0] - Add the "Ignore this violation" button for violation card of SCA diff --git a/package.json b/package.json index ed694a0..70c398d 100644 --- a/package.json +++ b/package.json @@ -65,39 +65,39 @@ "view/item/context": [ { "command": "cycode.secretScanForProject", - "when": "viewItem == secretScanTypeNode", + "when": "viewItem == secretScanTypeNode && cycode:modules.isSecretScanningEnabled", "group": "inline" }, { "command": "cycode.scaScan", - "when": "viewItem == scaScanTypeNode", + "when": "viewItem == scaScanTypeNode && cycode:modules.isScaScanningEnabled", "group": "inline" }, { "command": "cycode.iacScanForProject", - "when": "viewItem == iacScanTypeNode", + "when": "viewItem == iacScanTypeNode && cycode:modules.isIacScanningEnabled", "group": "inline" }, { "command": "cycode.sastScanForProject", - "when": "viewItem == sastScanTypeNode", + "when": "viewItem == sastScanTypeNode && cycode:modules.isSastScanningEnabled", "group": "inline" }, { "command": "cycode.secretScanForProject", - "when": "view == cycode.view.tree && viewItem == secretScanTypeNode" + "when": "view == cycode.view.tree && viewItem == secretScanTypeNode && cycode:modules.isSecretScanningEnabled" }, { "command": "cycode.scaScan", - "when": "view == cycode.view.tree && viewItem == scaScanTypeNode" + "when": "view == cycode.view.tree && viewItem == scaScanTypeNode && cycode:modules.isScaScanningEnabled" }, { "command": "cycode.iacScanForProject", - "when": "view == cycode.view.tree && viewItem == iacScanTypeNode" + "when": "view == cycode.view.tree && viewItem == iacScanTypeNode && cycode:modules.isIacScanningEnabled" }, { "command": "cycode.sastScanForProject", - "when": "view == cycode.view.tree && viewItem == sastScanTypeNode" + "when": "view == cycode.view.tree && viewItem == sastScanTypeNode && cycode:modules.isSastScanningEnabled" } ] }, diff --git a/src/commands/common.ts b/src/commands/common.ts index c98623e..422678a 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -11,7 +11,7 @@ export const getCommonCommand = (command: (...args: never[]) => void | Promise(StateServiceSymbol); - if (requiredAuth && !stateService.globalState.CliAuthed) { + if (requiredAuth && !stateService.tempState.CliAuthed) { vscode.window.showErrorMessage('Please authenticate with Cycode first'); return; } diff --git a/src/commands/run-all-scans-command.ts b/src/commands/run-all-scans-command.ts index 7375e35..f41a3e6 100644 --- a/src/commands/run-all-scans-command.ts +++ b/src/commands/run-all-scans-command.ts @@ -2,14 +2,27 @@ import { container } from 'tsyringe'; import { CycodeService, ICycodeService } from '../services/cycode-service'; import { CliScanType } from '../cli/models/cli-scan-type'; import { getCommonCommand } from './common'; +import { IStateService } from '../services/state-service'; +import { StateServiceSymbol } from '../symbols'; export default getCommonCommand(async () => { const cycodeService = container.resolve(CycodeService); + const stateService = container.resolve(StateServiceSymbol); const scanPromises = []; - scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Secret)); - scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sca)); - scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Iac)); - scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sast)); + + if (stateService.tempState.IsSecretScanningEnabled) { + scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Secret)); + } + if (stateService.tempState.IsScaScanningEnabled) { + scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sca)); + } + if (stateService.tempState.IsIacScanningEnabled) { + scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Iac)); + } + if (stateService.tempState.IsSastScanningEnabled) { + scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sast)); + } + await Promise.all(scanPromises); }); diff --git a/src/extension.ts b/src/extension.ts index 4ff2637..ccd4f82 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,22 +62,19 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(extensionStatusBar); // end refactor - if (!stateService.globalState.CliAuthed) { - statusBar.showAuthIsRequired(); - } - if (config.isTheiaIde) { stateService.globalState.EnvVsCode = false; stateService.save(); } registerCommands(context); - registerActivityBar(context); registerCodeLensProvider(context); registerCodeActionsProvider(context); registerOnDidSaveTextDocument(context); registerOnDidChangeActiveTextEditor(context); + stateService.tempState.ActivityBar = registerActivityBar(context); + // do not await because it blocks loading of the extension like views rendering void postActivate().then(() => { onProjectOpen(); diff --git a/src/listeners/on-did-save-text-document.ts b/src/listeners/on-did-save-text-document.ts index e226d15..d816166 100644 --- a/src/listeners/on-did-save-text-document.ts +++ b/src/listeners/on-did-save-text-document.ts @@ -18,7 +18,7 @@ export const OnDidSaveTextDocument = (document: vscode.TextDocument) => { } const stateService = container.resolve(StateServiceSymbol); - if (!stateService.globalState.CliAuthed) { + if (!stateService.tempState.CliAuthed) { return; } @@ -35,16 +35,18 @@ export const OnDidSaveTextDocument = (document: vscode.TextDocument) => { const cycodeService = container.resolve(CycodeService); - if (isSupportedPackageFile(document.fileName)) { + if (stateService.tempState.IsScaScanningEnabled && isSupportedPackageFile(document.fileName)) { void cycodeService.startScan(CliScanType.Sca, [fileFsPath], false); } - if (isSupportedIacFile(document.fileName)) { + if (stateService.tempState.IsIacScanningEnabled && isSupportedIacFile(document.fileName)) { void cycodeService.startScan(CliScanType.Iac, [fileFsPath], false); } // run Secrets scan on any saved file. CLI will exclude irrelevant files - void cycodeService.startScan(CliScanType.Secret, [fileFsPath], false); + if (stateService.tempState.IsSecretScanningEnabled) { + void cycodeService.startScan(CliScanType.Secret, [fileFsPath], false); + } }; export const registerOnDidSaveTextDocument = (context: vscode.ExtensionContext) => { diff --git a/src/listeners/on-project-open.ts b/src/listeners/on-project-open.ts index ff722fe..0dcf3f8 100644 --- a/src/listeners/on-project-open.ts +++ b/src/listeners/on-project-open.ts @@ -13,7 +13,7 @@ export const onProjectOpen = () => { */ const stateService = container.resolve(StateServiceSymbol); - if (!stateService.globalState.CliAuthed) { + if (!stateService.tempState.CliAuthed) { return; } diff --git a/src/services/cli-service.ts b/src/services/cli-service.ts index f066749..9149037 100644 --- a/src/services/cli-service.ts +++ b/src/services/cli-service.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import { setSentryUser } from '../sentry'; import { inject, singleton } from 'tsyringe'; import { ExtensionServiceSymbol, LoggerServiceSymbol, ScanResultsServiceSymbol, StateServiceSymbol } from '../symbols'; -import { GlobalExtensionState, IStateService } from './state-service'; +import { GlobalExtensionState, IStateService, TemporaryExtensionState } from './state-service'; import { ILoggerService } from './logger-service'; import { CliWrapper } from '../cli/cli-wrapper'; import { CliResult, isCliResultError, isCliResultPanic, isCliResultSuccess } from '../cli/models/cli-result'; @@ -27,6 +27,7 @@ import { CliIgnoreType } from '../cli/models/cli-ignore-type'; import { CliScanType } from '../cli/models/cli-scan-type'; import { StatusResult } from '../cli/models/status-result'; import { AiRemediationResult, AiRemediationResultData } from '../cli/models/ai-remediation-result'; +import statusBar from '../utils/status-bar'; export interface ICliService { getProjectRootDirectory(): string | undefined; // TODO REMOVE @@ -47,6 +48,7 @@ export interface ICliService { @singleton() export class CliService implements ICliService { private state: GlobalExtensionState; + private tempState: TemporaryExtensionState; private cli: CliWrapper; constructor(@inject(StateServiceSymbol) private stateService: IStateService, @@ -55,6 +57,7 @@ export class CliService implements ICliService { @inject(ExtensionServiceSymbol) private extensionService: IExtensionService, ) { this.state = this.stateService.globalState; + this.tempState = this.stateService.tempState; this.cli = new CliWrapper(this.getProjectRootDirectory()); } @@ -72,7 +75,7 @@ export class CliService implements ICliService { return null; } - this.state.CliInstalled = false; + this.tempState.CliInstalled = false; this.state.CliVer = null; this.stateService.save(); } @@ -140,15 +143,19 @@ export class CliService implements ICliService { return; } - this.state.CliInstalled = true; + this.tempState.CliInstalled = true; + this.tempState.CliAuthed = processedResult.result.isAuthenticated; + this.tempState.CliStatus = processedResult.result; + this.state.CliVer = processedResult.result.version; - this.state.CliAuthed = processedResult.result.isAuthenticated; - this.state.IsAiLargeLanguageModelEnabled = processedResult.result.supportedModules.aiLargeLanguageModel; + this.stateService.save(); - if (!this.state.CliAuthed) { + if (!this.tempState.CliAuthed) { + statusBar.showAuthIsRequired(); this.showErrorNotification('You are not authenticated in Cycode. Please authenticate'); } else { + statusBar.showDefault(); if (processedResult.result.userId && processedResult.result.tenantId) { setSentryUser(processedResult.result.userId, processedResult.result.tenantId); } @@ -167,14 +174,14 @@ export class CliService implements ICliService { return false; } - this.state.CliAuthed = processedResult.result.result; + this.tempState.CliAuthed = processedResult.result.result; this.stateService.save(); - if (!this.state.CliAuthed) { + if (!this.tempState.CliAuthed) { this.showErrorNotification('Authentication failed. Please try again'); } - return this.state.CliAuthed; + return this.tempState.CliAuthed; } private mapIgnoreTypeToOptionName(ignoreType: CliIgnoreType): string { diff --git a/src/services/state-service.ts b/src/services/state-service.ts index 38eae30..a0c20fd 100644 --- a/src/services/state-service.ts +++ b/src/services/state-service.ts @@ -3,16 +3,15 @@ import { inject, singleton } from 'tsyringe'; import { LoggerServiceSymbol } from '../symbols'; import { ILoggerService } from './logger-service'; import { GlobalKeyValueStorage, LocalKeyValueStorage } from './key-value-storage-service'; +import { StatusResult } from '../cli/models/status-result'; +import { ActivityBar } from '../ui/views/activity-bar'; export class GlobalExtensionState { public EnvVsCode = true; - public CliInstalled = false; - public CliAuthed = false; public CliVer: string | null = null; public CliHash: string | null = null; public CliDirHashes: Record | null = null; public CliLastUpdateCheckedAt: number | null = null; - public IsAiLargeLanguageModelEnabled = false; } export type GlobalExtensionStateKey = keyof GlobalExtensionState; @@ -22,6 +21,49 @@ export class LocalExtensionState { } export type LocalExtensionStateKey = keyof LocalExtensionState; +export class TemporaryExtensionState { + private _cliStatus: StatusResult | null = null; + + public CliInstalled = false; + public CliAuthed = false; + + public ActivityBar: ActivityBar | null = null; + + public get CliStatus(): StatusResult | null { + return this._cliStatus; + } + + public set CliStatus(value: StatusResult | null) { + this._cliStatus = value; + + if (this.ActivityBar && value?.supportedModules) { + this.ActivityBar.ScanView.postSupportedModules(value.supportedModules); + } + } + + public get IsSecretScanningEnabled(): boolean { + return this.CliStatus?.supportedModules?.secretScanning === true; + } + + public get IsScaScanningEnabled(): boolean { + return this.CliStatus?.supportedModules?.scaScanning === true; + } + + public get IsIacScanningEnabled(): boolean { + return this.CliStatus?.supportedModules?.iacScanning === true; + } + + public get IsSastScanningEnabled(): boolean { + return this.CliStatus?.supportedModules?.sastScanning === true; + } + + public get IsAiLargeLanguageModelEnabled(): boolean { + return this.CliStatus?.supportedModules?.aiLargeLanguageModel === true; + } +} + +export type TempExtensionStateKey = keyof TemporaryExtensionState; + const _GLOBAL_STATE_KEY = 'cycode:globalState'; const _LOCAL_STATE_KEY = 'cycode:localState'; @@ -29,22 +71,39 @@ enum VscodeStates { IsVsCodeEnv = 'env.isVsCode', IsAuthorized = 'auth.isAuthed', IsInstalled = 'cli.isInstalled', + // modules: + IsSecretScanningEnabled = 'modules.isSecretScanningEnabled', + IsScaScanningEnabled = 'modules.isScaScanningEnabled', + IsIacScanningEnabled = 'modules.isIacScanningEnabled', + IsSastScanningEnabled = 'modules.isSastScanningEnabled', + IsAiLargeLanguageModelEnabled = 'modules.isAiLargeLanguageModelEnabled', } const _CONTEXT_EXPORTED_GLOBAL_STATE_KEYS: Record = { // map global state keys to vscode context keys EnvVsCode: VscodeStates.IsVsCodeEnv, - CliAuthed: VscodeStates.IsAuthorized, - CliInstalled: VscodeStates.IsInstalled, }; const _CONTEXT_EXPORTED_LOCAL_STATE_KEYS: Record = { // map local state keys to vscode context keys }; +const _CONTEXT_EXPORTED_TEMP_STATE_KEYS: Record = { + // map temp state keys to vscode context keys + CliAuthed: VscodeStates.IsAuthorized, + CliInstalled: VscodeStates.IsInstalled, + // modules: + IsSecretScanningEnabled: VscodeStates.IsSecretScanningEnabled, + IsScaScanningEnabled: VscodeStates.IsScaScanningEnabled, + IsIacScanningEnabled: VscodeStates.IsIacScanningEnabled, + IsSastScanningEnabled: VscodeStates.IsSastScanningEnabled, + IsAiLargeLanguageModelEnabled: VscodeStates.IsAiLargeLanguageModelEnabled, +}; + export interface IStateService { globalState: GlobalExtensionState; localState: LocalExtensionState; + tempState: TemporaryExtensionState; initContext(context: vscode.ExtensionContext): void; @@ -56,12 +115,14 @@ export interface IStateService { export class StateService implements IStateService { private readonly _globalState: GlobalExtensionState; private readonly _localState: LocalExtensionState; + private readonly _temporaryState: TemporaryExtensionState; private localStorage = new LocalKeyValueStorage(); private globalStorage = new GlobalKeyValueStorage(); constructor(@inject(LoggerServiceSymbol) private logger?: ILoggerService) { this._globalState = new GlobalExtensionState(); this._localState = new LocalExtensionState(); + this._temporaryState = new TemporaryExtensionState(); } get globalState(): GlobalExtensionState { @@ -72,6 +133,10 @@ export class StateService implements IStateService { return this._localState; } + get tempState(): TemporaryExtensionState { + return this._temporaryState; + } + initContext(context: vscode.ExtensionContext): void { this.localStorage.initContext(context); this.globalStorage.initContext(context); @@ -120,12 +185,8 @@ export class StateService implements IStateService { this.loadLocalState(); /* - * TODO(MarshalX): should not be persistent state * reset the state to the default values on every extension initialization */ - this.globalState.CliInstalled = false; - this.globalState.CliAuthed = false; - this.globalState.IsAiLargeLanguageModelEnabled = false; this.saveGlobalState(); } @@ -143,9 +204,15 @@ export class StateService implements IStateService { this.logger?.debug('Save local state'); } + private saveTempState(): void { + this.exportTempStateToContext(); + this.logger?.debug('Save temp state'); + } + save(): void { this.saveGlobalState(); this.saveLocalState(); + this.saveTempState(); } private exportGlobalStateToContext(): void { @@ -160,19 +227,20 @@ export class StateService implements IStateService { } } + private exportTempStateToContext(): void { + for (const [stateKey, contextKey] of Object.entries(_CONTEXT_EXPORTED_TEMP_STATE_KEYS)) { + this.setContext(contextKey, this._temporaryState[stateKey as TempExtensionStateKey]); + } + } + private mergeGlobalState(extensionState: GlobalExtensionState): void { if (extensionState.EnvVsCode !== undefined) (this._globalState.EnvVsCode = extensionState.EnvVsCode); - if (extensionState.CliInstalled !== undefined) (this._globalState.CliInstalled = extensionState.CliInstalled); - if (extensionState.CliAuthed !== undefined) (this._globalState.CliAuthed = extensionState.CliAuthed); if (extensionState.CliVer !== undefined) (this._globalState.CliVer = extensionState.CliVer); if (extensionState.CliHash !== undefined) (this._globalState.CliHash = extensionState.CliHash); if (extensionState.CliDirHashes !== undefined) (this._globalState.CliDirHashes = extensionState.CliDirHashes); if (extensionState.CliLastUpdateCheckedAt !== undefined) ( this._globalState.CliLastUpdateCheckedAt = extensionState.CliLastUpdateCheckedAt ); - if (extensionState.IsAiLargeLanguageModelEnabled !== undefined) ( - this._globalState.IsAiLargeLanguageModelEnabled = extensionState.IsAiLargeLanguageModelEnabled - ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/ui/panels/violation/js.ts b/src/ui/panels/violation/js.ts index a336214..d933292 100644 --- a/src/ui/panels/violation/js.ts +++ b/src/ui/panels/violation/js.ts @@ -9,7 +9,7 @@ import { StateServiceSymbol } from '../../../symbols'; const isAiEnabled = () => { const stateService = container.resolve(StateServiceSymbol); - return stateService.globalState.IsAiLargeLanguageModelEnabled; + return stateService.tempState.IsAiLargeLanguageModelEnabled; }; export default (detectionType: CliScanType) => ` diff --git a/src/ui/views/activity-bar.ts b/src/ui/views/activity-bar.ts index a3d2933..a530734 100644 --- a/src/ui/views/activity-bar.ts +++ b/src/ui/views/activity-bar.ts @@ -3,21 +3,47 @@ import ScanView from './scan/scan-view'; import LoadingView from './loading/loading-view'; import AuthView from './auth/auth-view'; -export const registerActivityBar = (context: vscode.ExtensionContext): void => { +export class ActivityBar { + public ScanView: ScanView; + public LoadingView: LoadingView; + public AuthView: AuthView; + + constructor() { + this.ScanView = new ScanView(); + this.LoadingView = new LoadingView(); + this.AuthView = new AuthView(); + } +} + +export const registerActivityBar = (context: vscode.ExtensionContext): ActivityBar => { + const activityBar = new ActivityBar(); + + const registerOptions = { + webviewOptions: { + // we want to be able to update unconfused UI views; for example, to update supported modules + retainContextWhenHidden: true, + }, + }; + const scanView = vscode.window.registerWebviewViewProvider( ScanView.viewType, - new ScanView(), + activityBar.ScanView, + registerOptions, ); const loadingView = vscode.window.registerWebviewViewProvider( LoadingView.viewType, - new LoadingView(), + activityBar.LoadingView, + registerOptions, ); const authView = vscode.window.registerWebviewViewProvider( AuthView.viewType, - new AuthView(), + activityBar.AuthView, + registerOptions, ); context.subscriptions.push(scanView, loadingView, authView); + + return activityBar; }; diff --git a/src/ui/views/cycode-view.ts b/src/ui/views/cycode-view.ts index 1cbfcc8..c120411 100644 --- a/src/ui/views/cycode-view.ts +++ b/src/ui/views/cycode-view.ts @@ -3,6 +3,7 @@ import { VscodeCommands } from '../../commands'; export abstract class CycodeView implements vscode.WebviewViewProvider { protected _view?: vscode.WebviewView; + private _messageQueue: unknown[] = []; private readonly htmlContent: string; @@ -13,6 +14,7 @@ export abstract class CycodeView implements vscode.WebviewViewProvider { public resolveWebviewView(webviewView: vscode.WebviewView): void { this._view = webviewView; this.updateView(); + this.flushMessageQueue(); } private updateView() { @@ -27,10 +29,25 @@ export abstract class CycodeView implements vscode.WebviewViewProvider { if (Object.values(VscodeCommands).includes(command)) { // send command back after executing to unblock disabled buttons vscode.commands.executeCommand(command).then( - () => this._view?.webview.postMessage({ command, finished: true, success: true }), - () => this._view?.webview.postMessage({ command, finished: true, success: false }), + () => this.postMessage({ command, finished: true, success: true }), + () => this.postMessage({ command, finished: true, success: false }), ); } }); } + + public postMessage(message: unknown): Thenable { + if (!this._view) { + this._messageQueue.push(message); + return Promise.resolve(true); + } + + return this._view?.webview.postMessage(message); + } + + private flushMessageQueue(): void { + while (this._messageQueue.length > 0) { + this._view?.webview.postMessage(this._messageQueue.shift()); + } + } } diff --git a/src/ui/views/scan/content.ts b/src/ui/views/scan/content.ts index 7566b66..52cab0f 100644 --- a/src/ui/views/scan/content.ts +++ b/src/ui/views/scan/content.ts @@ -11,13 +11,13 @@ const getBtnText = (scanType: CliScanType, inProgress = false) => { const body = `

Ready to scan.

- +
- +
- +
- +

To easily scan your files, enable Scan On Save in @@ -47,6 +47,16 @@ registerButton( 'scan-iac-button', '${VscodeCommands.IacScanForProjectCommandId}', '${getBtnText(CliScanType.Iac, true)}' ); registerButton('open-cycode-settings', '${VscodeCommands.OpenSettingsCommandId}'); + +window.addEventListener('message', event => { + if (event.data.command === 'supportedModules' && event.data.modules) { + const modules = event.data.modules; + ge('scan-secrets-button').disabled = !modules.secretEnabled; + ge('scan-sca-button').disabled = !modules.scaEnabled; + ge('scan-sast-button').disabled = !modules.sastEnabled; + ge('scan-iac-button').disabled = !modules.iacEnabled; + } +}); `; diff --git a/src/ui/views/scan/scan-view.ts b/src/ui/views/scan/scan-view.ts index a9e5867..dfeb4e7 100644 --- a/src/ui/views/scan/scan-view.ts +++ b/src/ui/views/scan/scan-view.ts @@ -1,5 +1,6 @@ import { CycodeView } from '../cycode-view'; import content from './content'; +import { SupportedModulesStatus } from '../../../cli/models/status-result'; export default class ScanView extends CycodeView { public static readonly viewType = 'cycode.view.scan'; @@ -7,4 +8,15 @@ export default class ScanView extends CycodeView { constructor() { super(content); } + + public postSupportedModules(modules: SupportedModulesStatus): void { + const moduleStatus = { + secretEnabled: modules.secretScanning, + scaEnabled: modules.scaScanning, + iacEnabled: modules.iacScanning, + sastEnabled: modules.sastScanning, + aiEnabled: modules.aiLargeLanguageModel, + }; + this.postMessage({ command: 'supportedModules', modules: moduleStatus }); + } }