From 12e3ce8690d8606a4438b7e274ea15de57ca8283 Mon Sep 17 00:00:00 2001 From: Nieky Allen Date: Mon, 26 Aug 2024 22:55:44 -0500 Subject: [PATCH] feat: progress on CLI vault MVP --- cli/actions/secret/add.ts | 4 +- cli/actions/vault/export.ts | 20 ++++--- cli/actions/vault/import.ts | 26 +++++++++ cli/actions/vault/index.ts | 6 +++ cli/actions/vault/sync.ts | 36 +++++++++++++ cli/actions/vault/use.ts | 5 +- cli/core.ts | 102 ++++++++++++++++++++++++++++++++++++ cli/index.ts | 90 +------------------------------ cli/lib/env.ts | 54 +++++++++++++++++++ cli/scripts/esbuild.js | 1 + cli/scripts/prebuild.sh | 4 ++ 11 files changed, 250 insertions(+), 98 deletions(-) create mode 100644 cli/actions/vault/import.ts create mode 100644 cli/actions/vault/index.ts create mode 100644 cli/actions/vault/sync.ts create mode 100644 cli/core.ts create mode 100644 cli/lib/env.ts create mode 100644 cli/scripts/prebuild.sh diff --git a/cli/actions/secret/add.ts b/cli/actions/secret/add.ts index 4ab84c4..54f628f 100644 --- a/cli/actions/secret/add.ts +++ b/cli/actions/secret/add.ts @@ -9,9 +9,9 @@ export async function secretAdd(name: string, value: string) { const { vaults, active_vault } = config; - const { createSecret } = createSecretsHelpers(vaults[active_vault]); + const { addSecrets } = createSecretsHelpers(vaults[active_vault]); - await createSecret(name, value); + await addSecrets([{ name, value }]); logInfo('secret added successfully!'); } diff --git a/cli/actions/vault/export.ts b/cli/actions/vault/export.ts index 5a1a61f..91886d2 100644 --- a/cli/actions/vault/export.ts +++ b/cli/actions/vault/export.ts @@ -1,8 +1,15 @@ import { createSecretsHelpers } from 'db/secrets'; import { loadConfig } from 'lib/config'; +import { syncEnv } from 'lib/env'; import { logInfo } from 'lib/log'; +import { resolve } from 'path'; +import { cwd } from 'process'; -export async function vaultExport(vaultNameInput: string) { +// TODO format support (.env, JSON files) +export async function vaultExport( + vaultNameInput: string, + envDestinationPath: string, +) { const { config } = await loadConfig(); const { vaults, active_vault } = config; @@ -16,13 +23,14 @@ export async function vaultExport(vaultNameInput: string) { const secrets = await getAllSecrets(); const secretsMap = secrets.reduce( - (prev, { name, value }) => ({ - ...prev, - [name]: value, - }), - {} as Record, + (prev, { name, value }) => (prev += `${name}="${value}"\n`), + ``, ); logInfo(`Secrets retrieved for '${vaultNameInput}' vault!`); console.log(secretsMap); + + const fullEnvPath = resolve(cwd(), envDestinationPath); + + console.log(fullEnvPath); } diff --git a/cli/actions/vault/import.ts b/cli/actions/vault/import.ts new file mode 100644 index 0000000..312f032 --- /dev/null +++ b/cli/actions/vault/import.ts @@ -0,0 +1,26 @@ +import { vaultExists } from 'db/vaults'; +import { loadConfig } from 'lib/config'; +import { addEnvToVault } from 'lib/env'; +import { logError, logInfo } from 'lib/log'; +import { exit } from 'process'; + +export async function vaultImport(envPath: string) { + const { config } = await loadConfig(); + + const { vaults, active_vault } = config; + + const { key } = vaults[active_vault]; + + const location = vaultExists(vaults, active_vault); + + if (!location) { + logError('Default vault could not be found!'); + return exit(1); + } + + const newSecrets = await addEnvToVault(envPath, { key, location }); + + logInfo( + `${newSecrets.rowsAffected} secrets added to vault from '${envPath}'!`, + ); +} diff --git a/cli/actions/vault/index.ts b/cli/actions/vault/index.ts new file mode 100644 index 0000000..203c8e9 --- /dev/null +++ b/cli/actions/vault/index.ts @@ -0,0 +1,6 @@ +export * from './create'; +export * from './delete'; +export * from './export'; +export * from './import'; +export * from './sync'; +export * from './use'; diff --git a/cli/actions/vault/sync.ts b/cli/actions/vault/sync.ts new file mode 100644 index 0000000..7b5a444 --- /dev/null +++ b/cli/actions/vault/sync.ts @@ -0,0 +1,36 @@ +import { createSecretsHelpers } from 'db/secrets'; +import { loadConfig } from 'lib/config'; +import { syncEnv } from 'lib/env'; +import { logInfo } from 'lib/log'; +import { resolve } from 'path'; +import { cwd } from 'process'; + +export async function vaultSync( + vaultNameInput: string, + envDestinationPath: string, +) { + const { config } = await loadConfig(); + + const { vaults, active_vault } = config; + + const { location, key } = vaults[active_vault]; + + const { getAllSecrets } = createSecretsHelpers({ + location, + key, + }); + + const secrets = await getAllSecrets(); + const secretsMap = secrets.reduce( + (prev, { name, value }) => ({ + ...prev, + [name]: value, + }), + {}, + ); + + logInfo(`Secrets synced to ./.env for '${active_vault}' vault!`); + console.log(secretsMap); + + await syncEnv('./.env', secretsMap); +} diff --git a/cli/actions/vault/use.ts b/cli/actions/vault/use.ts index a524ad8..969e961 100644 --- a/cli/actions/vault/use.ts +++ b/cli/actions/vault/use.ts @@ -1,8 +1,8 @@ import { vaultExists } from 'db/vaults'; -import { existsSync } from 'fs'; import { loadConfig, saveConfig } from 'lib/config'; import { logError, logInfo } from 'lib/log'; import { cwd, exit } from 'process'; +import { DeadropConfig } from 'types/config'; export async function vaultUse(vaultNameInput: string) { const { config } = await loadConfig(); @@ -24,7 +24,8 @@ export async function vaultUse(vaultNameInput: string) { return exit(0); } - const updatedConfig = { + const updatedConfig: DeadropConfig = { + ...config, active_vault: vaultNameInput, vaults, }; diff --git a/cli/core.ts b/cli/core.ts new file mode 100644 index 0000000..377a20b --- /dev/null +++ b/cli/core.ts @@ -0,0 +1,102 @@ +import { Command } from 'commander'; +import { description, version } from '../package.json'; +import init from 'actions/init'; +import { drop } from 'actions/drop'; +import { grab } from 'actions/grab'; +import { secretAdd } from 'actions/secret/add'; +import { secretRemove } from 'actions/secret/remove'; +import { + vaultCreate, + vaultDelete, + vaultExport, + vaultImport, + vaultSync, + vaultUse, +} from 'actions/vault'; + +const deadrop = new Command(); + +deadrop.name('deadrop').description(description).version(version); + +deadrop.command('init').action(init); + +deadrop + .command('drop') + .description('drop a secret from a vault or in raw format') + .argument('[input]', 'secret to drop') + .option('-i, --input [input]', 'secret to drop') + .option('-f, --file', 'secret to drop is a file') + .action(drop); + +deadrop + .command('grab') + .description('grab a secret with a drop ID') + .argument('', 'drop session ID') + .action(grab); + +// vault commands + +const vaultRoot = deadrop + .command('vault') + .description('manage your vaults'); + +vaultRoot + .command('create') + .description( + 'create a new vault, optionally specify its parent folder', + ) + .argument('', 'name of the vault') + .argument('[location]', 'folder location of the vault') + .action(vaultCreate); + +vaultRoot + .command('use') + .description('change the current active vault deadrop is using') + .argument('', 'name of the vault to switch to as active') + .action(vaultUse); + +vaultRoot + .command('sync') + .description('sync the current active vault with .env file') + .action(vaultSync); + +vaultRoot + .command('export') + .description('export all the secrets of the specified vault') + .argument('', 'name of the vault to export') + .action(vaultExport); + +vaultRoot + .command('import') + .description( + 'import all the secrets of a given .env file to active vault', + ) + .argument('', 'path to the .env file') + .action(vaultImport); + +vaultRoot + .command('delete') + .description( + `delete the specified vault's database and remove it from config`, + ) + .argument('', 'name of the vault to delete') + .action(vaultDelete); + +// secrets commands + +const secretRoot = deadrop + .command('secret') + .description('manage your secrets in active vault'); + +secretRoot + .command('add') + .argument('[name]', 'name of the secret') + .argument('[value]', 'value of the secret') + .action(secretAdd); + +secretRoot + .command('remove') + .argument('[name]', 'name of the secret to remove') + .action(secretRemove); + +export { deadrop }; diff --git a/cli/index.ts b/cli/index.ts index 2f37a47..e27b89e 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,97 +1,11 @@ #!/usr/bin/env node -import { drop } from 'actions/drop'; -import { grab } from 'actions/grab'; -import init from 'actions/init'; -import { vaultCreate } from 'actions/vault/create'; -import { vaultDelete } from 'actions/vault/delete'; -import { vaultExport } from 'actions/vault/export'; -import { vaultUse } from 'actions/vault/use'; -import { Command } from 'commander'; import 'dotenv/config'; +import { deadrop } from 'core'; import { checkNodeVersion } from 'lib/util'; -import { description, version } from './package.json'; -import { secretAdd } from 'actions/secret/add'; checkNodeVersion(); -const program = new Command(); - -program.name('deadrop').description(description).version(version); - -program.command('init').action(init); - -program - .command('drop') - .description('drop a secret from a vault or in raw format') - .argument('[input]', 'secret to drop') - .option('-i, --input [input]', 'secret to drop') - .option('-f, --file', 'secret to drop is a file') - .action(drop); - -program - .command('grab') - .description('grab a secret with a drop ID') - .argument('', 'drop session ID') - .action(grab); - -// vault commands - -const vaultRoot = program - .command('vault') - .description('manage your vaults'); - -vaultRoot - .command('create') - .description( - 'create a new vault, optionally specify its parent folder', - ) - .argument('', 'name of the vault') - .argument('[location]', 'folder location of the vault') - .action(vaultCreate); - -vaultRoot - .command('use') - .description('change the current active vault deadrop is using') - .argument('', 'name of the vault to switch to as active') - .action(vaultUse); - -vaultRoot - .command('export') - .description('export all the secrets of the specified vault') - .argument('', 'name of the vault to export') - .action(vaultExport); - -vaultRoot - .command('delete') - .description( - `delete the specified vault's database and remove it from config`, - ) - .argument('', 'name of the vault to delete') - .action(vaultDelete); - -// secrets commands - -const secretRoot = program - .command('secret') - .description('manage your secrets (in current vault'); - -secretRoot - .command('add') - .argument('[name]', 'name of the secret') - .argument('[value]', 'value of the secret') - .action(secretAdd); - -secretRoot - .command('drop') - .argument('[name]', 'name of the secret to drop') - .action(vaultCreate); - -secretRoot - .command('remove') - .argument('[name]', 'name of the secret to remove') - .action(vaultCreate); - -program.parse(); +deadrop.parse(); const exitSignals: NodeJS.Signals[] = [ 'SIGINT', diff --git a/cli/lib/env.ts b/cli/lib/env.ts new file mode 100644 index 0000000..85181df --- /dev/null +++ b/cli/lib/env.ts @@ -0,0 +1,54 @@ +import { initDB } from 'db/init'; +import { secretsTable } from 'db/schema'; +import { stringify, parse } from 'envfile'; +import { appendFile, readFile, writeFile } from 'fs/promises'; +import { resolve } from 'path'; +import { cwd } from 'process'; +import { VaultDBConfig } from 'types/config'; + +type Env = Record; + +const encoding: BufferEncoding = 'utf-8'; + +export async function syncEnv( + filePath: string, + envVars: Env, + append = false, +) { + const fullPath = resolve(cwd(), filePath); + + const envAsString = stringify(envVars); + + const envContent = `# generated by deadrop\n\n${envAsString}\n`; + + if (append) await appendFile(fullPath, `\n${envContent}`, encoding); + else await writeFile(fullPath, envContent, encoding); +} + +export async function loadEnvFromFile(filePath: string) { + const fullPath = resolve(cwd(), filePath); + console.log(fullPath); + const envContent = await readFile(fullPath, encoding); + + const parsedEnv = parse(envContent); + + return parsedEnv; +} + +export async function addEnvToVault( + envPath: string, + vault: VaultDBConfig, +) { + const envVars = await loadEnvFromFile(envPath); + + const db = initDB(vault.location, vault.key); + + const secretsToAdd = Object.entries(envVars).map( + ([key, value]) => ({ + name: key, + value, + }), + ); + console.log(secretsToAdd); + return db.insert(secretsTable).values(secretsToAdd).run(); +} diff --git a/cli/scripts/esbuild.js b/cli/scripts/esbuild.js index 3ab64e8..921e3e0 100644 --- a/cli/scripts/esbuild.js +++ b/cli/scripts/esbuild.js @@ -16,6 +16,7 @@ if (!process.env.DEADDROP_API_URL || !process.env.PEER_SERVER_URL) { bundle: true, inject: ['./scripts/inject.js'], loader: { '.node': 'file' }, + external: ['libsql'], plugins: [ environmentPlugin({ DEADDROP_API_URL: process.env.DEADDROP_API_URL, diff --git a/cli/scripts/prebuild.sh b/cli/scripts/prebuild.sh new file mode 100644 index 0000000..d95767f --- /dev/null +++ b/cli/scripts/prebuild.sh @@ -0,0 +1,4 @@ + +rimraf dist/ + +cpx "../node_modules/figlet/fonts/*.flf" ./fonts