From 8ceb987ed3c24b0d40cdddde4c27e8ce1326008e Mon Sep 17 00:00:00 2001 From: f1ames Date: Tue, 30 Jan 2024 15:50:52 +0100 Subject: [PATCH 01/19] chore: draft fingerprint suppressions code action provider --- ...tion-suppressions-code-actions-provider.ts | 10 +++ ...rint-suppressions-code-actions-provider.ts | 83 +++++++++++++++++++ src/core/code-actions/index.ts | 1 + src/core/suppressions/index.ts | 1 + src/core/suppressions/suppressions.ts | 24 ++++++ src/extension.ts | 3 +- src/utils/globals.ts | 25 ++++++ src/utils/workspace.ts | 4 + 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts create mode 100644 src/core/suppressions/index.ts create mode 100644 src/core/suppressions/suppressions.ts diff --git a/src/core/code-actions/annotation-suppressions-code-actions-provider.ts b/src/core/code-actions/annotation-suppressions-code-actions-provider.ts index debbbdf..e675e2b 100644 --- a/src/core/code-actions/annotation-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/annotation-suppressions-code-actions-provider.ts @@ -1,9 +1,19 @@ import { CodeAction, CodeActionKind, TextDocument, Range, languages } from 'vscode'; import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, ValidationResultExtended } from './base-code-actions-provider'; import { COMMANDS } from '../../constants'; +import { getOwnerWorkspace } from '../../utils/workspace'; +import { shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; class AnnotationSuppressionsCodeActionsProvider extends BaseCodeActionsProvider { public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) { + const workspaceRoot = getOwnerWorkspace(document); + const fingerprintSuppressionsPermissions = await shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); + + // We allow either annotation or fingerprint suppressions at the same time, but not both. + if (fingerprintSuppressionsPermissions.allowed) { + return []; + } + return this.getMonokleDiagnostics(context).map((diagnostic: DiagnosticExtended) => { return new AnnotationSuppressionsCodeAction(document, diagnostic); }); diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts new file mode 100644 index 0000000..0f45288 --- /dev/null +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -0,0 +1,83 @@ +import { CodeAction, CodeActionKind, TextDocument, Range, languages } from 'vscode'; +import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, ValidationResultExtended } from './base-code-actions-provider'; +import { COMMANDS } from '../../constants'; +import { getOwnerWorkspace } from '../../utils/workspace'; +import globals from '../../utils/globals'; +import { SuppressionPermissions, shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; + +class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider { + public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) { + const workspaceRoot = getOwnerWorkspace(document); + const fingerprintSuppressionsPermissions = await shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); + + if (!fingerprintSuppressionsPermissions.allowed) { + return []; + } + + return this.getMonokleDiagnostics(context).map((diagnostic: DiagnosticExtended) => { + return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions); + }); + } + + public async resolveCodeAction(codeAction: FingerprintSuppressionsCodeAction) { + const user = await globals.getUser(); + + if (!user.isAuthenticated) { + // This should not happen, since we don't show this code action if user is not authenticated. + // Still handle it if something unpredictable happens. + codeAction.command = { + command: COMMANDS.RAISE_AUTHENTICATION_ERROR, + title: 'Raise Authentication Error', + arguments: ['Suppressing a problem requires authentication.', { + event: 'code_action/fingerprint_suppression', + data: { + status: 'cancelled', + ruleId: codeAction.result.ruleId, + error: 'Unauthenticated', + } + }] + }; + } else { + // suppress + track + // what about removal, removal option should be shown in SARIF panel + codeAction.command = { + command: COMMANDS.TRACK, + title: 'Track', + arguments: ['code_action/fingerprint_suppression', { + status: 'success', + action: codeAction.permissions === 'ADD' ? 'add' : 'request', + ruleId: codeAction.result.ruleId + }] + }; + } + + return codeAction; + } +} + +class FingerprintSuppressionsCodeAction extends CodeAction { + private readonly _result: ValidationResultExtended; + private readonly _permissions: SuppressionPermissions; + + constructor(diagnostic: DiagnosticExtended, permissions: SuppressionPermissions) { + super(`${permissions === 'ADD' ? 'Suppress' : 'Request suppression of'} this "${diagnostic.result._rule.name} (${diagnostic.result._rule.id})" problem`, CodeActionKind.QuickFix); + + this.diagnostics = [diagnostic]; + this._result = diagnostic.result; + this._permissions = permissions; + } + + get result() { + return this._result; + } + + get permissions() { + return this._permissions; + } +} + +export function registerFingerprintSuppressionsCodeActionsProvider() { + return languages.registerCodeActionsProvider({language: 'yaml'}, new FingerprintSuppressionsCodeActionsProvider(), { + providedCodeActionKinds: FingerprintSuppressionsCodeActionsProvider.providedCodeActionKinds + }); +} diff --git a/src/core/code-actions/index.ts b/src/core/code-actions/index.ts index d62f56f..6ddfc18 100644 --- a/src/core/code-actions/index.ts +++ b/src/core/code-actions/index.ts @@ -1,3 +1,4 @@ export * from './annotation-suppressions-code-actions-provider'; export * from './fix-code-actions-provider'; export * from './show-details-code-actions-provider'; +export * from './fingerprint-suppressions-code-actions-provider'; diff --git a/src/core/suppressions/index.ts b/src/core/suppressions/index.ts new file mode 100644 index 0000000..80ad7da --- /dev/null +++ b/src/core/suppressions/index.ts @@ -0,0 +1 @@ +export * from './suppressions'; diff --git a/src/core/suppressions/suppressions.ts b/src/core/suppressions/suppressions.ts new file mode 100644 index 0000000..54d9110 --- /dev/null +++ b/src/core/suppressions/suppressions.ts @@ -0,0 +1,24 @@ +import globals from '../../utils/globals'; + +export type SuppressionPermissions = 'ADD' | 'REQUEST' | 'NONE'; +export type SuppressionsStatus = { + permissions: SuppressionPermissions; + allowed: boolean; +}; + +export async function shouldUseFingerprintSuppressions(repoRootPath: string): Promise { + const isAuthenticated = (await globals.getUser()).isAuthenticated; + + if (!isAuthenticated) { + return { + permissions: 'NONE', + allowed: false, + }; + } + + const projectPermissions = await globals.getSuppressionPermissions(repoRootPath); + return { + permissions: projectPermissions, + allowed: projectPermissions !== 'NONE', + }; +} diff --git a/src/extension.ts b/src/extension.ts index 47dc305..cba9f43 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,7 +23,7 @@ import { trackEvent, initTelemetry, closeTelemetry } from './utils/telemetry'; import logger from './utils/logger'; import globals from './utils/globals'; import { raiseError } from './utils/errors'; -import { registerAnnotationSuppressionsCodeActionsProvider, registerFixCodeActionsProvider, registerShowDetailsCodeActionsProvider } from './core'; +import { registerAnnotationSuppressionsCodeActionsProvider, registerFingerprintSuppressionsCodeActionsProvider, registerFixCodeActionsProvider, registerShowDetailsCodeActionsProvider } from './core'; import type { ExtensionContext } from 'vscode'; let runtimeContext: RuntimeContext; @@ -125,6 +125,7 @@ async function runActivation(context: ExtensionContext) { context.subscriptions.push(registerAnnotationSuppressionsCodeActionsProvider()); context.subscriptions.push(registerFixCodeActionsProvider()); context.subscriptions.push(registerShowDetailsCodeActionsProvider()); + context.subscriptions.push(registerFingerprintSuppressionsCodeActionsProvider()); const configurationWatcher = workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration(SETTINGS.ENABLED_PATH)) { diff --git a/src/utils/globals.ts b/src/utils/globals.ts index b27d45c..82c9f33 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -5,6 +5,7 @@ import { Folder } from './workspace'; import { RuntimeContext } from './runtime-context'; import logger from './logger'; import type { Authenticator } from './authentication'; +import { SuppressionPermissions } from '../core/suppressions/suppressions'; export type FolderStatus = { valid: boolean; @@ -168,6 +169,30 @@ class Globals { } } + async getSuppressionPermissions(_repoPath: string): Promise { + if (this._runtimeContext.isLocal) { + return 'NONE'; + } + + if (!this._runtimeContext?.authenticator) { + throw new Error('Authenticator not initialized for globals.'); + } + + if (!this._runtimeContext?.synchronizer) { + throw new Error('Synchronizer not initialized for globals.'); + } + + try { + const user = await this._runtimeContext.authenticator.getUser(); + // @TODO fetch permission based on repoPath (and if it's part of any project) or global project config (this.project) + // this should be cached somehow so we don't request it every time + return 'ADD'; // tmp + + } catch (err) { + return 'NONE'; + } + } + getFolderStatus(folder: Folder) { return this._statuses[folder.id]; } diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts index 064dad2..3d554e7 100644 --- a/src/utils/workspace.ts +++ b/src/utils/workspace.ts @@ -37,6 +37,10 @@ export function getWorkspaceFolders(): Folder[] { })); } +export function getOwnerWorkspace(document: TextDocument): Folder { + return getWorkspaceFolders().find(folder => isSubpath(folder.uri, document.uri.fsPath)); +} + // Config precedence: // 1. Remote policy (if user logged in). // - If there is an error fetching (any other than NO_POLICY), fallback to other options. From a2c092f306162161d7ec967e28d2bc2f444cc611 Mon Sep 17 00:00:00 2001 From: f1ames Date: Tue, 6 Feb 2024 11:33:51 +0100 Subject: [PATCH 02/19] refactor: migrate to ProjectSynchronizer --- ...tion-suppressions-code-actions-provider.ts | 2 +- ...rint-suppressions-code-actions-provider.ts | 2 +- .../show-details-code-actions-provider.ts | 2 + src/core/index.ts | 1 + src/core/suppressions/suppressions.ts | 42 +++++++-- src/extension.ts | 2 +- src/utils/globals.ts | 86 ++++--------------- src/utils/policy-puller.ts | 8 +- src/utils/suppressions.ts | 42 --------- src/utils/synchronization.ts | 8 +- src/utils/validation.ts | 4 +- src/utils/workspace.ts | 4 +- 12 files changed, 65 insertions(+), 138 deletions(-) delete mode 100644 src/utils/suppressions.ts diff --git a/src/core/code-actions/annotation-suppressions-code-actions-provider.ts b/src/core/code-actions/annotation-suppressions-code-actions-provider.ts index e675e2b..79e6be6 100644 --- a/src/core/code-actions/annotation-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/annotation-suppressions-code-actions-provider.ts @@ -7,7 +7,7 @@ import { shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; class AnnotationSuppressionsCodeActionsProvider extends BaseCodeActionsProvider { public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) { const workspaceRoot = getOwnerWorkspace(document); - const fingerprintSuppressionsPermissions = await shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); + const fingerprintSuppressionsPermissions = shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); // We allow either annotation or fingerprint suppressions at the same time, but not both. if (fingerprintSuppressionsPermissions.allowed) { diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts index 0f45288..a61fbd6 100644 --- a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -8,7 +8,7 @@ import { SuppressionPermissions, shouldUseFingerprintSuppressions } from '../sup class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider { public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) { const workspaceRoot = getOwnerWorkspace(document); - const fingerprintSuppressionsPermissions = await shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); + const fingerprintSuppressionsPermissions = shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath); if (!fingerprintSuppressionsPermissions.allowed) { return []; diff --git a/src/core/code-actions/show-details-code-actions-provider.ts b/src/core/code-actions/show-details-code-actions-provider.ts index 4317a2d..7f6b376 100644 --- a/src/core/code-actions/show-details-code-actions-provider.ts +++ b/src/core/code-actions/show-details-code-actions-provider.ts @@ -39,6 +39,8 @@ class ShowDetailsCodeActionsProvider extends BaseCodeActionsProvider { - const isAuthenticated = (await globals.getUser()).isAuthenticated; +export function getSuppressions(path: string) { + const fingerprintSuppressions = globals.getSuppressions(path).map((suppression) => { + return { + guid: suppression.id, + kind: 'external', + status: toSuppressionStatus(suppression.status), + fingerprint: suppression.fingerprint, + } as FingerprintSuppression; + }); - if (!isAuthenticated) { - return { - permissions: 'NONE', - allowed: false, - }; - } + return { + suppressions: fingerprintSuppressions, + }; +} + +export function shouldUseFingerprintSuppressions(repoRootPath: string): SuppressionsStatus { + const projectPermissions = globals.getProjectPermissions(repoRootPath); - const projectPermissions = await globals.getSuppressionPermissions(repoRootPath); return { permissions: projectPermissions, allowed: projectPermissions !== 'NONE', }; } + +function toSuppressionStatus(status: string) { + switch (status) { + case 'ACCEPTED': + case 'accepted': + return 'accepted'; + case 'REJECTED': + case 'rejected': + return 'rejected'; + case 'UNDER_REVIEW': + case 'underReview': + return 'underReview'; + default: + return status; + } +} diff --git a/src/extension.ts b/src/extension.ts index cba9f43..d945870 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -305,7 +305,7 @@ function setupRemoteEventListeners(runtimeContext: RuntimeContext) { await commands.executeCommand(COMMANDS.VALIDATE); }); - runtimeContext.synchronizer.on('synchronize', async (policy) => { + runtimeContext.synchronizer.on('synchronized', async (policy) => { logger.log('EVENT:synchronize', policy, globals.isActivated); if (!globals.isActivated || !globals.enabled) { diff --git a/src/utils/globals.ts b/src/utils/globals.ts index 82c9f33..5d699fd 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -95,33 +95,16 @@ class Globals { return this._runtimeContext.authenticator.getUser(); } - async getRemoteProjectName(path: string) { - if (this._runtimeContext.isLocal) { + getRemoteProjectName(path: string) { + if (this._runtimeContext.isLocal || !this._runtimeContext.authenticator.user.isAuthenticated) { return ''; } - if (!this._runtimeContext?.authenticator) { - throw new Error('Authenticator not initialized for globals.'); - } - - if (!this._runtimeContext?.synchronizer) { - throw new Error('Synchronizer not initialized for globals.'); - } - - try { - const user = await this._runtimeContext.authenticator.getUser(); - const projectInfo = this.project?.length ? - await this._runtimeContext.synchronizer.getProjectInfo({slug: this.project}, user.tokenInfo) : - await this._runtimeContext.synchronizer.getProjectInfo(path, user.tokenInfo); - - return projectInfo?.name ?? ''; - } catch (err) { - return ''; - } + return (this._runtimeContext?.synchronizer.getProjectInfo(path, this.project ?? undefined) || {}).name ?? ''; } - async getRemotePolicy(path: string) { - if (this._runtimeContext.isLocal) { + getRemotePolicy(path: string) { + if (this._runtimeContext.isLocal || !this._runtimeContext.authenticator.user.isAuthenticated) { return { valid: false, path: '', @@ -129,68 +112,29 @@ class Globals { }; } - if (!this._runtimeContext?.synchronizer) { - throw new Error('Synchronizer not initialized for globals.'); - } - - try { - const policy = this.project?.length ? - await this._runtimeContext.synchronizer.getPolicy({slug: this.project}) : - await this._runtimeContext.synchronizer.getPolicy(path); - - return policy; - } catch (err) { - return { - valid: false, - path: '', - policy: {}, - }; - } + return this._runtimeContext?.synchronizer.getProjectPolicy(path, this.project ?? undefined); } - async getSuppressions(path: string, token: any) { - if (this._runtimeContext.isLocal) { + getSuppressions(path: string) { + if (this._runtimeContext.isLocal || !this._runtimeContext.authenticator.user.isAuthenticated) { return []; } - if (!this._runtimeContext?.synchronizer) { - throw new Error('Synchronizer not initialized for globals.'); - } - - try { - const suppressions = this.project?.length ? - await this._runtimeContext.synchronizer.getSuppressions({slug: this.project}, token) : - await this._runtimeContext.synchronizer.getSuppressions(path, token); - - return suppressions; - } catch (err) { - logger.error('getSuppressions', err); - return []; - } + return this._runtimeContext?.synchronizer.getRepositorySuppressions(path, this.project ?? undefined); } - async getSuppressionPermissions(_repoPath: string): Promise { - if (this._runtimeContext.isLocal) { + getProjectPermissions(path: string): SuppressionPermissions { + if (this._runtimeContext.isLocal || !this._runtimeContext.authenticator.user.isAuthenticated) { return 'NONE'; } - if (!this._runtimeContext?.authenticator) { - throw new Error('Authenticator not initialized for globals.'); - } - - if (!this._runtimeContext?.synchronizer) { - throw new Error('Synchronizer not initialized for globals.'); - } - - try { - const user = await this._runtimeContext.authenticator.getUser(); - // @TODO fetch permission based on repoPath (and if it's part of any project) or global project config (this.project) - // this should be cached somehow so we don't request it every time - return 'ADD'; // tmp + const permissions = this._runtimeContext?.synchronizer.getProjectPermissions(path, this.project ?? undefined); - } catch (err) { + if (!permissions) { return 'NONE'; } + + return permissions.repositories.write ? 'ADD' : 'REQUEST'; } getFolderStatus(folder: Folder) { diff --git a/src/utils/policy-puller.ts b/src/utils/policy-puller.ts index c0f682a..64c217d 100644 --- a/src/utils/policy-puller.ts +++ b/src/utils/policy-puller.ts @@ -112,9 +112,7 @@ export class PolicyPuller { } const user = await globals.getUser(); - const policy = globals.project?.length ? - await this._synchronizer.synchronize({slug: globals.project}, user.tokenInfo) : - await this._synchronizer.synchronize(root.uri.fsPath, user.tokenInfo); + const policy = await this._synchronizer.synchronize(user.tokenInfo, root.uri.fsPath, globals.project ?? undefined); return policy; }, { @@ -164,9 +162,9 @@ export class PolicyPuller { private async removeOutdatedPolicy(path: string) { try { - const outdatedPolicy = await this._synchronizer.getPolicy(path); + const outdatedPolicy = this._synchronizer.getProjectPolicy(path); - if (outdatedPolicy.path) { + if (outdatedPolicy?.path) { await rm(outdatedPolicy.path, { force: true }); } } catch (err) { diff --git a/src/utils/suppressions.ts b/src/utils/suppressions.ts deleted file mode 100644 index 5b45e42..0000000 --- a/src/utils/suppressions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FingerprintSuppression } from '@monokle/types'; -import globals from './globals'; - -export async function getSuppressions(path: string) { - const user = await globals.getUser(); - - if (!user.isAuthenticated) { - return { suppressions: [] }; - } - - await globals.forceRefreshToken(); - - const suppressions = await globals.getSuppressions(path, user.tokenInfo); - const fingerprintSuppressions = suppressions.map((suppression) => { - return { - guid: suppression.id, - kind: 'external', - status: toSuppressionStatus(suppression.status), - fingerprint: suppression.fingerprint, - } as FingerprintSuppression; - }); - - return { - suppressions: fingerprintSuppressions, - }; -} - -function toSuppressionStatus(status: string) { - switch (status) { - case 'ACCEPTED': - case 'accepted': - return 'accepted'; - case 'REJECTED': - case 'rejected': - return 'rejected'; - case 'UNDER_REVIEW': - case 'underReview': - return 'underReview'; - default: - return status; - } -} diff --git a/src/utils/synchronization.ts b/src/utils/synchronization.ts index 481d59d..f243cd3 100644 --- a/src/utils/synchronization.ts +++ b/src/utils/synchronization.ts @@ -5,7 +5,7 @@ export type Synchronizer = Awaited>; export async function getSynchronizer(origin?: string) { /* DEV_ONLY_START */ if (process.env.MONOKLE_VSC_ENV === 'TEST') { - const {Synchronizer, StorageHandlerPolicy, ApiHandler, GitHandler} = await import('@monokle/synchronizer'); + const {ProjectSynchronizer, StorageHandlerPolicy, ApiHandler, GitHandler} = await import('@monokle/synchronizer'); const gitHandler = new GitHandler(); (gitHandler as any).getRepoRemoteData = () => { @@ -17,7 +17,7 @@ export async function getSynchronizer(origin?: string) { }; }; - return new Synchronizer( + return new ProjectSynchronizer( new StorageHandlerPolicy(process.env.MONOKLE_TEST_CONFIG_PATH), new ApiHandler(process.env.MONOKLE_TEST_SERVER_URL), gitHandler @@ -25,10 +25,10 @@ export async function getSynchronizer(origin?: string) { } /* DEV_ONLY_END */ - const {createMonokleSynchronizerFromOrigin} = await import('@monokle/synchronizer'); + const {createMonokleProjectSynchronizerFromOrigin} = await import('@monokle/synchronizer'); try { - const synchronizer = await createMonokleSynchronizerFromOrigin(getClientConfig(), origin); + const synchronizer = await createMonokleProjectSynchronizerFromOrigin(getClientConfig(), origin); return synchronizer; } catch (err: any) { // Without this entire extension can run only in local mode. Needs to be obvious to users what went wrong and how to fix. diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 1537627..0fadaf3 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -10,7 +10,7 @@ import { getInvalidConfigError } from './errors'; import { trackEvent } from './telemetry'; import { getResultCache } from './result-cache'; import { getResourcesFromFolder } from './file-parser'; -import { getSuppressions } from './suppressions'; +import { getSuppressions } from '../core'; import logger from '../utils/logger'; import globals from './globals'; import type { Fix, Replacement, } from 'sarif'; @@ -145,7 +145,7 @@ export async function validateResourcesFromFolder(resources: Resource[], root: F }; } - const suppressions = await getSuppressions(root.uri.fsPath); + const suppressions = getSuppressions(root.uri.fsPath); let result: ValidationResponse = null; try { diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts index 3d554e7..9a3d1b9 100644 --- a/src/utils/workspace.ts +++ b/src/utils/workspace.ts @@ -52,8 +52,8 @@ export async function getWorkspaceConfig(workspaceFolder: Folder): Promise Date: Wed, 7 Feb 2024 16:37:27 +0100 Subject: [PATCH 03/19] feat: integrate suppressions API --- src/commands/suppress.ts | 62 +++++++++++++++++++ src/constants.ts | 1 + ...rint-suppressions-code-actions-provider.ts | 26 ++++---- src/core/suppressions/suppressions.ts | 2 +- src/extension.ts | 5 +- src/utils/synchronization.ts | 3 +- src/utils/telemetry.ts | 2 + src/utils/validation.ts | 59 ++++++++++++++++++ 8 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 src/commands/suppress.ts diff --git a/src/commands/suppress.ts b/src/commands/suppress.ts new file mode 100644 index 0000000..11763d4 --- /dev/null +++ b/src/commands/suppress.ts @@ -0,0 +1,62 @@ +import { Uri, commands } from 'vscode'; +import { canRun, disabledForLocal } from '../utils/commands'; +import { COMMANDS, COMMAND_NAMES } from '../constants'; +import { raiseWarning } from '../utils/errors'; +import { trackEvent } from '../utils/telemetry'; +import globals from '../utils/globals'; +import type { RuntimeContext } from '../utils/runtime-context'; +import { MONOKLE_FINGERPRINT_FIELD, ValidationResultExtended } from '../core/code-actions/base-code-actions-provider'; +import { SuppressionPermissions } from '../core'; +import { applySuppressions } from '../utils/validation'; +import { Folder } from '../utils/workspace'; + +export function getSuppressCommand(context: RuntimeContext) { + return async (result: ValidationResultExtended, permissions: SuppressionPermissions, root: Folder) => { + if (!canRun()) { + return null; + } + + if (permissions === 'NONE') { + // Should not happen, since permissions are checked in CodeActionProvider. Still handle gracefully. + raiseWarning('You do not have permissions to suppress this misconfiguration'); + + trackEvent('command/suppress', { + status: 'failure', + error: 'Suppression command run despite no permissions.' + }); + + return null; + } + + trackEvent('command/suppress', { + status: 'started' + }); + + const user = await globals.getUser(); + + if (!user.isAuthenticated) { + raiseWarning(`You need to be logged in to use suppressions. Run ${COMMAND_NAMES.LOGIN} to authenticate first.}`); + + trackEvent('command/suppress', { + status: 'failure', + error: 'User not authenticated.' + }); + + return null; + } + + await context.synchronizer.toggleSuppression( + user.tokenInfo, + result.fingerprints[MONOKLE_FINGERPRINT_FIELD], + result.message.text, + root.uri.fsPath, + globals.project || undefined + ); + + await applySuppressions(root); + + trackEvent('command/suppress', { + status: 'success' + }); + }; +} diff --git a/src/constants.ts b/src/constants.ts index 73176e7..0915688 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,6 +40,7 @@ export const COMMANDS = { TRACK: 'monokle.track', RAISE_AUTHENTICATION_ERROR: 'monokle.raiseAuthenticationError', RUN_COMMANDS: 'monokle.runCommands', + SUPPRESS: 'monokle.suppress', }; export const COMMAND_NAMES = { diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts index a61fbd6..184f246 100644 --- a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -1,7 +1,7 @@ -import { CodeAction, CodeActionKind, TextDocument, Range, languages } from 'vscode'; +import { CodeAction, CodeActionKind, TextDocument, Range, languages, Uri } from 'vscode'; import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, ValidationResultExtended } from './base-code-actions-provider'; import { COMMANDS } from '../../constants'; -import { getOwnerWorkspace } from '../../utils/workspace'; +import { Folder, getOwnerWorkspace } from '../../utils/workspace'; import globals from '../../utils/globals'; import { SuppressionPermissions, shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; @@ -15,7 +15,7 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider } return this.getMonokleDiagnostics(context).map((diagnostic: DiagnosticExtended) => { - return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions); + return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions, workspaceRoot); }); } @@ -38,16 +38,10 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider }] }; } else { - // suppress + track - // what about removal, removal option should be shown in SARIF panel codeAction.command = { - command: COMMANDS.TRACK, - title: 'Track', - arguments: ['code_action/fingerprint_suppression', { - status: 'success', - action: codeAction.permissions === 'ADD' ? 'add' : 'request', - ruleId: codeAction.result.ruleId - }] + command: COMMANDS.SUPPRESS, + title: 'Suppress misconfiguration', + arguments: [codeAction.result, codeAction.permissions, codeAction.root] }; } @@ -58,13 +52,15 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider class FingerprintSuppressionsCodeAction extends CodeAction { private readonly _result: ValidationResultExtended; private readonly _permissions: SuppressionPermissions; + private readonly _root: Folder; - constructor(diagnostic: DiagnosticExtended, permissions: SuppressionPermissions) { + constructor(diagnostic: DiagnosticExtended, permissions: SuppressionPermissions, root: Folder) { super(`${permissions === 'ADD' ? 'Suppress' : 'Request suppression of'} this "${diagnostic.result._rule.name} (${diagnostic.result._rule.id})" problem`, CodeActionKind.QuickFix); this.diagnostics = [diagnostic]; this._result = diagnostic.result; this._permissions = permissions; + this._root = root; } get result() { @@ -74,6 +70,10 @@ class FingerprintSuppressionsCodeAction extends CodeAction { get permissions() { return this._permissions; } + + get root() { + return this._root; + } } export function registerFingerprintSuppressionsCodeActionsProvider() { diff --git a/src/core/suppressions/suppressions.ts b/src/core/suppressions/suppressions.ts index 96a7e18..98bc842 100644 --- a/src/core/suppressions/suppressions.ts +++ b/src/core/suppressions/suppressions.ts @@ -8,7 +8,7 @@ export type SuppressionsStatus = { }; export function getSuppressions(path: string) { - const fingerprintSuppressions = globals.getSuppressions(path).map((suppression) => { + const fingerprintSuppressions: FingerprintSuppression[] = globals.getSuppressions(path).map((suppression) => { return { guid: suppression.id, kind: 'external', diff --git a/src/extension.ts b/src/extension.ts index d945870..8c40f44 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,7 @@ import globals from './utils/globals'; import { raiseError } from './utils/errors'; import { registerAnnotationSuppressionsCodeActionsProvider, registerFingerprintSuppressionsCodeActionsProvider, registerFixCodeActionsProvider, registerShowDetailsCodeActionsProvider } from './core'; import type { ExtensionContext } from 'vscode'; +import { getSuppressCommand } from './commands/suppress'; let runtimeContext: RuntimeContext; @@ -107,6 +108,7 @@ async function runActivation(context: ExtensionContext) { const commandTrack = commands.registerCommand(COMMANDS.TRACK, getTrackCommand(runtimeContext)); const commandRaiseAuthenticationError = commands.registerCommand(COMMANDS.RAISE_AUTHENTICATION_ERROR, getRaiseAuthenticationErrorCommand(runtimeContext)); const commandRunCommands = commands.registerCommand(COMMANDS.RUN_COMMANDS, getRunCommandsCommand(runtimeContext)); + const commandSuppress = commands.registerCommand(COMMANDS.SUPPRESS, getSuppressCommand(runtimeContext)); context.subscriptions.push( commandLogin, @@ -119,7 +121,8 @@ async function runActivation(context: ExtensionContext) { commandDownloadPolicy, commandTrack, commandRaiseAuthenticationError, - commandRunCommands + commandRunCommands, + commandSuppress, ); context.subscriptions.push(registerAnnotationSuppressionsCodeActionsProvider()); diff --git a/src/utils/synchronization.ts b/src/utils/synchronization.ts index f243cd3..247ea47 100644 --- a/src/utils/synchronization.ts +++ b/src/utils/synchronization.ts @@ -5,7 +5,7 @@ export type Synchronizer = Awaited>; export async function getSynchronizer(origin?: string) { /* DEV_ONLY_START */ if (process.env.MONOKLE_VSC_ENV === 'TEST') { - const {ProjectSynchronizer, StorageHandlerPolicy, ApiHandler, GitHandler} = await import('@monokle/synchronizer'); + const {ProjectSynchronizer, StorageHandlerPolicy, StorageHandlerJsonCache, ApiHandler, GitHandler} = await import('@monokle/synchronizer'); const gitHandler = new GitHandler(); (gitHandler as any).getRepoRemoteData = () => { @@ -19,6 +19,7 @@ export async function getSynchronizer(origin?: string) { return new ProjectSynchronizer( new StorageHandlerPolicy(process.env.MONOKLE_TEST_CONFIG_PATH), + new StorageHandlerJsonCache(process.env.MONOKLE_TEST_CONFIG_PATH), new ApiHandler(process.env.MONOKLE_TEST_SERVER_URL), gitHandler ); diff --git a/src/utils/telemetry.ts b/src/utils/telemetry.ts index 6093c46..b144e2d 100644 --- a/src/utils/telemetry.ts +++ b/src/utils/telemetry.ts @@ -106,6 +106,8 @@ export type EventMap = { 'code_action/annotation_suppression': BaseEvent & {[key: string]: string | number | boolean}; 'code_action/fix': BaseEvent & {[key: string]: string | number | boolean}; 'code_action/show_details': BaseEvent; + // Fingerprint-based suppressions. + 'command/suppress': BaseEvent & {[key: string]: string | number | boolean}; }; function getAppVersion(): string { diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 0fadaf3..cd24f54 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -205,6 +205,65 @@ export async function validateResourcesFromFolder(resources: Resource[], root: F return Uri.file(resultFilePath); } +export async function applySuppressions(root: Folder) { + const resources = await getResourcesFromFolder(root.uri.fsPath); + + if (!resources.length) { + return null; + } + + const workspaceConfig = await getWorkspaceConfig(root); + + if (workspaceConfig.isValid === false) { + return null; + } + + const validatorObj = await getValidator(root.id, workspaceConfig.config); + const response = await getValidationResult(root.id); + const suppressions = getSuppressions(root.uri.fsPath); + + let result: ValidationResponse = null; + try { + const resourcesRelative = resources.map(resource => { + return { + ...resource, + filePath: relative(root.uri.fsPath, resource.filePath), + }; + }); + + result = await validatorObj.validator.applySuppressions(response, resourcesRelative, suppressions.suppressions); + } catch(err: any) { + logger.error('Applying suppressions failed', err); + } + + if (!result) { + return null; + } + + result.runs.forEach(run => { + run.results.forEach((result: any) => { + const location = result.locations.find(location => location.physicalLocation?.artifactLocation?.uriBaseId === 'SRCROOT'); + + if (location && location.physicalLocation.artifactLocation.uri) { + location.physicalLocation.artifactLocation.uri = normalizePathForWindows(location.physicalLocation.artifactLocation.uri); + } + }); + }); + + // This causes SARIF panel to reload so we want to write new results only when they are different. + const resultUnchanged = await areValidationResultsSame(RESULTS.get(root.id), result); + if (!resultUnchanged) { + await saveValidationResults(result, root.id); + } + + RESULTS.set(root.id, result); + globals.setFolderStatus(root); + + const resultFilePath = await getValidationResultPath(root.id); + + return Uri.file(resultFilePath); +} + export async function getValidationResult(fileName: string) { const filePath = getValidationResultPath(fileName); From 5eb9897d75f61653c630f9ef8525b8c00a5b88e6 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 8 Feb 2024 14:40:29 +0100 Subject: [PATCH 04/19] fix: do not show code action for under review suppressed violations --- .../fingerprint-suppressions-code-actions-provider.ts | 10 ++++++---- src/core/suppressions/suppressions.ts | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts index 184f246..9cbb4da 100644 --- a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -3,7 +3,7 @@ import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, import { COMMANDS } from '../../constants'; import { Folder, getOwnerWorkspace } from '../../utils/workspace'; import globals from '../../utils/globals'; -import { SuppressionPermissions, shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; +import { SuppressionPermissions, isUnderReview, shouldUseFingerprintSuppressions } from '../suppressions/suppressions'; class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider { public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) { @@ -14,9 +14,11 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider return []; } - return this.getMonokleDiagnostics(context).map((diagnostic: DiagnosticExtended) => { - return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions, workspaceRoot); - }); + return this.getMonokleDiagnostics(context) + .filter((diagnostic: DiagnosticExtended) => !isUnderReview(diagnostic.result)) + .map((diagnostic: DiagnosticExtended) => { + return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions, workspaceRoot); + }); } public async resolveCodeAction(codeAction: FingerprintSuppressionsCodeAction) { diff --git a/src/core/suppressions/suppressions.ts b/src/core/suppressions/suppressions.ts index 98bc842..f11e7f5 100644 --- a/src/core/suppressions/suppressions.ts +++ b/src/core/suppressions/suppressions.ts @@ -1,5 +1,7 @@ import { FingerprintSuppression } from '@monokle/types'; import globals from '../../utils/globals'; +import { ValidationResult } from '../../utils/validation'; +import { ValidationResultExtended } from '../code-actions/base-code-actions-provider'; export type SuppressionPermissions = 'ADD' | 'REQUEST' | 'NONE'; export type SuppressionsStatus = { @@ -31,6 +33,10 @@ export function shouldUseFingerprintSuppressions(repoRootPath: string): Suppress }; } +export function isUnderReview(result: ValidationResult | ValidationResultExtended) { + return result.suppressions.length > 0 && result.suppressions.every(s => s.status === 'underReview'); +} + function toSuppressionStatus(status: string) { switch (status) { case 'ACCEPTED': From c4c286e5cc224b007563652821cce36cdd919e5f Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 8 Feb 2024 14:59:09 +0100 Subject: [PATCH 05/19] chore: bump synchronizer package --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fecb96..2d4b7aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.9.1", "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.13.0", + "@monokle/synchronizer": "^0.14.0", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0", @@ -841,9 +841,9 @@ } }, "node_modules/@monokle/synchronizer": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.13.0.tgz", - "integrity": "sha512-rTfzoDC4A3mLkxYnpUeYHzLK2cIjugBarxJ3+Dyq8MgSbXcPmW5GaxbqTJRZX2KEPlj2ozJHRmXAsoxcOpwQVA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.14.0.tgz", + "integrity": "sha512-5JL1U891DiwC1ISwFQTEk0CRWb2dnAy6BYGJoYlXpD8j+mgu/jJB6OAzp/D0pEnqdrqrejbGzxZWQ0nFIFie2Q==", "dependencies": { "@monokle/types": "*", "env-paths": "^2.2.1", diff --git a/package.json b/package.json index da645b0..8534823 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ }, "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.13.0", + "@monokle/synchronizer": "^0.14.0", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0", From 4144afa2e3331ae45737adeb9ed75d015938961e Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 8 Feb 2024 16:02:28 +0100 Subject: [PATCH 06/19] chore: fix sync logic and tests --- src/commands/synchronize.ts | 2 +- src/test/helpers/server.ts | 63 +++++++++++++++++++++++++++++++++++- src/test/run-test.ts | 3 -- src/utils/policy-puller.ts | 10 ++++-- src/utils/runtime-context.ts | 4 +-- 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/commands/synchronize.ts b/src/commands/synchronize.ts index 746b3fa..8076d12 100644 --- a/src/commands/synchronize.ts +++ b/src/commands/synchronize.ts @@ -29,7 +29,7 @@ export function getSynchronizeCommand(context: RuntimeContext) { return null; } - await context.refreshPolicyPuller(); + await context.refreshPolicyPuller(true); trackEvent('command/synchronize', { status: 'success' diff --git a/src/test/helpers/server.ts b/src/test/helpers/server.ts index 2bbc69f..8c3fde1 100644 --- a/src/test/helpers/server.ts +++ b/src/test/helpers/server.ts @@ -7,6 +7,7 @@ const schema = ` type Query { me: UserModel! getProject(input: GetProjectInput!): ProjectModel! + getSuppressions(input: GetSuppressionsInput!): [SuppressionsModel!]! } type UserModel { @@ -24,6 +25,8 @@ const schema = ` slug: String! name: String! repositories: [ProjectRepositoryModel!]! + repository(input: GetRepositoryInput!): ProjectRepositoryModel + permissions: ProjectPermissionsModel policy: ProjectPolicyModel } @@ -37,6 +40,46 @@ const schema = ` canEnablePrChecks: Boolean } + type ProjectPermissionsModel { + project: PermissionsModel1! + members: PermissionsModel1! + repositories: PermissionsModel2 + } + + type SuppressionsModel { + isSnapshot: Boolean!, + data: [SuppressionModel!]! + } + + type SuppressionModel { + id: String! + fingerprint: String! + description: String! + location: String! + status: String! + justification: String! + expiresAt: String! + updatedAt: String! + createdAt: String! + isUnderReview: Boolean! + isAccepted: Boolean! + isRejected: Boolean! + isExpired: Boolean! + isDeleted: Boolean! + repositoryId: String! + } + + type PermissionsModel1 { + view: Boolean! + update: Boolean! + delete: Boolean! + } + + type PermissionsModel2 { + read: Boolean! + write: Boolean! + } + enum RepositoryProviderEnum { BITBUCKET GITHUB @@ -45,15 +88,31 @@ const schema = ` scalar JSON + scalar DateISO + + scalar ID + type ProjectPolicyModel { id: String! json: JSON! + updatedAt: DateISO } input GetProjectInput { id: Int slug: String } + + input GetRepositoryInput { + owner: String! + name: String! + provider: String! + } + + input GetSuppressionsInput { + repositoryId: ID!, + from: String + } `; export const DEFAULT_POLICY = { @@ -68,7 +127,9 @@ const mockData = { owner: 'kubeshop', name: 'monokle-demo', }), - JSON: () => (DEFAULT_POLICY) + JSON: () => (DEFAULT_POLICY), + DateISO: () => (new Date()).toISOString(), + ID: () => 100 }; export function startMockServer(host = '0.0.0.0', port = 5000): Promise { diff --git a/src/test/run-test.ts b/src/test/run-test.ts index 46e7eea..084a87a 100644 --- a/src/test/run-test.ts +++ b/src/test/run-test.ts @@ -160,9 +160,6 @@ async function main() { // Run integration-like tests on multiple, different workspaces (local config). await runSuite('./suite-integration/index', [workspaces.withResources, workspaces.withoutResources, workspaces.workspace], { skipResultCache: true }); await runSuite('./suite-integration/index', [workspaces.withResources, workspaces.withoutResources, workspaces.workspace], { skipResultCache: true, validateOnSave: true }); - - // Run integration-like tests for remote config separately as it needs different setup. - await runSuite('./suite-integration/index', [workspaces.withConfig], { setupRemoteEnv: true }); } main(); diff --git a/src/utils/policy-puller.ts b/src/utils/policy-puller.ts index 64c217d..3500d21 100644 --- a/src/utils/policy-puller.ts +++ b/src/utils/policy-puller.ts @@ -11,6 +11,7 @@ const REFETCH_POLICY_INTERVAL_MS = 1000 * 30; export class PolicyPuller { + private _force = false; private _isPulling = false; private _pullPromise: Promise | undefined; private _policyFetcherId: NodeJS.Timer | undefined; @@ -19,7 +20,9 @@ export class PolicyPuller { private _synchronizer: Synchronizer ) {} - async refresh() { + async refresh(force = false) { + this._force = force; + const user = await globals.getUser(); if (!user.isAuthenticated) { @@ -101,6 +104,7 @@ export class PolicyPuller { } } + this._force = false; this._isPulling = false; this._pullPromise = undefined; } @@ -112,7 +116,9 @@ export class PolicyPuller { } const user = await globals.getUser(); - const policy = await this._synchronizer.synchronize(user.tokenInfo, root.uri.fsPath, globals.project ?? undefined); + const policy = this._force ? + await this._synchronizer.forceSynchronize(user.tokenInfo, root.uri.fsPath, globals.project ?? undefined) : + await this._synchronizer.synchronize(user.tokenInfo, root.uri.fsPath, globals.project ?? undefined); return policy; }, { diff --git a/src/utils/runtime-context.ts b/src/utils/runtime-context.ts index e1a5e05..d3635c9 100644 --- a/src/utils/runtime-context.ts +++ b/src/utils/runtime-context.ts @@ -60,12 +60,12 @@ export class RuntimeContext { } } - async refreshPolicyPuller() { + async refreshPolicyPuller(force = false) { if (!this.policyPuller) { return; } - await this.policyPuller.refresh(); + await this.policyPuller.refresh(force); } async reconfigure( From 9e8e09be810ec41ffbe5cb9489075abd2b3355b8 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 8 Feb 2024 16:25:32 +0100 Subject: [PATCH 07/19] test: add more code actions tests --- src/test/helpers/server.ts | 44 ++++++++--- src/test/run-test.ts | 1 + src/test/suite-code-actions/fix.test.ts | 97 +++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/src/test/helpers/server.ts b/src/test/helpers/server.ts index 8c3fde1..45113e7 100644 --- a/src/test/helpers/server.ts +++ b/src/test/helpers/server.ts @@ -4,6 +4,12 @@ import { mockServer } from '@graphql-tools/mock'; import { Server } from 'http'; const schema = ` + scalar JSON + scalar DateISO + scalar ID + scalar TRUE + scalar FALSE + type Query { me: UserModel! getProject(input: GetProjectInput!): ProjectModel! @@ -76,8 +82,8 @@ const schema = ` } type PermissionsModel2 { - read: Boolean! - write: Boolean! + read: TRUE! + write: FALSE! } enum RepositoryProviderEnum { @@ -86,12 +92,6 @@ const schema = ` GITLAB } - scalar JSON - - scalar DateISO - - scalar ID - type ProjectPolicyModel { id: String! json: JSON! @@ -117,7 +117,29 @@ const schema = ` export const DEFAULT_POLICY = { plugins: { - 'open-policy-agent': true + 'pod-security-standards': false, + 'yaml-syntax': false, + 'resource-links': false, + 'kubernetes-schema': false, + 'practices': true, + }, + rules: { + 'practices/no-mounted-docker-sock': false, + 'practices/no-writable-fs': false, + 'practices/drop-capabilities': false, + 'practices/no-low-group-id': false, + 'practices/no-automount-service-account-token': false, + 'practices/no-pod-create': false, + 'practices/no-pod-execute': false, + 'practices/no-no-root-group': 'err', + 'practices/no-sys-admin': false, + 'practices/cpu-limit': false, + 'practices/no-latest-image': false, + 'practices/cpu-request': false, + 'practices/memory-request': false, + 'practices/memory-limit': false, + 'practices/no-low-user-id': 'err', + 'practices/no-root-group': false, } }; @@ -129,7 +151,9 @@ const mockData = { }), JSON: () => (DEFAULT_POLICY), DateISO: () => (new Date()).toISOString(), - ID: () => 100 + ID: () => 100, + TRUE: () => true, + FALSE: () => false }; export function startMockServer(host = '0.0.0.0', port = 5000): Promise { diff --git a/src/test/run-test.ts b/src/test/run-test.ts index 084a87a..5e96c7d 100644 --- a/src/test/run-test.ts +++ b/src/test/run-test.ts @@ -156,6 +156,7 @@ async function main() { // Run code-actions tests await runSuite('./suite-code-actions/index', [workspaces.withCodeActions], { skipResultCache: true }); + await runSuite('./suite-code-actions/index', [workspaces.withCodeActions], { skipResultCache: true, setupRemoteEnv: true }); // Run integration-like tests on multiple, different workspaces (local config). await runSuite('./suite-integration/index', [workspaces.withResources, workspaces.withoutResources, workspaces.workspace], { skipResultCache: true }); diff --git a/src/test/suite-code-actions/fix.test.ts b/src/test/suite-code-actions/fix.test.ts index a3d9a46..367be93 100644 --- a/src/test/suite-code-actions/fix.test.ts +++ b/src/test/suite-code-actions/fix.test.ts @@ -9,6 +9,7 @@ import { Fix } from 'sarif'; import { removeValidationResult } from '../../utils/validation'; const RUN_ON = process.env.MONOKLE_TEST_VALIDATE_ON_SAVE === 'Y' ? 'onSave' : 'onType'; +const IS_REMOTE = process.env.MONOKLE_TEST_SERVER_URL?.length > 0; function getMisconfigurations(validationResponse) { return validationResponse.runs[0].results ?? []; @@ -26,13 +27,10 @@ async function getRange(validationResponse): Promise { return new Range(startLine - 1, startColumn, endLine - 1, endColumn); } - - suite(`CodeActions - quick fix (${RUN_ON}): ${process.env.ROOT_PATH}`, async function () { this.timeout(25000); const isDisabled = process.env.WORKSPACE_DISABLED === 'true'; - suiteSetup(async function () { await doSuiteSetup(); @@ -45,7 +43,6 @@ suite(`CodeActions - quick fix (${RUN_ON}): ${process.env.ROOT_PATH}`, async fun await doSuiteTeardown(); }); - test('Fix is available in the CodeAction quickfix list', async function () { const folders = getWorkspaceFolders(); @@ -63,7 +60,97 @@ suite(`CodeActions - quick fix (${RUN_ON}): ${process.env.ROOT_PATH}`, async fun const codeActions = await waitForCodeActionList(uri, range, 2, 5000); if (!codeActions.find(({ kind, title }) => { - return kind.value === CodeActionKind.QuickFix.value && title.includes('KBP104'); + return kind.value === CodeActionKind.QuickFix.value && title.includes('KBP104') && title.startsWith('Fix'); + })) { + fail('Quick fix missing'); + } + + } catch (error) { + fail(error.message); + } + }); + }); + + test('Show details is available in the CodeAction quickfix list', async function () { + const folders = getWorkspaceFolders(); + + await runForFolders(folders, async (folder) => { + try { + const file = resolve(folder.uri.fsPath, 'deployment.yaml'); + const uri = Uri.file(file); + + const validationResponse = await waitForValidationResults(folder); + assertValidationResults(validationResponse); + + const range = await getRange(validationResponse); + const document = await workspace.openTextDocument(uri); + await vscodeWindow.showTextDocument(document); + const codeActions = await waitForCodeActionList(uri, range, 3, 5000); + + if (!codeActions.find(({ kind, title }) => { + return kind.value === CodeActionKind.QuickFix.value && title.startsWith('Show details'); + })) { + fail('Quick fix missing'); + } + + } catch (error) { + fail(error.message); + } + }); + }); + + test('Suppress (annotation-based) is available in the CodeAction quickfix list', async function () { + if (IS_REMOTE) { + this.skip(); + } + const folders = getWorkspaceFolders(); + + await runForFolders(folders, async (folder) => { + try { + const file = resolve(folder.uri.fsPath, 'deployment.yaml'); + const uri = Uri.file(file); + + const validationResponse = await waitForValidationResults(folder); + assertValidationResults(validationResponse); + + const range = await getRange(validationResponse); + const document = await workspace.openTextDocument(uri); + await vscodeWindow.showTextDocument(document); + const codeActions = await waitForCodeActionList(uri, range, 3, 5000); + + if (!codeActions.find(({ kind, title }) => { + return kind.value === CodeActionKind.QuickFix.value && title.includes('for this resource') && title.startsWith('Suppress'); + })) { + fail('Quick fix missing'); + } + + } catch (error) { + fail(error.message); + } + }); + }); + + test('Suppress (fingerprint-based) is available in the CodeAction quickfix list', async function () { + if (!IS_REMOTE) { + this.skip(); + } + const folders = getWorkspaceFolders(); + + await runForFolders(folders, async (folder) => { + try { + const file = resolve(folder.uri.fsPath, 'deployment.yaml'); + const uri = Uri.file(file); + + const validationResponse = await waitForValidationResults(folder); + assertValidationResults(validationResponse); + + const range = await getRange(validationResponse); + const document = await workspace.openTextDocument(uri); + await vscodeWindow.showTextDocument(document); + const codeActions = await waitForCodeActionList(uri, range, 3, 5000); + + if (!codeActions.find(({ kind, title }) => { + return kind.value === CodeActionKind.QuickFix.value && title.startsWith('Request suppression of'); })) { fail('Quick fix missing'); } From ea62fa639721a0d650195fee2c7ca6c450a66bd0 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 8 Feb 2024 16:29:54 +0100 Subject: [PATCH 08/19] test: increase cc threshold --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8534823..0a0814f 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "test": "concurrently -s command-1 -k \"npm run test:server\" \"node ./out/test/run-test.js\"", "test:cc": "concurrently -s command-1 -k \"npm run test:server\" \"c8 node ./out/test/run-test.js\"", "test:server": "node ./out/test/run-server.js", - "test:ensure-coverage": "npx c8 check-coverage --lines 68 --functions 75 --branches 75 --statements 68" + "test:ensure-coverage": "npx c8 check-coverage --lines 68 --functions 76 --branches 82 --statements 68" }, "devDependencies": { "@graphql-tools/mock": "^9.0.0", From 65b18887aea4e01fed09dd232d736ca08e2dd46f Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 09:34:20 +0100 Subject: [PATCH 09/19] refactor: store parsed resources on validation to improve suppressions applying --- src/utils/validation.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/utils/validation.ts b/src/utils/validation.ts index cd24f54..7e4e031 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -52,6 +52,9 @@ const VALIDATORS = new Map(); +// Store parsed resources. +const RESOURCES = new Map(); + export async function getValidator(validatorId: string, config?: any) { const validatorItem = VALIDATORS.get(validatorId); const validatorObj = validatorItem?.validator ?? await getValidatorInstance(); @@ -131,6 +134,8 @@ export async function validateResourcesFromFolder(resources: Resource[], root: F }; }); + RESOURCES.set(root.id, resourcesRelative); + logger.log(root.name, 'workspaceConfig', workspaceConfig); const validatorObj = await getValidator(root.id, workspaceConfig.config); @@ -206,7 +211,17 @@ export async function validateResourcesFromFolder(resources: Resource[], root: F } export async function applySuppressions(root: Folder) { - const resources = await getResourcesFromFolder(root.uri.fsPath); + let resources: Resource[] = RESOURCES.get(root.id); + + if (!resources || resources.length === 0) { + resources = await getResourcesFromFolder(root.uri.fsPath); + resources = resources.map(resource => { + return { + ...resource, + filePath: relative(root.uri.fsPath, resource.filePath), + }; + }); + } if (!resources.length) { return null; @@ -224,14 +239,7 @@ export async function applySuppressions(root: Folder) { let result: ValidationResponse = null; try { - const resourcesRelative = resources.map(resource => { - return { - ...resource, - filePath: relative(root.uri.fsPath, resource.filePath), - }; - }); - - result = await validatorObj.validator.applySuppressions(response, resourcesRelative, suppressions.suppressions); + result = await validatorObj.validator.applySuppressions(response, resources, suppressions.suppressions); } catch(err: any) { logger.error('Applying suppressions failed', err); } From 7641768230e5d66e6b3189dd6da9ef74f7bddda9 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 11:03:08 +0100 Subject: [PATCH 10/19] fix: pass correct metadata when toggling suppression --- src/commands/suppress.ts | 5 +++-- src/core/code-actions/base-code-actions-provider.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/suppress.ts b/src/commands/suppress.ts index 11763d4..2ad0074 100644 --- a/src/commands/suppress.ts +++ b/src/commands/suppress.ts @@ -47,8 +47,9 @@ export function getSuppressCommand(context: RuntimeContext) { await context.synchronizer.toggleSuppression( user.tokenInfo, - result.fingerprints[MONOKLE_FINGERPRINT_FIELD], - result.message.text, + result.fingerprints?.[MONOKLE_FINGERPRINT_FIELD], + `${result.ruleId} - ${result.message.text}`, + result.locations.at(1).logicalLocations?.at(0)?.fullyQualifiedName, root.uri.fsPath, globals.project || undefined ); diff --git a/src/core/code-actions/base-code-actions-provider.ts b/src/core/code-actions/base-code-actions-provider.ts index 62ff17c..13618ef 100644 --- a/src/core/code-actions/base-code-actions-provider.ts +++ b/src/core/code-actions/base-code-actions-provider.ts @@ -6,6 +6,7 @@ import { ValidationResult, ValidationRule } from './../../utils/validation'; export type ValidationResultExtended = ValidationResult & { _id: [string, number, number]; _rule: ValidationRule; + _uri: string; }; export type DiagnosticExtended = Diagnostic & { From d6140e48a37956b9dc9553d16aaa0dec71dbc119 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 11:07:28 +0100 Subject: [PATCH 11/19] fix: allow 'Admin' to suppress 'under review' violations --- ...fingerprint-suppressions-code-actions-provider.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts index 9cbb4da..784ee21 100644 --- a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -14,9 +14,15 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider return []; } - return this.getMonokleDiagnostics(context) - .filter((diagnostic: DiagnosticExtended) => !isUnderReview(diagnostic.result)) - .map((diagnostic: DiagnosticExtended) => { + const diagnostics = this.getMonokleDiagnostics(context); + + // Filter out 'under review' violations if user has only 'request suppression' rights. There is no sense in requesting again. + // However, for 'Admin' there should be still an ability to suppress such violation (so it's like accepting a request). + const roleRelevantDiagnostics = fingerprintSuppressionsPermissions.permissions === 'ADD' ? + diagnostics : + diagnostics.filter((diagnostic: DiagnosticExtended) => !isUnderReview(diagnostic.result)); + + return roleRelevantDiagnostics.map((diagnostic: DiagnosticExtended) => { return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions, workspaceRoot); }); } From dd223b634d15f9270809e85a0821fd121974a24e Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 11:15:44 +0100 Subject: [PATCH 12/19] chore: bump synchronizer package --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d4b7aa..f8a657f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.9.1", "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.14.0", + "@monokle/synchronizer": "^0.14.1", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0", @@ -841,9 +841,9 @@ } }, "node_modules/@monokle/synchronizer": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.14.0.tgz", - "integrity": "sha512-5JL1U891DiwC1ISwFQTEk0CRWb2dnAy6BYGJoYlXpD8j+mgu/jJB6OAzp/D0pEnqdrqrejbGzxZWQ0nFIFie2Q==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.14.1.tgz", + "integrity": "sha512-d2gE2u32PiyltEvri6A6xU9tciTq0PAYYFHP41od45iFZ00wbWRzcVfno6EAOpSQj1XhaSPwSl00e2wZfAPo7A==", "dependencies": { "@monokle/types": "*", "env-paths": "^2.2.1", diff --git a/package.json b/package.json index 0a0814f..31a2db6 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ }, "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.14.0", + "@monokle/synchronizer": "^0.14.1", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0", From ff2f46e40d474f3576a52ceb9770d445c9b443a7 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 11:40:11 +0100 Subject: [PATCH 13/19] test: adjust config sync test --- src/test/suite-policies/policies.test.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/test/suite-policies/policies.test.ts b/src/test/suite-policies/policies.test.ts index 0be904c..871e230 100644 --- a/src/test/suite-policies/policies.test.ts +++ b/src/test/suite-policies/policies.test.ts @@ -1,4 +1,4 @@ -import { ok, deepEqual } from 'assert'; +import { ok, deepEqual, equal } from 'assert'; import { workspace, commands, ConfigurationTarget } from 'vscode'; import { join } from 'path'; import { existsSync } from 'fs'; @@ -36,12 +36,13 @@ suite(`Policies - Remote: ${process.env.ROOT_PATH}`, function () { await workspace.getConfiguration('monokle').update('enabled', true, ConfigurationTarget.Workspace); - const configRemote = await waitForValidationConfig(folder, 10000); + const configRemote = await waitForValidationConfig(folder, 15000, 'remote'); ok(configRemote); ok(configRemote.isValid); + equal(configRemote.type, 'remote'); deepEqual(configRemote.config, DEFAULT_POLICY); - }).timeout(15000); + }).timeout(25000); test('Refetches policy from remote API when authenticated and synchronize command run', async function () { const folders = getWorkspaceFolders(); @@ -69,7 +70,7 @@ suite(`Policies - Remote: ${process.env.ROOT_PATH}`, function () { }).timeout(35000); }); -async function waitForValidationConfig(workspaceFolder: Folder, timeoutMs?: number): Promise { +async function waitForValidationConfig(workspaceFolder: Folder, timeoutMs?: number, requiredType?: string): Promise { return new Promise((res) => { let timeoutId = null; let result = null; @@ -78,9 +79,11 @@ async function waitForValidationConfig(workspaceFolder: Folder, timeoutMs?: numb result = await getWorkspaceConfig(workspaceFolder); if (result && result.isValid) { - clearInterval(intervalId); - timeoutId && clearTimeout(timeoutId); - res(result); + if (requiredType && result.type === requiredType || requiredType === undefined) { + clearInterval(intervalId); + timeoutId && clearTimeout(timeoutId); + res(result); + } } }, 250); From dfcb8f14a023407705377dc576108b6330c88589 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 12:34:35 +0100 Subject: [PATCH 14/19] fix: apply suppressions optimistically --- src/commands/suppress.ts | 18 +++++++++++------- src/core/suppressions/suppressions.ts | 9 +++++++++ src/utils/validation.ts | 7 ++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/commands/suppress.ts b/src/commands/suppress.ts index 2ad0074..e159e03 100644 --- a/src/commands/suppress.ts +++ b/src/commands/suppress.ts @@ -1,14 +1,13 @@ -import { Uri, commands } from 'vscode'; -import { canRun, disabledForLocal } from '../utils/commands'; -import { COMMANDS, COMMAND_NAMES } from '../constants'; +import { canRun } from '../utils/commands'; +import { COMMAND_NAMES } from '../constants'; import { raiseWarning } from '../utils/errors'; import { trackEvent } from '../utils/telemetry'; -import globals from '../utils/globals'; -import type { RuntimeContext } from '../utils/runtime-context'; import { MONOKLE_FINGERPRINT_FIELD, ValidationResultExtended } from '../core/code-actions/base-code-actions-provider'; -import { SuppressionPermissions } from '../core'; +import { SuppressionPermissions, generateSuppression } from '../core'; import { applySuppressions } from '../utils/validation'; import { Folder } from '../utils/workspace'; +import globals from '../utils/globals'; +import type { RuntimeContext } from '../utils/runtime-context'; export function getSuppressCommand(context: RuntimeContext) { return async (result: ValidationResultExtended, permissions: SuppressionPermissions, root: Folder) => { @@ -45,6 +44,11 @@ export function getSuppressCommand(context: RuntimeContext) { return null; } + + const localSuppression = generateSuppression(result.fingerprints?.[MONOKLE_FINGERPRINT_FIELD], permissions); + + await applySuppressions(root, localSuppression); + await context.synchronizer.toggleSuppression( user.tokenInfo, result.fingerprints?.[MONOKLE_FINGERPRINT_FIELD], @@ -54,7 +58,7 @@ export function getSuppressCommand(context: RuntimeContext) { globals.project || undefined ); - await applySuppressions(root); + await applySuppressions(root, localSuppression); trackEvent('command/suppress', { status: 'success' diff --git a/src/core/suppressions/suppressions.ts b/src/core/suppressions/suppressions.ts index f11e7f5..b5a2955 100644 --- a/src/core/suppressions/suppressions.ts +++ b/src/core/suppressions/suppressions.ts @@ -24,6 +24,15 @@ export function getSuppressions(path: string) { }; } +export function generateSuppression(fingerprint: string, permissions: SuppressionPermissions) { + return { + guid: `sup-${Date.now()}`, + kind: 'external', + status: toSuppressionStatus(permissions === 'ADD' ? 'accepted' : 'underReview'), + fingerprint: fingerprint, + } as FingerprintSuppression; +} + export function shouldUseFingerprintSuppressions(repoRootPath: string): SuppressionsStatus { const projectPermissions = globals.getProjectPermissions(repoRootPath); diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 7e4e031..c0c0404 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -14,6 +14,7 @@ import { getSuppressions } from '../core'; import logger from '../utils/logger'; import globals from './globals'; import type { Fix, Replacement, } from 'sarif'; +import type { FingerprintSuppression } from '@monokle/types'; import type { Folder } from './workspace'; import type { Resource } from './file-parser'; @@ -210,7 +211,7 @@ export async function validateResourcesFromFolder(resources: Resource[], root: F return Uri.file(resultFilePath); } -export async function applySuppressions(root: Folder) { +export async function applySuppressions(root: Folder, localSuppression?: FingerprintSuppression) { let resources: Resource[] = RESOURCES.get(root.id); if (!resources || resources.length === 0) { @@ -237,6 +238,10 @@ export async function applySuppressions(root: Folder) { const response = await getValidationResult(root.id); const suppressions = getSuppressions(root.uri.fsPath); + if (localSuppression && !suppressions.suppressions.find(sup => sup.fingerprint === localSuppression.fingerprint)) { + suppressions.suppressions.push(localSuppression); + } + let result: ValidationResponse = null; try { result = await validatorObj.validator.applySuppressions(response, resources, suppressions.suppressions); From 71851f56ecba6e8fd71207e9540bb8ecc62ec30a Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 9 Feb 2024 12:50:45 +0100 Subject: [PATCH 15/19] fix: improve suppression code actions apperance --- src/core/code-actions/base-code-actions-provider.ts | 4 ++-- .../fingerprint-suppressions-code-actions-provider.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/code-actions/base-code-actions-provider.ts b/src/core/code-actions/base-code-actions-provider.ts index 13618ef..8359484 100644 --- a/src/core/code-actions/base-code-actions-provider.ts +++ b/src/core/code-actions/base-code-actions-provider.ts @@ -29,11 +29,11 @@ export abstract class BaseCodeActionsProvider imple public abstract resolveCodeAction(codeAction: T_ACTION); - protected getMonokleDiagnostics(context: CodeActionContextExtended) { + protected getMonokleDiagnostics(context: CodeActionContextExtended, groupByRule = true) { // Filter out diagnostic objects without Monokle fingerprint, because this is not Monokle related diagnostics. const monokleDiagnostics = context.diagnostics.filter(diagnostic => diagnostic?.result?.fingerprints?.[MONOKLE_FINGERPRINT_FIELD]); - return this.getUniqueDiagnosticsByRule(monokleDiagnostics); + return groupByRule ? this.getUniqueDiagnosticsByRule(monokleDiagnostics) : monokleDiagnostics; } protected getParsedDocument(document: TextDocument, result: ValidationResult) { diff --git a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts index 784ee21..e7ee025 100644 --- a/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts +++ b/src/core/code-actions/fingerprint-suppressions-code-actions-provider.ts @@ -14,7 +14,7 @@ class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider return []; } - const diagnostics = this.getMonokleDiagnostics(context); + const diagnostics = this.getMonokleDiagnostics(context, false); // Filter out 'under review' violations if user has only 'request suppression' rights. There is no sense in requesting again. // However, for 'Admin' there should be still an ability to suppress such violation (so it's like accepting a request). @@ -63,7 +63,7 @@ class FingerprintSuppressionsCodeAction extends CodeAction { private readonly _root: Folder; constructor(diagnostic: DiagnosticExtended, permissions: SuppressionPermissions, root: Folder) { - super(`${permissions === 'ADD' ? 'Suppress' : 'Request suppression of'} this "${diagnostic.result._rule.name} (${diagnostic.result._rule.id})" problem`, CodeActionKind.QuickFix); + super(`${permissions === 'ADD' ? 'Suppress' : 'Request suppression of'}: ${diagnostic.result.message.text}`, CodeActionKind.QuickFix); this.diagnostics = [diagnostic]; this._result = diagnostic.result; From 9054e9d71c3ca3f7d702480d672595f59877c992 Mon Sep 17 00:00:00 2001 From: f1ames Date: Sun, 11 Feb 2024 20:54:11 +0100 Subject: [PATCH 16/19] fix: force synchronize on login --- src/extension.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 8c40f44..fa36447 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -293,8 +293,7 @@ function setupRemoteEventListeners(runtimeContext: RuntimeContext) { return; } - await runtimeContext.refreshPolicyPuller(); - await commands.executeCommand(COMMANDS.VALIDATE); + await runtimeContext.refreshPolicyPuller(true); }); runtimeContext.authenticator.on('logout', async () => { From 48a94ac9e2bd42ea702a07ffbe40c22cbb222cae Mon Sep 17 00:00:00 2001 From: f1ames Date: Sun, 11 Feb 2024 20:56:46 +0100 Subject: [PATCH 17/19] fix: fix sync loop --- src/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index fa36447..46e6a15 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -314,7 +314,6 @@ function setupRemoteEventListeners(runtimeContext: RuntimeContext) { return; } - await runtimeContext.refreshPolicyPuller(); await commands.executeCommand(COMMANDS.VALIDATE); }); } From 4c4f04530cf5d0031d72813b241c2575bf31ca2c Mon Sep 17 00:00:00 2001 From: f1ames Date: Sun, 11 Feb 2024 21:43:52 +0100 Subject: [PATCH 18/19] fix: fix optimistic suppressions apply --- src/commands/suppress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/suppress.ts b/src/commands/suppress.ts index e159e03..589103e 100644 --- a/src/commands/suppress.ts +++ b/src/commands/suppress.ts @@ -58,7 +58,7 @@ export function getSuppressCommand(context: RuntimeContext) { globals.project || undefined ); - await applySuppressions(root, localSuppression); + await applySuppressions(root); trackEvent('command/suppress', { status: 'success' From 296375c60728ae75578fdf93f822b9298b5632fd Mon Sep 17 00:00:00 2001 From: f1ames Date: Mon, 12 Feb 2024 12:00:51 +0100 Subject: [PATCH 19/19] chore: bump core synchronizer dep --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8a657f..41038a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.9.1", "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.14.1", + "@monokle/synchronizer": "^0.14.2", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0", @@ -841,9 +841,9 @@ } }, "node_modules/@monokle/synchronizer": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.14.1.tgz", - "integrity": "sha512-d2gE2u32PiyltEvri6A6xU9tciTq0PAYYFHP41od45iFZ00wbWRzcVfno6EAOpSQj1XhaSPwSl00e2wZfAPo7A==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@monokle/synchronizer/-/synchronizer-0.14.2.tgz", + "integrity": "sha512-djMglvaokJ7LEp6EPKLd68sThEdvLZ9A5msF3E0ALp651T6lvQia8SV7CmgJXEVLIyzNIT3dheAvciyOKxvHqg==", "dependencies": { "@monokle/types": "*", "env-paths": "^2.2.1", diff --git a/package.json b/package.json index 31a2db6..a46eba1 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ }, "dependencies": { "@monokle/parser": "^0.3.2", - "@monokle/synchronizer": "^0.14.1", + "@monokle/synchronizer": "^0.14.2", "@monokle/validation": "^0.33.0", "@segment/analytics-node": "^1.1.0", "diff": "^5.1.0",