initial commit

This commit is contained in:
Hitesh Borase
2026-01-07 10:20:44 +05:30
parent 78eb0d41b4
commit 19d87f527a
7 changed files with 782 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+23
View File
@@ -0,0 +1,23 @@
# 1. Use a slim Node.js image to keep the plugin fast and small
# Note: As of January, 2026, when building this image, it uses Node.js version 25.2.1
# The 'node:slim' tag will automatically use the latest Node.js version every time the image is built
FROM node:slim
# 2. Set the working directory inside the container
WORKDIR /app
# 3. Copy package files first (to leverage Docker caching for faster builds)
COPY package*.json ./
# 4. Install production dependencies only
RUN npm install --omit=dev
# 5. Copy the rest of your source code
COPY . .
# 6. Ensure the entrypoint script is executable (important for Linux runners)
RUN chmod +x /app/entrypoint.sh
# 7. Set the Entrypoint
# This tells Docker to run the shell script first, which then calls your Node logic
ENTRYPOINT ["/app/entrypoint.sh"]
+480 -1
View File
@@ -1 +1,480 @@
# harness-integration
# Harness Keeper Security Plugin
A Harness CI/CD plugin that integrates with Keeper Security Secrets Manager (KSM) to securely fetch and inject secrets into your CI/CD pipelines at runtime.
## Overview
This plugin securely retrieves secrets from Keeper Security Secrets Manager and makes them available to subsequent steps in your Harness pipeline. The plugin implements a zero-knowledge architecture where secrets are retrieved directly from Keeper Vault at runtime and never pass through Harness systems in decrypted form.
**Key Features:**
- **Zero-Knowledge Architecture**: Secrets are retrieved directly from Keeper Vault at runtime
- **Multiple Authentication Methods**: Supports one-time access tokens (`US:...`) and full JSON config
- **Keeper Notation Support**: Query fields (`field`), custom fields (`custom_field`), and file attachments (`file`)
- **Multiple Output Formats**: Environment variables, file output, and output variables
- **Secure Storage**: Secrets written to `/harness/secrets/` with secure file permissions
- **Automatic Validation**: Validates token format and required config fields
- **Base64 Decoding**: Automatically decodes base64-encoded tokens
## Architecture
### Zero-Knowledge Design
The plugin ensures zero-knowledge security through the following design:
1. **Harness CI/CD** calls the custom Docker-based plugin
2. **Plugin authenticates** using KSM configuration stored in Harness Secrets Manager
3. **Secrets are retrieved** directly from Keeper Vault at runtime
4. **Decrypted secrets** are returned as step outputs for downstream stages
5. **No external storage**: Secrets never pass through Harness systems in decrypted form
All secret flow happens strictly between the plugin container and Keeper, while Harness provides orchestration, governance, and secure secret injection across the pipeline.
### Components
1. **`src/index.js`**: Main plugin logic
- Token processing and validation
- Keeper Secrets Manager integration
- Secret fetching using Keeper Notation
- Secret distribution
2. **`entrypoint.sh`**: Container entrypoint script
- Executes the Node.js plugin
- Captures and processes plugin output
- Writes secrets to Harness directories
- Manages security and cleanup
3. **`Example/Pipeline.yaml`**: Example Harness CI pipeline
- Demonstrates plugin configuration
- Shows secret consumption patterns
### Execution Flow
```
1. Container starts → entrypoint.sh executes
2. entrypoint.sh → Runs: node /app/src/index.js
3. Plugin processes:
- Reads KSM_CONFIG from environment
- Validates and decodes token (base64 if needed)
- Initializes Keeper storage
- Fetches secrets from Keeper using Keeper Notation
- Outputs secrets to stdout (with ENV:/OUT: prefixes)
4. entrypoint.sh captures stdout:
- Parses ENV:/OUT: prefixes
- Writes to /harness/outputs/outputs.txt (output variables)
- Writes to /harness/secrets/ (file-based access)
- Sets secure permissions (chmod 600)
5. Subsequent steps access secrets via:
- Files: /harness/secrets/VARIABLE_NAME (recommended)
```
## Prerequisites
- **Keeper Secrets Manager access** ([Quick Start Guide](https://docs.keeper.io/secrets-manager/secrets-manager/quick-start-guide))
- **Secrets Manager addon enabled** for your Keeper account
- **Membership in a Role** with the Secrets Manager enforcement policy enabled
- **A Keeper Secrets Manager Application** with secrets shared to it
- **An initialized Keeper Secrets Manager Configuration** (JSON or one-time token)
- **Record UID(s)** for the secrets you want to access ⚠️ **REQUIRED**
- **Harness CI/CD account** with a project set up
- **Docker** (for building the plugin image)
- **Node.js 18+** (for local development)
## Installation
### Build and Push Docker Image
Build the plugin Docker image:
```bash
docker build -t your-dockerhub-username/harness-keeper-plugin:latest .
```
Push to your container registry:
```bash
docker push your-dockerhub-username/harness-keeper-plugin:latest
```
**Note:** Replace `your-dockerhub-username` with your actual Docker Hub username or container registry path.
## Configuration
### 1. Create Keeper Access Token
#### Option A: One-Time Access Token
1. Log in to Keeper Security
2. Navigate to Secrets Manager
3. Generate a one-time access token (format: `US:xxxxx`)
4. Base64 encode (optional but recommended):
```bash
echo -n "US:your-token-here" | base64
```
#### Option B: Full JSON Config
Create a JSON config with:
```json
{
"hostname": "keepersecurity.com",
"clientId": "your-client-id",
"privateKey": "your-private-key"
}
```
Base64 encode:
```bash
echo -n '{"hostname":"...","clientId":"...","privateKey":"..."}' | base64
```
### 2. Create Harness Secret
In Harness, create a secret:
- **Type**: TEXT or FILE (both work)
- **Name**: `KSM_CONFIG` (or your preferred name)
- **Value**: Your base64-encoded token or JSON config
### 3. Configure Pipeline
**Before you begin:** You must have the Record UID(s) for the secrets you want to access. See "How to Get Record UID" section below.
```yaml
pipeline:
name: harness_keeper_plugin
identifier: harness_keeper_plugin
projectIdentifier: default_project
orgIdentifier: default
stages:
- stage:
name: HkpCI
identifier: HkpCI
type: CI
spec:
cloneCodebase: false
platform:
os: Linux
arch: Amd64
runtime:
type: Cloud
spec: {}
execution:
steps:
- step:
type: Plugin
name: Fetch_Keeper_Secrets
identifier: Fetch_Keeper_Secrets
spec:
image: your-dockerhub-username/harness-keeper-plugin:latest
settings:
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > env:API_KEY
JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key
envVariables:
KSM_CONFIG: <+secrets.getValue("KSM_CONFIG")>
- step:
type: Run
name: Use_Keeper_Secrets
identifier: Use_Keeper_Secrets
spec:
image: alpine:3.20
shell: Sh
command: |
DB_USERNAME=$(cat /harness/secrets/DB_USERNAME)
DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)
API_KEY=$(cat /harness/secrets/API_KEY)
echo "Secrets retrieved successfully"
# Use secrets in your build/deploy process
```
**Note:** `envVariables` is used ONLY to pass the Keeper configuration token to the plugin. The actual secrets retrieved from Keeper are stored in Harness pipeline volume storage at `/harness/secrets/`.
## Usage
### Secret Mapping Format
```
<keeper-notation> > <destination>
```
### Keeper Notation Format
The plugin supports three types of Keeper Notation queries:
1. **Standard Fields**: `<record-uid>/field/<field-type>`
- Examples: `password`, `login`, `url`, `text`, `host`, etc.
2. **Custom Fields**: `<record-uid>/custom_field/<field-label>`
- Examples: `API_Key`, `My Custom Field`, `Environment`, etc.
3. **File Attachments**: `<record-uid>/file/<file-name>`
- Examples: `api.key`, `certificate.pem`, `config.json`, etc.
**Important:** The first part (e.g., `JQPso5SMBpP29gKKGp6Enw`) is the **Record UID**, the unique identifier for a specific secret record in Keeper.
### Destination Types
| Destination Prefix | Description | Output Location |
|-------------------|-------------|----------------|
| `env:VARIABLE_NAME` | Environment variable | `/harness/secrets/VARIABLE_NAME` |
| `file:/path/to/file` | File output | Specified file path |
| `VARIABLE_NAME` (default) | Output variable | `/harness/secrets/VARIABLE_NAME` |
### Examples
#### 1. Standard Field → Environment Variable
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME
```
- Creates files: `/harness/secrets/DB_PASSWORD`, `/harness/secrets/DB_USERNAME`
- Access: `DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)`
#### 2. Custom Field → Output Variable
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY
JQPso5SMBpP29gKKGp6Enw/custom_field/Environment > ENV_NAME
```
- Creates files: `/harness/secrets/API_KEY`, `/harness/secrets/ENV_NAME`
- Access: `API_KEY=$(cat /harness/secrets/API_KEY)`
#### 3. File Attachment → File Path
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key
JQPso5SMBpP29gKKGp6Enw/file/certificate.pem > file:/app/ssl/cert.pem
```
- Downloads files and writes to specified paths
- Access: `cat /app/config/api.key`
#### 4. Mixed Usage
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY
JQPso5SMBpP29gKKGp6Enw/file/config.json > file:/app/config.json
```
### How to Get Record UID
The Record UID is **mandatory** and identifies which specific secret record to retrieve. Obtain it using one of these methods:
**Method 1: From Keeper Web Vault**
1. Log in to your Keeper Vault
2. Open the secret record you want to access
3. Click on the record details/info icon
4. The Record UID is displayed in the record information (usually at the bottom)
5. Copy the Record UID (format: `JQPso5SMBpP29gKKGp6Enw`)
**Method 2: Using Keeper Commander CLI**
```bash
keeper list
# This will show all records with their UIDs
```
**Method 3: Using Keeper Secrets Manager SDK/API**
- Use the Keeper Secrets Manager SDK to list records and retrieve their UIDs
- The Record UID is returned when querying records
**Method 4: From Keeper Admin Console**
1. Navigate to Secrets Manager in Admin Console
2. View the Application and its shared records
3. Record UIDs are visible in the record list
**Note:** The one-time access token (`US:...`) or JSON config is used to **authenticate** and access Keeper, but you still need the **Record UID** to specify which secret record to retrieve.
## Accessing Secrets in Subsequent Steps
### Method 1: File-Based Access (Recommended)
The most reliable method for accessing secrets in subsequent steps:
```yaml
- step:
type: Run
name: Use_Secrets
spec:
image: alpine:3.20
shell: Sh
command: |
DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)
DB_USERNAME=$(cat /harness/secrets/DB_USERNAME)
API_KEY=$(cat /harness/secrets/API_KEY)
# Use secrets in your process
echo "Secrets loaded successfully"
# Clean up after use (optional)
rm -f /harness/secrets/DB_PASSWORD /harness/secrets/DB_USERNAME
```
**Benefits:**
- Most reliable method
- No truncation issues
- Works with all secret types
- Supports long values and special characters
### Method 2: Direct File Access (for file: prefix)
When using `file:` prefix, files are written directly to the specified path:
```yaml
- step:
type: Run
name: Use_File
spec:
command: |
# File already at /app/config/api.key
cat /app/config/api.key
```
## Secret Storage Mechanism
**Important:** This plugin uses **Harness pipeline volume storage**, not Harness environment variables.
- **Storage Location:** `/harness/secrets/` directory (Harness shared workspace volume)
- **Storage Type:** File-based storage on pipeline-scoped volume
- **Access Method:** Read files directly using `cat` or file operations
- **Scope:** Pipeline execution only - automatically cleaned up after completion
- **Permissions:** Files have `600` permissions (owner read/write only)
The `envVariables` section in the plugin step is **only** used to pass the `KSM_CONFIG` (Keeper configuration token) to the plugin. The actual secrets retrieved from Keeper are written to files in `/harness/secrets/` directory.
## Security
### Zero-Knowledge Architecture
- **Direct Retrieval**: Secrets are retrieved directly from Keeper Vault at runtime
- **No Decrypted Storage**: Secrets never pass through Harness systems in decrypted form
- **Pipeline-Scoped**: All secrets are scoped to the current pipeline execution
- **Automatic Cleanup**: Harness CI automatically cleans up `/harness/outputs/` and `/harness/secrets/` after pipeline completion
- **File Permissions**: Secrets written to `/harness/secrets/` have `600` permissions (owner read/write only)
- **No Persistence**: Secrets are not accessible outside the pipeline execution context
### Best Practices
1. **Use one-time access tokens** instead of permanent credentials when possible
2. **Base64 encode tokens** before storing in Harness secrets
3. **Clean up secret files** after use in pipeline steps
4. **Use appropriate secret scope** (project/org/account)
5. **Verify Record UIDs** before configuring pipelines
## Troubleshooting
### Token Not Found
**Error:** `KSM config is required`
**Solutions:**
- Verify the secret exists in Harness
- Check the secret scope matches your project
- Ensure the expression `<+secrets.getValue("KSM_CONFIG")>` is correct
- Verify secret name matches exactly
### Invalid Token Format
**Error:** `Invalid token format - expected US:xxxxx or JSON config`
**Solutions:**
- Verify the token starts with `US:` (for one-time tokens)
- Check if the token is base64 encoded (it will be auto-decoded)
- For JSON config, ensure it starts with `{` and has required fields: `hostname`, `clientId`, `privateKey`
### Token Already Used / Expired
**Error:** `Failed: token ... invalid/expired`
**Solutions:**
1. Generate a new token in Keeper Security
2. Base64 encode: `echo -n "US:new-token" | base64`
3. Update the Harness secret with the new token
4. Run the pipeline again
**Note:** One-time access tokens are single-use - generate a new token for each use or use JSON config for reusable credentials.
### Secret File Not Found
**Error:** `ERROR: Secret files not found` or `Value not found for notation: <record-uid>/field/<field-type>`
**Solutions:**
- **Verify Record UID is correct**: The Record UID must match exactly (case-sensitive)
- **Check Record UID exists**: Ensure the record exists in Keeper and is shared to your Secrets Manager Application
- **Verify access permissions**: The one-time token or JSON config must have access to the record
- **Check field name**: Ensure the field type (`password`, `login`, `text`, etc.) or custom field label exists in the record
- **Verify Application sharing**: The record must be shared to the Secrets Manager Application you're using
- **Check logs**: `ls -la /harness/secrets/` to see what files were created
- **Verify notation format**: Ensure the notation follows the correct format (e.g., `/field/password`, `/custom_field/MyLabel`, `/file/filename`)
### Expression Not Resolved
**Error:** Token preview shows `${...}` or `<+...>` unresolved
**Solutions:**
- Use `envVariables` (not `settings`) for Harness expressions when passing `KSM_CONFIG` to the plugin
- Verify secret name matches exactly
- Check secret scope matches the reference
- Ensure you're using the correct Harness expression syntax: `<+secrets.getValue("SECRET_NAME")>`
### Harness Expression Not Resolved
**Error:** `Harness expression not resolved`
**Solutions:**
- Ensure `KSM_CONFIG` is passed via `envVariables`, not `settings`
- Verify the secret exists and is accessible in the current scope
- Check that the Harness expression syntax is correct: `<+secrets.getValue("SECRET_NAME")>`
## Local Development
### Test Locally
```bash
# Set environment variables
export PLUGIN_SECRETS="JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD"
export KSM_CONFIG="VVM6..." # Your base64 encoded token or JSON config
# Run the plugin
node ./src/index.js
```
### Project Structure
```
harness_keeper_plugin/
├── src/
│ └── index.js # Main plugin logic
├── entrypoint.sh # Container entrypoint script
├── Dockerfile # Docker image definition
├── package.json # Node.js dependencies
├── Example/
│ └── Pipeline.yaml # Example Harness CI pipeline
└── README.md # This file
```
## Example Pipeline
See `Example/Pipeline.yaml` for a complete working example that demonstrates:
- Plugin configuration
- Secret mapping with multiple record UIDs
- File-based secret consumption
- Error handling and failure strategies
## Support
For issues or questions:
- Check the [Keeper Security Documentation](https://docs.keeper.io/secrets-manager/)
- Review [Keeper Notation Documentation](https://docs.keeper.io/secrets-manager/secrets-manager/keeper-notation/)
- Review Harness CI documentation for plugin configuration
- Verify token format and Keeper record permissions
## License
ISC
## Version History
- **1.0.0** - Initial release
- Zero-knowledge architecture implementation
- Support for one-time tokens and JSON config
- Keeper Notation support (field, custom_field, file)
- File-based secret access
- Base64 token decoding
- Secure file permissions and cleanup
Executable
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# 1. Create a temporary file to store the secrets
# Using mktemp ensures the file name is unique and not guessable
SECRETS_FILE=$(mktemp)
# 2. Run the Node.js plugin
# Redirect STDOUT (secrets) to our file, and let STDERR (logs) flow to the console
node /app/src/index.js > "$SECRETS_FILE"
# 3. Securely process the secrets
# Parse lines in format: ENV:VAR_NAME='value', OUT:VAR_NAME='value', or VAR_NAME='value'
#
# SECURITY NOTE: All secrets are written to /harness/outputs/ and /harness/secrets/
# These directories are mounted volumes scoped ONLY to the current pipeline execution.
# Harness CI automatically cleans up these volumes after pipeline completion.
# Secrets are NOT accessible outside the pipeline execution context.
mkdir -p /harness/outputs /harness/secrets
while IFS= read -r line; do
# Skip empty lines
if [ -z "$line" ]; then
continue
fi
# Determine type: ENV:, OUT:, or default
if [[ "$line" =~ ^ENV: ]]; then
type="env"
line="${line#ENV:}"
elif [[ "$line" =~ ^OUT: ]]; then
type="out"
line="${line#OUT:}"
else
type="out" # Default to output variable
fi
# Parse the line: split on first '=' to get name and value
name="${line%%=*}"
value="${line#*=}"
# Remove surrounding single quotes from value if present
if [[ "$value" =~ ^\'.*\'$ ]]; then
value="${value#\'}"
value="${value%\'}"
fi
# Skip if name is empty
if [ -z "$name" ]; then
continue
fi
# Export for the current shell session (plugin container only - not passed to next steps)
export "$name=$value"
# Write to Harness CI Plugin Output (for output variables)
# These are scoped to the pipeline execution and accessible via:
# <+step.output.outputVariables.VAR_NAME> in subsequent steps
# Format: KEY=VALUE (one per line)
printf "%s=%s\n" "$name" "$value" >> /harness/outputs/outputs.txt
# For environment variables, also write to env_vars.txt for Harness to pick up
# These are available as output variables and can be referenced in envVariables section
if [ "$type" = "env" ]; then
printf "%s=%s\n" "$name" "$value" >> /harness/outputs/env_vars.txt
fi
# Write to file for direct access (bypasses Harness truncation)
# SECURITY: Files in /harness/secrets/ are scoped to pipeline execution only
# Harness CI automatically cleans up these files after pipeline completion
echo -n "$value" > "/harness/secrets/${name}"
chmod 600 "/harness/secrets/${name}" # Restrict permissions to owner only
# Debug: Log (removed to reduce log noise)
# value_length=${#value}
# if [ "$type" = "env" ]; then
# echo "INFO: Set environment variable: $name (length: $value_length)"
# else
# echo "INFO: Set output variable: $name (length: $value_length)"
# fi
# echo "INFO: Secret also written to /harness/secrets/${name} for direct file access"
done < "$SECRETS_FILE"
# 4. Secure Clean up
# Remove the temporary file to ensure no sensitive data remains on disk
# Note: /harness/outputs/ and /harness/secrets/ are cleaned up by Harness CI
# after pipeline execution completes - they are scoped to the pipeline only
rm -f "$SECRETS_FILE"
# 5. Hand over control to the Docker command (if any)
# This allows the container to be used as a wrapper for other commands
exec "$@"
+22
View File
@@ -0,0 +1,22 @@
{
"name": "harness_keeper_plugin",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "harness_keeper_plugin",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@keeper-security/secrets-manager-core": "^17.3.0"
}
},
"node_modules/@keeper-security/secrets-manager-core": {
"version": "17.3.0",
"resolved": "https://registry.npmjs.org/@keeper-security/secrets-manager-core/-/secrets-manager-core-17.3.0.tgz",
"integrity": "sha512-9Wm5s4qbJJlOFmB1L1p2+TqWLdMg/IQMcKEXKw2ZTOQlHjGzECrX2HoeCEdCG8datt/n7uo7f84uby4GvJyOXA==",
"license": "MIT"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "harness_keeper_plugin",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@keeper-security/secrets-manager-core": "^17.3.0"
}
}
+147
View File
@@ -0,0 +1,147 @@
const {
getSecrets,
getValue,
localConfigStorage,
downloadFile,
initializeStorage
} = require('@keeper-security/secrets-manager-core');
const fs = require('fs');
const path = require('path');
const KSM_CONFIG_PATH = '/app/ksm-config.json';
const REQUIRED_FIELDS = ['hostname', 'clientId', 'privateKey'];
const core = {
getMultilineInput: (name) => {
const val = process.env[`PLUGIN_${name.toUpperCase()}`] || '';
return val.split('\n').filter(line => line.trim() !== '');
},
info: (msg) => process.stderr.write(`INFO: ${msg}\n`),
error: (msg) => process.stderr.write(`::error::${msg}\n`),
warning: (msg) => process.stderr.write(`::warning::${msg}\n`)
};
const splitInput = (text) => {
const n = text.lastIndexOf('>');
return n < 0 ? [text, ''] : [text.substring(0, n).trim(), text.substring(n + 1).trim()];
};
const processToken = (rawToken) => {
if (!rawToken) {
core.error('KSM config is required');
core.error('Set KSM_CONFIG environment variable');
process.exit(1);
}
let token = rawToken.trim();
if (token.includes('${') || token.includes('<+') || token.includes('ngSecretManager')) {
core.error('Harness expression not resolved');
process.exit(1);
}
// Decode base64 if needed
if (!token.startsWith('US:') && !token.startsWith('{')) {
try {
token = Buffer.from(token, 'base64').toString('utf-8').trim();
} catch (e) {
// Not base64, use as-is
console.log(e)
}
}
token = token.trim();
if (!token) {
core.error('Token is null or empty');
process.exit(1);
}
const isConfigJson = token.startsWith('{');
if (isConfigJson) {
try {
const config = JSON.parse(token);
const missing = REQUIRED_FIELDS.filter(f => !config[f]);
if (missing.length) {
core.error(`Missing required fields: ${missing.join(', ')}`);
process.exit(1);
}
return { token, isConfigJson: true, config };
} catch (e) {
core.error(`Invalid JSON config: ${e.message}`);
process.exit(1);
}
}
if (!token.startsWith('US:')) {
core.error('Invalid token format - expected US:xxxxx or JSON config');
process.exit(1);
}
return { token, isConfigJson: false, config: null };
};
const parseSecretMappings = () => {
return core.getMultilineInput('secrets').map(line => {
const [notation, destRaw] = splitInput(line);
if (destRaw.startsWith('env:')) {
return { notation, destination: destRaw.slice(4), destinationType: 'environment' };
}
if (destRaw.startsWith('file:')) {
return { notation, destination: destRaw.slice(5), destinationType: 'file' };
}
return { notation, destination: destRaw, destinationType: 'output' };
});
};
const setupStorage = async (token, isConfigJson, config) => {
if (isConfigJson) {
fs.writeFileSync(KSM_CONFIG_PATH, JSON.stringify(config), 'utf8');
return localConfigStorage(KSM_CONFIG_PATH);
}
const storage = localConfigStorage(KSM_CONFIG_PATH);
await initializeStorage(storage, token);
return storage;
};
const runPlugin = async () => {
try {
fs.mkdirSync('/app', { recursive: true });
const { token, isConfigJson, config } = processToken(process.env.KSM_CONFIG);
const inputs = parseSecretMappings();
const storage = await setupStorage(token, isConfigJson, config);
const secrets = await getSecrets({ storage });
for (const input of inputs) {
const secret = getValue(secrets, input.notation);
if (!secret) {
core.warning(`Value not found for notation: ${input.notation}`);
continue;
}
if (input.destinationType === 'file') {
const fullPath = path.resolve(input.destination);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
const data = (typeof secret === 'object' && secret.fileId)
? await downloadFile(secret)
: secret;
fs.writeFileSync(fullPath, data);
} else if (input.destinationType === 'environment') {
// Output with ENV: prefix for environment variables
console.log(`ENV:${input.destination}='${secret}'`);
} else {
// Output variable (for Harness output variables)
console.log(`OUT:${input.destination}='${secret}'`);
}
}
} catch (error) {
core.error(`Failed: ${error.message}`);
if (error.message.includes('token') || error.message.includes('invalid') || error.message.includes('expired')) {
core.error('One-time access tokens are single-use - generate a new token');
}
process.exit(1);
}
};
runPlugin();