Skip to content

Commit 039a33a

Browse files
authored
Merge pull request #1145 from microsoft/benibenj/cultural-elephant
Allow packaging .env and secrets using command line flags
2 parents 468122d + 1e78223 commit 039a33a

File tree

5 files changed

+109
-18
lines changed

5 files changed

+109
-18
lines changed

src/main.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ module.exports = function (argv: string[]): void {
124124
.option('--allow-star-activation', 'Allow using * in activation events')
125125
.option('--allow-missing-repository', 'Allow missing a repository URL in package.json')
126126
.option('--allow-unused-files-pattern', 'Allow include patterns for the files field in package.json that does not match any file')
127+
.option('--allow-package-secrets <secrets...>', 'Allow packaging specific secrets. The names of the secrets can be found in the error message ([SECRET_NAME]).')
128+
.option('--allow-package-all-secrets', 'Allow to package all kinds of secrets')
129+
.option('--allow-package-env-file', 'Allow packaging .env files')
127130
.option('--skip-license', 'Allow packaging without license file')
128131
.option('--sign-tool <path>', 'Path to the VSIX signing tool. Will be invoked with two arguments: `SIGNTOOL <path/to/extension.signature.manifest> <path/to/extension.signature.p7s>`.')
129132
.option('--follow-symlinks', 'Recurse into symlinked directories instead of treating them as files')
@@ -153,6 +156,9 @@ module.exports = function (argv: string[]): void {
153156
allowStarActivation,
154157
allowMissingRepository,
155158
allowUnusedFilesPattern,
159+
allowPackageSecrets,
160+
allowPackageAllSecrets,
161+
allowPackageEnvFile,
156162
skipLicense,
157163
signTool,
158164
followSymlinks,
@@ -183,6 +189,9 @@ module.exports = function (argv: string[]): void {
183189
allowStarActivation,
184190
allowMissingRepository,
185191
allowUnusedFilesPattern,
192+
allowPackageSecrets,
193+
allowPackageAllSecrets,
194+
allowPackageEnvFile,
186195
skipLicense,
187196
signTool,
188197
followSymlinks,
@@ -230,6 +239,9 @@ module.exports = function (argv: string[]): void {
230239
.addOption(new Option('--noVerify', 'Allow all proposed APIs (deprecated: use --allow-all-proposed-apis instead)').hideHelp(true))
231240
.option('--allow-proposed-apis <apis...>', 'Allow specific proposed APIs')
232241
.option('--allow-all-proposed-apis', 'Allow all proposed APIs')
242+
.option('--allow-package-secrets <secrets...>', 'Allow packaging specific secrets. The names of the secrets can be found in the error message ([SECRET_NAME]).')
243+
.option('--allow-package-all-secrets', 'Allow to package all kinds of secrets')
244+
.option('--allow-package-env-file', 'Allow packaging .env files')
233245
.option('--ignoreFile <path>', 'Indicate alternative .vscodeignore')
234246
// default must remain undefined for dependencies or we will fail to load defaults from package.json
235247
.option('--dependencies', 'Enable dependency detection via npm or yarn', undefined)
@@ -267,6 +279,9 @@ module.exports = function (argv: string[]): void {
267279
noVerify,
268280
allowProposedApis,
269281
allowAllProposedApis,
282+
allowPackageSecrets,
283+
allowPackageAllSecrets,
284+
allowPackageEnvFile,
270285
ignoreFile,
271286
dependencies,
272287
preRelease,
@@ -303,6 +318,9 @@ module.exports = function (argv: string[]): void {
303318
noVerify: noVerify || !verify,
304319
allowProposedApis,
305320
allowAllProposedApis,
321+
allowPackageSecrets,
322+
allowPackageAllSecrets,
323+
allowPackageEnvFile,
306324
ignoreFile,
307325
dependencies,
308326
preRelease,

src/package.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as GitHost from 'hosted-git-info';
2727
import parseSemver from 'parse-semver';
2828
import * as jsonc from 'jsonc-parser';
2929
import * as vsceSign from '@vscode/vsce-sign';
30-
import { lintFiles, lintText, prettyPrintLintResult } from './secretLint';
30+
import { getRuleNameFromRuleId, lintFiles, lintText, prettyPrintLintResult } from './secretLint';
3131

3232
const MinimatchOptions: minimatch.IOptions = { dot: true };
3333

@@ -162,6 +162,10 @@ export interface IPackageOptions {
162162
readonly allowStarActivation?: boolean;
163163
readonly allowMissingRepository?: boolean;
164164
readonly allowUnusedFilesPattern?: boolean;
165+
readonly allowPackageSecrets?: string[];
166+
readonly allowPackageAllSecrets?: boolean;
167+
readonly allowPackageEnvFile?: boolean;
168+
165169
readonly skipLicense?: boolean;
166170

167171
readonly signTool?: string;
@@ -2108,10 +2112,17 @@ export async function printAndValidatePackagedFiles(files: IFile[], cwd: string,
21082112
message += '\n';
21092113
util.log.info(message);
21102114

2111-
await scanFilesForSecrets(files);
2115+
const fileExclusion = hasIgnoreFile ? FileExclusionType.VSCodeIgnore : manifest.files ? FileExclusionType.PackageFiles : FileExclusionType.None;
2116+
await scanFilesForSecrets(files, fileExclusion, options);
2117+
}
2118+
2119+
enum FileExclusionType {
2120+
None = 'none',
2121+
VSCodeIgnore = 'vscodeIgnore',
2122+
PackageFiles = 'package.files'
21122123
}
21132124

2114-
export async function scanFilesForSecrets(files: IFile[]): Promise<void> {
2125+
export async function scanFilesForSecrets(files: IFile[], fileExclusion: FileExclusionType, options: IPackageOptions): Promise<void> {
21152126
const onDiskFiles: ILocalFile[] = files.filter(file => !isInMemoryFile(file)) as ILocalFile[];
21162127
const inMemoryFiles: IInMemoryFile[] = files.filter(file => isInMemoryFile(file)) as IInMemoryFile[];
21172128

@@ -2121,25 +2132,49 @@ export async function scanFilesForSecrets(files: IFile[]): Promise<void> {
21212132
);
21222133

21232134
const secretsFound = [...inMemoryResults, onDiskResult].filter(result => !result.ok).flatMap(result => result.results);
2124-
if (secretsFound.length === 0) {
2125-
return;
2126-
}
21272135

21282136
// secrets found
2129-
const noneDotEnvSecretsFound = secretsFound.filter(result => result.ruleId !== '@secretlint/secretlint-rule-no-dotenv');
2137+
const noneDotEnvSecretsFound = secretsFound.filter(result =>
2138+
result.ruleId &&
2139+
result.ruleId !== '@secretlint/secretlint-rule-no-dotenv' &&
2140+
!options.allowPackageAllSecrets &&
2141+
!options.allowPackageSecrets?.includes(getRuleNameFromRuleId(result.ruleId))
2142+
);
21302143
if (noneDotEnvSecretsFound.length > 0) {
2131-
let errorOutput = '';
2132-
for (const secret of noneDotEnvSecretsFound) {
2133-
errorOutput += '\n' + prettyPrintLintResult(secret);
2134-
}
2135-
util.log.error(`Secrets have been detected in the files which are being packaged:\n\n${errorOutput}`);
2144+
const uniqueSecretIds = new Set<string>(noneDotEnvSecretsFound.map(result => result.ruleId!));
2145+
const secretsFoundRuleNames = Array.from(uniqueSecretIds).map(getRuleNameFromRuleId);
2146+
2147+
let errorMessage = `${chalk.bold('Potential security issue detected:')}`;
2148+
errorMessage += ` Your extension package contains sensitive information that should not be published.`
2149+
errorMessage += ` Please remove these secrets before packaging.`;
2150+
errorMessage += `\n` + noneDotEnvSecretsFound.map(prettyPrintLintResult).join('\n');
2151+
2152+
let hintMessage = `\nIn case of a false positives, you can allowlist secrets with `;
2153+
hintMessage += secretsFoundRuleNames.map(name => `--allow-package-secrets ${name}`).join(' ');
2154+
hintMessage += ` or use --allow-package-all-secrets to skip this check (not recommended).`;
2155+
2156+
util.log.error(errorMessage + chalk.italic(hintMessage));
2157+
process.exit(1);
21362158
}
21372159

21382160
// .env file found
21392161
const allRuleIds = new Set(secretsFound.map(result => result.ruleId).filter(Boolean));
2140-
if (allRuleIds.has('@secretlint/secretlint-rule-no-dotenv')) {
2141-
util.log.error(`${chalk.bold.red('.env')} files should not be packaged. Ignore them in your ${chalk.bold('.vscodeignore')} file or exclude them from the package.json ${chalk.bold('files')} property.`);
2162+
if (!options.allowPackageEnvFile && allRuleIds.has('@secretlint/secretlint-rule-no-dotenv')) {
2163+
let errorMessage = `${chalk.bold.red('.env')} files should not be packaged.`;
2164+
2165+
switch (fileExclusion) {
2166+
case FileExclusionType.None:
2167+
errorMessage += ` Ignore the file in your ${chalk.bold('.vscodeignore')} or exclude it from the package.json ${chalk.bold('files')} property.`; break;
2168+
case FileExclusionType.VSCodeIgnore:
2169+
errorMessage += ` Ignore the file in your ${chalk.bold('.vscodeignore')}.`; break;
2170+
case FileExclusionType.PackageFiles:
2171+
errorMessage += ` Do not include the file in your package.json ${chalk.bold('files')} property.`; break;
2172+
}
2173+
2174+
const hintMessage = `\nTo ignore this check, you can use --allow-package-env-file (not recommended).`;
2175+
2176+
util.log.error(errorMessage + chalk.italic(hintMessage));
2177+
process.exit(1);
21422178
}
21432179

2144-
process.exit(1);
21452180
}

src/publish.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export interface IPublishOptions {
7474
readonly noVerify?: boolean;
7575
readonly allowProposedApis?: string[];
7676
readonly allowAllProposedApis?: boolean;
77+
readonly allowPackageSecrets?: string[];
78+
readonly allowPackageAllSecrets?: boolean;
79+
readonly allowPackageEnvFile?: boolean;
7780
readonly dependencies?: boolean;
7881
readonly preRelease?: boolean;
7982
readonly allowStarActivation?: boolean;

src/secretLint.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ function parseResult(result: SecretLintEngineResult): SecretLintResult {
7474
return { ok: result.ok, results };
7575
}
7676

77+
export function getRuleNameFromRuleId(ruleId: string): string {
78+
const parts = ruleId.split('-rule-');
79+
return parts[parts.length - 1];
80+
}
81+
7782
export function prettyPrintLintResult(result: Result): string {
7883
if (!result.message.text) {
7984
return JSON.stringify(result);
@@ -82,7 +87,9 @@ export function prettyPrintLintResult(result: Result): string {
8287
const text = result.message.text;
8388
const titleColor = result.level === undefined || result.level === Level.Error ? chalk.bold.red : chalk.bold.yellow;
8489
const title = text.length > 54 ? text.slice(0, 50) + '...' : text;
85-
let output = `\t${titleColor(title)}\n`;
90+
const ruleName = result.ruleId ? getRuleNameFromRuleId(result.ruleId) : 'unknown';
91+
92+
let output = `\t${titleColor(title)} [${ruleName}]\n`;
8693

8794
if (result.locations) {
8895
result.locations.forEach(location => {

src/test/package.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ async function processExitExpected(fn: () => Promise<any>, errorMessage: string)
168168
describe('collect', function () {
169169
this.timeout(60000);
170170

171+
let testDir: tmp.DirResult;
172+
before(() => {
173+
testDir = tmp.dirSync({ unsafeCleanup: true, tmpdir: fixture('') });
174+
});
175+
after(() => {
176+
testDir.removeCallback();
177+
});
178+
179+
function getVisxOutputPath() {
180+
const randomValue = Math.random().toString(36).substring(2, 15);
181+
return path.join(testDir.name, `extension-${randomValue}.vsix`);
182+
}
183+
171184
it('should catch all files', () => {
172185
const cwd = fixture('uuid');
173186

@@ -373,12 +386,27 @@ describe('collect', function () {
373386

374387
it('should not package .env file', async function () {
375388
const cwd = fixture('env');
376-
await processExitExpected(() => pack({ cwd }), 'Expected package to throw: .env file should not be packaged');
389+
await processExitExpected(() => pack({ cwd, packagePath: getVisxOutputPath() }), 'Expected package to throw: .env file should not be packaged');
390+
});
391+
392+
it('allow packaging .env file with --allow-package-env-file', async function () {
393+
const cwd = fixture('env');
394+
await pack({ cwd, allowPackageEnvFile: true, packagePath: getVisxOutputPath() });
377395
});
378396

379397
it('should not package file which has a private key', async function () {
380398
const cwd = fixture('secret');
381-
await processExitExpected(() => pack({ cwd }), 'Expected package to throw: file which has a private key should not be packaged');
399+
await processExitExpected(() => pack({ cwd, packagePath: getVisxOutputPath() }), 'Expected package to throw: file which has a private key should not be packaged');
400+
});
401+
402+
it('allow packaging file which has a private key with --allow-package-secrets', async function () {
403+
const cwd = fixture('secret');
404+
await pack({ cwd, allowPackageSecrets: ['privatekey'], packagePath: getVisxOutputPath() });
405+
});
406+
407+
it('allow packaging file which has a private key with --allow-package-all-secrets', async function () {
408+
const cwd = fixture('secret');
409+
await pack({ cwd, allowPackageAllSecrets: true, packagePath: getVisxOutputPath() });
382410
});
383411
});
384412

0 commit comments

Comments
 (0)