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
14 changes: 7 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SyncSecret from "./src/SyncSecrets.js";
import Decrypt from "./src/Decrypt.js";
import logger from "./src/Logger.js";
import path from "path";
import SyncSecret from './src/SyncSecrets.js';
import Decrypt from './src/Decrypt.js';
import logger from './src/Logger.js';
import path from 'path';

export default class SyncSecretPlugin {
constructor(serverless, options) {
Expand All @@ -10,7 +10,7 @@ export default class SyncSecretPlugin {
this.servicePath = this.serverless.config.servicePath || process.cwd();
this.secrets = null;
this.provider = this.serverless.getProvider('aws');
this.stage = this.provider.getStage();
this.stage = this.provider.getStage();

logger.setServerless(serverless);

Expand Down Expand Up @@ -48,7 +48,7 @@ export default class SyncSecretPlugin {
this.config.ssm_prefix
);
try {
this.secrets = await decrypt.run();
this.secrets = await decrypt.run();
} catch (e) {
logger.logError(`Error decrypting secrets: ${e.message}`);
throw e;
Expand Down Expand Up @@ -111,7 +111,7 @@ export default class SyncSecretPlugin {
config = { ...config, ...service.custom.syncSecrets };

const boolKeys = ['create_secret', 'delete_secret', 'show_values', 'dry'];
boolKeys.forEach(key => {
boolKeys.forEach((key) => {
if (key in service.custom.syncSecrets) {
logger.logInfo(`${key}: ${service.custom.syncSecrets[key]}`);
config[key] = Boolean(service.custom.syncSecrets[key]);
Expand Down
34 changes: 12 additions & 22 deletions src/ChangeSet.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import lodash from "lodash";
import chalk from "chalk";
import SecretsManager from "./SecretsManager.js";
import lodash from 'lodash';
import chalk from 'chalk';

const logPrefix = chalk.cyan("SecretKey");
const skipTag = chalk.yellow("[SKIP]");
const addedTag = chalk.green("[ADDED]");
const changedTag = chalk.magenta("[CHANGED]");
const removedTag = chalk.red("[REMOVED]");
const valPlaceholder = "**********";
const logPrefix = chalk.cyan('SecretKey');
const skipTag = chalk.yellow('[SKIP]');
const addedTag = chalk.green('[ADDED]');
const changedTag = chalk.magenta('[CHANGED]');
const removedTag = chalk.red('[REMOVED]');
const valPlaceholder = '**********';

/**
* ChangeSet is a class that represents a set of changes to be applied to secrets in AWS Secrets Manager.
Expand Down Expand Up @@ -60,18 +59,11 @@ export default class ChangeSet {
* @param {boolean} showValues A flag to un-hide secret values on log messages
* @param {boolean} deleteSecret A flag to delete the secret
*/
constructor(
smClient,
newValues,
existingValues,
skipPattern,
showValues = false,
deleteSecret = false,
) {
constructor(smClient, newValues, existingValues, skipPattern, showValues = false, deleteSecret = false) {
this.#changeDesc = [];
this.#updatedValues = { ...existingValues };
this.#smClient = smClient;
this.#skipPattern = skipPattern || "";
this.#skipPattern = skipPattern || '';
this.#showValues = showValues;
this.#deleteSecret = deleteSecret;

Expand Down Expand Up @@ -109,7 +101,7 @@ export default class ChangeSet {
*/
#eval(newValues, existingValues) {
if (this.#deleteSecret) {
this.#removedDesc("ALL_KEYS");
this.#removedDesc('ALL_KEYS');
return;
}

Expand Down Expand Up @@ -207,8 +199,6 @@ export default class ChangeSet {
newVal = valPlaceholder;
}

this.#changeDesc.push(
`${logPrefix}: ${changedTag} '${key}': '${oldVal}' => '${newVal}'`,
);
this.#changeDesc.push(`${logPrefix}: ${changedTag} '${key}': '${oldVal}' => '${newVal}'`);
}
}
84 changes: 39 additions & 45 deletions src/Decrypt.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fs from "fs";
import util from "util";
import cp from "child_process";
import os from "os";
import path from "path";
import crypto from "crypto";
import lodash from "lodash";
import logger from "./Logger.js";
import fs from 'fs';
import util from 'util';
import cp from 'child_process';
import os from 'os';
import path from 'path';
import crypto from 'crypto';
import lodash from 'lodash';
import logger from './Logger.js';

export default class Decrypt {
#filePath;
Expand All @@ -18,13 +18,8 @@ export default class Decrypt {
* @param {string} filePath The path to the JSON file.
* @param {string} privateKey Optional private key for encryption.
* @param {string} ssm_prefix The SSM parameter name prefix for the private key.
*/
constructor(
serverless,
filePath,
privateKey,
ssm_prefix
) {
*/
constructor(serverless, filePath, privateKey, ssm_prefix) {
this.exec = util.promisify(cp.exec);
this.provider = serverless.getProvider('aws');
this.#filePath = filePath;
Expand All @@ -41,13 +36,13 @@ export default class Decrypt {
async #setEjsonPrivateKey() {
if (lodash.isNull(this.#ejsonPrivateKey) || lodash.isEmpty(this.#ejsonPrivateKey)) {
if (lodash.isNull(this.#ssm_prefix) || lodash.isEmpty(this.#ssm_prefix)) {
throw new Error("No provided private key for decryption and no SSM prefix provided");
throw new Error('No provided private key for decryption and no SSM prefix provided');
}
this.#ejsonPrivateKey = await this.#getEjsonPrivateKey();
}
return this;
}

/**
* Validate the existence of the JSON file at the specified path and the private key.
*
Expand All @@ -65,27 +60,27 @@ export default class Decrypt {
* @throws {Error} If any step of the process fails
* @returns {Promise<Object>} The decrypted JSON object
*/
async run() {
await this.#checkEjsonInstalled();
await this.#setEjsonPrivateKey();
return await this.#decrypt();
}
async run() {
await this.#checkEjsonInstalled();
await this.#setEjsonPrivateKey();
return await this.#decrypt();
}

/**
/**
* Checks if ejson is installed in the system.
*
*
* @throws {Error} If ejson is not installed
* @returns {Promise<boolean>} True if ejson is installed
*/
async #checkEjsonInstalled() {
logger.logInfo('Checking if ejson is installed...');
try {
await this.exec('which ejson');
return true;
} catch (error) {
throw new Error('ejson command not found. Please install it first.');
}
async #checkEjsonInstalled() {
logger.logInfo('Checking if ejson is installed...');
try {
await this.exec('which ejson');
return true;
} catch {
throw new Error('ejson command not found. Please install it first.');
}
}

/**
* Decrypt the JSON file using the ejson command and set the decrypted output.
Expand All @@ -99,7 +94,7 @@ export default class Decrypt {
let tmpKeyDir = null;
let privateKeyPath = null;

try{
try {
const ejsonContent = JSON.parse(fs.readFileSync(this.#filePath, 'utf8'));
const publicKey = ejsonContent?._public_key;
const tmpdir = os.tmpdir();
Expand All @@ -109,8 +104,8 @@ export default class Decrypt {
}

tmpKeyDir = path.join(tmpdir, `${crypto.randomBytes(16).toString('hex')}`);
fs.mkdirSync(tmpKeyDir, { mode: 0o700 });
fs.mkdirSync(tmpKeyDir, { mode: 0o700 });

privateKeyPath = path.join(tmpKeyDir, publicKey);

fs.writeFileSync(privateKeyPath, this.#ejsonPrivateKey, { mode: 0o600 });
Expand All @@ -130,7 +125,7 @@ export default class Decrypt {
logger.logInfo('Secrets decrypted successfully!');
return secrets;
} catch (error) {
throw new Error(`Error decrypting secrets: ${error.message}`);
throw new Error(`Error decrypting secrets: ${error.message}`);
} finally {
if (!lodash.isNull(tmpKeyDir) && fs.existsSync(tmpKeyDir) && fs.existsSync(privateKeyPath)) {
fs.unlinkSync(privateKeyPath);
Expand All @@ -139,29 +134,28 @@ export default class Decrypt {
}
}

/**
/**
* Get the private key from AWS SSM Parameter Store
*
*
* @throws {Error} If the key cannot be retrieved
* @returns {Promise<string>} The private key
*/
async #getEjsonPrivateKey() {
try {
const result = await this.provider.request('SSM', 'getParameter', {
Name: this.#ssm_prefix,
WithDecryption: true
});
Name: this.#ssm_prefix,
WithDecryption: true,
});

const privateKey = result.Parameter.Value;

if (lodash.isEmpty(privateKey)) {
throw new Error("No provided private key for decryption");
throw new Error('No provided private key for decryption');
}

return privateKey;
return privateKey;
} catch (error) {
throw new Error(`Error getting private key from SSM: ${error.message}`);
throw new Error(`Error getting private key from SSM: ${error.message}`);
}
}

}
8 changes: 4 additions & 4 deletions src/Logger.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import chalk from "chalk";
import chalk from 'chalk';

class Logger {
constructor(prefix = "SyncSecret") {
constructor(prefix = 'SyncSecret') {
this.prefix = prefix;
this.serverless = null;
}

setServerless(serverless) {
this.serverless = serverless;
}

/**
* Print a plugin info log message
*
Expand All @@ -19,7 +19,7 @@ class Logger {
this.serverless.cli.consoleLog(`${chalk.cyan(this.prefix)}: ${message}`);
}

/**
/**
* Print a plugin error log message
*
* @param {string} message Log message
Expand Down
13 changes: 6 additions & 7 deletions src/SecretsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
* secrets manager instance.
*/
export default class SecretsManager {

/**
/**
* Creates a new SecretsManager instance.
*
*
* @param {Serverless} serverless The Serverless instance.
* @param {string} secretName The name of the secret in AWS Secrets Manager.
*
*
*/
constructor(serverless, secretName) {
this.secretName = secretName;
Expand All @@ -21,7 +20,7 @@ export default class SecretsManager {
*
* @returns {Object}
*/
async getValues(){
async getValues() {
const data = await this.provider.request('SecretsManager', 'getSecretValue', {
SecretId: this.secretName,
});
Expand All @@ -35,7 +34,7 @@ export default class SecretsManager {
*/
async update(newValues) {
await this.provider.request('SecretsManager', 'updateSecret', {
SecretId: this.secretName,
SecretId: this.secretName,
SecretString: JSON.stringify(newValues),
});
}
Expand All @@ -49,7 +48,7 @@ export default class SecretsManager {
const result = await this.provider.request('SecretsManager', 'listSecrets', {
Filters: [
{
Key: "name",
Key: 'name',
Values: [this.secretName],
},
],
Expand Down
24 changes: 8 additions & 16 deletions src/SyncSecrets.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SecretsManager from "./SecretsManager.js";
import ChangeSet from "./ChangeSet.js";
import logger from "./Logger.js";
import SecretsManager from './SecretsManager.js';
import ChangeSet from './ChangeSet.js';
import logger from './Logger.js';

/**
* SyncSecret is a class representing an action to synchronize secrets with AWS Secrets Manager.
Expand Down Expand Up @@ -44,7 +44,7 @@ export default class SyncSecret {

/**
* Creates a new SyncSecret instance.
*
*
* @param {Serverless} serverless The Serverless instance.
* @param {string} secretName The name of the secret in AWS Secrets Manager.
* @param {Object} secrets The object containing the secret values.
Expand All @@ -57,15 +57,7 @@ export default class SyncSecret {
*
* @throws {Error} Throws an error if any required parameter is missing or if the JSON file doesn't exist.
*/
constructor(
serverless,
secretName,
secrets,
skipPattern,
showValues,
createSecret,
deleteSecret,
) {
constructor(serverless, secretName, secrets, skipPattern, showValues, createSecret, deleteSecret) {
this.#validateData(secretName);
this.#secrets = secrets;
this.#skipPattern = skipPattern;
Expand Down Expand Up @@ -96,7 +88,7 @@ export default class SyncSecret {
existingSecretData,
this.#skipPattern,
this.#showValues,
this.#deleteSecretFlag,
this.#deleteSecretFlag
);
}

Expand All @@ -105,7 +97,7 @@ export default class SyncSecret {
*/
async #createSecret() {
if (this.#deleteSecretFlag || !this.#createSecretFlag) {
logger.logInfo("Secret creation skip...");
logger.logInfo('Secret creation skip...');
return;
}

Expand All @@ -123,7 +115,7 @@ export default class SyncSecret {
*/
#validateData(secretName) {
if (!secretName) {
throw new Error("Missing secret_name");
throw new Error('Missing secret_name');
}
}
}
Loading