Skip to content
Merged
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
2,123 changes: 2,058 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
},
"dependencies": {
"@azure/identity": "^4.1.0",
"@secretlint/node": "^9.3.2",
"@secretlint/secretlint-formatter-sarif": "^9.3.2",
"@secretlint/secretlint-rule-no-dotenv": "^9.3.2",
"@secretlint/secretlint-rule-preset-recommend": "^9.3.2",
"@vscode/vsce-sign": "^2.0.0",
"azure-devops-node-api": "^12.5.0",
"chalk": "^2.4.2",
Expand All @@ -55,6 +59,7 @@
"minimatch": "^3.0.3",
"parse-semver": "^1.1.1",
"read": "^1.0.7",
"secretlint": "^9.3.2",
"semver": "^7.5.2",
"tmp": "^0.2.3",
"typed-rest-client": "^1.8.4",
Expand Down
36 changes: 36 additions & 0 deletions src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as GitHost from 'hosted-git-info';
import parseSemver from 'parse-semver';
import * as jsonc from 'jsonc-parser';
import * as vsceSign from '@vscode/vsce-sign';
import { lintFiles, lintText, prettyPrintLintResult } from './secretLint';

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

Expand Down Expand Up @@ -2106,4 +2107,39 @@ export async function printAndValidatePackagedFiles(files: IFile[], cwd: string,

message += '\n';
util.log.info(message);

await scanFilesForSecrets(files);
}

export async function scanFilesForSecrets(files: IFile[]): Promise<void> {
const onDiskFiles: ILocalFile[] = files.filter(file => !isInMemoryFile(file)) as ILocalFile[];
const inMemoryFiles: IInMemoryFile[] = files.filter(file => isInMemoryFile(file)) as IInMemoryFile[];

const onDiskResult = await lintFiles(onDiskFiles.map(file => file.localPath));
const inMemoryResults = await Promise.all(
inMemoryFiles.map(file => lintText(typeof file.contents === 'string' ? file.contents : file.contents.toString('utf8'), file.path))
);

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

// secrets found
const noneDotEnvSecretsFound = secretsFound.filter(result => result.ruleId !== '@secretlint/secretlint-rule-no-dotenv');
if (noneDotEnvSecretsFound.length > 0) {
let errorOutput = '';
for (const secret of noneDotEnvSecretsFound) {
errorOutput += '\n' + prettyPrintLintResult(secret);
}
util.log.error(`Secrets have been detected in the files which are being packaged:\n\n${errorOutput}`);
}

// .env file found
const allRuleIds = new Set(secretsFound.map(result => result.ruleId).filter(Boolean));
if (allRuleIds.has('@secretlint/secretlint-rule-no-dotenv')) {
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.`);
}

process.exit(1);
}
138 changes: 138 additions & 0 deletions src/secretLint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import chalk from "chalk";
import { Convert, Location, Region, Result, Level } from "./typings/secret-lint-types";

interface SecretLintEngineResult {
ok: boolean;
output: string;
}

interface SecretLintResult {
ok: boolean;
results: Result[];
}

const lintConfig = {
rules: [
{
id: "@secretlint/secretlint-rule-preset-recommend",
rules: [
{
"id": "@secretlint/secretlint-rule-basicauth",
"allowMessageIds": ["BasicAuth"]
}
]
}, {
id: "@secretlint/secretlint-rule-no-dotenv"
}

]
};

const lintOptions = {
configFileJSON: lintConfig,
formatter: "@secretlint/secretlint-formatter-sarif", // checkstyle, compact, jslint-xml, junit, pretty-error, stylish, tap, unix, json, mask-result, table
color: true,
maskSecrets: false
};

// Helper function to dynamically import the createEngine function
async function getEngine() {
// Use a raw dynamic import that will not be transformed
// This is necessary because @secretlint/node is an ESM module
const secretlintModule = await eval('import("@secretlint/node")');
const engine = await secretlintModule.createEngine(lintOptions);
return engine;
}

export async function lintFiles(
filePaths: string[]
): Promise<SecretLintResult> {
const engine = await getEngine();

const engineResult = await engine.executeOnFiles({
filePathList: filePaths
});
return parseResult(engineResult);
}

export async function lintText(
content: string,
fileName: string
): Promise<SecretLintResult> {
const engine = await getEngine();

const engineResult = await engine.executeOnContent({
content,
filePath: fileName
});
return parseResult(engineResult);
}

function parseResult(result: SecretLintEngineResult): SecretLintResult {
const output = Convert.toSecretLintOutput(result.output);
const results = output.runs.at(0)?.results ?? [];
return { ok: result.ok, results };
}

export function prettyPrintLintResult(result: Result): string {
if (!result.message.text) {
return JSON.stringify(result);
}

const text = result.message.text;
const titleColor = result.level === undefined || result.level === Level.Error ? chalk.bold.red : chalk.bold.yellow;
const title = text.length > 54 ? text.slice(0, 50) + '...' : text;
let output = `\t${titleColor(title)}\n`;

if (result.locations) {
result.locations.forEach(location => {
output += `\t${prettyPrintLocation(location)}\n`;
});
}
return output;
}

function prettyPrintLocation(location: Location): string {
if (!location.physicalLocation) { return JSON.stringify(location); }

const uri = location.physicalLocation.artifactLocation?.uri;
if (!uri) { return JSON.stringify(location); }

let output = uri;

const region = location.physicalLocation.region;
const regionStringified = region ? prettyPrintRegion(region) : undefined;
if (regionStringified) {
output += `#${regionStringified}`;
}

return output;
}

function prettyPrintRegion(region: Region): string | undefined {
const startPosition = prettyPrintPosition(region.startLine, region.startColumn);
const endPosition = prettyPrintPosition(region.endLine, region.endColumn);

if (!startPosition) {
return undefined;
}

let output = startPosition;
if (endPosition && startPosition !== endPosition) {
output += `-${endPosition}`;
}

return output;
}

function prettyPrintPosition(line: number | undefined, column: number | undefined): string | undefined {
if (line === undefined) {
return undefined;
}
let output: string = line.toString();
if (column !== undefined) {
output += `:${column}`;
}

return output;
}
3 changes: 3 additions & 0 deletions src/test/fixtures/env/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A=1
B=2
C=3
1 change: 1 addition & 0 deletions src/test/fixtures/env/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LICENSE...
1 change: 1 addition & 0 deletions src/test/fixtures/env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test
6 changes: 6 additions & 0 deletions src/test/fixtures/env/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

// Here a Fibonacci sequence function
export function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
15 changes: 15 additions & 0 deletions src/test/fixtures/env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "uuid",
"publisher": "joaomoreno",
"version": "1.0.0",
"engines": {
"vscode": "*"
},
"files": [
"main.ts",
"package.json",
"LICENSE",
"README.md",
".env"
]
}
1 change: 1 addition & 0 deletions src/test/fixtures/secret/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LICENSE...
1 change: 1 addition & 0 deletions src/test/fixtures/secret/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test
18 changes: 18 additions & 0 deletions src/test/fixtures/secret/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const k = {
'type': 'service_account',
'project_id': 'my-gcp-project',
'private_key_id': 'abcdef1234567890abcdef1234567890abcdef12',
'private_key': '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkq...\n-----END PRIVATE KEY-----\n',
'client_email': 'my-service-account@my-gcp-project.iam.gserviceaccount.com',
'client_id': '123456789012345678901',
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
'token_uri': 'https://oauth2.googleapis.com/token',
'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40my-gcp-project.iam.gserviceaccount.com'
};

