Skip to content

Commit

Permalink
Merge pull request #10 from step-security/release
Browse files Browse the repository at this point in the history
feat: add support for prefixing all secrets on copy
  • Loading branch information
shubham-stepsecurity authored Sep 2, 2024
2 parents e03733b + d86e2b0 commit ebaabc6
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 30 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "Build"
on:
on:
[push, pull_request]
permissions:
contents: read
Expand All @@ -12,8 +12,14 @@ jobs:
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit

- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- run: |
npm i
npm run all
- name: Upload coverage reports to Codecov
if: always()
uses: codecov/codecov-action@v3
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,7 @@ Thumbs.db

# Ignore built ts files
__tests__/runner/*
lib/**/*
lib/**/*

# Ignore IntelliJ files
.idea
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ If this value is set to the name of a valid environment in the target repositori

Target where secrets should be stored: `actions` (default), `codespaces` or `dependabot`.

### `new_secret_prefix`

If this value is set, the action will prefix the name of the secret with the provided value. This is useful when you want to use the same secret name in multiple repositories but want to avoid conflicts.

## Usage

```yaml
Expand Down
4 changes: 4 additions & 0 deletions __tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("getConfig", () => {
const RUN_DELETE = false;
const ENVIRONMENT = "production";
const TARGET = "actions";
const NEW_SECRET_PREFIX = "PREFIX_";

// Must implement because operands for delete must be optional in typescript >= 4.0
interface Inputs {
Expand All @@ -51,6 +52,7 @@ describe("getConfig", () => {
INPUT_RUN_DELETE: string;
INPUT_ENVIRONMENT: string;
INPUT_TARGET: string;
INPUT_NEW_SECRET_PREFIX: string;
}
const inputs: Inputs = {
INPUT_GITHUB_API_URL: String(GITHUB_API_URL),
Expand All @@ -64,6 +66,7 @@ describe("getConfig", () => {
INPUT_RUN_DELETE: String(RUN_DELETE),
INPUT_ENVIRONMENT: String(ENVIRONMENT),
INPUT_TARGET: String(TARGET),
INPUT_NEW_SECRET_PREFIX: String(NEW_SECRET_PREFIX),
};

beforeEach(() => {
Expand Down Expand Up @@ -93,6 +96,7 @@ describe("getConfig", () => {
RUN_DELETE,
ENVIRONMENT,
TARGET,
NEW_SECRET_PREFIX,
});
});

Expand Down
23 changes: 21 additions & 2 deletions __tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import * as config from "../src/config";

import {
DefaultOctokit,
deleteSecretForRepo,
filterReposByPatterns,
listAllMatchingRepos,
getRepos,
listAllMatchingRepos,
publicKeyCache,
setSecretForRepo,
deleteSecretForRepo,
} from "../src/github";

// @ts-ignore-next-line
Expand All @@ -41,6 +41,7 @@ beforeAll(() => {
REPOSITORIES_LIST_REGEX: true,
DRY_RUN: false,
RETRIES: 3,
NEW_SECRET_PREFIX: "",
});

octokit = DefaultOctokit({
Expand Down Expand Up @@ -189,6 +190,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -202,6 +204,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"dependabot"
);
Expand All @@ -215,6 +218,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"codespaces"
);
Expand All @@ -228,6 +232,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -242,6 +247,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"actions"
);
Expand All @@ -255,6 +261,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"dependabot"
);
Expand All @@ -268,6 +275,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"codespaces"
);
Expand Down Expand Up @@ -320,6 +328,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -333,6 +342,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -347,6 +357,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"dependabot"
);
Expand All @@ -361,6 +372,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
false,
"actions"
);
Expand Down Expand Up @@ -401,6 +413,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -414,6 +427,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"actions"
);
Expand All @@ -427,6 +441,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"dependabot"
);
Expand All @@ -440,6 +455,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"codespaces"
);
Expand Down Expand Up @@ -473,6 +489,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -486,6 +503,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"dependabot"
);
Expand All @@ -499,6 +517,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
false,
"actions"
);
Expand Down
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# action.yml
name: "Secrets Sync Action"
branding:
icon: 'copy'
icon: 'copy'
color: 'red'
description: "Copies secrets from the action's environment to many other repos."
inputs:
Expand Down Expand Up @@ -66,6 +66,11 @@ inputs:
Target where secrets should be stored: `actions` (default), `codespaces` or `dependabot`.
default: "actions"
required: false
new_secret_prefix:
default: ""
description: |
If this value is set, the action will prefix the secret name with this value.
required: false
runs:
using: 'node20'
main: 'dist/index.js'
30 changes: 17 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function getConfig() {
RUN_DELETE: ["1", "true"].includes(core.getInput("DELETE", { required: false }).toLowerCase()),
ENVIRONMENT: core.getInput("ENVIRONMENT", { required: false }),
TARGET: core.getInput("TARGET", { required: false }),
NEW_SECRET_PREFIX: core.getInput("NEW_SECRET_PREFIX", { required: false }),
};
if (config.DRY_RUN) {
core.info("[DRY_RUN='true'] No changes will be written to secrets");
Expand Down Expand Up @@ -265,27 +266,28 @@ function getPublicKey(octokit, repo, environment, target) {
});
}
exports.getPublicKey = getPublicKey;
function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, target) {
function setSecretForRepo(octokit, name, secret, repo, environment, new_secret_prefix, dry_run, target) {
return __awaiter(this, void 0, void 0, function* () {
const [repo_owner, repo_name] = repo.full_name.split("/");
const publicKey = yield getPublicKey(octokit, repo, environment, target);
const encrypted_value = (0, utils_1.encrypt)(secret, publicKey.key);
core.info(`Set \`${name} = ***\` on ${repo.full_name}`);
const final_name = new_secret_prefix ? new_secret_prefix + name : name;
core.info(`Set \`${final_name} = ***\` on ${repo.full_name}`);
if (!dry_run) {
switch (target) {
case "codespaces":
return octokit.codespaces.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
case "dependabot":
return octokit.dependabot.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -295,7 +297,7 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
return octokit.actions.createOrUpdateEnvironmentSecret({
repository_id: repo.id,
environment_name: environment,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -304,7 +306,7 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
return octokit.actions.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -314,24 +316,25 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
});
}
exports.setSecretForRepo = setSecretForRepo;
function deleteSecretForRepo(octokit, name, secret, repo, environment, dry_run, target) {
function deleteSecretForRepo(octokit, name, secret, repo, environment, new_secret_prefix, dry_run, target) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Remove ${name} from ${repo.full_name}`);
const final_name = new_secret_prefix ? new_secret_prefix + name : name;
core.info(`Remove ${final_name} from ${repo.full_name}`);
try {
if (!dry_run) {
const action = "DELETE";
switch (target) {
case "codespaces":
return octokit.request(`${action} /repos/${repo.full_name}/codespaces/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/codespaces/secrets/${final_name}`);
case "dependabot":
return octokit.request(`${action} /repos/${repo.full_name}/dependabot/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/dependabot/secrets/${final_name}`);
case "actions":
default:
if (environment) {
return octokit.request(`${action} /repositories/${repo.id}/environments/${environment}/secrets/${name}`);
return octokit.request(`${action} /repositories/${repo.id}/environments/${environment}/secrets/${final_name}`);
}
else {
return octokit.request(`${action} /repos/${repo.full_name}/actions/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/actions/secrets/${final_name}`);
}
}
}
Expand Down Expand Up @@ -513,6 +516,7 @@ function run() {
FOUND_SECRETS: Object.keys(secrets),
ENVIRONMENT: config.ENVIRONMENT,
TARGET: config.TARGET,
NEW_SECRET_PREFIX: config.NEW_SECRET_PREFIX,
}, null, 2));
const limit = (0, p_limit_1.default)(config.CONCURRENCY);
const calls = [];
Expand All @@ -521,7 +525,7 @@ function run() {
const action = config.RUN_DELETE
? github_1.deleteSecretForRepo
: github_1.setSecretForRepo;
calls.push(limit(() => action(octokit, k, secrets[k], repo, config.ENVIRONMENT, config.DRY_RUN, config.TARGET)));
calls.push(limit(() => action(octokit, k, secrets[k], repo, config.ENVIRONMENT, config.NEW_SECRET_PREFIX, config.DRY_RUN, config.TARGET)));
}
}
yield Promise.all(calls);
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Config {
RUN_DELETE: boolean;
ENVIRONMENT: string;
TARGET: string;
NEW_SECRET_PREFIX: string;
}

export function getConfig(): Config {
Expand All @@ -54,6 +55,7 @@ export function getConfig(): Config {
),
ENVIRONMENT: core.getInput("ENVIRONMENT", { required: false }),
TARGET: core.getInput("TARGET", { required: false }),
NEW_SECRET_PREFIX: core.getInput("NEW_SECRET_PREFIX", { required: false }),
};

if (config.DRY_RUN) {
Expand Down
Loading

0 comments on commit ebaabc6

Please sign in to comment.