Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"order": 1,
"properties": {
"snyk.advanced.authenticationMethod": {
"order": 1,
"type": "string",
"default": "OAuth2 (Recommended)",
"description": "Specifies whether to authenticate with OAuth2, PAT, or with an API token (Legacy). \n\nNote: OAuth2 authentication is recommended as it provides enhanced security.",
Expand All @@ -85,6 +86,7 @@
"markdownDescription": "Specifies whether to authenticate with OAuth2, PAT, or with an API token (Legacy). \n\nNote: OAuth2 authentication is recommended as it provides enhanced security."
},
"snyk.advanced.tokenStorage": {
"order": 3,
"type": "string",
"enum": [
"Always use VS Code's secret storage"
Expand All @@ -93,18 +95,27 @@
"markdownDescription": "Snyk uses VS Code's [secret storage](https://code.visualstudio.com/api/references/vscode-api#SecretStorage) to safely persist API token instead of saving it in plaintext in `settings.json`. To set the token manually, run the VS Code command [Snyk: Set Token](command:snyk.setToken)."
},
"snyk.advanced.customEndpoint": {
"order": 2,
"type": "string",
"markdownDescription": "If you're using SSO with Snyk and OAuth2, the custom endpoint configuration is automatically populated. \n\nOtherwise, for public regional instances, see our [documentation](https://docs.snyk.io/working-with-snyk/regional-hosting-and-data-residency#available-snyk-regions). \n\nFor private instances, contact your team or account manager.",
"scope": "window",
"pattern": "^(|(https?://)api.*.(snyk|snykgov).io)$"
},
"snyk.advanced.autoSelectOrganization": {
"order": 4,
"type": "boolean",
"markdownDescription": "Use automatic organization selection. When enabled, Snyk will automatically select the most appropriate organization for your project using context found in your repository and your authentication. If an organization is configured manually, this feature will be overridden. If an appropriate organization cannot be identified automatically, the preferred organization defined in your [web account settings](https://app.snyk.io/account) will be used as a fallback.",
"scope": "resource",
"default": true
},
"snyk.advanced.organization": {
"order": 5,
"type": "string",
"markdownDescription": "Specifies an organization ID to run tests for that organization.\n\nRetrieve the organization ID from the organization settings in the Snyk UI: `https://app.snyk.io/org/[ORG_NAME]/manage/settings` and copy the ID from the **Organization ID** section.\n\nNote: If not specified, the preferred organization as defined in your [web account settings](https://app.snyk.io/account) is used to run tests.",
"scope": "window"
"markdownDescription": "Specify the organization (ID or name) for Snyk to run scans against. If the organization is provided manually, automatic organization selection is overridden. If the organization value is blank or invalid, the preferred organization defined in your [web account settings](https://app.snyk.io/account) will be used.",
"scope": "resource"
},
"snyk.yesCrashReport": {
"//": "Name starts with y to put it at the end, as configs are sorted alphbetically",
"order": 6,
"type": "boolean",
"default": true,
"markdownDescription": "Send error reports to Snyk",
Expand All @@ -128,7 +139,7 @@
"order": 2,
"type": "boolean",
"title": "Snyk Code security issues",
"description": "Find and fix security issues in your application code in real time.",
"description": "Find and fix security issues in your application code in real time.\n\nFor these scans to run, note that it must be enabled for the organization.",
"default": true
},
"snyk.features.infrastructureAsCode": {
Expand Down Expand Up @@ -189,7 +200,7 @@
}
},
"additionalProperties": false,
"markdownDescription": "[Code Consistent Ignores](https://docs.snyk.io/manage-risk/prioritize-issues-for-fixing/ignore-issues/consistent-ignores-for-snyk-code) is a feature which provides consistent handling of \"ignore\" rules for code security findings across all surfaces - such as the CLI, IDE, and Snyk UI\n\nShow the following issues:",
"markdownDescription": "[Code Consistent Ignores](https://docs.snyk.io/manage-risk/prioritize-issues-for-fixing/ignore-issues/consistent-ignores-for-snyk-code) is a feature which provides consistent handling of \"ignore\" rules for code security findings across all surfaces - such as the CLI, IDE, and Snyk UI.\n\nNote: These filters will do nothing if Code Consistent Ignores (CCI) is disabled for the organization.\n\nShow the following issues:",
"scope": "window"
},
"snyk.allIssuesVsNetNewIssues": {
Expand Down Expand Up @@ -243,7 +254,6 @@
},
"snyk.yesBackgroundOssNotification": {
"order": 3,
"//": "Name starts with y to put it at the end, as configs are sorted alphabetically",
"type": "boolean",
"default": true,
"markdownDescription": "Show scan notification for critical Open Source Security issues when Snyk view is hidden",
Expand All @@ -260,7 +270,7 @@
"order": 1,
"type": "array",
"default": [],
"description": "Folder configuration for Snyk scans."
"description": "Folder configuration for Snyk scans. This value is read-only, any changes made will be overridden."
},
"snyk.securityAtInception.autoConfigureSnykMcpServer": {
"order": 2,
Expand Down Expand Up @@ -305,13 +315,14 @@
"order": 5,
"properties": {
"snyk.yesWelcomeNotification": {
"//": "Name starts with y to put it at the end, as configs are sorted alphabetically",
"order": 2,
"type": "boolean",
"default": true,
"markdownDescription": "Show welcome notification after installation and restart",
"scope": "application"
},
"snyk.trustedFolders": {
"order": 1,
"type": "array",
"default": [],
"description": "Folders to trust for Snyk scans."
Expand Down
2 changes: 1 addition & 1 deletion src/snyk/cli/services/cliService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class CliError {
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
constructor(public error: string | Error | unknown, public path?: string, public isCancellation = false) {}
constructor(public error: string, public path?: string, public isCancellation = false) {}
}
42 changes: 36 additions & 6 deletions src/snyk/common/commands/commandController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash';
import * as vscode from 'vscode';
import { IAuthenticationService } from '../../base/services/authenticationService';
import { createDCIgnore as createDCIgnoreUtil } from '../../snykCode/utils/ignoreFileUtils';
import { CodeIssueCommandArg } from '../../snykCode/views/interfaces';
Expand All @@ -15,7 +16,7 @@ import {
import { COMMAND_DEBOUNCE_INTERVAL } from '../constants/general';
import { ErrorHandler } from '../error/errorHandler';
import { ILanguageServer } from '../languageServer/languageServer';
import { CodeIssueData, IacIssueData } from '../languageServer/types';
import { CodeIssueData, IacIssueData, PresentableError } from '../languageServer/types';
import { ILog } from '../logger/interfaces';
import { IOpenerService } from '../services/openerService';
import { IProductService } from '../services/productService';
Expand Down Expand Up @@ -63,15 +64,21 @@ export class CommandController {

async openLocal(path: Uri, range?: Range): Promise<void> {
try {
await this.window.showTextDocumentViaUri(path, { viewColumn: 1, selection: range });
await this.window.showTextDocumentViaUri(path, {
viewColumn: 1,
selection: range,
});
} catch (e) {
ErrorHandler.handle(e, this.logger);
}
}

async openLocalFile(filePath: string, range?: Range): Promise<void> {
try {
await this.window.showTextDocumentViaFilepath(filePath, { viewColumn: 1, selection: range });
await this.window.showTextDocumentViaFilepath(filePath, {
viewColumn: 1,
selection: range,
});
} catch (e) {
ErrorHandler.handle(e, this.logger);
}
Expand All @@ -95,7 +102,7 @@ export class CommandController {

async createDCIgnore(custom = false, uriAdapter: IUriAdapter, path?: string): Promise<void> {
if (!path) {
const paths = this.workspace.getWorkspaceFolders();
const paths = this.workspace.getWorkspaceFolderPaths();
const promises = [];
for (const p of paths) {
promises.push(createDCIgnoreUtil(p, custom, this.workspace, this.window, uriAdapter));
Expand Down Expand Up @@ -161,9 +168,32 @@ export class CommandController {
return this.logger.showOutput();
}

showLsOutputChannel(): void {
async showLsOutputChannel(presentableError?: PresentableError): Promise<void> {
// To get an instance of an OutputChannel use createOutputChannel.
return this.languageServer.showOutputChannel();
this.languageServer.showOutputChannel();

if (presentableError?.error) {
// Format JSON as rows, excluding showNotification, treeNodeSuffix, and empty values
const details = Object.entries(presentableError)
.filter(([key]) => key !== 'showNotification' && key !== 'treeNodeSuffix')
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n');

const copyButton = 'Copy';
const result = await vscode.window.showInformationMessage(
details,
{
modal: true,
detail: `You can copy the error message and use the filter field in the output channel to locate it.`,
},
copyButton,
);

if (result === copyButton) {
await vscode.env.clipboard.writeText(presentableError.error);
}
}
}

async executeCommand(
Expand Down
90 changes: 90 additions & 0 deletions src/snyk/common/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ADVANCED_ADDITIONAL_PARAMETERS_SETTING,
ADVANCED_ADVANCED_MODE_SETTING,
ADVANCED_AUTHENTICATION_METHOD,
ADVANCED_AUTO_SELECT_ORGANIZATION,
ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT,
ADVANCED_AUTOSCAN_OSS_SETTING,
ADVANCED_CLI_BASE_DOWNLOAD_URL,
Expand Down Expand Up @@ -34,6 +35,7 @@ import {
} from '../constants/settings';
import SecretStorageAdapter from '../vscode/secretStorage';
import { IVSCodeWorkspace } from '../vscode/workspace';
import { WorkspaceFolder } from '../vscode/types';

export const NEWISSUES = 'Net new issues';
export const ALLISSUES = 'All issues';
Expand All @@ -57,6 +59,10 @@ export type FolderConfig = {
localBranches: string[] | undefined;
referenceFolderPath: string | undefined;
scanCommandConfig?: Record<string, ScanCommandConfig>;
orgSetByUser: boolean;
preferredOrg: string;
autoDeterminedOrg: string;
orgMigratedFromGlobalConfig: boolean;
};

export interface IssueViewOptions {
Expand Down Expand Up @@ -89,6 +95,8 @@ export const DEFAULT_SEVERITY_FILTER: SeverityFilter = {
low: true,
};

const DEFAULT_AUTO_ORGANIZATION = true; // Should match value in package.json.

export type PreviewFeatures = Record<string, never>;

export interface IConfiguration {
Expand Down Expand Up @@ -123,6 +131,16 @@ export interface IConfiguration {

organization: string | undefined;

isAutoSelectOrganizationEnabled(workspaceFolder: WorkspaceFolder): boolean;

setAutoSelectOrganization(workspaceFolder: WorkspaceFolder, autoSelectOrganization: boolean): Promise<void>;

getOrganization(workspaceFolder: WorkspaceFolder): string | undefined;

getOrganizationAtWorkspaceFolderLevel(workspaceFolder: WorkspaceFolder): string | undefined;

setOrganization(workspaceFolder: WorkspaceFolder, organization?: string): Promise<void>;

getAdditionalCliParameters(): string | undefined;

snykApiEndpoint: string;
Expand Down Expand Up @@ -179,6 +197,8 @@ export interface IConfiguration {

setFolderConfigs(folderConfig: FolderConfig[]): Promise<void>;

getConfigurationAtFolderLevelOnly<T>(configSettingName: string, workspaceFolder: WorkspaceFolder): T | undefined;

getSecureAtInceptionExecutionFrequency(): string;

setSecureAtInceptionExecutionFrequency(frequency: string): Promise<void>;
Expand Down Expand Up @@ -556,10 +576,67 @@ export class Configuration implements IConfiguration {
return config ?? DEFAULT_SEVERITY_FILTER;
}

/**
* Gets the auto organization setting for a workspace folder, considering all levels (folder, workspace, global, default).
*/
isAutoSelectOrganizationEnabled(workspaceFolder: WorkspaceFolder): boolean {
return (
this.workspace.getConfiguration<boolean>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_AUTO_SELECT_ORGANIZATION),
workspaceFolder,
) ?? DEFAULT_AUTO_ORGANIZATION
);
}

/**
* Sets the auto organization setting at the workspace folder level.
*/
async setAutoSelectOrganization(workspaceFolder: WorkspaceFolder, autoSelectOrganization: boolean): Promise<void> {
await this.workspace.updateConfiguration(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_AUTO_SELECT_ORGANIZATION),
autoSelectOrganization,
workspaceFolder,
);
}

/**
* Gets the organization setting from the global & workspace scopes only.
*/
get organization(): string | undefined {
return this.workspace.getConfiguration<string>(CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_ORGANIZATION));
}

getOrganization(workspaceFolder: WorkspaceFolder): string | undefined {
return this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_ORGANIZATION),
workspaceFolder,
);
}

getOrganizationAtWorkspaceFolderLevel(workspaceFolder: WorkspaceFolder): string | undefined {
return this.workspace.inspectConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_ORGANIZATION),
workspaceFolder,
)?.workspaceFolderValue;
}

/**
* Sets the organization at the workspace folder level.
* If the empty string or undefined is provided, the organization will be cleared.
*/
async setOrganization(workspaceFolder: WorkspaceFolder, organization?: string): Promise<void> {
await this.workspace.updateConfiguration(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_ORGANIZATION),
organization === '' ? undefined : organization,
workspaceFolder,
);
}

getPreviewFeatures(): PreviewFeatures {
const defaultSetting: PreviewFeatures = {};

Expand Down Expand Up @@ -644,6 +721,19 @@ export class Configuration implements IConfiguration {
);
}

/**
* Gets a configuration setting ONLY at the workspace folder level (no fallback to workspace/global).
* Returns undefined if the setting is not specifically set at the folder level.
*/
getConfigurationAtFolderLevelOnly<T>(configSettingName: string, workspaceFolder: WorkspaceFolder): T | undefined {
const inspectionResult = this.workspace.inspectConfiguration<T>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(configSettingName),
workspaceFolder,
);
return inspectionResult?.workspaceFolderValue;
}

private getConfigName = (setting: string) => setting.replace(`${CONFIGURATION_IDENTIFIER}.`, '');

async setSecureAtInceptionExecutionFrequency(frequency: string): Promise<void> {
Expand Down
17 changes: 1 addition & 16 deletions src/snyk/common/configuration/folderConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,16 @@ export interface IFolderConfigs {
setBranch(window: IVSCodeWindow, config: IConfiguration, folderPath: string): Promise<void>;

setReferenceFolder(window: IVSCodeWindow, config: IConfiguration, folderPath: string): Promise<void>;

resetFolderConfigsCache(): void;
}

export class FolderConfigs implements IFolderConfigs {
private folderConfigsCache?: ReadonlyArray<FolderConfig>;

getFolderConfig(config: IConfiguration, folderPath: string): FolderConfig | undefined {
const folderConfigs = this.getFolderConfigs(config);
return folderConfigs.find(i => i.folderPath === folderPath);
}

getFolderConfigs(config: IConfiguration): ReadonlyArray<FolderConfig> {
if (this.folderConfigsCache !== undefined) {
return this.folderConfigsCache;
}
const folderConfigs = config.getFolderConfigs();
this.folderConfigsCache = folderConfigs;

return folderConfigs;
return config.getFolderConfigs();
}

async setReferenceFolder(window: IVSCodeWindow, config: IConfiguration, folderPath: string): Promise<void> {
Expand Down Expand Up @@ -91,10 +81,5 @@ export class FolderConfigs implements IFolderConfigs {
i.folderPath === folderConfig.folderPath ? folderConfig : i,
);
await config.setFolderConfigs(finalFolderConfigs);
this.folderConfigsCache = finalFolderConfigs;
}

resetFolderConfigsCache() {
this.folderConfigsCache = undefined;
}
}
2 changes: 1 addition & 1 deletion src/snyk/common/constants/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Language Server name, used e.g. for the output channel
export const SNYK_LANGUAGE_SERVER_NAME = 'Snyk Language Server';
// The internal language server protocol version for custom messages and configuration
export const PROTOCOL_VERSION = 20;
export const PROTOCOL_VERSION = 21;

// LS protocol methods (needed for not having to rely on vscode dependencies in testing)
export const DID_CHANGE_CONFIGURATION_METHOD = 'workspace/didChangeConfiguration';
Expand Down
1 change: 1 addition & 0 deletions src/snyk/common/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const ADVANCED_ADVANCED_MODE_SETTING = `${CONFIGURATION_IDENTIFIER}.advan
export const ADVANCED_AUTOSCAN_OSS_SETTING = `${CONFIGURATION_IDENTIFIER}.advanced.autoScanOpenSourceSecurity`;
export const ADVANCED_ADDITIONAL_PARAMETERS_SETTING = `${CONFIGURATION_IDENTIFIER}.advanced.additionalParameters`;
export const ADVANCED_CUSTOM_ENDPOINT = `${CONFIGURATION_IDENTIFIER}.advanced.customEndpoint`;
export const ADVANCED_AUTO_SELECT_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.autoSelectOrganization`;
export const ADVANCED_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.organization`;
export const ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT = `${CONFIGURATION_IDENTIFIER}.advanced.automaticDependencyManagement`;
export const ADVANCED_CLI_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.cliPath`;
Expand Down
Loading
Loading