// Here a Fibonacci sequence function
export function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
14 changes: 14 additions & 0 deletions src/test/fixtures/secret/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "uuid",
"publisher": "joaomoreno",
"version": "1.0.0",
"engines": {
"vscode": "*"
},
"files": [
"main.ts",
"package.json",
"LICENSE",
"README.md"
]
}
29 changes: 29 additions & 0 deletions src/test/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,25 @@ async function testPrintAndValidatePackagedFiles(files: IFile[], cwd: string, ma
}
}

async function processExitExpected(fn: () => Promise<any>, errorMessage: string): Promise<void> {
const originalExit = process.exit;
let exitCalled = false;

try {
process.exit = (() => {
exitCalled = true;
throw new Error('Process exit was called');
}) as any;

await fn();
assert.fail(errorMessage);
} catch (error) {
assert.ok(exitCalled, errorMessage);
} finally {
process.exit = originalExit;
}
}

describe('collect', function () {
this.timeout(60000);

Expand Down Expand Up @@ -351,6 +370,16 @@ describe('collect', function () {
const manifest = await readManifest(cwd);
await collect(manifest, { cwd });
});

it('should not package .env file', async function () {
const cwd = fixture('env');
await processExitExpected(() => pack({ cwd }), 'Expected package to throw: .env file should not be packaged');
});

it('should not package file which has a private key', async function () {
const cwd = fixture('secret');
await processExitExpected(() => pack({ cwd }), 'Expected package to throw: file which has a private key should not be packaged');
});
});

describe('readManifest', () => {
Expand Down
Loading