diff --git a/apps/cli/src/commands/project.command.ts b/apps/cli/src/commands/project.command.ts index a8416087..639c9f24 100644 --- a/apps/cli/src/commands/project.command.ts +++ b/apps/cli/src/commands/project.command.ts @@ -3,7 +3,7 @@ import CreateProject from './project/create.project' import DeleteProject from './project/delete.project' import ForkProject from './project/fork.project' import GetProject from './project/get.project' -import ImportEnvs from './project/import.project' +import ImportFromEnv from './project/import.project' import ListProjectForks from './project/list-forks.project' import ListProject from './project/list.project' import SyncProject from './project/sync.project' @@ -30,7 +30,7 @@ export default class ProjectCommand extends BaseCommand { new SyncProject(), new UnlinkProject(), new UpdateProject(), - new ImportEnvs() + new ImportFromEnv() ] } } diff --git a/apps/cli/src/commands/project/import.project.ts b/apps/cli/src/commands/project/import.project.ts index 690c191a..fa9bc25c 100644 --- a/apps/cli/src/commands/project/import.project.ts +++ b/apps/cli/src/commands/project/import.project.ts @@ -1,6 +1,7 @@ import type { CommandActionData, - CommandArgument + CommandArgument, + CommandOption } from '@/types/command/command.types' import BaseCommand from '../base.command' import { confirm, text } from '@clack/prompts' @@ -11,7 +12,7 @@ import path from 'node:path' import dotenv from 'dotenv' import secretDetector from '@keyshade/secret-scan' -export default class ImportEnvs extends BaseCommand { +export default class ImportFromEnv extends BaseCommand { getName(): string { return 'import' } @@ -25,9 +26,15 @@ export default class ImportEnvs extends BaseCommand { { name: '', description: 'Slug of the project where envs will be imported.' - }, + } + ] + } + + getOptions(): CommandOption[] { + return [ { - name: '<.env file path>', + short: '-f', + long: '--env-file ', description: 'Path to the .env file' } ] @@ -37,29 +44,38 @@ export default class ImportEnvs extends BaseCommand { return true } - async action({ args }: CommandActionData): Promise { - const [projectSlug, dotEnvPath] = args + async action({ args, options }: CommandActionData): Promise { + const [projectSlug] = args + const { envFile } = options try { - const envFileContent = await fs.readFile( - path.resolve(dotEnvPath), - 'utf-8' - ) + const resolvedPath = path.resolve(envFile) + const exists = await fs + .access(resolvedPath) + .then(() => true) + .catch(() => false) + if (!exists) { + throw new Error(`The .env file does not exist at path: ${resolvedPath}`) + } + const envFileContent = await fs.readFile(resolvedPath, 'utf-8') const envVariables = dotenv.parse(envFileContent) + if (Object.keys(envVariables).length === 0) { + throw new Error('No environment variables found in the provided file') + } - const secretsAndVariables = secretDetector.detectObject(envVariables) + const secretsAndVariables = secretDetector.detectJsObject(envVariables) Logger.info( 'Detected secrets:\n' + - secretsAndVariables.secrets - .map((secret) => secret[0] + ' = ' + secret[1]) + Object.entries(secretsAndVariables.secrets) + .map(([key, value]) => key + ' = ' + value) .join('\n') ) Logger.info( 'Detected variables:\n' + - secretsAndVariables.variables - .map((variable) => variable[0] + ' = ' + variable[1]) + Object.entries(secretsAndVariables.variables) + .map(([key, value]) => key + ' = ' + value) .join('\n') ) @@ -85,8 +101,8 @@ export default class ImportEnvs extends BaseCommand { let noOfSecrets = 0 let noOfVariables = 0 const errors: string[] = [] - for (const [key, value] of secretsAndVariables.secrets) { - const { data, error, success } = + for (const [key, value] of Object.entries(secretsAndVariables.secrets)) { + const { error, success } = await ControllerInstance.getInstance().secretController.createSecret( { projectSlug, @@ -110,8 +126,10 @@ export default class ImportEnvs extends BaseCommand { } } - for (const [key, value] of secretsAndVariables.variables) { - const { data, error, success } = + for (const [key, value] of Object.entries( + secretsAndVariables.variables + )) { + const { error, success } = await ControllerInstance.getInstance().variableController.createVariable( { projectSlug, diff --git a/packages/secret-scan/src/index.ts b/packages/secret-scan/src/index.ts index 9612c32b..643bd3b8 100644 --- a/packages/secret-scan/src/index.ts +++ b/packages/secret-scan/src/index.ts @@ -1,5 +1,5 @@ import denylist from '@/denylist' -import type { SecretResult, ScanObjectResult } from '@/types' +import type { SecretResult, ScanJsObjectResult } from '@/types' export type SecretConfig = Record @@ -23,17 +23,22 @@ class SecretDetector { return { found: false } } - detectObject(input: Record): ScanObjectResult { - const result = { - secrets: [], - variables: [] + /** + * Detects if a given js object contains any secret patterns. + * @param input - The object to scan for secret patterns. + * @returns A `ScanJsObjectResult` object containing the secrets and variables found in the object. + */ + detectJsObject(input: Record): ScanJsObjectResult { + const result: ScanJsObjectResult = { + secrets: {}, + variables: {} } for (const [key, value] of Object.entries(input)) { const secretResult = this.detect(value) if (secretResult.found) { - result.secrets.push([key, value]) + result.secrets[key] = value } else { - result.variables.push([key, value]) + result.variables[key] = value } } diff --git a/packages/secret-scan/src/test/detect-js-object.test.ts b/packages/secret-scan/src/test/detect-js-object.test.ts index 0429a408..14d3a582 100644 --- a/packages/secret-scan/src/test/detect-js-object.test.ts +++ b/packages/secret-scan/src/test/detect-js-object.test.ts @@ -11,24 +11,24 @@ describe('Dectect Secrets and Variables from Object', () => { GOOGLE_ANALYTICS: 'UA-123456789-1', API_PORT: '3000' } - const result = secretDetector.detectObject(input) - expect(result.secrets).toEqual([ - ['GITHUB_KEY', input.GITHUB_KEY], - ['AWS_KEY', input.AWS_KEY], - ['OPENAI_KEY', input.OPENAI_KEY] - ]) - expect(result.variables).toEqual([ - ['NEXT_PUBLIC_API_KEY', input.NEXT_PUBLIC_API_KEY], - ['GOOGLE_ANALYTICS', input.GOOGLE_ANALYTICS], - ['API_PORT', input.API_PORT] - ]) + const result = secretDetector.detectJsObject(input) + expect(result.secrets).toEqual({ + GITHUB_KEY: input.GITHUB_KEY, + AWS_KEY: input.AWS_KEY, + OPENAI_KEY: input.OPENAI_KEY + }) + expect(result.variables).toEqual({ + NEXT_PUBLIC_API_KEY: input.NEXT_PUBLIC_API_KEY, + GOOGLE_ANALYTICS: input.GOOGLE_ANALYTICS, + API_PORT: input.API_PORT + }) }) - it('should return empty arrays for secrets and variables when input is empty', () => { + it('should return empty objects for secrets and variables when input is empty', () => { const input = {} - const result = secretDetector.detectObject(input) - expect(result.secrets).toEqual([]) - expect(result.variables).toEqual([]) + const result = secretDetector.detectJsObject(input) + expect(result.secrets).toEqual({}) + expect(result.variables).toEqual({}) }) it('should return only variables when there are no secrets', () => { @@ -37,13 +37,13 @@ describe('Dectect Secrets and Variables from Object', () => { GOOGLE_ANALYTICS: 'UA-123456789-1', API_PORT: '3000' } - const result = secretDetector.detectObject(input) - expect(result.secrets).toEqual([]) - expect(result.variables).toEqual([ - ['NEXT_PUBLIC_API_KEY', input.NEXT_PUBLIC_API_KEY], - ['GOOGLE_ANALYTICS', input.GOOGLE_ANALYTICS], - ['API_PORT', input.API_PORT] - ]) + const result = secretDetector.detectJsObject(input) + expect(result.secrets).toEqual({}) + expect(result.variables).toEqual({ + NEXT_PUBLIC_API_KEY: input.NEXT_PUBLIC_API_KEY, + GOOGLE_ANALYTICS: input.GOOGLE_ANALYTICS, + API_PORT: input.API_PORT + }) }) it('should return only secrets when there are no variables', () => { @@ -52,12 +52,12 @@ describe('Dectect Secrets and Variables from Object', () => { AWS_KEY: aws.testcases[0].input, OPENAI_KEY: openAI.testcases[0].input } - const result = secretDetector.detectObject(input) - expect(result.secrets).toEqual([ - ['GITHUB_KEY', input.GITHUB_KEY], - ['AWS_KEY', input.AWS_KEY], - ['OPENAI_KEY', input.OPENAI_KEY] - ]) - expect(result.variables).toEqual([]) + const result = secretDetector.detectJsObject(input) + expect(result.secrets).toEqual({ + GITHUB_KEY: input.GITHUB_KEY, + AWS_KEY: input.AWS_KEY, + OPENAI_KEY: input.OPENAI_KEY + }) + expect(result.variables).toEqual({}) }) }) diff --git a/packages/secret-scan/src/types/index.d.ts b/packages/secret-scan/src/types/index.d.ts index c5e73cd3..e52efe32 100644 --- a/packages/secret-scan/src/types/index.d.ts +++ b/packages/secret-scan/src/types/index.d.ts @@ -8,7 +8,7 @@ export interface SecretResult { regex?: RegExp } -export interface ScanObjectResult { - secrets: string[][] // string[] -> [key, value] - variables: string[][] // string[] -> [key, value] +export interface ScanJsObjectResult { + secrets: Record + variables: Record }