Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Add import sub commmand for project. #594

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
5 changes: 3 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,26 @@
"dependencies": {
"@clack/core": "^0.3.4",
"@clack/prompts": "^0.7.0",
"@keyshade/secret-scan": "workspace:*",
"chalk": "^4.1.2",
"cli-table": "^0.3.11",
"colors": "^1.4.0",
"commander": "^12.1.0",
"dotenv": "^16.4.7",
"eccrypto": "^1.1.6",
"figlet": "^1.7.0",
"fs": "0.0.1-security",
"glob": "^11.0.0",
"nodemon": "^3.1.4",
"@keyshade/secret-scan": "workspace:*",
"socket.io-client": "^4.7.5",
"typescript": "^5.5.2"
},
"devDependencies": {
"@swc/cli": "^0.4.0",
"@swc/core": "^1.6.13",
"@types/cli-table": "^0.3.4",
"@types/figlet": "^1.5.8",
"@types/eccrypto": "^1.1.6",
"@types/figlet": "^1.5.8",
"@types/node": "^20.14.10",
"eslint-config-standard-with-typescript": "^43.0.1",
"tsup": "^8.1.2"
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/commands/project.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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 ListProjectForks from './project/list-forks.project'
import ListProject from './project/list.project'
import SyncProject from './project/sync.project'
Expand All @@ -28,7 +29,8 @@ export default class ProjectCommand extends BaseCommand {
new ListProject(),
new SyncProject(),
new UnlinkProject(),
new UpdateProject()
new UpdateProject(),
new ImportEnvs()
muntaxir4 marked this conversation as resolved.
Show resolved Hide resolved
]
}
}
148 changes: 148 additions & 0 deletions apps/cli/src/commands/project/import.project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type {
CommandActionData,
CommandArgument
} from '@/types/command/command.types'
import BaseCommand from '../base.command'
import { confirm, text } from '@clack/prompts'
import ControllerInstance from '@/util/controller-instance'
import { Logger } from '@/util/logger'
import fs from 'node:fs/promises'
import path from 'node:path'
import dotenv from 'dotenv'
import secretDetector from '@keyshade/secret-scan'

export default class ImportEnvs extends BaseCommand {
getName(): string {
return 'import'
}

getDescription(): string {
return 'Imports environment secrets and variables from .env file to a project.'
}

getArguments(): CommandArgument[] {
return [
{
name: '<Project Slug>',
description: 'Slug of the project where envs will be imported.'
},
{
name: '<.env file path>',
muntaxir4 marked this conversation as resolved.
Show resolved Hide resolved
description: 'Path to the .env file'
}
]
}

canMakeHttpRequests(): boolean {
return true
}

async action({ args }: CommandActionData): Promise<void> {
const [projectSlug, dotEnvPath] = args

try {
const envFileContent = await fs.readFile(
path.resolve(dotEnvPath),
'utf-8'
)
muntaxir4 marked this conversation as resolved.
Show resolved Hide resolved

const envVariables = dotenv.parse(envFileContent)

const secretsAndVariables = secretDetector.detectObject(envVariables)
muntaxir4 marked this conversation as resolved.
Show resolved Hide resolved

Logger.info(
'Detected secrets:\n' +
secretsAndVariables.secrets
.map((secret) => secret[0] + ' = ' + secret[1])
.join('\n')
)
Logger.info(
'Detected variables:\n' +
secretsAndVariables.variables
.map((variable) => variable[0] + ' = ' + variable[1])
.join('\n')
)

const confirmImport = await confirm({
message:
'Do you want to proceed with importing the environment variables? (y/N)',
initialValue: false
})

if (!confirmImport) {
Logger.info('Import cancelled by the user.')
return
}

const environmentSlug = (await text({
message: 'Enter the environment slug to import to:'
})) as string

Logger.info(
`Importing secrets and variables to project: ${projectSlug} and environment: ${environmentSlug} with default settings`
)

let noOfSecrets = 0
let noOfVariables = 0
const errors: string[] = []
for (const [key, value] of secretsAndVariables.secrets) {
const { data, error, success } =

Check warning on line 89 in apps/cli/src/commands/project/import.project.ts

View workflow job for this annotation

GitHub Actions / Validate CLI

'data' is assigned a value but never used
await ControllerInstance.getInstance().secretController.createSecret(
{
projectSlug,
name: key,
entries: [
{
value,
environmentSlug
}
]
},
this.headers
)

if (success) {
++noOfSecrets
} else {
errors.push(
`Failed to create secret for ${key}. Error: ${error.message}.`
)
}
}

for (const [key, value] of secretsAndVariables.variables) {
const { data, error, success } =

Check warning on line 114 in apps/cli/src/commands/project/import.project.ts

View workflow job for this annotation

GitHub Actions / Validate CLI

'data' is assigned a value but never used
await ControllerInstance.getInstance().variableController.createVariable(
{
projectSlug,
name: key,
entries: [
{
value,
environmentSlug
}
]
},
this.headers
)

if (success) {
++noOfVariables
} else {
errors.push(
`Failed to create variable for ${key}. Error: ${error.message}.`
)
}
}
Logger.info(
`Imported ${noOfSecrets} secrets and ${noOfVariables} variables.`
)
if (errors.length) Logger.error(errors.join('\n'))
} catch (error) {
const errorMessage = (error as Error)?.message
Logger.error(
`Failed to import secrets and variables.${errorMessage ? '\n' + errorMessage : ''}`
)
}
}
}
19 changes: 18 additions & 1 deletion packages/secret-scan/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import denylist from '@/denylist'
import type { SecretResult } from '@/types'
import type { SecretResult, ScanObjectResult } from '@/types'

export type SecretConfig = Record<string, RegExp[]>

Expand All @@ -22,6 +22,23 @@ class SecretDetector {
}
return { found: false }
}

detectObject(input: Record<string, string>): ScanObjectResult {
const result = {
secrets: [],
variables: []
}
for (const [key, value] of Object.entries(input)) {
const secretResult = this.detect(value)
if (secretResult.found) {
result.secrets.push([key, value])
} else {
result.variables.push([key, value])
}
}

return result
}
}

const createSecretDetector = (config: SecretConfig): SecretDetector => {
Expand Down
63 changes: 63 additions & 0 deletions packages/secret-scan/src/test/detect-js-object.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import secretDetector from '@/index'
import { aws, github, openAI } from '@/rules'

describe('Dectect Secrets and Variables from Object', () => {
it('should be able to differentiate variables from secrets', () => {
const input = {
GITHUB_KEY: github.testcases[0].input,
AWS_KEY: aws.testcases[0].input,
OPENAI_KEY: openAI.testcases[0].input,
NEXT_PUBLIC_API_KEY: 'this-is-some-key',
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]
])
})

it('should return empty arrays for secrets and variables when input is empty', () => {
const input = {}
const result = secretDetector.detectObject(input)
expect(result.secrets).toEqual([])
expect(result.variables).toEqual([])
})

it('should return only variables when there are no secrets', () => {
const input = {
NEXT_PUBLIC_API_KEY: 'this-is-some-key',
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]
])
})

it('should return only secrets when there are no variables', () => {
const input = {
GITHUB_KEY: github.testcases[0].input,
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([])
})
})
5 changes: 5 additions & 0 deletions packages/secret-scan/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export interface SecretResult {
found: boolean
regex?: RegExp
}

export interface ScanObjectResult {
secrets: string[][] // string[] -> [key, value]
muntaxir4 marked this conversation as resolved.
Show resolved Hide resolved
variables: string[][] // string[] -> [key, value]
}
Loading
Loading