From 134c1ee80157c8a53177bb1cafd1f599131d8dac Mon Sep 17 00:00:00 2001 From: Johan Rooijakkers Date: Wed, 26 Feb 2025 16:55:25 +0100 Subject: [PATCH] update --- .gitignore | 9 ++ action.yml | 56 +------- dist/index.js | 4 + dist/main.js | 101 +++++++++++++ dist/parser.js | 47 ++++++ dist/validators.js | 30 ++++ package-lock.json | 347 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 22 +++ src/index.ts | 3 + src/main.ts | 103 ++++++++++++++ src/parser.ts | 48 +++++++ src/validators.ts | 31 ++++ tsconfig.json | 10 ++ 13 files changed, 757 insertions(+), 54 deletions(-) create mode 100644 .gitignore create mode 100644 dist/index.js create mode 100644 dist/main.js create mode 100644 dist/parser.js create mode 100644 dist/validators.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/main.ts create mode 100644 src/parser.ts create mode 100644 src/validators.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..815b401 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/node_modules + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* \ No newline at end of file diff --git a/action.yml b/action.yml index 07b4be9..0b84a23 100644 --- a/action.yml +++ b/action.yml @@ -24,57 +24,5 @@ inputs: required: true runs: - using: "composite" - steps: - - name: Configure Bitwarden Server - shell: sh - run: bw config server ${{ inputs.server }} - - - name: Unlock Vault - shell: sh - run: | - # Ensure Bitwarden is logged in - if ! bw login --check; then - bw login --apikey - fi - - # Unlock the vault and store the session key - BW_SESSION=$(bw unlock "${{ inputs.password }}" --raw) - - # Verify if BW_SESSION is set correctly - if [ -n "$BW_SESSION" ]; then - echo "BW_SESSION=$BW_SESSION" >> "$GITHUB_ENV" - export BW_SESSION - echo "✅ Vault unlocked successfully!" - else - echo "❌ Failed to unlock Bitwarden vault" - exit 1 - fi - env: - BW_CLIENTID: ${{ inputs.client-id }} - BW_CLIENTSECRET: ${{ inputs.client-secret }} - - - name: Retrieve Requested Secrets - shell: sh - run: | - INPUT_SECRETS=$(echo "${{ inputs.secrets }}" | tr "\n" ",") - - OLDIFS="$IFS" - IFS="," - set -- "$INPUT_SECRETS" - IFS="$OLDIFS" - - for pair in "$@"; do - SECRET_ID=$(echo "$pair" | cut -d">" -f1 | xargs) - ENV_VAR=$(echo "$pair" | cut -d">" -f2 | xargs) - - echo "Retrieving secret: $SECRET_ID" - SECRET_VALUE=$(bw get notes "$SECRET_ID" --session "$BW_SESSION") - - if [ -n "$SECRET_VALUE" ]; then - echo "$ENV_VAR=$SECRET_VALUE" >> "$GITHUB_ENV" - echo "✅ Stored $SECRET_ID in $ENV_VAR" - else - echo "❌ Failed to retrieve secret: $SECRET_ID" - fi - done \ No newline at end of file + using: "node20" + main: "dist/index.js" \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..e0b9f2e --- /dev/null +++ b/dist/index.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const main_1 = require("./main"); +(0, main_1.run)(); diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000..2fe408f --- /dev/null +++ b/dist/main.js @@ -0,0 +1,101 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = run; +const core = __importStar(require("@actions/core")); +const sdk_napi_1 = require("@bitwarden/sdk-napi"); +const parser_1 = require("./parser"); +async function run() { + try { + const inputs = readInputs(); + core.info("🔑 Logging into Bitwarden..."); + const client = await getBitwardenClient(inputs); + const secretInputs = (0, parser_1.parseSecretInput)(inputs.secrets); + core.info("🔍 Retrieving secrets..."); + await retrieveAndSetSecrets(client, secretInputs); + core.info("✅ Successfully retrieved and set secrets!"); + } + catch (error) { + core.setFailed(`❌ Error: ${error instanceof Error ? error.message : error}`); + } +} +function readInputs() { + const clientId = core.getInput("client-id", { required: true }); + const clientSecret = core.getInput("client-secret", { required: true }); + const password = core.getInput("password", { required: true }); + const server = core.getInput("server", { required: true }); // Je eigen Bitwarden server + const secretsRaw = core.getInput("secrets", { required: true }); + const secrets = secretsRaw + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.includes(">")); + if (secrets.length === 0) { + throw new Error("No valid secrets provided."); + } + return { clientId, clientSecret, password, server, secrets }; +} +async function retrieveAndSetSecrets(client, secretsInput) { + const secretIds = secretsInput.map((secret) => secret.id); + const secretResponse = await client.secrets().getByIds(secretIds); + if (secretResponse.success && secretResponse.data) { + const fetchedSecrets = secretResponse.data.data; + fetchedSecrets.forEach((secret) => { + const secretInput = secretsInput.find((input) => input.id === secret.id); + if (secretInput) { + core.setSecret(secret.value); + core.exportVariable(secretInput.outputEnvName, secret.value); + core.setOutput(secretInput.outputEnvName, secret.value); + } + }); + } + else { + throw new Error(`The secrets provided could not be found. Please check the machine account has access to the secret UUIDs provided.\nError: ${secretResponse.errorMessage}`); + } + core.info("✅ Completed setting secrets as environment variables."); +} +async function getBitwardenClient(inputs) { + const settings = { + identityUrl: `${inputs.server}/identity`, + apiUrl: `${inputs.server}/api`, + userAgent: "actions/warden", + deviceType: sdk_napi_1.DeviceType.SDK, + }; + const client = new sdk_napi_1.BitwardenClient(settings, 2 /* LogLevel.Info */); + const result = await client.loginWithAccessToken(inputs.clientSecret); + if (!result.success) { + throw Error(`Authentication with Bitwarden failed.\nError: ${result.errorMessage}`); + } + return client; +} diff --git a/dist/parser.js b/dist/parser.js new file mode 100644 index 0000000..02e2471 --- /dev/null +++ b/dist/parser.js @@ -0,0 +1,47 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SecretInput = void 0; +exports.parseSecretInput = parseSecretInput; +const validators_1 = require("./validators"); +class SecretInput { + constructor(id, outputEnvName) { + this.id = id; + this.outputEnvName = outputEnvName; + } +} +exports.SecretInput = SecretInput; +class ParsingError extends Error { + constructor(message) { + super(message); + } +} +function parseSecretInput(secrets) { + const results = secrets.map((secret) => { + try { + if (secret.indexOf(">") === -1) { + throw new ParsingError(`Expected format: > `); + } + let [id, envName] = secret.split(">", 2); + id = id.trim(); + envName = envName.trim(); + if (!(0, validators_1.isValidGuid)(id)) { + throw new ParsingError(`Id is not a valid GUID`); + } + if (!(0, validators_1.isValidEnvName)(envName)) { + throw new ParsingError(`Environment variable name is not valid`); + } + return new SecretInput(id, envName); + } + catch (e) { + const message = `Error occurred when attempting to parse ${secret}`; + if (e instanceof ParsingError) { + throw TypeError(`${message}. ${e.message}`); + } + throw TypeError(message); + } + }); + if (!(0, validators_1.isUniqueEnvNames)(results)) { + throw TypeError("Environmental variable names provided are not unique, names must be unique"); + } + return results; +} diff --git a/dist/validators.js b/dist/validators.js new file mode 100644 index 0000000..b090b29 --- /dev/null +++ b/dist/validators.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isValidUrl = isValidUrl; +exports.isValidEnvName = isValidEnvName; +exports.isValidGuid = isValidGuid; +exports.isUniqueEnvNames = isUniqueEnvNames; +const ENV_NAME_REGEX = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/; +const GUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; +function isValidUrl(url) { + try { + const tempUrl = new URL(url); + if (tempUrl.protocol === "https:") { + return true; + } + } + catch { + return false; + } + return false; +} +function isValidEnvName(name) { + return ENV_NAME_REGEX.test(name); +} +function isValidGuid(value) { + return GUID_REGEX.test(value); +} +function isUniqueEnvNames(secretInputs) { + const envNames = [...new Set(secretInputs.map((s) => s.outputEnvName))]; + return envNames.length === secretInputs.length; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..50503c1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,347 @@ +{ + "name": "ts-bw-secrets", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-bw-secrets", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@bitwarden/sdk-napi": "^0.3.1" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@bitwarden/sdk-napi": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi/-/sdk-napi-0.3.1.tgz", + "integrity": "sha512-G5oM/st2+1eQOposoxdgLqK52j52fkcjv6W3OiojBx/yHy854FnY5KfKZRf73DRV/U1AYnHWckHE4IQ53/u3CQ==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@bitwarden/sdk-napi-darwin-arm64": "0.3.1", + "@bitwarden/sdk-napi-darwin-x64": "0.3.1", + "@bitwarden/sdk-napi-linux-x64-gnu": "0.3.1", + "@bitwarden/sdk-napi-win32-x64-msvc": "0.3.1" + } + }, + "node_modules/@bitwarden/sdk-napi-darwin-arm64": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-darwin-arm64/-/sdk-napi-darwin-arm64-0.3.1.tgz", + "integrity": "sha512-kfQ6uEJOEO9x8sPHk+qysJGYvBfc9XIHLSTNMp8axkRpuVOkc+uq7IC2fvViUDNJXGRjdepvuC5blp75oT6BMA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@bitwarden/sdk-napi-darwin-x64": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-darwin-x64/-/sdk-napi-darwin-x64-0.3.1.tgz", + "integrity": "sha512-mV4DLakyQ4hhM3HI0jeZ55y62UnrDccj6qX7Z5ygItx/Q9w0mEn0TQLZiwcm/uqtOYWBsDr3Bg9QyyiOfCCT1g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@bitwarden/sdk-napi-linux-x64-gnu": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-linux-x64-gnu/-/sdk-napi-linux-x64-gnu-0.3.1.tgz", + "integrity": "sha512-NkS09B0P55zWy6YAyFKJ1MytJWyUgAUFCNgFCf6wx7L2W9uplKfunWaY4NhElAhn4pXX1aElNN1+T2OECUTykg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@bitwarden/sdk-napi-win32-x64-msvc": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-win32-x64-msvc/-/sdk-napi-win32-x64-msvc-0.3.1.tgz", + "integrity": "sha512-C70Y4skSbPcKxAVb3zUS++TmtHU8Wz5FCx+kdJvq1VWPAokuOG5OjlqiK06tY+zaiCIgNBAvjxfHCq1774N30w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce96478 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ts-bw-secrets", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dist": "pwsh ./pack.ps1 && ncc build src/index.ts --license licenses.txt --external @bitwarden/sdk-napi" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@bitwarden/sdk-napi": "^0.3.1" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c52e66a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import { run } from "./main"; + +run(); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a28e75b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,103 @@ +import * as core from "@actions/core"; +import { + BitwardenClient, + ClientSettings, + DeviceType, + LogLevel, +} from "@bitwarden/sdk-napi"; +import { parseSecretInput, SecretInput } from "./parser"; + +interface Inputs { + clientId: string; + clientSecret: string; + password: string; + server: string; + secrets: string[]; +} + +export async function run(): Promise { + try { + const inputs = readInputs(); + + core.info("🔑 Logging into Bitwarden..."); + const client = await getBitwardenClient(inputs); + + const secretInputs = parseSecretInput(inputs.secrets); + + core.info("🔍 Retrieving secrets..."); + await retrieveAndSetSecrets(client, secretInputs); + + core.info("✅ Successfully retrieved and set secrets!"); + } catch (error) { + core.setFailed( + `❌ Error: ${error instanceof Error ? error.message : error}` + ); + } +} + +function readInputs(): Inputs { + const clientId = core.getInput("client-id", { required: true }); + const clientSecret = core.getInput("client-secret", { required: true }); + const password = core.getInput("password", { required: true }); + const server = core.getInput("server", { required: true }); // Je eigen Bitwarden server + const secretsRaw = core.getInput("secrets", { required: true }); + + const secrets = secretsRaw + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.includes(">")); + + if (secrets.length === 0) { + throw new Error("No valid secrets provided."); + } + + return { clientId, clientSecret, password, server, secrets }; +} + +async function retrieveAndSetSecrets( + client: BitwardenClient, + secretsInput: SecretInput[] +) { + const secretIds = secretsInput.map((secret) => secret.id); + + const secretResponse = await client.secrets().getByIds(secretIds); + + if (secretResponse.success && secretResponse.data) { + const fetchedSecrets = secretResponse.data.data; + + fetchedSecrets.forEach((secret) => { + const secretInput = secretsInput.find((input) => input.id === secret.id); + + if (secretInput) { + core.setSecret(secret.value); + core.exportVariable(secretInput.outputEnvName, secret.value); + core.setOutput(secretInput.outputEnvName, secret.value); + } + }); + } else { + throw new Error( + `The secrets provided could not be found. Please check the machine account has access to the secret UUIDs provided.\nError: ${secretResponse.errorMessage}` + ); + } + + core.info("✅ Completed setting secrets as environment variables."); +} + +async function getBitwardenClient(inputs: Inputs): Promise { + const settings: ClientSettings = { + identityUrl: `${inputs.server}/identity`, + apiUrl: `${inputs.server}/api`, + userAgent: "actions/warden", + deviceType: DeviceType.SDK, + }; + + const client = new BitwardenClient(settings, LogLevel.Info); + const result = await client.loginWithAccessToken(inputs.clientSecret); + if (!result.success) { + throw Error( + `Authentication with Bitwarden failed.\nError: ${result.errorMessage}` + ); + } + + return client; +} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..d335ced --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,48 @@ +import { isUniqueEnvNames, isValidEnvName, isValidGuid } from "./validators"; + +export class SecretInput { + constructor(public id: string, public outputEnvName: string) {} +} + +class ParsingError extends Error { + constructor(message: string) { + super(message); + } +} + +export function parseSecretInput(secrets: string[]): SecretInput[] { + const results = secrets.map((secret) => { + try { + if (secret.indexOf(">") === -1) { + throw new ParsingError( + `Expected format: > ` + ); + } + let [id, envName] = secret.split(">", 2); + id = id.trim(); + envName = envName.trim(); + + if (!isValidGuid(id)) { + throw new ParsingError(`Id is not a valid GUID`); + } + + if (!isValidEnvName(envName)) { + throw new ParsingError(`Environment variable name is not valid`); + } + + return new SecretInput(id, envName); + } catch (e: unknown) { + const message = `Error occurred when attempting to parse ${secret}`; + if (e instanceof ParsingError) { + throw TypeError(`${message}. ${e.message}`); + } + throw TypeError(message); + } + }); + if (!isUniqueEnvNames(results)) { + throw TypeError( + "Environmental variable names provided are not unique, names must be unique" + ); + } + return results; +} diff --git a/src/validators.ts b/src/validators.ts new file mode 100644 index 0000000..5177ea2 --- /dev/null +++ b/src/validators.ts @@ -0,0 +1,31 @@ +import { SecretInput } from "./parser"; + +const ENV_NAME_REGEX = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/; + +const GUID_REGEX = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +export function isValidUrl(url: string): boolean { + try { + const tempUrl = new URL(url); + if (tempUrl.protocol === "https:") { + return true; + } + } catch { + return false; + } + return false; +} + +export function isValidEnvName(name: string): boolean { + return ENV_NAME_REGEX.test(name); +} + +export function isValidGuid(value: string): boolean { + return GUID_REGEX.test(value); +} + +export function isUniqueEnvNames(secretInputs: SecretInput[]): boolean { + const envNames = [...new Set(secretInputs.map((s) => s.outputEnvName))]; + return envNames.length === secretInputs.length; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4d1049f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "outDir": "dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +}